From 538b3d1b3c12cc07f00dcec374b16a469b3679ff Mon Sep 17 00:00:00 2001 From: pennae Date: Sun, 11 Jun 2023 00:34:32 +0200 Subject: 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. --- .../src/nixos_render_docs/html.py | 29 +++++++++++++ .../src/nixos_render_docs/manual.py | 4 ++ .../nixos-render-docs/src/nixos_render_docs/md.py | 47 ++++++++++++++++++++++ .../nix/nixos-render-docs/src/tests/test_html.py | 27 +++++++++++++ .../nixos-render-docs/src/tests/test_plugins.py | 25 ++++++++++++ 5 files changed, 132 insertions(+) diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py index 2c8113339b7b..ffe64cde4d34 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py @@ -298,6 +298,35 @@ class HTMLRenderer(Renderer): return f'' def td_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "" + 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'' + f'[{token.meta["id"] + 1}]' + '' + ) + def footnote_block_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return ( + '
' + '
' + '
' + ) + def footnote_block_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "
" + 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'
' + def footnote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "
" + def footnote_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str: + href = self._xref_targets[token.meta['target']].href() + return ( + f'' + f'[{token.meta["id"] + 1}]' + '' + ) def _make_hN(self, level: int) -> tuple[str, str]: return f"h{min(6, max(1, level + self._hlevel_offset))}", "" diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py index a0ba3116fe31..03c5a5dd3960 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py @@ -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) diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py index c5efe2021a25..f754b61b4439 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py @@ -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")) diff --git a/pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py b/pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py index ad1f7189be2c..96cf8d0b7dff 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py +++ b/pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py @@ -119,6 +119,33 @@ def test_tables() -> None: """) +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(""" +

+ foo [1]␣ + baz [1] +

+
+
+
+
+

+ note[1] + [1] +

+
+
+ """) + def test_full() -> None: c = Converter({ 'man(1)': 'http://example.org' }, {}) assert c._render(sample1) == unpretty(""" diff --git a/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py b/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py index fb7a4ab0117f..8564297efdd3 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py +++ b/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py @@ -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), + ] -- cgit 1.4.1