Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

👌 Add option for footnotes references to always be matched #108

Merged
merged 1 commit into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 66 additions & 27 deletions mdit_py_plugins/footnote/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Sequence
from functools import partial
from typing import TYPE_CHECKING, Sequence, TypedDict

from markdown_it import MarkdownIt
from markdown_it.helpers import parseLinkLabel
Expand All @@ -18,7 +19,13 @@
from markdown_it.utils import EnvType, OptionsDict


def footnote_plugin(md: MarkdownIt) -> None:
def footnote_plugin(
md: MarkdownIt,
*,
inline: bool = True,
move_to_end: bool = True,
always_match_refs: bool = False,
) -> None:
"""Plugin ported from
`markdown-it-footnote <https://github.com/markdown-it/markdown-it-footnote>`__.

Expand All @@ -38,13 +45,22 @@ def footnote_plugin(md: MarkdownIt) -> None:
Subsequent paragraphs are indented to show that they
belong to the previous footnote.

:param inline: If True, also parse inline footnotes (^[...]).
:param move_to_end: If True, move footnote definitions to the end of the token stream.
:param always_match_refs: If True, match references, even if the footnote is not defined.

"""
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)
_footnote_ref = partial(footnote_ref, always_match=always_match_refs)
if inline:
md.inline.ruler.after("image", "footnote_inline", footnote_inline)
md.inline.ruler.after("footnote_inline", "footnote_ref", _footnote_ref)
else:
md.inline.ruler.after("image", "footnote_ref", _footnote_ref)
if move_to_end:
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)
Expand All @@ -58,6 +74,29 @@ def footnote_plugin(md: MarkdownIt) -> None:
md.add_render_rule("footnote_anchor_name", render_footnote_anchor_name)


class _RefData(TypedDict, total=False):
# standard
label: str
count: int
# inline
content: str
tokens: list[Token]


class _FootnoteData(TypedDict):
refs: dict[str, int]
"""A mapping of all footnote labels (prefixed with ``:``) to their ID (-1 if not yet set)."""
list: dict[int, _RefData]
"""A mapping of all footnote IDs to their data."""


def _data_from_env(env: EnvType) -> _FootnoteData:
footnotes = env.setdefault("footnotes", {})
footnotes.setdefault("refs", {})
footnotes.setdefault("list", {})
return footnotes # type: ignore[no-any-return]


# ## RULES ##


Expand Down Expand Up @@ -97,7 +136,8 @@ def footnote_def(state: StateBlock, startLine: int, endLine: int, silent: bool)
pos += 1

label = state.src[start + 2 : pos - 2]
state.env.setdefault("footnotes", {}).setdefault("refs", {})[":" + label] = -1
footnote_data = _data_from_env(state.env)
footnote_data["refs"][":" + label] = -1

open_token = Token("footnote_reference_open", "", 1)
open_token.meta = {"label": label}
Expand Down Expand Up @@ -182,7 +222,7 @@ def footnote_inline(state: StateInline, silent: bool) -> bool:
# so all that's left to do is to call tokenizer.
#
if not silent:
refs = state.env.setdefault("footnotes", {}).setdefault("list", {})
refs = _data_from_env(state.env)["list"]
footnoteId = len(refs)

tokens: list[Token] = []
Expand All @@ -200,7 +240,9 @@ def footnote_inline(state: StateInline, silent: bool) -> bool:
return True


def footnote_ref(state: StateInline, silent: bool) -> bool:
def footnote_ref(
state: StateInline, silent: bool, *, always_match: bool = False
) -> bool:
"""Process footnote references ([^...])"""

maximum = state.posMax
Expand All @@ -210,7 +252,9 @@ def footnote_ref(state: StateInline, silent: bool) -> bool:
if start + 3 > maximum:
return False

if "footnotes" not in state.env or "refs" not in state.env["footnotes"]:
footnote_data = _data_from_env(state.env)

if not (always_match or footnote_data["refs"]):
return False
if state.src[start] != "[":
return False
Expand All @@ -219,9 +263,7 @@ def footnote_ref(state: StateInline, silent: bool) -> bool:

pos = start + 2
while pos < maximum:
if state.src[pos] == " ":
return False
if state.src[pos] == "\n":
if state.src[pos] in (" ", "\n"):
return False
if state.src[pos] == "]":
break
Expand All @@ -234,22 +276,19 @@ def footnote_ref(state: StateInline, silent: bool) -> bool:
pos += 1

label = state.src[start + 2 : pos - 1]
if (":" + label) not in state.env["footnotes"]["refs"]:
if ((":" + label) not in footnote_data["refs"]) and not always_match:
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
if footnote_data["refs"].get(":" + label, -1) < 0:
footnoteId = len(footnote_data["list"])
footnote_data["list"][footnoteId] = {"label": label, "count": 0}
footnote_data["refs"][":" + label] = footnoteId
else:
footnoteId = state.env["footnotes"]["refs"][":" + label]
footnoteId = footnote_data["refs"][":" + label]

footnoteSubId = state.env["footnotes"]["list"][footnoteId]["count"]
state.env["footnotes"]["list"][footnoteId]["count"] += 1
footnoteSubId = footnote_data["list"][footnoteId]["count"]
footnote_data["list"][footnoteId]["count"] += 1

token = state.push("footnote_ref", "", 0)
token.meta = {"id": footnoteId, "subId": footnoteSubId, "label": label}
Expand Down Expand Up @@ -295,14 +334,14 @@ def footnote_tail(state: StateCore) -> None:

state.tokens = [t for t, f in zip(state.tokens, tok_filter) if f]

if "list" not in state.env.get("footnotes", {}):
footnote_data = _data_from_env(state.env)
if not footnote_data["list"]:
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():
for i, foot_note in footnote_data["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
Expand All @@ -326,7 +365,7 @@ def footnote_tail(state: StateCore) -> None:
tokens.append(token)

elif "label" in foot_note:
tokens = refTokens[":" + foot_note["label"]]
tokens = refTokens.get(":" + foot_note["label"], [])

state.tokens.extend(tokens)
if state.tokens[len(state.tokens) - 1].type == "paragraph_close":
Expand Down
20 changes: 20 additions & 0 deletions tests/fixtures/footnote.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,23 @@ Indented by 4 spaces, DISABLE-CODEBLOCKS
</ol>
</section>
.

refs with no definition standard
.
[^1] [^1]
.
<p>[^1] [^1]</p>
.

refs with no definition, ALWAYS_MATCH-REFS
.
[^1] [^1]
.
<p><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> <sup class="footnote-ref"><a href="#fn1" id="fnref1:1">[1:1]</a></sup></p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"> <a href="#fnref1" class="footnote-backref">↩︎</a> <a href="#fnref1:1" class="footnote-backref">↩︎</a></li>
</ol>
</section>
.
6 changes: 4 additions & 2 deletions tests/test_footnote.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def test_footnote_def():
"hidden": False,
},
]
assert state.env == {"footnotes": {"refs": {":a": -1}}}
assert state.env == {"footnotes": {"refs": {":a": -1}, "list": {}}}


def test_footnote_ref():
Expand Down Expand Up @@ -440,7 +440,9 @@ def test_plugin_render():

@pytest.mark.parametrize("line,title,input,expected", read_fixture_file(FIXTURE_PATH))
def test_all(line, title, input, expected):
md = MarkdownIt("commonmark").use(footnote_plugin)
md = MarkdownIt().use(
footnote_plugin, always_match_refs="ALWAYS_MATCH-REFS" in title
)
if "DISABLE-CODEBLOCKS" in title:
md.disable("code")
md.options["xhtmlOut"] = False
Expand Down
Loading