#!/usr/bin/env python3 """Build docs/parity-gallery.html pairing iOS + Android goldens per screen. The output is a single self-contained HTML file that gitea's raw-file view can render directly. Relative paths resolve within the repo so the images load without any webserver. Filename convention (both platforms): __.png iOS snapshots live under iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/ with swift-snapshot-testing's `test_..png` prefix which we strip to align with Android's plain `.png`. Usage: python3 scripts/build_parity_gallery.py """ from __future__ import annotations import glob import html import os import re import sys REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ANDROID_DIR = "composeApp/src/androidUnitTest/roborazzi" IOS_DIR = "iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests" OUT_HTML = "docs/parity-gallery.html" OUT_MD = "docs/parity-gallery-grid.md" # swift-snapshot-testing names files "test_..png" — strip prefix IOS_NAME_RE = re.compile(r"^test_[^.]+\.(.+)\.png$") def canonical_name(platform: str, path: str) -> str | None: """Return the canonical __ key or None if unparseable.""" base = os.path.basename(path) if platform == "ios": m = IOS_NAME_RE.match(base) if not m: return None return m.group(1) return base[:-4] if base.endswith(".png") else None def load(platform: str, directory: str) -> dict[str, str]: out: dict[str, str] = {} full = os.path.join(REPO_ROOT, directory) if not os.path.isdir(full): return out for p in glob.glob(f"{full}/**/*.png", recursive=True): key = canonical_name(platform, p) if key is None: continue out[key] = os.path.relpath(p, os.path.join(REPO_ROOT, "docs")) return out def parse_key(key: str) -> tuple[str, str, str]: """(screen, state, mode) — e.g. 'login_empty_dark' → ('login', 'empty', 'dark').""" m = re.match(r"^(.+)_(empty|populated)_(light|dark)$", key) if m: return m.group(1), m.group(2), m.group(3) return key, "?", "?" def write_markdown(android: dict[str, str], ios: dict[str, str], screens: list[str]) -> None: """Emit a gitea-renderable grid as markdown tables. Gitea serves .html as text/plain (security), but renders .md natively at its /src/ URL with inline images. This output is the browser-viewable one. """ out = os.path.join(REPO_ROOT, OUT_MD) os.makedirs(os.path.dirname(out), exist_ok=True) # Markdown image paths are relative to the .md file, which lives in docs/. # We already compute relative-to-docs paths in load(); those apply here too. with open(out, "w", encoding="utf-8") as f: f.write("# honeyDue parity gallery\n\n") f.write(f"*{len(android)} Android · {len(ios)} iOS · {len(screens)} screens*\n\n") f.write("Auto-generated by `scripts/build_parity_gallery.py` — do not hand-edit.\n\n") f.write("See [parity-gallery.md](parity-gallery.md) for the workflow guide.\n\n") f.write("## Screens\n\n") for s in screens: f.write(f"- [{s}](#{s.replace('_', '-')})\n") f.write("\n---\n\n") for screen in screens: anchor = screen.replace("_", "-") f.write(f"## {screen}\n\n") f.write("| State / Mode | Android | iOS |\n") f.write("|---|---|---|\n") for state in ("empty", "populated"): for mode in ("light", "dark"): key = f"{screen}_{state}_{mode}" a = android.get(key) i = ios.get(key) a_cell = f"![]({a})" if a else "_missing_" i_cell = f"![]({i})" if i else "_missing_" f.write(f"| **{state}** / {mode} | {a_cell} | {i_cell} |\n") f.write("\n[top](#honeydue-parity-gallery)\n\n---\n\n") print(f"wrote {OUT_MD}") def main() -> int: android = load("android", ANDROID_DIR) ios = load("ios", IOS_DIR) keys = sorted(set(android) | set(ios)) screens = sorted({parse_key(k)[0] for k in keys}) write_markdown(android, ios, screens) out_path = os.path.join(REPO_ROOT, OUT_HTML) os.makedirs(os.path.dirname(out_path), exist_ok=True) with open(out_path, "w", encoding="utf-8") as f: f.write(PAGE_HEAD) f.write(f"
{len(android)} Android · {len(ios)} iOS · {len(screens)} screens
\n") f.write("\n") f.write( "
" "
" "
Android
" "
iOS
" "
\n" ) for screen in screens: f.write(f"
\n") f.write(f"

{html.escape(screen)}

\n") for state in ("empty", "populated"): for mode in ("light", "dark"): key = f"{screen}_{state}_{mode}" a = android.get(key) i = ios.get(key) a_cell = ( f"{key} Android" if a else "
Android missing
" ) i_cell = ( f"{key} iOS" if i else "
iOS missing
" ) f.write( f"
" f"
{state}
{mode}
" f"{a_cell}{i_cell}" f"
\n" ) f.write("
\n") f.write(PAGE_FOOT) print(f"wrote {OUT_HTML}: {len(screens)} screens, {len(android)} Android + {len(ios)} iOS images") return 0 PAGE_HEAD = """ honeyDue parity gallery

honeyDue parity gallery

""" PAGE_FOOT = """ """ if __name__ == "__main__": sys.exit(main())