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