nixos-render-docs: Support explicit anchors in markdown for optional compatibility with the HTML renderer (#370352)

This commit is contained in:
Robert Hensing
2025-01-14 23:10:41 +01:00
committed by GitHub
9 changed files with 185 additions and 4 deletions
+6
View File
@@ -1,3 +1,5 @@
# Tests: ./tests.nix
/**
Generates documentation for [nix modules](https://nix.dev/tutorials/module-system/index.html).
@@ -193,12 +195,16 @@ rec {
optionsCommonMark =
pkgs.runCommand "options.md"
{
__structuredAttrs = true;
nativeBuildInputs = [ pkgs.nixos-render-docs ];
# For overriding
extraArgs = [ ];
}
''
nixos-render-docs -j $NIX_BUILD_CORES options commonmark \
--manpage-urls ${pkgs.path + "/doc/manpage-urls.json"} \
--revision ${lib.escapeShellArg revision} \
''${extraArgs[@]} \
${optionsJSON}/share/doc/nixos/options.json \
$out
'';
+59
View File
@@ -0,0 +1,59 @@
# Run tests: nix-build -A tests.nixosOptionsDoc
{
lib,
nixosOptionsDoc,
runCommand,
}:
let
inherit (lib) mkOption types;
eval = lib.evalModules {
modules = [
{
options.foo.bar.enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable the foo bar feature.
'';
};
}
];
};
doc = nixosOptionsDoc {
inherit (eval) options;
};
in
{
/**
Test that
- the `nixosOptionsDoc` function can be invoked
- integration of the module system and `nixosOptionsDoc` (limited coverage)
The more interesting tests happen in the `nixos-render-docs` package.
*/
commonMark =
runCommand "test-nixosOptionsDoc-commonMark"
{
commonMarkDefault = doc.optionsCommonMark;
commonMarkAnchors = doc.optionsCommonMark.overrideAttrs {
extraArgs = [
"--anchor-prefix"
"my-opt-"
"--anchor-style"
"legacy"
];
};
}
''
env | grep ^commonMark | sed -e 's/=/ = /'
(
set -x
grep -F 'foo\.bar\.enable' $commonMarkDefault >/dev/null
grep -F '{#my-opt-foo.bar.enable}' $commonMarkAnchors >/dev/null
)
touch $out
'';
}
@@ -21,7 +21,7 @@ from .html import HTMLRenderer
from .manpage import ManpageRenderer, man_escape
from .manual_structure import make_xml_id, XrefTarget
from .md import Converter, md_escape, md_make_code
from .types import OptionLoc, Option, RenderedOption
from .types import OptionLoc, Option, RenderedOption, AnchorStyle
def option_is(option: Option, key: str, typ: str) -> Optional[dict[str, str]]:
if key not in option:
@@ -317,10 +317,15 @@ class OptionsCommonMarkRenderer(OptionDocsRestrictions, CommonMarkRenderer):
class CommonMarkConverter(BaseConverter[OptionsCommonMarkRenderer]):
__option_block_separator__ = ""
_anchor_style: AnchorStyle
_anchor_prefix: str
def __init__(self, manpage_urls: Mapping[str, str], revision: str):
def __init__(self, manpage_urls: Mapping[str, str], revision: str, anchor_style: AnchorStyle = AnchorStyle.NONE, anchor_prefix: str = ""):
super().__init__(revision)
self._renderer = OptionsCommonMarkRenderer(manpage_urls)
self._anchor_style = anchor_style
self._anchor_prefix = anchor_prefix
def _parallel_render_prepare(self) -> Any:
return (self._renderer._manpage_urls, self._revision)
@@ -342,11 +347,21 @@ class CommonMarkConverter(BaseConverter[OptionsCommonMarkRenderer]):
def _decl_def_footer(self) -> list[str]:
return []
def _make_anchor_suffix(self, loc: list[str]) -> str:
if self._anchor_style == AnchorStyle.NONE:
return ""
elif self._anchor_style == AnchorStyle.LEGACY:
sanitized = ".".join(map(make_xml_id, loc))
return f" {{#{self._anchor_prefix}{sanitized}}}"
else:
raise RuntimeError("unhandled anchor style", self._anchor_style)
def finalize(self) -> str:
result = []
for (name, opt) in self._sorted_options():
result.append(f"## {md_escape(name)}\n")
anchor_suffix = self._make_anchor_suffix(opt.loc)
result.append(f"## {md_escape(name)}{anchor_suffix}\n")
result += opt.lines
result.append("\n\n")
@@ -490,9 +505,30 @@ def _build_cli_manpage(p: argparse.ArgumentParser) -> None:
p.add_argument("infile")
p.add_argument("outfile")
def parse_anchor_style(value: str|AnchorStyle) -> AnchorStyle:
if isinstance(value, AnchorStyle):
# Used by `argparse.add_argument`'s `default`
return value
try:
return AnchorStyle(value.lower())
except ValueError:
raise argparse.ArgumentTypeError(f"Invalid value {value}\nExpected one of {', '.join(style.value for style in AnchorStyle)}")
def _build_cli_commonmark(p: argparse.ArgumentParser) -> None:
p.add_argument('--manpage-urls', required=True)
p.add_argument('--revision', required=True)
p.add_argument(
'--anchor-style',
required=False,
default=AnchorStyle.NONE.value,
choices = [style.value for style in AnchorStyle],
help = "(default: %(default)s) Anchor style to use for links to options. \nOnly none is standard CommonMark."
)
p.add_argument('--anchor-prefix',
required=False,
default="",
help="(default: no prefix) String to prepend to anchor ids. Not used when anchor style is none."
)
p.add_argument("infile")
p.add_argument("outfile")
@@ -527,7 +563,10 @@ def _run_cli_manpage(args: argparse.Namespace) -> None:
def _run_cli_commonmark(args: argparse.Namespace) -> None:
with open(args.manpage_urls, 'r') as manpage_urls:
md = CommonMarkConverter(json.load(manpage_urls), revision = args.revision)
md = CommonMarkConverter(json.load(manpage_urls),
revision = args.revision,
anchor_style = parse_anchor_style(args.anchor_style),
anchor_prefix = args.anchor_prefix)
with open(args.infile, 'r') as f:
md.add_options(json.load(f))
@@ -1,4 +1,5 @@
from collections.abc import Sequence
from enum import Enum
from typing import Callable, Optional, NamedTuple
from markdown_it.token import Token
@@ -12,3 +13,7 @@ class RenderedOption(NamedTuple):
links: Optional[list[str]] = None
RenderFn = Callable[[Token, Sequence[Token], int], str]
class AnchorStyle(Enum):
NONE = "none"
LEGACY = "legacy"
@@ -0,0 +1,17 @@
{
"services.frobnicator.types.<name>.enable": {
"declarations": [
"nixos/modules/services/frobnicator.nix"
],
"description": "Whether to enable the frobnication of this (`<name>`) type.",
"loc": [
"services",
"frobnicator",
"types",
"<name>",
"enable"
],
"readOnly": false,
"type": "boolean"
}
}
@@ -0,0 +1,13 @@
## services\.frobnicator\.types\.\<name>\.enable
Whether to enable the frobnication of this (` <name> `) type\.
*Type:*
boolean
*Declared by:*
- [\<nixpkgs/nixos/modules/services/frobnicator\.nix>](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/frobnicator.nix)
@@ -0,0 +1,13 @@
## services\.frobnicator\.types\.\<name>\.enable {#opt-services.frobnicator.types._name_.enable}
Whether to enable the frobnication of this (` <name> `) type\.
*Type:*
boolean
*Declared by:*
- [\<nixpkgs/nixos/modules/services/frobnicator\.nix>](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/frobnicator.nix)
@@ -1,6 +1,9 @@
import nixos_render_docs
from nixos_render_docs.options import AnchorStyle
import json
from markdown_it.token import Token
from pathlib import Path
import pytest
def test_option_headings() -> None:
@@ -12,3 +15,27 @@ def test_option_headings() -> None:
type='heading_open', tag='h1', nesting=1, attrs={}, map=[0, 1], level=0, children=None,
content='', markup='#', info='', meta={}, block=True, hidden=False
)
def test_options_commonmark() -> None:
c = nixos_render_docs.options.CommonMarkConverter({}, 'local')
with Path('tests/sample_options_simple.json').open() as f:
opts = json.load(f)
assert opts is not None
with Path('tests/sample_options_simple_default.md').open() as f:
expected = f.read()
c.add_options(opts)
s = c.finalize()
assert s == expected
def test_options_commonmark_legacy_anchors() -> None:
c = nixos_render_docs.options.CommonMarkConverter({}, 'local', anchor_style = AnchorStyle.LEGACY, anchor_prefix = 'opt-')
with Path('tests/sample_options_simple.json').open() as f:
opts = json.load(f)
assert opts is not None
with Path('tests/sample_options_simple_legacy.md').open() as f:
expected = f.read()
c.add_options(opts)
s = c.finalize()
assert s == expected
+2
View File
@@ -155,6 +155,8 @@ with pkgs;
nixos-functions = callPackage ./nixos-functions { };
nixosOptionsDoc = callPackage ../../nixos/lib/make-options-doc/tests.nix { };
overriding = callPackage ./overriding.nix { };
texlive = callPackage ./texlive { };