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