Skip to content

Commit 4a648d4

Browse files
committed
Fix mdBook navigation with absolute GitHub Pages URLs.
Rewrite internal href/src to /deepseek-tech-notes/ + encoded paths so sidebar and content links never escape the project prefix.
1 parent 65635eb commit 4a648d4

1 file changed

Lines changed: 98 additions & 35 deletions

File tree

scripts/fix_mdbook_paths.py

Lines changed: 98 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,145 @@
11
#!/usr/bin/env python3
22
"""Post-process mdBook HTML for GitHub Pages project sites.
33
4-
1. Replace root index.html with redirect to the real first chapter (avoids ``../``
5-
resolving outside ``/deepseek-tech-notes/``).
6-
2. Percent-encode non-ASCII path segments in relative href/src (GitHub Pages 400).
4+
GitHub Pages serves this book at ``/deepseek-tech-notes/``. mdBook emits
5+
relative ``../`` links that escape the project prefix on root/index pages, and
6+
unencoded CJK path segments return 400. Rewrite internal links to absolute,
7+
percent-encoded URLs under site-url and fix search index paths to match.
78
"""
89
from __future__ import annotations
910

11+
import json
1012
import re
11-
from pathlib import Path
12-
from urllib.parse import unquote, quote
13+
from pathlib import Path, PurePosixPath
14+
from urllib.parse import quote, unquote
1315

1416
REPO = Path(__file__).resolve().parents[1]
1517
OUT = REPO / "mdbook-out"
16-
FIRST_CHAPTER = "00-%E5%89%8D%E8%A8%80/02-%E4%B8%AD%E6%96%87%E5%AF%BC%E8%AF%B2.html"
18+
BOOK_TOML = REPO / "book.toml"
1719

18-
SKIP_PREFIXES = ("http://", "https://", "//", "#", "mailto:", "javascript:", "data:")
20+
SKIP_PREFIXES = ("http://", "https://", "//", "mailto:", "javascript:", "data:")
1921
ATTR_RE = re.compile(r'(href|src)="([^"]+)"')
22+
PATH_TO_ROOT_RE = re.compile(r'var path_to_root = "[^"]*";')
2023

2124

22-
def encode_path(path: str) -> str:
23-
if not path or path.startswith(SKIP_PREFIXES):
24-
return path
25+
def read_site_url() -> str:
26+
text = BOOK_TOML.read_text(encoding="utf-8")
27+
m = re.search(r'^site-url\s*=\s*"([^"]+)"', text, re.MULTILINE)
28+
if not m:
29+
raise SystemExit("book.toml missing output.html site-url")
30+
url = m.group(1)
31+
return url if url.endswith("/") else url + "/"
32+
33+
34+
SITE_URL = read_site_url()
35+
36+
37+
def encode_segment(part: str) -> str:
38+
if part in (".", "..", ""):
39+
return part
40+
return quote(unquote(part), safe="")
41+
42+
43+
def encode_book_rel(rel: str) -> str:
2544
frag = ""
26-
if "#" in path:
27-
path, frag = path.split("#", 1)
45+
if "#" in rel:
46+
rel, frag = rel.split("#", 1)
2847
frag = "#" + frag
29-
parts = path.split("/")
30-
encoded = "/".join(
31-
quote(unquote(part), safe="") if part not in (".", "..", "") else part
32-
for part in parts
33-
)
48+
encoded = "/".join(encode_segment(p) for p in rel.split("/") if p)
3449
return encoded + frag
3550

3651

