nixos-render-docs: Support explicit anchors in markdown for optional compatibility with the HTML renderer (#370352)
This commit is contained in:
@@ -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
|
||||
'';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 { };
|
||||
|
||||
Reference in New Issue
Block a user