From 8e3b2a4eaae045354f0fcbf39b5d5575042eca18 Mon Sep 17 00:00:00 2001 From: pennae Date: Wed, 25 Jan 2023 18:19:11 +0100 Subject: [PATCH] nixos-render-docs: add heading id support as with inline spans we support only ids being set for heading, not arbitrary attributes or classes. --- .../src/nixos_render_docs/docbook.py | 5 +- .../src/nixos_render_docs/md.py | 13 +++ .../src/tests/test_headings.py | 102 ++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 pkgs/tools/nix/nixos-render-docs/src/tests/test_headings.py diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/docbook.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/docbook.py index 3050093a5bd5..3676c17a9909 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/docbook.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/docbook.py @@ -246,4 +246,7 @@ class DocBookRenderer(Renderer): def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, env: MutableMapping[str, Any]) -> tuple[str, dict[str, str]]: - return ("section", {}) + attrs = {} + if id := token.attrs.get('id'): + attrs['xml:id'] = cast(str, id) + return ("section", attrs) 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 7aaa56031a3e..ad28dbabe574 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 @@ -316,6 +316,8 @@ def _block_comment_plugin(md: markdown_it.MarkdownIt) -> None: md.block.ruler.after("code", "block_comment", block_comment) +_HEADER_ID_RE = re.compile(r"\s*\{\s*\#([\w-]+)\s*\}\s*$") + class Converter(ABC): __renderer__: Callable[[Mapping[str, str], markdown_it.MarkdownIt], Renderer] @@ -346,6 +348,17 @@ class Converter(ABC): self._md.enable(["smartquotes", "replacements"]) def _post_parse(self, tokens: list[Token]) -> list[Token]: + for i in range(0, len(tokens)): + # parse header IDs. this is purposely simple and doesn't support + # classes or other inds of attributes. + if tokens[i].type == 'heading_open': + children = tokens[i + 1].children + assert children is not None + if len(children) == 0 or children[-1].type != 'text': + continue + if m := _HEADER_ID_RE.search(children[-1].content): + tokens[i].attrs['id'] = m[1] + children[-1].content = children[-1].content[:-len(m[0])].rstrip() return tokens def _parse(self, src: str, env: Optional[MutableMapping[str, Any]] = None) -> list[Token]: diff --git a/pkgs/tools/nix/nixos-render-docs/src/tests/test_headings.py b/pkgs/tools/nix/nixos-render-docs/src/tests/test_headings.py new file mode 100644 index 000000000000..0b73cdc8e7c7 --- /dev/null +++ b/pkgs/tools/nix/nixos-render-docs/src/tests/test_headings.py @@ -0,0 +1,102 @@ +import nixos_render_docs + +from markdown_it.token import Token + +class Converter(nixos_render_docs.md.Converter): + # actual renderer doesn't matter, we're just parsing. + __renderer__ = nixos_render_docs.docbook.DocBookRenderer + +def test_heading_id_absent() -> None: + c = Converter({}) + assert c._parse("# foo") == [ + Token(type='heading_open', tag='h1', nesting=1, attrs={}, map=[0, 1], level=0, children=None, + content='', markup='#', info='', meta={}, block=True, hidden=False), + Token(type='inline', tag='', nesting=0, attrs={}, map=[0, 1], level=1, + children=[ + Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None, + content='foo', markup='', info='', meta={}, block=False, hidden=False) + ], + content='foo', markup='', info='', meta={}, block=True, hidden=False), + Token(type='heading_close', tag='h1', nesting=-1, attrs={}, map=None, level=0, children=None, + content='', markup='#', info='', meta={}, block=True, hidden=False) + ] + +def test_heading_id_present() -> None: + c = Converter({}) + assert c._parse("# foo {#foo}\n## bar { #bar}\n### bal { #bal} ") == [ + Token(type='heading_open', tag='h1', nesting=1, attrs={'id': 'foo'}, map=[0, 1], level=0, + children=None, content='', markup='#', info='', meta={}, block=True, hidden=False), + Token(type='inline', tag='', nesting=0, attrs={}, map=[0, 1], level=1, + content='foo {#foo}', markup='', info='', meta={}, block=True, hidden=False, + children=[ + Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None, + content='foo', markup='', info='', meta={}, block=False, hidden=False) + ]), + Token(type='heading_close', tag='h1', nesting=-1, attrs={}, map=None, level=0, children=None, + content='', markup='#', info='', meta={}, block=True, hidden=False), + Token(type='heading_open', tag='h2', nesting=1, attrs={'id': 'bar'}, map=[1, 2], level=0, + children=None, content='', markup='##', info='', meta={}, block=True, hidden=False), + Token(type='inline', tag='', nesting=0, attrs={}, map=[1, 2], level=1, + content='bar { #bar}', markup='', info='', meta={}, block=True, hidden=False, + children=[ + Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None, + content='bar', markup='', info='', meta={}, block=False, hidden=False) + ]), + Token(type='heading_close', tag='h2', nesting=-1, attrs={}, map=None, level=0, children=None, + content='', markup='##', info='', meta={}, block=True, hidden=False), + Token(type='heading_open', tag='h3', nesting=1, attrs={'id': 'bal'}, map=[2, 3], level=0, + children=None, content='', markup='###', info='', meta={}, block=True, hidden=False), + Token(type='inline', tag='', nesting=0, attrs={}, map=[2, 3], level=1, + content='bal { #bal}', markup='', info='', meta={}, block=True, hidden=False, + children=[ + Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None, + content='bal', markup='', info='', meta={}, block=False, hidden=False) + ]), + Token(type='heading_close', tag='h3', nesting=-1, attrs={}, map=None, level=0, children=None, + content='', markup='###', info='', meta={}, block=True, hidden=False) + ] + +def test_heading_id_incomplete() -> None: + c = Converter({}) + assert c._parse("# foo {#}") == [ + Token(type='heading_open', tag='h1', nesting=1, attrs={}, map=[0, 1], level=0, children=None, + content='', markup='#', info='', meta={}, block=True, hidden=False), + Token(type='inline', tag='', nesting=0, attrs={}, map=[0, 1], level=1, + content='foo {#}', markup='', info='', meta={}, block=True, hidden=False, + children=[ + Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None, + content='foo {#}', markup='', info='', meta={}, block=False, hidden=False) + ]), + Token(type='heading_close', tag='h1', nesting=-1, attrs={}, map=None, level=0, children=None, + content='', markup='#', info='', meta={}, block=True, hidden=False) + ] + +def test_heading_id_double() -> None: + c = Converter({}) + assert c._parse("# foo {#a} {#b}") == [ + Token(type='heading_open', tag='h1', nesting=1, attrs={'id': 'b'}, map=[0, 1], level=0, + children=None, content='', markup='#', info='', meta={}, block=True, hidden=False), + Token(type='inline', tag='', nesting=0, attrs={}, map=[0, 1], level=1, + content='foo {#a} {#b}', markup='', info='', meta={}, block=True, hidden=False, + children=[ + Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None, + content='foo {#a}', markup='', info='', meta={}, block=False, hidden=False) + ]), + Token(type='heading_close', tag='h1', nesting=-1, attrs={}, map=None, level=0, children=None, + content='', markup='#', info='', meta={}, block=True, hidden=False) + ] + +def test_heading_id_suffixed() -> None: + c = Converter({}) + assert c._parse("# foo {#a} s") == [ + Token(type='heading_open', tag='h1', nesting=1, attrs={}, map=[0, 1], level=0, + children=None, content='', markup='#', info='', meta={}, block=True, hidden=False), + Token(type='inline', tag='', nesting=0, attrs={}, map=[0, 1], level=1, + content='foo {#a} s', markup='', info='', meta={}, block=True, hidden=False, + children=[ + Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None, + content='foo {#a} s', markup='', info='', meta={}, block=False, hidden=False) + ]), + Token(type='heading_close', tag='h1', nesting=-1, attrs={}, map=None, level=0, children=None, + content='', markup='#', info='', meta={}, block=True, hidden=False) + ]