1 # markdown is released under the BSD license 2 # Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later) 3 # Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) 4 # Copyright 2004 Manfred Stienstra (the original version) 5 # 6 # All rights reserved. 7 # 8 # Redistribution and use in source and binary forms, with or without 9 # modification, are permitted provided that the following conditions are met: 10 # 11 # * Redistributions of source code must retain the above copyright 12 # notice, this list of conditions and the following disclaimer. 13 # * Redistributions in binary form must reproduce the above copyright 14 # notice, this list of conditions and the following disclaimer in the 15 # documentation and/or other materials provided with the distribution. 16 # * Neither the name of the <organization> nor the 17 # names of its contributors may be used to endorse or promote products 18 # derived from this software without specific prior written permission. 19 # 20 # THIS SOFTWARE IS PROVIDED BY THE PYTHON MARKDOWN PROJECT ''AS IS'' AND ANY 21 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 # DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTORS TO THE PYTHON MARKDOWN PROJECT 24 # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 # POSSIBILITY OF SUCH DAMAGE. 31 32 33 """ 34 ========================= FOOTNOTES ================================= 35 36 This section adds footnote handling to markdown. It can be used as 37 an example for extending python-markdown with relatively complex 38 functionality. While in this case the extension is included inside 39 the module itself, it could just as easily be added from outside the 40 module. Not that all markdown classes above are ignorant about 41 footnotes. All footnote functionality is provided separately and 42 then added to the markdown instance at the run time. 43 44 Footnote functionality is attached by calling extendMarkdown() 45 method of FootnoteExtension. The method also registers the 46 extension to allow it's state to be reset by a call to reset() 47 method. 48 49 Example: 50 Footnotes[^1] have a label[^label] and a definition[^!DEF]. 51 52 [^1]: This is a footnote 53 [^label]: A footnote on "label" 54 [^!DEF]: The footnote for definition 55 56 """ 57 58 from __future__ import absolute_import 59 from __future__ import unicode_literals 60 from . import Extension 61 from ..preprocessors import Preprocessor 62 from ..inlinepatterns import Pattern 63 from ..treeprocessors import Treeprocessor 64 from ..postprocessors import Postprocessor 65 from ..util import etree, text_type 66 from ..odict import OrderedDict 67 import re 68 69 FN_BACKLINK_TEXT = "zz1337820767766393qq" 70 NBSP_PLACEHOLDER = "qq3936677670287331zz" 71 DEF_RE = re.compile(r'[ ]{0,3}\[\^([^\]]*)\]:\s*(.*)') 72 TABBED_RE = re.compile(r'((\t)|( ))(.*)') 73 74 class FootnoteExtension(Extension): 75 """ Footnote Extension. """ 76 77 def __init__ (self, configs): 78 """ Setup configs. """ 79 self.config = {'PLACE_MARKER': 80 ["///Footnotes Go Here///", 81 "The text string that marks where the footnotes go"], 82 'UNIQUE_IDS': 83 [False, 84 "Avoid name collisions across " 85 "multiple calls to reset()."], 86 "BACKLINK_TEXT": 87 ["↩", 88 "The text string that links from the footnote to the reader's place."] 89 } 90 91 for key, value in configs: 92 self.config[key][0] = value 93 94 # In multiple invocations, emit links that don't get tangled. 95 self.unique_prefix = 0 96 97 self.reset() 98 99 def extendMarkdown(self, md, md_globals): 100 """ Add pieces to Markdown. """ 101 md.registerExtension(self) 102 self.parser = md.parser 103 self.md = md 104 self.sep = ':' 105 if self.md.output_format in ['html5', 'xhtml5']: 106 self.sep = '-' 107 # Insert a preprocessor before ReferencePreprocessor 108 md.preprocessors.add("footnote", FootnotePreprocessor(self), 109 "<reference") 110 # Insert an inline pattern before ImageReferencePattern 111 FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah 112 md.inlinePatterns.add("footnote", FootnotePattern(FOOTNOTE_RE, self), 113 "<reference") 114 # Insert a tree-processor that would actually add the footnote div 115 # This must be before all other treeprocessors (i.e., inline and 116 # codehilite) so they can run on the the contents of the div. 117 md.treeprocessors.add("footnote", FootnoteTreeprocessor(self), 118 "_begin") 119 # Insert a postprocessor after amp_substitute oricessor 120 md.postprocessors.add("footnote", FootnotePostprocessor(self), 121 ">amp_substitute") 122 123 def reset(self): 124 """ Clear the footnotes on reset, and prepare for a distinct document. """ 125 self.footnotes = OrderedDict() 126 self.unique_prefix += 1 127 128 def findFootnotesPlaceholder(self, root): 129 """ Return ElementTree Element that contains Footnote placeholder. """ 130 def finder(element): 131 for child in element: 132 if child.text: 133 if child.text.find(self.getConfig("PLACE_MARKER")) > -1: 134 return child, element, True 135 if child.tail: 136 if child.tail.find(self.getConfig("PLACE_MARKER")) > -1: 137 return child, element, False 138 finder(child) 139 return None 140 141 res = finder(root) 142 return res 143 144 def setFootnote(self, id, text): 145 """ Store a footnote for later retrieval. """ 146 self.footnotes[id] = text 147 148 def makeFootnoteId(self, id): 149 """ Return footnote link id. """ 150 if self.getConfig("UNIQUE_IDS"): 151 return 'fn%s%d-%s' % (self.sep, self.unique_prefix, id) 152 else: 153 return 'fn%s%s' % (self.sep, id) 154 155 def makeFootnoteRefId(self, id): 156 """ Return footnote back-link id. """ 157 if self.getConfig("UNIQUE_IDS"): 158 return 'fnref%s%d-%s' % (self.sep, self.unique_prefix, id) 159 else: 160 return 'fnref%s%s' % (self.sep, id) 161 162 def makeFootnotesDiv(self, root): 163 """ Return div of footnotes as et Element. """ 164 165 if not list(self.footnotes.keys()): 166 return None 167 168 div = etree.Element("div") 169 div.set('class', 'footnote') 170 etree.SubElement(div, "hr") 171 ol = etree.SubElement(div, "ol") 172 173 for id in self.footnotes.keys(): 174 li = etree.SubElement(ol, "li") 175 li.set("id", self.makeFootnoteId(id)) 176 self.parser.parseChunk(li, self.footnotes[id]) 177 backlink = etree.Element("a") 178 backlink.set("href", "#" + self.makeFootnoteRefId(id)) 179 if self.md.output_format not in ['html5', 'xhtml5']: 180 backlink.set("rev", "footnote") # Invalid in HTML5 181 backlink.set("class", "footnote-backref") 182 backlink.set("title", "Jump back to footnote %d in the text" % \ 183 (self.footnotes.index(id)+1)) 184 backlink.text = FN_BACKLINK_TEXT 185 186 if li.getchildren(): 187 node = li[-1] 188 if node.tag == "p": 189 node.text = node.text + NBSP_PLACEHOLDER 190 node.append(backlink) 191 else: 192 p = etree.SubElement(li, "p") 193 p.append(backlink) 194 return div 195 196 197 class FootnotePreprocessor(Preprocessor): 198 """ Find all footnote references and store for later use. """ 199 200 def __init__ (self, footnotes): 201 self.footnotes = footnotes 202 203 def run(self, lines): 204 """ 205 Loop through lines and find, set, and remove footnote definitions. 206 207 Keywords: 208 209 * lines: A list of lines of text 210 211 Return: A list of lines of text with footnote definitions removed. 212 213 """ 214 newlines = [] 215 i = 0 216 while True: 217 m = DEF_RE.match(lines[i]) 218 if m: 219 fn, _i = self.detectTabbed(lines[i+1:]) 220 fn.insert(0, m.group(2)) 221 i += _i-1 # skip past footnote 222 self.footnotes.setFootnote(m.group(1), "\n".join(fn)) 223 else: 224 newlines.append(lines[i]) 225 if len(lines) > i+1: 226 i += 1 227 else: 228 break 229 return newlines 230 231 def detectTabbed(self, lines): 232 """ Find indented text and remove indent before further proccesing. 233 234 Keyword arguments: 235 236 * lines: an array of strings 237 238 Returns: a list of post processed items and the index of last line. 239 240 """ 241 items = [] 242 blank_line = False # have we encountered a blank line yet? 243 i = 0 # to keep track of where we are 244 245 def detab(line): 246 match = TABBED_RE.match(line) 247 if match: 248 return match.group(4) 249 250 for line in lines: 251 if line.strip(): # Non-blank line 252 detabbed_line = detab(line) 253 if detabbed_line: 254 items.append(detabbed_line) 255 i += 1 256 continue 257 elif not blank_line and not DEF_RE.match(line): 258 # not tabbed but still part of first par. 259 items.append(line) 260 i += 1 261 continue 262 else: 263 return items, i+1 264 265 else: # Blank line: _maybe_ we are done. 266 blank_line = True 267 i += 1 # advance 268 269 # Find the next non-blank line 270 for j in range(i, len(lines)): 271 if lines[j].strip(): 272 next_line = lines[j]; break 273 else: 274 break # There is no more text; we are done. 275 276 # Check if the next non-blank line is tabbed 277 if detab(next_line): # Yes, more work to do. 278 items.append("") 279 continue 280 else: 281 break # No, we are done. 282 else: 283 i += 1 284 285 return items, i 286 287 288 class FootnotePattern(Pattern): 289 """ InlinePattern for footnote markers in a document's body text. """ 290 291 def __init__(self, pattern, footnotes): 292 super(FootnotePattern, self).__init__(pattern) 293 self.footnotes = footnotes 294 295 def handleMatch(self, m): 296 id = m.group(2) 297 if id in self.footnotes.footnotes.keys(): 298 sup = etree.Element("sup") 299 a = etree.SubElement(sup, "a") 300 sup.set('id', self.footnotes.makeFootnoteRefId(id)) 301 a.set('href', '#' + self.footnotes.makeFootnoteId(id)) 302 if self.footnotes.md.output_format not in ['html5', 'xhtml5']: 303 a.set('rel', 'footnote') # invalid in HTML5 304 a.set('class', 'footnote-ref') 305 a.text = text_type(self.footnotes.footnotes.index(id) + 1) 306 return sup 307 else: 308 return None 309 310 311 class FootnoteTreeprocessor(Treeprocessor): 312 """ Build and append footnote div to end of document. """ 313 314 def __init__ (self, footnotes): 315 self.footnotes = footnotes 316 317 def run(self, root): 318 footnotesDiv = self.footnotes.makeFootnotesDiv(root) 319 if footnotesDiv: 320 result = self.footnotes.findFootnotesPlaceholder(root) 321 if result: 322 child, parent, isText = result 323 ind = parent.getchildren().index(child) 324 if isText: 325 parent.remove(child) 326 parent.insert(ind, footnotesDiv) 327 else: 328 parent.insert(ind + 1, footnotesDiv) 329 child.tail = None 330 else: 331 root.append(footnotesDiv) 332 333 class FootnotePostprocessor(Postprocessor): 334 """ Replace placeholders with html entities. """ 335 def __init__(self, footnotes): 336 self.footnotes = footnotes 337 338 def run(self, text): 339 text = text.replace(FN_BACKLINK_TEXT, self.footnotes.getConfig("BACKLINK_TEXT")) 340 return text.replace(NBSP_PLACEHOLDER, " ") 341 342 def makeExtension(configs=[]): 343 """ Return an instance of the FootnoteExtension """ 344 return FootnoteExtension(configs=configs) 345 346