PageRenderTime 52ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/Quicksilver/Tools/python-support/markdown/extensions/footnotes.py

http://github.com/quicksilver/Quicksilver
Python | 431 lines | 347 code | 32 blank | 52 comment | 26 complexity | 8644085aa1111891822ae5ffd8f71412 MD5 | raw file
Possible License(s): Apache-2.0
  1. """
  2. Footnotes Extension for Python-Markdown
  3. =======================================
  4. Adds footnote handling to Python-Markdown.
  5. See <https://Python-Markdown.github.io/extensions/footnotes>
  6. for documentation.
  7. Copyright The Python Markdown Project
  8. License: [BSD](http://www.opensource.org/licenses/bsd-license.php)
  9. """
  10. from __future__ import absolute_import
  11. from __future__ import unicode_literals
  12. from . import Extension
  13. from ..preprocessors import Preprocessor
  14. from ..inlinepatterns import Pattern
  15. from ..treeprocessors import Treeprocessor
  16. from ..postprocessors import Postprocessor
  17. from .. import util
  18. from ..odict import OrderedDict
  19. import re
  20. import copy
  21. FN_BACKLINK_TEXT = util.STX + "zz1337820767766393qq" + util.ETX
  22. NBSP_PLACEHOLDER = util.STX + "qq3936677670287331zz" + util.ETX
  23. DEF_RE = re.compile(r'[ ]{0,3}\[\^([^\]]*)\]:\s*(.*)')
  24. TABBED_RE = re.compile(r'((\t)|( ))(.*)')
  25. RE_REF_ID = re.compile(r'(fnref)(\d+)')
  26. class FootnoteExtension(Extension):
  27. """ Footnote Extension. """
  28. def __init__(self, *args, **kwargs):
  29. """ Setup configs. """
  30. self.config = {
  31. 'PLACE_MARKER':
  32. ["///Footnotes Go Here///",
  33. "The text string that marks where the footnotes go"],
  34. 'UNIQUE_IDS':
  35. [False,
  36. "Avoid name collisions across "
  37. "multiple calls to reset()."],
  38. "BACKLINK_TEXT":
  39. ["&#8617;",
  40. "The text string that links from the footnote "
  41. "to the reader's place."],
  42. "BACKLINK_TITLE":
  43. ["Jump back to footnote %d in the text",
  44. "The text string used for the title HTML attribute "
  45. "of the backlink. %d will be replaced by the "
  46. "footnote number."]
  47. }
  48. super(FootnoteExtension, self).__init__(*args, **kwargs)
  49. # In multiple invocations, emit links that don't get tangled.
  50. self.unique_prefix = 0
  51. self.found_refs = {}
  52. self.used_refs = set()
  53. self.reset()
  54. def extendMarkdown(self, md, md_globals):
  55. """ Add pieces to Markdown. """
  56. md.registerExtension(self)
  57. self.parser = md.parser
  58. self.md = md
  59. # Insert a preprocessor before ReferencePreprocessor
  60. md.preprocessors.add(
  61. "footnote", FootnotePreprocessor(self), "<reference"
  62. )
  63. # Insert an inline pattern before ImageReferencePattern
  64. FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah
  65. md.inlinePatterns.add(
  66. "footnote", FootnotePattern(FOOTNOTE_RE, self), "<reference"
  67. )
  68. # Insert a tree-processor that would actually add the footnote div
  69. # This must be before all other treeprocessors (i.e., inline and
  70. # codehilite) so they can run on the the contents of the div.
  71. md.treeprocessors.add(
  72. "footnote", FootnoteTreeprocessor(self), "_begin"
  73. )
  74. # Insert a tree-processor that will run after inline is done.
  75. # In this tree-processor we want to check our duplicate footnote tracker
  76. # And add additional backrefs to the footnote pointing back to the
  77. # duplicated references.
  78. md.treeprocessors.add(
  79. "footnote-duplicate", FootnotePostTreeprocessor(self), '>inline'
  80. )
  81. # Insert a postprocessor after amp_substitute oricessor
  82. md.postprocessors.add(
  83. "footnote", FootnotePostprocessor(self), ">amp_substitute"
  84. )
  85. def reset(self):
  86. """ Clear footnotes on reset, and prepare for distinct document. """
  87. self.footnotes = OrderedDict()
  88. self.unique_prefix += 1
  89. self.found_refs = {}
  90. self.used_refs = set()
  91. def unique_ref(self, reference, found=False):
  92. """ Get a unique reference if there are duplicates. """
  93. if not found:
  94. return reference
  95. original_ref = reference
  96. while reference in self.used_refs:
  97. ref, rest = reference.split(self.get_separator(), 1)
  98. m = RE_REF_ID.match(ref)
  99. if m:
  100. reference = '%s%d%s%s' % (m.group(1), int(m.group(2))+1, self.get_separator(), rest)
  101. else:
  102. reference = '%s%d%s%s' % (ref, 2, self.get_separator(), rest)
  103. self.used_refs.add(reference)
  104. if original_ref in self.found_refs:
  105. self.found_refs[original_ref] += 1
  106. else:
  107. self.found_refs[original_ref] = 1
  108. return reference
  109. def findFootnotesPlaceholder(self, root):
  110. """ Return ElementTree Element that contains Footnote placeholder. """
  111. def finder(element):
  112. for child in element:
  113. if child.text:
  114. if child.text.find(self.getConfig("PLACE_MARKER")) > -1:
  115. return child, element, True
  116. if child.tail:
  117. if child.tail.find(self.getConfig("PLACE_MARKER")) > -1:
  118. return child, element, False
  119. child_res = finder(child)
  120. if child_res is not None:
  121. return child_res
  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 get_separator(self):
  129. if self.md.output_format in ['html5', 'xhtml5']:
  130. return '-'
  131. return ':'
  132. def makeFootnoteId(self, id):
  133. """ Return footnote link id. """
  134. if self.getConfig("UNIQUE_IDS"):
  135. return 'fn%s%d-%s' % (self.get_separator(), self.unique_prefix, id)
  136. else:
  137. return 'fn%s%s' % (self.get_separator(), id)
  138. def makeFootnoteRefId(self, id, found=False):
  139. """ Return footnote back-link id. """
  140. if self.getConfig("UNIQUE_IDS"):
  141. return self.unique_ref('fnref%s%d-%s' % (self.get_separator(), self.unique_prefix, id), found)
  142. else:
  143. return self.unique_ref('fnref%s%s' % (self.get_separator(), id), found)
  144. def makeFootnotesDiv(self, root):
  145. """ Return div of footnotes as et Element. """
  146. if not list(self.footnotes.keys()):
  147. return None
  148. div = util.etree.Element("div")
  149. div.set('class', 'footnote')
  150. util.etree.SubElement(div, "hr")
  151. ol = util.etree.SubElement(div, "ol")
  152. surrogate_parent = util.etree.Element("div")
  153. for id in self.footnotes.keys():
  154. li = util.etree.SubElement(ol, "li")
  155. li.set("id", self.makeFootnoteId(id))
  156. # Parse footnote with surrogate parent as li cannot be used.
  157. # List block handlers have special logic to deal with li.
  158. # When we are done parsing, we will copy everything over to li.
  159. self.parser.parseChunk(surrogate_parent, self.footnotes[id])
  160. for el in list(surrogate_parent):
  161. li.append(el)
  162. surrogate_parent.remove(el)
  163. backlink = util.etree.Element("a")
  164. backlink.set("href", "#" + self.makeFootnoteRefId(id))
  165. if self.md.output_format not in ['html5', 'xhtml5']:
  166. backlink.set("rev", "footnote") # Invalid in HTML5
  167. backlink.set("class", "footnote-backref")
  168. backlink.set(
  169. "title",
  170. self.getConfig("BACKLINK_TITLE") %
  171. (self.footnotes.index(id)+1)
  172. )
  173. backlink.text = FN_BACKLINK_TEXT
  174. if len(li):
  175. node = li[-1]
  176. if node.tag == "p":
  177. node.text = node.text + NBSP_PLACEHOLDER
  178. node.append(backlink)
  179. else:
  180. p = util.etree.SubElement(li, "p")
  181. p.append(backlink)
  182. return div
  183. class FootnotePreprocessor(Preprocessor):
  184. """ Find all footnote references and store for later use. """
  185. def __init__(self, footnotes):
  186. self.footnotes = footnotes
  187. def run(self, lines):
  188. """
  189. Loop through lines and find, set, and remove footnote definitions.
  190. Keywords:
  191. * lines: A list of lines of text
  192. Return: A list of lines of text with footnote definitions removed.
  193. """
  194. newlines = []
  195. i = 0
  196. while True:
  197. m = DEF_RE.match(lines[i])
  198. if m:
  199. fn, _i = self.detectTabbed(lines[i+1:])
  200. fn.insert(0, m.group(2))
  201. i += _i-1 # skip past footnote
  202. footnote = "\n".join(fn)
  203. self.footnotes.setFootnote(m.group(1), footnote.rstrip())
  204. # Preserve a line for each block to prevent raw HTML indexing issue.
  205. # https://github.com/Python-Markdown/markdown/issues/584
  206. num_blocks = (len(footnote.split('\n\n')) * 2)
  207. newlines.extend([''] * (num_blocks))
  208. else:
  209. newlines.append(lines[i])
  210. if len(lines) > i+1:
  211. i += 1
  212. else:
  213. break
  214. return newlines
  215. def detectTabbed(self, lines):
  216. """ Find indented text and remove indent before further proccesing.
  217. Keyword arguments:
  218. * lines: an array of strings
  219. Returns: a list of post processed items and the index of last line.
  220. """
  221. items = []
  222. blank_line = False # have we encountered a blank line yet?
  223. i = 0 # to keep track of where we are
  224. def detab(line):
  225. match = TABBED_RE.match(line)
  226. if match:
  227. return match.group(4)
  228. for line in lines:
  229. if line.strip(): # Non-blank line
  230. detabbed_line = detab(line)
  231. if detabbed_line:
  232. items.append(detabbed_line)
  233. i += 1
  234. continue
  235. elif not blank_line and not DEF_RE.match(line):
  236. # not tabbed but still part of first par.
  237. items.append(line)
  238. i += 1
  239. continue
  240. else:
  241. return items, i+1
  242. else: # Blank line: _maybe_ we are done.
  243. blank_line = True
  244. i += 1 # advance
  245. # Find the next non-blank line
  246. for j in range(i, len(lines)):
  247. if lines[j].strip():
  248. next_line = lines[j]
  249. break
  250. else:
  251. # Include extreaneous padding to prevent raw HTML
  252. # parsing issue: https://github.com/Python-Markdown/markdown/issues/584
  253. items.append("")
  254. i += 1
  255. else:
  256. break # There is no more text; we are done.
  257. # Check if the next non-blank line is tabbed
  258. if detab(next_line): # Yes, more work to do.
  259. items.append("")
  260. continue
  261. else:
  262. break # No, we are done.
  263. else:
  264. i += 1
  265. return items, i
  266. class FootnotePattern(Pattern):
  267. """ InlinePattern for footnote markers in a document's body text. """
  268. def __init__(self, pattern, footnotes):
  269. super(FootnotePattern, self).__init__(pattern)
  270. self.footnotes = footnotes
  271. def handleMatch(self, m):
  272. id = m.group(2)
  273. if id in self.footnotes.footnotes.keys():
  274. sup = util.etree.Element("sup")
  275. a = util.etree.SubElement(sup, "a")
  276. sup.set('id', self.footnotes.makeFootnoteRefId(id, found=True))
  277. a.set('href', '#' + self.footnotes.makeFootnoteId(id))
  278. if self.footnotes.md.output_format not in ['html5', 'xhtml5']:
  279. a.set('rel', 'footnote') # invalid in HTML5
  280. a.set('class', 'footnote-ref')
  281. a.text = util.text_type(self.footnotes.footnotes.index(id) + 1)
  282. return sup
  283. else:
  284. return None
  285. class FootnotePostTreeprocessor(Treeprocessor):
  286. """ Ammend footnote div with duplicates. """
  287. def __init__(self, footnotes):
  288. self.footnotes = footnotes
  289. def add_duplicates(self, li, duplicates):
  290. """ Adjust current li and add the duplicates: fnref2, fnref3, etc. """
  291. for link in li.iter('a'):
  292. # Find the link that needs to be duplicated.
  293. if link.attrib.get('class', '') == 'footnote-backref':
  294. ref, rest = link.attrib['href'].split(self.footnotes.get_separator(), 1)
  295. # Duplicate link the number of times we need to
  296. # and point the to the appropriate references.
  297. links = []
  298. for index in range(2, duplicates + 1):
  299. sib_link = copy.deepcopy(link)
  300. sib_link.attrib['href'] = '%s%d%s%s' % (ref, index, self.footnotes.get_separator(), rest)
  301. links.append(sib_link)
  302. self.offset += 1
  303. # Add all the new duplicate links.
  304. el = list(li)[-1]
  305. for l in links:
  306. el.append(l)
  307. break
  308. def get_num_duplicates(self, li):
  309. """ Get the number of duplicate refs of the footnote. """
  310. fn, rest = li.attrib.get('id', '').split(self.footnotes.get_separator(), 1)
  311. link_id = '%sref%s%s' % (fn, self.footnotes.get_separator(), rest)
  312. return self.footnotes.found_refs.get(link_id, 0)
  313. def handle_duplicates(self, parent):
  314. """ Find duplicate footnotes and format and add the duplicates. """
  315. for li in list(parent):
  316. # Check number of duplicates footnotes and insert
  317. # additional links if needed.
  318. count = self.get_num_duplicates(li)
  319. if count > 1:
  320. self.add_duplicates(li, count)
  321. def run(self, root):
  322. """ Crawl the footnote div and add missing duplicate footnotes. """
  323. self.offset = 0
  324. for div in root.iter('div'):
  325. if div.attrib.get('class', '') == 'footnote':
  326. # Footnotes shoul be under the first orderd list under
  327. # the footnote div. So once we find it, quit.
  328. for ol in div.iter('ol'):
  329. self.handle_duplicates(ol)
  330. break
  331. class FootnoteTreeprocessor(Treeprocessor):
  332. """ Build and append footnote div to end of document. """
  333. def __init__(self, footnotes):
  334. self.footnotes = footnotes
  335. def run(self, root):
  336. footnotesDiv = self.footnotes.makeFootnotesDiv(root)
  337. if footnotesDiv is not None:
  338. result = self.footnotes.findFootnotesPlaceholder(root)
  339. if result:
  340. child, parent, isText = result
  341. ind = list(parent).index(child)
  342. if isText:
  343. parent.remove(child)
  344. parent.insert(ind, footnotesDiv)
  345. else:
  346. parent.insert(ind + 1, footnotesDiv)
  347. child.tail = None
  348. else:
  349. root.append(footnotesDiv)
  350. class FootnotePostprocessor(Postprocessor):
  351. """ Replace placeholders with html entities. """
  352. def __init__(self, footnotes):
  353. self.footnotes = footnotes
  354. def run(self, text):
  355. text = text.replace(
  356. FN_BACKLINK_TEXT, self.footnotes.getConfig("BACKLINK_TEXT")
  357. )
  358. return text.replace(NBSP_PLACEHOLDER, "&#160;")
  359. def makeExtension(*args, **kwargs):
  360. """ Return an instance of the FootnoteExtension """
  361. return FootnoteExtension(*args, **kwargs)