feat: draw site images from data, host them

scripts/sitediagrams.py writes the explainer, install flow, verification modes, MinUI card, pipeline and data layer diagrams as inline SVG from the registry, the platform files, the install manifests and the pipeline step list. A page asks for one with an image comment, which the decoration pass resolves. The two mermaid diagrams of the architecture page are replaced, so no page loads mermaid from unpkg.

Platform logos and system icons are copied once by fetch_site_images.py with their source and owner recorded, and the build rewrites the remote links to the copies. validate_site refuses an image from another host. Three registry logos named a personal avatar instead of the project's (RomM, ROCKNIX, MiSTer FPGA) and MyMinUI's was a GitHub identicon, now dropped.

One 1200x630 sharing card per section, a 404 page with search and the two entry pages, and a list of screenshots that need a real device.
This commit is contained in:
Abdessamad Derraz committed 2026-10-22 11:45:47 +02:00
1 parent f1c58820d1
commit 1b9ea86e1b
120 files changed
+1963 -176

No files matched your search

+17 -75
View File
@@ -2,25 +2,18 @@
from __future__ import annotations
import json
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from artifacts import write_text_atomic
WIKI_SRC_DIR = "wiki" # manually maintained wiki sources
SYSTEM_ICON_BASE = "https://raw.githubusercontent.com/libretro/retroarch-assets/master/xmb/systematic/png"
ICON_CACHE_PATH = Path(".cache") / "system_icons.json"
# Local copies of the upstream icons, named as upstream names them.
ICON_DIR = Path(__file__).resolve().parent.parent / "docs_assets" / "system-icons"
# Icon names confirmed to exist upstream. A name absent from this map has not
# been checked yet. A name mapped to False has no icon and gets none rendered,
# because a heading with a broken image reads worse than a heading without one.
# Icon names the site holds a copy of. A name mapped to False has none.
_icon_available: dict[str, bool] = {}
_CODE_SPAN = re.compile(r"(`+).+?\1", re.S)
@@ -73,77 +66,26 @@ def _icon_name(manufacturer: str, console_name: str) -> str:
def _icon_url(icon_name: str) -> str:
return f"{SYSTEM_ICON_BASE}/{urllib.parse.quote(icon_name)}.png"
def _read_icon_cache() -> dict[str, bool]:
"""The recorded icon checks, empty when none were recorded.
An unreadable file is reported, not taken for an empty cache in silence.
"""
try:
text = ICON_CACHE_PATH.read_text(encoding="utf-8")
except FileNotFoundError:
return {}
except OSError as exc:
print(f" WARNING: cannot read {ICON_CACHE_PATH}: {exc}", file=sys.stderr)
return {}
try:
data = json.loads(text)
except json.JSONDecodeError as exc:
print(f" WARNING: {ICON_CACHE_PATH} is not valid JSON ({exc}), "
"icons are checked again", file=sys.stderr)
return {}
return {str(k): bool(v) for k, v in data.items()} if isinstance(data, dict) else {}
def prime_system_icons(names: set[str]) -> None:
"""Record which system icons upstream actually serves.
"""Record which system icons the site holds a copy of.
Results persist in ``.cache`` so later runs skip the network. Names that
cannot be checked stay unavailable: the site never links an image it has
not seen answer.
The icons are copied once into ``docs_assets/system-icons/`` by
``scripts/fetch_site_images.py``: the build reads that folder and never
the network. A system without a copy gets no icon, because a heading
with a broken image reads worse than a heading without one.
"""
cached = _read_icon_cache()
unknown = sorted(n for n in names if n not in cached)
if unknown:
def check(name: str) -> tuple[str, bool | None]:
"""True when served, False when upstream says it is gone.
None on a transient failure, so a flaky run never records an
icon as absent for every later build.
"""
req = urllib.request.Request(_icon_url(name), method="HEAD")
try:
with urllib.request.urlopen(req, timeout=15) as resp:
return name, resp.status == 200
except urllib.error.HTTPError as exc:
return name, False if exc.code == 404 else None
except (urllib.error.URLError, OSError):
return name, None
print(f"Checking {len(unknown)} system icons...")
learned: dict[str, bool] = {}
with ThreadPoolExecutor(max_workers=8) as pool:
for name, ok in pool.map(check, unknown):
if ok is not None:
learned[name] = ok
cached.update(learned)
if learned:
# Another run may have recorded names meanwhile: merge with what
# the file holds now, and replace it whole so a reader never sees
# a half-written cache.
merged = {**_read_icon_cache(), **learned}
ICON_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
write_text_atomic(
str(ICON_CACHE_PATH),
json.dumps(merged, indent=2, sort_keys=True) + "\n",
)
_icon_available.update(cached)
missing = sum(1 for n in names if not cached.get(n))
held = {path.stem for path in ICON_DIR.glob("*.png")}
_icon_available.update({name: name in held for name in names})
missing = sum(1 for name in names if name not in held)
if missing:
print(f" {missing} systems have no upstream icon")
print(f" {missing} systems have no icon in {ICON_DIR.relative_to(ICON_DIR.parents[1])}")
def system_icon_markdown(manufacturer: str, console_name: str) -> str:
"""Icon image for a system heading, empty when upstream serves none."""
"""Icon image for a system heading, empty when the site holds none.
The link names the upstream file: ``sitediagrams.localize_images``
points it at the local copy, whose source is recorded beside it.
"""
name = _icon_name(manufacturer, console_name)
if not _icon_available.get(name):
return ""