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.
150 lines
5.5 KiB
Python
Executable file
150 lines
5.5 KiB
Python
Executable file
#!/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())
|