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 4a0504e61b0a..e5ffcdadba94 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 @@ -237,6 +237,26 @@ class HTMLRenderer(Renderer): f'' '' ) + def figure_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + if anchor := cast(str, token.attrs.get('id', '')): + anchor = f'' + return f'
{anchor}' + def figure_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return ( + '
' + '
' + ) + def figure_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return ( + '

' + ' ' + ) + def figure_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return ( + ' ' + '

' + '
' + ) 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 cb6b1a023567..a0ba3116fe31 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 @@ -426,6 +426,19 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): if next_level: result.append(f'
{"".join(next_level)}
') return result + def build_list(kind: str, id: str, lst: Sequence[TocEntry]) -> str: + if not lst: + return "" + entries = [ + f'
{i}. {e.target.toc_html}
' + for i, e in enumerate(lst, start=1) + ] + return ( + f'
' + f'

List of {kind}

' + f'
{"".join(entries)}
' + '
' + ) # we don't want to generate the "Title of Contents" header for sections, # docbook doesn't and it's only distracting clutter unless it's the main table. # we also want to generate tocs only for a top-level section (ie, one that is @@ -442,18 +455,8 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): toc_depth = self._html_params.toc_depth if not (items := walk_and_emit(toc, toc_depth)): return "" - examples = "" - if toc.examples: - examples_entries = [ - f'
{i + 1}. {ex.target.toc_html}
' - for i, ex in enumerate(toc.examples) - ] - examples = ( - '
' - '

List of Examples

' - f'
{"".join(examples_entries)}
' - '
' - ) + figures = build_list("Figures", "list-of-figures", toc.figures) + examples = build_list("Examples", "list-of-examples", toc.examples) return "".join([ f'
', '

Table of Contents

