Skip to content

Commit e13ba5e

Browse files
bittnerkhaneliman
authored andcommitted
docs: link module pages without an invalid anchor
Drops the URL fragment from links that address a `programs.foo` or `services.foo` page. Such a path names a module rather than an option, so the page carries no matching anchor and the fragment pointed nowhere. Option paths keep their fragment, including the single-segment options `lib`, `specialisation` and `uninstall`, whose name equals their page path. Moves the anchor handling that convert-markup.py and render-options.py had in common into a new option_links module imported by both. The derivations therefore pass the mdbook directory instead of a single script file, so the import resolves.
1 parent a0d058e commit e13ba5e

5 files changed

Lines changed: 85 additions & 83 deletions

File tree

docs/home-manager-manual.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ stdenv.mkDerivation {
4848
runHook preBuild
4949
5050
mkdir -p source
51-
python3 ${./mdbook/convert-markup.py} "$src/manual" source
52-
python3 ${./mdbook/convert-markup.py} \
51+
python3 ${./mdbook}/convert-markup.py "$src/manual" source
52+
python3 ${./mdbook}/convert-markup.py \
5353
--base-depth 1 \
5454
"$src/release-notes" \
5555
source/release-notes

docs/mdbook/convert-markup.py

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import sys
99
from pathlib import Path
1010

11+
from option_links import OPTION_LINK, option_label, option_target
12+
1113

1214
SIMPLE_ROLES = (
1315
"command",
@@ -23,55 +25,13 @@
2325
INLINE_ANCHOR = re.compile(r"\[\]\{#([^}]+)\}")
2426
OPTION_ROLE = re.compile(r"(?<![$`])\{option\}`([^`]*)`")
2527
SIMPLE_ROLE = re.compile(r"(?<![$`])\{(" + "|".join(SIMPLE_ROLES) + r")\}`([^`]*)`")
26-
OPTION_LINK = re.compile(
27-
r"\[(?P<label>[^\]]*)\]\(#(?P<anchor>(?:opt|nixos-opt|nix-darwin-opt)-[^)]+)\)"
28-
)
2928
LEFTOVER_ROLE = re.compile(
3029
r"(?<![$`])\{(" + "|".join(("option", *SIMPLE_ROLES)) + r")\}`[^`]*`"
3130
)
3231
FENCE = re.compile(r"^\s*(`{3,})(.*)$")
3332
FENCE_CLOSE = re.compile(r"^\s*`{3,}\s*$")
3433
ADMONITION_OPEN = re.compile(r"^\s*:::\s*\{\.(note|warning|example)\}\s*$")
3534
ADMONITION_CLOSE = re.compile(r"^\s*:::\s*$")
36-
DEEP_SPLIT_NAMESPACES = {"programs", "services"}
37-
38-
39-
def option_target(anchor: str, current_file: Path, base_depth: int) -> str:
40-
if anchor.startswith("nix-darwin-opt-"):
41-
option = anchor.removeprefix("nix-darwin-opt-")
42-
option = option.replace("<", "_").replace(">", "_")
43-
anchor = f"nix-darwin-opt-{option}"
44-
base = "options/nix-darwin"
45-
elif anchor.startswith("nixos-opt-"):
46-
option = anchor.removeprefix("nixos-opt-")
47-
option = option.replace("<", "_").replace(">", "_")
48-
anchor = f"nixos-opt-{option}"
49-
base = "options/nixos"
50-
else:
51-
option = anchor.removeprefix("opt-")
52-
option = option.replace("<", "_").replace(">", "_")
53-
anchor = f"opt-{option}"
54-
base = "options/home-manager"
55-
56-
page_parts = option_page_parts(option)
57-
prefix = "../" * (base_depth + len(current_file.parent.parts))
58-
return f"{prefix}{base}/{'/'.join(page_parts)}.md#{anchor}"
59-
60-
61-
def option_page_parts(option_name: str) -> list[str]:
62-
parts = option_name.split(".")
63-
namespace = parts[0]
64-
if namespace in DEEP_SPLIT_NAMESPACES and len(parts) > 1:
65-
return parts[:2]
66-
return [namespace]
67-
68-
69-
def option_label(anchor: str) -> str:
70-
if anchor.startswith("nix-darwin-opt-"):
71-
return anchor.removeprefix("nix-darwin-opt-")
72-
if anchor.startswith("nixos-opt-"):
73-
return anchor.removeprefix("nixos-opt-")
74-
return anchor.removeprefix("opt-")
7535

7636

7737
def markdown_label(value: str) -> str:

docs/mdbook/option_links.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python3
2+
"""Shared helpers for turning option anchors into mdbook links.
3+
4+
Both the manual conversion and the option page rendering refer to options
5+
through `opt-`, `nixos-opt-` and `nix-darwin-opt-` anchors. The options are
6+
split over one page per namespace, and per module for the namespaces in
7+
`DEEP_SPLIT_NAMESPACES`, so an anchor has to be resolved to a page before it
8+
can be linked to.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import re
14+
from pathlib import Path
15+
16+
17+
OPTION_LINK = re.compile(
18+
r"\[(?P<label>[^\]]*)\]\(#(?P<anchor>(?:opt|nixos-opt|nix-darwin-opt)-[^)]+)\)"
19+
)
20+
OPTION_HREF = re.compile(r'href="#(?P<anchor>(?:opt|nixos-opt|nix-darwin-opt)-[^"]+)"')
21+
DEEP_SPLIT_NAMESPACES = {"programs", "services"}
22+
ANCHOR_BASES = (
23+
("nix-darwin-opt-", "options/nix-darwin"),
24+
("nixos-opt-", "options/nixos"),
25+
("opt-", "options/home-manager"),
26+
)
27+
28+
29+
def option_label(anchor: str) -> str:
30+
"""Return the option name an anchor refers to."""
31+
for prefix, _ in ANCHOR_BASES:
32+
if anchor.startswith(prefix):
33+
return anchor.removeprefix(prefix)
34+
return anchor
35+
36+
37+
def option_page_parts(option_name: str) -> list[str]:
38+
"""Return the path segments of the page documenting an option."""
39+
parts = option_name.split(".")
40+
namespace = parts[0]
41+
if namespace in DEEP_SPLIT_NAMESPACES and len(parts) > 1:
42+
return parts[:2]
43+
return [namespace]
44+
45+
46+
def option_fragment(option_name: str, page_parts: list[str], anchor: str) -> str:
47+
"""Return the URL fragment addressing an option on its page.
48+
49+
A `programs.foo` or `services.foo` path names a module rather than an
50+
option, so its page holds no matching anchor and the fragment is empty,
51+
which links to the page itself.
52+
"""
53+
if len(page_parts) > 1 and option_name.split(".") == page_parts:
54+
return ""
55+
return f"#{anchor}"
56+
57+
58+
def option_target(anchor: str, current_file: Path, base_depth: int = 0) -> str:
59+
"""Return a link from `current_file` to the option an anchor refers to.
60+
61+
`base_depth` is the depth of `current_file` below the manual source root.
62+
"""
63+
for prefix, base in ANCHOR_BASES:
64+
if anchor.startswith(prefix):
65+
option = anchor.removeprefix(prefix).replace("<", "_").replace(">", "_")
66+
anchor = f"{prefix}{option}"
67+
break
68+
else:
69+
raise ValueError(f"not an option anchor: {anchor}")
70+
71+
page_parts = option_page_parts(option)
72+
prefix = "../" * (base_depth + len(current_file.parent.parts))
73+
fragment = option_fragment(option, page_parts, anchor)
74+
return f"{prefix}{base}/{'/'.join(page_parts)}.md{fragment}"

docs/mdbook/options.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pkgs.runCommand "home-manager-mdbook-options"
1515
passAsFile = [ "optionDocsJson" ];
1616
}
1717
''
18-
python3 ${./render-options.py} \
18+
python3 ${./.}/render-options.py \
1919
"$optionDocsJsonPath" \
2020
${manpageUrls} \
2121
${revision} \

docs/mdbook/render-options.py

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,17 @@
22
from __future__ import annotations
33

44
import json
5-
import re
65
import subprocess
76
import sys
87
from pathlib import Path
98

10-
11-
OPTION_LINK = re.compile(
12-
r"\[(?P<label>[^\]]*)\]\(#(?P<anchor>(?:opt|nixos-opt|nix-darwin-opt)-[^)]+)\)"
9+
from option_links import (
10+
OPTION_HREF,
11+
OPTION_LINK,
12+
option_label,
13+
option_page_parts,
14+
option_target,
1315
)
14-
OPTION_HREF = re.compile(r'href="#(?P<anchor>(?:opt|nixos-opt|nix-darwin-opt)-[^"]+)"')
15-
DEEP_SPLIT_NAMESPACES = {"programs", "services"}
16-
17-
18-
def option_label(anchor: str) -> str:
19-
if anchor.startswith("nix-darwin-opt-"):
20-
return anchor.removeprefix("nix-darwin-opt-")
21-
if anchor.startswith("nixos-opt-"):
22-
return anchor.removeprefix("nixos-opt-")
23-
return anchor.removeprefix("opt-")
24-
25-
26-
def option_target(anchor: str, current_file: Path) -> str:
27-
if anchor.startswith("nix-darwin-opt-"):
28-
option = anchor.removeprefix("nix-darwin-opt-")
29-
base = "options/nix-darwin"
30-
elif anchor.startswith("nixos-opt-"):
31-
option = anchor.removeprefix("nixos-opt-")
32-
base = "options/nixos"
33-
else:
34-
option = anchor.removeprefix("opt-")
35-
base = "options/home-manager"
36-
37-
page_parts = option_page_parts(option)
38-
prefix = "../" * len(current_file.parent.parts)
39-
return f"{prefix}{base}/{'/'.join(page_parts)}.md#{anchor}"
4016

4117

4218
def rewrite_option_links(text: str, current_file: Path) -> str:
@@ -57,14 +33,6 @@ def namespace_for(option_name: str) -> str:
5733
return option_name.split(".", 1)[0]
5834

5935

60-
def option_page_parts(option_name: str) -> list[str]:
61-
parts = option_name.split(".")
62-
namespace = parts[0]
63-
if namespace in DEEP_SPLIT_NAMESPACES and len(parts) > 1:
64-
return parts[:2]
65-
return [namespace]
66-
67-
6836
def option_group_for(option_name: str) -> str:
6937
return "/".join(option_page_parts(option_name))
7038

0 commit comments

Comments
 (0)