52+
def site_href(rel: str) -> str:
53+
return SITE_URL + encode_book_rel(rel)
54+
55+
56+
def resolve_relative(href: str, page_dir: PurePosixPath) -> str | None:
57+
if not href or href.startswith(SKIP_PREFIXES):
58+
return None
59+
if href.startswith(SITE_URL):
60+
return href
61+
frag = ""
62+
if "#" in href:
63+
href, frag = href.split("#", 1)
64+
frag = "#" + frag
65+
if href.startswith("/"):
66+
rel = href.lstrip("/")
67+
else:
68+
rel = PurePosixPath(page_dir, href).as_posix()
69+
parts: list[str] = []
70+
for part in rel.split("/"):
71+
if part == "..":
72+
if parts:
73+
parts.pop()
74+
elif part not in (".", ""):
75+
parts.append(part)
76+
if not parts:
77+
return None
78+
return site_href("/".join(parts)) + frag
79+
80+
81+
def patch_html(path: Path) -> None:
82+
page_dir = PurePosixPath(path.relative_to(OUT).parent.as_posix())
83+
if page_dir.parts == (".",):
84+
page_dir = PurePosixPath()
85+
86+
def repl(m: re.Match[str]) -> str:
87+
attr, val = m.group(1), m.group(2)
88+
new = resolve_relative(val, page_dir)
89+
return m.group(0) if new is None else f'{attr}="{new}"'
90+
91+
text = path.read_text(encoding="utf-8")
92+
text = ATTR_RE.sub(repl, text)
93+
text = PATH_TO_ROOT_RE.sub(f'var path_to_root = "{SITE_URL}";', text)
94+
path.write_text(text, encoding="utf-8")
95+
96+
3797
def write_redirect_index() -> None:
98+
target = site_href("00-前言/02-中文导读.html")
3899
html = f"""<!DOCTYPE html>
39100
<html lang="zh-Hans">
40101
<head>
41102
<meta charset="utf-8">
42103
<title>Redirecting…</title>
43-
<link rel="canonical" href="{FIRST_CHAPTER}">
44-
<meta http-equiv="refresh" content="0; url={FIRST_CHAPTER}">
45-
<script>location.replace("{FIRST_CHAPTER}");</script>
104+
<link rel="canonical" href="{target}">
105+
<meta http-equiv="refresh" content="0; url={target}">
106+
<script>location.replace("{target}");</script>
46107
</head>
47108
<body>
48-
<p><a href="{FIRST_CHAPTER}">中文导读</a></p>
109+
<p><a href="{target}">中文导读</a></p>
49110
</body>
50111
</html>
51112
"""
52113
(OUT / "index.html").write_text(html, encoding="utf-8")
53114

54115

55-
def patch_html(path: Path) -> int:
56-
text = path.read_text(encoding="utf-8")
57-
58-
def repl(m: re.Match[str]) -> str:
59-
attr, val = m.group(1), m.group(2)
60-
new = encode_path(val)
61-
return m.group(0) if new == val else f'{attr}="{new}"'
62-
63-
patched = ATTR_RE.sub(repl, text)
64-
if patched != text:
65-
path.write_text(patched, encoding="utf-8")
66-
return text.count('href="') + text.count('src="')
116+
def patch_searchindex() -> None:
117+
path = OUT / "searchindex.json"
118+
if not path.is_file():
119+
return
120+
data = json.loads(path.read_text(encoding="utf-8"))
121+
data["doc_urls"] = [encode_book_rel(u) for u in data["doc_urls"]]
122+
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
123+
js = OUT / "searchindex.js"
124+
if js.is_file():
125+
js.write_text(
126+
"Object.assign(window.search, " + json.dumps(data, ensure_ascii=False) + ");",
127+
encoding="utf-8",
128+
)
67129

68130

69131
def main() -> None:
70132
if not OUT.is_dir():
71133
raise SystemExit(f"missing {OUT.relative_to(REPO)} — run mdbook build first")
72-
write_redirect_index()
73134
n = 0
74135
for html in OUT.rglob("*.html"):
75136
if html.parent == OUT and html.name == "index.html":
76137
continue
77138
patch_html(html)
78139
n += 1
79-
print(f"OK fix_mdbook_paths: redirect index + encoded paths in {n} html files")
140+
write_redirect_index()
141+
patch_searchindex()
142+
print(f"OK fix_mdbook_paths: absolute URLs under {SITE_URL} ({n} chapter html files)")
80143

81144

82145
if __name__ == "__main__":

0 commit comments

Comments
 (0)