nixos-render-docs: check book structure

text content in the toplevel file of a book will not render properly.
the first proper element will be a preface, part, or chapter anyway, and
those require includes to produce.

parts do not currently allow headings in the part file itself, but
that's mainly a renderer limitation. we can add support for headings in
part intros when we need them

in all other cases includes must be followed by either another include,
a heading, or end of file. text content could not be properly linked to
from a TOC without a preceding heading.
This commit is contained in:
pennae 2023-02-18 20:48:12 +01:00
parent 163b667352
commit 768794d6c1
2 changed files with 45 additions and 4 deletions

View File

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

View File

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