| 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,}") |
| |
| 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: "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 |
|
|
|
|
| def render_footnote_ref(renderer: "BaseRenderer", key: str, index: int) -> str: |
| i = str(index) |
| html = '<sup class="footnote-ref" id="fnref-' + i + '">' |
| return html + '<a href="#fn-' + i + '">' + i + "</a></sup>" |
|
|
|
|
| def render_footnotes(renderer: "BaseRenderer", text: str) -> str: |
| return '<section class="footnotes">\n<ol>\n' + text + "</ol>\n</section>\n" |
|
|
|
|
| def render_footnote_item(renderer: "BaseRenderer", text: str, key: str, index: int) -> str: |
| 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: "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 |
| |
| <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) |
|
|