#!/usr/bin/env python3 """Check local Markdown links and anchors that should resolve in this repository.""" from __future__ import annotations import re import sys import urllib.parse from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SKIP_PARTS = {".git", ".history", "node_modules"} SKIP_PREFIXES = [ Path(".github/wiki"), Path("tools/external"), Path("tools/chat-vault"), ] LINK_PATTERNS = [ re.compile(r"!??\[[^\]]*\]\(([^)]+)\)"), re.compile(r"\b(?:href|src)=[\"']([^\"']+)[\"']"), ] EXTERNAL_PREFIXES = ( "http://", "https://", "wss://", "ws://", "mailto:", "tel:", "data:", ) def github_slug(title: str) -> str: """Approximate GitHub Markdown heading anchors for local validation.""" title = re.sub(r"<[^>]+>", "", title.strip().lower()) title = re.sub(r"[`*_~]", "", title) title = re.sub(r"[^\w\u4e00-\u9fff\- ]+", "", title) title = re.sub(r"\s+", "-", title).strip("-") return title def strip_fenced_code(text: str) -> str: """Remove fenced code blocks before scanning inline links.""" lines: list[str] = [] in_fence = False fence_marker = "" for line in text.splitlines(): stripped = line.lstrip() if stripped.startswith(("```", "~~~")): marker = stripped[:3] if not in_fence: in_fence = True fence_marker = marker elif marker == fence_marker: in_fence = False fence_marker = "" lines.append("") continue lines.append("" if in_fence else line) return "\n".join(lines) def markdown_anchors(path: Path) -> set[str]: text = path.read_text(encoding="utf-8", errors="ignore") anchors = set(re.findall(r" bool: rel = path.relative_to(ROOT) if any(part in SKIP_PARTS for part in rel.parts): return True if is_research_repository_snapshot(rel): return True return any(rel == prefix or prefix in rel.parents for prefix in SKIP_PREFIXES) def is_research_repository_snapshot(rel: Path) -> bool: parts = rel.parts return ( len(parts) >= 4 and parts[0] == "research" and parts[2] == "raw" and parts[3] == "repository" ) def normalize_target(raw: str) -> tuple[str, str]: raw = raw.strip() if not raw or raw.startswith(EXTERNAL_PREFIXES): return "", "" target = raw.split()[0] if " " in raw and not raw.startswith("<") else raw target = target.strip("<>") if "#" in target: path_part, anchor = target.split("#", 1) else: path_part, anchor = target, "" return urllib.parse.unquote(path_part), urllib.parse.unquote(anchor) def main() -> int: missing_links: list[tuple[Path, str, Path]] = [] missing_anchors: list[tuple[Path, str, Path, str]] = [] anchor_cache: dict[Path, set[str]] = {} checked_files = 0 for markdown_file in sorted(ROOT.rglob("*.md")): if should_skip(markdown_file): continue checked_files += 1 text = markdown_file.read_text(encoding="utf-8", errors="ignore") scan_text = strip_fenced_code(text) for pattern in LINK_PATTERNS: for match in pattern.finditer(scan_text): raw = match.group(1) target, anchor = normalize_target(raw) if target.startswith(("/", "\\")): continue destination = (markdown_file.parent / target).resolve() if target else markdown_file.resolve() try: destination.relative_to(ROOT) except ValueError: missing_links.append((markdown_file.relative_to(ROOT), raw, destination)) continue if not destination.exists(): missing_links.append((markdown_file.relative_to(ROOT), raw, markdown_file.parent / target)) continue if anchor and destination.suffix.lower() == ".md": if destination not in anchor_cache: anchor_cache[destination] = markdown_anchors(destination) if anchor not in anchor_cache[destination]: missing_anchors.append((markdown_file.relative_to(ROOT), raw, destination.relative_to(ROOT), anchor)) if missing_links or missing_anchors: print("MISSING_LINKS") for source, raw, resolved in missing_links: try: display_path = resolved.relative_to(ROOT) except ValueError: display_path = resolved print(f"{source} -> {raw} => {display_path}") print("MISSING_ANCHORS") for source, raw, destination, anchor in missing_anchors: print(f"{source} -> {raw} => {destination}#{anchor}") print(f"TOTAL_LINKS={len(missing_links)}") print(f"TOTAL_ANCHORS={len(missing_anchors)}") return 1 print(f"OK local links and anchors checked: {checked_files} files") return 0 if __name__ == "__main__": sys.exit(main())