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

authored by

Robert Hensing and committed by
GitHub
bfefff96 a5924c24

+185 -4
+6
nixos/lib/make-options-doc/default.nix
··· 1 + # Tests: ./tests.nix 2 + 1 3 /** 2 4 Generates documentation for [nix modules](https://nix.dev/tutorials/module-system/index.html). 3 5 ··· 193 195 optionsCommonMark = 194 196 pkgs.runCommand "options.md" 195 197 { 198 + __structuredAttrs = true; 196 199 nativeBuildInputs = [ pkgs.nixos-render-docs ]; 200 + # For overriding 201 + extraArgs = [ ]; 197 202 } 198 203 '' 199 204 nixos-render-docs -j $NIX_BUILD_CORES options commonmark \ 200 205 --manpage-urls ${pkgs.path + "/doc/manpage-urls.json"} \ 201 206 --revision ${lib.escapeShellArg revision} \ 207 + ''${extraArgs[@]} \ 202 208 ${optionsJSON}/share/doc/nixos/options.json \ 203 209 $out 204 210 '';
+59
nixos/lib/make-options-doc/tests.nix
··· 1 + # Run tests: nix-build -A tests.nixosOptionsDoc 2 + 3 + { 4 + lib, 5 + nixosOptionsDoc, 6 + runCommand, 7 + }: 8 + let 9 + inherit (lib) mkOption types; 10 + 11 + eval = lib.evalModules { 12 + modules = [ 13 + { 14 + options.foo.bar.enable = mkOption { 15 + type = types.bool; 16 + default = false; 17 + description = '' 18 + Enable the foo bar feature. 19 + ''; 20 + }; 21 + } 22 + ]; 23 + }; 24 + 25 + doc = nixosOptionsDoc { 26 + inherit (eval) options; 27 + }; 28 + in 29 + { 30 + /** 31 + Test that 32 + - the `nixosOptionsDoc` function can be invoked 33 + - integration of the module system and `nixosOptionsDoc` (limited coverage) 34 + 35 + The more interesting tests happen in the `nixos-render-docs` package. 36 + */ 37 + commonMark = 38 + runCommand "test-nixosOptionsDoc-commonMark" 39 + { 40 + commonMarkDefault = doc.optionsCommonMark; 41 + commonMarkAnchors = doc.optionsCommonMark.overrideAttrs { 42 + extraArgs = [ 43 + "--anchor-prefix" 44 + "my-opt-" 45 + "--anchor-style" 46 + "legacy" 47 + ]; 48 + }; 49 + } 50 + '' 51 + env | grep ^commonMark | sed -e 's/=/ = /' 52 + ( 53 + set -x 54 + grep -F 'foo\.bar\.enable' $commonMarkDefault >/dev/null 55 + grep -F '{#my-opt-foo.bar.enable}' $commonMarkAnchors >/dev/null 56 + ) 57 + touch $out 58 + ''; 59 + }
+43 -4
pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/options.py
··· 21 21 from .manpage import ManpageRenderer, man_escape 22 22 from .manual_structure import make_xml_id, XrefTarget 23 23 from .md import Converter, md_escape, md_make_code 24 - from .types import OptionLoc, Option, RenderedOption 24 + from .types import OptionLoc, Option, RenderedOption, AnchorStyle 25 25 26 26 def option_is(option: Option, key: str, typ: str) -> Optional[dict[str, str]]: 27 27 if key not in option: ··· 317 317 318 318 class CommonMarkConverter(BaseConverter[OptionsCommonMarkRenderer]): 319 319 __option_block_separator__ = "" 320 + _anchor_style: AnchorStyle 321 + _anchor_prefix: str 320 322 321 - def __init__(self, manpage_urls: Mapping[str, str], revision: str): 323 + 324 + def __init__(self, manpage_urls: Mapping[str, str], revision: str, anchor_style: AnchorStyle = AnchorStyle.NONE, anchor_prefix: str = ""): 322 325 super().__init__(revision) 323 326 self._renderer = OptionsCommonMarkRenderer(manpage_urls) 327 + self._anchor_style = anchor_style 328 + self._anchor_prefix = anchor_prefix 324 329 325 330 def _parallel_render_prepare(self) -> Any: 326 331 return (self._renderer._manpage_urls, self._revision) ··· 342 347 def _decl_def_footer(self) -> list[str]: 343 348 return [] 344 349 350 + def _make_anchor_suffix(self, loc: list[str]) -> str: 351 + if self._anchor_style == AnchorStyle.NONE: 352 + return "" 353 + elif self._anchor_style == AnchorStyle.LEGACY: 354 + sanitized = ".".join(map(make_xml_id, loc)) 355 + return f" {{#{self._anchor_prefix}{sanitized}}}" 356 + else: 357 + raise RuntimeError("unhandled anchor style", self._anchor_style) 358 + 345 359 def finalize(self) -> str: 346 360 result = [] 347 361 348 362 for (name, opt) in self._sorted_options(): 349 - result.append(f"## {md_escape(name)}\n") 363 + anchor_suffix = self._make_anchor_suffix(opt.loc) 364 + result.append(f"## {md_escape(name)}{anchor_suffix}\n") 350 365 result += opt.lines 351 366 result.append("\n\n") 352 367 ··· 490 505 p.add_argument("infile") 491 506 p.add_argument("outfile") 492 507 508 + def parse_anchor_style(value: str|AnchorStyle) -> AnchorStyle: 509 + if isinstance(value, AnchorStyle): 510 + # Used by `argparse.add_argument`'s `default` 511 + return value 512 + try: 513 + return AnchorStyle(value.lower()) 514 + except ValueError: 515 + raise argparse.ArgumentTypeError(f"Invalid value {value}\nExpected one of {', '.join(style.value for style in AnchorStyle)}") 516 + 493 517 def _build_cli_commonmark(p: argparse.ArgumentParser) -> None: 494 518 p.add_argument('--manpage-urls', required=True) 495 519 p.add_argument('--revision', required=True) 520 + p.add_argument( 521 + '--anchor-style', 522 + required=False, 523 + default=AnchorStyle.NONE.value, 524 + choices = [style.value for style in AnchorStyle], 525 + help = "(default: %(default)s) Anchor style to use for links to options. \nOnly none is standard CommonMark." 526 + ) 527 + p.add_argument('--anchor-prefix', 528 + required=False, 529 + default="", 530 + help="(default: no prefix) String to prepend to anchor ids. Not used when anchor style is none." 531 + ) 496 532 p.add_argument("infile") 497 533 p.add_argument("outfile") 498 534 ··· 527 563 528 564 def _run_cli_commonmark(args: argparse.Namespace) -> None: 529 565 with open(args.manpage_urls, 'r') as manpage_urls: 530 - md = CommonMarkConverter(json.load(manpage_urls), revision = args.revision) 566 + md = CommonMarkConverter(json.load(manpage_urls), 567 + revision = args.revision, 568 + anchor_style = parse_anchor_style(args.anchor_style), 569 + anchor_prefix = args.anchor_prefix) 531 570 532 571 with open(args.infile, 'r') as f: 533 572 md.add_options(json.load(f))
+5
pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/types.py
··· 1 1 from collections.abc import Sequence 2 + from enum import Enum 2 3 from typing import Callable, Optional, NamedTuple 3 4 4 5 from markdown_it.token import Token ··· 12 13 links: Optional[list[str]] = None 13 14 14 15 RenderFn = Callable[[Token, Sequence[Token], int], str] 16 + 17 + class AnchorStyle(Enum): 18 + NONE = "none" 19 + LEGACY = "legacy"
+17
pkgs/by-name/ni/nixos-render-docs/src/tests/sample_options_simple.json
··· 1 + { 2 + "services.frobnicator.types.<name>.enable": { 3 + "declarations": [ 4 + "nixos/modules/services/frobnicator.nix" 5 + ], 6 + "description": "Whether to enable the frobnication of this (`<name>`) type.", 7 + "loc": [ 8 + "services", 9 + "frobnicator", 10 + "types", 11 + "<name>", 12 + "enable" 13 + ], 14 + "readOnly": false, 15 + "type": "boolean" 16 + } 17 + }
+13
pkgs/by-name/ni/nixos-render-docs/src/tests/sample_options_simple_default.md
··· 1 + ## services\.frobnicator\.types\.\<name>\.enable 2 + 3 + Whether to enable the frobnication of this (` <name> `) type\. 4 + 5 + 6 + 7 + *Type:* 8 + boolean 9 + 10 + *Declared by:* 11 + - [\<nixpkgs/nixos/modules/services/frobnicator\.nix>](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/frobnicator.nix) 12 + 13 +
+13
pkgs/by-name/ni/nixos-render-docs/src/tests/sample_options_simple_legacy.md
··· 1 + ## services\.frobnicator\.types\.\<name>\.enable {#opt-services.frobnicator.types._name_.enable} 2 + 3 + Whether to enable the frobnication of this (` <name> `) type\. 4 + 5 + 6 + 7 + *Type:* 8 + boolean 9 + 10 + *Declared by:* 11 + - [\<nixpkgs/nixos/modules/services/frobnicator\.nix>](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/frobnicator.nix) 12 + 13 +
+27
pkgs/by-name/ni/nixos-render-docs/src/tests/test_options.py
··· 1 1 import nixos_render_docs 2 + from nixos_render_docs.options import AnchorStyle 2 3 4 + import json 3 5 from markdown_it.token import Token 6 + from pathlib import Path 4 7 import pytest 5 8 6 9 def test_option_headings() -> None: ··· 12 15 type='heading_open', tag='h1', nesting=1, attrs={}, map=[0, 1], level=0, children=None, 13 16 content='', markup='#', info='', meta={}, block=True, hidden=False 14 17 ) 18 + 19 + def test_options_commonmark() -> None: 20 + c = nixos_render_docs.options.CommonMarkConverter({}, 'local') 21 + with Path('tests/sample_options_simple.json').open() as f: 22 + opts = json.load(f) 23 + assert opts is not None 24 + with Path('tests/sample_options_simple_default.md').open() as f: 25 + expected = f.read() 26 + 27 + c.add_options(opts) 28 + s = c.finalize() 29 + assert s == expected 30 + 31 + def test_options_commonmark_legacy_anchors() -> None: 32 + c = nixos_render_docs.options.CommonMarkConverter({}, 'local', anchor_style = AnchorStyle.LEGACY, anchor_prefix = 'opt-') 33 + with Path('tests/sample_options_simple.json').open() as f: 34 + opts = json.load(f) 35 + assert opts is not None 36 + with Path('tests/sample_options_simple_legacy.md').open() as f: 37 + expected = f.read() 38 + 39 + c.add_options(opts) 40 + s = c.finalize() 41 + assert s == expected
+2
pkgs/test/default.nix
··· 155 155 156 156 nixos-functions = callPackage ./nixos-functions { }; 157 157 158 + nixosOptionsDoc = callPackage ../../nixos/lib/make-options-doc/tests.nix { }; 159 + 158 160 overriding = callPackage ./overriding.nix { }; 159 161 160 162 texlive = callPackage ./texlive { };