Home | History | Annotate | Download | only in extensions
      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                        ["&#8617;",
     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, "&#160;")
    341 
    342 def makeExtension(configs=[]):
    343     """ Return an instance of the FootnoteExtension """
    344     return FootnoteExtension(configs=configs)
    345 
    346