aboutsummaryrefslogtreecommitdiffstats
path: root/mdx_footnotes.py
diff options
context:
space:
mode:
Diffstat (limited to 'mdx_footnotes.py')
-rw-r--r--mdx_footnotes.py252
1 files changed, 252 insertions, 0 deletions
diff --git a/mdx_footnotes.py b/mdx_footnotes.py
new file mode 100644
index 0000000..56ffd3a
--- /dev/null
+++ b/mdx_footnotes.py
@@ -0,0 +1,252 @@
+"""
+========================= FOOTNOTES =================================
+
+This section adds footnote handling to markdown. It can be used as
+an example for extending python-markdown with relatively complex
+functionality. While in this case the extension is included inside
+the module itself, it could just as easily be added from outside the
+module. Not that all markdown classes above are ignorant about
+footnotes. All footnote functionality is provided separately and
+then added to the markdown instance at the run time.
+
+Footnote functionality is attached by calling extendMarkdown()
+method of FootnoteExtension. The method also registers the
+extension to allow it's state to be reset by a call to reset()
+method.
+"""
+
+FN_BACKLINK_TEXT = "zz1337820767766393qq"
+
+
+import re, markdown, random
+
+class FootnoteExtension (markdown.Extension):
+
+ DEF_RE = re.compile(r'(\ ?\ ?\ ?)\[\^([^\]]*)\]:\s*(.*)')
+ SHORT_USE_RE = re.compile(r'\[\^([^\]]*)\]', re.M) # [^a]
+
+ def __init__ (self, configs) :
+
+ self.config = {'PLACE_MARKER' :
+ ["///Footnotes Go Here///",
+ "The text string that marks where the footnotes go"]}
+
+ for key, value in configs :
+ self.config[key][0] = value
+
+ self.reset()
+
+ def extendMarkdown(self, md, md_globals) :
+
+ self.md = md
+
+ # Stateless extensions do not need to be registered
+ md.registerExtension(self)
+
+ # Insert a preprocessor before ReferencePreprocessor
+ index = md.preprocessors.index(md_globals['REFERENCE_PREPROCESSOR'])
+ preprocessor = FootnotePreprocessor(self)
+ preprocessor.md = md
+ md.preprocessors.insert(index, preprocessor)
+
+ # Insert an inline pattern before ImageReferencePattern
+ FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah
+ index = md.inlinePatterns.index(md_globals['IMAGE_REFERENCE_PATTERN'])
+ md.inlinePatterns.insert(index, FootnotePattern(FOOTNOTE_RE, self))
+
+ # Insert a post-processor that would actually add the footnote div
+ postprocessor = FootnotePostprocessor(self)
+ postprocessor.extension = self
+
+ md.postprocessors.append(postprocessor)
+
+ textPostprocessor = FootnoteTextPostprocessor(self)
+
+ md.textPostprocessors.append(textPostprocessor)
+
+
+ def reset(self) :
+ # May be called by Markdown is state reset is desired
+
+ self.footnote_suffix = "-" + str(int(random.random()*1000000000))
+ self.used_footnotes={}
+ self.footnotes = {}
+
+ def findFootnotesPlaceholder(self, doc) :
+ def findFootnotePlaceholderFn(node=None, indent=0):
+ if node.type == 'text':
+ if node.value.find(self.getConfig("PLACE_MARKER")) > -1 :
+ return True
+
+ fn_div_list = doc.find(findFootnotePlaceholderFn)
+ if fn_div_list :
+ return fn_div_list[0]
+
+
+ def setFootnote(self, id, text) :
+ self.footnotes[id] = text
+
+ def makeFootnoteId(self, num) :
+ return 'fn%d%s' % (num, self.footnote_suffix)
+
+ def makeFootnoteRefId(self, num) :
+ return 'fnr%d%s' % (num, self.footnote_suffix)
+
+ def makeFootnotesDiv (self, doc) :
+ """Creates the div with class='footnote' and populates it with
+ the text of the footnotes.
+
+ @returns: the footnote div as a dom element """
+
+ if not self.footnotes.keys() :
+ return None
+
+ div = doc.createElement("div")
+ div.setAttribute('class', 'footnote')
+ hr = doc.createElement("hr")
+ div.appendChild(hr)
+ ol = doc.createElement("ol")
+ div.appendChild(ol)
+
+ footnotes = [(self.used_footnotes[id], id)
+ for id in self.footnotes.keys()]
+ footnotes.sort()
+
+ for i, id in footnotes :
+ li = doc.createElement('li')
+ li.setAttribute('id', self.makeFootnoteId(i))
+
+ self.md._processSection(li, self.footnotes[id].split("\n"))
+
+ #li.appendChild(doc.createTextNode(self.footnotes[id]))
+
+ backlink = doc.createElement('a')
+ backlink.setAttribute('href', '#' + self.makeFootnoteRefId(i))
+ backlink.setAttribute('class', 'footnoteBackLink')
+ backlink.setAttribute('title',
+ 'Jump back to footnote %d in the text' % 1)
+ backlink.appendChild(doc.createTextNode(FN_BACKLINK_TEXT))
+
+ if li.childNodes :
+ node = li.childNodes[-1]
+ if node.type == "text" :
+ node = li
+ node.appendChild(backlink)
+
+ ol.appendChild(li)
+
+ return div
+
+
+class FootnotePreprocessor :
+
+ def __init__ (self, footnotes) :
+ self.footnotes = footnotes
+
+ def run(self, lines) :
+
+ self.blockGuru = markdown.BlockGuru()
+ lines = self._handleFootnoteDefinitions (lines)
+
+ # Make a hash of all footnote marks in the text so that we
+ # know in what order they are supposed to appear. (This
+ # function call doesn't really substitute anything - it's just
+ # a way to get a callback for each occurence.
+
+ text = "\n".join(lines)
+ self.footnotes.SHORT_USE_RE.sub(self.recordFootnoteUse, text)
+
+ return text.split("\n")
+
+
+ def recordFootnoteUse(self, match) :
+
+ id = match.group(1)
+ id = id.strip()
+ nextNum = len(self.footnotes.used_footnotes.keys()) + 1
+ self.footnotes.used_footnotes[id] = nextNum
+
+
+ def _handleFootnoteDefinitions(self, lines) :
+ """Recursively finds all footnote definitions in the lines.
+
+ @param lines: a list of lines of text
+ @returns: a string representing the text with footnote
+ definitions removed """
+
+ i, id, footnote = self._findFootnoteDefinition(lines)
+
+ if id :
+
+ plain = lines[:i]
+
+ detabbed, theRest = self.blockGuru.detectTabbed(lines[i+1:])
+
+ self.footnotes.setFootnote(id,
+ footnote + "\n"
+ + "\n".join(detabbed))
+
+ more_plain = self._handleFootnoteDefinitions(theRest)
+ return plain + [""] + more_plain
+
+ else :
+ return lines
+
+ def _findFootnoteDefinition(self, lines) :
+ """Finds the first line of a footnote definition.
+
+ @param lines: a list of lines of text
+ @returns: the index of the line containing a footnote definition """
+
+ counter = 0
+ for line in lines :
+ m = self.footnotes.DEF_RE.match(line)
+ if m :
+ return counter, m.group(2), m.group(3)
+ counter += 1
+ return counter, None, None
+
+
+class FootnotePattern (markdown.Pattern) :
+
+ def __init__ (self, pattern, footnotes) :
+
+ markdown.Pattern.__init__(self, pattern)
+ self.footnotes = footnotes
+
+ def handleMatch(self, m, doc) :
+ sup = doc.createElement('sup')
+ a = doc.createElement('a')
+ sup.appendChild(a)
+ id = m.group(2)
+ num = self.footnotes.used_footnotes[id]
+ sup.setAttribute('id', self.footnotes.makeFootnoteRefId(num))
+ a.setAttribute('href', '#' + self.footnotes.makeFootnoteId(num))
+ a.appendChild(doc.createTextNode(str(num)))
+ return sup
+
+class FootnotePostprocessor (markdown.Postprocessor):
+
+ def __init__ (self, footnotes) :
+ self.footnotes = footnotes
+
+ def run(self, doc) :
+ footnotesDiv = self.footnotes.makeFootnotesDiv(doc)
+ if footnotesDiv :
+ fnPlaceholder = self.extension.findFootnotesPlaceholder(doc)
+ if fnPlaceholder :
+ fnPlaceholder.parent.replaceChild(fnPlaceholder, footnotesDiv)
+ else :
+ doc.documentElement.appendChild(footnotesDiv)
+
+class FootnoteTextPostprocessor (markdown.Postprocessor):
+
+ def __init__ (self, footnotes) :
+ self.footnotes = footnotes
+
+ def run(self, text) :
+ return text.replace(FN_BACKLINK_TEXT, "↩")
+
+def makeExtension(configs=None) :
+ return FootnoteExtension(configs=configs)
+