Files
2026-07-13 13:13:17 +08:00

143 lines
4.9 KiB
Python

"""Rewrite `<!-- TIXL_MOVED_BANNER -->` blocks in the t3.wiki checkout to point at
the rendered help.tixl.app URLs instead of the GitHub source.
Reads the mapping table from `.agentic/Plans/Plan_UpdateHelp.md` (Section 5), so the
plan stays the single source of truth. Idempotent — runs safely multiple times.
Usage:
python .help/scripts/wiki/update_banners.py [--wiki-dir ../t3.wiki] [--dry-run]
The script expects a sibling t3.wiki checkout by default.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
PLAN_FILE = REPO_ROOT / ".agentic" / "Plans" / "Plan_UpdateHelp.md"
SITE_BASE = "https://help.tixl.app"
BANNER_MARKER = "<!-- TIXL_MOVED_BANNER -->"
# The banner block is the marker comment plus one contiguous run of lines starting
# with `>` (the blockquote). Everything after that first non-quote line is preserved.
_BANNER_BLOCK_RE = re.compile(
r"^" + re.escape(BANNER_MARKER) + r"\n((?:>.*\n)+)",
re.MULTILINE,
)
def _parse_mapping_table(plan_text: str) -> dict[str, str]:
"""Return {wiki_basename: rendered_url_path} from the plan's Section 5 table.
wiki_basename is the leftmost column stripped of backticks (e.g. `help.AddingFonts`).
rendered_url_path is the third column (e.g. `/advanced/AddingFonts/`). Rows with `—`
in the URL column are skipped (not-migrated pages).
"""
mapping: dict[str, str] = {}
in_table = False
for line in plan_text.splitlines():
stripped = line.strip()
if stripped.startswith("| Legacy wiki page"):
in_table = True
continue
if in_table:
if not stripped.startswith("|"):
break # table ended
# Skip the separator row (|---|---|---|)
if set(stripped.replace("|", "").strip()) <= {"-", " "}:
continue
cells = [c.strip() for c in stripped.strip("|").split("|")]
if len(cells) < 3:
continue
wiki_cell, _source_cell, url_cell = cells[0], cells[1], cells[2]
url_path = url_cell.strip("`").strip()
if url_path in ("—", "-", ""):
continue
wiki_name = wiki_cell.strip("`").strip()
if not wiki_name:
continue
mapping[wiki_name] = url_path
return mapping
def _new_banner(rendered_url_path: str) -> str:
full_url = SITE_BASE + rendered_url_path
return (
f"{BANNER_MARKER}\n"
f"> ⚠ **This page has moved.** The current version is at "
f"[{full_url.removeprefix('https://')}]({full_url}).\n"
f"> This wiki copy is no longer maintained — edits made here will be lost.\n"
)
def _rewrite_file(path: Path, rendered_url_path: str, *, dry_run: bool) -> str:
"""Returns one of: "updated", "already-current", "no-banner", "missing"."""
if not path.is_file():
return "missing"
text = path.read_text(encoding="utf-8")
if BANNER_MARKER not in text:
return "no-banner"
new_block = _new_banner(rendered_url_path)
replaced, count = _BANNER_BLOCK_RE.subn(new_block, text, count=1)
if count == 0:
# Marker present but block shape unexpected — surface it rather than guessing.
return "no-banner"
if replaced == text:
return "already-current"
if not dry_run:
path.write_text(replaced, encoding="utf-8")
return "updated"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--wiki-dir",
default=str(REPO_ROOT.parent / "t3.wiki"),
help="Path to the t3.wiki checkout (default: sibling of the repo root).",
)
parser.add_argument("--dry-run", action="store_true", help="Don't write files.")
args = parser.parse_args()
wiki_dir = Path(args.wiki_dir).resolve()
if not wiki_dir.is_dir():
print(f"error: wiki dir not found: {wiki_dir}", file=sys.stderr)
return 2
plan_text = PLAN_FILE.read_text(encoding="utf-8")
mapping = _parse_mapping_table(plan_text)
if not mapping:
print("error: no mapping rows parsed from plan", file=sys.stderr)
return 2
counts = {"updated": 0, "already-current": 0, "no-banner": 0, "missing": 0}
for wiki_name, url_path in sorted(mapping.items()):
wiki_path = wiki_dir / f"{wiki_name}.md"
status = _rewrite_file(wiki_path, url_path, dry_run=args.dry_run)
counts[status] += 1
marker = {"updated": "+", "already-current": "=", "no-banner": "!", "missing": "?"}[status]
print(f" {marker} {wiki_name}{url_path} [{status}]")
print()
print("Summary:")
for k, v in counts.items():
print(f" {k:>16}: {v}")
if args.dry_run:
print("(dry run — no files written)")
return 0 if counts["no-banner"] == 0 and counts["missing"] == 0 else 1
if __name__ == "__main__":
sys.exit(main())