mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-21 11:19:11 -05:00
Profiles, the wiki, comments, docstrings, command output and schema descriptions carried em-dashes, clause-joining semicolons, unicode arrows, box-drawing banners and check-mark glyphs. Each sentence was rewritten rather than having its character swapped. Hashes, paths, source_ref values and every cited file:line are unchanged, which a comparison of each parsed profile against the previous revision confirms. validate_pr comments now say PASS and FAIL in words, and source_ref values in a profile's structured blocks render as code. test_prose_style fails if any of these comes back, in tracked files or in the generated site.
218 lines
8.1 KiB
Python
218 lines
8.1 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"
|
|
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] = {}
|
|
|
|
_CODE_SPAN = re.compile(r"(`+).+?\1", re.S)
|
|
|
|
|
|
def _code(value: object) -> str:
|
|
"""A value as an inline code span it cannot leave.
|
|
|
|
Markdown shows HTML inside a code span as text, so the one way out is a
|
|
backtick in the value closing the span early. Data from a profile, a
|
|
platform file or a file name is not ours to trust that far.
|
|
"""
|
|
return "`" + str(value).replace("`", "'") + "`"
|
|
|
|
|
|
def _escape_tags(text: str) -> str:
|
|
"""Show a placeholder such as ``<system_dir>`` as it was written.
|
|
|
|
Profile prose names paths with angle brackets. Passed through as HTML
|
|
they are unknown tags and vanish from the page, and ``<title id>`` opens
|
|
a title element that swallows everything after it. Code spans keep their
|
|
brackets, and an autolink stays a link.
|
|
"""
|
|
def outside(chunk: str) -> str:
|
|
return re.sub(r"<(?!https?://|mailto:)", "<", chunk)
|
|
|
|
out: list[str] = []
|
|
position = 0
|
|
for match in _CODE_SPAN.finditer(text):
|
|
out.append(outside(text[position:match.start()]))
|
|
out.append(match.group(0))
|
|
position = match.end()
|
|
out.append(outside(text[position:]))
|
|
return "".join(out)
|
|
|
|
|
|
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\\#", _escape_tags(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 _field_text(key: object, value: object) -> str:
|
|
"""A scalar of a structured block. A source reference reads as code."""
|
|
text = str(value)
|
|
return f"`{text}`" if key == "source_ref" else text
|
|
|
|
|
|
def _render_yaml_value(lines: list[str], val: object, 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}: {_field_text(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}:** {_field_text(k, sv)}")
|
|
elif isinstance(val, list):
|
|
for item in val:
|
|
if isinstance(item, dict):
|
|
parts = [
|
|
f"{ik}: {_field_text(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: str | dict, 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 "")
|