root/branches/mbutscher/work/lib/pwiki/DocPages.py @ 230

Revision 230, 85.0 kB (checked in by mbutscher, 2 years ago)

branches/stable-2.0:
* Bug fixed: Stack overflow for too deeply nested

wikis on tree-sorted print/export

* Internal: Layout changes to better support

internationalization

* Internal: Support for "sysPathAppend" in

"binInst.ini"

branches/mbutscher/work:
* Basic support for indexed search
* Bug fixed: Stack overflow for too deeply nested

wikis on tree sorted print/export

* Internal: Support for "sysPathAppend" in

"binInst.ini"

Line 
1from __future__ import with_statement
2
3# import profilehooks
4# profile = profilehooks.profile(filename="profile.prf", immediate=False)
5
6
7import os.path, re, struct, time, traceback, threading, collections
8
9from .rtlibRepl import minidom
10
11import wx
12
13from MiscEvent import MiscEventSourceMixin, KeyFunctionSinkAR
14
15import Consts
16from WikiExceptions import *
17
18from StringOps import strToBool, fileContentToUnicode, lineendToInternal, \
19        loadEntireTxtFile, writeEntireFile
20
21import Utilities
22from Utilities import DUMBTHREADSTOP, FunctionThreadStop, TimeoutRLock, \
23        callInMainThread, callInMainThreadAsync
24
25from WikiPyparsing import buildSyntaxNode
26import ParseUtilities
27
28import Serialization
29# from Serialization import SerializeStream
30
31
32
33# Dummy
34UNDEFINED = object()
35
36
37class DocPage(object, MiscEventSourceMixin):
38    """
39    Abstract common base class for WikiPage and FunctionalPage
40    """
41
42    def __init__(self, wikiDocument):
43        MiscEventSourceMixin.__init__(self)
44       
45        self.wikiDocument = wikiDocument
46        self.txtEditors = []  # List of all editors (views) showing this page
47        self.livePageAst = None   # Cached page AST of live text
48
49        # lock while building live AST
50        self.livePageAstBuildLock = TimeoutRLock(Consts.DEADBLOCKTIMEOUT)
51
52        # lock while setting, getting or saving self.editorText and some other ops
53        self.textOperationLock = TimeoutRLock(Consts.DEADBLOCKTIMEOUT)
54
55        # lock while changing or reading self.txtEditors list
56        self.txtEditorListLock = TimeoutRLock(Consts.DEADBLOCKTIMEOUT)
57        self.editorText = None  # Contains editor text,
58                # if no text editor is registered, this cache is invalid
59#         self.pageState = STATE_TEXTCACHE_MATCHES_EDITOR
60
61
62    def invalidate(self):
63        """
64        Make page invalid to prevent yet running threads from changing
65        database.
66        """
67        with self.textOperationLock:
68            with self.txtEditorListLock:
69                # self.wikiDocument = None
70                self.txtEditors = None
71                self.livePageAst = None
72                self.setEditorText(None)
73
74
75    def isInvalid(self):
76        return self.txtEditors is None
77
78    def getTextOperationLock(self):
79        return self.textOperationLock
80
81    def getWikiDocument(self):
82        return self.wikiDocument
83       
84    def setEditorText(self, text, dirty=True):
85        """
86        Just set editor text. Derived class overwrites this to set flags
87        """
88        with self.textOperationLock:
89            self.editorText = text
90            self.livePageAst = None
91
92
93    def getEditorText(self):
94        return self.editorText
95
96    def addTxtEditor(self, txted):
97        """
98        Add txted to the list of editors (views) showing this page.
99        """
100        # TODO Set text in editor if first editor is created?
101        with self.txtEditorListLock:
102            if not txted in self.txtEditors:
103                if len(self.txtEditors) == 0:
104                    with self.textOperationLock:
105                        # We are assuming that editor content came from
106                        # database
107                        self.setEditorText(txted.GetText(), dirty=False)
108
109                self.txtEditors.append(txted)
110
111
112    def removeTxtEditor(self, txted):
113        """
114        Remove txted from the list of editors (views) showing this page.
115        If the last is removed, text is saved to database.
116        """
117        with self.txtEditorListLock:
118            try:
119                idx = self.txtEditors.index(txted)
120                if len(self.txtEditors) == 1:
121                    self.setEditorText(None)
122
123                del self.txtEditors[idx]
124   
125            except ValueError:
126                # txted not in list
127                pass
128
129    def getTxtEditor(self):
130        """
131        Returns an arbitrary text editor associated with the page
132        or None if no editor is associated.
133        """
134        with self.txtEditorListLock:
135            if len(self.txtEditors) > 0:
136                return self.txtEditors[0]
137            else:
138                return None
139
140
141    def getLiveText(self):
142        """
143        Return current text of page, either from a text editor or
144        from the database
145        """
146        with self.textOperationLock:
147            if self.getEditorText() is not None:
148                return self.getEditorText()
149           
150            return self.getContent()
151
152
153
154    def getLiveTextNoTemplate(self):
155        """
156        Return None if page isn't existing instead of creating an automatic
157        live text (e.g. by template).
158        """
159        raise NotImplementedError   # abstract
160
161
162    def appendLiveText(self, text, fireEvent=True):
163        """
164        Append some text to page which is either loaded in one or more
165        editor(s) or only stored in the database (with automatic update).
166
167        fireEvent -- Send event if database was directly modified
168        """
169        with self.textOperationLock:
170            if self.isReadOnlyEffect():
171                return
172
173            self.setDirty(True)
174            txtEditor = self.getTxtEditor()
175            self.livePageAst = None
176            if txtEditor is not None:
177                # page is in text editor(s), so call AppendText on one of it
178                # TODO Call self.SetReadOnly(False) first?
179                txtEditor.AppendText(text)
180                return
181
182            # Modify database
183            text = self.getContent() + text
184            self.writeToDatabase(text, fireEvent=fireEvent)
185
186
187    def replaceLiveText(self, text, fireEvent=True):
188        with self.textOperationLock:
189            if self.isReadOnlyEffect():
190                return
191
192            self.setDirty(True)
193            txtEditor = self.getTxtEditor()
194            self.livePageAst = None
195            if txtEditor is not None:
196                # page is in text editor(s), so call replace on one of it
197                # TODO Call self.SetReadOnly(False) first?
198                txtEditor.replaceText(text)
199                return
200
201            self.writeToDatabase(text, fireEvent=fireEvent)
202
203
204    def informEditorTextChanged(self, changer):
205        """
206        Called by the txt editor control. Must be called in GUI(=main) thread
207        """
208        with self.textOperationLock:
209            txtEditor = self.getTxtEditor()
210            self.setEditorText(txtEditor.GetText())
211
212        self.fireMiscEventProps({"changed editor text": True,
213                "changed live text": True, "changer": changer})
214
215
216    def informVisited(self):
217        """
218        Called to inform the page that it was visited and should set
219        the "visited" entry in the database to current time.
220        """
221        pass
222
223    def getWikiLanguageName(self):
224        """
225        Returns the internal name of the wiki language of this page.
226        """
227        return self.wikiDocument.getWikiDefaultWikiLanguage()
228
229
230
231    def createWikiLanguageHelper(self):
232        return wx.GetApp().createWikiLanguageHelper(self.getWikiLanguageName())
233
234
235    def getContent(self):
236        """
237        Returns page content. If page doesn't exist already some content
238        is created automatically (may be empty string).
239        """
240        raise NotImplementedError #abstract
241
242    def setDirty(self, dirt):
243        raise NotImplementedError #abstract
244
245    def getDirty(self):
246        raise NotImplementedError #abstract
247
248
249    def getTitle(self):
250        """
251        Return human readable title of the page.
252        """
253        raise NotImplementedError #abstract
254
255
256    def getUnifiedPageName(self):
257        """
258        Return the name of the unified name of the page, which is
259        "wikipage/" + the wiki word for wiki pages or the functional tag
260        for functional pages.
261        """
262        raise NotImplementedError #abstract
263
264
265    def isReadOnlyEffect(self):
266        """
267        Return true if page is effectively read-only, this means
268        "for any reason", regardless if error or intention.
269        """
270        return (self.wikiDocument is None) or self.wikiDocument.isReadOnlyEffect()
271
272
273    def writeToDatabase(self, text=None, fireEvent=True):
274        """
275        Write current text to database and initiate update of meta-data.
276        """
277        with self.textOperationLock:
278            s, u = self.getDirty()
279            if s:
280                if text is None:
281                    text = self.getLiveText()
282                self._save(text, fireEvent=fireEvent)
283                self.initiateUpdate(fireEvent=fireEvent)
284            elif u:
285                self.initiateUpdate(fireEvent=fireEvent)
286            else:
287                if self.getMetaDataState(self.wikiWord) < \
288                        self.getWikiDocument().getFinalMetaDataState():
289                    self.updateDirtySince = time.time()
290                    self.initiateUpdate(fireEvent=fireEvent)
291
292
293    def _save(self, text, fireEvent=True):
294        """
295        Saves the content of current doc page.
296        """
297        raise NotImplementedError #abstract
298
299
300    def initiateUpdate(self, fireEvent=True):
301        """
302        Initiate update of page meta-data. This function may call update
303        directly if can be done fast
304        """
305        raise NotImplementedError #abstract
306
307
308#     def _update(self, fireEvent=True):
309#         """
310#         Update additional cached informations of doc page
311#         """
312#         raise NotImplementedError #abstract
313
314
315
316class AliasWikiPage(DocPage):
317    """
318    Fake page for an alias name of a wiki page. Most functions are delegated
319    to underlying real page
320    Fetched via the (WikiDocument=) WikiDataManager.getWikiPage method.
321    """
322    def __init__(self, wikiDocument, aliasWikiWord, realWikiPage):
323        self.wikiDocument = wikiDocument
324        self.aliasWikiWord = aliasWikiWord
325        self.realWikiPage = realWikiPage
326
327    def getWikiWord(self):
328        return self.aliasWikiWord
329
330    def getTitle(self):
331        """
332        Return human readable title of the page.
333        """
334        return self.aliasWikiWord
335       
336    def getUnifiedPageName(self):
337        """
338        Return the name of the unified name of the page, which is
339        "wikipage/" + the wiki word for wiki pages or the functional tag
340        for functional pages.
341        """
342        return u"wikipage/" + self.aliasWikiWord
343
344    def getNonAliasPage(self):
345        """
346        If this page belongs to an alias of a wiki word, return a page for
347        the real one, otherwise return self
348        """
349        return self.realWikiPage
350#         word = self.wikiDocument.getWikiData().getUnAliasedWikiWord(self.wikiWord)
351#         return self.wikiDocument.getWikiPageNoError(word)
352
353    def getContent(self):
354        """
355        Returns page content. If page doesn't exist already some content
356        is created automatically (may be empty string).
357        """
358        return self.realWikiPage.getContent()
359
360
361    def setDirty(self, dirt):
362        return self.realWikiPage.setDirty(dirt)
363
364    def _save(self, text, fireEvent=True):
365        """
366        Saves the content of current doc page.
367        """
368        return self.realWikiPage._save(text, fireEvent)
369
370    def initiateUpdate(self):
371        return self.realWikiPage.initiateUpdate()
372       
373#     def update(self, fireEvent=True):
374#         return self.realWikiPage.update(fireEvent)
375
376    def getLivePageAst(self, fireEvent=True, dieOnChange=False,
377            threadstop=DUMBTHREADSTOP):
378        return self.realWikiPage.getLivePageAst(fireEvent, dieOnChange,
379                threadstop)
380
381
382    # TODO A bit hackish, maybe remove
383    def __getattr__(self, attr):
384        return getattr(self.realWikiPage, attr)
385
386
387class DataCarryingPage(DocPage):
388    """
389    A page that carries data for itself (mainly everything except an alias page)
390    """
391    def __init__(self, wikiDocument):
392        DocPage.__init__(self, wikiDocument)
393       
394        # does this page need to be saved?
395       
396        # None, if not dirty or timestamp when it became dirty
397        # Inside self.textOperationLock, it is ensured that it is None iff
398        # the editorText is None or is in sync with the database.
399        # This applies not only to editorText, but also to the text returned
400        # by getLiveText().
401
402        self.saveDirtySince = None
403        self.updateDirtySince = None
404
405        # To not store the content of the page here, a placeholder
406        # object is stored instead. Each time, the live text may have changed,
407        # a new object is created. Functions running in a separate thread can
408        # keep a reference to the object at beginning and check for
409        # identity at the end of their work.
410        self.liveTextPlaceHold = object()
411
412
413#         # To not store the full DB content of the page here, a placeholder
414#         # object is stored instead. Each time, text is written to DB,
415#         # a new object is created. Functions running in a separate task can
416#         # keep a reference to the object at beginning and check for
417#         # identity at the end of their work.
418#         self.dbContentPlaceHold = object()
419       
420
421    def setDirty(self, dirt):
422        if self.isReadOnlyEffect():
423            return
424
425        if dirt:
426            if self.saveDirtySince is None:
427                ti = time.time()
428                self.saveDirtySince = ti
429                self.updateDirtySince = ti
430        else:
431            self.saveDirtySince = None
432            self.updateDirtySince = None
433
434    def getDirty(self):
435        return (self.saveDirtySince is not None,
436                self.updateDirtySince is not None)
437
438    def getDirtySince(self):
439        return (self.saveDirtySince, self.updateDirtySince)
440
441
442    def setEditorText(self, text, dirty=True):
443        with self.textOperationLock:
444            super(DataCarryingPage, self).setEditorText(text)
445            if text is None:
446                if self.saveDirtySince is not None:
447                    """
448                    Editor text was removed although it wasn't in sync with
449                    database, so the self.liveTextPlaceHold must be updated,
450                    but self.saveDirtySince is set to None because
451                    self.editorText isn't valid anymore
452                    """
453                    self.saveDirtySince = None
454                    self.liveTextPlaceHold = object()
455            else:
456                if dirty:
457                    self.setDirty(True)
458                    self.liveTextPlaceHold = object()
459
460    def checkFileSignatureAndMarkDirty(self, fireEvent=True):
461        return True
462   
463   
464    def markTextChanged(self):
465        """
466        Mark text as changed and cached pageAst as invalid.
467        Mainly called when an external file change is detected.
468        """
469        self.liveTextPlaceHold = object()
470
471
472class AbstractWikiPage(DataCarryingPage):
473    """
474    Abstract base for WikiPage and Versioning.WikiPageSnapshot
475    """
476
477    def __init__(self, wikiDocument, wikiWord):
478        DataCarryingPage.__init__(self, wikiDocument)
479
480        self.livePageBasePlaceHold = None   # liveTextPlaceHold object on which
481                # the livePageAst is based.
482                # This is needed to check for changes when saving
483        self.livePageBaseFormatDetails = None   # Cached format details on which the
484                # page-ast bases
485
486        # List of words unknown to spellchecker
487        self.liveSpellCheckerUnknownWords = None
488
489        # liveTextPlaceHold object on which the liveSpellCheckerUnknownWords is based.
490        self.liveSpellCheckerUnknownWordsBasePlaceHold = None
491
492        self.__sinkWikiDocumentSpellSession = KeyFunctionSinkAR((
493                ("modified spell checker session", self.onModifiedSpellCheckerSession),
494        ))
495
496#         self.metaDataProcessLock = threading.RLock()  # lock while processing
497#                 # meta-data
498
499        self.wikiWord = wikiWord
500        self.childRelations = None
501        self.childRelationSet = set()
502        self.todos = None
503        self.attrs = None
504        self.modified, self.created, self.visited = None, None, None
505        self.suggNewPageTitle = None  # Title to use for page if it is
506                # newly created
507
508#         if self.getWikiData().getMetaDataState(self.wikiWord) != 1:
509#             self.updateDirtySince = time.time()
510
511    def invalidate(self):
512        super(AbstractWikiPage, self).invalidate()
513        self.__sinkWikiDocumentSpellSession.setEventSource(None)
514
515    def getWikiWord(self):
516        return self.wikiWord
517
518    def getTitle(self):
519        """
520        Return human readable title of the page.
521        """
522        return self.getWikiWord()
523
524
525    def getUnifiedPageName(self):
526        """
527        Return the name of the unified name of the page, which is
528        "wikipage/" + the wiki word for wiki pages or the functional tag
529        for functional pages.
530        """
531        return u"wikipage/" + self.wikiWord
532
533    def getWikiDocument(self):
534        return self.wikiDocument
535
536    def getWikiData(self):
537        return self.wikiDocument.getWikiData()
538       
539    def getMetaDataState(self):
540        return self.getWikiData().getMetaDataState(self.wikiWord)
541
542    def addTxtEditor(self, txted):
543        """
544        Add txted to the list of editors (views) showing this page.
545        """
546        with self.txtEditorListLock:
547            if len(self.txtEditors) == 0:
548                with self.textOperationLock:
549                    if not self.checkFileSignatureAndMarkDirty():
550                        self.initiateUpdate()
551
552            super(AbstractWikiPage, self).addTxtEditor(txted)
553
554
555        # TODO Set text in editor here if first editor is created?
556
557#         with self.txtEditorListLock:
558            if not txted in self.txtEditors:
559                if len(self.txtEditors) == 0:
560                    with self.textOperationLock:
561                        # We are assuming that editor content came from
562                        # database
563                        self.setEditorText(txted.GetText(), dirty=False)
564
565                self.txtEditors.append(txted)
566
567
568    def getTimestamps(self):
569        """
570        Return tuple (<last mod. time>, <creation time>, <last visit time>)
571        of this page.
572        """
573        if self.modified is None:
574            self.modified, self.created, self.visited = \
575                    self.getWikiData().getTimestamps(self.wikiWord)
576                   
577        if self.modified is None:
578            ti = time.time()
579            self.modified, self.created, self.visited = ti, ti, ti
580       
581        return self.modified, self.created, self.visited
582
583    def setTimestamps(self, timestamps):
584        if self.isReadOnlyEffect():
585            return
586
587        timestamps = timestamps[:3]
588        self.modified, self.created, self.visited = timestamps
589       
590        self.getWikiData().setTimestamps(self.wikiWord, timestamps)
591
592
593    def getSuggNewPageTitle(self):
594        return self.suggNewPageTitle
595       
596    def setSuggNewPageTitle(self, suggNewPageTitle):
597        self.suggNewPageTitle = suggNewPageTitle
598
599    def getParentRelationships(self):
600        return self.getWikiData().getParentRelationships(self.wikiWord)
601
602
603    def getChildRelationships(self, existingonly=False, selfreference=True,
604            withFields=(), excludeSet=frozenset(),
605            includeSet=frozenset()):
606        """
607        get the child relations of this word
608        existingonly -- List only existing wiki words
609        selfreference -- List also wikiWord if it references itself
610        withFields -- Seq. of names of fields which should be included in
611            the output. If this is not empty, tuples are returned
612            (relation, ...) with ... as further fields in the order mentioned
613            in withfields.
614
615            Possible field names:
616                "firstcharpos": position of link in page (may be -1 to represent
617                    unknown)
618                "modified": Modification date
619        excludeSet -- set of words which should be excluded from the list
620        includeSet -- wikiWords to include in the result
621
622        Does not support caching
623        """
624        with self.textOperationLock:
625            wikiData = self.getWikiData()
626            wikiDocument = self.getWikiDocument()
627           
628            if withFields is None:
629                withFields = ()
630   
631            relations = wikiData.getChildRelationships(self.wikiWord,
632                    existingonly, selfreference, withFields=withFields)
633   
634            if len(excludeSet) > 0:
635                # Filter out members of excludeSet
636                if len(withFields) > 0:
637                    relations = [r for r in relations if not r[0] in excludeSet]
638                else:
639                    relations = [r for r in relations if not r in excludeSet]
640   
641            if len(includeSet) > 0:
642                # First unalias wiki pages and remove non-existing ones
643                clearedIncSet = set()
644                for w in includeSet:
645                    w = wikiDocument.getUnAliasedWikiWord(w)
646                    if w is None:
647                        continue
648
649#                     if not wikiDocument.isDefinedWikiLink(w):
650#                         continue
651   
652                    clearedIncSet.add(w)
653
654                # Then remove items already present in relations
655                if len(clearedIncSet) > 0:
656                    if len(withFields) > 0:
657                        for r in relations:
658                            clearedIncSet.discard(r[0])
659                    else:
660                        for r in relations:
661                            clearedIncSet.discard(r)
662   
663                # Now collect info
664                if len(clearedIncSet) > 0:
665                    relations += [wikiData.getExistingWikiWordInfo(r,
666                            withFields=withFields) for r in clearedIncSet]
667   
668            return relations
669
670
671    def getAttributes(self):
672        with self.textOperationLock:
673            if self.attrs is not None:
674                return self.attrs
675           
676            data = self.getWikiData().getAttributesForWord(self.wikiWord)
677
678#         with self.textOperationLock:
679#             if self.attrs is not None:
680#                 return self.attrs
681
682            self.attrs = {}
683            for (key, val) in data:
684                self._addAttribute(key, val)
685           
686            return self.attrs
687
688    getProperties = getAttributes  # TODO remove "property"-compatibility
689
690
691    def getAttribute(self, attrkey, default=None):
692        with self.textOperationLock:
693            attrs = self.getAttributes()
694            if attrs.has_key(attrkey):
695                return attrs[attrkey][-1]
696            else:
697                return default
698
699    def getAttributeOrGlobal(self, attrkey, default=None):
700        """
701        Tries to find an attribute on this page and returns the first value.
702        If it can't be found for page, it is searched for a global
703        attribute with this name. If this also can't be found,
704        default (normally None) is returned.
705        """
706        with self.textOperationLock:
707            attrs = self.getAttributes()
708            if attrs.has_key(attrkey):
709                return attrs[attrkey][-1]
710            else:
711                globalAttrs = self.getWikiData().getGlobalAttributes()     
712                return globalAttrs.get(u"global."+attrkey, default)
713
714    getPropertyOrGlobal = getAttributeOrGlobal # TODO remove "property"-compatibility
715   
716   
717    def _addAttribute(self, key, val):
718        values = self.attrs.get(key)
719        if not values:
720            values = []
721            self.attrs[key] = values
722        values.append(val)
723
724
725    def getTodos(self):
726        with self.textOperationLock:
727            if self.todos is None:
728                self.todos = self.getWikiData().getTodosForWord(self.wikiWord)
729                       
730            return self.todos
731
732
733    def getAnchors(self):
734        """
735        Return sequence of anchors in page
736        """
737        pageAst = self.getLivePageAst()
738        return [node.anchorLink
739                for node in pageAst.iterDeepByName("anchorDef")]
740
741
742    def getLiveTextNoTemplate(self):
743        """
744        Return None if page isn't existing instead of creating an automatic
745        live text (e.g. by template).
746        """
747        with self.textOperationLock:
748            if self.getTxtEditor() is not None:
749                return self.getLiveText()
750            else:
751                if self.isDefined():
752                    return self.getContent()
753                else:
754                    return None
755
756
757    def getFormatDetails(self):
758        """
759        According to currently stored settings, return a
760        ParseUtilities.WikiPageFormatDetails object to describe
761        formatting
762        """
763        with self.textOperationLock:
764            withCamelCase = strToBool(self.getAttributeOrGlobal(
765                    u"camelCaseWordsEnabled"), True)
766   
767#             footnotesAsWws = self.wikiDocument.getWikiConfig().getboolean(
768#                     "main", "footnotes_as_wikiwords", False)
769   
770            autoLinkMode = self.getAttributeOrGlobal(u"auto_link", u"off").lower()
771
772            paragraphMode = strToBool(self.getAttributeOrGlobal(
773                    u"paragraph_mode"), False)
774                   
775            langHelper = wx.GetApp().createWikiLanguageHelper(
776                    self.wikiDocument.getWikiDefaultWikiLanguage())
777
778            wikiLanguageDetails = langHelper.createWikiLanguageDetails(
779                    self.wikiDocument, self)
780
781            return ParseUtilities.WikiPageFormatDetails(
782                    withCamelCase=withCamelCase,
783                    wikiDocument=self.wikiDocument,
784                    basePage=self,
785                    autoLinkMode=autoLinkMode,
786                    paragraphMode=paragraphMode,
787                    wikiLanguageDetails=wikiLanguageDetails)
788
789
790    def isDefined(self):
791        return self.getWikiDocument().isDefinedWikiPage(self.getWikiWord())
792
793
794    @staticmethod
795    def extractAttributeNodesFromPageAst(pageAst):
796        """
797        Return an iterator of attribute nodes in pageAst. This does not return
798        attributes inside of todo entries.
799        """
800        # Complicated version for compatibility with old language plugins
801        # TODO remove "property"-compatibility
802        return Utilities.iterMergesort((
803                pageAst.iterUnselectedDeepByName("attribute",
804                frozenset(("todoEntry",))),
805                pageAst.iterUnselectedDeepByName("property",
806                frozenset(("todoEntry",))) ),
807                key=lambda n: n.pos)
808       
809        # Simple one for later
810#         return pageAst.iterUnselectedDeepByName("attribute",
811#                 frozenset(("todoEntry",)))
812
813
814    @staticmethod
815    def extractTodoNodesFromPageAst(pageAst):
816        """
817        Return an iterator of todo nodes in pageAst.
818        """
819        return pageAst.iterDeepByName("todoEntry")
820
821
822    def _save(self, text, fireEvent=True):
823        """
824        Saves the content of current doc page.
825        """
826        pass
827
828
829    def setPresentation(self, data, startPos):
830        """
831        Set (a part of) the presentation tuple. This is silently ignored
832        if the "write access failed" or "read access failed" flags are
833        set in the wiki document.
834        data -- tuple with new presentation data
835        startPos -- start position in the presentation tuple which should be
836                overwritten with data.
837        """
838        raise NotImplementedError   # abstract
839
840
841    def initiateUpdate(self, fireEvent=True):
842        """
843        Initiate update of page meta-data. This function may call update
844        directly if can be done fast
845        """
846        pass
847
848
849    def getLivePageAstIfAvailable(self):
850        """
851        Return the current, up-to-data page AST if available, None otherwise
852        """
853        with self.textOperationLock:
854            # Current state
855            text = self.getLiveText()
856            formatDetails = self.getFormatDetails()
857
858            # AST state
859            pageAst = self.livePageAst
860            baseFormatDetails = self.livePageBaseFormatDetails
861
862            if pageAst is not None and \
863                    baseFormatDetails is not None and \
864                    formatDetails.isEquivTo(baseFormatDetails) and \
865                    self.liveTextPlaceHold is self.livePageBasePlaceHold:
866                return pageAst
867
868            return None
869
870
871
872    def getLivePageAst(self, fireEvent=True, dieOnChange=False,
873            threadstop=DUMBTHREADSTOP, allowMetaDataUpdate=False):
874        """
875        Return PageAst of live text. In rare cases the live text may have
876        changed while method is running and the result is inaccurate.
877        """
878#         if self.livePageAstBuildLock.acquire(False):
879#             self.livePageAstBuildLock.release()
880#         else:
881#             if wx.Thread_IsMain(): traceback.print_stack()
882
883        with self.livePageAstBuildLock:   # TODO: Timeout?
884            threadstop.testRunning()
885
886            with self.textOperationLock:
887                text = self.getLiveText()
888                liveTextPlaceHold = self.liveTextPlaceHold
889                formatDetails = self.getFormatDetails()
890
891                pageAst = self.getLivePageAstIfAvailable()
892
893            if pageAst is not None:
894                return pageAst
895
896            if dieOnChange:
897                if threadstop is DUMBTHREADSTOP:
898                    threadstop = FunctionThreadStop(
899                            lambda: liveTextPlaceHold is self.liveTextPlaceHold)
900                else:
901                    origThreadstop = threadstop
902                    threadstop = FunctionThreadStop(
903                            lambda: origThreadstop.isRunning() and 
904                            liveTextPlaceHold is self.liveTextPlaceHold)
905
906            if len(text) == 0:
907                pageAst = buildSyntaxNode([], 0)
908            else:
909                pageAst = self.parseTextInContext(text, formatDetails=formatDetails,
910                        threadstop=threadstop)
911
912            with self.textOperationLock:
913                threadstop.testRunning()
914
915                self.livePageAst = pageAst
916                self.livePageBasePlaceHold = liveTextPlaceHold
917                self.livePageBaseFormatDetails = formatDetails
918
919
920        if self.isReadOnlyEffect():
921            threadstop.testRunning()
922            return pageAst
923
924#         if False and allowMetaDataUpdate:   # TODO: Option
925#             self._refreshMetaData(pageAst, formatDetails, fireEvent=fireEvent,
926#                     threadstop=threadstop)
927
928        with self.textOperationLock:
929            threadstop.testRunning()
930            return pageAst
931
932
933    def onModifiedSpellCheckerSession(self, miscevt):
934        """
935        Invalidate spell checker data when e.g. new words are added to
936        dictionary
937        """
938        with self.textOperationLock:
939            self.__sinkWikiDocumentSpellSession.setEventSource(None)
940
941            self.liveSpellCheckerUnknownWords = None
942            self.liveSpellCheckerUnknownWordsBasePlaceHold = None
943
944        self.fireMiscEventKeys(("modified spell checker session",))
945
946
947    def getSpellCheckerUnknownWordsIfAvailable(self):
948        """
949        Return the current, up-to-data page AST if available, None otherwise
950        """
951        with self.textOperationLock:
952            # unknown words
953            unknownWords = self.liveSpellCheckerUnknownWords
954
955            if unknownWords is not None and \
956                    self.liveTextPlaceHold is \
957                    self.liveSpellCheckerUnknownWordsBasePlaceHold:
958                return unknownWords
959
960            return None
961
962
963
964    def getSpellCheckerUnknownWords(self, dieOnChange=False,
965            threadstop=DUMBTHREADSTOP):
966        """
967        Return list of unknown words as list of WikiPyparsing.TerminalNode of live text.
968        In rare cases the live text may have changed while method is running and
969        the result is inaccurate.
970        """
971#         with self.livePageAstBuildLock:   # TODO: Timeout?
972        threadstop.testRunning()
973       
974        spellSession = self.getWikiDocument().createOnlineSpellCheckerSessionClone()
975        if spellSession is None:
976            return
977           
978        spellSession.setCurrentDocPage(self)
979
980        with self.textOperationLock:
981            text = self.getLiveText()
982            liveTextPlaceHold = self.liveTextPlaceHold
983
984            unknownWords = self.getSpellCheckerUnknownWordsIfAvailable()
985
986        if unknownWords is not None:
987            return unknownWords
988
989        if dieOnChange:
990            if threadstop is DUMBTHREADSTOP:
991                threadstop = FunctionThreadStop(
992                        lambda: liveTextPlaceHold is self.liveTextPlaceHold)
993            else:
994                origThreadstop = threadstop
995                threadstop = FunctionThreadStop(
996                        lambda: origThreadstop.isRunning() and 
997                        liveTextPlaceHold is self.liveTextPlaceHold)
998
999        if len(text) == 0:
1000            unknownWords = []
1001        else:
1002            unknownWords = spellSession.buildUnknownWordList(text,
1003                    threadstop=threadstop)
1004
1005        with self.textOperationLock:
1006            threadstop.testRunning()
1007
1008            self.liveSpellCheckerUnknownWords = unknownWords
1009            self.liveSpellCheckerUnknownWordsBasePlaceHold = liveTextPlaceHold
1010           
1011            self.__sinkWikiDocumentSpellSession.setEventSource(
1012                    self.getWikiDocument().getOnlineSpellCheckerSession())
1013
1014
1015        if self.isReadOnlyEffect():
1016            threadstop.testRunning()
1017            return unknownWords
1018
1019#         if False and allowMetaDataUpdate:   # TODO: Option
1020#             self._refreshMetaData(pageAst, formatDetails, fireEvent=fireEvent,
1021#                     threadstop=threadstop)
1022
1023        with self.textOperationLock:
1024            threadstop.testRunning()
1025            return unknownWords
1026
1027
1028##     @profile
1029    def parseTextInContext(self, text, formatDetails=None,
1030            threadstop=DUMBTHREADSTOP):
1031        """
1032        Return PageAst of text in the context of this page (wiki language and
1033        format details).
1034
1035        text: unistring with text
1036        """
1037        parser = wx.GetApp().createWikiParser(self.getWikiLanguageName()) # TODO debug mode  , True
1038
1039        if formatDetails is None:
1040            formatDetails = self.getFormatDetails()
1041
1042        try:
1043            pageAst = parser.parse(self.getWikiLanguageName(), text,
1044                    formatDetails, threadstop=threadstop)
1045        finally:
1046            wx.GetApp().freeWikiParser(parser)
1047
1048        threadstop.testRunning()
1049
1050        return pageAst
1051
1052
1053    _DEFAULT_PRESENTATION = (0, 0, 0, 0, 0, None)
1054
1055    def getPresentation(self):
1056        """
1057        Get the presentation tuple (<cursor pos>, <editor scroll pos x>,
1058            <e.s.p. y>, <preview s.p. x>, <p.s.p. y>, <folding list>)
1059        The folding list may be None or a list of UInt32 numbers
1060        containing fold level, header flag and expand flag for each line
1061        in editor.
1062        """
1063        wikiData = self.wikiDocument.getWikiData()
1064
1065        if wikiData is None:
1066            return AbstractWikiPage._DEFAULT_PRESENTATION
1067
1068        datablock = wikiData.getPresentationBlock(
1069                self.getWikiWord())
1070
1071        if datablock is None or datablock == "":
1072            return AbstractWikiPage._DEFAULT_PRESENTATION
1073
1074        try:
1075            # TODO: On next file format change: Change '=' to '>'
1076            if len(datablock) == struct.calcsize("=iiiii"):
1077                # Version 0
1078                return struct.unpack("=iiiii", datablock) + (None,)
1079            else:
1080                ss = Serialization.SerializeStream(stringBuf=datablock)
1081                rcVer = ss.serUint8(1)
1082                if rcVer == 1:
1083                    # Compatible to version 1               
1084                    ver = ss.serUint8(1)
1085                    pt = [ss.serInt32(0), ss.serInt32(0), ss.serInt32(0),
1086                            ss.serInt32(0), ss.serInt32(0), None]
1087   
1088                    # Fold list
1089                    fl = ss.serArrUint32([])
1090                    if len(fl) == 0:
1091                        fl = None
1092   
1093                    pt[5] = fl
1094   
1095                    return tuple(pt)
1096                else:
1097                    return AbstractWikiPage._DEFAULT_PRESENTATION
1098        except struct.error:
1099            return AbstractWikiPage._DEFAULT_PRESENTATION
1100
1101
1102
1103
1104class WikiPage(AbstractWikiPage):
1105    """
1106    holds the data for a real wikipage (no alias).
1107
1108    Fetched via the WikiDataManager.getWikiPage method.
1109    """
1110    def __init__(self, wikiDocument, wikiWord):
1111        AbstractWikiPage.__init__(self, wikiDocument, wikiWord)
1112
1113        self.versionOverview = UNDEFINED
1114
1115
1116    def getVersionOverview(self):
1117        """
1118        Return Versioning.VersionOverview object. If necessary create one.
1119        """
1120        with self.textOperationLock:
1121            if self.versionOverview is UNDEFINED or self.versionOverview is None:
1122                from .timeView.Versioning import VersionOverview
1123               
1124                versionOverview = VersionOverview(self.getWikiDocument(),
1125                        self)
1126                versionOverview.readOverview()
1127                self.versionOverview = versionOverview
1128   
1129            return self.versionOverview
1130
1131
1132    def getExistingVersionOverview(self):
1133        """
1134        Return Versioning.VersionOverview object.
1135        If not existing already return None.
1136        """
1137        with self.textOperationLock:
1138            if self.versionOverview is UNDEFINED:
1139                from .timeView.Versioning import VersionOverview
1140
1141                versionOverview = VersionOverview(self.getWikiDocument(),
1142                        self)
1143
1144                if versionOverview.isNotInDatabase():
1145                    self.versionOverview = None
1146                else:
1147                    versionOverview.readOverview()
1148                    self.versionOverview = versionOverview
1149
1150            return self.versionOverview
1151
1152    def releaseVersionOverview(self):
1153        """
1154        Should only be called by VersionOverview.invalidate()
1155        """
1156        self.versionOverview = UNDEFINED
1157
1158
1159    def getNonAliasPage(self):
1160        """
1161        If this page belongs to an alias of a wiki word, return a page for
1162        the real one, otherwise return self.
1163        This class always returns self
1164        """
1165        return self
1166
1167
1168    def setPresentation(self, data, startPos):
1169        """
1170        Set (a part of) the presentation tuple. This is silently ignored
1171        if the "write access failed" or "read access failed" flags are
1172        set in the wiki document.
1173        data -- tuple with new presentation data
1174        startPos -- start position in the presentation tuple which should be
1175                overwritten with data.
1176        """
1177        if self.isReadOnlyEffect():
1178            return
1179
1180        if self.wikiDocument.getReadAccessFailed() or \
1181                self.wikiDocument.getWriteAccessFailed():
1182            return
1183
1184        try:
1185            pt = self.getPresentation()
1186            pt = pt[:startPos] + data + pt[startPos+len(data):]
1187   
1188            wikiData = self.wikiDocument.getWikiData()
1189            if wikiData is None:
1190                return
1191               
1192            if pt[5] is None:
1193                # Write it in old version 0
1194                # TODO: On next file format change: Change '=' to '>'
1195                wikiData.setPresentationBlock(self.getWikiWord(),
1196                        struct.pack("=iiiii", *pt[:5]))
1197            else:
1198                # Write it in new version 1
1199                ss = Serialization.SerializeStream(stringBuf=True, readMode=False)
1200                ss.serUint8(1)  # Read compatibility version
1201                ss.serUint8(1)  # Real version
1202                # First five numbers
1203                for n in pt[:5]:
1204                    ss.serInt32(n)
1205                # Folding tuple
1206                ft = pt[5]
1207                if ft is None:
1208                    ft = ()
1209                ss.serArrUint32(ft)
1210
1211                wikiData.setPresentationBlock(self.getWikiWord(),
1212                        ss.getBytes())
1213
1214        except AttributeError:
1215            traceback.print_exc()
1216
1217
1218    def informVisited(self):
1219        """
1220        Called to inform the page that it was visited and should set
1221        the "visited" entry in the database to current time.
1222        """
1223        with self.textOperationLock:
1224            if self.isReadOnlyEffect():
1225                return
1226   
1227            if not self.isDefined():
1228                return
1229
1230            wikiData = self.wikiDocument.getWikiData()
1231            word = self.getWikiWord()
1232            ts = wikiData.getTimestamps(word)
1233            ts = ts[:2] + (time.time(),) + ts[3:]
1234            wikiData.setTimestamps(word, ts)
1235
1236
1237    def _changeHeadingForTemplate(self, content):
1238        """
1239        Modify the heading of a template page's content to match the page
1240        created from the template.
1241        """
1242        # Prefix is normally u"++"
1243        pageTitlePrefix = \
1244                self.getWikiDocument().getPageTitlePrefix() + u" "
1245               
1246        if self.suggNewPageTitle is None:
1247            wikiWordHead = self.getWikiDocument().getWikiPageTitle(
1248                    self.getWikiWord())
1249        else:
1250            wikiWordHead = self.suggNewPageTitle
1251
1252        if wikiWordHead is None:
1253            return content
1254
1255        wikiWordHead = pageTitlePrefix + wikiWordHead + u"\n"
1256
1257        # Remove current heading, if present. removeFirst holds number of
1258        # characters to remove at beginning when prepending new title
1259
1260        removeFirst = 0
1261        if content.startswith(pageTitlePrefix):
1262            try:
1263                removeFirst = content.index(u"\n", len(pageTitlePrefix)) + 1
1264            except ValueError:
1265                pass
1266
1267        return wikiWordHead + content[removeFirst:]
1268
1269
1270    def getContentOfTemplate(self, templatePage, parentPage):
1271        # getLiveText() would be more logical, but this may
1272        # mean that content is up to date, while attributes
1273        # are not updated.
1274        content = templatePage.getContent()
1275       
1276        # Check if template title should be changed
1277        tplHeading = parentPage.getAttributeOrGlobal(
1278                u"template_head", u"auto")
1279        if tplHeading in (u"auto", u"automatic"):
1280            content = self._changeHeadingForTemplate(content)
1281
1282        return content
1283
1284
1285    def setMetaDataFromTemplate(self, templatePage):
1286        # Load attributes from template page
1287        self.attrs = templatePage._cloneDeepAttributes()
1288       
1289
1290    def getContent(self):
1291        """
1292        Returns page content. If page doesn't exist already the template
1293        creation is done here. After calling this function, attributes
1294        are also accessible for a non-existing page
1295        """
1296        content = None
1297        try:
1298            content = self.getWikiData().getContent(self.wikiWord)
1299        except WikiFileNotFoundException, e:
1300            # Create initial content of new page
1301
1302            # Check for "template" attribute
1303            parents = self.getParentRelationships()
1304            if len(parents) > 0:
1305                langHelper = wx.GetApp().createWikiLanguageHelper(
1306                        self.getWikiLanguageName())
1307
1308                templateSource = None
1309                templateParent = None
1310                conflict = False
1311                for parent in parents:
1312                    try:
1313                        parentPage = self.wikiDocument.getWikiPage(parent)
1314                        templateWord = langHelper.resolveWikiWordLink(
1315                                parentPage.getAttribute("template"),
1316                                parentPage)
1317                        if templateWord is not None and \
1318                                self.wikiDocument.isDefinedWikiLink(templateWord):
1319                            if templateSource is None:
1320                                templateSource = templateWord
1321                                templateParentPage = parentPage
1322                            elif templateSource != templateWord:
1323                                # More than one possible template source
1324                                # -> no template, stop here
1325                                templateSource = None
1326                                templateParentPage = None
1327                                conflict = True
1328                                break
1329                    except (WikiWordNotFoundException, WikiFileNotFoundException):
1330                        continue
1331
1332                if templateSource is None and not conflict:
1333                    # No individual template attributes, try to find global one
1334                    globalAttrs = self.getWikiData().getGlobalAttributes()     
1335                   
1336                    templateWord = globalAttrs.get(u"global.template")
1337                    if templateWord is not None and \
1338                            self.wikiDocument.isDefinedWikiLink(templateWord):
1339                        templateSource = templateWord
1340                        templateParentPage = self.wikiDocument.getWikiPage(
1341                                parents[0])
1342
1343               
1344                if templateSource is not None:
1345                    templatePage = self.wikiDocument.getWikiPage(templateSource)
1346
1347                    content = self.getContentOfTemplate(templatePage,
1348                            templateParentPage)
1349                    self.setMetaDataFromTemplate(templatePage)
1350
1351
1352#             if len(parents) == 1:
1353#                 # Check if there is a template page
1354#                 try:
1355#                     parentPage = self.wikiDocument.getWikiPage(parents[0])
1356#
1357#
1358#                     # TODO Error checking
1359# #                     templateWord = parentPage.getAttributeOrGlobal("template")
1360#                     templateWord = langHelper.resolveWikiWordLink(
1361#                             parentPage.getAttributeOrGlobal("template"),
1362#                             parentPage)
1363#
1364#                     templatePage = self.wikiDocument.getWikiPage(templateWord)
1365#                     
1366#                     content = self.getContentOfTemplate(templatePage, parentPage)
1367#                     self.setMetaDataFromTemplate(templatePage)
1368#
1369#                 except (WikiWordNotFoundException, WikiFileNotFoundException):
1370#                     pass
1371
1372            if content is None:
1373                if self.suggNewPageTitle is None:
1374                    title = self.getWikiDocument().getWikiPageTitle(
1375                            self.getWikiWord())
1376                else:
1377                    title = self.suggNewPageTitle
1378
1379                if title is not None:
1380                    content = u"%s %s\n\n" % \
1381                            (self.wikiDocument.getPageTitlePrefix(),
1382                            title)
1383                else:
1384                    content = u""
1385
1386        return content
1387
1388
1389#     def isDefined(self):
1390#         return self.getWikiDocument().isDefinedWikiPage(self.getWikiWord())
1391
1392
1393    def pseudoDeletePage(self):
1394        """
1395        Delete a page which doesn't really exist.
1396        Just sends an appropriate event.
1397        """
1398        wx.CallAfter(self.fireMiscEventKeys,
1399                ("pseudo-deleted page", "pseudo-deleted wiki page"))
1400
1401
1402    def deletePage(self):
1403        """
1404        Deletes the page from database
1405        """
1406        with self.textOperationLock:
1407            if self.isReadOnlyEffect():
1408                return
1409   
1410            if self.isDefined():
1411                self.getWikiData().deleteWord(self.getWikiWord())
1412
1413            vo = self.getExistingVersionOverview()
1414            if vo is not None:
1415                vo.delete()
1416                self.versionOverview = UNDEFINED
1417       
1418            self.removeFromSearchIndex()   # TODO: Check for (dead-)locks
1419
1420            wx.CallAfter(self.fireMiscEventKeys,
1421                    ("deleted page", "deleted wiki page"))
1422
1423
1424    def renameVersionData(self, newWord):
1425        """
1426        This is called by WikiDocument(=WikiDataManager) during
1427        WikiDocument.renameWikiWord() and shouldn't be called elsewhere.
1428        """
1429        with self.textOperationLock:
1430            vo = self.getExistingVersionOverview()
1431            if vo is None:
1432                return
1433           
1434            vo.renameTo(u"wikipage/" + newWord)
1435            self.versionOverview = UNDEFINED
1436
1437
1438    def informRenamedWikiPage(self, newWord):
1439        """
1440        Informs object that the page was renamed to newWord.
1441        This page object itself does not change its name but becomes invalid!
1442
1443        This function should be called by WikiDocument(=WikiDataManager) only,
1444        use WikiDocument.renameWikiWord() to rename a page.
1445        """
1446
1447        p = {}
1448        p["renamed page"] = True
1449        p["renamed wiki page"] = True
1450        p["newWord"] = newWord
1451       
1452        callInMainThreadAsync(self.fireMiscEventProps, p)
1453
1454
1455    def _cloneDeepAttributes(self):
1456        with self.textOperationLock:
1457            result = {}
1458            for key, value in self.getAttributes().iteritems():
1459                result[key] = value[:]
1460               
1461            return result
1462
1463
1464    def checkFileSignatureAndMarkDirty(self, fireEvent=True):
1465        """
1466        First checks if file signature is valid, if not, the
1467        "metadataprocessed" field of the word is set to 0 to mark
1468        meta-data as not up-to-date. At last the signature is
1469        refreshed.
1470       
1471        This all is done inside the lock of the WikiData so it is
1472        somewhat atomically.
1473        """
1474        with self.textOperationLock:
1475            if self.wikiDocument.isReadOnlyEffect():
1476                return True  # TODO Error message?
1477   
1478            if not self.isDefined():
1479                return True  # TODO Error message?
1480   
1481            wikiData = self.getWikiData()
1482            word = self.wikiWord
1483
1484            proxyAccessLock = getattr(wikiData, "proxyAccessLock", None)
1485            if proxyAccessLock is not None:
1486                proxyAccessLock.acquire()
1487            try:
1488                valid = wikiData.validateFileSignatureForWord(word)
1489               
1490                if valid:
1491                    return True
1492   
1493                wikiData.setMetaDataState(word,
1494                        Consts.WIKIWORDMETADATA_STATE_DIRTY)
1495                wikiData.refreshFileSignatureForWord(word)
1496                self.markTextChanged()
1497            finally:
1498                if proxyAccessLock is not None:
1499                    proxyAccessLock.release()
1500
1501            editor = self.getTxtEditor()
1502       
1503        if editor is not None:
1504            # TODO Check for deadlocks
1505            callInMainThread(editor.handleInvalidFileSignature, self)
1506
1507        if fireEvent:
1508            wx.CallAfter(self.fireMiscEventKeys,
1509                    ("checked file signature invalid",))
1510
1511        return False
1512
1513
1514    def markMetaDataDirty(self):
1515        self.getWikiData().setMetaDataState(self.wikiWord,
1516                Consts.WIKIWORDMETADATA_STATE_DIRTY)
1517
1518
1519    def _refreshMetaData(self, pageAst, formatDetails, fireEvent=True,
1520            threadstop=DUMBTHREADSTOP):
1521
1522        self.refreshAttributesFromPageAst(pageAst, threadstop=threadstop)
1523
1524        formatDetails2 = self.getFormatDetails()
1525        if not formatDetails.isEquivTo(formatDetails2):
1526            # Formatting details have changed -> stop and wait for
1527            # new round to update
1528            return False
1529
1530        return self.refreshMainDbCacheFromPageAst(pageAst, fireEvent=fireEvent,
1531                threadstop=threadstop)
1532
1533
1534    def refreshSyncUpdateMatchTerms(self):
1535        """
1536        Refresh those match terms which must be refreshed synchronously
1537        """
1538        if self.isReadOnlyEffect():
1539            return
1540
1541        WORD_TYPE = Consts.WIKIWORDMATCHTERMS_TYPE_ASLINK | \
1542                Consts.WIKIWORDMATCHTERMS_TYPE_FROM_WORD | \
1543                Consts.WIKIWORDMATCHTERMS_TYPE_SYNCUPDATE
1544
1545        matchTerms = [(self.wikiWord, WORD_TYPE, self.wikiWord, -1, 0)]
1546        self.getWikiData().updateWikiWordMatchTerms(self.wikiWord, matchTerms,
1547                syncUpdate=True)
1548
1549
1550    def refreshAttributesFromPageAst(self, pageAst, threadstop=DUMBTHREADSTOP):
1551        """
1552        Update properties (aka attributes) only.
1553        This is step one in update/rebuild process.
1554        """
1555        if self.isReadOnlyEffect():
1556            return True  # TODO Error?
1557
1558        langHelper = wx.GetApp().createWikiLanguageHelper(
1559                self.getWikiLanguageName())
1560
1561        attrs = {}
1562
1563        def addAttribute(key, value):
1564            threadstop.testRunning()
1565            values = attrs.get(key)
1566            if not values:
1567                values = []
1568                attrs[key] = values
1569            values.append(value)
1570
1571
1572        attrNodes = self.extractAttributeNodesFromPageAst(pageAst)
1573        for node in attrNodes:
1574            for attrKey, attrValue in \
1575                    (getattr(node, "attrs", []) + getattr(node, "props", [])):  # TODO remove "property"-compatibility
1576                addAttribute(attrKey, attrValue)
1577
1578        with self.textOperationLock:
1579            threadstop.testRunning()
1580
1581            self.attrs = None
1582
1583        try:
1584            self.getWikiData().updateAttributes(self.wikiWord, attrs)
1585        except WikiWordNotFoundException:
1586            return False
1587
1588        valid = False
1589
1590        with self.textOperationLock:
1591            if self.saveDirtySince is None and \
1592                    self.livePageBasePlaceHold is self.liveTextPlaceHold and \
1593                    self.livePageBaseFormatDetails is not None and \
1594                    self.getFormatDetails().isEquivTo(self.livePageBaseFormatDetails) and \
1595                    pageAst is self.livePageAst:
1596
1597                threadstop.testRunning()
1598                # clear the dirty flag
1599
1600                self.getWikiData().setMetaDataState(self.wikiWord,
1601                        Consts.WIKIWORDMETADATA_STATE_ATTRSPROCESSED)
1602
1603                valid = True
1604
1605        return valid
1606
1607
1608
1609    def refreshMainDbCacheFromPageAst(self, pageAst, fireEvent=True,
1610            threadstop=DUMBTHREADSTOP):
1611        """
1612        Update everything else (todos, relations).
1613        This is step two in update/rebuild process.
1614        """
1615        if self.isReadOnlyEffect():
1616            return True   # return True or False?
1617
1618        todos = []
1619        childRelations = []
1620        childRelationSet = set()
1621
1622        def addTodo(todoKey, todoValue):
1623            threadstop.testRunning()
1624            todo = (todoKey, todoValue)
1625            if todo not in todos:
1626                todos.append(todo)
1627
1628        def addChildRelationship(toWord, pos):
1629            threadstop.testRunning()
1630            if toWord not in childRelationSet:
1631                childRelations.append((toWord, pos))
1632                childRelationSet.add(toWord)
1633
1634        # Add todo entries
1635        todoNodes = pageAst.iterDeepByName("todoEntry")
1636        for node in todoNodes:
1637            for todoKey, todoValueNode in node.todos:
1638                addTodo(todoKey, todoValueNode.getString())
1639
1640        threadstop.testRunning()
1641
1642        # Add child relations
1643        wwTokens = pageAst.iterDeepByName("wikiWord")
1644        for t in wwTokens:
1645            addChildRelationship(t.wikiWord, t.pos)
1646
1647        threadstop.testRunning()
1648       
1649        # Add aliases to match terms
1650        matchTerms = []
1651
1652        ALIAS_TYPE = Consts.WIKIWORDMATCHTERMS_TYPE_EXPLICIT_ALIAS | \
1653                Consts.WIKIWORDMATCHTERMS_TYPE_ASLINK | \
1654                Consts.WIKIWORDMATCHTERMS_TYPE_FROM_ATTRIBUTES
1655
1656        langHelper = wx.GetApp().createWikiLanguageHelper(
1657                self.getWikiLanguageName())
1658
1659        for w, k, v in self.getWikiDocument().getAttributeTriples(
1660                self.wikiWord, u"alias", None):
1661            threadstop.testRunning()
1662            if not langHelper.checkForInvalidWikiLink(v,
1663                    self.getWikiDocument()):
1664#                 matchTerms.append((v, ALIAS_TYPE, self.wikiWord, -1, -1))
1665                matchTerms.append((langHelper.resolveWikiWordLink(v, self),
1666                        ALIAS_TYPE, self.wikiWord, -1, -1))
1667
1668        # Add headings to match terms if wanted
1669        depth = self.wikiDocument.getWikiConfig().getint(
1670                "main", "headingsAsAliases_depth")
1671
1672        if depth > 0:
1673            HEADALIAS_TYPE = Consts.WIKIWORDMATCHTERMS_TYPE_FROM_CONTENT
1674            for node in pageAst.iterFlatByName("heading"):
1675                threadstop.testRunning()
1676                if node.level > depth:
1677                    continue
1678
1679                title = node.getString()
1680                if title.endswith(u"\n"):
1681                    title = title[:-1]
1682               
1683                matchTerms.append((title, HEADALIAS_TYPE, self.wikiWord,
1684                        node.pos + node.strLength, 0))
1685
1686        with self.textOperationLock:
1687            threadstop.testRunning()
1688
1689            self.todos = None
1690            self.childRelations = None
1691            self.childRelationSet = set()
1692        try:
1693            self.getWikiData().updateTodos(self.wikiWord, todos)
1694            threadstop.testRunning()
1695            self.getWikiData().updateChildRelations(self.wikiWord, childRelations)
1696            threadstop.testRunning()
1697            self.getWikiData().updateWikiWordMatchTerms(self.wikiWord, matchTerms)
1698            threadstop.testRunning()
1699        except WikiWordNotFoundException:
1700            return False
1701#             self.modified = None   # ?
1702#             self.created = None
1703
1704
1705            # Now we check the whole chain if flags can be set:
1706            # db content is identical to liveText
1707            # liveText is basis of current livePageAst
1708            # formatDetails are same as the ones used for livePageAst
1709            # and livePageAst is identical to pageAst processed in this method
1710
1711        valid = False
1712        with self.textOperationLock:
1713#             print "--refreshMainDbCacheFromPageAst43", repr((self.wikiWord, self.saveDirtySince,
1714#                     self.livePageBasePlaceHold is self.liveTextPlaceHold,
1715#                     self.livePageBaseFormatDetails is not None,
1716# #                     self.getFormatDetails().isEquivTo(self.livePageBaseFormatDetails),
1717#                     pageAst is self.livePageAst))
1718            if self.saveDirtySince is None and \
1719                    self.livePageBasePlaceHold is self.liveTextPlaceHold and \
1720                    self.livePageBaseFormatDetails is not None and \
1721                    self.getFormatDetails().isEquivTo(self.livePageBaseFormatDetails) and \
1722                    pageAst is self.livePageAst:
1723
1724                threadstop.testRunning()
1725                # clear the dirty flag
1726                self.updateDirtySince = None
1727
1728                self.getWikiData().setMetaDataState(self.wikiWord,
1729                        Consts.WIKIWORDMETADATA_STATE_SYNTAXPROCESSED)
1730                valid = True
1731
1732        if fireEvent:
1733            callInMainThreadAsync(self.fireMiscEventKeys,
1734                    ("updated wiki page", "updated page"))
1735
1736        return valid
1737
1738   
1739    def putIntoSearchIndex(self, threadstop=DUMBTHREADSTOP):
1740        """
1741        Add or update the reverse index for the given docPage
1742        """
1743        with self.textOperationLock:
1744            if not self.getWikiDocument().isSearchIndexEnabled():
1745                return True  # Or false?
1746           
1747            liveTextPlaceHold = self.liveTextPlaceHold
1748            content = self.getLiveText()
1749
1750        writer = None
1751        try:
1752            searchIdx = self.getWikiDocument().getSearchIndex()
1753            writer = searchIdx.writer()
1754
1755            unifName = self.getUnifiedPageName()
1756
1757            writer.delete_by_term("unifName", unifName)
1758           
1759            writer.add_document(unifName=unifName,
1760                    modTimestamp=self.getTimestamps()[0],
1761                    content=content)
1762        except:
1763            if writer is not None:
1764                writer.cancel()
1765            raise
1766
1767        # Check within lock if data is current yet
1768        with self.textOperationLock:
1769            if not liveTextPlaceHold is self.liveTextPlaceHold:
1770                writer.cancel()
1771                return False
1772            else:
1773                writer.commit()
1774                self.getWikiData().setMetaDataState(self.wikiWord,
1775                        Consts.WIKIWORDMETADATA_STATE_INDEXED)
1776                return True
1777
1778    def removeFromSearchIndex(self):
1779        unifName = self.getUnifiedPageName()
1780       
1781        if not self.getWikiDocument().isSearchIndexEnabled():
1782            return
1783        try:
1784            searchIdx = self.getWikiDocument().getSearchIndex()
1785            writer = searchIdx.writer()
1786           
1787            writer.delete_by_term("unifName", unifName)
1788        except:
1789            writer.cancel()
1790            raise
1791
1792        writer.commit()
1793
1794
1795#     def update(self):
1796#         return self.runDatabaseUpdate(step=-2)
1797
1798    def runDatabaseUpdate(self, step=-1, threadstop=DUMBTHREADSTOP):
1799        with self.textOperationLock:
1800            if not self.isDefined():
1801                return False
1802            if self.isReadOnlyEffect():
1803                return False
1804
1805            liveTextPlaceHold = self.liveTextPlaceHold
1806            formatDetails = self.getFormatDetails()
1807
1808        try:
1809            pageAst = self.getLivePageAst(dieOnChange=True,
1810                    threadstop=threadstop)
1811
1812            # Check within lock if data is current yet
1813            with self.textOperationLock:
1814                if not liveTextPlaceHold is self.liveTextPlaceHold:
1815                    return False
1816   
1817   
1818            if step == -1:
1819                self._refreshMetaData(pageAst, formatDetails, threadstop=threadstop)
1820   
1821                with self.textOperationLock:
1822                    if not liveTextPlaceHold is self.liveTextPlaceHold:
1823                        return False
1824                    if not formatDetails.isEquivTo(self.getFormatDetails()):
1825                        self.initiateUpdate()
1826                        return False
1827#             elif step == -2:
1828#                 for i in range(15):   # while True  is too dangerous
1829#                     metaState = self.getWikiData().getMetaDataState(self.wikiWord)
1830#
1831#                     if not liveTextPlaceHold is self.liveTextPlaceHold:
1832#                         return False
1833#                     if not formatDetails.isEquivTo(self.getFormatDetails()):
1834#                         self.initiateUpdate()
1835#                         return False
1836#
1837#                     if metaState == Consts.WIKIWORDMETADATA_STATE_SYNTAXPROCESSED:
1838#                         return True
1839#
1840#                     elif metaState == Consts.WIKIWORDMETADATA_STATE_ATTRSPROCESSED:
1841#                         self.refreshMainDbCacheFromPageAst(pageAst,
1842#                                 threadstop=threadstop)
1843#                         continue
1844#
1845#                     else: # step == Consts.WIKIWORDMETADATA_STATE_DIRTY
1846#                         self.refreshAttributesFromPageAst(pageAst,
1847#                                 threadstop=threadstop)
1848#                         continue
1849            else:
1850                metaState = self.getMetaDataState()
1851
1852                if metaState >= self.getWikiDocument().getFinalMetaDataState() or \
1853                        metaState != step:
1854                    return False
1855   
1856                if step == Consts.WIKIWORDMETADATA_STATE_SYNTAXPROCESSED:
1857                    #
1858                    return self.putIntoSearchIndex(threadstop=threadstop)
1859
1860                elif step == Consts.WIKIWORDMETADATA_STATE_ATTRSPROCESSED:
1861                    return self.refreshMainDbCacheFromPageAst(pageAst,
1862                            threadstop=threadstop)
1863                else: # step == Consts.WIKIWORDMETADATA_STATE_DIRTY
1864                    return self.refreshAttributesFromPageAst(pageAst,
1865                            threadstop=threadstop)
1866
1867        except NotCurrentThreadException:
1868            return False
1869
1870
1871
1872    def initiateUpdate(self, fireEvent=True):
1873        """
1874        Initiate update of page meta-data. This function may call update
1875        directly if it can be done fast
1876        """
1877        with self.textOperationLock:
1878            self.wikiDocument.pushUpdatePage(self)
1879
1880
1881    def _save(self, text, fireEvent=True):
1882        """
1883        Saves the content of current wiki page.
1884        """
1885        if self.isReadOnlyEffect():
1886            return
1887       
1888        with self.textOperationLock:
1889            if not self.getWikiDocument().isDefinedWikiPage(self.wikiWord):
1890                # Pages isn't yet in database  -> fire event
1891                # The event may be needed to invalidate a cache
1892                self.fireMiscEventKeys(("saving new wiki page",))
1893
1894            self.getWikiData().setContent(self.wikiWord, text)
1895            self.refreshSyncUpdateMatchTerms()
1896            self.saveDirtySince = None
1897#             self.dbContentPlaceHold = object()
1898            if self.getEditorText() is None:
1899                self.liveTextPlaceHold = object()
1900
1901
1902            # Clear timestamp cache
1903            self.modified = None
1904
1905
1906
1907
1908    # ----- Advanced functions -----
1909
1910    def getChildRelationshipsTreeOrder(self, existingonly=False,
1911            excludeSet=frozenset(), includeSet=frozenset()):
1912        """
1913        Return a list of children wiki words of the page, ordered as they would
1914        appear in tree. Some children may be missing if they e.g.
1915        are set as hidden.
1916        existingonly -- true iff non-existing words should be hidden
1917        excludeSet -- set of words which should be excluded from the list
1918        includeSet -- wikiWords to include in the result
1919        """
1920       
1921        wikiDocument = self.wikiDocument
1922       
1923        # get the sort order for the children
1924        childSortOrder = self.getAttributeOrGlobal(u'child_sort_order',
1925                u"ascending")
1926           
1927        # Apply sort order
1928        if childSortOrder == u"natural":
1929            # TODO: Do it right
1930            # Retrieve relations as list of tuples (child, firstcharpos)
1931            relations = self.getChildRelationships(existingonly,
1932                    selfreference=False, withFields=("firstcharpos",),
1933                    excludeSet=excludeSet, includeSet=includeSet)
1934
1935            relations.sort(_cmpNumbersItem1)
1936            # Remove firstcharpos
1937            relations = [r[0] for r in relations]
1938        elif childSortOrder == u"mod_oldest":
1939            # Retrieve relations as list of tuples (child, modifTime)
1940            relations = self.getChildRelationships(existingonly,
1941                    selfreference=False, withFields=("modified",),
1942                    excludeSet=excludeSet, includeSet=includeSet)
1943            relations.sort(_cmpNumbersItem1)
1944            # Remove firstcharpos
1945            relations = [r[0] for r in relations]
1946        elif childSortOrder == u"mod_newest":
1947            # Retrieve relations as list of tuples (child, modifTime)
1948            relations = self.getChildRelationships(existingonly,
1949                    selfreference=False, withFields=("modified",),
1950                    excludeSet=excludeSet, includeSet=includeSet)
1951            relations.sort(_cmpNumbersItem1Rev)
1952            # Remove firstcharpos
1953            relations = [r[0] for r in relations]           
1954        else:
1955            # Retrieve relations as list of children words
1956            relations = self.getChildRelationships(existingonly,
1957                    selfreference=False, withFields=(),
1958                    excludeSet=excludeSet, includeSet=includeSet)
1959            if childSortOrder.startswith(u"desc"):
1960                coll = wikiDocument.getCollator()
1961
1962                def cmpLowerDesc(a, b):
1963                    return coll.strcoll(
1964                            b.lower(), a.lower())
1965                           
1966                # TODO Python 3.0 supports only key argument, no cmp. function
1967                relations.sort(cmpLowerDesc) # sort alphabetically
1968            elif childSortOrder.startswith(u"asc"):
1969                coll = wikiDocument.getCollator()
1970
1971                def cmpLowerAsc(a, b):
1972                    return coll.strcoll(
1973                            a.lower(), b.lower())
1974
1975                relations.sort(cmpLowerAsc)
1976
1977
1978
1979        priorized = []
1980        positioned = []
1981        other = []
1982
1983        # Put relations into their appropriate arrays
1984        for relation in relations:
1985            relationPage = wikiDocument.getWikiPageNoError(relation)
1986            attrs = relationPage.getAttributes()
1987            try:
1988                if (attrs.has_key(u'tree_position')):
1989                    positioned.append((int(attrs[u'tree_position'][-1]) - 1, relation))
1990                elif (attrs.has_key(u'priority')):
1991                    priorized.append((int(attrs[u'priority'][-1]), relation))
1992                else:
1993                    other.append(relation)
1994            except:
1995                other.append(relation)
1996               
1997        # Sort special arrays
1998        priorized.sort(key=lambda t: t[0])
1999        positioned.sort(key=lambda t: t[0])
2000
2001
2002        result = []
2003        ipr = 0
2004        ipo = 0
2005        iot = 0
2006
2007        for i in xrange(len(relations)):
2008            if ipo < len(positioned) and positioned[ipo][0] <= i:
2009                result.append(positioned[ipo][1])
2010                ipo += 1
2011                continue
2012           
2013            if ipr < len(priorized):
2014                result.append(priorized[ipr][1])
2015                ipr += 1
2016                continue
2017           
2018            if iot < len(other):
2019                result.append(other[iot])
2020                iot += 1
2021                continue
2022           
2023            # When reaching this, only positioned can have elements yet
2024            if ipo < len(positioned):
2025                result.append(positioned[ipo][1])
2026                ipo += 1
2027                continue
2028           
2029            raise InternalError("Empty relation sorting arrays")
2030       
2031
2032        return result
2033
2034
2035#         # TODO Remove aliases?
2036#     def _flatTreeHelper(self, page, deepness, excludeSet, includeSet, result,
2037#             unalias):
2038#         """
2039#         Recursive part of getFlatTree
2040#         """
2041# #         print "_flatTreeHelper1", repr((page.getWikiWord(), deepness, len(excludeSet)))
2042#
2043#         word = page.getWikiWord()
2044#         nonAliasWord = page.getNonAliasPage().getWikiWord()
2045#         excludeSet.add(nonAliasWord)
2046#
2047#         children = page.getChildRelationshipsTreeOrder(existingonly=True)
2048#
2049#         for word in children:
2050#             subpage = self.wikiDocument.getWikiPage(word)
2051#             nonAliasWord = subpage.getNonAliasPage().getWikiWord()
2052#             if nonAliasWord in excludeSet:
2053#                 continue
2054#             if unalias:
2055#                 result.append((nonAliasWord, deepness + 1))
2056#             else:
2057#                 result.append((word, deepness + 1))
2058#             
2059#             if includeSet is not None:
2060#                 includeSet.discard(word)
2061#                 includeSet.discard(nonAliasWord)
2062#                 if len(includeSet) == 0:
2063#                     return
2064#             
2065#             self._flatTreeHelper(subpage, deepness + 1, excludeSet, includeSet,
2066#                     result, unalias)
2067#
2068#
2069#     def getFlatTree(self, unalias=False, includeSet=None):
2070#         """
2071#         Returns a sequence of tuples (word, deepness) where the current
2072#         word is the first one with deepness 0.
2073#         The words may contain aliases, but no word appears twice neither
2074#         will both a word and its alias appear in the list.
2075#         unalias -- replace all aliases by their real word
2076#         TODO EXPLAIN FUNCTION !!!
2077#         """
2078#         word = self.getWikiWord()
2079#         nonAliasWord = self.getNonAliasPage().getWikiWord()
2080#
2081#         if unalias:
2082#             result = [(nonAliasWord, 0)]
2083#         else:
2084#             result = [(word, 0)]
2085#
2086#         if includeSet is not None:
2087#             includeSet.discard(word)
2088#             includeSet.discard(nonAliasWord)
2089#             if len(includeSet) == 0:
2090#                 return result
2091#
2092#         excludeSet = set()   # set((self.getWikiWord(),))
2093#
2094#         self._flatTreeHelper(self, 0, excludeSet, includeSet, result, unalias)
2095#
2096# #         print "getFlatTree", repr(result)
2097#
2098#         return result
2099
2100
2101    def getFlatTree(self, unalias=False, includeSet=None, maxdepth=-1,
2102            resetdepth=0):
2103        """
2104        Returns a sequence of tuples (word, deepness) where the current
2105        word is the first one with deepness 0.
2106        The words may contain aliases, but no word appears twice neither
2107        will both a word and its alias appear in the list.
2108       
2109        unalias -- if to replace all aliases by their real word
2110        maxdepth -- don't create entries deeper than that level (-1: no limit)
2111        resetdepth -- if entry outreaches maxdepth it is inserted later
2112                with level  resetdepth  (-1: entry isn't inserted anymore)
2113        """
2114        getUnAliasedWikiWord = self.getWikiDocument().getUnAliasedWikiWord
2115
2116        checkList = [(self.getWikiWord(), self.getNonAliasPage().getWikiWord(),
2117                0)]
2118
2119        mixins = collections.deque()
2120        resultSet = set()
2121        result = []
2122
2123        while True:
2124            if len(checkList) > 0:
2125                word, nonAliasWord, chLevel = checkList[-1]
2126
2127                if len(mixins) > 0 and mixins[-1][2] >= chLevel:
2128                    word, nonAliasWord, chLevel = mixins.pop()
2129                else:
2130                    del checkList[-1]
2131            else:
2132                if len(mixins) == 0:
2133                    break # Everything empty -> terminate
2134                else:
2135                    mixList = list(mixins)
2136                    mixList.reverse()
2137                    checkList.extend((w, naw, 0) for w, naw, l in mixList)
2138                    mixins.clear()
2139                    continue
2140
2141            if nonAliasWord in resultSet:
2142                continue
2143
2144            if maxdepth > -1 and chLevel > maxdepth:
2145                # Don't go deeper
2146                if resetdepth > -1:
2147                    mixins.appendleft((word, nonAliasWord, resetdepth))
2148                continue
2149
2150            if unalias:
2151                result.append((nonAliasWord, chLevel))
2152            else:
2153                result.append((word, chLevel))
2154
2155            resultSet.add(nonAliasWord)
2156
2157            if includeSet is not None:
2158                includeSet.discard(word)
2159                includeSet.discard(nonAliasWord)
2160                if len(includeSet) == 0:
2161                    return result
2162
2163
2164            page = self.getWikiDocument().getWikiPage(nonAliasWord)
2165            children = page.getChildRelationshipsTreeOrder(existingonly=True)
2166
2167            children = [(c, getUnAliasedWikiWord(c), chLevel + 1)
2168                    for c in children]
2169            children.reverse()
2170            checkList += children
2171
2172        return result
2173
2174
2175
2176    def getDependentDataBlocks(self):
2177        vo = self.getExistingVersionOverview()
2178       
2179        if vo is None:
2180            return []
2181       
2182        return vo.getDependentDataBlocks()
2183
2184
2185
2186
2187
2188# TODO: Maybe split into single classes for each tag
2189
2190class FunctionalPage(DataCarryingPage):
2191    """
2192    holds the data for a functional page. Such a page controls the behavior
2193    of the application or a special wiki
2194    """
2195    def __init__(self, wikiDocument, funcTag):
2196        DataCarryingPage.__init__(self, wikiDocument)
2197       
2198        if not isFuncTag(funcTag):
2199            raise BadFuncPageTagException(
2200                    _(u"Func. tag %s does not exist") % funcTag)
2201
2202        self.funcTag = funcTag
2203
2204        # does this page need to be saved?
2205        self.saveDirtySince = None  # None if not dirty or timestamp when it became dirty
2206        self.updateDirtySince = None
2207
2208
2209    def getWikiWord(self):
2210        return None
2211
2212    def getTitle(self):
2213        """
2214        Return human readable title of the page.
2215        """
2216        return u"<" + getHrNameForFuncTag(self.funcTag) + u">"
2217
2218
2219    def getFuncTag(self):
2220        """
2221        Return the functional tag of the page (a kind of filepath
2222        for the page)
2223        """
2224        return self.funcTag
2225
2226    def getUnifiedPageName(self):
2227        """
2228        Return the name of the unified name of the page, which is
2229        "wikipage/" + the wiki word for wiki pages or the functional tag
2230        for functional pages.
2231        """
2232        return self.funcTag
2233
2234
2235    def _loadGlobalPage(self, subtag):
2236        tbLoc = os.path.join(wx.GetApp().getGlobalConfigSubDir(),
2237                "[%s].wiki" % subtag)
2238        try:
2239            tbContent = loadEntireTxtFile(tbLoc)
2240            return fileContentToUnicode(lineendToInternal(tbContent))
2241        except:
2242            return u""
2243
2244
2245    def _loadDbSpecificPage(self, funcTag):
2246        content = self.wikiDocument.getWikiData().retrieveDataBlockAsText(funcTag)
2247        if content is None:
2248            return u""
2249       
2250        return content
2251
2252#         if self.wikiDocument.isDefinedWikiWord(subtag):
2253#             return self.wikiDocument.getWikiData().getContent(subtag)
2254#         else:
2255#             return u""
2256
2257
2258    def getLiveTextNoTemplate(self):
2259        """
2260        Return None if page isn't existing instead of creating an automatic
2261        live text (e.g. by template).
2262        Functional pages by definition exist always
2263        """
2264        return self.getLiveText()
2265
2266
2267    def getContent(self):
2268        if self.funcTag in (u"global/TextBlocks", u"global/PWL",
2269                u"global/CCBlacklist", u"global/FavoriteWikis"):
2270            return self._loadGlobalPage(self.funcTag[7:])
2271        elif self.funcTag in (u"wiki/TextBlocks", u"wiki/PWL",
2272                u"wiki/CCBlacklist"):
2273            return self._loadDbSpecificPage(self.funcTag)
2274
2275
2276    def getFormatDetails(self):
2277        """
2278        According to currently stored settings, return a
2279        ParseUtilities.WikiPageFormatDetails object to describe
2280        formatting.
2281       
2282        For functional pages this is normally no formatting
2283        """
2284        return ParseUtilities.WikiPageFormatDetails(noFormat=True)
2285
2286
2287    def getLivePageAstIfAvailable(self):
2288        return self.getLivePageAst()
2289
2290
2291    # TODO Checking with dieOnChange == True
2292    def getLivePageAst(self, fireEvent=True, dieOnChange=False,
2293            threadstop=DUMBTHREADSTOP):
2294        """
2295        The PageAst of a func. page is always a single "default" token
2296        containing the whole text.
2297        """
2298        with self.livePageAstBuildLock:
2299            threadstop.testRunning()
2300   
2301            pageAst = self.livePageAst
2302           
2303            if pageAst is not None:
2304                return pageAst
2305
2306            with self.textOperationLock:
2307                pageAst = buildSyntaxNode([buildSyntaxNode(
2308                        self.getLiveText(), 0, "plainText")], 0, "text")
2309
2310                threadstop.testRunning()
2311
2312                self.livePageAst = pageAst
2313
2314                return pageAst
2315
2316
2317
2318    def _saveGlobalPage(self, text, subtag):
2319        tbLoc = os.path.join(wx.GetApp().getGlobalConfigSubDir(),
2320                "[%s].wiki" % subtag)
2321
2322        writeEntireFile(tbLoc, text, True)
2323
2324
2325    def _saveDbSpecificPage(self, text, funcTag):
2326        if self.isReadOnlyEffect():
2327            return
2328
2329        wikiData = self.wikiDocument.getWikiData()
2330       
2331        if text == u"":
2332            wikiData.deleteDataBlock(funcTag)
2333        else:
2334            wikiData.storeDataBlock(funcTag, text,
2335                    storeHint=Consts.DATABLOCK_STOREHINT_EXTERN)
2336
2337
2338#         if self.wikiDocument.isDefinedWikiWord(subtag) and text == u"":
2339#             # Delete content
2340#             wikiData.deleteContent(subtag)
2341#         else:
2342#             if text != u"":
2343#                 wikiData.setContent(subtag, text)
2344
2345
2346    def _save(self, text, fireEvent=True):
2347        """
2348        Saves the content of current wiki page.
2349        """
2350        if self.isReadOnlyEffect():
2351            return
2352       
2353        with self.textOperationLock:
2354            # text = self.getLiveText()
2355   
2356            if self.funcTag in (u"global/TextBlocks", u"global/PWL",
2357                    u"global/CCBlacklist", u"global/FavoriteWikis"):
2358                self._saveGlobalPage(text, self.funcTag[7:])
2359            elif self.funcTag in (u"wiki/TextBlocks", u"wiki/PWL",
2360                    u"wiki/CCBlacklist"):
2361                self._saveDbSpecificPage(text, self.funcTag)
2362
2363            self.saveDirtySince = None
2364
2365
2366
2367    def initiateUpdate(self, fireEvent=True):
2368        """
2369        Update additional cached informations (attributes, todos, relations).
2370        Here it is done directly in initiateUpdate() because it doesn't need
2371        much work.
2372        """
2373        if self.isReadOnlyEffect():
2374            return
2375
2376        with self.textOperationLock:
2377            # clear the dirty flag
2378            self.updateDirtySince = None
2379   
2380            if fireEvent:
2381                if self.funcTag.startswith(u"wiki/"):
2382                    evtSource = self
2383                else:
2384                    evtSource = wx.GetApp()
2385   
2386                if self.funcTag in (u"global/TextBlocks", u"wiki/TextBlocks"):
2387                    # The text blocks for the text blocks submenu was updated
2388                    evtSource.fireMiscEventKeys(("updated func page", "updated page",
2389                            "reread text blocks needed"))
2390                elif self.funcTag in (u"global/PWL", u"wiki/PWL"):
2391                    # The personal word list (words to ignore by spell checker)
2392                    # was updated
2393                    evtSource.fireMiscEventKeys(("updated func page", "updated page",
2394                            "reread personal word list needed"))
2395                elif self.funcTag in (u"global/CCBlacklist", u"wiki/CCBlacklist"):
2396                    # The blacklist of camelcase words not to mark as wiki links
2397                    # was updated
2398                    evtSource.fireMiscEventKeys(("updated func page", "updated page",
2399                            "reread cc blacklist needed"))
2400                elif self.funcTag == u"global/FavoriteWikis":
2401                    # The list of favorite wikis was updated (there is no
2402                    # wiki-bound version of favorite wikis
2403                    evtSource.fireMiscEventKeys(("updated func page", "updated page",
2404                            "reread favorite wikis needed"))
2405
2406    def isReadOnlyEffect(self):
2407        """
2408        Return true if page is effectively read-only, this means
2409        "for any reason", regardless if error or intention.
2410        Global func. pages do not depend on the wiki state so they are writable.
2411        """
2412        if self.funcTag.startswith(u"global/"):
2413            # Global pages are not stored in the wiki and are always writable
2414            return False
2415        else:
2416            return DataCarryingPage.isReadOnlyEffect(self)
2417
2418
2419    def getPresentation(self):
2420        """Dummy"""
2421        return (0, 0, 0, 0, 0)
2422
2423    def setPresentation(self, data, startPos):
2424        """Dummy"""
2425        pass
2426       
2427
2428
2429
2430# Two search helpers for WikiPage.getChildRelationshipsTreeOrder
2431
2432def _floatToCompInt(f):
2433    if f > 0:
2434        return 1
2435    elif f < 0:
2436        return -1
2437    else:
2438        return 0
2439
2440
2441
2442# TODO: Remove for Python 3.0
2443def _cmpNumbersItem1(a, b):
2444    """
2445    Compare "natural", means using the char. positions or moddates of
2446    the links in page.
2447    """
2448    return _floatToCompInt(a[1] - b[1])
2449
2450
2451def _cmpNumbersItem1Rev(a, b):
2452    """
2453    Compare "natural", means using the char. positions or moddates of
2454    the links in page.
2455    """
2456    return _floatToCompInt(b[1] - a[1])
2457
2458
2459
2460_FUNCTAG_TO_HR_NAME_MAP = {
2461            u"global/TextBlocks": N_(u"Global text blocks"),
2462            u"wiki/TextBlocks": N_(u"Wiki text blocks"),
2463            u"global/PWL": N_(u"Global spell list"),
2464            u"wiki/PWL": N_(u"Wiki spell list"),
2465            u"global/CCBlacklist": N_(u"Global cc. blacklist"),
2466            u"wiki/CCBlacklist": N_(u"Wiki cc. blacklist"),
2467            u"global/FavoriteWikis": N_(u"Favorite wikis"),
2468        }
2469
2470
2471def getHrNameForFuncTag(funcTag):
2472    """
2473    Return the human readable name of functional page with tag funcTag.
2474    """
2475    return _(_FUNCTAG_TO_HR_NAME_MAP.get(funcTag, funcTag))
2476   
2477
2478def getFuncTags():
2479    """
2480    Return all available func tags
2481    """
2482    return _FUNCTAG_TO_HR_NAME_MAP.keys()
2483
2484
2485def isFuncTag(funcTag):
2486    return _FUNCTAG_TO_HR_NAME_MAP.has_key(funcTag)
Note: See TracBrowser for help on using the browser.