"""Post-build processing: fix .md links and generate download zips.
1. Fix .md → .html links in built HTML files.
Raw HTML <a href="...md"> tags inside .md source files are preserved
as-is by myst-parser, causing broken navigation in the browser.
2. Generate filtered download zips:
_build/html/download_all.zip — all source .md/.rst files (original, unfiltered)
_build/html/download_950.zip — only 950-supported .md/.rst files,
with product support tables stripped to 950-only rows
"""
import json
import re
import urllib.parse
import zipfile
from pathlib import Path
HREF_MD_RE = re.compile(r'href="([^"]+)\.md(#(?:.*?))?"', re.IGNORECASE)
def fix_md_links(html_dir: Path) -> tuple[int, int]:
"""Replace href="*.md" with href="*.html" in all HTML files.
Returns (files_changed, total_replacements).
"""
if not html_dir.is_dir():
return 0, 0
total_fixed = 0
total_files = 0
for html_file in html_dir.rglob('*.html'):
content = html_file.read_text(encoding='utf-8')
new_content, count = HREF_MD_RE.subn(r'href="\1.html\2"', content)
if count:
html_file.write_text(new_content, encoding='utf-8')
total_files += 1
total_fixed += count
return total_files, total_fixed
EXCLUDE_DIRS = {'context', 'figures', '_build', '_static', '_templates', '.git', '__pycache__'}
def filter_md_for_950(content: str) -> str:
"""Remove non-950 product rows from all '产品支持情况' tables in *content*.
For each ``## 产品支持情况`` section, locate the ``<tbody>`` and keep
only the ``<tr>`` whose first ``<td>`` contains *both* "950PR" and "950DT".
The ``<thead>`` header row is always preserved.
If no 950 row is found, the section is left untouched.
"""
def _replace_section(match: re.Match) -> str:
section = match.group(0)
tbody_m = re.search(
r"<tbody[^>]*>(.*?)</tbody>", section, re.DOTALL | re.IGNORECASE
)
if not tbody_m:
return section
tbody_content = tbody_m.group(1)
rows = re.findall(
r"<tr\b.*?</tr>", tbody_content, re.DOTALL | re.IGNORECASE
)
kept: list[str] = []
for row in rows:
td_m = re.search(
r"<td[^>]*>(.*?)</td>", row, re.DOTALL | re.IGNORECASE
)
if td_m:
td_text = re.sub(r"<[^>]+>", "", td_m.group(1)).strip()
if "950PR" in td_text and "950DT" in td_text:
kept.append(row)
break
if not kept:
return section
prefix = section[: tbody_m.start()]
suffix = section[tbody_m.end():]
return prefix + "<tbody>" + "".join(kept) + "</tbody>" + suffix
return re.sub(
r"## 产品支持情况.*?(?=\n## |\Z)",
_replace_section,
content,
flags=re.DOTALL,
)
def _find_source_file(rel_path: str, src_root: Path) -> Path | None:
for ext in (".md", ".rst"):
candidate = src_root / (rel_path + ext)
if candidate.exists():
return candidate
return None
def load_950_supported_sources(json_path: Path, src_root: Path) -> set[Path]:
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
supported: set[Path] = set()
for key, is_supported in data.items():
if not is_supported:
continue
html_name = urllib.parse.unquote(key)
rel_path = html_name.rsplit(".", 1)[0]
src_file = _find_source_file(rel_path, src_root)
if src_file:
supported.add(src_file)
return supported
def collect_all_sources(src_root: Path) -> list[Path]:
"""Collect all .md/.rst source files, excluding build/config directories."""
sources = []
for ext in ('.md', '.rst'):
for f in sorted(src_root.rglob(f'*{ext}')):
if any(p in EXCLUDE_DIRS for p in f.parts):
continue
sources.append(f)
return sources
def build_src_zip(
out_path: Path,
source_files: list[Path],
src_root: Path,
filter_fn=None,
) -> None:
"""Create a zip archive of source .md/.rst files.
Args:
out_path: Destination zip file path.
source_files: List of source file paths to include.
src_root: Root directory for computing relative arcnames.
filter_fn: Optional callable(str) -> str to transform .md content before zipping.
"""
out_path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as zf:
for src_file in sorted(source_files):
arcname = str(src_file.relative_to(src_root))
if filter_fn and src_file.suffix == ".md":
content = src_file.read_text(encoding="utf-8")
filtered = filter_fn(content)
zf.writestr(arcname, filtered)
else:
zf.write(src_file, arcname)
def main():
src_root = Path(__file__).resolve().parent.parent
html_dir = src_root / "_build" / "dirhtml"
if not html_dir.is_dir():
html_dir = src_root / "_build" / "html"
json_path = src_root / "_static" / "version_filter.json"
if not html_dir.is_dir():
return
files, fixed = fix_md_links(html_dir)
all_sources = collect_all_sources(src_root)
all_zip = html_dir / "download_all.zip"
build_src_zip(all_zip, all_sources, src_root)
supported = load_950_supported_sources(json_path, src_root)
zip_950 = html_dir / "download_950.zip"
supported_list = sorted(supported)
build_src_zip(zip_950, supported_list, src_root, filter_fn=filter_md_for_950)
if __name__ == "__main__":
main()