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