chore: merge folded cards and one install command

This commit is contained in:
Abdessamad Derraz committed 2026-10-27 21:45:48 +01:00
commit 793b16cf67
15 files changed
+974 -66

No files matched your search

+1 -1
View File
@@ -5049,7 +5049,7 @@ class CollectionCardsCarryFullHashes(unittest.TestCase):
"crc32": "504086c1", "sha256": "b" * 64}
text = "\n".join(_system_file_card(entry, FileConsumers({}, {})))
for value in (entry["sha1"], entry["md5"], entry["crc32"], entry["sha256"]):
self.assertIn(f'<span class="rb-hash">{value}</span>', text)
self.assertIn(f'<dd class="rb-hash">{value}</dd>', text)
+302
View File
@@ -0,0 +1,302 @@
"""File cards and install boxes read at a glance (density round, 2026-10-27).
A system page wrote every setup's install command for each console, twice:
12 long lines for the PSP. A file card showed four full hashes, the
collection path and the catalogue records before the next file. Now a box
shows one command for the setup picked, and a card shows the name, the
size, the download, a short SHA1, the setups and the emulators. The rest
sits in closed folds, every fact still on the page.
"""
from __future__ import annotations
import re
import sys
import types
import unittest
from html.parser import HTMLParser
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import generate_site as gs # noqa: E402
import sitemarkup as markup # noqa: E402
from cross_reference import FileConsumers # noqa: E402
DIST = REPO_ROOT / "website" / "dist"
SITE_TS = REPO_ROOT / "website" / "src" / "scripts" / "site.ts"
SHA1 = "30a33f3651f8e1b0967a5dfe6ff714baebcce41a"
MD5 = "866855cc330b9b95cc69135fb7b41d38"
CRC32 = "7b57fa78"
SHA256 = "3ab7fcf6956d0281ada94d501fc6d1cfa8fc034f139e25fed6c829f723d92a95"
class _Visible(HTMLParser):
"""Words shown before any fold is opened: text outside closed details
(their summary counts), hidden elements, scripts and styles."""
VOID = {"br", "img", "input", "wbr", "hr", "meta", "link", "source"}
def __init__(self) -> None:
super().__init__(convert_charrefs=True)
self.stack: list[tuple[str, object]] = []
self.hidden = 0
self.words: list[str] = []
def handle_starttag(self, tag: str, attrs: list) -> None:
if tag in self.VOID:
return
names = dict(attrs)
if tag == "summary" and self.stack and self.stack[-1] == ("details", True):
self.hidden -= 1
self.stack.append((tag, "shown"))
return
hides = tag in ("script", "style") or "hidden" in names or (
tag == "details" and "open" not in names)
self.hidden += hides
self.stack.append((tag, hides))
def handle_endtag(self, tag: str) -> None:
while self.stack:
name, hides = self.stack.pop()
if hides == "shown":
self.hidden += 1
elif hides:
self.hidden -= 1
if name == tag:
break
def handle_data(self, data: str) -> None:
if not self.hidden:
self.words += data.split()
def visible_words(html_text: str) -> list[str]:
parser = _Visible()
parser.feed(html_text)
return parser.words
def _main(page: str) -> str:
text = (DIST / page / "index.html").read_text(encoding="utf-8")
return text[text.index("<main"):text.index("</main>")]
def _card_consumers(sha1: str) -> types.SimpleNamespace:
return types.SimpleNamespace(
platforms={sha1: {"batocera", "retroarch"}}, emulators={sha1: {"ppsspp"}},
platform_systems={sha1: {"batocera": {"sony-psp"}, "retroarch": {"sony-psp"}}},
platform_destinations={sha1: {"batocera": {"psp/ppge_atlas.zim"},
"retroarch": {"PPSSPP/ppge_atlas.zim"}}},
installed={sha1: {"knulli": {"PPSSPP/ppge_atlas.zim"}}},
emulator_entries={}, emulator_files={},
)
def _card() -> dict:
return {"name": "ppge_atlas.zim", "sha1": SHA1, "md5": MD5, "crc32": CRC32,
"sha256": SHA256, "size": 666539,
"path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"provenance": {"tosec": {"name": "ppge_atlas (PPSSPP).zim"}}}
class ASystemCardShowsTheAnswerFirst(unittest.TestCase):
def setUp(self) -> None:
self.lines = gs._system_file_card(_card(), _card_consumers(SHA1))
self.text = "\n".join(self.lines)
self.fold = self.text[self.text.index("<details"):self.text.index("</details>")]
self.shown = self.text.replace(self.fold, "")
def test_the_open_part(self) -> None:
for fact in ("<code>ppge_atlas.zim</code>", ">650.9 KB<", ">Download</a>",
"<dt>On the setup's list</dt>", "<dt>Packed for its emulators by</dt>",
"<dt>Emulators</dt>", f'<dt>SHA1</dt><dd class="rb-hash">{SHA1[:12]}</dd>'):
self.assertIn(fact, self.shown)
def test_every_hash_whole_in_one_fold_with_the_rest(self) -> None:
for label, value in (("SHA1", SHA1), ("MD5", MD5), ("CRC32", CRC32), ("SHA256", SHA256)):
self.assertIn(f'<dt>{label}</dt><dd class="rb-hash">{value}</dd>', self.fold)
shown = " ".join(visible_words(self.text))
for value in (MD5, CRC32, SHA256, SHA1):
self.assertNotIn(value, shown)
self.assertIn("<dt>Collection path</dt>", self.fold)
self.assertIn("<dt>Catalogued</dt>", self.fold)
self.assertEqual(self.text.count("<details"), 1)
self.assertIn("<summary>Hashes, collection path and catalogues</summary>", self.fold)
def test_the_fold_is_out_of_the_search(self) -> None:
self.assertTrue(self.fold.startswith("<details data-pagefind-ignore>\n<summary>"))
def test_no_fact_is_lost(self) -> None:
# The yardstick list a page is cut by holds every fact: the
# Starlight card holds each of its values.
with markup.using("mkdocs"):
yardstick = "\n".join(gs._system_file_card(_card(), _card_consumers(SHA1)))
for label, values in re.findall(r"(?m)^- ([^:]+): (.*)$", yardstick):
self.assertIn(f"<dt>{label}</dt>", self.text)
for value in re.findall(r"[0-9a-f]{8,64}|`[^`]+`", values):
self.assertIn(value.strip("`"), self.text, label)
def test_a_card_reads_in_few_words(self) -> None:
# Measured on this card: 31 words open, 40 before the round.
self.assertLessEqual(len(visible_words(self.text)), min(40, int(31 * 1.05)))
class AnEmulatorCardFoldsItsHashes(unittest.TestCase):
def setUp(self) -> None:
entry = {"name": "scph5501.bin", "md5": "490f666e1afb15b7362b406ed1cea246",
"sha1": "b05def971d8ec59f346f2d9ac21fb742e3eb6917", "size": 524288,
"required": True, "note": "Read at boot.", "priority": 5,
"validation": ["md5"], "source_ref": "src/core/bios.cpp:47"}
profile = {"emulator": "DuckStation", "files": [entry], "systems": ["sony-playstation"]}
self.text = "\n".join(gs._render_emulator_file(
entry, profile, None, profile["files"], lambda _f: True, anchor="file-scph5501-bin"))
self.fold = self.text[self.text.index("<details"):self.text.index("</details>")]
self.shown = self.text.replace(self.fold, "")
def test_a_short_hash_open_and_every_hash_folded(self) -> None:
# On the size's line.
self.assertIn('- Size: 512.0 KB, SHA1 <span class="rb-hash" '
'data-copy="b05def971d8ec59f346f2d9ac21fb742e3eb6917">b05def971d8e</span>',
self.shown)
self.assertIn('<code class="rb-hash" data-copy="">490f666e1afb15b7362b406ed1cea246</code>',
self.fold)
self.assertNotIn("490f666e1afb15b7362b406ed1cea246", self.shown)
def test_notes_open_the_rest_folded(self) -> None:
self.assertIn("Read at boot.", self.shown)
for fact in ("Priority: 5", "Validation: md5", "Source:"):
self.assertIn(fact, self.fold)
self.assertNotIn(fact, self.shown)
self.assertIn("<summary>Hashes, priority, validation and more</summary>", self.fold)
def test_the_markdown_yardstick_is_unchanged(self) -> None:
with markup.using("mkdocs"):
entry = {"name": "a.bin", "md5": "b" * 32, "size": 4}
text = "\n".join(gs._render_emulator_file(
entry, {"files": [entry]}, None, [entry], lambda _f: True))
self.assertNotIn("<details", text)
self.assertIn("- MD5: ", text)
PAIRS = [("emudeck", "sony-psp"), ("retroarch", "sony-psp"), ("retrobat", "Neo Geo")]
class AnInstallBoxShowsOneCommand(unittest.TestCase):
def setUp(self) -> None:
self.text = "\n".join(gs._console_install_block("PlayStation Portable", PAIRS))
def test_one_command_per_tab_for_the_setup_picked(self) -> None:
tabs = self.text[self.text.index("<Tabs"):self.text.index("</Tabs>")]
self.assertEqual(re.findall(r'<TabItem label="([^"]+)"', tabs),
["Windows", "macOS", "Linux", "Steam Deck"])
self.assertEqual(tabs.count("--platform"), 4)
self.assertEqual(tabs.count("--platform emudeck --system sony-psp"), 4)
def test_the_setup_list_carries_every_command(self) -> None:
select = re.search(r"<select>(.*?)</select>", self.text).group(1)
options = re.findall(r'<option value="([^"]+)" data-sh="([^"]+)" data-ps="([^"]+)">', select)
self.assertEqual([value for value, _sh, _ps in options], ["emudeck", "retroarch", "retrobat"])
self.assertIn(("retrobat", "--platform retrobat --system 'Neo Geo'",
"--platform retrobat --system 'Neo Geo'"), options)
def test_without_script_the_list_of_every_command_shows(self) -> None:
self.assertIn('<div class="rb-install" data-rb-install="" hidden>', self.text)
listed = self.text[self.text.index('<details class="rb-install-all" open>'):]
for platform, _system in PAIRS:
self.assertEqual(listed.count(f"--platform {platform} --system"), 2, platform)
self.assertIn("install.sh | sh -s -- --platform retrobat --system 'Neo Geo'</code>", listed)
self.assertIn("install.ps1))) --platform retrobat --system 'Neo Geo'</code>", listed)
# The URL is no link inside the command, and the break adds nothing.
self.assertIn("https://<wbr>raw.githubusercontent.com", listed)
def test_one_setup_keeps_its_two_blocks(self) -> None:
text = "\n".join(gs._console_install_block("PlayStation Portable", PAIRS[:1]))
self.assertNotIn("<select", text)
self.assertIn('```sh title="Terminal"', text)
self.assertIn('```powershell title="PowerShell"', text)
def test_the_yardstick_is_unchanged(self) -> None:
# The budgets that cut a system page are measured on this Markdown:
# a box that changed it would move cards to other pages.
with markup.using("mkdocs"):
text = "\n".join(gs._console_install_block("PlayStation Portable", PAIRS))
self.assertNotIn("<select", text)
self.assertEqual(text.count("# RetroBat"), 2)
self.assertEqual(text.count("install.sh | sh"), 3)
class TheSetupListLooksLikeAList(unittest.TestCase):
"""The setup list had a rule of its own without the arrow, and read as
a text field at 390 and 1440 px."""
def test_the_select_takes_the_site_arrow(self) -> None:
css = (REPO_ROOT / "website" / "src" / "styles" / "retrobios.css").read_text(encoding="utf-8")
shared = re.search(r"\.sl-markdown-content select \{([^}]*)\}", css)
self.assertIsNotNone(shared)
self.assertIn("appearance: none", shared.group(1))
self.assertIn("var(--rb-select-arrow)", shared.group(1))
# No rule for the install box's select overrides it.
self.assertNotRegex(css, r"rb-install-setup select")
# The arrow is defined for both themes.
self.assertEqual(len(re.findall(r"--rb-select-arrow:", css)), 2)
# The box's select sits in Markdown content, where the shared rule applies.
box = "\n".join(gs._console_install_block("PlayStation Portable", PAIRS))
self.assertIn('<label class="rb-install-setup">Your setup <select>', box)
def test_the_built_select_shows_the_arrow(self) -> None:
if not (DIST / "systems").is_dir():
self.skipTest("no built site")
css = "".join(path.read_text(encoding="utf-8") for path in (DIST / "_astro").glob("*.css"))
self.assertNotIn("rb-install-setup select", css)
self.assertRegex(css, r"\.sl-markdown-content select\{[^}]*--rb-select-arrow")
class TheScriptWiresBoth(unittest.TestCase):
def test_pickers_and_copy_buttons_run(self) -> None:
source = SITE_TS.read_text(encoding="utf-8")
self.assertIn("\ninstallPickers();\n", source)
self.assertIn("\ncopyButtons();\n", source)
self.assertIn("const SETUP_KEY = 'rb-setup';", source)
self.assertIn("const COPYABLE = '[data-copy], .rb-sys-file dd.rb-hash';", source)
# Visible words of each built page: measured before the round and after it,
# the ceiling being the after value with 5 % of room.
WORD_CEILINGS = {
"systems/sony": (2012, 1407),
"systems/sony-playstation": (2709, 2130),
"systems/nintendo": (3598, 2941),
"systems/sega": (4151, 3370),
"emulators/duckstation": (1188, 1117),
"emulators/yaps2": (3159, 2655),
}
class BuiltPagesReadInFewerWords(unittest.TestCase):
def test_visible_words_under_the_ceiling(self) -> None:
if not (DIST / "systems").is_dir():
self.skipTest("no built site")
for page, (before, after) in WORD_CEILINGS.items():
with self.subTest(page=page):
ceiling = min(before, int(after * 1.05))
self.assertLessEqual(len(visible_words(_main(page))), ceiling)
def test_a_built_card_holds_its_hashes(self) -> None:
if not (DIST / "systems").is_dir():
self.skipTest("no built site")
main = _main("systems/sony")
card = main[main.index('id="sha1-30a33f3651f8"'):]
card = card[:card.index('class="rb-sys-file"')]
self.assertRegex(card, r'<dd class="rb-hash">30a33f3651f8</dd>')
self.assertIn('<details data-pagefind-ignore', card)
self.assertIn(">30a33f3651f8e1b0967a5dfe6ff714baebcce41a</dd>", card)
# No link inside a command of the list: the URL is text.
listed = main[main.index("rb-install-all"):]
self.assertNotRegex(listed[:listed.index("</details>")], r"<code[^>]*>[^<]*<a ")
if __name__ == "__main__":
unittest.main()
+3 -2
View File
@@ -369,7 +369,7 @@ class AVariantShowsWhoPinsIt(unittest.TestCase):
".variants/advision.zip.1c086064</code>", block)
self.assertIn('<a href="../platforms/batocera.md">Batocera</a>', block)
self.assertIn('<a href="../platforms/retrobat.md">RetroBat</a>', block)
self.assertIn('<span class="rb-hash">' + "c" * 40 + "</span>", block)
self.assertIn('<dd class="rb-hash">' + "c" * 40 + "</dd>", block)
self.assertIn("Catalogued", block)
@@ -992,7 +992,8 @@ class TheOneLinerIsWhereUsersLand(unittest.TestCase):
page = generate_site.generate_system_page(
"Microsoft", {"MSX": [entry]}, FileConsumers({"files": {}}),
{"MSX": [("recalbox", "msx1,msx2,msxturbor"), ("batocera", "msx1")]})
self.assertIn("--platform recalbox --system msx1,msx2,msxturbor\n", page)
# The whole system id, up to the end of the command.
self.assertRegex(page, r"--platform recalbox --system msx1,msx2,msxturbor(\n|</code>)")
self.assertIn("--platform batocera --system msx1", page)
def test_a_system_is_offered_only_where_the_manifest_records_it(self):
+6 -1
View File
@@ -285,7 +285,12 @@ class CommandBlocks(unittest.TestCase):
self.assertIn("install.ps1", group)
block = "\n".join(generate_site._console_install_block(
"PlayStation", [("batocera", "psx"), ("recalbox", "psx")]))
self.assertEqual(block.count("install.sh | sh"), block.count("install.ps1))) "))
# Every setup's options in both forms: the list of every command,
# and the one command under the OS tabs.
sh_args = re.findall(r"install\.sh \| sh -s -- ([^<\n]+)", block)
ps_args = re.findall(r"install\.ps1\)\)\) ([^<\n]+)", block)
self.assertEqual(sorted(set(sh_args)), sorted(set(ps_args)))
self.assertEqual(len(set(sh_args)), 2)
self.assertIn('```powershell title="PowerShell"', block)
for key, guide in siteguides.GUIDES.items():
if "windows" in guide.run:
+16 -8
View File
@@ -182,8 +182,14 @@ class TheCardLinesSayWhatTheyMean(unittest.TestCase):
def test_the_facts_are_out_of_the_search(self):
# The setup list made every card's excerpt a run of names and
# paths (round 4, V6): the card's title is what the search reads.
lists = [line for line in self.lines if line.startswith("<dl")]
# The facts shown open are one list, the others one fold, both
# out of the search.
shown = self.lines[:next(i for i, line in enumerate(self.lines) if line.startswith("<details"))]
lists = [line for line in shown if line.startswith("<dl")]
self.assertEqual(lists, ['<dl class="rb-facts" data-pagefind-ignore>'])
folds = [line for line in self.lines if line.startswith("<details")]
self.assertEqual(len(folds), 1)
self.assertEqual(folds, ["<details data-pagefind-ignore>"])
def test_the_page_defines_both_lines(self):
page = gs.generate_system_page("Sony", {"PlayStation": [_file("scph5501.bin", "4")]},
@@ -206,15 +212,17 @@ class FileFactsAreAGrid(unittest.TestCase):
"provenance": {"redump": {"name": "ps-30a.bin"},
"tosec": {"name": "SCPH-5501 (1996).bin"}}}
lines = gs._system_file_card(entry, FileConsumers({"files": {}}))
facts = [line for line in lines if line.startswith("<dt>")]
self.assertTrue(facts)
for line in facts:
self.assertEqual(line.count("<dt>"), 1, line)
self.assertEqual(line.count("<dd>"), 1, line)
text = "\n".join(lines)
self.assertIn('<dt>SHA1</dt><dd><span class="rb-hash">' + "4" * 40 + "</span></dd>", text)
# One value cell after each label, in the open list and in the fold.
facts = re.findall(r"<dt>[^<]*</dt><dd[^>]*>.*?</dd>", text)
self.assertTrue(facts)
self.assertEqual(len(facts), text.count("<dt>"))
self.assertEqual(text.count("<dt>"), len(re.findall(r"<dd[ >]", text)))
# The hash is the cell's text, styled by the cell: no element around it.
self.assertIn('<dt>SHA1</dt><dd class="rb-hash">' + "4" * 40 + "</dd>", text)
self.assertNotIn("<code class=\"rb-hash\">", text)
catalogued = next(line for line in facts if line.startswith("<dt>Catalogued"))
self.assertNotIn('<span class="rb-hash">', text)
catalogued = next(fact for fact in facts if fact.startswith("<dt>Catalogued"))
self.assertIn("<br>", catalogued)
self.assertNotRegex(text, r"(?m)^- ")
+4 -1
View File
@@ -141,7 +141,10 @@ class TheVisitorsTab(unittest.TestCase):
block = source[source.index("const osTabs = () =>"):source.index("// The sidebar lists")]
self.assertIn("savedTab(OS_SYNC_KEY)", block)
self.assertIn("set.switchTab(set.tabs[index], index, false)", block)
self.assertNotIn("localStorage.setItem", source)
self.assertNotIn("localStorage.setItem", block)
# The one choice the script saves is the setup picked in an install
# box, under its own key: never the OS guess.
self.assertEqual(re.findall(r"localStorage\.setItem\((\w+)", source), ["SETUP_KEY"])
self.assertNotIn("keepTabInView", source)
+2 -1
View File
@@ -149,7 +149,8 @@ class TheNeoGeoOpensOnItsSet(unittest.TestCase):
pairs = gs._install_pairs([("batocera", "snk-neogeo")], "a" * 40, consumers)
self.assertIn(("retroarch", "arcade"), pairs)
block = "\n".join(gs._console_install_block("Neo Geo", pairs))
self.assertIn("# RetroArch\n", block)
self.assertIn("<li>RetroArch: <code", block)
self.assertIn("--platform retroarch --system arcade</code>", block)
def test_the_built_section(self) -> None:
html = _page(DIST / "systems" / "snk" / "index.html")
+3 -2
View File
@@ -260,8 +260,9 @@ class EnginesGoByEngine(unittest.TestCase):
if not SYSTEMS.is_dir():
self.skipTest("pages not generated")
for old, new in gs.SYSTEM_PAGES_MOVED.items():
self.assertFalse((CONTENT / old).exists(), old)
self.assertTrue((CONTENT / new).exists(), new)
self.assertFalse((CONTENT / old).exists() or (CONTENT / old).with_suffix(".mdx").exists(), old)
# A page with tabs is written as MDX.
self.assertTrue((CONTENT / new).exists() or (CONTENT / new).with_suffix(".mdx").exists(), new)
redirects = CONTENT.parents[1] / "generated" / "redirects.json"
if redirects.is_file():
table = json.loads(redirects.read_text(encoding="utf-8"))