Make catalog PDF links host-relative and add a hostname rewrite script.
Some checks are pending
offline / test (push) Waiting to run
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.
This commit is contained in:
parent
85b5b53ea9
commit
20155de96b
4 changed files with 246 additions and 40 deletions
150
scripts/rewrite-docs-host.py
Executable file
150
scripts/rewrite-docs-host.py
Executable file
|
|
@ -0,0 +1,150 @@
|
|||
#!/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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue