#!/usr/bin/env python3 """Discover and verify optional template page examples; never write or activate a template.""" from __future__ import annotations import argparse from copy import deepcopy import json from pathlib import Path import re import sys from urllib.parse import unquote, urlsplit from xml.etree import ElementTree as ET from template_ref import (InputError, VERSION, digest, file_path, identifier, input_hashes, load_manifest, read_json, require, string, strings) def checked_file(root: Path, record: object, label: str) -> Path: require(isinstance(record, dict), f"{label}: expected path/sha256 object") path = file_path(root, record.get("path"), label) checksum = string(record.get("sha256"), f"{label}.sha256") require(bool(re.fullmatch(r"[0-9a-f]{64}", checksum)), f"{label}: invalid SHA-256") require(digest(path) == checksum, f"{label}: SHA-256 mismatch: {record['path']}") return path def check_source(source: Path, declared: set[Path]) -> None: text = source.read_text(encoding="utf-8") require(" tuple[dict, Path | None, dict | None]: manifest = load_manifest(manifest_path) if "page_library" not in manifest: return manifest, None, None root = manifest_path.parent path = file_path(root, manifest["page_library"], "page_library") catalog = read_json(path, (1,)) require(catalog.get("template_id") == manifest["template_id"], "library template_id mismatch") entries = catalog.get("entries") require(isinstance(entries, list), "library entries: expected array") seen = set() for entry in entries: require(isinstance(entry, dict), "library entry: expected object") key = identifier(entry.get("id"), "library entry ID") require(key not in seen, f"Duplicate library entry: {key}") seen.add(key) string(entry.get("title"), "entry.title") require(entry.get("status") in ("candidate", "reference", "deprecated"), "entry.status invalid") versions = strings(entry.get("template_versions"), "entry.template_versions", nonempty=True) require(all(VERSION.fullmatch(v) for v in versions), "entry.template_versions: expected exact SemVer values") require(entry.get("layout") in {item["id"] for item in manifest["layouts"]}, "Unknown library layout") strings(entry.get("tags"), "entry.tags") checked_file(root, entry.get("design"), f"{key}.design") string(entry.get("origin"), f"{key}.origin") variants = entry.get("variants") require(isinstance(variants, dict) and bool(variants), f"{key}.variants: expected non-empty object") for theme, variant in variants.items(): require(theme in manifest.get("themes", manifest.get("modes", {})), f"Unknown library theme: {theme}") require(isinstance(variant, dict), f"{key}.{theme}: expected object") files = variant.get("files") require(isinstance(files, list) and bool(files), f"{key}.{theme}.files: expected non-empty array") paths = [checked_file(root, item, f"{key}.{theme}.files") for item in files] require(len(paths) == len(set(paths)), f"{key}.{theme}: duplicate files") source = file_path(root, variant.get("source"), "variant.source") preview = file_path(root, variant.get("preview"), "variant.preview") require(source in paths and preview in paths, "source and preview must be declared files") require(source.suffix.lower() == ".svg", "library source must be .svg") require(preview.suffix.lower() in (".png", ".jpg", ".jpeg"), "library preview must be PNG/JPEG") evidence = variant.get("evidence") require(isinstance(evidence, dict), "variant.evidence: expected object") require(evidence.get("level") in ("source-only", "svg-rendered", "powerpoint-rendered", "powerpoint-edited"), "variant.evidence.level invalid") string(evidence.get("source_version"), "evidence.source_version") string(evidence.get("notes"), "evidence.notes") check_source(source, set(paths)) return manifest, path, catalog def inspect_entry(manifest_path: Path, manifest: dict, path: Path, entry: dict, theme: str, for_use: bool) -> dict: compatible = manifest["version"] in entry["template_versions"] eligible = entry["status"] == "reference" and compatible and theme in entry["variants"] if for_use: require(manifest["status"] == "released", "Library reuse requires a released template") require(eligible, "Entry is not a compatible reference for the selected theme") require(theme in entry["variants"], f"Entry has no variant for theme {theme}") root = manifest_path.parent variant = deepcopy(entry["variants"][theme]) inputs = {manifest_path, path, file_path(root, entry["design"]["path"], "design")} inputs.update(file_path(root, item["path"], "variant file") for item in variant["files"]) return {"template_id": manifest["template_id"], "template_version": manifest["version"], "entry_id": entry["id"], "title": entry["title"], "status": entry["status"], "layout": entry["layout"], "theme": theme, "compatible": compatible, "eligible": eligible, "design": str(file_path(root, entry["design"]["path"], "design")), "source": str(file_path(root, variant["source"], "source")), "preview": str(file_path(root, variant["preview"], "preview")), "variant": variant, "inputs": input_hashes(root, inputs), "verification_scope": "declared metadata, file hashes and SVG href closure; preview provenance is declared, not rerendered"} def main() -> int: parser = argparse.ArgumentParser(description=__doc__) sub = parser.add_subparsers(dest="command", required=True) for name in ("list", "check", "inspect"): item = sub.add_parser(name) item.add_argument("--manifest", type=Path, required=True, action="append") if name == "list": item.add_argument("--theme", action="append") item.add_argument("--layout", action="append") if name == "inspect": item.add_argument("--entry", required=True, action="append") item.add_argument("--theme", required=True, action="append") item.add_argument("--for-use", action="store_true") args = parser.parse_args() try: for key in ("manifest", "theme", "layout", "entry"): values = getattr(args, key, None) if values is not None: require(len(values) == 1, f"Only one --{key} may be active") setattr(args, key, values[0]) manifest_path = args.manifest.resolve() manifest, path, catalog = load_library(manifest_path) if args.command == "inspect": require(catalog is not None, "Template has no page library") entry = next((item for item in catalog["entries"] if item["id"] == args.entry), None) require(entry is not None, f"Unknown library entry: {args.entry}") output = inspect_entry(manifest_path, manifest, path, entry, args.theme, args.for_use) else: entries = [] if catalog is None else catalog["entries"] theme, layout = getattr(args, "theme", None), getattr(args, "layout", None) if theme: require(theme in manifest.get("themes", manifest.get("modes", {})), "Unknown theme filter") if layout: require(layout in {item["id"] for item in manifest["layouts"]}, "Unknown layout filter") entries = [{"id": entry["id"], "title": entry["title"], "status": entry["status"], "layout": entry["layout"], "themes": list(entry["variants"]), "tags": entry["tags"], "compatible": manifest["version"] in entry["template_versions"]} for entry in entries if (not theme or theme in entry["variants"]) and (not layout or layout == entry["layout"])] output = {"template_id": manifest["template_id"], "available": catalog is not None, "catalog": str(path) if path else None, "entries": entries, "count": len(entries)} print(json.dumps(output, ensure_ascii=False, indent=2)) return 0 except (InputError, OSError, ValueError, ET.ParseError) as error: print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr) return 2 if __name__ == "__main__": raise SystemExit(main())