1"""
2Footnotes Extension for Python-Markdown
3=======================================
4
5Adds footnote handling to Python-Markdown.
6
7See <https://pythonhosted.org/Markdown/extensions/footnotes.html>
8for documentation.
9
10Copyright The Python Markdown Project
11
12License: [BSD](http://www.opensource.org/licenses/bsd-license.php)
13
14"""
15
16from __future__ import absolute_import
17from __future__ import unicode_literals
18from . import Extension
19from ..preprocessors import Preprocessor
20from ..inlinepatterns import Pattern
21from ..treeprocessors import Treeprocessor
22from ..postprocessors import Postprocessor
23from ..util import etree, text_type
24from ..odict import OrderedDict
25import re
26
27FN_BACKLINK_TEXT = "zz1337820767766393qq"
28NBSP_PLACEHOLDER = "qq3936677670287331zz"
29DEF_RE = re.compile(r'[ ]{0,3}\[\^([^\]]*)\]:\s*(.*)')
30TABBED_RE = re.compile(r'((\t)|(    ))(.*)')
31
32
33class FootnoteExtension(Extension):
34    """ Footnote Extension. """
35
36    def __init__(self, *args, **kwargs):
37        """ Setup configs. """
38
39        self.config = {
40            '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            "BACKLINK_TEXT":
48                ["&#8617;",
49                 "The text string that links from the footnote "
50                 "to the reader's place."]
51        }
52        super(FootnoteExtension, self).__init__(*args, **kwargs)
53
54        # In multiple invocations, emit links that don't get tangled.
55        self.unique_prefix = 0
56
57        self.reset()
58
59    def extendMarkdown(self, md, md_globals):
60        """ Add pieces to Markdown. """
61        md.registerExtension(self)
62        self.parser = md.parser
63        self.md = md
64        # Insert a preprocessor before ReferencePreprocessor
65        md.preprocessors.add(
66            "footnote", FootnotePreprocessor(self), "<reference"
67        )
68        # Insert an inline pattern before ImageReferencePattern
69        FOOTNOTE_RE = r'\[\^([^\]]*)\]'  # blah blah [^1] blah
70        md.inlinePatterns.add(
71            "footnote", FootnotePattern(FOOTNOTE_RE, self), "<reference"
72        )
73        # Insert a tree-processor that would actually add the footnote div
74        # This must be before all other treeprocessors (i.e., inline and
75        # codehilite) so they can run on the the contents of the div.
76        md.treeprocessors.add(
77            "footnote", FootnoteTreeprocessor(self), "_begin"
78        )
79        # Insert a postprocessor after amp_substitute oricessor
80        md.postprocessors.add(
81            "footnote", FootnotePostprocessor(self), ">amp_substitute"
82        )
83
84    def reset(self):
85        """ Clear footnotes on reset, and prepare for distinct document. """
86        self.footnotes = OrderedDict()
87        self.unique_prefix += 1
88
89    def findFootnotesPlaceholder(self, root):
90        """ Return ElementTree Element that contains Footnote placeholder. """
91        def finder(element):
92            for child in element:
93                if child.text:
94                    if child.text.find(self.getConfig("PLACE_MARKER")) > -1:
95                        return child, element, True
96                if child.tail:
97                    if child.tail.find(self.getConfig("PLACE_MARKER")) > -1:
98                        return child, element, False
99                finder(child)
100            return None
101
102        res = finder(root)
103        return res
104
105    def setFootnote(self, id, text):
106        """ Store a footnote for later retrieval. """
107        self.footnotes[id] = text
108
109    def get_separator(self):
110        if self.md.output_format in ['html5', 'xhtml5']:
111            return '-'
112        return ':'
113
114    def makeFootnoteId(self, id):
115        """ Return footnote link id. """
116        if self.getConfig("UNIQUE_IDS"):
117            return 'fn%s%d-%s' % (self.get_separator(), self.unique_prefix, id)
118        else:
119            return 'fn%s%s' % (self.get_separator(), id)
120
121    def makeFootnoteRefId(self, id):
122        """ Return footnote back-link id. """
123        if self.getConfig("UNIQUE_IDS"):
124            return 'fnref%s%d-%s' % (self.get_separator(),
125                                     self.unique_prefix, id)
126        else:
127            return 'fnref%s%s' % (self.get_separator(), id)
128
129    def makeFootnotesDiv(self, root):
130        """ Return div of footnotes as et Element. """
131
132        if not list(self.footnotes.keys()):
133            return None
134
135        div = etree.Element("div")
136        div.set('class', 'footnote')
137        etree.SubElement(div, "hr")
138        ol = etree.SubElement(div, "ol")
139
140        for id in self.footnotes.keys():
141            li = etree.SubElement(ol, "li")
142            li.set("id", self.makeFootnoteId(id))
143            self.parser.parseChunk(li, self.footnotes[id])
144            backlink = etree.Element("a")
145            backlink.set("href", "#" + self.makeFootnoteRefId(id))
146            if self.md.output_format not in ['html5', 'xhtml5']:
147                backlink.set("rev", "footnote")  # Invalid in HTML5
148            backlink.set("class", "footnote-backref")
149            backlink.set(
150                "title",
151                "Jump back to footnote %d in the text" %
152                (self.footnotes.index(id)+1)
153            )
154            backlink.text = FN_BACKLINK_TEXT
155
156            if li.getchildren():
157                node = li[-1]
158                if node.tag == "p":
159                    node.text = node.text + NBSP_PLACEHOLDER
160                    node.append(backlink)
161                else:
162                    p = etree.SubElement(li, "p")
163                    p.append(backlink)
164        return div
165
166
167class FootnotePreprocessor(Preprocessor):
168    """ Find all footnote references and store for later use. """
169
170    def __init__(self, footnotes):
171        self.footnotes = footnotes
172
173    def run(self, lines):
174        """
175        Loop through lines and find, set, and remove footnote definitions.
176
177        Keywords:
178
179        * lines: A list of lines of text
180
181        Return: A list of lines of text with footnote definitions removed.
182
183        """
184        newlines = []
185        i = 0
186        while True:
187            m = DEF_RE.match(lines[i])
188            if m:
189                fn, _i = self.detectTabbed(lines[i+1:])
190                fn.insert(0, m.group(2))
191                i += _i-1  # skip past footnote
192                self.footnotes.setFootnote(m.group(1), "\n".join(fn))
193            else:
194                newlines.append(lines[i])
195            if len(lines) > i+1:
196                i += 1
197            else:
198                break
199        return newlines
200
201    def detectTabbed(self, lines):
202        """ Find indented text and remove indent before further proccesing.
203
204        Keyword arguments:
205
206        * lines: an array of strings
207
208        Returns: a list of post processed items and the index of last line.
209
210        """
211        items = []
212        blank_line = False  # have we encountered a blank line yet?
213        i = 0  # to keep track of where we are
214
215        def detab(line):
216            match = TABBED_RE.match(line)
217            if match:
218                return match.group(4)
219
220        for line in lines:
221            if line.strip():  # Non-blank line
222                detabbed_line = detab(line)
223                if detabbed_line:
224                    items.append(detabbed_line)
225                    i += 1
226                    continue
227                elif not blank_line and not DEF_RE.match(line):
228                    # not tabbed but still part of first par.
229                    items.append(line)
230                    i += 1
231                    continue
232                else:
233                    return items, i+1
234
235            else:  # Blank line: _maybe_ we are done.
236                blank_line = True
237                i += 1  # advance
238
239                # Find the next non-blank line
240                for j in range(i, len(lines)):
241                    if lines[j].strip():
242                        next_line = lines[j]
243                        break
244                else:
245                    break  # There is no more text; we are done.
246
247                # Check if the next non-blank line is tabbed
248                if detab(next_line):  # Yes, more work to do.
249                    items.append("")
250                    continue
251                else:
252                    break  # No, we are done.
253        else:
254            i += 1
255
256        return items, i
257
258
259class FootnotePattern(Pattern):
260    """ InlinePattern for footnote markers in a document's body text. """
261
262    def __init__(self, pattern, footnotes):
263        super(FootnotePattern, self).__init__(pattern)
264        self.footnotes = footnotes
265
266    def handleMatch(self, m):
267        id = m.group(2)
268        if id in self.footnotes.footnotes.keys():
269            sup = etree.Element("sup")
270            a = etree.SubElement(sup, "a")
271            sup.set('id', self.footnotes.makeFootnoteRefId(id))
272            a.set('href', '#' + self.footnotes.makeFootnoteId(id))
273            if self.footnotes.md.output_format not in ['html5', 'xhtml5']:
274                a.set('rel', 'footnote')  # invalid in HTML5
275            a.set('class', 'footnote-ref')
276            a.text = text_type(self.footnotes.footnotes.index(id) + 1)
277            return sup
278        else:
279            return None
280
281
282class FootnoteTreeprocessor(Treeprocessor):
283    """ Build and append footnote div to end of document. """
284
285    def __init__(self, footnotes):
286        self.footnotes = footnotes
287
288    def run(self, root):
289        footnotesDiv = self.footnotes.makeFootnotesDiv(root)
290        if footnotesDiv is not None:
291            result = self.footnotes.findFootnotesPlaceholder(root)
292            if result:
293                child, parent, isText = result
294                ind = parent.getchildren().index(child)
295                if isText:
296                    parent.remove(child)
297                    parent.insert(ind, footnotesDiv)
298                else:
299                    parent.insert(ind + 1, footnotesDiv)
300                    child.tail = None
301            else:
302                root.append(footnotesDiv)
303
304
305class FootnotePostprocessor(Postprocessor):
306    """ Replace placeholders with html entities. """
307    def __init__(self, footnotes):
308        self.footnotes = footnotes
309
310    def run(self, text):
311        text = text.replace(
312            FN_BACKLINK_TEXT, self.footnotes.getConfig("BACKLINK_TEXT")
313        )
314        return text.replace(NBSP_PLACEHOLDER, "&#160;")
315
316
317def makeExtension(*args, **kwargs):
318    """ Return an instance of the FootnoteExtension """
319    return FootnoteExtension(*args, **kwargs)
320