Skip to content

Commit 72620f2

Browse files
committed
feat: improve docs page with expandable toolchain versions
Remove redundant default column from packages table. Toolchain versions now display as version pills with expand/collapse arrows that reveal the exact resolved component packages and versions.
1 parent e8393a1 commit 72620f2

1 file changed

Lines changed: 120 additions & 24 deletions

File tree

docs/generate.py

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,68 @@
22
"""Generate GitHub Pages documentation from the toolbox package registry."""
33

44
import json
5-
import os
65
import re
76
from pathlib import Path
87

98

10-
def read_toolchain_components(nix_path: Path) -> list[str]:
11-
"""Parse a toolchain default.nix to extract component package names."""
9+
def load_package_defaults(packages_dir: Path) -> dict[str, str]:
10+
"""Load the default version for every package that has a data.json."""
11+
defaults = {}
12+
for pkg_dir in packages_dir.iterdir():
13+
data_json = pkg_dir / "data.json"
14+
if pkg_dir.is_dir() and data_json.exists():
15+
data = json.loads(data_json.read_text())
16+
defaults[pkg_dir.name] = data.get("_meta", {}).get("default", "")
17+
return defaults
18+
19+
20+
def parse_toolchain_versions(
21+
nix_path: Path, pkg_defaults: dict[str, str]
22+
) -> tuple[list[str], dict[str, list[dict]]]:
23+
"""Parse a toolchain default.nix to extract versions and their resolved components.
24+
25+
Returns (version_names, {version: [{name, version}, ...]}).
26+
"""
1227
content = nix_path.read_text()
13-
return re.findall(r"toolbox\.(\w[\w-]*)\.versions", content)
28+
29+
# Extract the default version
30+
default_match = re.search(r'default\s*=\s*"([^"]+)"', content)
31+
default = default_match.group(1) if default_match else ""
32+
33+
# Split into version blocks: find each "version" = pkgs.symlinkJoin { ... };
34+
version_pattern = re.compile(
35+
r'"([^"]+)"\s*=\s*pkgs\.symlinkJoin\s*\{[^}]*paths\s*=\s*\[(.*?)\]',
36+
re.DOTALL,
37+
)
38+
39+
versions = {}
40+
version_names = []
41+
for m in version_pattern.finditer(content):
42+
ver = m.group(1)
43+
paths_block = m.group(2)
44+
version_names.append(ver)
45+
46+
components = []
47+
# Match toolbox.<pkg>.versions.${toolbox.<pkg>.default} (uses default)
48+
for pkg in re.findall(
49+
r"toolbox\.([\w-]+)\.versions\.\$\{toolbox\.\1\.default\}", paths_block
50+
):
51+
resolved = pkg_defaults.get(pkg, "?")
52+
components.append({"name": pkg, "version": resolved})
53+
54+
# Match toolbox.<pkg>.versions.<literal_version> (pinned)
55+
for pkg, ver_literal in re.findall(
56+
r'toolbox\.([\w-]+)\.versions\."?([^"\s};]+)"?', paths_block
57+
):
58+
# Skip if already matched as a default reference
59+
if not re.search(
60+
rf"toolbox\.{re.escape(pkg)}\.versions\.\$\{{", paths_block
61+
):
62+
components.append({"name": pkg, "version": ver_literal})
63+
64+
versions[ver] = components
65+
66+
return default, version_names, versions
1467

1568

1669
def main():
@@ -19,6 +72,8 @@ def main():
1972
out_dir = repo_root / "docs" / "_site"
2073
out_dir.mkdir(parents=True, exist_ok=True)
2174

75+
pkg_defaults = load_package_defaults(packages_dir)
76+
2277
packages = []
2378
toolchains = []
2479

@@ -46,45 +101,57 @@ def main():
46101
}
47102
)
48103
else:
49-
# Toolchain meta-package
50104
nix_path = pkg_dir / "default.nix"
51-
components = (
52-
read_toolchain_components(nix_path) if nix_path.exists() else []
53-
)
54-
toolchains.append(
55-
{
56-
"name": name,
57-
"components": components,
58-
}
59-
)
105+
if nix_path.exists():
106+
default, version_names, version_map = parse_toolchain_versions(
107+
nix_path, pkg_defaults
108+
)
109+
toolchains.append(
110+
{
111+
"name": name,
112+
"default": default,
113+
"versions": version_names,
114+
"expansion": version_map,
115+
}
116+
)
60117

