check_chevron_links.py 1.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. #!/usr/bin/env python3
  2. # Find <<anchor>> links whose target is not defined on the assembled page.
  3. # include::partial$... directives are expanded so chevrons inside partials are
  4. # checked against every page that includes them.
  5. from pathlib import Path
  6. import re
  7. import sys
  8. ROOT = Path(__file__).resolve().parent
  9. PAGES = ROOT / "pages"
  10. PARTIALS = ROOT / "partials"
  11. PARTIALS_ROOT = PARTIALS.resolve()
  12. INCLUDE_RE = re.compile(r"include::partial\$([^\[\]]+)\[([^\]]*)\]")
  13. CHEVRON_RE = re.compile(r"<<([^,>]+)(?:,[^>]*)?>>")
  14. def expand_partials(content, stack):
  15. def replace(match):
  16. rel = match.group(1)
  17. partial_path = (PARTIALS / rel).resolve()
  18. if not partial_path.is_relative_to(PARTIALS_ROOT):
  19. return ""
  20. if not partial_path.is_file() or partial_path in stack:
  21. return ""
  22. included = partial_path.read_text()
  23. return expand_partials(included, stack | {partial_path})
  24. return INCLUDE_RE.sub(replace, content)
  25. def count_anchors(content, anchor_id):
  26. escaped = re.escape(anchor_id)
  27. pattern = rf"\[(?:#|\[){escaped}(?=[,\]])"
  28. return len(re.findall(pattern, content))
  29. def find_unresolved(content):
  30. unresolved = []
  31. for match in CHEVRON_RE.finditer(content):
  32. anchor_id = match.group(1).strip()
  33. if count_anchors(content, anchor_id) == 1:
  34. continue
  35. if anchor_id not in unresolved:
  36. unresolved.append(anchor_id)
  37. return unresolved
  38. def main():
  39. filelist = {}
  40. for page in sorted(PAGES.rglob("*.adoc")):
  41. assembled = expand_partials(page.read_text(), set())
  42. missing = find_unresolved(assembled)
  43. if missing:
  44. filelist[str(page.relative_to(ROOT))] = missing
  45. print("Files:", len(filelist))
  46. for file in filelist:
  47. print(file)
  48. for match in filelist[file]:
  49. print("\t", match)
  50. sys.exit(1 if filelist else 0)
  51. if __name__ == "__main__":
  52. main()