#!/usr/bin/env python3 """Check required lesson sections and local Markdown links.""" from __future__ import annotations import re import sys from pathlib import Path REQUIRED = ( "本节目标", "前置知识", "问题动机", "核心", "实现指南", "扩展与联系", "精选例题", "总结与自检", "课后训练建议", ) def main() -> int: root = Path(sys.argv[1] if len(sys.argv) > 1 else "课程") errors: list[str] = [] lessons = [ path for path in sorted(root.rglob("*.md")) if path.name != "README.md" and not path.name.startswith("00-") ] for path in lessons: text = path.read_text(encoding="utf-8") headings = "\n".join(re.findall(r"^#{1,6}\s+(.+)$", text, flags=re.MULTILINE)) for required in REQUIRED: if required not in headings: errors.append(f"{path}: missing section containing {required!r}") if "
" not in text: errors.append(f"{path}: no folded hints/self-check content") if re.search(r"```(?:cpp|c\+\+)\n.*?\bint\s+main\s*\(", text, flags=re.DOTALL): errors.append(f"{path}: standalone C++ implementation must be an attachment") if re.search(r"^\\\[\s*$|^\\\]\s*$", text, flags=re.MULTILINE): errors.append(f"{path}: display LaTeX must use $$ delimiters, not \\[ or \\]") for path in sorted(root.rglob("*.md")): text = path.read_text(encoding="utf-8") prose = re.sub(r"```.*?```", "", text, flags=re.DOTALL) for target in re.findall(r"\[[^]]*\]\(([^)]+)\)", prose): target = target.split("#", 1)[0] if not target or re.match(r"(?:https?|mailto):", target): continue resolved = (path.parent / target).resolve() if not resolved.exists(): errors.append(f"{path}: broken local link {target}") for error in errors: print(error, file=sys.stderr) print(f"lessons={len(lessons)} errors={len(errors)}") return 1 if errors else 0 if __name__ == "__main__": raise SystemExit(main())