summaryrefslogtreecommitdiffstats
path: root/mdit_py_plugins/footnote/index.py
diff options
context:
space:
mode:
Diffstat (limited to 'mdit_py_plugins/footnote/index.py')
-rw-r--r--mdit_py_plugins/footnote/index.py430
1 files changed, 430 insertions, 0 deletions
diff --git a/mdit_py_plugins/footnote/index.py b/mdit_py_plugins/footnote/index.py
new file mode 100644
index 0000000..119fb71
--- /dev/null
+++ b/mdit_py_plugins/footnote/index.py
@@ -0,0 +1,430 @@
+# Process footnotes
+#
+
+from typing import List, Optional
+
+from markdown_it import MarkdownIt
+from markdown_it.common.utils import isSpace
+from markdown_it.helpers import parseLinkLabel
+from markdown_it.rules_block import StateBlock
+from markdown_it.rules_inline import StateInline
+from markdown_it.token import Token
+
+
+def footnote_plugin(md: MarkdownIt):
+ """Plugin ported from
+ `markdown-it-footnote <https://github.com/markdown-it/markdown-it-footnote>`__.
+
+ It is based on the
+ `pandoc definition <http://johnmacfarlane.net/pandoc/README.html#footnotes>`__:
+
+ .. code-block:: md
+
+ Normal footnote:
+
+ Here is a footnote reference,[^1] and another.[^longnote]
+
+ [^1]: Here is the footnote.
+
+ [^longnote]: Here's one with multiple blocks.
+
+ Subsequent paragraphs are indented to show that they
+ belong to the previous footnote.
+
+ """
+ md.block.ruler.before(
+ "reference", "footnote_def", footnote_def, {"alt": ["paragraph", "reference"]}
+ )
+ md.inline.ruler.after("image", "footnote_inline", footnote_inline)
+ md.inline.ruler.after("footnote_inline", "footnote_ref", footnote_ref)
+ md.core.ruler.after("inline", "footnote_tail", footnote_tail)
+
+ md.add_render_rule("footnote_ref", render_footnote_ref)
+ md.add_render_rule("footnote_block_open", render_footnote_block_open)
+ md.add_render_rule("footnote_block_close", render_footnote_block_close)
+ md.add_render_rule("footnote_open", render_footnote_open)
+ md.add_render_rule("footnote_close", render_footnote_close)
+ md.add_render_rule("footnote_anchor", render_footnote_anchor)
+
+ # helpers (only used in other rules, no tokens are attached to those)
+ md.add_render_rule("footnote_caption", render_footnote_caption)
+ md.add_render_rule("footnote_anchor_name", render_footnote_anchor_name)
+
+
+# ## RULES ##
+
+
+def footnote_def(state: StateBlock, startLine: int, endLine: int, silent: bool):
+ """Process footnote block definition"""
+
+ start = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ # line should be at least 5 chars - "[^x]:"
+ if start + 4 > maximum:
+ return False
+
+ if state.srcCharCode[start] != 0x5B: # /* [ */
+ return False
+ if state.srcCharCode[start + 1] != 0x5E: # /* ^ */
+ return False
+
+ pos = start + 2
+ while pos < maximum:
+ if state.srcCharCode[pos] == 0x20:
+ return False
+ if state.srcCharCode[pos] == 0x5D: # /* ] */
+ break
+ pos += 1
+
+ if pos == start + 2: # no empty footnote labels
+ return False
+ pos += 1
+ if pos >= maximum or state.srcCharCode[pos] != 0x3A: # /* : */
+ return False
+ if silent:
+ return True
+ pos += 1
+
+ label = state.src[start + 2 : pos - 2]
+ state.env.setdefault("footnotes", {}).setdefault("refs", {})[":" + label] = -1
+
+ open_token = Token("footnote_reference_open", "", 1)
+ open_token.meta = {"label": label}
+ open_token.level = state.level
+ state.level += 1
+ state.tokens.append(open_token)
+
+ oldBMark = state.bMarks[startLine]
+ oldTShift = state.tShift[startLine]
+ oldSCount = state.sCount[startLine]
+ oldParentType = state.parentType
+
+ posAfterColon = pos
+ initial = offset = (
+ state.sCount[startLine]
+ + pos
+ - (state.bMarks[startLine] + state.tShift[startLine])
+ )
+
+ while pos < maximum:
+ ch = state.srcCharCode[pos]
+
+ if isSpace(ch):
+ if ch == 0x09:
+ offset += 4 - offset % 4
+ else:
+ offset += 1
+
+ else:
+ break
+
+ pos += 1
+
+ state.tShift[startLine] = pos - posAfterColon
+ state.sCount[startLine] = offset - initial
+
+ state.bMarks[startLine] = posAfterColon
+ state.blkIndent += 4
+ state.parentType = "footnote"
+
+ if state.sCount[startLine] < state.blkIndent:
+ state.sCount[startLine] += state.blkIndent
+
+ state.md.block.tokenize(state, startLine, endLine, True)
+
+ state.parentType = oldParentType
+ state.blkIndent -= 4
+ state.tShift[startLine] = oldTShift
+ state.sCount[startLine] = oldSCount
+ state.bMarks[startLine] = oldBMark
+
+ open_token.map = [startLine, state.line]
+
+ token = Token("footnote_reference_close", "", -1)
+ state.level -= 1
+ token.level = state.level
+ state.tokens.append(token)
+
+ return True
+
+
+def footnote_inline(state: StateInline, silent: bool):
+ """Process inline footnotes (^[...])"""
+
+ maximum = state.posMax
+ start = state.pos
+
+ if start + 2 >= maximum:
+ return False
+ if state.srcCharCode[start] != 0x5E: # /* ^ */
+ return False
+ if state.srcCharCode[start + 1] != 0x5B: # /* [ */
+ return False
+
+ labelStart = start + 2
+ labelEnd = parseLinkLabel(state, start + 1)
+
+ # parser failed to find ']', so it's not a valid note
+ if labelEnd < 0:
+ return False
+
+ # We found the end of the link, and know for a fact it's a valid link
+ # so all that's left to do is to call tokenizer.
+ #
+ if not silent:
+ refs = state.env.setdefault("footnotes", {}).setdefault("list", {})
+ footnoteId = len(refs)
+
+ tokens: List[Token] = []
+ state.md.inline.parse(
+ state.src[labelStart:labelEnd], state.md, state.env, tokens
+ )
+
+ token = state.push("footnote_ref", "", 0)
+ token.meta = {"id": footnoteId}
+
+ refs[footnoteId] = {"content": state.src[labelStart:labelEnd], "tokens": tokens}
+
+ state.pos = labelEnd + 1
+ state.posMax = maximum
+ return True
+
+
+def footnote_ref(state: StateInline, silent: bool):
+ """Process footnote references ([^...])"""
+
+ maximum = state.posMax
+ start = state.pos
+
+ # should be at least 4 chars - "[^x]"
+ if start + 3 > maximum:
+ return False
+
+ if "footnotes" not in state.env or "refs" not in state.env["footnotes"]:
+ return False
+ if state.srcCharCode[start] != 0x5B: # /* [ */
+ return False
+ if state.srcCharCode[start + 1] != 0x5E: # /* ^ */
+ return False
+
+ pos = start + 2
+ while pos < maximum:
+ if state.srcCharCode[pos] == 0x20:
+ return False
+ if state.srcCharCode[pos] == 0x0A:
+ return False
+ if state.srcCharCode[pos] == 0x5D: # /* ] */
+ break
+ pos += 1
+
+ if pos == start + 2: # no empty footnote labels
+ return False
+ if pos >= maximum:
+ return False
+ pos += 1
+
+ label = state.src[start + 2 : pos - 1]
+ if (":" + label) not in state.env["footnotes"]["refs"]:
+ return False
+
+ if not silent:
+ if "list" not in state.env["footnotes"]:
+ state.env["footnotes"]["list"] = {}
+
+ if state.env["footnotes"]["refs"][":" + label] < 0:
+ footnoteId = len(state.env["footnotes"]["list"])
+ state.env["footnotes"]["list"][footnoteId] = {"label": label, "count": 0}
+ state.env["footnotes"]["refs"][":" + label] = footnoteId
+ else:
+ footnoteId = state.env["footnotes"]["refs"][":" + label]
+
+ footnoteSubId = state.env["footnotes"]["list"][footnoteId]["count"]
+ state.env["footnotes"]["list"][footnoteId]["count"] += 1
+
+ token = state.push("footnote_ref", "", 0)
+ token.meta = {"id": footnoteId, "subId": footnoteSubId, "label": label}
+
+ state.pos = pos
+ state.posMax = maximum
+ return True
+
+
+def footnote_tail(state: StateBlock, *args, **kwargs):
+ """Post-processing step, to move footnote tokens to end of the token stream.
+
+ Also removes un-referenced tokens.
+ """
+
+ insideRef = False
+ refTokens = {}
+
+ if "footnotes" not in state.env:
+ return
+
+ current: List[Token] = []
+ tok_filter = []
+ for tok in state.tokens:
+
+ if tok.type == "footnote_reference_open":
+ insideRef = True
+ current = []
+ currentLabel = tok.meta["label"]
+ tok_filter.append(False)
+ continue
+
+ if tok.type == "footnote_reference_close":
+ insideRef = False
+ # prepend ':' to avoid conflict with Object.prototype members
+ refTokens[":" + currentLabel] = current
+ tok_filter.append(False)
+ continue
+
+ if insideRef:
+ current.append(tok)
+
+ tok_filter.append((not insideRef))
+
+ state.tokens = [t for t, f in zip(state.tokens, tok_filter) if f]
+
+ if "list" not in state.env.get("footnotes", {}):
+ return
+ foot_list = state.env["footnotes"]["list"]
+
+ token = Token("footnote_block_open", "", 1)
+ state.tokens.append(token)
+
+ for i, foot_note in foot_list.items():
+ token = Token("footnote_open", "", 1)
+ token.meta = {"id": i, "label": foot_note.get("label", None)}
+ # TODO propagate line positions of original foot note
+ # (but don't store in token.map, because this is used for scroll syncing)
+ state.tokens.append(token)
+
+ if "tokens" in foot_note:
+ tokens = []
+
+ token = Token("paragraph_open", "p", 1)
+ token.block = True
+ tokens.append(token)
+
+ token = Token("inline", "", 0)
+ token.children = foot_note["tokens"]
+ token.content = foot_note["content"]
+ tokens.append(token)
+
+ token = Token("paragraph_close", "p", -1)
+ token.block = True
+ tokens.append(token)
+
+ elif "label" in foot_note:
+ tokens = refTokens[":" + foot_note["label"]]
+
+ state.tokens.extend(tokens)
+ if state.tokens[len(state.tokens) - 1].type == "paragraph_close":
+ lastParagraph: Optional[Token] = state.tokens.pop()
+ else:
+ lastParagraph = None
+
+ t = (
+ foot_note["count"]
+ if (("count" in foot_note) and (foot_note["count"] > 0))
+ else 1
+ )
+ j = 0
+ while j < t:
+ token = Token("footnote_anchor", "", 0)
+ token.meta = {"id": i, "subId": j, "label": foot_note.get("label", None)}
+ state.tokens.append(token)
+ j += 1
+
+ if lastParagraph:
+ state.tokens.append(lastParagraph)
+
+ token = Token("footnote_close", "", -1)
+ state.tokens.append(token)
+
+ token = Token("footnote_block_close", "", -1)
+ state.tokens.append(token)
+
+
+########################################
+# Renderer partials
+
+
+def render_footnote_anchor_name(self, tokens, idx, options, env):
+ n = str(tokens[idx].meta["id"] + 1)
+ prefix = ""
+
+ doc_id = env.get("docId", None)
+ if isinstance(doc_id, str):
+ prefix = f"-{doc_id}-"
+
+ return prefix + n
+
+
+def render_footnote_caption(self, tokens, idx, options, env):
+ n = str(tokens[idx].meta["id"] + 1)
+
+ if tokens[idx].meta.get("subId", -1) > 0:
+ n += ":" + str(tokens[idx].meta["subId"])
+
+ return "[" + n + "]"
+
+
+def render_footnote_ref(self, tokens, idx, options, env):
+ ident = self.rules["footnote_anchor_name"](tokens, idx, options, env)
+ caption = self.rules["footnote_caption"](tokens, idx, options, env)
+ refid = ident
+
+ if tokens[idx].meta.get("subId", -1) > 0:
+ refid += ":" + str(tokens[idx].meta["subId"])
+
+ return (
+ '<sup class="footnote-ref"><a href="#fn'
+ + ident
+ + '" id="fnref'
+ + refid
+ + '">'
+ + caption
+ + "</a></sup>"
+ )
+
+
+def render_footnote_block_open(self, tokens, idx, options, env):
+ return (
+ (
+ '<hr class="footnotes-sep" />\n'
+ if options.xhtmlOut
+ else '<hr class="footnotes-sep">\n'
+ )
+ + '<section class="footnotes">\n'
+ + '<ol class="footnotes-list">\n'
+ )
+
+
+def render_footnote_block_close(self, tokens, idx, options, env):
+ return "</ol>\n</section>\n"
+
+
+def render_footnote_open(self, tokens, idx, options, env):
+ ident = self.rules["footnote_anchor_name"](tokens, idx, options, env)
+
+ if tokens[idx].meta.get("subId", -1) > 0:
+ ident += ":" + tokens[idx].meta["subId"]
+
+ return '<li id="fn' + ident + '" class="footnote-item">'
+
+
+def render_footnote_close(self, tokens, idx, options, env):
+ return "</li>\n"
+
+
+def render_footnote_anchor(self, tokens, idx, options, env):
+ ident = self.rules["footnote_anchor_name"](tokens, idx, options, env)
+
+ if tokens[idx].meta["subId"] > 0:
+ ident += ":" + str(tokens[idx].meta["subId"])
+
+ # ↩ with escape code to prevent display as Apple Emoji on iOS
+ return ' <a href="#fnref' + ident + '" class="footnote-backref">\u21a9\uFE0E</a>'