nixos-render-docs: add examples support

the nixos manual contains enough examples to support them as a proper
toc entity with specialized rendering, and if in the future the nixpkgs
wants to use nixos-render-docs we will definitely have to support them.
this also allows us to restore some examples that were lost in previous
translation steps because there were too few to add renderer support
back then.

pennae 407f6196 69259eec

+1 -1
nixos/doc/manual/development/freeform-modules.section.md
···
submodules.
::: {#ex-freeform-module .example}
-
**Example: Freeform submodule**
The following shows a submodule assigning a freeform type that allows
arbitrary attributes with `str` values below `settings`, but also
···
submodules.
::: {#ex-freeform-module .example}
+
### Freeform submodule
The following shows a submodule assigning a freeform type that allows
arbitrary attributes with `str` values below `settings`, but also
+7 -3
nixos/doc/manual/development/option-declarations.section.md
···
For example:
::: {#ex-options-declarations-util-mkEnableOption-magic .example}
```nix
lib.mkEnableOption (lib.mdDoc "magic")
# is like
···
Examples:
::: {#ex-options-declarations-util-mkPackageOption-hello .example}
```nix
lib.mkPackageOptionMD pkgs "hello" { }
# is like
···
:::
::: {#ex-options-declarations-util-mkPackageOption-ghc .example}
```nix
lib.mkPackageOptionMD pkgs "GHC" {
default = [ "ghc" ];
···
:::
::: {#ex-options-declarations-util-mkPackageOption-extraDescription .example}
```nix
mkPackageOption pkgs [ "python39Packages" "pytorch" ] {
extraDescription = "This is an example and doesn't actually do anything.";
···
enforces that there can only be a single display manager enabled.
::: {#ex-option-declaration-eot-service .example}
-
**Example: Extensible type placeholder in the service module**
```nix
services.xserver.displayManager.enable = mkOption {
description = "Display manager to use";
···
:::
::: {#ex-option-declaration-eot-backend-gdm .example}
-
**Example: Extending `services.xserver.displayManager.enable` in the `gdm` module**
```nix
services.xserver.displayManager.enable = mkOption {
type = with types; nullOr (enum [ "gdm" ]);
···
:::
::: {#ex-option-declaration-eot-backend-sddm .example}
-
**Example: Extending `services.xserver.displayManager.enable` in the `sddm` module**
```nix
services.xserver.displayManager.enable = mkOption {
type = with types; nullOr (enum [ "sddm" ]);
···
For example:
::: {#ex-options-declarations-util-mkEnableOption-magic .example}
+
### `mkEnableOption` usage
```nix
lib.mkEnableOption (lib.mdDoc "magic")
# is like
···
Examples:
::: {#ex-options-declarations-util-mkPackageOption-hello .example}
+
### Simple `mkPackageOption` usage
```nix
lib.mkPackageOptionMD pkgs "hello" { }
# is like
···
:::
::: {#ex-options-declarations-util-mkPackageOption-ghc .example}
+
### `mkPackageOption` with explicit default and example
```nix
lib.mkPackageOptionMD pkgs "GHC" {
default = [ "ghc" ];
···
:::
::: {#ex-options-declarations-util-mkPackageOption-extraDescription .example}
+
### `mkPackageOption` with additional description text
```nix
mkPackageOption pkgs [ "python39Packages" "pytorch" ] {
extraDescription = "This is an example and doesn't actually do anything.";
···
enforces that there can only be a single display manager enabled.
::: {#ex-option-declaration-eot-service .example}
+
### Extensible type placeholder in the service module
```nix
services.xserver.displayManager.enable = mkOption {
description = "Display manager to use";
···
:::
::: {#ex-option-declaration-eot-backend-gdm .example}
+
### Extending `services.xserver.displayManager.enable` in the `gdm` module
```nix
services.xserver.displayManager.enable = mkOption {
type = with types; nullOr (enum [ "gdm" ]);
···
:::
::: {#ex-option-declaration-eot-backend-sddm .example}
+
### Extending `services.xserver.displayManager.enable` in the `sddm` module
```nix
services.xserver.displayManager.enable = mkOption {
type = with types; nullOr (enum [ "sddm" ]);
+9 -9
nixos/doc/manual/development/option-types.section.md
···
together. This type is recommended when the option type is unknown.
::: {#ex-types-anything .example}
-
**Example: `types.anything` Example**
Two definitions of this type like
···
if you want to allow users to leave it undefined.
::: {#ex-submodule-direct .example}
-
**Example: Directly defined submodule**
```nix
options.mod = mkOption {
description = "submodule example";
···
:::
::: {#ex-submodule-reference .example}
-
**Example: Submodule defined as a reference**
```nix
let
modOptions = {
···
([Example: Definition of a list of submodules](#ex-submodule-listof-definition)).
::: {#ex-submodule-listof-declaration .example}
-
**Example: Declaration of a list of submodules**
```nix
options.mod = mkOption {
description = "submodule example";
···
:::
::: {#ex-submodule-listof-definition .example}
-
**Example: Definition of a list of submodules**
```nix
config.mod = [
{ foo = 1; bar = "one"; }
···
([Example: Definition of attribute sets of submodules](#ex-submodule-attrsof-definition)).
::: {#ex-submodule-attrsof-declaration .example}
-
**Example: Declaration of attribute sets of submodules**
```nix
options.mod = mkOption {
description = "submodule example";
···
:::
::: {#ex-submodule-attrsof-definition .example}
-
**Example: Definition of attribute sets of submodules**
```nix
config.mod.one = { foo = 1; bar = "one"; };
config.mod.two = { foo = 2; bar = "two"; };
···
([Example: Overriding a type check](#ex-extending-type-check-2)).
::: {#ex-extending-type-check-1 .example}
-
**Example: Adding a type check**
```nix
byte = mkOption {
···
:::
::: {#ex-extending-type-check-2 .example}
-
**Example: Overriding a type check**
```nix
nixThings = mkOption {
···
together. This type is recommended when the option type is unknown.
::: {#ex-types-anything .example}
+
### `types.anything`
Two definitions of this type like
···
if you want to allow users to leave it undefined.
::: {#ex-submodule-direct .example}
+
### Directly defined submodule
```nix
options.mod = mkOption {
description = "submodule example";
···
:::
::: {#ex-submodule-reference .example}
+
### Submodule defined as a reference
```nix
let
modOptions = {
···
([Example: Definition of a list of submodules](#ex-submodule-listof-definition)).
::: {#ex-submodule-listof-declaration .example}
+
### Declaration of a list of submodules
```nix
options.mod = mkOption {
description = "submodule example";
···
:::
::: {#ex-submodule-listof-definition .example}
+
### Definition of a list of submodules
```nix
config.mod = [
{ foo = 1; bar = "one"; }
···
([Example: Definition of attribute sets of submodules](#ex-submodule-attrsof-definition)).
::: {#ex-submodule-attrsof-declaration .example}
+
### Declaration of attribute sets of submodules
```nix
options.mod = mkOption {
description = "submodule example";
···
:::
::: {#ex-submodule-attrsof-definition .example}
+
### Definition of attribute sets of submodules
```nix
config.mod.one = { foo = 1; bar = "one"; };
config.mod.two = { foo = 2; bar = "two"; };
···
([Example: Overriding a type check](#ex-extending-type-check-2)).
::: {#ex-extending-type-check-1 .example}
+
### Adding a type check
```nix
byte = mkOption {
···
:::
::: {#ex-extending-type-check-2 .example}
+
### Overriding a type check
```nix
nixThings = mkOption {
+2 -2
nixos/doc/manual/development/settings-options.section.md
···
:::
::: {#ex-settings-nix-representable .example}
-
**Example: Module with conventional `settings` option**
The following shows a module for an example program that uses a JSON
configuration file. It demonstrates how above values can be used, along
···
up in the manual.
::: {#ex-settings-typed-attrs .example}
-
**Example: Declaring a type-checked `settings` attribute**
```nix
settings = lib.mkOption {
type = lib.types.submodule {
···
:::
::: {#ex-settings-nix-representable .example}
+
### Module with conventional `settings` option
The following shows a module for an example program that uses a JSON
configuration file. It demonstrates how above values can be used, along
···
up in the manual.
::: {#ex-settings-typed-attrs .example}
+
### Declaring a type-checked `settings` attribute
```nix
settings = lib.mkOption {
type = lib.types.submodule {
+3 -3
nixos/doc/manual/development/writing-modules.chapter.md
···
is shown in [Example: Structure of NixOS Modules](#ex-module-syntax).
::: {#ex-module-syntax .example}
-
**Example: Structure of NixOS Modules**
```nix
{ config, pkgs, ... }:
···
functions system environment substitution should *not* be disabled explicitly.
::: {#locate-example .example}
-
**Example: NixOS Module for the "locate" Service**
```nix
{ config, lib, pkgs, ... }:
···
:::
::: {#exec-escaping-example .example}
-
**Example: Escaping in Exec directives**
```nix
{ config, lib, pkgs, utils, ... }:
···
is shown in [Example: Structure of NixOS Modules](#ex-module-syntax).
::: {#ex-module-syntax .example}
+
### Structure of NixOS Modules
```nix
{ config, pkgs, ... }:
···
functions system environment substitution should *not* be disabled explicitly.
::: {#locate-example .example}
+
### NixOS Module for the "locate" Service
```nix
{ config, lib, pkgs, ... }:
···
:::
::: {#exec-escaping-example .example}
+
### Escaping in Exec directives
```nix
{ config, lib, pkgs, utils, ... }:
+4 -4
nixos/doc/manual/installation/installing.chapter.md
···
corresponding configuration Nix expression.
::: {#ex-partition-scheme-MBR .example}
-
**Example: Example partition schemes for NixOS on `/dev/sda` (MBR)**
```ShellSession
# parted /dev/sda -- mklabel msdos
# parted /dev/sda -- mkpart primary 1MB -8GB
···
:::
::: {#ex-partition-scheme-UEFI .example}
-
**Example: Example partition schemes for NixOS on `/dev/sda` (UEFI)**
```ShellSession
# parted /dev/sda -- mklabel gpt
# parted /dev/sda -- mkpart primary 512MB -8GB
···
:::
::: {#ex-install-sequence .example}
-
**Example: Commands for Installing NixOS on `/dev/sda`**
With a partitioned disk.
···
:::
::: {#ex-config .example}
-
**Example: NixOS Configuration**
```ShellSession
{ config, pkgs, ... }: {
imports = [
···
corresponding configuration Nix expression.
::: {#ex-partition-scheme-MBR .example}
+
### Example partition schemes for NixOS on `/dev/sda` (MBR)
```ShellSession
# parted /dev/sda -- mklabel msdos
# parted /dev/sda -- mkpart primary 1MB -8GB
···
:::
::: {#ex-partition-scheme-UEFI .example}
+
### Example partition schemes for NixOS on `/dev/sda` (UEFI)
```ShellSession
# parted /dev/sda -- mklabel gpt
# parted /dev/sda -- mkpart primary 512MB -8GB
···
:::
::: {#ex-install-sequence .example}
+
### Commands for Installing NixOS on `/dev/sda`
With a partitioned disk.
···
:::
::: {#ex-config .example}
+
### Example: NixOS Configuration
```ShellSession
{ config, pkgs, ... }: {
imports = [
+8 -4
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/docbook.py
···
result += f"<partintro{maybe_id}>"
return result
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
-
if id := token.attrs.get('id'):
-
return f"<anchor xml:id={quoteattr(cast(str, id))} />"
-
return ""
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
-
return ""
def _close_headings(self, level: Optional[int]) -> str:
# we rely on markdown-it producing h{1..6} tags in token.tag for this to work
···
result += f"<partintro{maybe_id}>"
return result
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
+
if id := cast(str, token.attrs.get('id', '')):
+
id = f'xml:id={quoteattr(id)}' if id else ''
+
return f'<example {id}>'
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
+
return "</example>"
+
def example_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
+
return "<title>"
+
def example_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
+
return "</title>"
def _close_headings(self, level: Optional[int]) -> str:
# we rely on markdown-it producing h{1..6} tags in token.tag for this to work
+8 -4
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py
···
self._ordered_list_nesting -= 1;
return "</ol></div>"
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
-
if id := token.attrs.get('id'):
-
return f'<a id="{escape(cast(str, id), True)}" />'
-
return ""
def example_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))}", ""
···
self._ordered_list_nesting -= 1;
return "</ol></div>"
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
+
if id := cast(str, token.attrs.get('id', '')):
+
id = f'id="{escape(id, True)}"' if id else ''
+
return f'<div class="example"><a {id} />'
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
+
return '</div></div><br class="example-break" />'
+
def example_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
+
return '<p class="title"><strong>'
+
def example_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
+
return '</strong></p><div class="example-contents">'
def _make_hN(self, level: int) -> tuple[str, str]:
return f"h{min(6, max(1, level + self._hlevel_offset))}", ""
+40
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py
···
)
if not (items := walk_and_emit(toc, toc_depth)):
return ""
return (
f'<div class="toc">'
f' <p><strong>Table of Contents</strong></p>'
···
f' {"".join(items)}'
f' </dl>'
f'</div>'
)
def _make_hN(self, level: int) -> tuple[str, str]:
···
self._redirection_targets.add(into)
return tokens
# xref | (id, type, heading inlines, file, starts new file)
def _collect_ids(self, tokens: Sequence[Token], target_file: str, typ: str, file_changed: bool
) -> list[XrefTarget | tuple[str, str, Token, str, bool]]:
···
subtyp = bt.type.removeprefix('included_').removesuffix('s')
for si, (sub, _path) in enumerate(bt.meta['included']):
result += self._collect_ids(sub, sub_file, subtyp, si == 0 and sub_file != target_file)
elif bt.type == 'inline':
assert bt.children
result += self._collect_ids(bt.children, target_file, typ, False)
···
title = prefix + title_html
toc_html = f"{n}. {title_html}"
title_html = f"Appendix&nbsp;{n}"
else:
toc_html, title = title_html, title_html
title_html = (
···
return XrefTarget(id, title_html, toc_html, re.sub('<.*?>', '', title), path, drop_fragment)
def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) -> None:
xref_queue = self._collect_ids(tokens, outfile.name, 'book', True)
failed = False
···
)
if not (items := walk_and_emit(toc, toc_depth)):
return ""
+
examples = ""
+
if toc.examples:
+
examples_entries = [
+
f'<dt>{i + 1}. <a href="{ex.target.href()}">{ex.target.toc_html}</a></dt>'
+
for i, ex in enumerate(toc.examples)
+
]
+
examples = (
+
'<div class="list-of-examples">'
+
'<p><strong>List of Examples</strong><p>'
+
f'<dl>{"".join(examples_entries)}</dl>'
+
'</div>'
+
)
return (
f'<div class="toc">'
f' <p><strong>Table of Contents</strong></p>'
···
f' {"".join(items)}'
f' </dl>'
f'</div>'
+
f'{examples}'
)
def _make_hN(self, level: int) -> tuple[str, str]:
···
self._redirection_targets.add(into)
return tokens
+
def _number_examples(self, tokens: Sequence[Token], start: int = 1) -> int:
+
for (i, token) in enumerate(tokens):
+
if token.type == "example_title_open":
+
title = tokens[i + 1]
+
assert title.type == 'inline' and title.children
+
# the prefix is split into two tokens because the xref title_html will want
+
# only the first of the two, but both must be rendered into the example itself.
+
title.children = (
+
[
+
Token('text', '', 0, content=f'Example {start}'),
+
Token('text', '', 0, content='. ')
+
] + title.children
+
)
+
start += 1
+
elif token.type.startswith('included_') and token.type != 'included_options':
+
for sub, _path in token.meta['included']:
+
start = self._number_examples(sub, start)
+
return start
+
# xref | (id, type, heading inlines, file, starts new file)
def _collect_ids(self, tokens: Sequence[Token], target_file: str, typ: str, file_changed: bool
) -> list[XrefTarget | tuple[str, str, Token, str, bool]]:
···
subtyp = bt.type.removeprefix('included_').removesuffix('s')
for si, (sub, _path) in enumerate(bt.meta['included']):
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 == 'inline':
assert bt.children
result += self._collect_ids(bt.children, target_file, typ, False)
···
title = prefix + title_html
toc_html = f"{n}. {title_html}"
title_html = f"Appendix&nbsp;{n}"
+
elif typ == 'example':
+
# skip the prepended `Example N. ` from _number_examples
+
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])
else:
toc_html, title = title_html, title_html
title_html = (
···
return XrefTarget(id, title_html, toc_html, re.sub('<.*?>', '', title), path, drop_fragment)
def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) -> None:
+
self._number_examples(tokens)
xref_queue = self._collect_ids(tokens, outfile.name, 'book', True)
failed = False
+15 -7
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual_structure.py
···
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 is_include(token: Token) -> bool:
return token.type == "fence" and token.info.startswith("{=include=} ")
···
next: TocEntry | None = None
children: list[TocEntry] = dc.field(default_factory=list)
starts_new_chunk: bool = False
@property
def root(self) -> TocEntry:
···
@classmethod
def collect_and_link(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token]) -> TocEntry:
-
result = cls._collect_entries(xrefs, tokens, 'book')
def flatten_with_parent(this: TocEntry, parent: TocEntry | None) -> Iterable[TocEntry]:
this.parent = parent
return itertools.chain([this], *[ flatten_with_parent(c, this) for c in this.children ])
-
flat = list(flatten_with_parent(result, None))
prev = flat[0]
prev.starts_new_chunk = True
paths_seen = set([prev.target.path])
···
prev = c
paths_seen.add(c.target.path)
for c in flat:
c.freeze()
-
return result
@classmethod
def _collect_entries(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token],
-
kind: TocEntryType) -> 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]] = []
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:
-
entries[-1][1].children.append(cls._collect_entries(xrefs, fragment, fragment_type))
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])
entries.append((token.tag,
TocEntry(kind if token.tag == 'h1' else 'section', xrefs[id])))
token.meta['TocEntry'] = entries[-1][1]
while len(entries) > 1:
entries[-2][1].children.append(entries.pop()[1])
-
return entries[0][1]
···
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']
def is_include(token: Token) -> bool:
return token.type == "fence" and token.info.startswith("{=include=} ")
···
next: TocEntry | None = None
children: list[TocEntry] = dc.field(default_factory=list)
starts_new_chunk: bool = False
+
examples: list[TocEntry] = dc.field(default_factory=list)
@property
def root(self) -> TocEntry:
···
@classmethod
def collect_and_link(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token]) -> TocEntry:
+
entries, examples = cls._collect_entries(xrefs, tokens, 'book')
def flatten_with_parent(this: TocEntry, parent: TocEntry | None) -> Iterable[TocEntry]:
this.parent = parent
return itertools.chain([this], *[ flatten_with_parent(c, this) for c in this.children ])
+
flat = list(flatten_with_parent(entries, None))
prev = flat[0]
prev.starts_new_chunk = True
paths_seen = set([prev.target.path])
···
prev = c
paths_seen.add(c.target.path)
+
flat[0].examples = examples
+
for c in flat:
c.freeze()
+
return entries
@classmethod
def _collect_entries(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token],
+
kind: TocEntryType) -> tuple[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] = []
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)
+
entries[-1][1].children.append(subentries)
+
examples += subexamples
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])
entries.append((token.tag,
TocEntry(kind if token.tag == 'h1' else 'section', xrefs[id])))
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]))
while len(entries) > 1:
entries[-2][1].children.append(entries.pop()[1])
+
return (entries[0][1], examples)
+33
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py
···
"ordered_list_close": self.ordered_list_close,
"example_open": self.example_open,
"example_close": self.example_close,
}
self._admonitions = {
···
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
def example_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:
···
md.core.ruler.push("block_attr", block_attr)
TR = TypeVar('TR', bound='Renderer')
class Converter(ABC, Generic[TR]):
···
self._md.use(_heading_ids)
self._md.use(_compact_list_attr)
self._md.use(_block_attr)
self._md.enable(["smartquotes", "replacements"])
def _parse(self, src: str) -> list[Token]:
···
"ordered_list_close": self.ordered_list_close,
"example_open": self.example_open,
"example_close": self.example_close,
+
"example_title_open": self.example_title_open,
+
"example_title_close": self.example_title_close,
}
self._admonitions = {
···
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
+
raise RuntimeError("md token not supported", token)
+
def example_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
+
raise RuntimeError("md token not supported", token)
+
def example_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:
···
md.core.ruler.push("block_attr", block_attr)
+
def _example_titles(md: markdown_it.MarkdownIt) -> None:
+
"""
+
find title headings of examples and stick them into meta for renderers, then
+
remove them from the token stream. also checks whether any example contains a
+
non-title heading since those would make toc generation extremely complicated.
+
"""
+
def example_titles(state: markdown_it.rules_core.StateCore) -> None:
+
in_example = [False]
+
for i, token in enumerate(state.tokens):
+
if token.type == 'example_open':
+
if state.tokens[i + 1].type == 'heading_open':
+
assert state.tokens[i + 3].type == 'heading_close'
+
state.tokens[i + 1].type = 'example_title_open'
+
state.tokens[i + 3].type = 'example_title_close'
+
else:
+
assert token.map
+
raise RuntimeError(f"found example without title in line {token.map[0] + 1}")
+
in_example.append(True)
+
elif token.type == 'example_close':
+
in_example.pop()
+
elif token.type == 'heading_open' and in_example[-1]:
+
assert token.map
+
raise RuntimeError(f"unexpected non-title heading in example in line {token.map[0] + 1}")
+
+
md.core.ruler.push("example_titles", example_titles)
+
TR = TypeVar('TR', bound='Renderer')
class Converter(ABC, Generic[TR]):
···
self._md.use(_heading_ids)
self._md.use(_compact_list_attr)
self._md.use(_block_attr)
+
self._md.use(_example_titles)
self._md.enable(["smartquotes", "replacements"])
def _parse(self, src: str) -> list[Token]:
+55 -6
pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py
···
import nixos_render_docs as nrd
from markdown_it.token import Token
···
def test_example() -> None:
c = Converter({})
-
assert c._parse("::: {.example}") == [
-
Token(type='example_open', tag='div', nesting=1, attrs={}, map=[0, 1], level=0, children=None,
content='', markup=':::', info=' {.example}', meta={}, block=True, hidden=False),
Token(type='example_close', tag='div', nesting=-1, attrs={}, map=None, level=0, children=None,
-
content='', markup=':::', info='', meta={}, block=True, hidden=False)
]
-
assert c._parse("::: {#eid .example}") == [
-
Token(type='example_open', tag='div', nesting=1, attrs={'id': 'eid'}, map=[0, 1], level=0,
children=None, content='', markup=':::', info=' {#eid .example}', meta={}, block=True,
hidden=False),
Token(type='example_close', tag='div', nesting=-1, attrs={}, map=None, level=0, children=None,
-
content='', markup=':::', info='', meta={}, block=True, hidden=False)
]
assert c._parse("::: {.example .note}") == [
Token(type='paragraph_open', tag='p', nesting=1, attrs={}, map=[0, 1], level=0, children=None,
···
Token(type='paragraph_close', tag='p', nesting=-1, attrs={}, map=None, level=0, children=None,
content='', markup='', info='', meta={}, block=True, hidden=False)
]
···
import nixos_render_docs as nrd
+
import pytest
from markdown_it.token import Token
···
def test_example() -> None:
c = Converter({})
+
assert c._parse("::: {.example}\n# foo") == [
+
Token(type='example_open', tag='div', nesting=1, attrs={}, map=[0, 2], level=0, children=None,
content='', markup=':::', info=' {.example}', meta={}, block=True, hidden=False),
+
Token(type='example_title_open', tag='h1', nesting=1, attrs={}, map=[1, 2], level=1, children=None,
+
content='', markup='#', info='', meta={}, block=True, hidden=False),
+
Token(type='inline', tag='', nesting=0, attrs={}, map=[1, 2], level=2,
+
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='example_title_close', tag='h1', nesting=-1, attrs={}, map=None, level=1, children=None,
+
content='', markup='#', info='', meta={}, block=True, hidden=False),
Token(type='example_close', tag='div', nesting=-1, attrs={}, map=None, level=0, children=None,
+
content='', markup='', info='', meta={}, block=True, hidden=False)
]
+
assert c._parse("::: {#eid .example}\n# foo") == [
+
Token(type='example_open', tag='div', nesting=1, attrs={'id': 'eid'}, map=[0, 2], level=0,
children=None, content='', markup=':::', info=' {#eid .example}', meta={}, block=True,
hidden=False),
+
Token(type='example_title_open', tag='h1', nesting=1, attrs={}, map=[1, 2], level=1, children=None,
+
content='', markup='#', info='', meta={}, block=True, hidden=False),
+
Token(type='inline', tag='', nesting=0, attrs={}, map=[1, 2], level=2,
+
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='example_title_close', tag='h1', nesting=-1, attrs={}, map=None, level=1, children=None,
+
content='', markup='#', info='', meta={}, block=True, hidden=False),
Token(type='example_close', tag='div', nesting=-1, attrs={}, map=None, level=0, children=None,
+
content='', markup='', info='', meta={}, block=True, hidden=False)
]
assert c._parse("::: {.example .note}") == [
Token(type='paragraph_open', tag='p', nesting=1, attrs={}, map=[0, 1], level=0, children=None,
···
Token(type='paragraph_close', tag='p', nesting=-1, attrs={}, map=None, level=0, children=None,
content='', markup='', info='', meta={}, block=True, hidden=False)
]
+
assert c._parse("::: {.example}\n### foo: `code`\nbar\n:::\nbaz") == [
+
Token(type='example_open', tag='div', nesting=1, map=[0, 3], markup=':::', info=' {.example}',
+
block=True),
+
Token(type='example_title_open', tag='h3', nesting=1, map=[1, 2], level=1, markup='###', block=True),
+
Token(type='inline', tag='', nesting=0, map=[1, 2], level=2, content='foo: `code`', block=True,
+
children=[
+
Token(type='text', tag='', nesting=0, content='foo: '),
+
Token(type='code_inline', tag='code', nesting=0, content='code', markup='`')
+
]),
+
Token(type='example_title_close', tag='h3', nesting=-1, level=1, markup='###', block=True),
+
Token(type='paragraph_open', tag='p', nesting=1, map=[2, 3], level=1, block=True),
+
Token(type='inline', tag='', nesting=0, map=[2, 3], level=2, content='bar', block=True,
+
children=[
+
Token(type='text', tag='', nesting=0, content='bar')
+
]),
+
Token(type='paragraph_close', tag='p', nesting=-1, level=1, block=True),
+
Token(type='example_close', tag='div', nesting=-1, markup=':::', block=True),
+
Token(type='paragraph_open', tag='p', nesting=1, map=[4, 5], block=True),
+
Token(type='inline', tag='', nesting=0, map=[4, 5], level=1, content='baz', block=True,
+
children=[
+
Token(type='text', tag='', nesting=0, content='baz')
+
]),
+
Token(type='paragraph_close', tag='p', nesting=-1, block=True)
+
]
+
+
with pytest.raises(RuntimeError) as exc:
+
c._parse("::: {.example}\n### foo\n### bar\n:::")
+
assert exc.value.args[0] == 'unexpected non-title heading in example in line 3'