"""Normalize the public version selector without rebuilding frozen docs."""
from __future__ import annotations
import argparse
import re
from pathlib import Path
SELECT_RE = re.compile(
r'<select\s+id="version-switcher"[^>]*>.*?</select>',
re.DOTALL,
)
OPTIONS = {
"zh": (
"SPINE V8(当前)",
"SPINE V6(维护)",
"SPINE V4(维护)",
"SPINE V3(历史)",
),
"en": (
"SPINE V8 (Current)",
"SPINE V6 (Maintenance)",
"SPINE V4 (Maintenance)",
"SPINE V3 (Legacy)",
),
}
def replacement(language: str) -> str:
v8, v6, v4, v3 = OPTIONS[language]
return (
'<select id="version-switcher" aria-label="Version switcher" '
'onchange="switchDocVersion(this.value)">\n'
f' <option value="v8">{v8}</option>\n'
f' <option value="v6">{v6}</option>\n'
f' <option value="v4">{v4}</option>\n'
f' <option value="v3">{v3}</option>\n'
" </select>"
)
def normalize_html(text: str, language: str) -> tuple[str, int]:
return SELECT_RE.subn(replacement(language), text)
def normalize_site(site_root: Path) -> dict[str, int]:
counts: dict[str, int] = {}
for version in ("v3", "v4", "v6", "v8"):
version_count = 0
for language in ("zh", "en"):
language_root = site_root / version / language
if not language_root.is_dir():
raise RuntimeError(f"Missing documentation tree: {language_root}")
for html_file in sorted(language_root.rglob("*.html")):
original = html_file.read_text(encoding="utf-8", errors="strict")
updated, replacements = normalize_html(original, language)
if replacements:
html_file.write_text(updated, encoding="utf-8", newline="")
version_count += replacements
if version_count == 0:
raise RuntimeError(f"No version selector found under {site_root / version}")
counts[version] = version_count
return counts
def verify_preserved_tree(site_root: Path, specification: str) -> None:
if "=" not in specification:
raise RuntimeError(
f"Invalid --verify-preserved value {specification!r}; expected VERSION=PATH"
)
version, raw_source = specification.split("=", 1)
if version not in {"v3", "v4", "v6"}:
raise RuntimeError(f"Unsupported preserved version: {version}")
source = Path(raw_source).resolve()
target = (site_root / version).resolve()
source_files = {
path.relative_to(source): path for path in source.rglob("*") if path.is_file()
}
target_files = {
path.relative_to(target): path for path in target.rglob("*") if path.is_file()
}
if source_files.keys() != target_files.keys():
missing = sorted(str(path) for path in source_files.keys() - target_files.keys())
extra = sorted(str(path) for path in target_files.keys() - source_files.keys())
raise RuntimeError(
f"Preserved {version} file set changed; missing={missing[:5]}, extra={extra[:5]}"
)
for relative_path, source_file in source_files.items():
target_file = target_files[relative_path]
if source_file.suffix.lower() == ".html":
language = relative_path.parts[0] if relative_path.parts else ""
if language not in OPTIONS:
expected = source_file.read_bytes()
else:
source_text = source_file.read_text(encoding="utf-8", errors="strict")
expected_text, _ = normalize_html(source_text, language)
expected = expected_text.encode("utf-8")
else:
expected = source_file.read_bytes()
if target_file.read_bytes() != expected:
raise RuntimeError(
f"Preserved {version} content changed outside version navigation: "
f"{relative_path}"
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("site_root", type=Path)
parser.add_argument(
"--verify-preserved",
action="append",
default=[],
metavar="VERSION=PATH",
)
args = parser.parse_args()
site_root = args.site_root.resolve()
counts = normalize_site(site_root)
for specification in args.verify_preserved:
verify_preserved_tree(site_root, specification)
summary = ", ".join(f"{version}={count}" for version, count in counts.items())
print(f"Version navigation normalized: {summary}")
if args.verify_preserved:
print("Frozen documentation verified: only version navigation changed")
return 0
if __name__ == "__main__":
raise SystemExit(main())