From 9ea6083b8ab5ca1af44fef3190a73d53233783d3 Mon Sep 17 00:00:00 2001
From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com>
Date: Sun, 4 Oct 2026 18:07:18 +0200
Subject: [PATCH] fix: show path placeholders and hash lists on site pages
---
emulators/samcoupe.yml | 2 +-
scripts/generate_site.py | 35 +++++++++++++++++------------------
scripts/siterender.py | 26 +++++++++++++++++++++++++-
tests/test_site_validation.py | 20 ++++++++++++++++++++
4 files changed, 63 insertions(+), 20 deletions(-)
diff --git a/emulators/samcoupe.yml b/emulators/samcoupe.yml
index 650d7f88..4d69b7ac 100644
--- a/emulators/samcoupe.yml
+++ b/emulators/samcoupe.yml
@@ -1,4 +1,4 @@
-emulator: SimCoupe
+emulator: "SimCoupe (standalone)"
type: standalone
core_classification: official_port
source: "https://github.com/simonowen/simcoupe"
diff --git a/scripts/generate_site.py b/scripts/generate_site.py
index 42132875..05f9dae2 100644
--- a/scripts/generate_site.py
+++ b/scripts/generate_site.py
@@ -1982,7 +1982,7 @@ def _render_emulator_file(
lines.append("")
lines.append(f"**`{fname}`** {badge_str}")
if desc:
- lines.append(f"
{desc}")
+ lines.append(f"
{_escape_tags(str(desc))}")
lines.append("")
details = []
@@ -2011,25 +2011,13 @@ def _render_emulator_file(
bounds.append(f"max {_fmt_size(fmax)}")
details.append(f"Size: {', '.join(bounds)}")
if fsha1:
- s = fsha1[:12]
- details.append(
- f'SHA1: '
- f"`{s}...`"
- )
+ details.append(f"SHA1: {_hash_spans(fsha1)}")
if fmd5:
- s = fmd5[:12]
- details.append(
- f'MD5: '
- f"`{s}...`"
- )
+ details.append(f"MD5: {_hash_spans(fmd5)}")
if fcrc32:
details.append(f"CRC32: `{fcrc32}`")
if fsha256:
- s = fsha256[:12]
- details.append(
- f'SHA256: '
- f"`{s}...`"
- )
+ details.append(f"SHA256: {_hash_spans(fsha256)}")
if fadler32:
details.append(f"Adler32: `{fadler32}`")
if aliases:
@@ -2077,7 +2065,7 @@ def _render_emulator_file(
for d in details:
lines.append(f"- {d}")
if fnote:
- lines.append(f"- {fnote}")
+ lines.append(f"- {_escape_tags(str(fnote))}")
if contents:
lines.append(f"- Contents ({len(contents)} entries):")
for c in contents[:10]:
@@ -2194,12 +2182,22 @@ def _render_structured_blocks(profile: dict) -> list[str]:
continue
lines.append(f'???+ abstract "{label}"')
lines.append("")
+ start = len(lines)
_render_yaml_value(lines, val, indent=4)
+ lines[start:] = [_escape_tags(line) for line in lines[start:]]
lines.append("")
return lines
+def _hash_spans(value: str | list) -> str:
+ """One abbreviated hash, or each of the hashes a profile accepts."""
+ values = [value] if isinstance(value, str) else [str(v) for v in value]
+ return ", ".join(
+ f'`{v[:12]}...`' for v in values
+ )
+
+
def _availability_check(db: dict, data_names):
"""A predicate answering whether the collection holds a file.
@@ -2361,7 +2359,7 @@ def generate_emulator_page(
[
"",
'!!! info "Why no files"',
- f" {exclusion}",
+ f" {_escape_tags(str(exclusion))}",
]
)
else:
@@ -3972,6 +3970,7 @@ from siterender import ( # noqa: E402,F401
_fmt_size,
_pct,
_admonition_body,
+ _escape_tags,
_icon_name,
_icon_url,
system_icon_markdown,
diff --git a/scripts/siterender.py b/scripts/siterender.py
index 6a82ff63..41fbdb52 100644
--- a/scripts/siterender.py
+++ b/scripts/siterender.py
@@ -17,9 +17,33 @@ ICON_CACHE_PATH = Path(".cache") / "system_icons.json"
# 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 _escape_tags(text: str) -> str:
+ """Show a placeholder such as ```` 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 ```` 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\\#", text)
+ 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:
diff --git a/tests/test_site_validation.py b/tests/test_site_validation.py
index 134c0ba0..5d5b6f7e 100644
--- a/tests/test_site_validation.py
+++ b/tests/test_site_validation.py
@@ -33,6 +33,26 @@ def _page(title: str, description: str, body: str = "") -> str:
"""
+class ProsePlaceholders(unittest.TestCase):
+ """A path written with angle brackets reaches the reader as written."""
+
+ def test_a_placeholder_is_text_not_a_tag(self):
+ from siterender import _admonition_body, _escape_tags
+
+ self.assertEqual(
+ _escape_tags("gameProfiles/.ini under /Cemu"),
+ "gameProfiles/<title id>.ini under <system_dir>/Cemu",
+ )
+ self.assertIn("<title id>", _admonition_body("read .ini"))
+
+ def test_code_spans_and_links_are_left_alone(self):
+ from siterender import _escape_tags
+
+ self.assertEqual(_escape_tags("`/x`"), "`/x`")
+ self.assertEqual(_escape_tags("see "), "see ")
+ self.assertEqual(_escape_tags("size < 4096"), "size < 4096")
+
+
class RenderedSiteValidation(unittest.TestCase):
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory(dir=TMP_ROOT)