1import re
2from typing import TYPE_CHECKING, Any, Dict, List, Match, Union
3
4from ..core import BlockState
5from ..helpers import LINK_LABEL
6from ..util import unikey
7
8if TYPE_CHECKING:
9 from ..block_parser import BlockParser
10 from ..core import BaseRenderer, InlineState
11 from ..inline_parser import InlineParser
12 from ..markdown import Markdown
13
14__all__ = ["footnotes"]
15
16_PARAGRAPH_SPLIT = re.compile(r"\n{2,}")
17# https://michelf.ca/projects/php-markdown/extra/#footnotes
18REF_FOOTNOTE = (
19 r"^(?P<footnote_lead> {0,3})"
20 r"\[\^(?P<footnote_key>" + LINK_LABEL + r")]:[ \t]"
21 r"(?P<footnote_text>[^\n]*(?:\n+|$)"
22 r"(?:(?P=footnote_lead) {1,3}(?! )[^\n]*\n+)*"
23 r")"
24)
25
26INLINE_FOOTNOTE = r"\[\^(?P<footnote_key>" + LINK_LABEL + r")\]"
27
28
29def parse_inline_footnote(inline: "InlineParser", m: Match[str], state: "InlineState") -> int:
30 key = unikey(m.group("footnote_key"))
31 ref = state.env.get("ref_footnotes")
32 if ref and key in ref:
33 notes = state.env.get("footnotes")
34 if not notes:
35 notes = []
36 if key not in notes:
37 notes.append(key)
38 state.env["footnotes"] = notes
39 state.append_token({"type": "footnote_ref", "raw": key, "attrs": {"index": notes.index(key) + 1}})
40 else:
41 state.append_token({"type": "text", "raw": m.group(0)})
42 return m.end()
43
44
45def parse_ref_footnote(block: "BlockParser", m: Match[str], state: BlockState) -> int:
46 ref = state.env.get("ref_footnotes")
47 if not ref:
48 ref = {}
49
50 key = unikey(m.group("footnote_key"))
51 if key not in ref:
52 ref[key] = m.group("footnote_text")
53 state.env["ref_footnotes"] = ref
54 return m.end()
55
56
57def parse_footnote_item(block: "BlockParser", key: str, index: int, state: BlockState) -> Dict[str, Any]:
58 ref = state.env.get("ref_footnotes")
59 if not ref:
60 raise ValueError("Missing 'ref_footnotes'.")
61 text = ref[key]
62
63 lines = text.splitlines()
64 second_line = None
65 for second_line in lines[1:]:
66 if second_line:
67 break
68
69 if second_line:
70 spaces = len(second_line) - len(second_line.lstrip())
71 pattern = re.compile(r"^ {" + str(spaces) + r",}", flags=re.M)
72 text = pattern.sub("", text).strip()
73 items = _PARAGRAPH_SPLIT.split(text)
74 children = [{"type": "paragraph", "text": s} for s in items]
75 else:
76 text = text.strip()
77 children = [{"type": "paragraph", "text": text}]
78 return {"type": "footnote_item", "children": children, "attrs": {"key": key, "index": index}}
79
80
81def md_footnotes_hook(
82 md: "Markdown", result: Union[str, List[Dict[str, Any]]], state: BlockState
83) -> Union[str, List[Dict[str, Any]]]:
84 notes = state.env.get("footnotes")
85 if not notes:
86 return result
87
88 children = [parse_footnote_item(md.block, k, i + 1, state) for i, k in enumerate(notes)]
89 state = BlockState()
90 state.tokens = [{"type": "footnotes", "children": children}]
91 output = md.render_state(state)
92 return result + output # type: ignore[operator]
93
94
95def render_footnote_ref(renderer: "BaseRenderer", key: str, index: int) -> str:
96 i = str(index)
97 html = '<sup class="footnote-ref" id="fnref-' + i + '">'
98 return html + '<a href="#fn-' + i + '">' + i + "</a></sup>"
99
100
101def render_footnotes(renderer: "BaseRenderer", text: str) -> str:
102 return '<section class="footnotes">\n<ol>\n' + text + "</ol>\n</section>\n"
103
104
105def render_footnote_item(renderer: "BaseRenderer", text: str, key: str, index: int) -> str:
106 i = str(index)
107 back = '<a href="#fnref-' + i + '" class="footnote">↩</a>'
108 text = text.rstrip()[:-4] + back + "</p>"
109 return '<li id="fn-' + i + '">' + text + "</li>\n"
110
111
112def footnotes(md: "Markdown") -> None:
113 """A mistune plugin to support footnotes, spec defined at
114 https://michelf.ca/projects/php-markdown/extra/#footnotes
115
116 Here is an example:
117
118 .. code-block:: text
119
120 That's some text with a footnote.[^1]
121
122 [^1]: And that's the footnote.
123
124 It will be converted into HTML:
125
126 .. code-block:: html
127
128 <p>That's some text with a footnote.<sup class="footnote-ref" id="fnref-1"><a href="#fn-1">1</a></sup></p>
129 <section class="footnotes">
130 <ol>
131 <li id="fn-1"><p>And that's the footnote.<a href="#fnref-1" class="footnote">↩</a></p></li>
132 </ol>
133 </section>
134
135 :param md: Markdown instance
136 """
137 md.inline.register(
138 "footnote",
139 INLINE_FOOTNOTE,
140 parse_inline_footnote,
141 before="link",
142 )
143 md.block.register(
144 "ref_footnote",
145 REF_FOOTNOTE,
146 parse_ref_footnote,
147 before="ref_link",
148 )
149 md.after_render_hooks.append(md_footnotes_hook)
150
151 if md.renderer and md.renderer.NAME == "html":
152 md.renderer.register("footnote_ref", render_footnote_ref)
153 md.renderer.register("footnote_item", render_footnote_item)
154 md.renderer.register("footnotes", render_footnotes)