#!/usr/bin/env python3 """Check that every internal link and image of the built site points to an existing file. Usage: tools/check_links.py [public] (exit status 1 when something is broken) """ import html import re import sys from pathlib import Path from urllib.parse import unquote, urldefrag, urlparse # minified pages drop the quotes around simple attribute values ATTR = re.compile(r'\b(?:href|src)=(?:"([^"]*)"|\'([^\']*)\'|([^\s>"\']+))', re.I) # code samples show markup, they do not link (escaped HTML in a
 would look like attributes)
PRE = re.compile(r'', re.I | re.S)


def main():
    root = Path(sys.argv[1] if len(sys.argv) > 1 else "public").resolve()
    broken, checked = {}, 0
    for page in sorted(root.rglob("*.html")):
        text = PRE.sub("", page.read_text(encoding="utf-8", errors="replace"))
        for url in {a or b or c for a, b, c in ATTR.findall(text)}:
            url = urldefrag(html.unescape(url))[0]
            parsed = urlparse(url)
            if not url or parsed.scheme or parsed.netloc:
                continue  # external link, mailto:, pure anchor
            path = unquote(parsed.path)
            target = (root / path.lstrip("/")) if path.startswith("/") else (page.parent / path)
            target = target.resolve()
            if target.is_dir():
                target = target / "index.html"
            checked += 1
            if not target.is_file():
                broken.setdefault(url, []).append(str(page.relative_to(root)))
    print(f"{checked} internal references checked, {len(broken)} broken")
    for url, pages in sorted(broken.items()):
        print(f"  {url}  (in {pages[0]}{f' and {len(pages) - 1} more' if len(pages) > 1 else ''})")
    sys.exit(1 if broken else 0)


if __name__ == "__main__":
    main()