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