Skip to content

Commit b76ce38

Browse files
committed
add explicit nav
add script building explicit `nav.yml` from `.pages` files wire `nav.yml` into config (`INHERIT`)
1 parent 6bfa15d commit b76ce38

6 files changed

Lines changed: 703 additions & 16 deletions

File tree

.claude/settings.local.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(git log *)",
5+
"WebFetch(domain:zensical.org)",
6+
"WebSearch",
7+
"Bash(gh release *)"
8+
]
9+
}
10+
}

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
INHERIT: nav.yml
23
# {{{ Main site config
34
site_name: documentation.eccenca.com
45
site_url: https://documentation.eccenca.com/

nav.yml

Lines changed: 571 additions & 0 deletions
Large diffs are not rendered by default.

poetry.lock

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ mkdocs-material = {git = "git@github.com:eccenca/mkdocs-material-insiders.git",
2323
cmem-cmempy = "^25.3.0"
2424
pydantic = "^2.11.7"
2525
jinja2 = "^3.1.6"
26-
zensical = "^0.0.42"
26+
zensical = "^0.0.43"
2727

2828
[tool.poetry.group.dev.dependencies]
2929
linkcheckmd = "^1.4.0"

tools/build_nav.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Build nav.yml from docs/.pages files recursively."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from pathlib import Path
7+
8+
import yaml
9+
10+
DOCS_DIR = Path("docs")
11+
12+
13+
def read_pages(directory: Path) -> dict | None:
14+
pf = directory / ".pages"
15+
if not pf.exists():
16+
return None
17+
text = pf.read_text(encoding="utf-8").strip()
18+
return yaml.safe_load(text) if text else None
19+
20+
21+
def expand_dir(title: str | None, abs_dir: Path, docs_rel: Path) -> object:
22+
"""Expand a directory into nav items using its .pages file if present."""
23+
pages = read_pages(abs_dir)
24+
if pages and "nav" in pages and pages["nav"]:
25+
children = build_nav_list(pages["nav"], abs_dir, docs_rel)
26+
return {title: children} if title is not None else children
27+
# No nav — emit a bare directory reference (zensical auto-discovers)
28+
path_str = str(docs_rel).replace("\\", "/")
29+
return {title: path_str} if title is not None else path_str
30+
31+
32+
def expand_item(item: object, abs_base: Path, docs_rel_base: Path) -> object:
33+
"""Convert one .pages nav entry to MkDocs nav format."""
34+
if isinstance(item, str):
35+
# Plain path, no title
36+
abs_path = abs_base / item
37+
docs_rel = docs_rel_base / item
38+
if abs_path.is_dir():
39+
# Inherit title from sub-directory's .pages `title:` key if present
40+
sub = read_pages(abs_path)
41+
inherited_title = sub.get("title") if sub else None
42+
return expand_dir(inherited_title, abs_path, docs_rel)
43+
return str(docs_rel).replace("\\", "/")
44+
45+
if isinstance(item, dict):
46+
assert len(item) == 1, f"Expected single-key dict, got: {item}"
47+
title, value = next(iter(item.items()))
48+
49+
if isinstance(value, list):
50+
# Inline section: "Title: [children]"
51+
children = []
52+
for child in value:
53+
resolved = expand_item(child, abs_base, docs_rel_base)
54+
if resolved is not None:
55+
if isinstance(resolved, list):
56+
children.extend(resolved)
57+
else:
58+
children.append(resolved)
59+
return {title: children}
60+
61+
if isinstance(value, str):
62+
abs_path = abs_base / value
63+
docs_rel = docs_rel_base / value
64+
if abs_path.is_dir():
65+
return expand_dir(title, abs_path, docs_rel)
66+
return {title: str(docs_rel).replace("\\", "/")}
67+
68+
return None
69+
70+
71+
def build_nav_list(nav: list, abs_dir: Path, docs_rel: Path) -> list:
72+
result = []
73+
for item in nav:
74+
resolved = expand_item(item, abs_dir, docs_rel)
75+
if resolved is None:
76+
continue
77+
if isinstance(resolved, list):
78+
result.extend(resolved)
79+
else:
80+
result.append(resolved)
81+
return result
82+
83+
84+
def main() -> None:
85+
pages = read_pages(DOCS_DIR)
86+
if not pages or "nav" not in pages:
87+
print("ERROR: docs/.pages missing or has no nav: block", file=sys.stderr)
88+
sys.exit(1)
89+
90+
nav = build_nav_list(pages["nav"], DOCS_DIR, Path(""))
91+
92+
# Dump with a custom representer that keeps strings unquoted where safe
93+
# and preserves unicode (e.g.   in titles)
94+
output = yaml.dump(
95+
{"nav": nav},
96+
default_flow_style=False,
97+
allow_unicode=True,
98+
width=120,
99+
indent=2,
100+
)
101+
print(output, end="")
102+
103+
104+
if __name__ == "__main__":
105+
main()

0 commit comments

Comments
 (0)