' if print_title else "", @@ -461,6 +464,7 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): f' {"".join(items)}' f' ' f'
' + f'{figures}' f'{examples}' ]) @@ -612,6 +616,8 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): result += self._collect_ids(sub, sub_file, subtyp, si == 0 and sub_file != target_file) elif bt.type == 'example_open' and (id := cast(str, bt.attrs.get('id', ''))): 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 == 'inline': assert bt.children result += self._collect_ids(bt.children, target_file, typ, False) @@ -636,8 +642,8 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): title = prefix + title_html toc_html = f"{n}. {title_html}" title_html = f"Appendix {n}" - elif typ == 'example': - # skip the prepended `Example N. ` from numbering + elif typ in ['example', 'figure']: + # skip the prepended `{Example,Figure} N. ` from numbering toc_html, title = self._renderer.renderInline(inlines.children[2:]), title_html # xref title wants only the prepended text, sans the trailing colon and space title_html = self._renderer.renderInline(inlines.children[0:1]) @@ -653,6 +659,7 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) -> None: self._number_block('example', "Example", tokens) + self._number_block('figure', "Figure", tokens) xref_queue = self._collect_ids(tokens, outfile.name, 'book', True) failed = False 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 fcc3f02f19a8..c6e6bf429370 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 @@ -14,7 +14,7 @@ from .utils import Freezeable 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', 'example'] +TocEntryType = Literal['book', 'preface', 'part', 'chapter', 'section', 'appendix', 'example', 'figure'] def is_include(token: Token) -> bool: return token.type == "fence" and token.info.startswith("{=include=} ") @@ -128,6 +128,7 @@ class TocEntry(Freezeable): children: list[TocEntry] = dc.field(default_factory=list) starts_new_chunk: bool = False examples: list[TocEntry] = dc.field(default_factory=list) + figures: list[TocEntry] = dc.field(default_factory=list) @property def root(self) -> TocEntry: @@ -142,7 +143,7 @@ class TocEntry(Freezeable): @classmethod def collect_and_link(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token]) -> TocEntry: - entries, examples = cls._collect_entries(xrefs, tokens, 'book') + entries, examples, figures = cls._collect_entries(xrefs, tokens, 'book') def flatten_with_parent(this: TocEntry, parent: TocEntry | None) -> Iterable[TocEntry]: this.parent = parent @@ -160,6 +161,7 @@ class TocEntry(Freezeable): paths_seen.add(c.target.path) flat[0].examples = examples + flat[0].figures = figures for c in flat: c.freeze() @@ -168,21 +170,23 @@ class TocEntry(Freezeable): @classmethod def _collect_entries(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token], - kind: TocEntryType) -> tuple[TocEntry, list[TocEntry]]: + kind: TocEntryType) -> tuple[TocEntry, list[TocEntry], list[TocEntry]]: # we assume that check_structure has been run recursively over the entire input. # list contains (tag, entry) pairs that will collapse to a single entry for # the full sequence. entries: list[tuple[str, TocEntry]] = [] examples: list[TocEntry] = [] + figures: list[TocEntry] = [] for token in tokens: if token.type.startswith('included_') and (included := token.meta.get('included')): fragment_type_str = token.type[9:].removesuffix('s') assert fragment_type_str in get_args(TocEntryType) fragment_type = cast(TocEntryType, fragment_type_str) for fragment, _path in included: - subentries, subexamples = cls._collect_entries(xrefs, fragment, fragment_type) + subentries, subexamples, subfigures = cls._collect_entries(xrefs, fragment, fragment_type) entries[-1][1].children.append(subentries) examples += subexamples + figures += subfigures elif token.type == 'heading_open' and (id := cast(str, token.attrs.get('id', ''))): while len(entries) > 1 and entries[-1][0] >= token.tag: entries[-2][1].children.append(entries.pop()[1]) @@ -191,7 +195,9 @@ class TocEntry(Freezeable): token.meta['TocEntry'] = entries[-1][1] elif token.type == 'example_open' and (id := cast(str, token.attrs.get('id', ''))): examples.append(TocEntry('example', xrefs[id])) + elif token.type == 'figure_open' and (id := cast(str, token.attrs.get('id', ''))): + figures.append(TocEntry('figure', xrefs[id])) while len(entries) > 1: entries[-2][1].children.append(entries.pop()[1]) - return (entries[0][1], examples) + return (entries[0][1], examples, figures) 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 35a1657ef6bb..1ee21986b0ff 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 @@ -40,7 +40,7 @@ def md_make_code(code: str, info: str = "", multiline: Optional[bool] = None) -> ticks, sep = ('`' * (longest + (3 if multiline else 1)), '\n' if multiline else ' ') return f"{ticks}{info}{sep}{code}{sep}{ticks}" -AttrBlockKind = Literal['admonition', 'example'] +AttrBlockKind = Literal['admonition', 'example', 'figure'] AdmonitionKind = Literal["note", "caution", "tip", "important", "warning"] @@ -91,6 +91,10 @@ class Renderer: "example_title_open": self.example_title_open, "example_title_close": self.example_title_close, "image": self.image, + "figure_open": self.figure_open, + "figure_close": self.figure_close, + "figure_title_open": self.figure_title_open, + "figure_title_close": self.figure_title_close, } self._admonitions = { @@ -228,6 +232,14 @@ class Renderer: raise RuntimeError("md token not supported", token) def image(self, token: Token, tokens: Sequence[Token], i: int) -> str: raise RuntimeError("md token not supported", token) + def figure_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def figure_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def figure_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def figure_title_close(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 @@ -270,6 +282,8 @@ def _parse_blockattrs(info: str) -> Optional[tuple[AttrBlockKind, Optional[str], return ('admonition', id, classes) if classes == ['example']: return ('example', id, classes) + elif classes == ['figure']: + return ('figure', id, classes) return None def _attr_span_plugin(md: markdown_it.MarkdownIt) -> None: @@ -419,6 +433,11 @@ def _block_attr(md: markdown_it.MarkdownIt) -> None: if id is not None: token.attrs['id'] = id stack.append('example_close') + elif kind == 'figure': + token.type = 'figure_open' + if id is not None: + token.attrs['id'] = id + stack.append('figure_close') else: assert_never(kind) elif token.type == 'container_blockattr_close': @@ -501,6 +520,7 @@ class Converter(ABC, Generic[TR]): self._md.use(_compact_list_attr) self._md.use(_block_attr) self._md.use(_block_titles("example")) + self._md.use(_block_titles("figure")) self._md.enable(["smartquotes", "replacements"]) def _parse(self, src: str) -> list[Token]: