root/branches/mbutscher/work/extensions/wikidPadParser/WikidPadParser.py @ 275

Revision 275, 79.2 kB (checked in by mbutscher, 2 years ago)

branches/stable-2.1:
* Internal: Bug fixed: Error in error handling for plugins
* Internal: Bug fixed: Some functions in plugins were not registered

for calling

* Bug fixed: WikidPad can't be closed if volume access lost during session

branches/mbutscher/work:
* Internal: Bug fixed: Error in error handling for plugins
* Internal: Bug fixed: Some functions in plugins were not registered

for calling

* Grammar change to forbid bold and italics to span over a heading
* Bug fixed: WikidPad can't be closed if volume access lost during session

Line 
1## import hotshot
2## _prof = hotshot.Profile("hotshot.prf")
3
4# Official parser plugin for wiki language "WikidPad default 2.0"
5# Last modified (format YYYY-MM-DD): 2011-06-02
6
7
8import locale, pprint, time, sys, string, traceback
9
10from textwrap import fill
11
12import wx
13
14import re    # from pwiki.rtlibRepl import re
15from pwiki.WikiExceptions import *
16from pwiki.StringOps import UPPERCASE, LOWERCASE, revStr, urlFromPathname, \
17        urlQuoteSpecific
18
19from pwiki.WikiDocument import WikiDocument
20from pwiki.OptionsDialog import PluginOptionsPanel
21
22sys.stderr = sys.stdout
23
24locale.setlocale(locale.LC_ALL, '')
25
26from pwiki.WikiPyparsing import *
27
28
29WIKIDPAD_PLUGIN = (("WikiParser", 1),)
30
31WIKI_LANGUAGE_NAME = "wikidpad_default_2_0"
32WIKI_HR_LANGUAGE_NAME = u"WikidPad default 2.0"
33
34
35LETTERS = UPPERCASE + LOWERCASE
36
37
38# The specialized optimizer in WikiPyParsing can't handle automatic whitespace
39# removing
40ParserElement.setDefaultWhitespaceChars("")
41
42
43
44RE_FLAGS = re.DOTALL | re.UNICODE | re.MULTILINE
45
46
47class IndentInfo(object):
48    __slots__ = ("level", "type")
49    def __init__(self, type):
50        self.level = 0
51        self.type = type
52
53
54
55def buildRegex(regex, name=None, hideOnEmpty=False):
56    if name is None:
57        element = Regex(regex, RE_FLAGS)
58    else:
59        element = Regex(regex, RE_FLAGS).setResultsName(name).setName(name)
60   
61    if hideOnEmpty:
62        element.setParseAction(actionHideOnEmpty)
63       
64    return element
65
66stringEnd = buildRegex(ur"(?!.)", "stringEnd")
67
68
69
70def getFirstTerminalNode(t):
71    if t.getChildrenCount() == 0:
72        return None
73   
74    lt = t[-1]
75    if not isinstance(lt, TerminalNode):
76        return None
77       
78    return lt
79
80
81def actionHideOnEmpty(s, l, st, t):
82    if t.strLength == 0:
83        return []
84
85
86def actionCutRightWhitespace(s, l, st, t):
87    lt = getFirstTerminalNode(t)
88    if lt is None:
89        return None
90
91    txt = lt.getText()
92    for i in xrange(len(txt) - 1, -1, -1):
93        if txt[i] not in (u"\t", u" ", u"\n", u"\r"):
94            if i < len(txt) - 1:
95                lt.text = txt[:i+1]
96                lt.recalcStrLength()
97               
98                t2 = buildSyntaxNode(txt[i+1:], lt.pos + i + 1)
99                t.append(t2)
100            return t
101
102    return None
103
104 
105_CHECK_LEFT_RE = re.compile(ur"[ \t]*$", RE_FLAGS)
106
107
108def preActCheckNothingLeft(s, l, st, pe):
109    # Technically we have to look behind, but this is not supported very
110    # well by reg. ex., so we take the reverse string and look ahead
111    revText = st.revText
112    revLoc = len(s) - l
113#     print "--preActCheckNothingLeft4", repr((revLoc, revText[revLoc: revLoc+20]))
114    if not _CHECK_LEFT_RE.match(revText, revLoc):
115        raise ParseException(s, l, "left of block markup (e.g. table) not empty")
116
117
118def validateNonEmpty(s, l, st, t):
119    if t.strLength == 0:
120        raise ParseException(s, l, "matched token must not be empty")
121
122
123
124
125def precTest(s, l, st, pe):
126    print "--precTest", repr((l, st, type(pe)))
127
128
129
130def createCheckNotIn(tokNames):
131    tokNames = frozenset(tokNames)
132
133    def checkNoContain(s, l, st, pe):
134        for tn in tokNames:
135            if tn in st.nameStack[:-1]:
136#                 print "--notcontain exc", repr(st.nameStack[:-1]), tn
137                raise ParseException(s, l, "token '%s' is not allowed here" % tn)
138
139    return checkNoContain
140
141
142def pseudoActionFindMarkup(s, l, st, t):
143    if t.strLength == 0:
144        return []
145    t.name = "plainText"
146    return t
147
148
149
150
151
152# Forward definition of normal content and content in table cells, headings, ...
153content = Forward()
154oneLineContent = Forward()
155
156tableContentInCell = Forward().setResultsNameNoCopy("tableCell")
157headingContent = Forward().setResultsNameNoCopy("headingContent")
158todoContent = Forward().setResultsNameNoCopy("value")
159titleContent = Forward().setResultsNameNoCopy("title")
160characterAttributionContent = Forward().setResultsNameNoCopy("title")
161
162whitespace = buildRegex(ur"[ \t]*")
163whitespace = whitespace.setParseAction(actionHideOnEmpty)
164
165
166# The mode appendix for URLs and tables
167def actionModeAppendix(s, l, st, t):
168    entries = []
169
170    for entry in t.iterFlatByName("entry"):
171        key = entry.findFlatByName("key").getText()
172        data = entry.findFlatByName("data").getText()
173        entries.append((key, data))
174       
175    t.entries = entries
176    return t
177
178
179
180modeAppendixEntry = buildRegex(ur"(?![;\|\]])\S", "key") + buildRegex(ur"(?:(?![;\|\]])\S)*", "data")
181modeAppendixEntry = modeAppendixEntry.setResultsNameNoCopy("entry")
182modeAppendix = modeAppendixEntry + ZeroOrMore(buildRegex(ur";") + modeAppendixEntry)
183modeAppendix = modeAppendix.addParseAction(actionModeAppendix)
184
185
186
187
188# -------------------- Simple formatting --------------------
189
190EscapePlainCharPAT = ur"\\"
191
192
193escapedChar = buildRegex(EscapePlainCharPAT) + buildRegex(ur".", "plainText")
194
195italicsStart = buildRegex(ur"\b_")
196italicsStart = italicsStart.setParseStartAction(createCheckNotIn(("italics",)))
197
198italicsEnd = buildRegex(ur"_\b")
199
200italics = italicsStart + characterAttributionContent + italicsEnd
201italics = italics.setResultsNameNoCopy("italics").setName("italics")
202
203boldStart = buildRegex(ur"\*(?=\S)")
204boldStart = boldStart.setParseStartAction(createCheckNotIn(("bold",)))
205
206boldEnd = buildRegex(ur"\*")
207
208bold = boldStart + characterAttributionContent + boldEnd
209bold = bold.setResultsNameNoCopy("bold").setName("bold")
210
211
212script = buildRegex(ur"<%") + buildRegex(ur".*?(?=%>)", "code") + \
213        buildRegex(ur"%>")
214script = script.setResultsNameNoCopy("script")
215
216horizontalLine = buildRegex(ur"----+[ \t]*$", "horizontalLine")\
217        .setParseStartAction(preActCheckNothingLeft)
218
219
220# -------------------- HTML --------------------
221
222htmlTag = buildRegex(ur"</?[A-Za-z][A-Za-z0-9:]*(?:/| [^\n>]*)?>", "htmlTag")
223
224htmlEntity = buildRegex(
225        ur"&(?:[A-Za-z0-9]{2,10}|#[0-9]{1,10}|#x[0-9a-fA-F]{1,8});",
226        "htmlEntity")
227
228
229
230# -------------------- Heading --------------------
231
232def actionHeading(s, l, st, t):
233    t.level = len(t[0].getText())
234    t.contentNode = t.findFlatByName("headingContent")
235    if t.contentNode is None:
236        raise ParseException(s, l, "a heading needs content")
237
238headingEnd = buildRegex(ur"\n")
239
240heading = buildRegex(ur"^\+{1,15}(?!\+)") + Optional(buildRegex(ur" ")) + \
241        headingContent + headingEnd
242heading = heading.setResultsNameNoCopy("heading").setParseAction(actionHeading)
243
244
245
246# -------------------- Todo-Entry --------------------
247
248def actionTodoEntry(s, l, st, t):
249    t.key = t.findFlatByName("key").getString()
250    t.keyComponents = t.key.split(u".")
251    t.delimiter = t.findFlatByName("todoDelimiter").getString()
252    t.valueNode = t.findFlatByName("value")
253    t.todos = [(t.key, t.valueNode)]
254
255
256
257todoKey = buildRegex(ur"\b(?:todo|done|wait|action|track|issue|"
258        ur"question|project)(?:\.[^:\s]+)?", "key")
259# todoKey = todoKey.setParseStartAction(preActCheckNothingLeft)
260
261todoEnd = buildRegex(ur"\n|\||(?!.)")
262
263todoEntry = todoKey + buildRegex(ur":", "todoDelimiter") + todoContent + \
264        Optional(buildRegex(ur"\|"))
265
266todoEntry = todoEntry.setResultsNameNoCopy("todoEntry")\
267        .setParseAction(actionTodoEntry)
268
269# Only for LanguageHelper.parseTodoEntry()
270todoAsWhole = todoEntry + stringEnd
271
272# -------------------- Indented text, (un)ordered list --------------------
273
274def validateLessIndent(s, l, st, t):
275    if t.strLength >= st.dictStack["indentInfo"].level:
276        raise ParseException(s, l, "expected less indentation")
277
278
279def validateMoreIndent(s, l, st, t):
280    if t.strLength <= st.dictStack["indentInfo"].level:
281        raise ParseException(s, l, "expected more indentation")
282
283
284def validateEqualIndent(s, l, st, t):
285    if t.strLength > st.dictStack["indentInfo"].level:
286        raise ParseException(s, l, "expected equal indentation, but more found")
287    if t.strLength < st.dictStack["indentInfo"].level:
288        raise ParseException(s, l, "expected equal indentation, but less found")
289
290
291def validateEquivalIndent(s, l, st, t):
292    if t.strLength > st.dictStack["indentInfo"].level and \
293            st.dictStack["indentInfo"].type == "normal":
294        raise ParseException(s, l, "expected equival. indentation, but more found")
295    if t.strLength < st.dictStack["indentInfo"].level:
296        raise ParseException(s, l, "expected equival. indentation, but less found")
297
298
299def validateInmostIndentNormal(s, l, st, t):
300    if st.dictStack["indentInfo"].type != "normal":
301        raise ParseException(s, l, 'Inmost indentation not "normal"')
302
303
304def actionResetIndent(s, l, st, t):
305    st.dictStack.getSubTopDict()["lastIdentation"] = 0
306
307
308def actionIndent(s, l, st, t):
309    st.dictStack.getSubTopDict()["lastIdentation"] = t.strLength
310
311
312def actionListStartIndent(s, l, st, t):
313    if t.strLength <= st.dictStack["indentInfo"].level:
314        raise ParseException(s, l, "expected list start indentation, but less or equal found")
315
316    return actionIndent(s, l, st, t)
317
318
319def actionMoreIndent(s, l, st, t):
320    """
321    Called for more indentation before a general indented text.
322    """
323    newIdInfo = IndentInfo("normal")
324   
325    result = actionIndent(s, l, st, t)
326
327    newIdInfo.level = st.dictStack.getSubTopDict().get("lastIdentation", 0)
328    st.dictStack.getSubTopDict()["indentInfo"] = newIdInfo
329   
330    return result
331
332
333
334def preActNewLinesParagraph(s, l, st, pe):
335    if "preHtmlTag" in st.nameStack:
336        raise ParseException(s, l, "Newlines aren't paragraph inside <pre> tag")
337   
338    wikiFormatDetails = st.dictStack["wikiFormatDetails"]
339    if not wikiFormatDetails.paragraphMode:
340        raise ParseException(s, l, "Newlines are only paragraph in paragraph mode")
341
342
343def preActNewLineLineBreak(s, l, st, pe):
344    if "preHtmlTag" in st.nameStack:
345        raise ParseException(s, l, "Newline isn't line break inside <pre> tag")
346   
347    wikiFormatDetails = st.dictStack["wikiFormatDetails"]
348    if wikiFormatDetails.paragraphMode:
349        raise ParseException(s, l, "Newline isn't line break in paragraph mode")
350
351
352def preActNewLineWhitespace(s, l, st, pe):
353    if "preHtmlTag" in st.nameStack:
354        raise ParseException(s, l, "Newline isn't whitespace inside <pre> tag")
355   
356    wikiFormatDetails = st.dictStack["wikiFormatDetails"]
357    if not wikiFormatDetails.paragraphMode:
358        raise ParseException(s, l, "Newline is only whitespace in paragraph mode")
359
360
361
362
363def preActUlPrepareStack(s, l, st, pe):
364    oldIdInfo = st.dictStack.getSubTopDict().get("indentInfo")
365    newIdInfo = IndentInfo("ul")
366
367    newIdInfo.level = st.dictStack.get("lastIdentation", 0)
368    st.dictStack["indentInfo"] = newIdInfo
369
370def preActOlPrepareStack(s, l, st, pe):
371    oldIdInfo = st.dictStack.getSubTopDict().get("indentInfo")
372    newIdInfo = IndentInfo("ol")
373
374    newIdInfo.level = st.dictStack.get("lastIdentation", 0)
375    st.dictStack["indentInfo"] = newIdInfo
376
377
378
379def inmostIndentChecker(typ):
380    def startAction(s, l, st, pe):
381        if st.dictStack["indentInfo"].type != typ:
382            raise ParseException(s, l, 'Expected inmost indent type "%s"' % typ)
383
384    return startAction
385
386
387
388# Only an empty line
389fakeIndentation = buildRegex(ur"^[ \t]+$")
390
391newLine = buildRegex(ur"\n") + Optional(fakeIndentation)
392
393
394
395newLinesParagraph = newLine + OneOrMore(newLine)
396newLinesParagraph = newLinesParagraph.setResultsNameNoCopy("newParagraph")\
397        .setParseStartAction(preActNewLinesParagraph)\
398        .setParseAction(actionResetIndent)
399
400
401newLineLineBreak = newLine
402newLineLineBreak = newLineLineBreak.setResultsName("lineBreak")\
403        .setParseStartAction(preActNewLineLineBreak)\
404        .setParseAction(actionResetIndent)
405
406
407newLineWhitespace = newLine
408newLineWhitespace = newLineWhitespace.setResultsName("whitespace")\
409        .setParseStartAction(preActNewLineWhitespace)
410
411
412moreIndentation = buildRegex(ur"^[ \t]*(?!\n)").setValidateAction(validateMoreIndent)
413moreIndentation = moreIndentation.setParseStartAction(validateInmostIndentNormal).\
414        setParseAction(actionMoreIndent).setName("moreIndentation")
415
416equalIndentation = buildRegex(ur"^[ \t]*(?!\n)").setValidateAction(validateEqualIndent)
417equalIndentation = equalIndentation.setParseAction(actionIndent).\
418        setName("equalIndentation")
419
420lessIndentation = buildRegex(ur"^[ \t]*(?!\n)").setValidateAction(validateLessIndent)
421lessIndentation = lessIndentation.setParseAction(actionIndent).\
422        setName("lessIndentation")
423
424lessIndentOrEnd = stringEnd | lessIndentation
425lessIndentOrEnd = lessIndentOrEnd.setName("lessIndentOrEnd")
426
427
428equivalIndentation = buildRegex(ur"^[ \t]+(?!\n)").setValidateAction(validateEquivalIndent)
429equivalIndentation = equivalIndentation.setParseAction(actionIndent).\
430        setName("equivalIndentation")
431
432
433indentedText = moreIndentation + content + FollowedBy(lessIndentOrEnd)
434indentedText = indentedText.setResultsNameNoCopy("indentedText")
435
436
437listStartIndentation  = buildRegex(ur"^[ \t]*")
438listStartIndentation = listStartIndentation.\
439        setParseAction(actionListStartIndent).setName("listStartIndentation")
440
441
442bullet = buildRegex(ur"\*[ \t]", "bullet")
443
444bulletEntry = equalIndentation.copy()\
445        .addParseStartAction(inmostIndentChecker("ul")) + bullet  # + \
446       
447
448
449unorderedList = listStartIndentation + bullet + \
450        (content + FollowedBy(lessIndentOrEnd))\
451        .addParseStartAction(preActUlPrepareStack)
452unorderedList = unorderedList.setResultsNameNoCopy("unorderedList")
453
454
455number = buildRegex(ur"(?:\d+\.)*(\d+)\.[ \t]|#[ \t]", "number")
456
457numberEntry = equalIndentation.copy()\
458        .addParseStartAction(inmostIndentChecker("ol")) + number
459
460
461
462orderedList = listStartIndentation + number + \
463        (content + FollowedBy(lessIndentOrEnd))\
464        .addParseStartAction(preActOlPrepareStack)
465orderedList = orderedList.setResultsNameNoCopy("orderedList")
466
467
468
469
470# -------------------- Table --------------------
471
472
473tableEnd = buildRegex(ur"^[ \t]*>>[ \t]*(?:\n|$)")
474newRow = buildRegex(ur"\n")
475
476newCellBar = buildRegex(ur"\|")
477newCellTab = buildRegex(ur"\t")
478
479
480def chooseCellEnd(s, l, st, pe):
481    """
482    """
483    if st.dictStack.get("table.tabSeparated", False):
484        return newCellTab
485    else:
486        return newCellBar
487
488
489newCell = Choice([newCellBar, newCellTab], chooseCellEnd)
490
491
492tableRow = tableContentInCell + ZeroOrMore(newCell + tableContentInCell)
493tableRow = tableRow.setResultsNameNoCopy("tableRow").setParseAction(actionHideOnEmpty)
494
495
496def actionTableModeAppendix(s, l, st, t):
497    for key, data in t.entries:
498        if key == "t":
499            st.dictStack.getNamedDict("table")["table.tabSeparated"] = True
500            return
501
502    st.dictStack.getNamedDict("table")["table.tabSeparated"] = False
503
504
505tableModeAppendix = modeAppendix.setResultsName("tableModeAppendix").addParseAction(actionTableModeAppendix)
506
507table = buildRegex(ur"<<\|").setParseStartAction(preActCheckNothingLeft) + \
508        Optional(tableModeAppendix) + buildRegex(ur"[ \t]*\n") + tableRow + ZeroOrMore(newRow + tableRow) + tableEnd
509table = table.setResultsNameNoCopy("table")
510
511
512
513# -------------------- Suppress highlighting and no export --------------------
514
515suppressHighlightingMultipleLines = buildRegex(ur"<<[ \t]*\n")\
516        .setParseStartAction(preActCheckNothingLeft) + \
517        buildRegex(ur".*?(?=^[ \t]*>>[ \t]*(?:\n|$))", "plainText") + \
518        buildRegex(ur"^[ \t]*>>[ \t]*(?:\n|$)")
519
520suppressHighlightingSingleLine = buildRegex(ur"<<") + \
521        buildRegex(ur"[^\n]*?(?=>>)", "plainText") + buildRegex(ur">>")
522
523# suppressHighlighting = suppressHighlightingMultipleLines | suppressHighlightingSingleLine
524
525
526
527
528# -------------------- No export area--------------------
529
530def actionNoExport(s, l, st, t):
531    # Change name to reduce work when interpreting
532    t.name = "noExport"
533
534
535
536noExportMultipleLinesEnd = buildRegex(ur"^[ \t]*>>[ \t]*(?:\n|$)")
537noExportSingleLineEnd = buildRegex(ur">>")
538
539
540noExportMultipleLines = buildRegex(ur"<<hide[ \t]*\n")\
541        .setParseStartAction(preActCheckNothingLeft,
542        createCheckNotIn(("noExportMl", "noExportSl"))) + \
543        content + noExportMultipleLinesEnd
544noExportMultipleLines = noExportMultipleLines.setResultsNameNoCopy("noExportMl")\
545        .setParseAction(actionNoExport)
546
547noExportSingleLine = buildRegex(ur"<<hide[ \t]") + oneLineContent + \
548        noExportSingleLineEnd
549noExportSingleLine = noExportSingleLine.setResultsNameNoCopy("noExportSl")\
550        .setParseStartAction(
551        createCheckNotIn(("noExportMl", "noExportSl")))\
552        .setParseAction(actionNoExport)
553
554
555
556# -------------------- Pre block --------------------
557
558preBlock = buildRegex(ur"<<pre[ \t]*\n")\
559        .setParseStartAction(preActCheckNothingLeft) + \
560        buildRegex(ur".*?(?=^[ \t]*>>[ \t]*(?:\n|$))", "preText") + \
561        buildRegex(ur"^[ \t]*>>[ \t]*(?:\n|$)")
562preBlock = preBlock.setResultsNameNoCopy("preBlock")
563
564
565# -------------------- Auto generated area --------------------
566# TODO
567
568# autoGeneratedArea = buildRegex(ur"<<[ \t]+")\
569#         .setParseStartAction(preActCheckNothingLeft) + \
570#         buildRegex(ur"[^\n]+", "expression") + \
571#         buildRegex(ur"\n") + buildRegex(ur".*?(?=>>)", "plainText") + \
572#         buildRegex(ur">>[ \t]*$").setParseStartAction(preActCheckNothingLeft)
573
574
575
576
577# -------------------- <pre> html tag --------------------
578
579def actionPreHtmlTag(s, l, st, t):
580    """
581    Remove the node name so this doesn't become an own NTNode.
582    """
583    t.name = None
584
585
586
587preHtmlStart = buildRegex(ur"<pre(?: [^\n>]*)?>", "htmlTag")\
588        .setParseStartAction(createCheckNotIn(("preHtmlTag",)))
589
590preHtmlEnd = buildRegex(ur"</pre(?: [^\n>]*)?>", "htmlTag")
591
592preHtmlTag = preHtmlStart + content + preHtmlEnd
593preHtmlTag = preHtmlTag.setResultsNameNoCopy("preHtmlTag")\
594        .setParseAction(actionPreHtmlTag)
595
596
597
598# -------------------- Wikiwords and URLs --------------------
599
600BracketStart = u"["
601BracketStartPAT = ur"\["
602
603BracketEnd = u"]"
604BracketEndPAT = ur"\]"
605# WikiWordNccPAT = ur"/?(?:/?[^\\/\[\]\|\000-\037=:;#!]+)+" # ur"[\w\-\_ \t]+"
606
607# Single part of subpage path
608WikiWordPathPartPAT = ur"(?!\.\.)[^\\/\[\]\|\000-\037=:;#!]+"
609WikiPageNamePAT = WikiWordPathPartPAT + "(?:/" + WikiWordPathPartPAT + ")*"
610
611# Begins with dotted path parts which mean to go upward in subpage path
612WikiWordDottedPathPAT = ur"\.\.(/\.\.)*(?:/" + WikiWordPathPartPAT + ")*"
613WikiWordNonDottedPathPAT = ur"/{0,2}" + WikiPageNamePAT
614
615WikiWordNccPAT = WikiWordDottedPathPAT + ur"|" + WikiWordNonDottedPathPAT
616
617WikiWordTitleStartPAT = ur"\|"
618WikiWordAnchorStart = u"!"
619WikiWordAnchorStartPAT = ur"!"
620
621# Bracket start, escaped for reverse RE pattern (for autocompletion)
622BracketStartRevPAT = ur"\["
623# Bracket end, escaped for reverse RE pattern (for autocompletion)
624BracketEndRevPAT = ur"\]"
625
626WikiWordNccRevPAT = ur"[^\\\[\]\|\000-\037=:;#!]+?"  # ur"[\w\-\_ \t.]+?"
627
628
629
630WikiWordCcPAT = (ur"(?:[" +
631        UPPERCASE +
632        ur"]+[" +
633        LOWERCASE +
634        ur"]+[" +
635        UPPERCASE +
636        ur"]+[" +
637        LETTERS + string.digits +
638        ur"]*|[" +
639        UPPERCASE +
640        ur"]{2,}[" +
641        LOWERCASE +
642        ur"]+)")
643
644
645UrlPAT = ur'(?:(?:https?|ftp|rel)://|mailto:|Outlook:\S|wiki:/|file:/)'\
646        ur'(?:(?![.,;:!?)]+(?:["\s]|$))[^"\s|\]<>])*'
647
648
649UrlInBracketsPAT = ur'(?:(?:https?|ftp|rel)://|mailto:|Outlook:\S|wiki:/|file:/)'\
650        ur'(?:(?![ \t]+[|\]])(?: |[^"\s|\]<>]))*'
651
652
653bracketStart = buildRegex(BracketStartPAT)
654bracketEnd = buildRegex(BracketEndPAT)
655
656
657UnescapeExternalFragmentRE   = re.compile(ur"#(.)",
658                              re.DOTALL | re.UNICODE | re.MULTILINE)
659
660
661def reThrough(matchobj):
662    return matchobj.group(1)
663
664
665def actionSearchFragmentExtern(s, l, st, t):
666    """
667    Called to unescape external fragment of wikiword.
668    """
669    lt2 = getFirstTerminalNode(t)
670    if lt2 is None:
671        return None
672   
673    lt2.unescaped = UnescapeExternalFragmentRE.sub(ur"\1", lt2.text)
674
675
676UnescapeStandardRE = re.compile(EscapePlainCharPAT + ur"(.)",
677                              re.DOTALL | re.UNICODE | re.MULTILINE)
678
679def actionSearchFragmentIntern(s, l, st, t):
680    lt2 = getFirstTerminalNode(t)
681    if lt2 is None:
682        return None
683
684    lt2.unescaped = UnescapeStandardRE.sub(ur"\1", lt2.text)
685
686
687
688def resolveWikiWordLink(link, basePage):
689    """
690    If using subpages this is used to resolve a link to the right wiki word
691    relative to basePage on which the link is placed.
692    It returns the absolute link (page name).
693    """
694    return _TheHelper.resolvePrefixSilenceAndWikiWordLink(link, basePage)[2]
695   
696
697
698
699def actionWikiWordNcc(s, l, st, t):
700    t.wikiWord = t.findFlatByName("word")
701    if t.wikiWord is not None:
702        t.wikiWord = resolveWikiWordLink(t.wikiWord.getString(),
703                st.dictStack["wikiFormatDetails"].basePage)
704
705        if t.wikiWord == u"":
706            raise ParseException(s, l, "Subpage resolution of wikiword failed")
707
708    t.titleNode = t.findFlatByName("title")
709
710    t.fragmentNode = t.findFlatByName("searchFragment")
711    if t.fragmentNode is not None:
712        t.searchFragment = t.fragmentNode.unescaped
713    else:
714        t.searchFragment = None
715   
716    t.anchorLink = t.findFlatByName("anchorLink")
717    if t.anchorLink is not None:
718        t.anchorLink = t.anchorLink.getString()
719
720
721
722def preActCheckWikiWordCcAllowed(s, l, st, pe):
723    try:
724        wikiFormatDetails = st.dictStack["wikiFormatDetails"]
725       
726        if not wikiFormatDetails.withCamelCase:
727            raise ParseException(s, l, "CamelCase words not allowed here")
728    except KeyError:
729        pass
730
731
732def actionWikiWordCc(s, l, st, t):
733    t.wikiWord = t.findFlatByName("word")
734    if t.wikiWord is not None:
735        wikiFormatDetails = st.dictStack["wikiFormatDetails"]
736
737        t.wikiWord = resolveWikiWordLink(t.wikiWord.getString(),
738                wikiFormatDetails.basePage)
739
740        if t.wikiWord == u"":
741            raise ParseException(s, l, "Subpage resolution of wikiword failed")
742
743        try:
744#             wikiFormatDetails = st.dictStack["wikiFormatDetails"]
745           
746            if t.wikiWord in wikiFormatDetails.wikiDocument.getCcWordBlacklist():
747                raise ParseException(s, l, "CamelCase word is in blacklist")
748        except KeyError:
749            pass
750
751    t.titleNode = None
752
753    t.fragmentNode = t.findFlatByName("searchFragment")
754    if t.fragmentNode is not None:
755        t.searchFragment = t.fragmentNode.unescaped
756    else:
757        t.searchFragment = None
758
759    t.anchorLink = t.findFlatByName("anchorLink")
760    if t.anchorLink is not None:
761        t.anchorLink = t.anchorLink.getString()
762
763
764
765
766
767def actionExtractableWikiWord(s, l, st, t):
768    t.wikiWord = t.findFlatByName("word")
769    if t.wikiWord is not None:
770        t.wikiWord = t.wikiWord.getString()
771
772
773
774def actionUrlLink(s, l, st, t):
775    if t.name == "urlLinkBare":
776        t.bracketed = False
777    else:
778        t.bracketed = True
779   
780    t.name = "urlLink"       
781    t.appendixNode = t.findFlatByName("urlModeAppendix")
782    t.coreNode = t.findFlatByName("url")
783
784    # Valid URL but may differ from original input
785    t.url = urlQuoteSpecific(t.coreNode.getString(), ' ')
786    t.titleNode = t.findFlatByName("title")
787#     print "--actionUrlLink3", repr(t.url)
788
789
790def actionAnchorDef(s, l, st, t):
791    t.anchorLink = t.findFlatByName("anchor").getString()
792
793
794searchFragmentExtern = buildRegex(ur"#") + \
795        buildRegex(ur"(?:(?:#.)|[^ \t\n#])+", "searchFragment")\
796        .setParseAction(actionSearchFragmentExtern)
797
798searchFragmentIntern = buildRegex(ur"#") + buildRegex(ur"(?:(?:" + EscapePlainCharPAT +
799        ur".)|(?!" + WikiWordTitleStartPAT +
800        ur"|" +  BracketEndPAT + ur").)+", "searchFragment")\
801        .setParseAction(actionSearchFragmentIntern)
802
803wikiWordAnchorLink = buildRegex(WikiWordAnchorStartPAT) + \
804        buildRegex(ur"[A-Za-z0-9\_]+", "anchorLink")
805
806
807title = buildRegex(WikiWordTitleStartPAT + ur"[ \t]*") + titleContent    # content.setResultsName("title")
808
809
810wikiWordNccCore = buildRegex(WikiWordNccPAT, "word")
811
812wikiWordNcc = bracketStart + \
813        wikiWordNccCore.copy().addParseAction(actionCutRightWhitespace) + \
814        Optional(MatchFirst([searchFragmentIntern, wikiWordAnchorLink])) + whitespace + \
815        Optional(title) + bracketEnd + \
816        Optional(MatchFirst([searchFragmentExtern, wikiWordAnchorLink]))
817
818wikiWordNcc = wikiWordNcc.setResultsNameNoCopy("wikiWord").setName("wikiWordNcc")\
819        .setParseAction(actionWikiWordNcc)
820
821
822anchorDef = buildRegex(ur"^[ \t]*anchor:[ \t]*") + buildRegex(ur"[A-Za-z0-9\_]+",
823        "anchor") + buildRegex(ur"\n")
824anchorDef = anchorDef.setResultsNameNoCopy("anchorDef").setParseAction(actionAnchorDef)
825
826
827AnchorRE = re.compile(ur"^[ \t]*anchor:[ \t]*(?P<anchorValue>[A-Za-z0-9\_]+)\n",
828        re.DOTALL | re.UNICODE | re.MULTILINE)
829
830
831
832urlModeAppendix = modeAppendix.setResultsName("urlModeAppendix")
833
834urlWithAppend = buildRegex(UrlPAT, "url") + Optional(buildRegex(ur">") + \
835        urlModeAppendix)
836
837urlWithAppendInBrackets = buildRegex(UrlInBracketsPAT, "url") + Optional(buildRegex(ur">") + \
838        urlModeAppendix)
839
840
841urlBare = urlWithAppend.setResultsName("urlLinkBare")
842urlBare = urlBare.setParseAction(actionUrlLink)
843
844urlTitled = bracketStart + urlWithAppendInBrackets + whitespace + \
845        Optional(title) + bracketEnd
846# urlTitled = bracketStart + urlWithAppend + whitespace + \
847#         Optional(title) + bracketEnd
848urlTitled = urlTitled.setResultsNameNoCopy("urlLinkBracketed").setParseAction(actionUrlLink)
849
850
851
852urlRef = urlTitled | urlBare
853
854
855# TODO anchor/fragment
856wikiWordCc = buildRegex(ur"\b(?<!~)" + WikiWordCcPAT + ur"\b", "word") + \
857        Optional(MatchFirst([searchFragmentExtern, wikiWordAnchorLink])) # Group( )
858wikiWordCc = wikiWordCc.setResultsNameNoCopy("wikiWord").setName("wikiWordCc")\
859        .setParseStartAction(preActCheckWikiWordCcAllowed)\
860        .setParseAction(actionWikiWordCc)
861
862wikiWord = wikiWordNcc | wikiWordCc
863
864
865
866# Needed for _TheHelper.extractWikiWordFromLink()
867
868extractableWikiWord = (wikiWordNccCore | wikiWordNcc) + stringEnd
869extractableWikiWord = extractableWikiWord.setResultsNameNoCopy("extractableWikiWord")\
870        .setParseAction(actionExtractableWikiWord).optimize(("regexcombine",))\
871        .parseWithTabs()
872
873
874wikiPageNameRE = re.compile(ur"^" + WikiPageNamePAT + ur"$",
875        re.DOTALL | re.UNICODE | re.MULTILINE)
876
877
878wikiWordCcRE = re.compile(ur"^" + WikiWordCcPAT + ur"$",
879        re.DOTALL | re.UNICODE | re.MULTILINE)
880
881def isCcWikiWord(word):
882    return bool(wikiWordCcRE.match(word))
883
884
885wikiLinkCoreRE = re.compile(ur"^" + WikiWordNccPAT + ur"$",
886        re.DOTALL | re.UNICODE | re.MULTILINE)
887
888
889
890# -------------------- Footnotes --------------------
891
892footnotePAT = ur"[0-9]+"
893
894def preActCheckFootnotesAllowed(s, l, st, pe):
895    wikiFormatDetails = st.dictStack["wikiFormatDetails"]
896   
897    if wikiFormatDetails.wikiLanguageDetails.footnotesAsWws:
898        raise ParseException(s, l, "CamelCase words not allowed here")
899
900
901def actionFootnote(s, l, st, t):
902    t.footnoteId = t.findFlatByName("footnoteId").getString()
903
904
905footnote = bracketStart + buildRegex(footnotePAT, "footnoteId") + bracketEnd
906footnote = footnote.setResultsNameNoCopy("footnote")\
907        .setParseStartAction(preActCheckFootnotesAllowed)\
908        .setParseAction(actionFootnote)
909
910
911footnoteRE = re.compile(ur"^" + footnotePAT + ur"$",
912        re.DOTALL | re.UNICODE | re.MULTILINE)
913
914
915# -------------------- Attributes (=properties) and insertions --------------------
916
917
918def actionAttrInsValueQuoteStart(s, l, st, t):
919    st.dictStack.getSubTopDict()["attrInsValueQuote"] = t[0].text
920
921def actionAttrInsValueQuoteEnd(s, l, st, t):
922    if t[0].text != st.dictStack.getSubTopDict().get("attrInsValueQuote"):
923        raise ParseException(s, l, "End quote of attribute/insertion does not match start")
924
925
926def pseudoActionAttrInsQuotedValue(s, l, st, t):
927    if t.strLength == 0:
928        return []
929    t.name = "value"
930    return t
931
932
933def actionAttribute(s, l, st, t):
934    key = t.findFlatByName("key").getString()
935    t.key = key
936    t.keyComponents = t.key.split(u".")
937    t.attrs = [(key, vNode.getString()) for vNode in t.iterFlatByName("value")]
938
939
940def actionInsertion(s, l, st, t):
941    t.key = t.findFlatByName("key").getString()
942    t.keyComponents = t.key.split(u".")
943    values = list(vNode.getString() for vNode in t.iterFlatByName("value"))
944    t.value = values[0]
945    del values[0]
946    t.appendices = values
947
948
949
950attrInsQuote = buildRegex(ur"\"+|'+|/+|\\+")
951attrInsQuoteStart = attrInsQuote.copy()\
952        .setParseAction(actionAttrInsValueQuoteStart)
953attrInsQuoteEnd = attrInsQuote.copy()\
954        .setParseAction(actionAttrInsValueQuoteEnd)
955
956attrInsQuotedValue = FindFirst([], attrInsQuoteEnd)\
957        .setPseudoParseAction(pseudoActionAttrInsQuotedValue)
958
959# attrInsNonQuotedValue = buildRegex(ur"[\w\-\_ \t:,.!?#%|/]*", "value")
960attrInsNonQuotedValue = buildRegex(ur"(?:[ \t]*[\w\-\_:,.!?#%|/]+)*", "value")
961
962
963attrInsValue = whitespace + ((attrInsQuoteStart + attrInsQuotedValue + \
964        attrInsQuoteEnd) | attrInsNonQuotedValue)
965
966attrInsKey = buildRegex(ur"[\w\-\_\.]+", "key")
967
968attribute = bracketStart + whitespace + attrInsKey + \
969        buildRegex(ur"[ \t]*[=:]") + attrInsValue + \
970        ZeroOrMore(buildRegex(ur";") + attrInsValue) + whitespace + bracketEnd
971attribute = attribute.setResultsNameNoCopy("attribute").setParseAction(actionAttribute)
972
973
974insertion = bracketStart + buildRegex(ur":") + whitespace + attrInsKey + \
975        buildRegex(ur"[ \t]*[=:]") + attrInsValue + \
976        ZeroOrMore(buildRegex(ur";") + attrInsValue) + whitespace + bracketEnd
977insertion = insertion.setResultsNameNoCopy("insertion").setParseAction(actionInsertion)
978
979
980
981# -------------------- Additional regexes to provide --------------------
982
983
984# Needed for auto-bullet/auto-unbullet functionality of editor
985BulletRE        = re.compile(ur"^(?P<indentBullet>[ \t]*)(?P<actualBullet>\*[ \t])",
986        re.DOTALL | re.UNICODE | re.MULTILINE)
987NumericSimpleBulletRE = re.compile(ur"^(?P<indentBullet>[ \t]*)(?P<actualBullet>#[ \t])",
988        re.DOTALL | re.UNICODE | re.MULTILINE)
989NumericBulletRE = re.compile(ur"^(?P<indentNumeric>[ \t]*)(?P<preLastNumeric>(?:\d+\.)*)(\d+)\.[ \t]",
990        re.DOTALL | re.UNICODE | re.MULTILINE)
991
992
993# Needed for handleRewrapText
994EmptyLineRE     = re.compile(ur"^[ \t\r\n]*$",
995        re.DOTALL | re.UNICODE | re.MULTILINE)
996
997
998
999
1000# Reverse REs for autocompletion
1001revSingleWikiWord    =       (ur"(?:[" +
1002                             LETTERS + string.digits +
1003                             ur"]*[" +
1004                             UPPERCASE+
1005                             ur"])")   # Needed for auto-completion
1006
1007RevWikiWordRE      = re.compile(ur"^" +
1008                             revSingleWikiWord + ur"(?![\~])\b",
1009                             re.DOTALL | re.UNICODE | re.MULTILINE)
1010                             # Needed for auto-completion
1011
1012
1013RevWikiWordRE2     = re.compile(ur"^" + WikiWordNccRevPAT + BracketStartRevPAT,
1014        re.DOTALL | re.UNICODE | re.MULTILINE)  # Needed for auto-completion
1015
1016RevAttributeValue     = re.compile(
1017        ur"^([\w\-\_ \t:;,.!?#/|]*?)([ \t]*[=:][ \t]*)([\w\-\_ \t\.]+?)" +
1018        BracketStartRevPAT,
1019        re.DOTALL | re.UNICODE | re.MULTILINE)  # Needed for auto-completion
1020
1021
1022RevTodoKeyRE = re.compile(ur"^(?:[^:\s]{0,40}\.)??"
1023        ur"(?:odot|enod|tiaw|noitca|kcart|eussi|noitseuq|tcejorp)",
1024        re.DOTALL | re.UNICODE | re.MULTILINE)  # Needed for auto-completion
1025
1026RevTodoValueRE = re.compile(ur"^[^\n:]{0,30}:" + RevTodoKeyRE.pattern[1:],
1027        re.DOTALL | re.UNICODE | re.MULTILINE)  # Needed for auto-completion
1028
1029
1030RevWikiWordAnchorRE = re.compile(ur"^(?P<anchorBegin>[A-Za-z0-9\_]{0,20})" +
1031        WikiWordAnchorStartPAT + ur"(?P<wikiWord>" + RevWikiWordRE.pattern[1:] + ur")",
1032        re.DOTALL | re.UNICODE | re.MULTILINE)  # Needed for auto-completion
1033       
1034RevWikiWordAnchorRE2 = re.compile(ur"^(?P<anchorBegin>[A-Za-z0-9\_]{0,20})" +
1035        WikiWordAnchorStartPAT + BracketEndRevPAT + ur"(?P<wikiWord>" +
1036        WikiWordNccRevPAT + ur")" + BracketStartRevPAT,
1037        re.DOTALL | re.UNICODE | re.MULTILINE)  # Needed for auto-completion
1038
1039
1040# Simple todo RE for autocompletion.
1041ToDoREWithCapturing = re.compile(ur"^([^:\s]+):[ \t]*(.+?)$",
1042        re.DOTALL | re.UNICODE | re.MULTILINE)
1043
1044
1045
1046# For auto-link mode relax
1047AutoLinkRelaxSplitRE = re.compile(r"[\W]+", re.IGNORECASE | re.UNICODE)
1048
1049AutoLinkRelaxJoinPAT = ur"[\W]+"
1050AutoLinkRelaxJoinFlags = re.IGNORECASE | re.UNICODE
1051
1052
1053
1054# For spell checking
1055TextWordRE = re.compile(ur"(?P<negative>[0-9]+|"+ UrlPAT + u"|\b(?<!~)" +
1056        WikiWordCcPAT + ur"\b)|\b[\w']+",
1057        re.DOTALL | re.UNICODE | re.MULTILINE)
1058
1059
1060
1061
1062
1063# -------------------- End tokens --------------------
1064
1065
1066TOKEN_TO_END = {
1067        "bold": boldEnd,
1068        "italics": italicsEnd,
1069        "unorderedList": lessIndentOrEnd,
1070        "orderedList": lessIndentOrEnd,
1071        "indentedText": lessIndentOrEnd,
1072        "wikiWord": bracketEnd,
1073        "urlLinkBracketed": bracketEnd,
1074        "table": tableEnd,
1075        "preHtmlTag": preHtmlEnd,
1076        "heading": headingEnd,
1077        "todoEntry": todoEnd,
1078        "noExportMl": noExportMultipleLinesEnd,
1079        "noExportSl": noExportSingleLineEnd
1080    }
1081
1082
1083def chooseEndToken(s, l, st, pe):
1084    """
1085    """
1086    for tokName in reversed(st.nameStack):
1087        end = TOKEN_TO_END.get(tokName)
1088        if end is not None:
1089            return end
1090
1091    return stringEnd
1092
1093
1094endToken = Choice([stringEnd]+TOKEN_TO_END.values(), chooseEndToken)
1095
1096endTokenInTable = endToken | newCell | newRow
1097
1098endTokenInTitle = endToken | buildRegex(ur"\n")
1099
1100endTokenInCharacterAttribution = endToken | heading
1101
1102
1103
1104# -------------------- Content definitions --------------------
1105
1106
1107findMarkupInCell = FindFirst([bold, italics, noExportSingleLine,
1108        suppressHighlightingSingleLine,
1109        urlRef, insertion, escapedChar, footnote, wikiWord,
1110        htmlTag, htmlEntity], endTokenInTable)
1111findMarkupInCell = findMarkupInCell.setPseudoParseAction(pseudoActionFindMarkup)
1112
1113temp = ZeroOrMore(NotAny(endTokenInTable) + findMarkupInCell)
1114temp = temp.leaveWhitespace().parseWithTabs()
1115tableContentInCell << temp
1116
1117
1118
1119findMarkupInTitle = FindFirst([bold, italics, noExportSingleLine,
1120        suppressHighlightingSingleLine,
1121        urlRef, insertion, escapedChar, footnote, htmlTag, htmlEntity],
1122        endTokenInTitle)
1123findMarkupInTitle = findMarkupInTitle.setPseudoParseAction(pseudoActionFindMarkup)
1124
1125temp = ZeroOrMore(NotAny(endTokenInTitle) + findMarkupInTitle)
1126temp = temp.leaveWhitespace().parseWithTabs()
1127titleContent << temp
1128
1129
1130
1131findMarkupInHeading = FindFirst([bold, italics, noExportSingleLine,
1132        suppressHighlightingSingleLine,
1133        urlRef, insertion, escapedChar, footnote, wikiWord, htmlTag,
1134        htmlEntity], endToken)
1135findMarkupInHeading = findMarkupInHeading.setPseudoParseAction(
1136        pseudoActionFindMarkup)
1137
1138temp = ZeroOrMore(NotAny(endToken) + findMarkupInHeading)
1139temp = temp.leaveWhitespace().parseWithTabs()
1140headingContent << temp
1141
1142
1143
1144findMarkupInTodo = FindFirst([bold, italics, noExportSingleLine,
1145        suppressHighlightingSingleLine,
1146        urlRef, attribute, insertion, escapedChar, footnote, wikiWord,
1147        htmlTag, htmlEntity], endToken)
1148findMarkupInTodo = findMarkupInTodo.setPseudoParseAction(
1149        pseudoActionFindMarkup)
1150
1151temp = OneOrMore(NotAny(endToken) + findMarkupInTodo)
1152temp = temp.leaveWhitespace().parseWithTabs()
1153todoContent << temp
1154oneLineContent << temp
1155
1156
1157
1158findMarkupInCharacterAttribution = FindFirst([bold, italics, noExportSingleLine,
1159        suppressHighlightingSingleLine, urlRef,
1160        attribute, insertion, escapedChar, footnote, wikiWord,
1161        newLinesParagraph, newLineLineBreak, newLineWhitespace,
1162        todoEntry, anchorDef, preHtmlTag, htmlTag,
1163        htmlEntity, bulletEntry, unorderedList, numberEntry, orderedList,
1164        indentedText, table, preBlock, noExportMultipleLines,
1165        suppressHighlightingMultipleLines, equivalIndentation],
1166        endTokenInCharacterAttribution)
1167findMarkupInCharacterAttribution = findMarkupInCharacterAttribution\
1168        .setPseudoParseAction(pseudoActionFindMarkup)
1169
1170temp = ZeroOrMore(NotAny(endTokenInCharacterAttribution) +
1171        findMarkupInCharacterAttribution)
1172temp = temp.leaveWhitespace().parseWithTabs()
1173characterAttributionContent << temp
1174
1175
1176
1177findMarkup = FindFirst([bold, italics, noExportSingleLine,
1178        suppressHighlightingSingleLine, urlRef,
1179        attribute, insertion, escapedChar, footnote, wikiWord,
1180        newLinesParagraph, newLineLineBreak, newLineWhitespace, heading,
1181        todoEntry, anchorDef, preHtmlTag, htmlTag,
1182        htmlEntity, bulletEntry, unorderedList, numberEntry, orderedList,
1183        indentedText, table, preBlock, noExportMultipleLines,
1184        suppressHighlightingMultipleLines,
1185        script, horizontalLine, equivalIndentation], endToken)
1186findMarkup = findMarkup.setPseudoParseAction(pseudoActionFindMarkup)
1187
1188
1189content << ZeroOrMore(NotAny(endToken) + findMarkup)  # .setResultsName("ZeroOrMore")
1190content = content.leaveWhitespace().setValidateAction(validateNonEmpty).parseWithTabs()
1191
1192
1193
1194text = content + stringEnd
1195
1196
1197# Run optimizer
1198
1199# Separate element for LanguageHelper.parseTodoEntry()
1200todoAsWhole = todoAsWhole.optimize(("regexcombine",)).parseWithTabs()
1201
1202# Whole text, optimizes subelements recursively
1203text = text.optimize(("regexcombine",)).parseWithTabs()
1204# text = text.parseWithTabs()
1205
1206
1207# text.setDebugRecurs(True)
1208
1209
1210
1211def _buildBaseDict(wikiDocument=None, formatDetails=None):
1212    if formatDetails is None:
1213        if wikiDocument is None:
1214            formatDetails = WikiDocument.getUserDefaultWikiPageFormatDetails()
1215            formatDetails.setWikiLanguageDetails(WikiLanguageDetails(None, None))
1216        else:
1217            formatDetails = wikiDocument.getWikiDefaultWikiPageFormatDetails()
1218
1219    return {"indentInfo": IndentInfo("normal"),
1220            "wikiFormatDetails": formatDetails
1221        }
1222
1223
1224
1225# -------------------- API for plugin WikiParser --------------------
1226# During beta state of the WikidPad version, this API isn't stable yet,
1227# so changes may occur!
1228
1229
1230class _TheParser(object):
1231    @staticmethod
1232    def reset():
1233        """
1234        Reset possible internal states of a (non-thread-safe) object for
1235        later reuse.
1236        """
1237        pass
1238
1239    @staticmethod
1240    def getWikiLanguageName():
1241        """
1242        Return the internal name of the wiki language implemented by this
1243        parser.
1244        """
1245        return WIKI_LANGUAGE_NAME
1246
1247
1248
1249    @staticmethod
1250    def _postProcessing(intLanguageName, content, formatDetails, pageAst,
1251            threadstop):
1252        """
1253        Do some cleanup after main parsing.
1254        Not part of public API.
1255        """
1256        autoLinkRelaxRE = None
1257        if formatDetails.autoLinkMode == u"relax":
1258            relaxList = formatDetails.wikiDocument.getAutoLinkRelaxInfo()
1259
1260            def recursAutoLink(ast):
1261                newAstNodes = []
1262                for node in ast.getChildren():
1263                    if isinstance(node, NonTerminalNode):
1264                        newAstNodes.append(recursAutoLink(node))
1265                        continue
1266   
1267                    if node.name == "plainText":
1268                        text = node.text
1269                        start = node.pos
1270                       
1271                        threadstop.testRunning()
1272                        while text != u"":
1273                            # The foundWordText is the text as typed in the page
1274                            # foundWord is the word as entered in database
1275                            # These two may differ (esp. in whitespaces)
1276                            foundPos = len(text)
1277                            foundWord = None
1278                            foundWordText = None
1279                           
1280                            # Search all regexes for the earliest match
1281                            for regex, word in relaxList:
1282                                match = regex.search(text)
1283                                if match:
1284                                    pos = match.start(0)
1285                                    if pos < foundPos:
1286                                        # Match is earlier than previous
1287                                        foundPos = pos
1288                                        foundWord = word
1289                                        foundWordText = match.group(0)
1290                                        if pos == 0:
1291                                            # Can't find a better match -> stop loop
1292                                            break
1293
1294                            # Add token for text before found word (if any)
1295                            preText = text[:foundPos]
1296                            if preText != u"":
1297                                newAstNodes.append(buildSyntaxNode(preText,
1298                                        start, "plainText"))
1299               
1300                                start += len(preText)
1301                                text = text[len(preText):]
1302                           
1303                            if foundWord is not None:
1304                                wwNode = buildSyntaxNode(
1305                                        [buildSyntaxNode(foundWordText, start, "word")],
1306                                        start, "wikiWord")
1307                                       
1308                                wwNode.searchFragment = None
1309                                wwNode.anchorLink = None
1310                                wwNode.wikiWord = foundWord
1311                                wwNode.titleNode = buildSyntaxNode(foundWordText, start, "plainText") # None
1312
1313                                newAstNodes.append(wwNode)
1314
1315                                inc = max(len(foundWordText), 1)
1316                                start += inc
1317                                text = text[inc:]
1318
1319                        continue
1320
1321                    newAstNodes.append(node)
1322
1323
1324                ast.sub = newAstNodes
1325   
1326                return ast
1327
1328            pageAst = recursAutoLink(pageAst)
1329       
1330        return pageAst
1331
1332    @staticmethod
1333    def parse(intLanguageName, content, formatDetails, threadstop):
1334        """
1335        Parse the  content  written in wiki language  intLanguageName  using
1336        formatDetails  and regularly call  threadstop.testRunning()  to
1337        raise exception if execution thread is no longer current parsing
1338        thread.
1339        """
1340
1341        if len(content) == 0:
1342            return buildSyntaxNode([], 0, "text")
1343
1344        if formatDetails.noFormat:
1345            return buildSyntaxNode([buildSyntaxNode(content, 0, "plainText")],
1346                    0, "text")
1347
1348        baseDict = _buildBaseDict(formatDetails=formatDetails)
1349
1350##         _prof.start()
1351        try:
1352            t = text.parseString(content, parseAll=True, baseDict=baseDict,
1353                    threadstop=threadstop)
1354            t = buildSyntaxNode(t, 0, "text")
1355
1356            t = _TheParser._postProcessing(intLanguageName, content, formatDetails,
1357                    t, threadstop)
1358
1359        finally:
1360##             _prof.stop()
1361            pass
1362
1363        return t
1364
1365THE_PARSER = _TheParser()
1366
1367
1368
1369
1370
1371class WikiLanguageDetails(object):
1372    """
1373    Stores state of wiki language specific options and allows to check if
1374    two option sets are equivalent.
1375    """
1376    __slots__ = ("__weakref__", "footnotesAsWws", "wikiDocument")
1377
1378    def __init__(self, wikiDocument, docPage):
1379        self.wikiDocument = wikiDocument
1380        if self.wikiDocument is None:
1381            # Set wiki-independent default values
1382            self.footnotesAsWws = False
1383        else:
1384            self.footnotesAsWws = self.wikiDocument.getWikiConfig().getboolean(
1385                    "main", "footnotes_as_wikiwords", False)
1386
1387    @staticmethod
1388    def getWikiLanguageName():
1389        return WIKI_LANGUAGE_NAME
1390
1391
1392    def isEquivTo(self, details):
1393        """
1394        Compares with other details object if both are "equivalent"
1395        """
1396        return self.getWikiLanguageName() == details.getWikiLanguageName() and \
1397                self.footnotesAsWws == details.footnotesAsWws
1398
1399
1400class _WikiLinkPath(object):
1401    __slots__ = ("upwardCount", "components")
1402    def __init__(self, link=None, pageName=None, upwardCount=-1,
1403            components=None):
1404        assert (link is None) or (pageName is None)
1405
1406        if pageName is not None:
1407            # Handle wiki word as absolute link
1408            self.upwardCount = -1
1409            self.components = pageName.split(u"/")
1410            return
1411
1412        if link is None:
1413            if components is None:
1414                components = []
1415
1416            self.upwardCount = upwardCount
1417            self.components = components
1418            return
1419       
1420        if link == u".":
1421            # Link to self
1422            self.upwardCount = 0
1423            self.components = []
1424            return
1425
1426        if link.startswith(u"//"):
1427            self.upwardCount = -1
1428            self.components = link[2:].split(u"/")
1429            return
1430       
1431        if link.startswith(u"/"):
1432            self.upwardCount = 0
1433            self.components = link[1:].split(u"/")
1434            return
1435
1436        comps = link.split(u"/")
1437
1438        for i in xrange(0, len(comps)):
1439            if comps[i] != "..":
1440                self.upwardCount = i + 1
1441                self.components = comps[i:]
1442                return
1443       
1444        self.upwardCount = len(comps)
1445        self.components = []
1446       
1447    def clone(self):
1448        result = _WikiLinkPath()
1449        result.upwardCount = self.upwardCount
1450        result.components = self.components[:]
1451       
1452        return result
1453
1454    def __repr__(self):
1455        return "_WikiLinkPath(upwardCount=%i, components=%s)" % \
1456                (self.upwardCount, repr(self.components))
1457
1458    def isAbsolute(self):
1459        return self.upwardCount == -1
1460       
1461    def join(self, otherPath):
1462        if otherPath.upwardCount == -1:
1463            self.upwardCount = -1
1464            self.components = otherPath.components[:]
1465            return
1466        elif otherPath.upwardCount == 0:
1467            self.components = self.components + otherPath.components
1468        else:
1469            if otherPath.upwardCount <= len(self.components):
1470                self.components = self.components[:-otherPath.upwardCount] + \
1471                        otherPath.components
1472            else:
1473                # Going back further than self was deep (eliminating
1474                # more components than self had)
1475
1476                if self.upwardCount == -1:
1477                    # Actually an error (going upward after already reaching root)
1478                    # TODO: Handle as error?
1479                    self.components = otherPath.components[:]
1480                else:
1481                    # Add up upwardCount of other path after subtracting
1482                    # number of own components because otherPath walked
1483                    # over them already
1484                    self.upwardCount += otherPath.upwardCount - \
1485                            len(self.components)
1486
1487                    self.components = otherPath.components[:]
1488
1489
1490    def getLinkCore(self):
1491        comps = u"/".join(self.components)
1492        if self.upwardCount == -1:
1493            return u"//" + comps
1494        elif self.upwardCount == 0:
1495            return u"/" + comps
1496        elif self.upwardCount == 1:
1497            return comps
1498        else:
1499            return u"/".join([u".."] * (self.upwardCount - 1)) + u"/" + comps
1500
1501
1502    def resolveWikiWord(self, basePath):
1503        if self.isAbsolute():
1504            # Absolute is checked separately so basePath can be None if
1505            # self is absolute
1506            return u"/".join(self.components)
1507
1508        absPath = basePath.joinTo(self)
1509        return u"/".join(absPath.components)
1510
1511
1512    def resolvePrefixSilenceAndWikiWordLink(self, basePath):
1513        """
1514        If using subpages this is used to resolve a link to the right wiki word
1515        for autocompletion. It returns a tuple (prefix, silence, pageName).
1516        Autocompletion now searches for all wiki words starting with pageName. For
1517        all found items it removes the first  silence  characters, prepends the  prefix
1518        instead and uses the result as suggestion for autocompletion.
1519       
1520        If prefix is None autocompletion is not possible.
1521        """
1522        if self.isAbsolute():
1523            return u"//", 0, self.resolveWikiWord(None)
1524
1525        assert basePath.isAbsolute()
1526       
1527        if len(self.components) == 0:
1528            # link path only consists of ".." -> autocompletion not possible
1529            if self.upwardCount == 0:
1530                return None, None, u"/".join(basePath.components)
1531
1532            return None, None, u"/".join(basePath.components[:-self.upwardCount])
1533
1534        if self.upwardCount == 0:
1535            return u"/", len(basePath.resolveWikiWord(None)) + 1, \
1536                    u"/".join(basePath.components + self.components)
1537
1538        def lenAddOne(s):
1539            return len(s) + 1 if s != "" else 0
1540
1541        if self.upwardCount == 1:
1542            return u"", \
1543                    lenAddOne(u"/".join(basePath.components[:-1])), \
1544                    u"/".join(basePath.components[:-1] + self.components)
1545
1546        return u"/".join([u".."] * (self.upwardCount - 1)) + u"/", \
1547                lenAddOne(u"/".join(basePath.components[:-self.upwardCount])), \
1548                u"/".join(basePath.components[:-self.upwardCount] +
1549                self.components)
1550
1551
1552
1553    def joinTo(self, otherPath):
1554        result = self.clone()
1555        result.join(otherPath)
1556        return result
1557
1558
1559
1560    @staticmethod
1561    def isAbsoluteLinkCore(linkCore):
1562        return linkCore.startswith(u"//")
1563
1564
1565    @staticmethod
1566    def getRelativePathByAbsPaths(targetAbsPath, baseAbsPath,
1567            downwardOnly=True):
1568        """
1569        Create a link to targetAbsPath relative to baseAbsPath.
1570        If downwardOnly is False, the link may contain parts to go to parents
1571            or siblings
1572        in path (in this wiki language, ".." are used for this).
1573        If downwardOnly is True, the function may return None if a relative
1574        link can't be constructed.
1575        """
1576        assert targetAbsPath.isAbsolute() and baseAbsPath.isAbsolute()
1577
1578        wordPath = targetAbsPath.components[:]
1579        baseWordPath = baseAbsPath.components[:]
1580       
1581        result = _WikiLinkPath()
1582       
1583        if downwardOnly:
1584            if len(baseWordPath) >= len(wordPath):
1585                return None
1586            if baseWordPath != wordPath[:len(baseWordPath)]:
1587                return None
1588           
1589            result.upwardCount = 0
1590            result.components = wordPath[len(baseWordPath):]
1591            return result
1592        # TODO test downwardOnly == False
1593        else:
1594            # Remove common path elements
1595            while len(wordPath) > 0 and len(baseWordPath) > 0 and \
1596                    wordPath[0] == baseWordPath[0]:
1597                del wordPath[0]
1598                del baseWordPath[0]
1599           
1600            if len(baseWordPath) == 0:
1601                if len(wordPath) == 0:
1602                    return None  # word == baseWord, TODO return u"." or something
1603
1604                result.upwardCount = 0
1605                result.components = wordPath
1606                return result
1607
1608            result.upwardCount = len(baseWordPath)
1609            result.components = wordPath
1610            return result
1611       
1612
1613
1614
1615
1616
1617
1618_RE_LINE_INDENT = re.compile(ur"^[ \t]*")
1619
1620class _TheHelper(object):
1621    @staticmethod
1622    def reset():
1623        pass
1624
1625    @staticmethod
1626    def getWikiLanguageName():
1627        return WIKI_LANGUAGE_NAME
1628
1629
1630    # TODO More descriptive error messages (which character(s) is/are wrong?)
1631    @staticmethod   # isValidWikiWord
1632    def checkForInvalidWikiWord(word, wikiDocument=None, settings=None):
1633        """
1634        Test if word is syntactically a valid wiki word and no settings
1635        are against it. The camelCase black list is not checked.
1636        The function returns None IFF THE WORD IS VALID, an error string
1637        otherwise
1638        """
1639        if settings is not None and settings.has_key("footnotesAsWws"):
1640            footnotesAsWws = settings["footnotesAsWws"]
1641        else:
1642            if wikiDocument is None:
1643                footnotesAsWws = False
1644            else:
1645                footnotesAsWws = wikiDocument.getWikiConfig().getboolean(
1646                        "main", "footnotes_as_wikiwords", False)
1647
1648        if not footnotesAsWws and footnoteRE.match(word):
1649            return _(u"This is a footnote")
1650
1651        if wikiPageNameRE.match(word):
1652            return None
1653        else:
1654            return _(u"This is syntactically not a wiki word")
1655
1656
1657    # TODO More descriptive error messages (which character(s) is/are wrong?)
1658    @staticmethod   # isValidWikiWord
1659    def checkForInvalidWikiLink(word, wikiDocument=None, settings=None):
1660        """
1661        Test if word is syntactically a valid wiki word and no settings
1662        are against it. The camelCase black list is not checked.
1663        The function returns None IFF THE WORD IS VALID, an error string
1664        otherwise
1665        """
1666        if settings is not None and settings.has_key("footnotesAsWws"):
1667            footnotesAsWws = settings["footnotesAsWws"]
1668        else:
1669            if wikiDocument is None:
1670                footnotesAsWws = False
1671            else:
1672                footnotesAsWws = wikiDocument.getWikiConfig().getboolean(
1673                        "main", "footnotes_as_wikiwords", False)
1674
1675        if not footnotesAsWws and footnoteRE.match(word):
1676            return _(u"This is a footnote")
1677
1678        if wikiLinkCoreRE.match(word):
1679            return None
1680        else:
1681            return _(u"This is syntactically not a wiki word")
1682
1683
1684    @staticmethod
1685    def extractWikiWordFromLink(word, wikiDocument=None, basePage=None):  # TODO Problems with subpages?
1686        """
1687        Strip brackets and other link details if present and return wikiWord
1688        if a valid wiki word can be extracted, None otherwise.
1689        """
1690        if wikiDocument is None and basePage is not None:
1691            wikiDocument = basePage.getWikiDocument()
1692
1693        if basePage is None:
1694            baseDict = _buildBaseDict(wikiDocument=wikiDocument)
1695        else:
1696            baseDict = _buildBaseDict(formatDetails=basePage.getFormatDetails())
1697
1698        try:
1699            t = extractableWikiWord.parseString(word, parseAll=True,
1700                    baseDict=baseDict)
1701            t = t[0]
1702            return t.wikiWord
1703        except ParseException:
1704            return None
1705
1706
1707    resolveWikiWordLink = staticmethod(resolveWikiWordLink)
1708    """
1709    If using subpages this is used to resolve a link to the right wiki word
1710    relative to basePage on which the link is placed.
1711    It returns the absolute link (page name).
1712    """
1713
1714
1715    @staticmethod
1716    def resolvePrefixSilenceAndWikiWordLink(link, basePage):
1717        """
1718        If using subpages this is used to resolve a link to the right wiki word
1719        for autocompletion. It returns a tuple (prefix, silence, pageName).
1720        Autocompletion now searches for all wiki words starting with pageName. For
1721        all found items it removes the first  silence  characters, prepends the  prefix
1722        instead and uses the result as suggestion for autocompletion.
1723       
1724        If prefix is None autocompletion is not possible.
1725        """
1726        linkPath = _WikiLinkPath(link=link)
1727        if linkPath.isAbsolute():
1728            return linkPath.resolvePrefixSilenceAndWikiWordLink(None)
1729
1730        if basePage is None:
1731            return u"", 0, link  # TODO:  Better reaction?
1732       
1733        basePageName = basePage.getWikiWord()
1734        if basePageName is None:
1735            return u"", 0, link  # TODO:  Better reaction?
1736       
1737        return linkPath.resolvePrefixSilenceAndWikiWordLink(_WikiLinkPath(
1738                pageName=basePageName))
1739
1740
1741
1742    @staticmethod
1743    def parseTodoValue(todoValue, wikiDocument=None):
1744        """
1745        Parse a todo value (right of the colon) and return the node or
1746        return None if value couldn't be parsed
1747        """
1748        baseDict = _buildBaseDict(wikiDocument=wikiDocument)
1749        try:
1750            t = todoContent.parseString(todoValue, parseAll=True,
1751                    baseDict=baseDict)
1752            return t[0]
1753        except:
1754            return None
1755
1756
1757    @staticmethod
1758    def parseTodoEntry(entry, wikiDocument=None):
1759        """
1760        Parse a complete todo entry (without end-token) and return the node or
1761        return None if value couldn't be parsed
1762        """
1763        baseDict = _buildBaseDict(wikiDocument=wikiDocument)
1764        try:
1765            t = todoAsWhole.parseString(entry, parseAll=True,
1766                    baseDict=baseDict)
1767            return t[0]
1768        except:
1769            traceback.print_exc()
1770            return None
1771
1772
1773    @staticmethod
1774    def _createAutoLinkRelaxWordEntryRE(word):
1775        """
1776        Get compiled regular expression for one word in autoLink "relax"
1777        mode.
1778
1779        Not part of public API.
1780        """
1781        # Split into parts of contiguous alphanumeric characters
1782        parts = AutoLinkRelaxSplitRE.split(word)
1783        # Filter empty parts
1784        parts = [p for p in parts if p != u""]
1785
1786        # Instead of original non-alphanum characters allow arbitrary
1787        # non-alphanum characters
1788        pat = ur"\b" + (AutoLinkRelaxJoinPAT.join(parts)) + ur"\b"
1789        regex = re.compile(pat, AutoLinkRelaxJoinFlags)
1790
1791        return regex
1792
1793
1794    @staticmethod
1795    def buildAutoLinkRelaxInfo(wikiDocument):
1796        """
1797        Build some cache info needed to process auto-links in "relax" mode.
1798        This info will be given back in the formatDetails when calling
1799        _TheParser.parse().
1800        The implementation for this plugin creates a list of regular
1801        expressions and the related wiki words, but this is not mandatory.
1802        """
1803        # Build up regular expression
1804        # First fetch all wiki words
1805        words = wikiDocument.getWikiData().getAllProducedWikiLinks()
1806
1807        # Sort longest words first
1808        words.sort(key=lambda w: len(w), reverse=True)
1809       
1810        return [(_TheHelper._createAutoLinkRelaxWordEntryRE(w), w)
1811                for w in words if w != u""]
1812
1813
1814    @staticmethod
1815    def createWikiLinkPathObject(*args, **kwargs):
1816        return _WikiLinkPath(*args, **kwargs)
1817
1818
1819    @staticmethod
1820    def isAbsoluteLinkCore(linkCore):
1821        return _WikiLinkPath.isAbsoluteLinkCore(linkCore)
1822
1823
1824    @staticmethod
1825    def createLinkFromWikiWord(word, wikiPage, forceAbsolute=False):
1826        """
1827        Create a link from word which should be put on wikiPage.
1828        """
1829        wikiDocument = wikiPage.getWikiDocument()
1830       
1831        targetPath = _WikiLinkPath(pageName=word)
1832
1833        if forceAbsolute:
1834            return BracketStart + targetPath.getLinkCore() + BracketEnd
1835
1836        linkCore = _TheHelper.createRelativeLinkFromWikiWord(
1837                word, wikiPage.getWikiWord(), downwardOnly=False)
1838               
1839        if _TheHelper.isCcWikiWord(word) and _TheHelper.isCcWikiWord(linkCore):
1840            wikiFormatDetails = wikiPage.getFormatDetails()
1841            if wikiFormatDetails.withCamelCase:
1842               
1843                ccBlacklist = wikiDocument.getCcWordBlacklist()
1844                if not word in ccBlacklist:
1845                    return linkCore
1846       
1847        return BracketStart + linkCore + BracketEnd
1848
1849
1850    @staticmethod
1851    def createAbsoluteLinksFromWikiWords(words, wikiPage=None):
1852        """
1853        Create particularly stable links from a list of words which should be
1854        put on wikiPage.
1855        """
1856        return u"\n".join([u"%s//%s%s" % (BracketStart, w, BracketEnd)
1857                for w in words])
1858               
1859#     # For compatibility. TODO: Remove
1860#     createStableLinksFromWikiWords = createAbsoluteLinksFromWikiWords
1861
1862    @staticmethod
1863    def createRelativeLinkFromWikiWord(word, baseWord, downwardOnly=True):
1864        """
1865        Create a link to wikiword word relative to baseWord.
1866        If downwardOnly is False, the link may contain parts to go to parents
1867            or siblings
1868        in path (in this wiki language, ".." are used for this).
1869        If downwardOnly is True, the function may return None if a relative
1870        link can't be constructed.
1871        """
1872       
1873        relPath = _WikiLinkPath.getRelativePathByAbsPaths(_WikiLinkPath(
1874                pageName=word), _WikiLinkPath(pageName=baseWord),
1875                downwardOnly=downwardOnly)
1876       
1877        if relPath is None:
1878            return None
1879       
1880        return relPath.getLinkCore()
1881
1882    @staticmethod
1883    def createUrlLinkFromPath(wikiDocument, path, relative=False, bracketed=False):
1884        if bracketed:
1885            addSafe = ' '
1886        else:
1887            addSafe = ''
1888
1889        if relative:
1890            url = wikiDocument.makeAbsPathRelUrl(path, addSafe=addSafe)
1891
1892            if url is None:
1893                # Relative not possible -> absolute instead
1894                url = u"file:" + urlFromPathname(path, addSafe=addSafe)
1895        else:
1896            url = u"file:" + urlFromPathname(path, addSafe=addSafe)
1897       
1898        if bracketed:
1899            url = BracketStart + url + BracketEnd
1900       
1901        return url
1902
1903
1904    @staticmethod
1905    def createAttributeFromComponents(key, value, wikiPage=None):
1906        """
1907        Build an attribute from key and value.
1908        TODO: Check for necessary escaping
1909        """
1910        return u"%s%s: %s%s\n" % (BracketStart, key, value, BracketEnd)
1911
1912
1913    @staticmethod
1914    def isCcWikiWord(word):
1915        return bool(wikiWordCcRE.match(word))
1916
1917
1918    @staticmethod
1919    def findNextWordForSpellcheck(text, startPos, wikiPage):
1920        """
1921        Find in text next word to spellcheck, beginning at position startPos
1922       
1923        Returns tuple (start, end, spWord) which is either (None, None, None)
1924        if no more word can be found or returns start and after-end of the
1925        spWord to spellcheck.
1926       
1927        TODO: Move away because this is specific to human language,
1928            not wiki language.
1929        """
1930        while True:
1931            mat = TextWordRE.search(text, startPos)
1932            if mat is None:
1933                # No further word
1934                return (None, None, None)
1935
1936            if mat.group("negative") is not None:
1937                startPos = mat.end()
1938                continue
1939
1940            start, end = mat.span()
1941            spWord = mat.group()
1942
1943            return (start, end, spWord)
1944
1945
1946    @staticmethod
1947    def prepareAutoComplete(editor, text, charPos, lineStartCharPos,
1948            wikiDocument, docPage, settings):
1949        """
1950        Called when user wants autocompletion.
1951        text -- Whole text of page
1952        charPos -- Cursor position in characters
1953        lineStartCharPos -- For convenience and speed, position of the
1954                start of text line in which cursor is.
1955        wikiDocument -- wiki document object
1956        docPage -- DocPage object on which autocompletion is done
1957        closingBracket -- boolean iff a closing bracket should be suggested
1958                for bracket wikiwords and attributes
1959
1960        returns -- a list of tuples (sortKey, entry, backStepChars) where
1961            sortKey -- unistring to use for sorting entries alphabetically
1962                using right collator
1963            entry -- actual unistring entry to show and to insert if
1964                selected
1965            backStepChars -- numbers of chars to delete to the left of cursor
1966                before inserting entry
1967        """
1968        line = text[lineStartCharPos:charPos]
1969        rline = revStr(line)
1970        backStepMap = {}
1971        closingBracket = settings.get("closingBracket", False)
1972        builtinAttribs = settings.get("builtinAttribs", False)
1973
1974        # TODO Sort entries appropriately (whatever this means)
1975
1976        wikiData = wikiDocument.getWikiData()
1977        baseWordSegments = docPage.getWikiWord().split(u"/")
1978
1979        mat1 = RevWikiWordRE.match(rline)
1980        if mat1:
1981            # may be CamelCase word
1982            tofind = line[-mat1.end():]
1983            backstep = len(tofind)
1984            prefix, silence, tofind = _TheHelper.resolvePrefixSilenceAndWikiWordLink(
1985                    tofind, docPage)
1986           
1987            # We don't want prefixes here
1988            if prefix == u"":
1989                ccBlacklist = wikiDocument.getCcWordBlacklist()
1990                for word in wikiData.getWikiLinksStartingWith(tofind, True, True):
1991                    if not _TheHelper.isCcWikiWord(word[silence:]) or word in ccBlacklist:
1992                        continue
1993
1994                    backStepMap[word[silence:]] = backstep
1995
1996        mat2 = RevWikiWordRE2.match(rline)
1997        mat3 = RevAttributeValue.match(rline)
1998        if mat2:
1999            # may be not-CamelCase word or in an attribute name
2000            tofind = line[-mat2.end():]
2001
2002            # Should a closing bracket be appended to suggested words?
2003            if closingBracket:
2004                wordBracketEnd = BracketEnd
2005            else:
2006                wordBracketEnd = u""
2007           
2008            backstep = len(tofind)
2009
2010            prefix, silence, link = _TheHelper.resolvePrefixSilenceAndWikiWordLink(
2011                    tofind[len(BracketStart):], docPage)
2012           
2013            if prefix is not None:
2014                for word in wikiData.getWikiLinksStartingWith(
2015                        link, True, True):
2016                    backStepMap[BracketStart + prefix + word[silence:] +
2017                            wordBracketEnd] = backstep
2018
2019            for prop in wikiDocument.getAttributeNamesStartingWith(
2020                    tofind[len(BracketStart):], builtinAttribs):
2021                backStepMap[BracketStart + prop] = backstep
2022        elif mat3:
2023            # In an attribute value
2024            tofind = line[-mat3.end():]
2025            propkey = revStr(mat3.group(3))
2026            propfill = revStr(mat3.group(2))
2027            propvalpart = revStr(mat3.group(1))
2028            values = filter(lambda pv: pv.startswith(propvalpart),
2029                    wikiDocument.getDistinctAttributeValuesByKey(propkey,
2030                    builtinAttribs))
2031
2032            for v in values:
2033                backStepMap[BracketStart + propkey +
2034                        propfill + v + BracketEnd] = len(tofind)
2035
2036        mat = RevTodoKeyRE.match(rline)
2037        if mat:
2038            # Might be todo entry
2039            tofind = line[-mat.end():]
2040            for t in wikiData.getTodos():
2041                td = t[1]
2042                if not td.startswith(tofind):
2043                    continue
2044
2045#                 tdmat = ToDoREWithCapturing.match(td)
2046#                 key = tdmat.group(1) + u":"
2047                key = td + u":"
2048                backStepMap[key] = len(tofind)
2049
2050        mat = RevTodoValueRE.match(rline)
2051        if mat:
2052            # Might be todo entry
2053            tofind = line[-mat.end():]
2054            combinedTodos = [t[1] + ":" + t[2] for t in wikiData.getTodos()]
2055#             todos = [t[1] for t in wikiData.getTodos() if t[1].startswith(tofind)]
2056            todos = [t for t in combinedTodos if t.startswith(tofind)]
2057            for t in todos:
2058                backStepMap[t] = len(tofind)
2059
2060        mat = RevWikiWordAnchorRE2.match(rline)
2061        if mat:
2062            # In an anchor of a possible bracketed wiki word
2063            tofind = line[-mat.end():]
2064            wikiLinkCore = revStr(mat.group("wikiWord"))
2065            wikiWord = _TheHelper.resolvePrefixSilenceAndWikiWordLink(
2066                    wikiLinkCore, docPage)[2]
2067
2068            anchorBegin = revStr(mat.group("anchorBegin"))
2069
2070            try:
2071                page = wikiDocument.getWikiPage(wikiWord) # May throw exception
2072                anchors = [a for a in page.getAnchors()
2073                        if a.startswith(anchorBegin)]
2074
2075                for a in anchors:
2076                    backStepMap[BracketStart + wikiLinkCore +
2077                            BracketEnd +
2078                            WikiWordAnchorStart + a] = len(tofind)
2079            except WikiWordNotFoundException:
2080                # wikiWord isn't a wiki word
2081                pass
2082
2083        mat = RevWikiWordAnchorRE.match(rline)
2084        if mat:
2085            # In an anchor of a possible camel case word
2086            tofind = line[-mat.end():]
2087            wikiLinkCore = revStr(mat.group("wikiWord"))
2088            wikiWord = _TheHelper.resolvePrefixSilenceAndWikiWordLink(
2089                    wikiLinkCore, docPage)[2]
2090
2091            anchorBegin = revStr(mat.group("anchorBegin"))
2092
2093            try:
2094                page = wikiDocument.getWikiPage(wikiWord) # May throw exception
2095                anchors = [a for a in page.getAnchors()
2096                        if a.startswith(anchorBegin)]
2097                       
2098                for a in anchors:
2099                    backStepMap[wikiWord + wikiLinkCore +
2100                            a] = len(tofind)
2101            except WikiWordNotFoundException:
2102                # wikiWord isn't a wiki word
2103                pass
2104
2105
2106        acresult = backStepMap.keys()
2107       
2108        if len(acresult) > 0:
2109            # formatting.BracketEnd
2110            acresultTuples = []
2111            for r in acresult:
2112                if r.endswith(BracketEnd):
2113                    rc = r[: -len(BracketEnd)]
2114                else:
2115                    rc = r
2116                acresultTuples.append((rc, r, backStepMap[r]))
2117
2118            return acresultTuples
2119        else:
2120            return []
2121
2122
2123    @staticmethod
2124    def handleNewLineBeforeEditor(editor, text, charPos, lineStartCharPos,
2125            wikiDocument, settings):
2126        """
2127        Processes pressing of a newline in editor before editor processes it.
2128        Returns True iff the actual newline should be processed by
2129            editor yet.
2130        """
2131        # autoIndent, autoBullet, autoUnbullet
2132       
2133        line = text[lineStartCharPos:charPos]
2134
2135        if settings.get("autoUnbullet", False):
2136            # Check for lonely bullet or number
2137            mat = BulletRE.match(line)
2138            if mat and mat.end(0) == len(line):
2139                editor.SetSelectionByCharPos(lineStartCharPos, charPos)
2140                editor.ReplaceSelection(mat.group("indentBullet"))
2141                return False
2142
2143            mat = NumericSimpleBulletRE.match(line)
2144            if mat and mat.end(0) == len(line):
2145                editor.SetSelectionByCharPos(lineStartCharPos, charPos)
2146                editor.ReplaceSelection(mat.group("indentBullet"))
2147                return False
2148
2149            mat = NumericBulletRE.match(line)
2150            if mat and mat.end(0) == len(line):
2151                replacement = mat.group("indentNumeric")
2152                if mat.group("preLastNumeric") != u"":
2153                    replacement += mat.group("preLastNumeric") + u" "
2154
2155                editor.SetSelectionByCharPos(lineStartCharPos, charPos)
2156                editor.ReplaceSelection(replacement)
2157                return False
2158       
2159        return True
2160
2161
2162    @staticmethod
2163    def handleNewLineAfterEditor(editor, text, charPos, lineStartCharPos,
2164            wikiDocument, settings):
2165        """
2166        Processes pressing of a newline after editor processed it (if
2167        handleNewLineBeforeEditor returned True).
2168        """
2169        # autoIndent, autoBullet, autoUnbullet
2170
2171        currentLine = editor.GetCurrentLine()
2172
2173        if currentLine > 0:
2174            previousLine = editor.GetLine(currentLine - 1)
2175            indent = _RE_LINE_INDENT.match(previousLine).group(0)
2176   
2177            # check if the prev level was a bullet level
2178            if settings.get("autoBullets", False):
2179                match = BulletRE.match(previousLine)
2180                if match:
2181                    editor.AddText(indent + match.group("actualBullet"))
2182                    return
2183
2184                match = NumericSimpleBulletRE.match(previousLine)
2185                if match:
2186                    editor.AddText(indent + match.group("actualBullet"))
2187                    return False
2188
2189                match = NumericBulletRE.search(previousLine)
2190                if match:
2191                    prevNumStr = match.group(3)
2192                    prevNum = int(prevNumStr)
2193                    nextNum = prevNum+1
2194#                     adjustment = len(str(nextNum)) - len(prevNumStr)
2195#                     if adjustment == 0:
2196                    editor.AddText(u"%s%s%d. " % (indent,
2197                            match.group(2), int(prevNum)+1))
2198#                     else:
2199#                         editor.AddText(u"%s%s%d. " % (u" " *
2200#                                 (editor.GetLineIndentation(currentLine - 1) - adjustment),
2201#                                 match.group(2), int(prevNum)+1))
2202                    return
2203
2204            if settings.get("autoIndent", False):
2205                editor.AddText(indent)
2206                return
2207
2208
2209    @staticmethod
2210    def handleRewrapText(editor, settings):
2211        curPos = editor.GetCurrentPos()
2212
2213        # search back for start of the para
2214        curLineNum = editor.GetCurrentLine()
2215        curLine = editor.GetLine(curLineNum)
2216        while curLineNum > 0:
2217            # don't wrap previous bullets with this bullet
2218            if (BulletRE.match(curLine) or NumericBulletRE.match(curLine)):
2219                break
2220
2221            if EmptyLineRE.match(curLine):
2222                curLineNum = curLineNum + 1
2223                break
2224
2225            curLineNum = curLineNum - 1
2226            curLine = editor.GetLine(curLineNum)
2227        startLine = curLineNum
2228
2229        # search forward for end of the para
2230        curLineNum = editor.GetCurrentLine()
2231        curLine = editor.GetLine(curLineNum)
2232        while curLineNum <= editor.GetLineCount():
2233            # don't wrap the next bullet with this bullet
2234            if curLineNum > startLine:
2235                if (BulletRE.match(curLine) or NumericBulletRE.match(curLine)):
2236                    curLineNum = curLineNum - 1
2237                    break
2238
2239            if EmptyLineRE.match(curLine):
2240                curLineNum = curLineNum - 1
2241                break
2242
2243            curLineNum = curLineNum + 1
2244            curLine = editor.GetLine(curLineNum)
2245        endLine = curLineNum
2246       
2247        if (startLine <= endLine):
2248            # get the start and end of the lines
2249            startPos = editor.PositionFromLine(startLine)
2250            endPos = editor.GetLineEndPosition(endLine)
2251
2252            # get the indentation for rewrapping
2253            indent = _RE_LINE_INDENT.match(editor.GetLine(startLine)).group(0)
2254            subIndent = indent
2255
2256            # if the start of the para is a bullet the subIndent has to change
2257            if BulletRE.match(editor.GetLine(startLine)):
2258                subIndent = indent + u"  "
2259            else:
2260                match = NumericBulletRE.match(editor.GetLine(startLine))
2261                if match:
2262                    subIndent = indent + u" " * (len(match.group(2)) + 2)
2263
2264            # get the text that will be wrapped
2265            text = editor.GetTextRange(startPos, endPos)
2266            # remove spaces, newlines, etc
2267            text = re.sub("[\s\r\n]+", " ", text)
2268
2269            # wrap the text
2270            wrapPosition = 70
2271            try:
2272                wrapPosition = int(
2273                        editor.getLoadedDocPage().getAttributeOrGlobal(
2274                        "wrap", "70"))
2275            except:
2276                pass
2277
2278            # make the min wrapPosition 5
2279            if wrapPosition < 5:
2280                wrapPosition = 5
2281
2282            filledText = fill(text, width=wrapPosition,
2283                    initial_indent=indent,
2284                    subsequent_indent=subIndent)
2285
2286            # replace the text based on targetting
2287            editor.SetTargetStart(startPos)
2288            editor.SetTargetEnd(endPos)
2289            editor.ReplaceTarget(filledText)
2290            editor.GotoPos(curPos)
2291
2292
2293    @staticmethod
2294    def getNewDefaultWikiSettingsPage(mainControl):
2295        """
2296        Return default text of the "WikiSettings" page for a new wiki.
2297        """
2298        return _(u"""++ Wiki Settings
2299
2300These are your default global settings.
2301
2302[global.importance.low.color: grey]
2303[global.importance.high.bold: true]
2304[global.contact.icon: contact]
2305[global.wrap: 70]
2306
2307[icon: cog]
2308"""# TODO Localize differently?
2309
2310
2311    @staticmethod
2312    def createWikiLanguageDetails(wikiDocument, docPage):
2313        """
2314        Returns a new WikiLanguageDetails object based on current configuration
2315        """
2316        return WikiLanguageDetails(wikiDocument, docPage)
2317
2318
2319THE_LANGUAGE_HELPER = _TheHelper()
2320
2321
2322
2323def describeWikiLanguage(ver, app):
2324    """
2325    API function for "WikiParser" plugins
2326    Returns a sequence of tuples describing the supported
2327    insertion keys. Each tuple has the form (intLanguageName, hrLanguageName,
2328            parserFactory, parserIsThreadsafe, editHelperFactory,
2329            editHelperIsThreadsafe)
2330    Where the items mean:
2331        intLanguageName -- internal unique name (should be ascii only) to
2332            identify wiki language processed by parser
2333        hrLanguageName -- human readable language name, unistring
2334            (TODO: localization)
2335        parserFactory -- factory function to create parser object(s) fulfilling
2336
2337        parserIsThreadsafe -- boolean if parser is threadsafe. If not this
2338            will currently lead to a very inefficient operation
2339        processHelperFactory -- factory for helper object containing further
2340            functions needed for editing, tree presentation and so on.
2341        editHelperIsThreadsafe -- boolean if edit helper functions are
2342            threadsafe.
2343
2344    Parameters:
2345
2346    ver -- API version (can only be 1 currently)
2347    app -- wxApp object
2348    """
2349
2350    return ((WIKI_LANGUAGE_NAME, WIKI_HR_LANGUAGE_NAME, parserFactory,
2351             True, languageHelperFactory, True),)
2352
2353
2354
2355
2356def parserFactory(intLanguageName, debugMode):
2357    """
2358    Builds up a parser object. If the parser is threadsafe this function is
2359    allowed to return the same object multiple times (currently it should do
2360    so for efficiency).
2361    For seldom needed parsers it is recommended to put the actual parser
2362    construction as singleton in this function to reduce startup time of WikidPad.
2363    For non-threadsafe parsers it is required to create one inside this
2364    function at each call.
2365
2366    intLanguageName -- internal unique name (should be ascii only) to
2367        identify wiki language to process by parser
2368    """
2369    if text.getDebug() != debugMode:
2370        text.setDebugRecurs(debugMode)
2371
2372    return THE_PARSER
2373
2374
2375def languageHelperFactory(intLanguageName, debugMode):
2376    """
2377    Builds up a language helper object. If the object is threadsafe this function is
2378    allowed to return the same object multiple times (currently it should do
2379    so for efficiency).
2380
2381    intLanguageName -- internal unique name (should be ascii only) to
2382        identify wiki language to process by helper
2383    """
2384    return THE_LANGUAGE_HELPER
Note: See TracBrowser for help on using the browser.