Skip to content

Commit 1d68d6d

Browse files
authored
Merge pull request NixOS#281212 from hsjobeki/nrd/auto-id-prefix
adds block_args for autogenerated ids from trustworthy sources
2 parents dc1bee5 + 48a2178 commit 1d68d6d

File tree

3 files changed

+135
-6
lines changed

3 files changed

+135
-6
lines changed

doc/doc-support/lib-function-docs.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ stdenv.mkDerivation {
2929
mkdir -p "$out"
3030
3131
cat > "$out/index.md" << 'EOF'
32-
```{=include=} sections
32+
```{=include=} sections auto-id-prefix=auto-generated
3333
EOF
3434
3535
${lib.concatMapStrings ({ name, baseName ? name, description }: ''

pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from abc import abstractmethod
99
from collections.abc import Mapping, Sequence
1010
from pathlib import Path
11-
from typing import Any, cast, ClassVar, Generic, get_args, NamedTuple
11+
from typing import Any, Callable, cast, ClassVar, Generic, get_args, NamedTuple
1212

1313
from markdown_it.token import Token
1414

@@ -44,8 +44,40 @@ def convert(self, infile: Path, outfile: Path) -> None:
4444
def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) -> None:
4545
pass
4646

47-
def _parse(self, src: str) -> list[Token]:
47+
def _handle_headings(self, tokens: list[Token], *, on_heading: Callable[[Token,str],None]) -> None:
48+
# Headings in a globally numbered order
49+
# h1 to h6
50+
curr_heading_pos: list[int] = []
51+
for token in tokens:
52+
if token.type == "heading_open":
53+
if token.tag not in ["h1", "h2", "h3", "h4", "h5", "h6"]:
54+
raise RuntimeError(f"Got invalid heading tag {token.tag} in line {token.map[0] + 1 if token.map else 'NOT FOUND'}. Only h1 to h6 headings are allowed.")
55+
56+
idx = int(token.tag[1:]) - 1
57+
58+
if idx >= len(curr_heading_pos):
59+
# extend the list if necessary
60+
curr_heading_pos.extend([0 for _i in range(idx+1 - len(curr_heading_pos))])
61+
62+
curr_heading_pos = curr_heading_pos[:idx+1]
63+
curr_heading_pos[-1] += 1
64+
65+
66+
ident = ".".join(f"{a}" for a in curr_heading_pos)
67+
on_heading(token,ident)
68+
69+
70+
71+
def _parse(self, src: str, *, auto_id_prefix: None | str = None) -> list[Token]:
4872
tokens = super()._parse(src)
73+
if auto_id_prefix:
74+
def set_token_ident(token: Token, ident: str) -> None:
75+
if "id" not in token.attrs:
76+
token.attrs["id"] = f"{auto_id_prefix}-{ident}"
77+
78+
self._handle_headings(tokens, on_heading=set_token_ident)
79+
80+
4981
check_structure(self._current_type[-1], tokens)
5082
for token in tokens:
5183
if not is_include(token):
@@ -89,7 +121,12 @@ def _parse_included_blocks(self, token: Token, block_args: dict[str, str]) -> No
89121
try:
90122
self._base_paths.append(path)
91123
with open(path, 'r') as f:
92-
tokens = self._parse(f.read())
124+
prefix = None
125+
if "auto-id-prefix" in block_args:
126+
# include the current file number to prevent duplicate ids within include blocks
127+
prefix = f"{block_args.get('auto-id-prefix')}-{lnum}"
128+
129+
tokens = self._parse(f.read(), auto_id_prefix=prefix)
93130
included.append((tokens, path))
94131
self._base_paths.pop()
95132
except Exception as e:
@@ -554,8 +591,8 @@ def convert(self, infile: Path, outfile: Path) -> None:
554591
infile.parent, outfile.parent)
555592
super().convert(infile, outfile)
556593

557-
def _parse(self, src: str) -> list[Token]:
558-
tokens = super()._parse(src)
594+
def _parse(self, src: str, *, auto_id_prefix: None | str = None) -> list[Token]:
595+
tokens = super()._parse(src,auto_id_prefix=auto_id_prefix)
559596
for token in tokens:
560597
if not token.type.startswith('included_') \
561598
or not (into := token.meta['include-args'].get('into-file')):
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from pathlib import Path
2+
3+
from markdown_it.token import Token
4+
from nixos_render_docs.manual import HTMLConverter, HTMLParameters
5+
from nixos_render_docs.md import Converter
6+
7+
auto_id_prefix="TEST_PREFIX"
8+
def set_prefix(token: Token, ident: str) -> None:
9+
token.attrs["id"] = f"{auto_id_prefix}-{ident}"
10+
11+
12+
def test_auto_id_prefix_simple() -> None:
13+
md = HTMLConverter("1.0.0", HTMLParameters("", [], [], 2, 2, 2, Path("")), {})
14+
15+
src = f"""
16+
# title
17+
18+
## subtitle
19+
"""
20+
tokens = Converter()._parse(src)
21+
md._handle_headings(tokens, on_heading=set_prefix)
22+
23+
assert [
24+
{**token.attrs, "tag": token.tag}
25+
for token in tokens
26+
if token.type == "heading_open"
27+
] == [
28+
{"id": "TEST_PREFIX-1", "tag": "h1"},
29+
{"id": "TEST_PREFIX-1.1", "tag": "h2"}
30+
]
31+
32+
33+
def test_auto_id_prefix_repeated() -> None:
34+
md = HTMLConverter("1.0.0", HTMLParameters("", [], [], 2, 2, 2, Path("")), {})
35+
36+
src = f"""
37+
# title
38+
39+
## subtitle
40+
41+
# title2
42+
43+
## subtitle2
44+
"""
45+
tokens = Converter()._parse(src)
46+
md._handle_headings(tokens, on_heading=set_prefix)
47+
48+
assert [
49+
{**token.attrs, "tag": token.tag}
50+
for token in tokens
51+
if token.type == "heading_open"
52+
] == [
53+
{"id": "TEST_PREFIX-1", "tag": "h1"},
54+
{"id": "TEST_PREFIX-1.1", "tag": "h2"},
55+
{"id": "TEST_PREFIX-2", "tag": "h1"},
56+
{"id": "TEST_PREFIX-2.1", "tag": "h2"},
57+
]
58+
59+
def test_auto_id_prefix_maximum_nested() -> None:
60+
md = HTMLConverter("1.0.0", HTMLParameters("", [], [], 2, 2, 2, Path("")), {})
61+
62+
src = f"""
63+
# h1
64+
65+
## h2
66+
67+
### h3
68+
69+
#### h4
70+
71+
##### h5
72+
73+
###### h6
74+
75+
## h2.2
76+
"""
77+
tokens = Converter()._parse(src)
78+
md._handle_headings(tokens, on_heading=set_prefix)
79+
80+
assert [
81+
{**token.attrs, "tag": token.tag}
82+
for token in tokens
83+
if token.type == "heading_open"
84+
] == [
85+
{"id": "TEST_PREFIX-1", "tag": "h1"},
86+
{"id": "TEST_PREFIX-1.1", "tag": "h2"},
87+
{"id": "TEST_PREFIX-1.1.1", "tag": "h3"},
88+
{"id": "TEST_PREFIX-1.1.1.1", "tag": "h4"},
89+
{"id": "TEST_PREFIX-1.1.1.1.1", "tag": "h5"},
90+
{"id": "TEST_PREFIX-1.1.1.1.1.1", "tag": "h6"},
91+
{"id": "TEST_PREFIX-1.2", "tag": "h2"},
92+
]

0 commit comments

Comments
 (0)