summary refs log tree commit diff
path: root/pkgs/tools/nix/nixos-render-docs
diff options
context:
space:
mode:
authorpennae <github@quasiparticle.net>2023-06-11 00:34:32 +0200
committerpennae <github@quasiparticle.net>2023-07-01 20:27:29 +0200
commit538b3d1b3c12cc07f00dcec374b16a469b3679ff (patch)
tree17a8b9fb46d63f4471e00502aa816b7a54f8d454 /pkgs/tools/nix/nixos-render-docs
parentac7be1f106c139f6153576a80372b2e216f005fc (diff)
downloadnixpkgs-538b3d1b3c12cc07f00dcec374b16a469b3679ff.tar
nixpkgs-538b3d1b3c12cc07f00dcec374b16a469b3679ff.tar.gz
nixpkgs-538b3d1b3c12cc07f00dcec374b16a469b3679ff.tar.bz2
nixpkgs-538b3d1b3c12cc07f00dcec374b16a469b3679ff.tar.lz
nixpkgs-538b3d1b3c12cc07f00dcec374b16a469b3679ff.tar.xz
nixpkgs-538b3d1b3c12cc07f00dcec374b16a469b3679ff.tar.zst
nixpkgs-538b3d1b3c12cc07f00dcec374b16a469b3679ff.zip
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.
Diffstat (limited to 'pkgs/tools/nix/nixos-render-docs')
-rw-r--r--pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py29
-rw-r--r--pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py4
-rw-r--r--pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py47
-rw-r--r--pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py27
-rw-r--r--pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py25
5 files changed, 132 insertions, 0 deletions
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 2c8113339b7..ffe64cde4d3 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'<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))}", ""
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 a0ba3116fe3..03c5a5dd396 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 c5efe2021a2..f754b61b443 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 ad1f7189be2..96cf8d0b7df 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:
       </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("""
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 fb7a4ab0117..8564297efdd 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),
+    ]