fix: show path placeholders and hash lists on site pages

This commit is contained in:
Abdessamad Derraz committed 2026-10-04 18:07:18 +02:00
1 parent 63af5167ed
commit 9ea6083b8a
4 files changed
+63 -20

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
emulator: SimCoupe emulator: "SimCoupe (standalone)"
type: standalone type: standalone
core_classification: official_port core_classification: official_port
source: "https://github.com/simonowen/simcoupe" source: "https://github.com/simonowen/simcoupe"
+17 -18
View File
@@ -1982,7 +1982,7 @@ def _render_emulator_file(
lines.append("") lines.append("")
lines.append(f"**`{fname}`** {badge_str}") lines.append(f"**`{fname}`** {badge_str}")
if desc: if desc:
lines.append(f"<br>{desc}") lines.append(f"<br>{_escape_tags(str(desc))}")
lines.append("") lines.append("")
details = [] details = []
@@ -2011,25 +2011,13 @@ def _render_emulator_file(
bounds.append(f"max {_fmt_size(fmax)}") bounds.append(f"max {_fmt_size(fmax)}")
details.append(f"Size: {', '.join(bounds)}") details.append(f"Size: {', '.join(bounds)}")
if fsha1: if fsha1:
s = fsha1[:12] details.append(f"SHA1: {_hash_spans(fsha1)}")
details.append(
f'SHA1: <span class="rb-hash" title="{fsha1}">'
f"`{s}...`</span>"
)
if fmd5: if fmd5:
s = fmd5[:12] details.append(f"MD5: {_hash_spans(fmd5)}")
details.append(
f'MD5: <span class="rb-hash" title="{fmd5}">'
f"`{s}...`</span>"
)
if fcrc32: if fcrc32:
details.append(f"CRC32: `{fcrc32}`") details.append(f"CRC32: `{fcrc32}`")
if fsha256: if fsha256:
s = fsha256[:12] details.append(f"SHA256: {_hash_spans(fsha256)}")
details.append(
f'SHA256: <span class="rb-hash" title="{fsha256}">'
f"`{s}...`</span>"
)
if fadler32: if fadler32:
details.append(f"Adler32: `{fadler32}`") details.append(f"Adler32: `{fadler32}`")
if aliases: if aliases:
@@ -2077,7 +2065,7 @@ def _render_emulator_file(
for d in details: for d in details:
lines.append(f"- {d}") lines.append(f"- {d}")
if fnote: if fnote:
lines.append(f"- {fnote}") lines.append(f"- {_escape_tags(str(fnote))}")
if contents: if contents:
lines.append(f"- Contents ({len(contents)} entries):") lines.append(f"- Contents ({len(contents)} entries):")
for c in contents[:10]: for c in contents[:10]:
@@ -2194,12 +2182,22 @@ def _render_structured_blocks(profile: dict) -> list[str]:
continue continue
lines.append(f'???+ abstract "{label}"') lines.append(f'???+ abstract "{label}"')
lines.append("") lines.append("")
start = len(lines)
_render_yaml_value(lines, val, indent=4) _render_yaml_value(lines, val, indent=4)
lines[start:] = [_escape_tags(line) for line in lines[start:]]
lines.append("") lines.append("")
return lines 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'<span class="rb-hash" title="{v}">`{v[:12]}...`</span>' for v in values
)
def _availability_check(db: dict, data_names): def _availability_check(db: dict, data_names):
"""A predicate answering whether the collection holds a file. """A predicate answering whether the collection holds a file.
@@ -2361,7 +2359,7 @@ def generate_emulator_page(
[ [
"", "",
'!!! info "Why no files"', '!!! info "Why no files"',
f" {exclusion}", f" {_escape_tags(str(exclusion))}",
] ]
) )
else: else:
@@ -3972,6 +3970,7 @@ from siterender import ( # noqa: E402,F401
_fmt_size, _fmt_size,
_pct, _pct,
_admonition_body, _admonition_body,
_escape_tags,
_icon_name, _icon_name,
_icon_url, _icon_url,
system_icon_markdown, system_icon_markdown,
+25 -1
View File
@@ -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. # because a heading with a broken image reads worse than a heading without one.
_icon_available: dict[str, bool] = {} _icon_available: dict[str, bool] = {}
_CODE_SPAN = re.compile(r"(`+).+?\1", re.S)
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:)", "&lt;", 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: def _admonition_body(text: str) -> str:
"""Indent prose without turning source tokens such as ``#if`` into H1s.""" """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 ") return escaped.replace("\n", "\n ")
def _icon_name(manufacturer: str, console_name: str) -> str: def _icon_name(manufacturer: str, console_name: str) -> str:
+20
View File
@@ -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/<title id>.ini under <system_dir>/Cemu"),
"gameProfiles/&lt;title id>.ini under &lt;system_dir>/Cemu",
)
self.assertIn("&lt;title id>", _admonition_body("read <title id>.ini"))
def test_code_spans_and_links_are_left_alone(self):
from siterender import _escape_tags
self.assertEqual(_escape_tags("`<system_dir>/x`"), "`<system_dir>/x`")
self.assertEqual(_escape_tags("see <https://example.org/a>"), "see <https://example.org/a>")
self.assertEqual(_escape_tags("size < 4096"), "size &lt; 4096")
class RenderedSiteValidation(unittest.TestCase): class RenderedSiteValidation(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory(dir=TMP_ROOT) self.temp = tempfile.TemporaryDirectory(dir=TMP_ROOT)