#!/usr/bin/env python3 """Fail when the static site contains broken or non-portable local references.""" from html.parser import HTMLParser from pathlib import Path from urllib.parse import unquote, urlsplit ROOT = Path(__file__).resolve().parents[1] SKIP_PREFIXES = ("#", "data:", "mailto:", "tel:", "javascript:") class PageParser(HTMLParser): def __init__(self): super().__init__() self.references = [] self.ids = set() self.has_viewport = False self.has_csp = False def handle_starttag(self, tag, attrs): values = dict(attrs) element_id = values.get("id") if element_id: self.ids.add(element_id) if tag == "meta" and values.get("name", "").lower() == "viewport": self.has_viewport = True if tag == "meta" and values.get("http-equiv", "").lower() == "content-security-policy": self.has_csp = True for attribute in ("href", "src", "action"): value = values.get(attribute) if value: self.references.append((tag, attribute, value)) def parse_page(path): parser = PageParser() parser.feed(path.read_text(encoding="utf-8")) return parser def main(): pages = sorted(ROOT.glob("*.html")) parsed = {page.resolve(): parse_page(page) for page in pages} errors = [] if not (ROOT / "index.html").is_file(): errors.append("missing GitHub Pages entry point: index.html") for page in pages: parser = parsed[page.resolve()] if not parser.has_viewport: errors.append(f"{page.name}: missing viewport metadata") if not parser.has_csp: errors.append(f"{page.name}: missing Content Security Policy") for tag, attribute, raw_value in parser.references: value = raw_value.strip() if not value or value.startswith(SKIP_PREFIXES): continue url = urlsplit(value) if url.scheme or url.netloc: if attribute != "href": errors.append(f"{page.name}: remote dependency in {attribute}: {value}") continue if url.path.startswith("/"): errors.append(f"{page.name}: root-absolute path breaks project Pages: {value}") continue target = (page.parent / unquote(url.path or page.name)).resolve() try: target.relative_to(ROOT) except ValueError: errors.append(f"{page.name}: reference leaves repository: {value}") continue if not target.exists(): errors.append(f"{page.name}: missing {tag} {attribute} target: {value}") continue if url.fragment and target.suffix.lower() == ".html": target_parser = parsed.get(target) if target_parser is None: target_parser = parse_page(target) parsed[target] = target_parser if unquote(url.fragment) not in target_parser.ids: errors.append(f"{page.name}: missing anchor target: {value}") if errors: print("Site validation failed:") for error in errors: print(f"- {error}") raise SystemExit(1) print(f"Site validation passed: {len(pages)} HTML pages checked.") if __name__ == "__main__": main()