nixos-render-docs: add heading id support

as with inline spans we support only ids being set for heading, not
arbitrary attributes or classes.
This commit is contained in:
pennae 2023-01-25 18:19:11 +01:00
parent 82d5698e22
commit 8e3b2a4eaa
3 changed files with 119 additions and 1 deletions

View file

@ -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)

View file

@ -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]:

View file

@ -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)
]