diff options
Diffstat (limited to 'src/mistune/plugins/footnotes.py')
-rw-r--r-- | src/mistune/plugins/footnotes.py | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/src/mistune/plugins/footnotes.py b/src/mistune/plugins/footnotes.py new file mode 100644 index 0000000..2e10704 --- /dev/null +++ b/src/mistune/plugins/footnotes.py @@ -0,0 +1,153 @@ +import re +from ..core import BlockState +from ..util import unikey +from ..helpers import LINK_LABEL + +__all__ = ['footnotes'] + +_PARAGRAPH_SPLIT = re.compile(r'\n{2,}') +# https://michelf.ca/projects/php-markdown/extra/#footnotes +REF_FOOTNOTE = ( + r'^(?P<footnote_lead> {0,3})' + r'\[\^(?P<footnote_key>' + LINK_LABEL + r')]:[ \t]' + r'(?P<footnote_text>[^\n]*(?:\n+|$)' + r'(?:(?P=footnote_lead) {1,3}(?! )[^\n]*\n+)*' + r')' +) + +INLINE_FOOTNOTE = r'\[\^(?P<footnote_key>' + LINK_LABEL + r')\]' + + +def parse_inline_footnote(inline, m: re.Match, state): + key = unikey(m.group('footnote_key')) + ref = state.env.get('ref_footnotes') + if ref and key in ref: + notes = state.env.get('footnotes') + if not notes: + notes = [] + if key not in notes: + notes.append(key) + state.env['footnotes'] = notes + state.append_token({ + 'type': 'footnote_ref', + 'raw': key, + 'attrs': {'index': notes.index(key) + 1} + }) + else: + state.append_token({'type': 'text', 'raw': m.group(0)}) + return m.end() + + +def parse_ref_footnote(block, m: re.Match, state: BlockState): + ref = state.env.get('ref_footnotes') + if not ref: + ref = {} + + key = unikey(m.group('footnote_key')) + if key not in ref: + ref[key] = m.group('footnote_text') + state.env['ref_footnotes'] = ref + return m.end() + + +def parse_footnote_item(block, key: str, index: int, state: BlockState): + ref = state.env.get('ref_footnotes') + text = ref[key] + + lines = text.splitlines() + second_line = None + for second_line in lines[1:]: + if second_line: + break + + if second_line: + spaces = len(second_line) - len(second_line.lstrip()) + pattern = re.compile(r'^ {' + str(spaces) + r',}', flags=re.M) + text = pattern.sub('', text).strip() + items = _PARAGRAPH_SPLIT.split(text) + children = [{'type': 'paragraph', 'text': s} for s in items] + else: + text = text.strip() + children = [{'type': 'paragraph', 'text': text}] + return { + 'type': 'footnote_item', + 'children': children, + 'attrs': {'key': key, 'index': index} + } + + +def md_footnotes_hook(md, result: str, state: BlockState): + notes = state.env.get('footnotes') + if not notes: + return result + + children = [ + parse_footnote_item(md.block, k, i + 1, state) + for i, k in enumerate(notes) + ] + state = BlockState() + state.tokens = [{'type': 'footnotes', 'children': children}] + output = md.render_state(state) + return result + output + + +def render_footnote_ref(renderer, key: str, index: int): + i = str(index) + html = '<sup class="footnote-ref" id="fnref-' + i + '">' + return html + '<a href="#fn-' + i + '">' + i + '</a></sup>' + + +def render_footnotes(renderer, text: str): + return '<section class="footnotes">\n<ol>\n' + text + '</ol>\n</section>\n' + + +def render_footnote_item(renderer, text: str, key: str, index: int): + i = str(index) + back = '<a href="#fnref-' + i + '" class="footnote">↩</a>' + text = text.rstrip()[:-4] + back + '</p>' + return '<li id="fn-' + i + '">' + text + '</li>\n' + + +def footnotes(md): + """A mistune plugin to support footnotes, spec defined at + https://michelf.ca/projects/php-markdown/extra/#footnotes + + Here is an example: + + .. code-block:: text + + That's some text with a footnote.[^1] + + [^1]: And that's the footnote. + + It will be converted into HTML: + + .. code-block:: html + + <p>That's some text with a footnote.<sup class="footnote-ref" id="fnref-1"><a href="#fn-1">1</a></sup></p> + <section class="footnotes"> + <ol> + <li id="fn-1"><p>And that's the footnote.<a href="#fnref-1" class="footnote">↩</a></p></li> + </ol> + </section> + + :param md: Markdown instance + """ + md.inline.register( + 'footnote', + INLINE_FOOTNOTE, + parse_inline_footnote, + before='link', + ) + md.block.register( + 'ref_footnote', + REF_FOOTNOTE, + parse_ref_footnote, + before='ref_link', + ) + md.after_render_hooks.append(md_footnotes_hook) + + if md.renderer and md.renderer.NAME == 'html': + md.renderer.register('footnote_ref', render_footnote_ref) + md.renderer.register('footnote_item', render_footnote_item) + md.renderer.register('footnotes', render_footnotes) |