Files
libretro/scripts/siterender.py
T
Abdessamad Derraz b8bdeec5ce refactor: lift the site's shared layers out
Three groups leave generate_site.py: the write-and-sweep bookkeeping the
whole build shares, the pinned permalinks into cited sources, and the
small renderings every page uses. Each sits below what calls it, so the
page generators stay the only composite layer.

Verified against the previous revision on frozen inputs: 571 artefacts
identical, and the rendered site still resolves all local links across
511 pages.
2026-08-12 12:37:27 +02:00

180 lines
6.7 KiB
Python

"""Small renderings shared by every page."""
from __future__ import annotations
import re
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import json
import urllib.request
WIKI_SRC_DIR = "wiki" # manually maintained wiki sources
SYSTEM_ICON_BASE = "https://raw.githubusercontent.com/libretro/retroarch-assets/master/xmb/systematic/png"
SYSTEM_ICON_BASE = "https://raw.githubusercontent.com/libretro/retroarch-assets/master/xmb/systematic/png"
ICON_CACHE_PATH = Path(".cache") / "system_icons.json"
# 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_available: dict[str, bool] = {}
def _admonition_body(text: str) -> str:
"""Indent prose without turning source tokens such as ``#if`` into H1s."""
escaped = re.sub(r"(?m)^(\s*)#", r"\1\\#", text)
return escaped.replace("\n", "\n ")
def _icon_name(manufacturer: str, console_name: str) -> str:
return f"{manufacturer} - {console_name}".replace("/", " ")
def _icon_url(icon_name: str) -> str:
return f"{SYSTEM_ICON_BASE}/{urllib.parse.quote(icon_name)}.png"
def prime_system_icons(names: set[str]) -> None:
"""Record which system icons upstream actually serves.
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.
"""
cached: dict[str, bool] = {}
if ICON_CACHE_PATH.exists():
try:
with open(ICON_CACHE_PATH) as f:
cached = json.load(f)
except (json.JSONDecodeError, OSError):
cached = {}
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...")
with ThreadPoolExecutor(max_workers=8) as pool:
for name, ok in pool.map(check, unknown):
if ok is not None:
cached[name] = ok
ICON_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(ICON_CACHE_PATH, "w") as f:
json.dump(cached, f, indent=2, sort_keys=True)
_icon_available.update(cached)
missing = sum(1 for n in names if not cached.get(n))
if missing:
print(f" {missing} systems have no upstream icon")
def system_icon_markdown(manufacturer: str, console_name: str) -> str:
"""Icon image for a system heading, empty when upstream serves none."""
name = _icon_name(manufacturer, console_name)
if not _icon_available.get(name):
return ""
return f"![{console_name}]({_icon_url(name)}){{ width=24 }} "
# Global index: maps system_id -> (manufacturer_slug, console_name) for cross-linking
_system_page_map: dict[str, tuple[str, str]] = {}
def _slugify_anchor(text: str) -> str:
"""Slugify text for MkDocs anchor compatibility."""
import re
slug = text.lower()
slug = re.sub(r"[^\w\s-]", "", slug)
slug = re.sub(r"[\s]+", "-", slug)
slug = slug.strip("-")
return slug
def _system_link(sys_id: str, prefix: str = "") -> str:
"""Generate a markdown link to a system page with anchor."""
if sys_id in _system_page_map:
slug, console = _system_page_map[sys_id]
anchor = _slugify_anchor(console)
return f"[{sys_id}]({prefix}systems/{slug}.md#{anchor})"
return sys_id
def _render_yaml_value(lines: list[str], val, indent: int = 4) -> None:
"""Render any YAML value as indented markdown."""
pad = " " * indent
if isinstance(val, dict):
for k, v in val.items():
if isinstance(v, dict):
lines.append(f"{pad}**{k}:**")
lines.append("")
_render_yaml_value(lines, v, indent + 4)
elif isinstance(v, list):
lines.append(f"{pad}**{k}:**")
lines.append("")
for item in v:
if isinstance(item, dict):
parts = [
f"{ik}: {iv}"
for ik, iv in item.items()
if not isinstance(iv, (dict, list))
]
lines.append(f"{pad}- {', '.join(parts)}")
else:
lines.append(f"{pad}- {item}")
lines.append("")
else:
# Truncate very long strings in tables
sv = str(v)
if len(sv) > 200:
sv = sv[:200] + "..."
lines.append(f"{pad}- **{k}:** {sv}")
elif isinstance(val, list):
for item in val:
if isinstance(item, dict):
parts = [
f"{ik}: {iv}"
for ik, iv in item.items()
if not isinstance(iv, (dict, list))
]
lines.append(f"{pad}- {', '.join(parts)}")
else:
lines.append(f"{pad}- {item}")
elif isinstance(val, str) and "\n" in val:
for line in val.split("\n"):
lines.append(f"{pad}{line}")
else:
lines.append(f"{pad}{val}")
def _platform_link(name: str, display: str, prefix: str = "") -> str:
"""Generate a markdown link to a platform page."""
return f"[{display}]({prefix}platforms/{name}.md)"
def _emulator_link(name: str, prefix: str = "") -> str:
"""Generate a markdown link to an emulator page."""
return f"[{name}]({prefix}emulators/{name}.md)"
def _fmt_size(size: int) -> str:
if size >= 1024 * 1024 * 1024:
return f"{size / (1024**3):.1f} GB"
if size >= 1024 * 1024:
return f"{size / (1024**2):.1f} MB"
if size >= 1024:
return f"{size / 1024:.1f} KB"
return f"{size} B"
def _pct(n: int, total: int) -> str:
if total == 0:
return "0%"
return f"{n / total * 100:.1f}%"
def _by_mode(value, mode: str) -> str:
"""Read a profile field that may be keyed by build mode."""
if isinstance(value, dict):
return str(value.get(mode) or "") if mode else ""
return str(value or "")