76 lines
2.2 KiB
Python
76 lines
2.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Check that relative markdown links point to files that exist.
|
|
|
|
Scans every tracked *.md file in the repo and validates relative link targets
|
|
(external URLs, anchors and mailto links are skipped; code blocks and inline
|
|
code spans are ignored). Exits 1 if any broken link is found.
|
|
|
|
Run locally: python3 scripts/check_md_links.py
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
LINK_RE = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)")
|
|
FENCED_CODE_RE = re.compile(r"```.*?```", re.DOTALL)
|
|
INLINE_CODE_RE = re.compile(r"`[^`\n]*`")
|
|
SKIP_PREFIXES = ("http://", "https://", "mailto:", "#", "<")
|
|
|
|
|
|
def repo_root() -> str:
|
|
return subprocess.check_output(
|
|
["git", "rev-parse", "--show-toplevel"], text=True
|
|
).strip()
|
|
|
|
|
|
def tracked_markdown_files(root: str) -> list[str]:
|
|
out = subprocess.check_output(["git", "ls-files", "*.md"], text=True, cwd=root)
|
|
return [line for line in out.splitlines() if line]
|
|
|
|
|
|
def main() -> int:
|
|
root = repo_root()
|
|
broken: list[str] = []
|
|
|
|
for rel in tracked_markdown_files(root):
|
|
path = os.path.join(root, rel)
|
|
try:
|
|
with open(path, encoding="utf-8") as fh:
|
|
text = fh.read()
|
|
except OSError:
|
|
continue
|
|
|
|
text = FENCED_CODE_RE.sub("", text)
|
|
text = INLINE_CODE_RE.sub("", text)
|
|
|
|
for match in LINK_RE.finditer(text):
|
|
target = match.group(1)
|
|
if target.startswith(SKIP_PREFIXES):
|
|
continue
|
|
file_part = target.split("#")[0].split("?")[0]
|
|
if not file_part:
|
|
continue
|
|
if file_part.startswith("/"):
|
|
resolved = os.path.join(root, file_part.lstrip("/"))
|
|
else:
|
|
resolved = os.path.normpath(
|
|
os.path.join(os.path.dirname(path), file_part)
|
|
)
|
|
if not os.path.exists(resolved):
|
|
broken.append(f"{rel}: {target}")
|
|
|
|
if broken:
|
|
print(f"{len(broken)} broken relative link(s):")
|
|
for entry in broken:
|
|
print(f" {entry}")
|
|
return 1
|
|
|
|
print("All relative markdown links resolve.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|