Last active
June 1, 2016 18:27
-
-
Save apas/fbdcc1617be4b9dbcab8895ad028b285 to your computer and use it in GitHub Desktop.
footnotes extension Python Markdown
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Footnotes Extension for Python-Markdown | |
| ======================================= | |
| Adds footnote handling to Python-Markdown. | |
| See <https://pythonhosted.org/Markdown/extensions/footnotes.html> | |
| 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), "<reference" | |
| ) | |
| # Insert an inline pattern before ImageReferencePattern | |
| FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah | |
| md.inlinePatterns.add( | |
| "footnote", FootnotePattern(FOOTNOTE_RE, self), "<reference" | |
| ) | |
| # Insert a tree-processor that would actually add the footnote div | |
| # This must be before all other treeprocessors (i.e., inline and | |
| # codehilite) so they can run on the the contents of the div. | |
| md.treeprocessors.add( | |
| "footnote", FootnoteTreeprocessor(self), "_begin" | |
| ) | |
| # Insert a postprocessor after amp_substitute oricessor | |
| md.postprocessors.add( | |
| "footnote", FootnotePostprocessor(self), ">amp_substitute" | |
| ) | |
| def reset(self): | |
| """ Clear footnotes on reset, and prepare for 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 | |
| child_res = finder(child) | |
| if child_res is not None: | |
| return child_res | |
| 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") | |
| # apas changes | |
| span = etree.Element("span") | |
| span.set("class", "sidenote") | |
| for id in self.footnotes.keys(): | |
| span.text = self.footnotes.values()[0] | |
| # 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 | |
| # node = span | |
| # if node.tag == "p": | |
| # node.text = node.text | |
| # if li.getchildren(): | |
| # node = li[-1] | |
| # if node.tag == "p": | |
| # node.text = node.text + NBSP_PLACEHOLDER + node.text | |
| # node.append(backlink) | |
| # else: | |
| # p = etree.SubElement(li, "p") | |
| # p.append(backlink) | |
| return span | |
| 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 | |
| global tries | |
| tries = 0 | |
| 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 | |
| # apas changes | |
| label = etree.Element("label") | |
| label.set("for", "sn-anything") | |
| label.set("class", "margin-toggle sidenote-number") | |
| inputlink = etree.SubElement(label, "input") | |
| inputlink.set("type", "checkbox") | |
| inputlink.set("id", "sn-anything") | |
| inputlink.set("class", "margin-toggle") | |
| i = 0 | |
| try: | |
| # span = etree.SubElement(label, "span") | |
| span = etree.Element("span") | |
| span.set("class", "sidenote") | |
| global tries | |
| span.text = self.footnotes.footnotes.values()[tries] | |
| tries = tries + 1 | |
| res = label.append(span) | |
| except Exception as e: | |
| print e | |
| return label | |
| 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): | |
| global footnotesDiv | |
| footnotesDiv = self.footnotes.makeFootnotesDiv(root) | |
| if footnotesDiv is not None: | |
| result = self.footnotes.findFootnotesPlaceholder(root) | |
| if result: | |
| print "inside" | |
| 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: | |
| pass | |
| # 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment