Some checks are pending
offline / test (push) Waiting to run
Module and model book cards now point at generated index.html/index.pdf instead of directory URLs that 404. WeasyPrint file:// annotations are mapped to https://zapier.georgelambert.org/ paths that exist. Developer module, architecture, and research snapshot links target published files.
111 lines
3.4 KiB
Python
111 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Check catalog homepage + PDF URI targets after a site build."""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from html.parser import HTMLParser
|
|
from pathlib import Path
|
|
from urllib.parse import unquote, urljoin, urlparse
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SITE = ROOT / "site"
|
|
WEB = "https://zapier.georgelambert.org/"
|
|
|
|
|
|
class Anchors(HTMLParser):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.hrefs: list[str] = []
|
|
|
|
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
if tag == "a":
|
|
d = dict(attrs)
|
|
if d.get("href"):
|
|
self.hrefs.append(d["href"] or "")
|
|
|
|
|
|
def local_ok(url: str) -> bool:
|
|
if url.startswith(WEB):
|
|
rel = url[len(WEB) :].split("#", 1)[0]
|
|
dest = SITE / rel
|
|
if dest.is_file():
|
|
return True
|
|
if dest.is_dir() and any(
|
|
(dest / n).exists() for n in ("index.html", "index.pdf", "README.html", "README.pdf")
|
|
):
|
|
return True
|
|
# yaml/svg/png on homepage
|
|
return False
|
|
if url.startswith("file:"):
|
|
return False
|
|
return True # http(s) off-site; homepage checker uses GET separately
|
|
|
|
|
|
def main() -> int:
|
|
idx = SITE / "index.html"
|
|
if not idx.exists():
|
|
print("no site/index.html — run scripts/build-docs-site.py")
|
|
return 1
|
|
p = Anchors()
|
|
p.feed(idx.read_text(encoding="utf-8", errors="replace"))
|
|
broken: list[str] = []
|
|
for h in sorted(set(p.hrefs)):
|
|
if h.startswith("#") or h.startswith("mailto:"):
|
|
continue
|
|
if h.startswith("https://git.georgelambert.org"):
|
|
continue
|
|
if h.startswith("https://") and not h.startswith(WEB):
|
|
continue
|
|
if h.startswith(WEB):
|
|
if not local_ok(h):
|
|
broken.append(f"homepage {h}")
|
|
continue
|
|
rel = h.lstrip("/")
|
|
dest = SITE / rel
|
|
if not dest.exists():
|
|
broken.append(f"homepage missing {h}")
|
|
|
|
from pypdf import PdfReader
|
|
|
|
n_file = 0
|
|
for pdf in SITE.rglob("*.pdf"):
|
|
try:
|
|
reader = PdfReader(str(pdf))
|
|
except Exception as exc:
|
|
broken.append(f"unreadable {pdf.relative_to(SITE)} {exc}")
|
|
continue
|
|
for page in reader.pages:
|
|
annots = page.get("/Annots")
|
|
if not annots:
|
|
continue
|
|
for annot in annots:
|
|
obj = annot.get_object()
|
|
action = obj.get("/A")
|
|
if not action:
|
|
continue
|
|
uri = action.get("/URI")
|
|
if not uri:
|
|
continue
|
|
raw = str(uri)
|
|
if raw.startswith("file:"):
|
|
n_file += 1
|
|
broken.append(f"file URI {pdf.relative_to(SITE)} -> {raw[:120]}")
|
|
continue
|
|
if raw.startswith(WEB):
|
|
path = raw.split("#", 1)[0]
|
|
if "/research/" in path:
|
|
continue
|
|
if not local_ok(path):
|
|
broken.append(f"pdf missing {pdf.relative_to(SITE)} -> {raw}")
|
|
|
|
print(f"file:// leftover {n_file}")
|
|
print(f"broken {len(broken)}")
|
|
for b in broken[:80]:
|
|
print(" ", b)
|
|
if len(broken) > 80:
|
|
print(f" … {len(broken) - 80} more")
|
|
return 1 if broken else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|