61118
html = render_html(packages, toolchains)
62119
(out_dir / "index.html").write_text(html)
63-
print(f"Generated docs with {len(packages)} packages and {len(toolchains)} toolchains")
120+
print(
121+
f"Generated docs with {len(packages)} packages and {len(toolchains)} toolchains"
122+
)
64123

65124

66125
def render_html(packages: list[dict], toolchains: list[dict]) -> str:
67126
package_rows = ""
68127
for pkg in packages:
69128
versions_html = ", ".join(
70-
f'<span class="version{"" if v != pkg["default"] else " default"}">{v}</span>'
129+
f'<span class="version{" default" if v == pkg["default"] else ""}">{v}</span>'
71130
for v in pkg["versions"]
72131
)
73132
package_rows += f""" <tr>
74133
<td class="pkg-name">{pkg["name"]}</td>
75-
<td><code>{pkg["default"]}</code></td>
76134
<td>{versions_html}</td>
77135
</tr>
78136
"""
79137

80138
toolchain_rows = ""
81139
for tc in toolchains:
82-
components_html = ", ".join(
83-
f"<code>{c}</code>" for c in tc["components"]
84-
)
140+
versions_html = ""
141+
for ver in tc["versions"]:
142+
is_default = ver == tc["default"]
143+
components = tc["expansion"].get(ver, [])
144+
comp_rows = "".join(
145+
f'<tr><td><code>{c["name"]}</code></td><td>{c["version"]}</td></tr>'
146+
for c in components
147+
)
148+
versions_html += f"""<details class="tc-details">
149+
<summary><span class="version{" default" if is_default else ""}">{ver}</span></summary>
150+
<table class="tc-expansion"><tbody>{comp_rows}</tbody></table>
151+
</details>"""
85152
toolchain_rows += f""" <tr>
86153
<td class="pkg-name">{tc["name"]}</td>
87-
<td>{components_html}</td>
154+
<td>{versions_html}</td>
88155
</tr>
89156
"""
90157

@@ -176,6 +243,7 @@ def render_html(packages: list[dict], toolchains: list[dict]) -> str:
176243
.pkg-name {{
177244
font-weight: 600;
178245
color: var(--accent);
246+
white-space: nowrap;
179247
}}
180248
code {{
181249
background: var(--bg);
@@ -196,6 +264,35 @@ def render_html(packages: list[dict], toolchains: list[dict]) -> str:
196264
color: var(--green);
197265
font-weight: 600;
198266
}}
267+
.tc-details {{
268+
display: inline-block;
269+
margin: 0.1em;
270+
}}
271+
.tc-details summary {{
272+
cursor: pointer;
273+
list-style: none;
274+
}}
275+
.tc-details summary::-webkit-details-marker {{
276+
display: none;
277+
}}
278+
.tc-details summary .version::after {{
279+
content: " \u25b8";
280+
font-size: 0.7em;
281+
color: var(--text-muted);
282+
vertical-align: middle;
283+
}}
284+
.tc-details[open] summary .version::after {{
285+
content: " \u25be";
286+
}}
287+
.tc-expansion {{
288+
margin: 0.4em 0 0.2em 0.5em;
289+
background: var(--bg);
290+
border: 1px solid var(--border);
291+
font-size: 0.85em;
292+
}}
293+
.tc-expansion td {{
294+
padding: 0.25rem 0.6rem;
295+
}}
199296
footer {{
200297
margin-top: 3rem;
201298
padding-top: 1rem;
@@ -233,8 +330,7 @@ def render_html(packages: list[dict], toolchains: list[dict]) -> str:
233330
<thead>
234331
<tr>
235332
<th>Package</th>
236-
<th>Default</th>
237-
<th>Available Versions</th>
333+
<th>Versions</th>
238334
</tr>
239335
</thead>
240336
<tbody>
@@ -246,7 +342,7 @@ def render_html(packages: list[dict], toolchains: list[dict]) -> str:
246342
<thead>
247343
<tr>
248344
<th>Toolchain</th>
249-
<th>Included Packages</th>
345+
<th>Versions</th>
250346
</tr>
251347
</thead>
252348
<tbody>

0 commit comments

Comments
 (0)