OLD | NEW |
| (Empty) |
1 """ | |
2 Footnotes Extension for Python-Markdown | |
3 ======================================= | |
4 | |
5 Adds footnote handling to Python-Markdown. | |
6 | |
7 See <https://pythonhosted.org/Markdown/extensions/footnotes.html> | |
8 for documentation. | |
9 | |
10 Copyright The Python Markdown Project | |
11 | |
12 License: [BSD](http://www.opensource.org/licenses/bsd-license.php) | |
13 | |
14 """ | |
15 | |
16 from __future__ import absolute_import | |
17 from __future__ import unicode_literals | |
18 from . import Extension | |
19 from ..preprocessors import Preprocessor | |
20 from ..inlinepatterns import Pattern | |
21 from ..treeprocessors import Treeprocessor | |
22 from ..postprocessors import Postprocessor | |
23 from ..util import etree, text_type | |
24 from ..odict import OrderedDict | |
25 import re | |
26 | |
27 FN_BACKLINK_TEXT = "zz1337820767766393qq" | |
28 NBSP_PLACEHOLDER = "qq3936677670287331zz" | |
29 DEF_RE = re.compile(r'[ ]{0,3}\[\^([^\]]*)\]:\s*(.*)') | |
30 TABBED_RE = re.compile(r'((\t)|( ))(.*)') | |
31 | |
32 | |
33 class 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 | |
167 class 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 | |
259 class 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 | |
282 class 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 | |
305 class 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 | |
317 def makeExtension(*args, **kwargs): | |
318 """ Return an instance of the FootnoteExtension """ | |
319 return FootnoteExtension(*args, **kwargs) | |
OLD | NEW |