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 7e1923f35ec4..858ecad9c11a 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 @@ -12,7 +12,7 @@ from markdown_it.token import Token from . import md, options from .docbook import DocBookRenderer, Heading -from .manual_structure import check_titles, FragmentType, TocEntryType +from .manual_structure import check_structure, FragmentType, is_include, TocEntryType from .md import Converter class BaseConverter(Converter[md.TR], Generic[md.TR]): @@ -30,9 +30,9 @@ class BaseConverter(Converter[md.TR], Generic[md.TR]): def _parse(self, src: str) -> list[Token]: tokens = super()._parse(src) - check_titles(self._current_type[-1], tokens) + check_structure(self._current_type[-1], tokens) for token in tokens: - if token.type != "fence" or not token.info.startswith("{=include=} "): + if not is_include(token): continue typ = token.info[12:].strip() if typ == 'options': diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual_structure.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual_structure.py index 32b6287b34ad..93a8ecc3f935 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual_structure.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual_structure.py @@ -8,7 +8,41 @@ FragmentType = Literal['preface', 'part', 'chapter', 'section', 'appendix'] # in the TOC all fragments are allowed, plus the all-encompassing book. TocEntryType = Literal['book', 'preface', 'part', 'chapter', 'section', 'appendix'] -def check_titles(kind: TocEntryType, tokens: Sequence[Token]) -> None: +def is_include(token: Token) -> bool: + return token.type == "fence" and token.info.startswith("{=include=} ") + +# toplevel file must contain only the title headings and includes, anything else +# would cause strange rendering. +def _check_book_structure(tokens: Sequence[Token]) -> None: + for token in tokens[6:]: + if not is_include(token): + assert token.map + raise RuntimeError(f"unexpected content in line {token.map[0] + 1}, " + "expected structural include") + +# much like books, parts may not contain headings other than their title heading. +# this is a limitation of the current renderers that do not handle this case well +# even though it is supported in docbook (and probably supportable anywhere else). +def _check_part_structure(tokens: Sequence[Token]) -> None: + _check_fragment_structure(tokens) + for token in tokens[3:]: + if token.type == 'heading_open': + assert token.map + raise RuntimeError(f"unexpected heading in line {token.map[0] + 1}") + +# two include blocks must either be adjacent or separated by a heading, otherwise +# we cannot generate a correct TOC (since there'd be nothing to link to between +# the two includes). +def _check_fragment_structure(tokens: Sequence[Token]) -> None: + for i, token in enumerate(tokens): + if is_include(token) \ + and i + 1 < len(tokens) \ + and not (is_include(tokens[i + 1]) or tokens[i + 1].type == 'heading_open'): + assert token.map + raise RuntimeError(f"unexpected content in line {token.map[0] + 1}, " + "expected heading or structural include") + +def check_structure(kind: TocEntryType, tokens: Sequence[Token]) -> None: wanted = { 'h1': 'title' } wanted |= { 'h2': 'subtitle' } if kind == 'book' else {} for (i, (tag, role)) in enumerate(wanted.items()): @@ -46,3 +80,10 @@ def check_titles(kind: TocEntryType, tokens: Sequence[Token]) -> None: raise RuntimeError(f"heading in line {token.map[0] + 1} skips one or more heading levels, " "which is currently not allowed") last_heading_level = level + + if kind == 'book': + _check_book_structure(tokens) + elif kind == 'part': + _check_part_structure(tokens) + else: + _check_fragment_structure(tokens)