1"""
2========================= FOOTNOTES =================================
3
4This section adds footnote handling to markdown.  It can be used as
5an example for extending python-markdown with relatively complex
6functionality.  While in this case the extension is included inside
7the module itself, it could just as easily be added from outside the
8module.  Not that all markdown classes above are ignorant about
9footnotes.  All footnote functionality is provided separately and
10then added to the markdown instance at the run time.
11
12Footnote functionality is attached by calling extendMarkdown()
13method of FootnoteExtension.  The method also registers the
14extension to allow it's state to be reset by a call to reset()
15method.
16
17Example:
18    Footnotes[^1] have a label[^label] and a definition[^!DEF].
19
20    [^1]: This is a footnote
21    [^label]: A footnote on "label"
22    [^!DEF]: The footnote for definition
23
24"""
25
26import re, markdown
27from markdown import etree
28
29FN_BACKLINK_TEXT = "zz1337820767766393qq"
30NBSP_PLACEHOLDER =  "qq3936677670287331zz"
31DEF_RE = re.compile(r'(\ ?\ ?\ ?)\[\^([^\]]*)\]:\s*(.*)')
32TABBED_RE = re.compile(r'((\t)|(    ))(.*)')
33
34class FootnoteExtension(markdown.Extension):
35    """ Footnote Extension. """
36
37    def __init__ (self, configs):
38        """ Setup configs. """
39        self.config = {'PLACE_MARKER':
40                       ["///Footnotes Go Here///",
41                        "The text string that marks where the footnotes go"],
42                       'UNIQUE_IDS':
43                       [False,
44                        "Avoid name collisions across "
45                        "multiple calls to reset()."]}
46
47        for key, value in configs:
48            self.config[key][0] = value
49
50        # In multiple invocations, emit links that don't get tangled.
51        self.unique_prefix = 0
52
53        self.reset()
54
55    def extendMarkdown(self, md, md_globals):
56        """ Add pieces to Markdown. """
57        md.registerExtension(self)
58        self.parser = md.parser
59        # Insert a preprocessor before ReferencePreprocessor
60        md.preprocessors.add("footnote", FootnotePreprocessor(self),
61                             "<reference")
62        # Insert an inline pattern before ImageReferencePattern
63        FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah
64        md.inlinePatterns.add("footnote", FootnotePattern(FOOTNOTE_RE, self),
65                              "<reference")
66        # Insert a tree-processor that would actually add the footnote div
67        # This must be before the inline treeprocessor so inline patterns
68        # run on the contents of the div.
69        md.treeprocessors.add("footnote", FootnoteTreeprocessor(self),
70                                 "<inline")
71        # Insert a postprocessor after amp_substitute oricessor
72        md.postprocessors.add("footnote", FootnotePostprocessor(self),
73                                  ">amp_substitute")
74
75    def reset(self):
76        """ Clear the footnotes on reset, and prepare for a distinct document. """
77        self.footnotes = markdown.odict.OrderedDict()
78        self.unique_prefix += 1
79
80    def findFootnotesPlaceholder(self, root):
81        """ Return ElementTree Element that contains Footnote placeholder. """
82        def finder(element):
83            for child in element:
84                if child.text:
85                    if child.text.find(self.getConfig("PLACE_MARKER")) > -1:
86                        return child, True
87                if child.tail:
88                    if child.tail.find(self.getConfig("PLACE_MARKER")) > -1:
89                        return (child, element), False
90                finder(child)
91            return None
92
93        res = finder(root)
94        return res
95
96    def setFootnote(self, id, text):
97        """ Store a footnote for later retrieval. """
98        self.footnotes[id] = text
99
100    def makeFootnoteId(self, id):
101        """ Return footnote link id. """
102        if self.getConfig("UNIQUE_IDS"):
103            return 'fn:%d-%s' % (self.unique_prefix, id)
104        else:
105            return 'fn:%s' % id
106
107    def makeFootnoteRefId(self, id):
108        """ Return footnote back-link id. """
109        if self.getConfig("UNIQUE_IDS"):
110            return 'fnref:%d-%s' % (self.unique_prefix, id)
111        else:
112            return 'fnref:%s' % id
113
114    def makeFootnotesDiv(self, root):
115        """ Return div of footnotes as et Element. """
116
117        if not self.footnotes.keys():
118            return None
119
120        div = etree.Element("div")
121        div.set('class', 'footnote')
122        hr = etree.SubElement(div, "hr")
123        ol = etree.SubElement(div, "ol")
124
125        for id in self.footnotes.keys():
126            li = etree.SubElement(ol, "li")
127            li.set("id", self.makeFootnoteId(id))
128            self.parser.parseChunk(li, self.footnotes[id])
129            backlink = etree.Element("a")
130            backlink.set("href", "#" + self.makeFootnoteRefId(id))
131            backlink.set("rev", "footnote")
132            backlink.set("title", "Jump back to footnote %d in the text" % \
133                            (self.footnotes.index(id)+1))
134            backlink.text = FN_BACKLINK_TEXT
135
136            if li.getchildren():
137                node = li[-1]
138                if node.tag == "p":
139                    node.text = node.text + NBSP_PLACEHOLDER
140                    node.append(backlink)
141                else:
142                    p = etree.SubElement(li, "p")
143                    p.append(backlink)
144        return div
145
146
147class FootnotePreprocessor(markdown.preprocessors.Preprocessor):
148    """ Find all footnote references and store for later use. """
149
150    def __init__ (self, footnotes):
151        self.footnotes = footnotes
152
153    def run(self, lines):
154        lines = self._handleFootnoteDefinitions(lines)
155        text = "\n".join(lines)
156        return text.split("\n")
157
158    def _handleFootnoteDefinitions(self, lines):
159        """
160        Recursively find all footnote definitions in lines.
161
162        Keywords:
163
164        * lines: A list of lines of text
165
166        Return: A list of lines with footnote definitions removed.
167
168        """
169        i, id, footnote = self._findFootnoteDefinition(lines)
170
171        if id :
172            plain = lines[:i]
173            detabbed, theRest = self.detectTabbed(lines[i+1:])
174            self.footnotes.setFootnote(id,
175                                       footnote + "\n"
176                                       + "\n".join(detabbed))
177            more_plain = self._handleFootnoteDefinitions(theRest)
178            return plain + [""] + more_plain
179        else :
180            return lines
181
182    def _findFootnoteDefinition(self, lines):
183        """
184        Find the parts of a footnote definition.
185
186        Keywords:
187
188        * lines: A list of lines of text.
189
190        Return: A three item tuple containing the index of the first line of a
191        footnote definition, the id of the definition and the body of the
192        definition.
193
194        """
195        counter = 0
196        for line in lines:
197            m = DEF_RE.match(line)
198            if m:
199                return counter, m.group(2), m.group(3)
200            counter += 1
201        return counter, None, None
202
203    def detectTabbed(self, lines):
204        """ Find indented text and remove indent before further proccesing.
205
206        Keyword arguments:
207
208        * lines: an array of strings
209
210        Returns: a list of post processed items and the unused
211        remainder of the original list
212
213        """
214        items = []
215        item = -1
216        i = 0 # to keep track of where we are
217
218        def detab(line):
219            match = TABBED_RE.match(line)
220            if match:
221               return match.group(4)
222
223        for line in lines:
224            if line.strip(): # Non-blank line
225                line = detab(line)
226                if line:
227                    items.append(line)
228                    i += 1
229                    continue
230                else:
231                    return items, lines[i:]
232
233            else: # Blank line: _maybe_ we are done.
234                i += 1 # advance
235
236                # Find the next non-blank line
237                for j in range(i, len(lines)):
238                    if lines[j].strip():
239                        next_line = lines[j]; break
240                else:
241                    break # There is no more text; we are done.
242
243                # Check if the next non-blank line is tabbed
244                if detab(next_line): # Yes, more work to do.
245                    items.append("")
246                    continue
247                else:
248                    break # No, we are done.
249        else:
250            i += 1
251
252        return items, lines[i:]
253
254
255class FootnotePattern(markdown.inlinepatterns.Pattern):
256    """ InlinePattern for footnote markers in a document's body text. """
257
258    def __init__(self, pattern, footnotes):
259        markdown.inlinepatterns.Pattern.__init__(self, pattern)
260        self.footnotes = footnotes
261
262    def handleMatch(self, m):
263        sup = etree.Element("sup")
264        a = etree.SubElement(sup, "a")
265        id = m.group(2)
266        sup.set('id', self.footnotes.makeFootnoteRefId(id))
267        a.set('href', '#' + self.footnotes.makeFootnoteId(id))
268        a.set('rel', 'footnote')
269        a.text = str(self.footnotes.footnotes.index(id) + 1)
270        return sup
271
272
273class FootnoteTreeprocessor(markdown.treeprocessors.Treeprocessor):
274    """ Build and append footnote div to end of document. """
275
276    def __init__ (self, footnotes):
277        self.footnotes = footnotes
278
279    def run(self, root):
280        footnotesDiv = self.footnotes.makeFootnotesDiv(root)
281        if footnotesDiv:
282            result = self.footnotes.findFootnotesPlaceholder(root)
283            if result:
284                node, isText = result
285                if isText:
286                    node.text = None
287                    node.getchildren().insert(0, footnotesDiv)
288                else:
289                    child, element = node
290                    ind = element.getchildren().find(child)
291                    element.getchildren().insert(ind + 1, footnotesDiv)
292                    child.tail = None
293                fnPlaceholder.parent.replaceChild(fnPlaceholder, footnotesDiv)
294            else:
295                root.append(footnotesDiv)
296
297class FootnotePostprocessor(markdown.postprocessors.Postprocessor):
298    """ Replace placeholders with html entities. """
299
300    def run(self, text):
301        text = text.replace(FN_BACKLINK_TEXT, "&#8617;")
302        return text.replace(NBSP_PLACEHOLDER, "&#160;")
303
304def makeExtension(configs=[]):
305    """ Return an instance of the FootnoteExtension """
306    return FootnoteExtension(configs=configs)
307
308