diff --git a/nixos/doc/manual/development/freeform-modules.section.md b/nixos/doc/manual/development/freeform-modules.section.md index 514a06f97ee7..4f344dd80460 100644 --- a/nixos/doc/manual/development/freeform-modules.section.md +++ b/nixos/doc/manual/development/freeform-modules.section.md @@ -13,7 +13,7 @@ checking for entire option trees, it is only recommended for use in submodules. ::: {#ex-freeform-module .example} -**Example: Freeform submodule** +### Freeform submodule The following shows a submodule assigning a freeform type that allows arbitrary attributes with `str` values below `settings`, but also diff --git a/nixos/doc/manual/development/option-declarations.section.md b/nixos/doc/manual/development/option-declarations.section.md index f6fed3e16837..3448b07722b8 100644 --- a/nixos/doc/manual/development/option-declarations.section.md +++ b/nixos/doc/manual/development/option-declarations.section.md @@ -77,6 +77,7 @@ The option's description is "Whether to enable \.". For example: ::: {#ex-options-declarations-util-mkEnableOption-magic .example} +### `mkEnableOption` usage ```nix lib.mkEnableOption (lib.mdDoc "magic") # is like @@ -126,6 +127,7 @@ During the transition to CommonMark documentation `mkPackageOption` creates an o Examples: ::: {#ex-options-declarations-util-mkPackageOption-hello .example} +### Simple `mkPackageOption` usage ```nix lib.mkPackageOptionMD pkgs "hello" { } # is like @@ -139,6 +141,7 @@ lib.mkOption { ::: ::: {#ex-options-declarations-util-mkPackageOption-ghc .example} +### `mkPackageOption` with explicit default and example ```nix lib.mkPackageOptionMD pkgs "GHC" { default = [ "ghc" ]; @@ -156,6 +159,7 @@ lib.mkOption { ::: ::: {#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."; @@ -217,7 +221,7 @@ changing the main service module file and the type system automatically 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** +### Extensible type placeholder in the service module ```nix services.xserver.displayManager.enable = mkOption { description = "Display manager to use"; @@ -227,7 +231,7 @@ services.xserver.displayManager.enable = mkOption { ::: ::: {#ex-option-declaration-eot-backend-gdm .example} -**Example: Extending `services.xserver.displayManager.enable` in the `gdm` module** +### Extending `services.xserver.displayManager.enable` in the `gdm` module ```nix services.xserver.displayManager.enable = mkOption { type = with types; nullOr (enum [ "gdm" ]); @@ -236,7 +240,7 @@ services.xserver.displayManager.enable = mkOption { ::: ::: {#ex-option-declaration-eot-backend-sddm .example} -**Example: Extending `services.xserver.displayManager.enable` in the `sddm` module** +### Extending `services.xserver.displayManager.enable` in the `sddm` module ```nix services.xserver.displayManager.enable = mkOption { type = with types; nullOr (enum [ "sddm" ]); diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md index 51977c58333f..9e2ecb8e3562 100644 --- a/nixos/doc/manual/development/option-types.section.md +++ b/nixos/doc/manual/development/option-types.section.md @@ -36,7 +36,7 @@ merging is handled. together. This type is recommended when the option type is unknown. ::: {#ex-types-anything .example} - **Example: `types.anything` Example** + ### `types.anything` Two definitions of this type like @@ -356,7 +356,7 @@ you will still need to provide a default value (e.g. an empty attribute set) if you want to allow users to leave it undefined. ::: {#ex-submodule-direct .example} -**Example: Directly defined submodule** +### Directly defined submodule ```nix options.mod = mkOption { description = "submodule example"; @@ -375,7 +375,7 @@ options.mod = mkOption { ::: ::: {#ex-submodule-reference .example} -**Example: Submodule defined as a reference** +### Submodule defined as a reference ```nix let modOptions = { @@ -403,7 +403,7 @@ multiple definitions of the submodule option set ([Example: Definition of a list of submodules](#ex-submodule-listof-definition)). ::: {#ex-submodule-listof-declaration .example} -**Example: Declaration of a list of submodules** +### Declaration of a list of submodules ```nix options.mod = mkOption { description = "submodule example"; @@ -422,7 +422,7 @@ options.mod = mkOption { ::: ::: {#ex-submodule-listof-definition .example} -**Example: Definition of a list of submodules** +### Definition of a list of submodules ```nix config.mod = [ { foo = 1; bar = "one"; } @@ -437,7 +437,7 @@ multiple named definitions of the submodule option set ([Example: Definition of attribute sets of submodules](#ex-submodule-attrsof-definition)). ::: {#ex-submodule-attrsof-declaration .example} -**Example: Declaration of attribute sets of submodules** +### Declaration of attribute sets of submodules ```nix options.mod = mkOption { description = "submodule example"; @@ -456,7 +456,7 @@ options.mod = mkOption { ::: ::: {#ex-submodule-attrsof-definition .example} -**Example: Definition of attribute sets of submodules** +### Definition of attribute sets of submodules ```nix config.mod.one = { foo = 1; bar = "one"; }; config.mod.two = { foo = 2; bar = "two"; }; @@ -476,7 +476,7 @@ Types are mainly characterized by their `check` and `merge` functions. ([Example: Overriding a type check](#ex-extending-type-check-2)). ::: {#ex-extending-type-check-1 .example} - **Example: Adding a type check** + ### Adding a type check ```nix byte = mkOption { @@ -487,7 +487,7 @@ Types are mainly characterized by their `check` and `merge` functions. ::: ::: {#ex-extending-type-check-2 .example} - **Example: Overriding a type check** + ### Overriding a type check ```nix nixThings = mkOption { diff --git a/nixos/doc/manual/development/settings-options.section.md b/nixos/doc/manual/development/settings-options.section.md index 476ba4b03f9d..5060dd98f58f 100644 --- a/nixos/doc/manual/development/settings-options.section.md +++ b/nixos/doc/manual/development/settings-options.section.md @@ -143,7 +143,7 @@ These functions all return an attribute set with these values: ::: ::: {#ex-settings-nix-representable .example} -**Example: Module with conventional `settings` option** +### 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 @@ -218,7 +218,7 @@ the port, which will enforce it to be a valid integer and make it show up in the manual. ::: {#ex-settings-typed-attrs .example} -**Example: Declaring a type-checked `settings` attribute** +### Declaring a type-checked `settings` attribute ```nix settings = lib.mkOption { type = lib.types.submodule { diff --git a/nixos/doc/manual/development/writing-modules.chapter.md b/nixos/doc/manual/development/writing-modules.chapter.md index ae657458d768..e07b899e6df7 100644 --- a/nixos/doc/manual/development/writing-modules.chapter.md +++ b/nixos/doc/manual/development/writing-modules.chapter.md @@ -37,7 +37,7 @@ options, but does not declare any. The structure of full NixOS modules is shown in [Example: Structure of NixOS Modules](#ex-module-syntax). ::: {#ex-module-syntax .example} -**Example: Structure of NixOS Modules** +### Structure of NixOS Modules ```nix { config, pkgs, ... }: @@ -100,7 +100,7 @@ Exec directives](#exec-escaping-example) for an example. When using these functions system environment substitution should *not* be disabled explicitly. ::: {#locate-example .example} -**Example: NixOS Module for the "locate" Service** +### NixOS Module for the "locate" Service ```nix { config, lib, pkgs, ... }: @@ -161,7 +161,7 @@ in { ::: ::: {#exec-escaping-example .example} -**Example: Escaping in Exec directives** +### Escaping in Exec directives ```nix { config, lib, pkgs, utils, ... }: diff --git a/nixos/doc/manual/installation/installing.chapter.md b/nixos/doc/manual/installation/installing.chapter.md index 7d67894e59f9..53cf9ed14c33 100644 --- a/nixos/doc/manual/installation/installing.chapter.md +++ b/nixos/doc/manual/installation/installing.chapter.md @@ -538,7 +538,7 @@ drive (here `/dev/sda`). [Example: NixOS Configuration](#ex-config) shows a corresponding configuration Nix expression. ::: {#ex-partition-scheme-MBR .example} -**Example: Example partition schemes for NixOS on `/dev/sda` (MBR)** +### Example partition schemes for NixOS on `/dev/sda` (MBR) ```ShellSession # parted /dev/sda -- mklabel msdos # parted /dev/sda -- mkpart primary 1MB -8GB @@ -547,7 +547,7 @@ corresponding configuration Nix expression. ::: ::: {#ex-partition-scheme-UEFI .example} -**Example: Example partition schemes for NixOS on `/dev/sda` (UEFI)** +### Example partition schemes for NixOS on `/dev/sda` (UEFI) ```ShellSession # parted /dev/sda -- mklabel gpt # parted /dev/sda -- mkpart primary 512MB -8GB @@ -558,7 +558,7 @@ corresponding configuration Nix expression. ::: ::: {#ex-install-sequence .example} -**Example: Commands for Installing NixOS on `/dev/sda`** +### Commands for Installing NixOS on `/dev/sda` With a partitioned disk. @@ -578,7 +578,7 @@ With a partitioned disk. ::: ::: {#ex-config .example} -**Example: NixOS Configuration** +### Example: NixOS Configuration ```ShellSession { config, pkgs, ... }: { imports = [ diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/docbook.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/docbook.py index 4c90606ff455..1c1e95a29ef2 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/docbook.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/docbook.py @@ -218,11 +218,15 @@ class DocBookRenderer(Renderer): result += f"" return result def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: - if id := token.attrs.get('id'): - return f"" - return "" + if id := cast(str, token.attrs.get('id', '')): + id = f'xml:id={quoteattr(id)}' if id else '' + return f'' def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: - return "" + return "" + def example_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "" + def example_title_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 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 39d2da6adf8c..ed9cd5485546 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 @@ -214,11 +214,15 @@ class HTMLRenderer(Renderer): self._ordered_list_nesting -= 1; return "" def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: - if id := token.attrs.get('id'): - return f'' - return "" + if id := cast(str, token.attrs.get('id', '')): + id = f'id="{escape(id, True)}"' if id else '' + return f'
' + def example_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return '

' + def example_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 40dea3c7d1d8..1963989d5365 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 @@ -402,6 +402,18 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): ) 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)}
' + '
' + ) return ( f'
' f'

Table of Contents

' @@ -409,6 +421,7 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): f' {"".join(items)}' f' ' f'
' + f'{examples}' ) def _make_hN(self, level: int) -> tuple[str, str]: @@ -513,6 +526,25 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): 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]]: @@ -534,6 +566,8 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): 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) @@ -558,6 +592,11 @@ 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 _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 = ( @@ -569,6 +608,7 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): 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 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 c271ca3c5aa5..95e6e9474e73 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'] +TocEntryType = Literal['book', 'preface', 'part', 'chapter', 'section', 'appendix', 'example'] def is_include(token: Token) -> bool: return token.type == "fence" and token.info.startswith("{=include=} ") @@ -124,6 +124,7 @@ class TocEntry(Freezeable): 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: @@ -138,13 +139,13 @@ class TocEntry(Freezeable): @classmethod def collect_and_link(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token]) -> TocEntry: - result = cls._collect_entries(xrefs, tokens, 'book') + 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(result, None)) + flat = list(flatten_with_parent(entries, None)) prev = flat[0] prev.starts_new_chunk = True paths_seen = set([prev.target.path]) @@ -155,32 +156,39 @@ class TocEntry(Freezeable): prev = c paths_seen.add(c.target.path) + flat[0].examples = examples + for c in flat: c.freeze() - return result + return entries @classmethod def _collect_entries(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token], - kind: TocEntryType) -> TocEntry: + 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: - entries[-1][1].children.append(cls._collect_entries(xrefs, fragment, fragment_type)) + 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] + return (entries[0][1], examples) 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 e8fee1b71328..ce79b0dee794 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 @@ -88,6 +88,8 @@ class Renderer: "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 = { @@ -219,6 +221,10 @@ class Renderer: 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: found = 0 @@ -417,6 +423,32 @@ def _block_attr(md: markdown_it.MarkdownIt) -> None: 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]): @@ -459,6 +491,7 @@ 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]: diff --git a/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py b/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py index f94ede6382bf..fb7a4ab0117f 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py +++ b/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py @@ -1,4 +1,5 @@ import nixos_render_docs as nrd +import pytest from markdown_it.token import Token @@ -427,18 +428,38 @@ def test_admonitions() -> None: 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, + 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) + 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, + 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) + 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, @@ -452,3 +473,31 @@ def test_example() -> 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'