mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-11 05:53:23 -05:00
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.
180 lines
6.7 KiB
Python
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"}){{ 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 "")
|