mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-06-13 05:05:29 +03:00
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.
This commit is contained in:
parent
69259eec23
commit
407f6196a2
12 changed files with 185 additions and 43 deletions
|
@ -13,7 +13,7 @@ checking for entire option trees, it is only recommended for use in
|
||||||
submodules.
|
submodules.
|
||||||
|
|
||||||
::: {#ex-freeform-module .example}
|
::: {#ex-freeform-module .example}
|
||||||
**Example: Freeform submodule**
|
### Freeform submodule
|
||||||
|
|
||||||
The following shows a submodule assigning a freeform type that allows
|
The following shows a submodule assigning a freeform type that allows
|
||||||
arbitrary attributes with `str` values below `settings`, but also
|
arbitrary attributes with `str` values below `settings`, but also
|
||||||
|
|
|
@ -77,6 +77,7 @@ The option's description is "Whether to enable \<name\>.".
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
::: {#ex-options-declarations-util-mkEnableOption-magic .example}
|
::: {#ex-options-declarations-util-mkEnableOption-magic .example}
|
||||||
|
### `mkEnableOption` usage
|
||||||
```nix
|
```nix
|
||||||
lib.mkEnableOption (lib.mdDoc "magic")
|
lib.mkEnableOption (lib.mdDoc "magic")
|
||||||
# is like
|
# is like
|
||||||
|
@ -126,6 +127,7 @@ During the transition to CommonMark documentation `mkPackageOption` creates an o
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
::: {#ex-options-declarations-util-mkPackageOption-hello .example}
|
::: {#ex-options-declarations-util-mkPackageOption-hello .example}
|
||||||
|
### Simple `mkPackageOption` usage
|
||||||
```nix
|
```nix
|
||||||
lib.mkPackageOptionMD pkgs "hello" { }
|
lib.mkPackageOptionMD pkgs "hello" { }
|
||||||
# is like
|
# is like
|
||||||
|
@ -139,6 +141,7 @@ lib.mkOption {
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-options-declarations-util-mkPackageOption-ghc .example}
|
::: {#ex-options-declarations-util-mkPackageOption-ghc .example}
|
||||||
|
### `mkPackageOption` with explicit default and example
|
||||||
```nix
|
```nix
|
||||||
lib.mkPackageOptionMD pkgs "GHC" {
|
lib.mkPackageOptionMD pkgs "GHC" {
|
||||||
default = [ "ghc" ];
|
default = [ "ghc" ];
|
||||||
|
@ -156,6 +159,7 @@ lib.mkOption {
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-options-declarations-util-mkPackageOption-extraDescription .example}
|
::: {#ex-options-declarations-util-mkPackageOption-extraDescription .example}
|
||||||
|
### `mkPackageOption` with additional description text
|
||||||
```nix
|
```nix
|
||||||
mkPackageOption pkgs [ "python39Packages" "pytorch" ] {
|
mkPackageOption pkgs [ "python39Packages" "pytorch" ] {
|
||||||
extraDescription = "This is an example and doesn't actually do anything.";
|
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.
|
enforces that there can only be a single display manager enabled.
|
||||||
|
|
||||||
::: {#ex-option-declaration-eot-service .example}
|
::: {#ex-option-declaration-eot-service .example}
|
||||||
**Example: Extensible type placeholder in the service module**
|
### Extensible type placeholder in the service module
|
||||||
```nix
|
```nix
|
||||||
services.xserver.displayManager.enable = mkOption {
|
services.xserver.displayManager.enable = mkOption {
|
||||||
description = "Display manager to use";
|
description = "Display manager to use";
|
||||||
|
@ -227,7 +231,7 @@ services.xserver.displayManager.enable = mkOption {
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-option-declaration-eot-backend-gdm .example}
|
::: {#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
|
```nix
|
||||||
services.xserver.displayManager.enable = mkOption {
|
services.xserver.displayManager.enable = mkOption {
|
||||||
type = with types; nullOr (enum [ "gdm" ]);
|
type = with types; nullOr (enum [ "gdm" ]);
|
||||||
|
@ -236,7 +240,7 @@ services.xserver.displayManager.enable = mkOption {
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-option-declaration-eot-backend-sddm .example}
|
::: {#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
|
```nix
|
||||||
services.xserver.displayManager.enable = mkOption {
|
services.xserver.displayManager.enable = mkOption {
|
||||||
type = with types; nullOr (enum [ "sddm" ]);
|
type = with types; nullOr (enum [ "sddm" ]);
|
||||||
|
|
|
@ -36,7 +36,7 @@ merging is handled.
|
||||||
together. This type is recommended when the option type is unknown.
|
together. This type is recommended when the option type is unknown.
|
||||||
|
|
||||||
::: {#ex-types-anything .example}
|
::: {#ex-types-anything .example}
|
||||||
**Example: `types.anything` Example**
|
### `types.anything`
|
||||||
|
|
||||||
Two definitions of this type like
|
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.
|
if you want to allow users to leave it undefined.
|
||||||
|
|
||||||
::: {#ex-submodule-direct .example}
|
::: {#ex-submodule-direct .example}
|
||||||
**Example: Directly defined submodule**
|
### Directly defined submodule
|
||||||
```nix
|
```nix
|
||||||
options.mod = mkOption {
|
options.mod = mkOption {
|
||||||
description = "submodule example";
|
description = "submodule example";
|
||||||
|
@ -375,7 +375,7 @@ options.mod = mkOption {
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-submodule-reference .example}
|
::: {#ex-submodule-reference .example}
|
||||||
**Example: Submodule defined as a reference**
|
### Submodule defined as a reference
|
||||||
```nix
|
```nix
|
||||||
let
|
let
|
||||||
modOptions = {
|
modOptions = {
|
||||||
|
@ -403,7 +403,7 @@ multiple definitions of the submodule option set
|
||||||
([Example: Definition of a list of submodules](#ex-submodule-listof-definition)).
|
([Example: Definition of a list of submodules](#ex-submodule-listof-definition)).
|
||||||
|
|
||||||
::: {#ex-submodule-listof-declaration .example}
|
::: {#ex-submodule-listof-declaration .example}
|
||||||
**Example: Declaration of a list of submodules**
|
### Declaration of a list of submodules
|
||||||
```nix
|
```nix
|
||||||
options.mod = mkOption {
|
options.mod = mkOption {
|
||||||
description = "submodule example";
|
description = "submodule example";
|
||||||
|
@ -422,7 +422,7 @@ options.mod = mkOption {
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-submodule-listof-definition .example}
|
::: {#ex-submodule-listof-definition .example}
|
||||||
**Example: Definition of a list of submodules**
|
### Definition of a list of submodules
|
||||||
```nix
|
```nix
|
||||||
config.mod = [
|
config.mod = [
|
||||||
{ foo = 1; bar = "one"; }
|
{ 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)).
|
([Example: Definition of attribute sets of submodules](#ex-submodule-attrsof-definition)).
|
||||||
|
|
||||||
::: {#ex-submodule-attrsof-declaration .example}
|
::: {#ex-submodule-attrsof-declaration .example}
|
||||||
**Example: Declaration of attribute sets of submodules**
|
### Declaration of attribute sets of submodules
|
||||||
```nix
|
```nix
|
||||||
options.mod = mkOption {
|
options.mod = mkOption {
|
||||||
description = "submodule example";
|
description = "submodule example";
|
||||||
|
@ -456,7 +456,7 @@ options.mod = mkOption {
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-submodule-attrsof-definition .example}
|
::: {#ex-submodule-attrsof-definition .example}
|
||||||
**Example: Definition of attribute sets of submodules**
|
### Definition of attribute sets of submodules
|
||||||
```nix
|
```nix
|
||||||
config.mod.one = { foo = 1; bar = "one"; };
|
config.mod.one = { foo = 1; bar = "one"; };
|
||||||
config.mod.two = { foo = 2; bar = "two"; };
|
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)).
|
([Example: Overriding a type check](#ex-extending-type-check-2)).
|
||||||
|
|
||||||
::: {#ex-extending-type-check-1 .example}
|
::: {#ex-extending-type-check-1 .example}
|
||||||
**Example: Adding a type check**
|
### Adding a type check
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
byte = mkOption {
|
byte = mkOption {
|
||||||
|
@ -487,7 +487,7 @@ Types are mainly characterized by their `check` and `merge` functions.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-extending-type-check-2 .example}
|
::: {#ex-extending-type-check-2 .example}
|
||||||
**Example: Overriding a type check**
|
### Overriding a type check
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
nixThings = mkOption {
|
nixThings = mkOption {
|
||||||
|
|
|
@ -143,7 +143,7 @@ These functions all return an attribute set with these values:
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-settings-nix-representable .example}
|
::: {#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
|
The following shows a module for an example program that uses a JSON
|
||||||
configuration file. It demonstrates how above values can be used, along
|
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.
|
up in the manual.
|
||||||
|
|
||||||
::: {#ex-settings-typed-attrs .example}
|
::: {#ex-settings-typed-attrs .example}
|
||||||
**Example: Declaring a type-checked `settings` attribute**
|
### Declaring a type-checked `settings` attribute
|
||||||
```nix
|
```nix
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
|
|
|
@ -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).
|
is shown in [Example: Structure of NixOS Modules](#ex-module-syntax).
|
||||||
|
|
||||||
::: {#ex-module-syntax .example}
|
::: {#ex-module-syntax .example}
|
||||||
**Example: Structure of NixOS Modules**
|
### Structure of NixOS Modules
|
||||||
```nix
|
```nix
|
||||||
{ config, pkgs, ... }:
|
{ 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.
|
functions system environment substitution should *not* be disabled explicitly.
|
||||||
|
|
||||||
::: {#locate-example .example}
|
::: {#locate-example .example}
|
||||||
**Example: NixOS Module for the "locate" Service**
|
### NixOS Module for the "locate" Service
|
||||||
```nix
|
```nix
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
@ -161,7 +161,7 @@ in {
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#exec-escaping-example .example}
|
::: {#exec-escaping-example .example}
|
||||||
**Example: Escaping in Exec directives**
|
### Escaping in Exec directives
|
||||||
```nix
|
```nix
|
||||||
{ config, lib, pkgs, utils, ... }:
|
{ config, lib, pkgs, utils, ... }:
|
||||||
|
|
||||||
|
|
|
@ -538,7 +538,7 @@ drive (here `/dev/sda`). [Example: NixOS Configuration](#ex-config) shows a
|
||||||
corresponding configuration Nix expression.
|
corresponding configuration Nix expression.
|
||||||
|
|
||||||
::: {#ex-partition-scheme-MBR .example}
|
::: {#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
|
```ShellSession
|
||||||
# parted /dev/sda -- mklabel msdos
|
# parted /dev/sda -- mklabel msdos
|
||||||
# parted /dev/sda -- mkpart primary 1MB -8GB
|
# parted /dev/sda -- mkpart primary 1MB -8GB
|
||||||
|
@ -547,7 +547,7 @@ corresponding configuration Nix expression.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-partition-scheme-UEFI .example}
|
::: {#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
|
```ShellSession
|
||||||
# parted /dev/sda -- mklabel gpt
|
# parted /dev/sda -- mklabel gpt
|
||||||
# parted /dev/sda -- mkpart primary 512MB -8GB
|
# parted /dev/sda -- mkpart primary 512MB -8GB
|
||||||
|
@ -558,7 +558,7 @@ corresponding configuration Nix expression.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-install-sequence .example}
|
::: {#ex-install-sequence .example}
|
||||||
**Example: Commands for Installing NixOS on `/dev/sda`**
|
### Commands for Installing NixOS on `/dev/sda`
|
||||||
|
|
||||||
With a partitioned disk.
|
With a partitioned disk.
|
||||||
|
|
||||||
|
@ -578,7 +578,7 @@ With a partitioned disk.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {#ex-config .example}
|
::: {#ex-config .example}
|
||||||
**Example: NixOS Configuration**
|
### Example: NixOS Configuration
|
||||||
```ShellSession
|
```ShellSession
|
||||||
{ config, pkgs, ... }: {
|
{ config, pkgs, ... }: {
|
||||||
imports = [
|
imports = [
|
||||||
|
|
|
@ -218,11 +218,15 @@ class DocBookRenderer(Renderer):
|
||||||
result += f"<partintro{maybe_id}>"
|
result += f"<partintro{maybe_id}>"
|
||||||
return result
|
return result
|
||||||
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||||
if id := token.attrs.get('id'):
|
if id := cast(str, token.attrs.get('id', '')):
|
||||||
return f"<anchor xml:id={quoteattr(cast(str, id))} />"
|
id = f'xml:id={quoteattr(id)}' if id else ''
|
||||||
return ""
|
return f'<example {id}>'
|
||||||
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||||
return ""
|
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:
|
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
|
# we rely on markdown-it producing h{1..6} tags in token.tag for this to work
|
||||||
|
|
|
@ -214,11 +214,15 @@ class HTMLRenderer(Renderer):
|
||||||
self._ordered_list_nesting -= 1;
|
self._ordered_list_nesting -= 1;
|
||||||
return "</ol></div>"
|
return "</ol></div>"
|
||||||
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||||
if id := token.attrs.get('id'):
|
if id := cast(str, token.attrs.get('id', '')):
|
||||||
return f'<a id="{escape(cast(str, id), True)}" />'
|
id = f'id="{escape(id, True)}"' if id else ''
|
||||||
return ""
|
return f'<div class="example"><a {id} />'
|
||||||
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||||
return ""
|
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]:
|
def _make_hN(self, level: int) -> tuple[str, str]:
|
||||||
return f"h{min(6, max(1, level + self._hlevel_offset))}", ""
|
return f"h{min(6, max(1, level + self._hlevel_offset))}", ""
|
||||||
|
|
|
@ -402,6 +402,18 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer):
|
||||||
)
|
)
|
||||||
if not (items := walk_and_emit(toc, toc_depth)):
|
if not (items := walk_and_emit(toc, toc_depth)):
|
||||||
return ""
|
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 (
|
return (
|
||||||
f'<div class="toc">'
|
f'<div class="toc">'
|
||||||
f' <p><strong>Table of Contents</strong></p>'
|
f' <p><strong>Table of Contents</strong></p>'
|
||||||
|
@ -409,6 +421,7 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer):
|
||||||
f' {"".join(items)}'
|
f' {"".join(items)}'
|
||||||
f' </dl>'
|
f' </dl>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
|
f'{examples}'
|
||||||
)
|
)
|
||||||
|
|
||||||
def _make_hN(self, level: int) -> tuple[str, str]:
|
def _make_hN(self, level: int) -> tuple[str, str]:
|
||||||
|
@ -513,6 +526,25 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]):
|
||||||
self._redirection_targets.add(into)
|
self._redirection_targets.add(into)
|
||||||
return tokens
|
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)
|
# xref | (id, type, heading inlines, file, starts new file)
|
||||||
def _collect_ids(self, tokens: Sequence[Token], target_file: str, typ: str, file_changed: bool
|
def _collect_ids(self, tokens: Sequence[Token], target_file: str, typ: str, file_changed: bool
|
||||||
) -> list[XrefTarget | tuple[str, str, Token, str, bool]]:
|
) -> list[XrefTarget | tuple[str, str, Token, str, bool]]:
|
||||||
|
@ -534,6 +566,8 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]):
|
||||||
subtyp = bt.type.removeprefix('included_').removesuffix('s')
|
subtyp = bt.type.removeprefix('included_').removesuffix('s')
|
||||||
for si, (sub, _path) in enumerate(bt.meta['included']):
|
for si, (sub, _path) in enumerate(bt.meta['included']):
|
||||||
result += self._collect_ids(sub, sub_file, subtyp, si == 0 and sub_file != target_file)
|
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':
|
elif bt.type == 'inline':
|
||||||
assert bt.children
|
assert bt.children
|
||||||
result += self._collect_ids(bt.children, target_file, typ, False)
|
result += self._collect_ids(bt.children, target_file, typ, False)
|
||||||
|
@ -558,6 +592,11 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]):
|
||||||
title = prefix + title_html
|
title = prefix + title_html
|
||||||
toc_html = f"{n}. {title_html}"
|
toc_html = f"{n}. {title_html}"
|
||||||
title_html = f"Appendix {n}"
|
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:
|
else:
|
||||||
toc_html, title = title_html, title_html
|
toc_html, title = title_html, 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)
|
return XrefTarget(id, title_html, toc_html, re.sub('<.*?>', '', title), path, drop_fragment)
|
||||||
|
|
||||||
def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) -> None:
|
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)
|
xref_queue = self._collect_ids(tokens, outfile.name, 'book', True)
|
||||||
|
|
||||||
failed = False
|
failed = False
|
||||||
|
|
|
@ -14,7 +14,7 @@ from .utils import Freezeable
|
||||||
FragmentType = Literal['preface', 'part', 'chapter', 'section', 'appendix']
|
FragmentType = Literal['preface', 'part', 'chapter', 'section', 'appendix']
|
||||||
|
|
||||||
# in the TOC all fragments are allowed, plus the all-encompassing book.
|
# 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:
|
def is_include(token: Token) -> bool:
|
||||||
return token.type == "fence" and token.info.startswith("{=include=} ")
|
return token.type == "fence" and token.info.startswith("{=include=} ")
|
||||||
|
@ -124,6 +124,7 @@ class TocEntry(Freezeable):
|
||||||
next: TocEntry | None = None
|
next: TocEntry | None = None
|
||||||
children: list[TocEntry] = dc.field(default_factory=list)
|
children: list[TocEntry] = dc.field(default_factory=list)
|
||||||
starts_new_chunk: bool = False
|
starts_new_chunk: bool = False
|
||||||
|
examples: list[TocEntry] = dc.field(default_factory=list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def root(self) -> TocEntry:
|
def root(self) -> TocEntry:
|
||||||
|
@ -138,13 +139,13 @@ class TocEntry(Freezeable):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def collect_and_link(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token]) -> TocEntry:
|
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]:
|
def flatten_with_parent(this: TocEntry, parent: TocEntry | None) -> Iterable[TocEntry]:
|
||||||
this.parent = parent
|
this.parent = parent
|
||||||
return itertools.chain([this], *[ flatten_with_parent(c, this) for c in this.children ])
|
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 = flat[0]
|
||||||
prev.starts_new_chunk = True
|
prev.starts_new_chunk = True
|
||||||
paths_seen = set([prev.target.path])
|
paths_seen = set([prev.target.path])
|
||||||
|
@ -155,32 +156,39 @@ class TocEntry(Freezeable):
|
||||||
prev = c
|
prev = c
|
||||||
paths_seen.add(c.target.path)
|
paths_seen.add(c.target.path)
|
||||||
|
|
||||||
|
flat[0].examples = examples
|
||||||
|
|
||||||
for c in flat:
|
for c in flat:
|
||||||
c.freeze()
|
c.freeze()
|
||||||
|
|
||||||
return result
|
return entries
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _collect_entries(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token],
|
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.
|
# 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
|
# list contains (tag, entry) pairs that will collapse to a single entry for
|
||||||
# the full sequence.
|
# the full sequence.
|
||||||
entries: list[tuple[str, TocEntry]] = []
|
entries: list[tuple[str, TocEntry]] = []
|
||||||
|
examples: list[TocEntry] = []
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
if token.type.startswith('included_') and (included := token.meta.get('included')):
|
if token.type.startswith('included_') and (included := token.meta.get('included')):
|
||||||
fragment_type_str = token.type[9:].removesuffix('s')
|
fragment_type_str = token.type[9:].removesuffix('s')
|
||||||
assert fragment_type_str in get_args(TocEntryType)
|
assert fragment_type_str in get_args(TocEntryType)
|
||||||
fragment_type = cast(TocEntryType, fragment_type_str)
|
fragment_type = cast(TocEntryType, fragment_type_str)
|
||||||
for fragment, _path in included:
|
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', ''))):
|
elif token.type == 'heading_open' and (id := cast(str, token.attrs.get('id', ''))):
|
||||||
while len(entries) > 1 and entries[-1][0] >= token.tag:
|
while len(entries) > 1 and entries[-1][0] >= token.tag:
|
||||||
entries[-2][1].children.append(entries.pop()[1])
|
entries[-2][1].children.append(entries.pop()[1])
|
||||||
entries.append((token.tag,
|
entries.append((token.tag,
|
||||||
TocEntry(kind if token.tag == 'h1' else 'section', xrefs[id])))
|
TocEntry(kind if token.tag == 'h1' else 'section', xrefs[id])))
|
||||||
token.meta['TocEntry'] = entries[-1][1]
|
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:
|
while len(entries) > 1:
|
||||||
entries[-2][1].children.append(entries.pop()[1])
|
entries[-2][1].children.append(entries.pop()[1])
|
||||||
return entries[0][1]
|
return (entries[0][1], examples)
|
||||||
|
|
|
@ -88,6 +88,8 @@ class Renderer:
|
||||||
"ordered_list_close": self.ordered_list_close,
|
"ordered_list_close": self.ordered_list_close,
|
||||||
"example_open": self.example_open,
|
"example_open": self.example_open,
|
||||||
"example_close": self.example_close,
|
"example_close": self.example_close,
|
||||||
|
"example_title_open": self.example_title_open,
|
||||||
|
"example_title_close": self.example_title_close,
|
||||||
}
|
}
|
||||||
|
|
||||||
self._admonitions = {
|
self._admonitions = {
|
||||||
|
@ -219,6 +221,10 @@ class Renderer:
|
||||||
raise RuntimeError("md token not supported", token)
|
raise RuntimeError("md token not supported", token)
|
||||||
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
||||||
raise RuntimeError("md token not supported", token)
|
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:
|
def _is_escaped(src: str, pos: int) -> bool:
|
||||||
found = 0
|
found = 0
|
||||||
|
@ -417,6 +423,32 @@ def _block_attr(md: markdown_it.MarkdownIt) -> None:
|
||||||
|
|
||||||
md.core.ruler.push("block_attr", block_attr)
|
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')
|
TR = TypeVar('TR', bound='Renderer')
|
||||||
|
|
||||||
class Converter(ABC, Generic[TR]):
|
class Converter(ABC, Generic[TR]):
|
||||||
|
@ -459,6 +491,7 @@ class Converter(ABC, Generic[TR]):
|
||||||
self._md.use(_heading_ids)
|
self._md.use(_heading_ids)
|
||||||
self._md.use(_compact_list_attr)
|
self._md.use(_compact_list_attr)
|
||||||
self._md.use(_block_attr)
|
self._md.use(_block_attr)
|
||||||
|
self._md.use(_example_titles)
|
||||||
self._md.enable(["smartquotes", "replacements"])
|
self._md.enable(["smartquotes", "replacements"])
|
||||||
|
|
||||||
def _parse(self, src: str) -> list[Token]:
|
def _parse(self, src: str) -> list[Token]:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import nixos_render_docs as nrd
|
import nixos_render_docs as nrd
|
||||||
|
import pytest
|
||||||
|
|
||||||
from markdown_it.token import Token
|
from markdown_it.token import Token
|
||||||
|
|
||||||
|
@ -427,18 +428,38 @@ def test_admonitions() -> None:
|
||||||
|
|
||||||
def test_example() -> None:
|
def test_example() -> None:
|
||||||
c = Converter({})
|
c = Converter({})
|
||||||
assert c._parse("::: {.example}") == [
|
assert c._parse("::: {.example}\n# foo") == [
|
||||||
Token(type='example_open', tag='div', nesting=1, attrs={}, map=[0, 1], level=0, children=None,
|
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),
|
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,
|
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}") == [
|
assert c._parse("::: {#eid .example}\n# foo") == [
|
||||||
Token(type='example_open', tag='div', nesting=1, attrs={'id': 'eid'}, map=[0, 1], level=0,
|
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,
|
children=None, content='', markup=':::', info=' {#eid .example}', meta={}, block=True,
|
||||||
hidden=False),
|
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,
|
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}") == [
|
assert c._parse("::: {.example .note}") == [
|
||||||
Token(type='paragraph_open', tag='p', nesting=1, attrs={}, map=[0, 1], level=0, children=None,
|
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,
|
Token(type='paragraph_close', tag='p', 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}\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'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue