Skip to content

Commit 8d4fa94

Browse files
authored
Update export_docs.py
1 parent 8a49677 commit 8d4fa94

1 file changed

Lines changed: 61 additions & 46 deletions

File tree

‎scripts/export_docs.py‎

Lines changed: 61 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66
docs/doc/<lang>/print.md, preserving the authoring syntax (admonitions, tabs,
77
attr_list, icons) so that Zensical renders it with the usual styling.
88
9-
To survive concatenation, every heading gets an explicit id prefixed with the
10-
page id (e.g. "{#2-2-settings}" on page 2.2.-Settings), and cross-page links
11-
are rewritten to same-document anchors pointing at those ids.
12-
13-
Image paths are rewritten from site-root-absolute (/images/...) to a path
14-
relative to the print page URL (/<lang>/print/ -> ../../images/...).
9+
Heading ids are prefixed with the page id (first h1 gets the bare page id) so
10+
that anchors stay unique after concatenation; cross-page links are rewritten
11+
to same-document anchors pointing at those ids. Site-root shortcut links
12+
(/latest, /community...) and their trailing attr_list are reduced to their
13+
label so no orphan `{ .md-button }` leaks into the rendered page.
1514
1615
Usage:
17-
python scripts/export_docs.py # -> print.md for every multi-page language
16+
python scripts/export_docs.py # every multi-page language
1817
python scripts/export_docs.py fr # -> docs/doc/fr/print.md only
1918
python scripts/export_docs.py fr -o t.md
2019
"""
@@ -34,26 +33,24 @@
3433
FRONT_MATTER_RE = re.compile(r"\A---\s*\n.*?\n---\s*\n", re.DOTALL)
3534
HEADING_RE = re.compile(r"^(#{1,6}) (.+?)\s*(\{[^}]*})?\s*$")
3635
ATTR_LIST_RE = re.compile(r"\s*(\{[^}]*})\s*$")
37-
SITE_LINK_RE = re.compile(r"\[([^]]+)]\(/[^)]*\)")
38-
DOC_LINK_RE = re.compile(
39-
r"\[([^]]+)]\(([^)#\s]+?)(?:\.md)?(?:/)?(?:#([^)]*))?\)")
36+
# Site-root shortcut link, optionally followed by an attr_list that would
37+
# otherwise leak as literal text once the link is reduced to its label.
38+
SITE_LINK_RE = re.compile(r"\[([^]]+)]\(/[^)]*\)(\s*\{[^}]*})?")
39+
DOC_LINK_RE = re.compile(r"\[([^]]+)]\(([^)#\s]+?)(?:\.md)?(?:/)?(?:#([^)]*))?\)")
4040
# Root-absolute image paths (/images/...) become relative to /<lang>/print/.
4141
IMAGE_RE = re.compile(r"(\!\[[^]]*]\()(/?images/)")
4242

4343

4444
def load_config() -> dict:
45-
"""Parse zensical.toml or exit with an error."""
4645
if not CONFIG_FILE.exists():
4746
sys.exit(f"error: {CONFIG_FILE} not found")
4847
with open(CONFIG_FILE, "rb") as fh:
4948
return tomllib.load(fh)
5049

5150

5251
def load_nav_entries(config: dict) -> list:
53-
"""Flat list of the top-level nav entries of zensical.toml."""
54-
entries = config["project"]["nav"]
5552
out = []
56-
for entry in entries:
53+
for entry in config["project"]["nav"]:
5754
out.extend(entry.values() if isinstance(entry, dict) else [entry])
5855
return out
5956

@@ -104,9 +101,8 @@ def collect_lang_counts(node, counts: dict[str, int]) -> None:
104101

105102
def nav_langs() -> dict[str, int]:
106103
"""Count the pages per language found in the nav of zensical.toml."""
107-
config = load_config()
108104
counts: dict[str, int] = {}
109-
for value in load_nav_entries(config):
105+
for value in load_nav_entries(load_config()):
110106
collect_lang_counts(value, counts)
111107
return counts
112108

@@ -130,36 +126,61 @@ def frontmatter_path(page: Path) -> str:
130126
return match.group(1) if match else page.stem
131127

132128

129+
def iter_lines(text: str):
130+
"""Yield (line, heading_match); headings inside fenced code are ignored."""
131+
fence = False
132+
for line in text.splitlines():
133+
if line.lstrip().startswith("```"):
134+
fence = not fence
135+
yield line, None
136+
continue
137+
yield line, (None if fence else HEADING_RE.match(line))
138+
139+
140+
def unique_id(base: str, used: set[str]) -> str:
141+
"""Return base, or base-1, base-2... if it was already used."""
142+
if base not in used:
143+
used.add(base)
144+
return base
145+
suffix = 1
146+
while f"{base}-{suffix}" in used:
147+
suffix += 1
148+
hid = f"{base}-{suffix}"
149+
used.add(hid)
150+
return hid
151+
152+
133153
def build_anchor_map(pages: list[Path]) -> dict[tuple[str, str | None], str]:
134-
"""Map (page key, heading or None) -> same-document anchor id.
154+
"""Map (page key, heading slug or None) -> same-document anchor id.
135155
136156
The page key accepts both the file stem and the front-matter 'path' value
137157
so that links written either way keep working.
138158
"""
139159
anchors: dict[tuple[str, str | None], str] = {}
140160
for page in pages:
141161
pid = page_id(page)
142-
for key in {page.stem, frontmatter_path(page)}:
162+
keys = {page.stem, frontmatter_path(page)}
163+
for key in keys:
143164
anchors[(key, None)] = pid
165+
text = FRONT_MATTER_RE.sub("", page.read_text(encoding="utf-8"), count=1)
144166
used: set[str] = set()
145-
for line in page.read_text(encoding="utf-8").splitlines():
146-
match = HEADING_RE.match(line)
167+
first_h1 = True
168+
for _, match in iter_lines(text):
147169
if not match:
148170
continue
149-
hid = f"{pid}-{slugify(match.group(2))}"
150-
if hid in used:
151-
suffix = 1
152-
while f"{hid}-{suffix}" in used:
153-
suffix += 1
154-
hid = f"{hid}-{suffix}"
155-
used.add(hid)
156-
for key in {page.stem, frontmatter_path(page)}:
171+
if first_h1 and len(match.group(1)) == 1:
172+
hid = pid
173+
first_h1 = False
174+
else:
175+
hid = f"{pid}-{slugify(match.group(2))}"
176+
hid = unique_id(hid, used)
177+
for key in keys:
157178
anchors[(key, slugify(match.group(2)))] = hid
158179
return anchors
159180

160181

161182
def rewrite_links(text: str, anchors: dict[tuple[str, str | None], str]) -> str:
162-
"""Rewrite cross-page Markdown links to same-document anchors."""
183+
"""Rewrite cross-page links to anchors; reduce shortcut links to labels."""
163184
def doc_link_sub(match: re.Match) -> str:
164185
label, target, heading = match.group(1), match.group(2), match.group(3)
165186
key = (target, slugify(heading) if heading else None)
@@ -172,25 +193,19 @@ def doc_link_sub(match: re.Match) -> str:
172193

173194

174195
def add_heading_ids(text: str, pid: str) -> str:
175-
"""Give every heading an explicit attr_list id prefixed with the page id."""
196+
"""Give every heading an explicit id; the first h1 becomes the page id."""
176197
used: set[str] = set()
198+
first_h1 = True
177199
out_lines: list[str] = []
178-
in_fence = False
179-
for line in text.splitlines():
180-
if line.lstrip().startswith("```"):
181-
in_fence = not in_fence
182-
out_lines.append(line)
183-
continue
184-
match = HEADING_RE.match(line) if not in_fence else None
200+
for line, match in iter_lines(text):
185201
if match:
186-
heading_id = f"{pid}-{slugify(match.group(2))}"
187-
if heading_id in used:
188-
suffix = 1
189-
while f"{heading_id}-{suffix}" in used:
190-
suffix += 1
191-
heading_id = f"{heading_id}-{suffix}"
192-
used.add(heading_id)
193-
out_lines.append(f"{match.group(1)} {match.group(2)} {{#{heading_id}}}")
202+
if first_h1 and len(match.group(1)) == 1:
203+
hid = pid
204+
first_h1 = False
205+
else:
206+
hid = f"{pid}-{slugify(match.group(2))}"
207+
hid = unique_id(hid, used)
208+
out_lines.append(f"{match.group(1)} {match.group(2)} {{#{hid}}}")
194209
else:
195210
out_lines.append(line)
196211
return "\n".join(out_lines)
@@ -218,7 +233,7 @@ def build_markdown(lang: str, pages: list[Path]) -> str:
218233
"""Concatenate the pages with a table of contents and a front-matter."""
219234
anchors = build_anchor_map(pages)
220235
parts = ["---", "path: print", "---", "",
221-
f"# Documentation QualCoder ({lang.upper()})", "",
236+
f"# Documentation QualCoder ({lang.upper()}) {{#top}}", "",
222237
"## Sommaire {#sommaire}", ""]
223238
for page in pages:
224239
parts.append(f"- [{page_title(page)}](#{page_id(page)})")

0 commit comments

Comments
 (0)