Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/mdit_py_plugins/footnote/index.py: 88%
256 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:15 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:15 +0000
1"""Process footnotes"""
2from __future__ import annotations
4from typing import TYPE_CHECKING, List, Optional, Sequence
6from markdown_it import MarkdownIt
7from markdown_it.helpers import parseLinkLabel
8from markdown_it.rules_block import StateBlock
9from markdown_it.rules_core import StateCore
10from markdown_it.rules_inline import StateInline
11from markdown_it.token import Token
13from mdit_py_plugins.utils import is_code_block
15if TYPE_CHECKING:
16 from markdown_it.renderer import RendererProtocol
17 from markdown_it.utils import EnvType, OptionsDict
20def footnote_plugin(md: MarkdownIt) -> None:
21 """Plugin ported from
22 `markdown-it-footnote <https://github.com/markdown-it/markdown-it-footnote>`__.
24 It is based on the
25 `pandoc definition <http://johnmacfarlane.net/pandoc/README.html#footnotes>`__:
27 .. code-block:: md
29 Normal footnote:
31 Here is a footnote reference,[^1] and another.[^longnote]
33 [^1]: Here is the footnote.
35 [^longnote]: Here's one with multiple blocks.
37 Subsequent paragraphs are indented to show that they
38 belong to the previous footnote.
40 """
41 md.block.ruler.before(
42 "reference", "footnote_def", footnote_def, {"alt": ["paragraph", "reference"]}
43 )
44 md.inline.ruler.after("image", "footnote_inline", footnote_inline)
45 md.inline.ruler.after("footnote_inline", "footnote_ref", footnote_ref)
46 md.core.ruler.after("inline", "footnote_tail", footnote_tail)
48 md.add_render_rule("footnote_ref", render_footnote_ref)
49 md.add_render_rule("footnote_block_open", render_footnote_block_open)
50 md.add_render_rule("footnote_block_close", render_footnote_block_close)
51 md.add_render_rule("footnote_open", render_footnote_open)
52 md.add_render_rule("footnote_close", render_footnote_close)
53 md.add_render_rule("footnote_anchor", render_footnote_anchor)
55 # helpers (only used in other rules, no tokens are attached to those)
56 md.add_render_rule("footnote_caption", render_footnote_caption)
57 md.add_render_rule("footnote_anchor_name", render_footnote_anchor_name)
60# ## RULES ##
63def footnote_def(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
64 """Process footnote block definition"""
66 if is_code_block(state, startLine):
67 return False
69 start = state.bMarks[startLine] + state.tShift[startLine]
70 maximum = state.eMarks[startLine]
72 # line should be at least 5 chars - "[^x]:"
73 if start + 4 > maximum:
74 return False
76 if state.src[start] != "[":
77 return False
78 if state.src[start + 1] != "^":
79 return False
81 pos = start + 2
82 while pos < maximum:
83 if state.src[pos] == " ":
84 return False
85 if state.src[pos] == "]":
86 break
87 pos += 1
89 if pos == start + 2: # no empty footnote labels
90 return False
91 pos += 1
92 if pos >= maximum or state.src[pos] != ":":
93 return False
94 if silent:
95 return True
96 pos += 1
98 label = state.src[start + 2 : pos - 2]
99 state.env.setdefault("footnotes", {}).setdefault("refs", {})[":" + label] = -1
101 open_token = Token("footnote_reference_open", "", 1)
102 open_token.meta = {"label": label}
103 open_token.level = state.level
104 state.level += 1
105 state.tokens.append(open_token)
107 oldBMark = state.bMarks[startLine]
108 oldTShift = state.tShift[startLine]
109 oldSCount = state.sCount[startLine]
110 oldParentType = state.parentType
112 posAfterColon = pos
113 initial = offset = (
114 state.sCount[startLine]
115 + pos
116 - (state.bMarks[startLine] + state.tShift[startLine])
117 )
119 while pos < maximum:
120 ch = state.src[pos]
122 if ch == "\t":
123 offset += 4 - offset % 4
124 elif ch == " ":
125 offset += 1
127 else:
128 break
130 pos += 1
132 state.tShift[startLine] = pos - posAfterColon
133 state.sCount[startLine] = offset - initial
135 state.bMarks[startLine] = posAfterColon
136 state.blkIndent += 4
137 state.parentType = "footnote"
139 if state.sCount[startLine] < state.blkIndent:
140 state.sCount[startLine] += state.blkIndent
142 state.md.block.tokenize(state, startLine, endLine)
144 state.parentType = oldParentType
145 state.blkIndent -= 4
146 state.tShift[startLine] = oldTShift
147 state.sCount[startLine] = oldSCount
148 state.bMarks[startLine] = oldBMark
150 open_token.map = [startLine, state.line]
152 token = Token("footnote_reference_close", "", -1)
153 state.level -= 1
154 token.level = state.level
155 state.tokens.append(token)
157 return True
160def footnote_inline(state: StateInline, silent: bool) -> bool:
161 """Process inline footnotes (^[...])"""
163 maximum = state.posMax
164 start = state.pos
166 if start + 2 >= maximum:
167 return False
168 if state.src[start] != "^":
169 return False
170 if state.src[start + 1] != "[":
171 return False
173 labelStart = start + 2
174 labelEnd = parseLinkLabel(state, start + 1)
176 # parser failed to find ']', so it's not a valid note
177 if labelEnd < 0:
178 return False
180 # We found the end of the link, and know for a fact it's a valid link
181 # so all that's left to do is to call tokenizer.
182 #
183 if not silent:
184 refs = state.env.setdefault("footnotes", {}).setdefault("list", {})
185 footnoteId = len(refs)
187 tokens: List[Token] = []
188 state.md.inline.parse(
189 state.src[labelStart:labelEnd], state.md, state.env, tokens
190 )
192 token = state.push("footnote_ref", "", 0)
193 token.meta = {"id": footnoteId}
195 refs[footnoteId] = {"content": state.src[labelStart:labelEnd], "tokens": tokens}
197 state.pos = labelEnd + 1
198 state.posMax = maximum
199 return True
202def footnote_ref(state: StateInline, silent: bool) -> bool:
203 """Process footnote references ([^...])"""
205 maximum = state.posMax
206 start = state.pos
208 # should be at least 4 chars - "[^x]"
209 if start + 3 > maximum:
210 return False
212 if "footnotes" not in state.env or "refs" not in state.env["footnotes"]:
213 return False
214 if state.src[start] != "[":
215 return False
216 if state.src[start + 1] != "^":
217 return False
219 pos = start + 2
220 while pos < maximum:
221 if state.src[pos] == " ":
222 return False
223 if state.src[pos] == "\n":
224 return False
225 if state.src[pos] == "]":
226 break
227 pos += 1
229 if pos == start + 2: # no empty footnote labels
230 return False
231 if pos >= maximum:
232 return False
233 pos += 1
235 label = state.src[start + 2 : pos - 1]
236 if (":" + label) not in state.env["footnotes"]["refs"]:
237 return False
239 if not silent:
240 if "list" not in state.env["footnotes"]:
241 state.env["footnotes"]["list"] = {}
243 if state.env["footnotes"]["refs"][":" + label] < 0:
244 footnoteId = len(state.env["footnotes"]["list"])
245 state.env["footnotes"]["list"][footnoteId] = {"label": label, "count": 0}
246 state.env["footnotes"]["refs"][":" + label] = footnoteId
247 else:
248 footnoteId = state.env["footnotes"]["refs"][":" + label]
250 footnoteSubId = state.env["footnotes"]["list"][footnoteId]["count"]
251 state.env["footnotes"]["list"][footnoteId]["count"] += 1
253 token = state.push("footnote_ref", "", 0)
254 token.meta = {"id": footnoteId, "subId": footnoteSubId, "label": label}
256 state.pos = pos
257 state.posMax = maximum
258 return True
261def footnote_tail(state: StateCore) -> None:
262 """Post-processing step, to move footnote tokens to end of the token stream.
264 Also removes un-referenced tokens.
265 """
267 insideRef = False
268 refTokens = {}
270 if "footnotes" not in state.env:
271 return
273 current: List[Token] = []
274 tok_filter = []
275 for tok in state.tokens:
276 if tok.type == "footnote_reference_open":
277 insideRef = True
278 current = []
279 currentLabel = tok.meta["label"]
280 tok_filter.append(False)
281 continue
283 if tok.type == "footnote_reference_close":
284 insideRef = False
285 # prepend ':' to avoid conflict with Object.prototype members
286 refTokens[":" + currentLabel] = current
287 tok_filter.append(False)
288 continue
290 if insideRef:
291 current.append(tok)
293 tok_filter.append((not insideRef))
295 state.tokens = [t for t, f in zip(state.tokens, tok_filter) if f]
297 if "list" not in state.env.get("footnotes", {}):
298 return
299 foot_list = state.env["footnotes"]["list"]
301 token = Token("footnote_block_open", "", 1)
302 state.tokens.append(token)
304 for i, foot_note in foot_list.items():
305 token = Token("footnote_open", "", 1)
306 token.meta = {"id": i, "label": foot_note.get("label", None)}
307 # TODO propagate line positions of original foot note
308 # (but don't store in token.map, because this is used for scroll syncing)
309 state.tokens.append(token)
311 if "tokens" in foot_note:
312 tokens = []
314 token = Token("paragraph_open", "p", 1)
315 token.block = True
316 tokens.append(token)
318 token = Token("inline", "", 0)
319 token.children = foot_note["tokens"]
320 token.content = foot_note["content"]
321 tokens.append(token)
323 token = Token("paragraph_close", "p", -1)
324 token.block = True
325 tokens.append(token)
327 elif "label" in foot_note:
328 tokens = refTokens[":" + foot_note["label"]]
330 state.tokens.extend(tokens)
331 if state.tokens[len(state.tokens) - 1].type == "paragraph_close":
332 lastParagraph: Optional[Token] = state.tokens.pop()
333 else:
334 lastParagraph = None
336 t = (
337 foot_note["count"]
338 if (("count" in foot_note) and (foot_note["count"] > 0))
339 else 1
340 )
341 j = 0
342 while j < t:
343 token = Token("footnote_anchor", "", 0)
344 token.meta = {"id": i, "subId": j, "label": foot_note.get("label", None)}
345 state.tokens.append(token)
346 j += 1
348 if lastParagraph:
349 state.tokens.append(lastParagraph)
351 token = Token("footnote_close", "", -1)
352 state.tokens.append(token)
354 token = Token("footnote_block_close", "", -1)
355 state.tokens.append(token)
358########################################
359# Renderer partials
362def render_footnote_anchor_name(
363 self: RendererProtocol,
364 tokens: Sequence[Token],
365 idx: int,
366 options: OptionsDict,
367 env: EnvType,
368) -> str:
369 n = str(tokens[idx].meta["id"] + 1)
370 prefix = ""
372 doc_id = env.get("docId", None)
373 if isinstance(doc_id, str):
374 prefix = f"-{doc_id}-"
376 return prefix + n
379def render_footnote_caption(
380 self: RendererProtocol,
381 tokens: Sequence[Token],
382 idx: int,
383 options: OptionsDict,
384 env: EnvType,
385) -> str:
386 n = str(tokens[idx].meta["id"] + 1)
388 if tokens[idx].meta.get("subId", -1) > 0:
389 n += ":" + str(tokens[idx].meta["subId"])
391 return "[" + n + "]"
394def render_footnote_ref(
395 self: RendererProtocol,
396 tokens: Sequence[Token],
397 idx: int,
398 options: OptionsDict,
399 env: EnvType,
400) -> str:
401 ident: str = self.rules["footnote_anchor_name"](tokens, idx, options, env) # type: ignore[attr-defined]
402 caption: str = self.rules["footnote_caption"](tokens, idx, options, env) # type: ignore[attr-defined]
403 refid = ident
405 if tokens[idx].meta.get("subId", -1) > 0:
406 refid += ":" + str(tokens[idx].meta["subId"])
408 return (
409 '<sup class="footnote-ref"><a href="#fn'
410 + ident
411 + '" id="fnref'
412 + refid
413 + '">'
414 + caption
415 + "</a></sup>"
416 )
419def render_footnote_block_open(
420 self: RendererProtocol,
421 tokens: Sequence[Token],
422 idx: int,
423 options: OptionsDict,
424 env: EnvType,
425) -> str:
426 return (
427 (
428 '<hr class="footnotes-sep" />\n'
429 if options.xhtmlOut
430 else '<hr class="footnotes-sep">\n'
431 )
432 + '<section class="footnotes">\n'
433 + '<ol class="footnotes-list">\n'
434 )
437def render_footnote_block_close(
438 self: RendererProtocol,
439 tokens: Sequence[Token],
440 idx: int,
441 options: OptionsDict,
442 env: EnvType,
443) -> str:
444 return "</ol>\n</section>\n"
447def render_footnote_open(
448 self: RendererProtocol,
449 tokens: Sequence[Token],
450 idx: int,
451 options: OptionsDict,
452 env: EnvType,
453) -> str:
454 ident: str = self.rules["footnote_anchor_name"](tokens, idx, options, env) # type: ignore[attr-defined]
456 if tokens[idx].meta.get("subId", -1) > 0:
457 ident += ":" + tokens[idx].meta["subId"]
459 return '<li id="fn' + ident + '" class="footnote-item">'
462def render_footnote_close(
463 self: RendererProtocol,
464 tokens: Sequence[Token],
465 idx: int,
466 options: OptionsDict,
467 env: EnvType,
468) -> str:
469 return "</li>\n"
472def render_footnote_anchor(
473 self: RendererProtocol,
474 tokens: Sequence[Token],
475 idx: int,
476 options: OptionsDict,
477 env: EnvType,
478) -> str:
479 ident: str = self.rules["footnote_anchor_name"](tokens, idx, options, env) # type: ignore[attr-defined]
481 if tokens[idx].meta["subId"] > 0:
482 ident += ":" + str(tokens[idx].meta["subId"])
484 # ↩ with escape code to prevent display as Apple Emoji on iOS
485 return ' <a href="#fnref' + ident + '" class="footnote-backref">\u21a9\uFE0E</a>'