""" Footnotes Extension for Python-Markdown ======================================= Adds footnote handling to Python-Markdown. See for documentation. Copyright The Python Markdown Project License: [BSD](http://www.opensource.org/licenses/bsd-license.php) """ from __future__ import absolute_import from __future__ import unicode_literals from . import Extension from ..preprocessors import Preprocessor from ..inlinepatterns import Pattern from ..treeprocessors import Treeprocessor from ..postprocessors import Postprocessor from ..util import etree, text_type from ..odict import OrderedDict import re FN_BACKLINK_TEXT = "zz1337820767766393qq" NBSP_PLACEHOLDER = "qq3936677670287331zz" DEF_RE = re.compile(r'[ ]{0,3}\[\^([^\]]*)\]:\s*(.*)') TABBED_RE = re.compile(r'((\t)|( ))(.*)') class FootnoteExtension(Extension): """ Footnote Extension. """ def __init__ (self, *args, **kwargs): """ Setup configs. """ self.config = { 'PLACE_MARKER': ["///Footnotes Go Here///", "The text string that marks where the footnotes go"], 'UNIQUE_IDS': [False, "Avoid name collisions across " "multiple calls to reset()."], "BACKLINK_TEXT": ["↩", "The text string that links from the footnote to the reader's place."] } super(FootnoteExtension, self).__init__(*args, **kwargs) # In multiple invocations, emit links that don't get tangled. self.unique_prefix = 0 self.reset() def extendMarkdown(self, md, md_globals): """ Add pieces to Markdown. """ md.registerExtension(self) self.parser = md.parser self.md = md # Insert a preprocessor before ReferencePreprocessor md.preprocessors.add("footnote", FootnotePreprocessor(self), "amp_substitute") def reset(self): """ Clear the footnotes on reset, and prepare for a distinct document. """ self.footnotes = OrderedDict() self.unique_prefix += 1 def findFootnotesPlaceholder(self, root): """ Return ElementTree Element that contains Footnote placeholder. """ def finder(element): for child in element: if child.text: if child.text.find(self.getConfig("PLACE_MARKER")) > -1: return child, element, True if child.tail: if child.tail.find(self.getConfig("PLACE_MARKER")) > -1: return child, element, False finder(child) return None res = finder(root) return res def setFootnote(self, id, text): """ Store a footnote for later retrieval. """ self.footnotes[id] = text def get_separator(self): if self.md.output_format in ['html5', 'xhtml5']: return '-' return ':' def makeFootnoteId(self, id): """ Return footnote link id. """ if self.getConfig("UNIQUE_IDS"): return 'fn%s%d-%s' % (self.get_separator(), self.unique_prefix, id) else: return 'fn%s%s' % (self.get_separator(), id) def makeFootnoteRefId(self, id): """ Return footnote back-link id. """ if self.getConfig("UNIQUE_IDS"): return 'fnref%s%d-%s' % (self.get_separator(), self.unique_prefix, id) else: return 'fnref%s%s' % (self.get_separator(), id) def makeFootnotesDiv(self, root): """ Return div of footnotes as et Element. """ if not list(self.footnotes.keys()): return None div = etree.Element("div") div.set('class', 'footnote') etree.SubElement(div, "hr") ol = etree.SubElement(div, "ol") for id in self.footnotes.keys(): li = etree.SubElement(ol, "li") li.set("id", self.makeFootnoteId(id)) self.parser.parseChunk(li, self.footnotes[id]) backlink = etree.Element("a") backlink.set("href", "#" + self.makeFootnoteRefId(id)) if self.md.output_format not in ['html5', 'xhtml5']: backlink.set("rev", "footnote") # Invalid in HTML5 backlink.set("class", "footnote-backref") backlink.set("title", "Jump back to footnote %d in the text" % \ (self.footnotes.index(id)+1)) backlink.text = FN_BACKLINK_TEXT if li.getchildren(): node = li[-1] if node.tag == "p": node.text = node.text + NBSP_PLACEHOLDER node.append(backlink) else: p = etree.SubElement(li, "p") p.append(backlink) return div class FootnotePreprocessor(Preprocessor): """ Find all footnote references and store for later use. """ def __init__ (self, footnotes): self.footnotes = footnotes def run(self, lines): """ Loop through lines and find, set, and remove footnote definitions. Keywords: * lines: A list of lines of text Return: A list of lines of text with footnote definitions removed. """ newlines = [] i = 0 while True: m = DEF_RE.match(lines[i]) if m: fn, _i = self.detectTabbed(lines[i+1:]) fn.insert(0, m.group(2)) i += _i-1 # skip past footnote self.footnotes.setFootnote(m.group(1), "\n".join(fn)) else: newlines.append(lines[i]) if len(lines) > i+1: i += 1 else: break return newlines def detectTabbed(self, lines): """ Find indented text and remove indent before further proccesing. Keyword arguments: * lines: an array of strings Returns: a list of post processed items and the index of last line. """ items = [] blank_line = False # have we encountered a blank line yet? i = 0 # to keep track of where we are def detab(line): match = TABBED_RE.match(line) if match: return match.group(4) for line in lines: if line.strip(): # Non-blank line detabbed_line = detab(line) if detabbed_line: items.append(detabbed_line) i += 1 continue elif not blank_line and not DEF_RE.match(line): # not tabbed but still part of first par. items.append(line) i += 1 continue else: return items, i+1 else: # Blank line: _maybe_ we are done. blank_line = True i += 1 # advance # Find the next non-blank line for j in range(i, len(lines)): if lines[j].strip(): next_line = lines[j]; break else: break # There is no more text; we are done. # Check if the next non-blank line is tabbed if detab(next_line): # Yes, more work to do. items.append("") continue else: break # No, we are done. else: i += 1 return items, i class FootnotePattern(Pattern): """ InlinePattern for footnote markers in a document's body text. """ def __init__(self, pattern, footnotes): super(FootnotePattern, self).__init__(pattern) self.footnotes = footnotes def handleMatch(self, m): id = m.group(2) if id in self.footnotes.footnotes.keys(): sup = etree.Element("sup") a = etree.SubElement(sup, "a") sup.set('id', self.footnotes.makeFootnoteRefId(id)) a.set('href', '#' + self.footnotes.makeFootnoteId(id)) if self.footnotes.md.output_format not in ['html5', 'xhtml5']: a.set('rel', 'footnote') # invalid in HTML5 a.set('class', 'footnote-ref') a.text = text_type(self.footnotes.footnotes.index(id) + 1) return sup else: return None class FootnoteTreeprocessor(Treeprocessor): """ Build and append footnote div to end of document. """ def __init__ (self, footnotes): self.footnotes = footnotes def run(self, root): footnotesDiv = self.footnotes.makeFootnotesDiv(root) if footnotesDiv: result = self.footnotes.findFootnotesPlaceholder(root) if result: child, parent, isText = result ind = parent.getchildren().index(child) if isText: parent.remove(child) parent.insert(ind, footnotesDiv) else: parent.insert(ind + 1, footnotesDiv) child.tail = None else: root.append(footnotesDiv) class FootnotePostprocessor(Postprocessor): """ Replace placeholders with html entities. """ def __init__(self, footnotes): self.footnotes = footnotes def run(self, text): text = text.replace(FN_BACKLINK_TEXT, self.footnotes.getConfig("BACKLINK_TEXT")) return text.replace(NBSP_PLACEHOLDER, " ") def makeExtension(*args, **kwargs): """ Return an instance of the FootnoteExtension """ return FootnoteExtension(*args, **kwargs)