Files
website/tools/check_links.py
T

44 lines
1.8 KiB
Python
Executable File

#!/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 <pre> would look like attributes)
PRE = re.compile(r'<pre\b.*?</pre>', 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()