mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-06-10 19:55:41 +03:00
nixos-render-docs: add footnote support
this is only used in the stdenv chapter, but footnotes could be useful in other places as well. since markdown-it has a plugin to parse footnote syntax we may as well just support them even if they're rare.
This commit is contained in:
parent
ac7be1f106
commit
538b3d1b3c
5 changed files with 132 additions and 0 deletions
|
@ -298,6 +298,35 @@ class HTMLRenderer(Renderer):
|
|||
return f'<td align="{cast(str, token.attrs.get("style", "left")).removeprefix("text-align:")}">'
|
||||
def td_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
return "</td>"
|
||||
def footnote_ref(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
href = self._xref_targets[token.meta['target']].href()
|
||||
id = escape(cast(str, token.attrs["id"]), True)
|
||||
return (
|
||||
f'<a href="{href}" class="footnote" id="{id}">'
|
||||
f'<sup class="footnote">[{token.meta["id"] + 1}]</sup>'
|
||||
'</a>'
|
||||
)
|
||||
def footnote_block_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
return (
|
||||
'<div class="footnotes">'
|
||||
'<br />'
|
||||
'<hr style="width:100; text-align:left;margin-left: 0" />'
|
||||
)
|
||||
def footnote_block_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
return "</div>"
|
||||
def footnote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
# meta id,label
|
||||
id = escape(self._xref_targets[token.meta["label"]].id, True)
|
||||
return f'<div id="{id}" class="footnote">'
|
||||
def footnote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
return "</div>"
|
||||
def footnote_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
href = self._xref_targets[token.meta['target']].href()
|
||||
return (
|
||||
f'<a href="{href}" class="para">'
|
||||
f'<sup class="para">[{token.meta["id"] + 1}]</sup>'
|
||||
'</a>'
|
||||
)
|
||||
|
||||
def _make_hN(self, level: int) -> tuple[str, str]:
|
||||
return f"h{min(6, max(1, level + self._hlevel_offset))}", ""
|
||||
|
|
|
@ -618,6 +618,10 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]):
|
|||
result.append((id, 'example', tokens[i + 2], target_file, False))
|
||||
elif bt.type == 'figure_open' and (id := cast(str, bt.attrs.get('id', ''))):
|
||||
result.append((id, 'figure', tokens[i + 2], target_file, False))
|
||||
elif bt.type == 'footnote_open' and (id := cast(str, bt.attrs.get('id', ''))):
|
||||
result.append(XrefTarget(id, "???", None, None, target_file))
|
||||
elif bt.type == 'footnote_ref' and (id := cast(str, bt.attrs.get('id', ''))):
|
||||
result.append(XrefTarget(id, "???", None, None, target_file))
|
||||
elif bt.type == 'inline':
|
||||
assert bt.children
|
||||
result += self._collect_ids(bt.children, target_file, typ, False)
|
||||
|
|
|
@ -12,6 +12,7 @@ from markdown_it.token import Token
|
|||
from markdown_it.utils import OptionsDict
|
||||
from mdit_py_plugins.container import container_plugin # type: ignore[attr-defined]
|
||||
from mdit_py_plugins.deflist import deflist_plugin # type: ignore[attr-defined]
|
||||
from mdit_py_plugins.footnote import footnote_plugin # type: ignore[attr-defined]
|
||||
from mdit_py_plugins.myst_role import myst_role_plugin # type: ignore[attr-defined]
|
||||
|
||||
_md_escape_table = {
|
||||
|
@ -107,6 +108,12 @@ class Renderer:
|
|||
"tbody_close": self.tbody_close,
|
||||
"td_open": self.td_open,
|
||||
"td_close": self.td_close,
|
||||
"footnote_ref": self.footnote_ref,
|
||||
"footnote_block_open": self.footnote_block_open,
|
||||
"footnote_block_close": self.footnote_block_close,
|
||||
"footnote_open": self.footnote_open,
|
||||
"footnote_close": self.footnote_close,
|
||||
"footnote_anchor": self.footnote_anchor,
|
||||
}
|
||||
|
||||
self._admonitions = {
|
||||
|
@ -276,6 +283,18 @@ class Renderer:
|
|||
raise RuntimeError("md token not supported", token)
|
||||
def td_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
raise RuntimeError("md token not supported", token)
|
||||
def footnote_ref(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
raise RuntimeError("md token not supported", token)
|
||||
def footnote_block_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
raise RuntimeError("md token not supported", token)
|
||||
def footnote_block_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
raise RuntimeError("md token not supported", token)
|
||||
def footnote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
raise RuntimeError("md token not supported", token)
|
||||
def footnote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
raise RuntimeError("md token not supported", token)
|
||||
def footnote_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||
raise RuntimeError("md token not supported", token)
|
||||
|
||||
def _is_escaped(src: str, pos: int) -> bool:
|
||||
found = 0
|
||||
|
@ -421,6 +440,32 @@ def _heading_ids(md: markdown_it.MarkdownIt) -> None:
|
|||
|
||||
md.core.ruler.before("replacements", "heading_ids", heading_ids)
|
||||
|
||||
def _footnote_ids(md: markdown_it.MarkdownIt) -> None:
|
||||
"""generate ids for footnotes, their refs, and their backlinks. the ids we
|
||||
generate here are derived from the footnote label, making numeric footnote
|
||||
labels invalid.
|
||||
"""
|
||||
def generate_ids(tokens: Sequence[Token]) -> None:
|
||||
for token in tokens:
|
||||
if token.type == 'footnote_open':
|
||||
if token.meta["label"][:1].isdigit():
|
||||
assert token.map
|
||||
raise RuntimeError(f"invalid footnote label in line {token.map[0] + 1}")
|
||||
token.attrs['id'] = token.meta["label"]
|
||||
elif token.type == 'footnote_anchor':
|
||||
token.meta['target'] = f'{token.meta["label"]}.__back.{token.meta["subId"]}'
|
||||
elif token.type == 'footnote_ref':
|
||||
token.attrs['id'] = f'{token.meta["label"]}.__back.{token.meta["subId"]}'
|
||||
token.meta['target'] = token.meta["label"]
|
||||
elif token.type == 'inline':
|
||||
assert token.children
|
||||
generate_ids(token.children)
|
||||
|
||||
def footnote_ids(state: markdown_it.rules_core.StateCore) -> None:
|
||||
generate_ids(state.tokens)
|
||||
|
||||
md.core.ruler.after("footnote_tail", "footnote_ids", footnote_ids)
|
||||
|
||||
def _compact_list_attr(md: markdown_it.MarkdownIt) -> None:
|
||||
@dataclasses.dataclass
|
||||
class Entry:
|
||||
|
@ -549,11 +594,13 @@ class Converter(ABC, Generic[TR]):
|
|||
validate=lambda name, *args: _parse_blockattrs(name),
|
||||
)
|
||||
self._md.use(deflist_plugin)
|
||||
self._md.use(footnote_plugin)
|
||||
self._md.use(myst_role_plugin)
|
||||
self._md.use(_attr_span_plugin)
|
||||
self._md.use(_inline_comment_plugin)
|
||||
self._md.use(_block_comment_plugin)
|
||||
self._md.use(_heading_ids)
|
||||
self._md.use(_footnote_ids)
|
||||
self._md.use(_compact_list_attr)
|
||||
self._md.use(_block_attr)
|
||||
self._md.use(_block_titles("example"))
|
||||
|
|
|
@ -119,6 +119,33 @@ def test_tables() -> None:
|
|||
</div>
|
||||
""")
|
||||
|
||||
def test_footnotes() -> None:
|
||||
c = Converter({}, {
|
||||
"bar": nrd.manual_structure.XrefTarget("bar", "", None, None, ""),
|
||||
"bar.__back.0": nrd.manual_structure.XrefTarget("bar.__back.0", "", None, None, ""),
|
||||
"bar.__back.1": nrd.manual_structure.XrefTarget("bar.__back.1", "", None, None, ""),
|
||||
})
|
||||
assert c._render(textwrap.dedent("""
|
||||
foo [^bar] baz [^bar]
|
||||
|
||||
[^bar]: note
|
||||
""")) == unpretty("""
|
||||
<p>
|
||||
foo <a href="#bar" class="footnote" id="bar.__back.0"><sup class="footnote">[1]</sup></a>␣
|
||||
baz <a href="#bar" class="footnote" id="bar.__back.1"><sup class="footnote">[1]</sup></a>
|
||||
</p>
|
||||
<div class="footnotes">
|
||||
<br />
|
||||
<hr style="width:100; text-align:left;margin-left: 0" />
|
||||
<div id="bar" class="footnote">
|
||||
<p>
|
||||
note<a href="#bar.__back.0" class="para"><sup class="para">[1]</sup></a>
|
||||
<a href="#bar.__back.1" class="para"><sup class="para">[1]</sup></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
""")
|
||||
|
||||
def test_full() -> None:
|
||||
c = Converter({ 'man(1)': 'http://example.org' }, {})
|
||||
assert c._render(sample1) == unpretty("""
|
||||
|
|
|
@ -501,3 +501,28 @@ def test_example() -> None:
|
|||
with pytest.raises(RuntimeError) as exc:
|
||||
c._parse("::: {.example}\n### foo\n### bar\n:::")
|
||||
assert exc.value.args[0] == 'unexpected non-title heading in example in line 3'
|
||||
|
||||
def test_footnotes() -> None:
|
||||
c = Converter({})
|
||||
assert c._parse("text [^foo]\n\n[^foo]: bar") == [
|
||||
Token(type='paragraph_open', tag='p', nesting=1, map=[0, 1], block=True),
|
||||
Token(type='inline', tag='', nesting=0, map=[0, 1], level=1, content='text [^foo]', block=True,
|
||||
children=[
|
||||
Token(type='text', tag='', nesting=0, content='text '),
|
||||
Token(type='footnote_ref', tag='', nesting=0, attrs={'id': 'foo.__back.0'},
|
||||
meta={'id': 0, 'subId': 0, 'label': 'foo', 'target': 'foo'})
|
||||
]),
|
||||
Token(type='paragraph_close', tag='p', nesting=-1, block=True),
|
||||
Token(type='footnote_block_open', tag='', nesting=1),
|
||||
Token(type='footnote_open', tag='', nesting=1, attrs={'id': 'foo'}, meta={'id': 0, 'label': 'foo'}),
|
||||
Token(type='paragraph_open', tag='p', nesting=1, map=[2, 3], level=1, block=True, hidden=False),
|
||||
Token(type='inline', tag='', nesting=0, map=[2, 3], level=2, content='bar', block=True,
|
||||
children=[
|
||||
Token(type='text', tag='', nesting=0, content='bar')
|
||||
]),
|
||||
Token(type='footnote_anchor', tag='', nesting=0,
|
||||
meta={'id': 0, 'label': 'foo', 'subId': 0, 'target': 'foo.__back.0'}),
|
||||
Token(type='paragraph_close', tag='p', nesting=-1, level=1, block=True),
|
||||
Token(type='footnote_close', tag='', nesting=-1),
|
||||
Token(type='footnote_block_close', tag='', nesting=-1),
|
||||
]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue