footnotes.py 13 KB

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