check_course_docs.py 2.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. #!/usr/bin/env python3
  2. """Check required lesson sections and local Markdown links."""
  3. from __future__ import annotations
  4. import re
  5. import sys
  6. from pathlib import Path
  7. REQUIRED = (
  8. "本节目标",
  9. "前置知识",
  10. "问题动机",
  11. "核心",
  12. "实现指南",
  13. "扩展与联系",
  14. "精选例题",
  15. "总结与自检",
  16. "课后训练建议",
  17. )
  18. def main() -> int:
  19. root = Path(sys.argv[1] if len(sys.argv) > 1 else "课程")
  20. errors: list[str] = []
  21. lessons = [
  22. path
  23. for path in sorted(root.rglob("*.md"))
  24. if path.name != "README.md" and not path.name.startswith("00-")
  25. ]
  26. for path in lessons:
  27. text = path.read_text(encoding="utf-8")
  28. headings = "\n".join(re.findall(r"^#{1,6}\s+(.+)$", text, flags=re.MULTILINE))
  29. for required in REQUIRED:
  30. if required not in headings:
  31. errors.append(f"{path}: missing section containing {required!r}")
  32. if "<details>" not in text:
  33. errors.append(f"{path}: no folded hints/self-check content")
  34. if re.search(r"```(?:cpp|c\+\+)\n.*?\bint\s+main\s*\(", text, flags=re.DOTALL):
  35. errors.append(f"{path}: standalone C++ implementation must be an attachment")
  36. if re.search(r"^\\\[\s*$|^\\\]\s*$", text, flags=re.MULTILINE):
  37. errors.append(f"{path}: display LaTeX must use $$ delimiters, not \\[ or \\]")
  38. for path in sorted(root.rglob("*.md")):
  39. text = path.read_text(encoding="utf-8")
  40. prose = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
  41. for target in re.findall(r"\[[^]]*\]\(([^)]+)\)", prose):
  42. target = target.split("#", 1)[0]
  43. if not target or re.match(r"(?:https?|mailto):", target):
  44. continue
  45. resolved = (path.parent / target).resolve()
  46. if not resolved.exists():
  47. errors.append(f"{path}: broken local link {target}")
  48. for error in errors:
  49. print(error, file=sys.stderr)
  50. print(f"lessons={len(lessons)} errors={len(errors)}")
  51. return 1 if errors else 0
  52. if __name__ == "__main__":
  53. raise SystemExit(main())