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)
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions are met:
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.
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.
34 ========================= FOOTNOTES =================================
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.
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()
50 Footnotes[^1] have a label[^label] and a definition[^!DEF].
52 [^1]: This is a footnote
53 [^label]: A footnote on "label"
54 [^!DEF]: The footnote for definition
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
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)|( ))(.*)')
74 class FootnoteExtension(Extension
):
75 """ Footnote Extension. """
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"],
84 "Avoid name collisions across "
85 "multiple calls to reset()."],
88 "The text string that links from the footnote to the reader's place."]
91 for key
, value
in configs
:
92 self
.config
[key
][0] = value
94 # In multiple invocations, emit links that don't get tangled.
95 self
.unique_prefix
= 0
99 def extendMarkdown(self
, md
, md_globals
):
100 """ Add pieces to Markdown. """
101 md
.registerExtension(self
)
102 self
.parser
= md
.parser
105 if self
.md
.output_format
in ['html5', 'xhtml5']:
107 # Insert a preprocessor before ReferencePreprocessor
108 md
.preprocessors
.add("footnote", FootnotePreprocessor(self
),
110 # Insert an inline pattern before ImageReferencePattern
111 FOOTNOTE_RE
= r
'\[\^([^\]]*)\]' # blah blah [^1] blah
112 md
.inlinePatterns
.add("footnote", FootnotePattern(FOOTNOTE_RE
, self
),
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
),
119 # Insert a postprocessor after amp_substitute oricessor
120 md
.postprocessors
.add("footnote", FootnotePostprocessor(self
),
124 """ Clear the footnotes on reset, and prepare for a distinct document. """
125 self
.footnotes
= OrderedDict()
126 self
.unique_prefix
+= 1
128 def findFootnotesPlaceholder(self
, root
):
129 """ Return ElementTree Element that contains Footnote placeholder. """
131 for child
in element
:
133 if child
.text
.find(self
.getConfig("PLACE_MARKER")) > -1:
134 return child
, element
, True
136 if child
.tail
.find(self
.getConfig("PLACE_MARKER")) > -1:
137 return child
, element
, False
144 def setFootnote(self
, id, text
):
145 """ Store a footnote for later retrieval. """
146 self
.footnotes
[id] = text
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)
153 return 'fn%s%s' % (self
.sep
, id)
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)
160 return 'fnref%s%s' % (self
.sep
, id)
162 def makeFootnotesDiv(self
, root
):
163 """ Return div of footnotes as et Element. """
165 if not list(self
.footnotes
.keys()):
168 div
= etree
.Element("div")
169 div
.set('class', 'footnote')
170 etree
.SubElement(div
, "hr")
171 ol
= etree
.SubElement(div
, "ol")
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
189 node
.text
= node
.text
+ NBSP_PLACEHOLDER
190 node
.append(backlink
)
192 p
= etree
.SubElement(li
, "p")
197 class FootnotePreprocessor(Preprocessor
):
198 """ Find all footnote references and store for later use. """
200 def __init__ (self
, footnotes
):
201 self
.footnotes
= footnotes
203 def run(self
, lines
):
205 Loop through lines and find, set, and remove footnote definitions.
209 * lines: A list of lines of text
211 Return: A list of lines of text with footnote definitions removed.
217 m
= DEF_RE
.match(lines
[i
])
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
))
224 newlines
.append(lines
[i
])
231 def detectTabbed(self
, lines
):
232 """ Find indented text and remove indent before further proccesing.
236 * lines: an array of strings
238 Returns: a list of post processed items and the index of last line.
242 blank_line
= False # have we encountered a blank line yet?
243 i
= 0 # to keep track of where we are
246 match
= TABBED_RE
.match(line
)
248 return match
.group(4)
251 if line
.strip(): # Non-blank line
252 detabbed_line
= detab(line
)
254 items
.append(detabbed_line
)
257 elif not blank_line
and not DEF_RE
.match(line
):
258 # not tabbed but still part of first par.
265 else: # Blank line: _maybe_ we are done.
269 # Find the next non-blank line
270 for j
in range(i
, len(lines
)):
272 next_line
= lines
[j
]; break
274 break # There is no more text; we are done.
276 # Check if the next non-blank line is tabbed
277 if detab(next_line
): # Yes, more work to do.
281 break # No, we are done.
288 class FootnotePattern(Pattern
):
289 """ InlinePattern for footnote markers in a document's body text. """
291 def __init__(self
, pattern
, footnotes
):
292 super(FootnotePattern
, self
).__init
__(pattern
)
293 self
.footnotes
= footnotes
295 def handleMatch(self
, m
):
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)
311 class FootnoteTreeprocessor(Treeprocessor
):
312 """ Build and append footnote div to end of document. """
314 def __init__ (self
, footnotes
):
315 self
.footnotes
= footnotes
318 footnotesDiv
= self
.footnotes
.makeFootnotesDiv(root
)
320 result
= self
.footnotes
.findFootnotesPlaceholder(root
)
322 child
, parent
, isText
= result
323 ind
= parent
.getchildren().index(child
)
326 parent
.insert(ind
, footnotesDiv
)
328 parent
.insert(ind
+ 1, footnotesDiv
)
331 root
.append(footnotesDiv
)
333 class FootnotePostprocessor(Postprocessor
):
334 """ Replace placeholders with html entities. """
335 def __init__(self
, footnotes
):
336 self
.footnotes
= footnotes
339 text
= text
.replace(FN_BACKLINK_TEXT
, self
.footnotes
.getConfig("BACKLINK_TEXT"))
340 return text
.replace(NBSP_PLACEHOLDER
, " ")
342 def makeExtension(configs
=[]):
343 """ Return an instance of the FootnoteExtension """
344 return FootnoteExtension(configs
=configs
)