#!/usr/bin/env python3 """Rewrite catalog hostnames in a built ./site tree (HTML + PDF annotations). Catalog HTML links are already relative. PDF annotations used to be baked as https://zapier.georgelambert.org/… — this script retargets those (and any HTML that still names a docs origin) when the tree is hosted on another name, e.g. docs.verae-time.net. It does **not** rewrite live service doors (portal/api/iam/…) or Forgejo. Examples: # Lab catalog → future docs host (keep absolute URLs) python3 scripts/rewrite-docs-host.py \\ --site ./site \\ --from https://zapier.georgelambert.org \\ --to https://docs.verae-time.net # Drop the host entirely (path-relative PDF links; any hostname at site root) python3 scripts/rewrite-docs-host.py --site ./site --relative python3 scripts/rewrite-docs-host.py --site ./site --from … --to … --dry-run """ from __future__ import annotations import argparse import os import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] def _norm_origin(url: str) -> str: url = url.strip() if not url: return "" if "://" not in url: url = "https://" + url return url.rstrip("/") + "/" def rewrite_html(site: Path, src: str, dst: str, dry: bool) -> int: n = 0 src_bare = src.rstrip("/") dst_bare = dst.rstrip("/") or "/" for path in site.rglob("*"): if not path.is_file() or path.suffix.lower() not in {".html", ".htm", ".xml", ".svg", ".js", ".css", ".md"}: continue text = path.read_text(encoding="utf-8", errors="replace") if src not in text and src_bare not in text: continue new = text.replace(src, dst).replace(src_bare, dst_bare) if new == text: continue n += 1 if dry: print(f" html {path.relative_to(site)}") else: path.write_text(new, encoding="utf-8") return n def rewrite_pdfs(site: Path, src: str, dst: str | None, dry: bool) -> int: try: from pypdf import PdfReader, PdfWriter from pypdf.generic import NameObject, create_string_object except ImportError: print("pypdf missing; pip install pypdf", file=sys.stderr) return 0 n_pdf = 0 n_uri = 0 src_http = src.replace("https://", "http://", 1) if src.startswith("https://") else src for pdf in site.rglob("*.pdf"): try: reader = PdfReader(str(pdf)) except Exception: continue changed = False 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) new = raw if raw.startswith(src) or raw.startswith(src_http): rest = raw[len(src) :] if raw.startswith(src) else raw[len(src_http) :] if dst: new = dst + rest else: dest = site / rest.split("#", 1)[0] frag = "#" + rest.split("#", 1)[1] if "#" in rest else "" if not dest.exists(): dest = site / "index.html" rel = os.path.relpath(dest.resolve(), pdf.parent.resolve()) new = Path(rel).as_posix() + frag if new != raw: action[NameObject("/URI")] = create_string_object(new) changed = True n_uri += 1 if changed: n_pdf += 1 if dry: print(f" pdf {pdf.relative_to(site)}") else: writer = PdfWriter(clone_from=reader) tmp = pdf.with_suffix(".pdf.tmp") with tmp.open("wb") as fh: writer.write(fh) tmp.replace(pdf) print(f"pdf annotations {n_uri} in {n_pdf} files") return n_pdf def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--site", type=Path, default=ROOT / "site", help="built catalog directory") ap.add_argument("--from", dest="src", default="https://zapier.georgelambert.org", help="current docs origin") g = ap.add_mutually_exclusive_group(required=True) g.add_argument("--to", help="new docs origin, e.g. https://docs.verae-time.net") g.add_argument("--relative", action="store_true", help="convert docs-origin URLs to path-relative links") ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() site = args.site.resolve() if not site.is_dir(): print(f"no site at {site}", file=sys.stderr) return 1 src = _norm_origin(args.src) dst = None if args.relative else _norm_origin(args.to) print(f"{src} → {dst or 'relative paths'} ({site})") # HTML: root-relative `/…` so nested pages still hit the site root on any host. html_dst = "/" if dst is None else dst n_html = rewrite_html(site, src, html_dst, args.dry_run) print(f"html/text files {n_html}") rewrite_pdfs(site, src, dst, args.dry_run) if args.dry_run: print("dry-run; nothing written") return 0 if __name__ == "__main__": sys.exit(main())