import re from typing import TYPE_CHECKING, Any, Dict, List, Match, Union from ..core import BlockState from ..helpers import LINK_LABEL from ..util import unikey if TYPE_CHECKING: from ..block_parser import BlockParser from ..core import BaseRenderer, InlineState from ..inline_parser import InlineParser from ..markdown import Markdown __all__ = ["footnotes"] _PARAGRAPH_SPLIT = re.compile(r"\n{2,}") # https://michelf.ca/projects/php-markdown/extra/#footnotes REF_FOOTNOTE = ( r"^(?P {0,3})" r"\[\^(?P" + LINK_LABEL + r")]:[ \t]" r"(?P[^\n]*(?:\n+|$)" r"(?:(?P=footnote_lead) {1,3}(?! )[^\n]*\n+)*" r")" ) INLINE_FOOTNOTE = r"\[\^(?P" + LINK_LABEL + r")\]" def parse_inline_footnote(inline: "InlineParser", m: Match[str], state: "InlineState") -> int: 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: "BlockParser", m: Match[str], state: BlockState) -> int: 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: "BlockParser", key: str, index: int, state: BlockState) -> Dict[str, Any]: ref = state.env.get("ref_footnotes") if not ref: raise ValueError("Missing '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: "Markdown", result: Union[str, List[Dict[str, Any]]], state: BlockState ) -> Union[str, List[Dict[str, Any]]]: 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 # type: ignore[operator] def render_footnote_ref(renderer: "BaseRenderer", key: str, index: int) -> str: i = str(index) html = '' return html + '' + i + "" def render_footnotes(renderer: "BaseRenderer", text: str) -> str: return '
\n
    \n' + text + "
\n
\n" def render_footnote_item(renderer: "BaseRenderer", text: str, key: str, index: int) -> str: i = str(index) back = '' text = text.rstrip()[:-4] + back + "

" return '
  • ' + text + "
  • \n" def footnotes(md: "Markdown") -> None: """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

    That's some text with a footnote.1

    1. And that's the footnote.

    :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)