1""" 2========================= FOOTNOTES ================================= 3 4This section adds footnote handling to markdown. It can be used as 5an example for extending python-markdown with relatively complex 6functionality. While in this case the extension is included inside 7the module itself, it could just as easily be added from outside the 8module. Not that all markdown classes above are ignorant about 9footnotes. All footnote functionality is provided separately and 10then added to the markdown instance at the run time. 11 12Footnote functionality is attached by calling extendMarkdown() 13method of FootnoteExtension. The method also registers the 14extension to allow it's state to be reset by a call to reset() 15method. 16 17Example: 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 26import re, markdown 27from markdown import etree 28 29FN_BACKLINK_TEXT = "zz1337820767766393qq" 30NBSP_PLACEHOLDER = "qq3936677670287331zz" 31DEF_RE = re.compile(r'(\ ?\ ?\ ?)\[\^([^\]]*)\]:\s*(.*)') 32TABBED_RE = re.compile(r'((\t)|( ))(.*)') 33 34class 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 147class 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 255class 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 273class 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 297class FootnotePostprocessor(markdown.postprocessors.Postprocessor): 298 """ Replace placeholders with html entities. """ 299 300 def run(self, text): 301 text = text.replace(FN_BACKLINK_TEXT, "↩") 302 return text.replace(NBSP_PLACEHOLDER, " ") 303 304def makeExtension(configs=[]): 305 """ Return an instance of the FootnoteExtension """ 306 return FootnoteExtension(configs=configs) 307 308