Some checks are pending
offline / test (push) Waiting to run
New builds write path-relative PDF annotations so the tree can be served from docs.verae-time.net (or any host at the site root) without a rebuild. scripts/rewrite-docs-host.py retargets an already-built site when absolute URLs are preferred. Live service doors stay on their own hostnames.
131 lines
4 KiB
Python
131 lines
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
|
|
|
|
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 _exists(dest: Path) -> bool:
|
|
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
|
|
return False
|
|
|
|
|
|
def local_ok(url: str) -> bool:
|
|
if url.startswith(WEB) or url.startswith("http://zapier.georgelambert.org/"):
|
|
rel = url.split("://", 1)[-1].split("/", 1)[-1].split("#", 1)[0]
|
|
return _exists(SITE / rel)
|
|
if url.startswith("file:"):
|
|
return False
|
|
return True # http(s) off-site; homepage checker uses GET separately
|
|
|
|
|
|
def pdf_dest_ok(pdf: Path, raw: str) -> bool:
|
|
raw = raw.split("#", 1)[0]
|
|
if raw.startswith("file:"):
|
|
return False
|
|
if raw.startswith("mailto:") or raw.startswith("data:"):
|
|
return True
|
|
if raw.startswith("http://") or raw.startswith("https://"):
|
|
if raw.startswith(WEB) or raw.startswith("http://zapier.georgelambert.org/"):
|
|
return local_ok(raw)
|
|
return True
|
|
if raw.startswith("/"):
|
|
dest = SITE / raw.lstrip("/")
|
|
else:
|
|
dest = (pdf.parent / raw).resolve()
|
|
try:
|
|
dest.relative_to(SITE.resolve())
|
|
except ValueError:
|
|
return False
|
|
return _exists(dest)
|
|
|
|
|
|
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 "/research/" in raw:
|
|
continue
|
|
if not pdf_dest_ok(pdf, raw):
|
|
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())
|