From 79ae0463432dab7c024595d516d2f2e5272ce0ab Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:51:48 +0100 Subject: [PATCH] feat: list emulator files once as table rows --- scripts/generate_site.py | 48 +++++++++------------------------ tests/test_site_entry_tables.py | 2 +- tests/test_site_r2_emulators.py | 20 +++++++------- tests/test_site_r3_emulators.py | 24 ++++++++--------- tests/test_site_r4_emulators.py | 9 ++++--- tests/test_site_r5_search.py | 7 ++++- 6 files changed, 45 insertions(+), 65 deletions(-) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 1509f07ee..5a91f7754 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -9205,10 +9205,6 @@ CARD_SECTIONS = (("required", "Required"), ("optional", "Optional"), ("game_data ("with_app", "Comes with the app")) _SECTION_RANK = {key: rank for rank, (key, _title) in enumerate(CARD_SECTIONS)} -# Below this many cards the page is one screen of them, and a table above -# would repeat it. -EMULATOR_SUMMARY_MIN = 6 - def _card_section(f: dict) -> str: if comes_with_app(f): @@ -9236,11 +9232,12 @@ def emulator_parts(name: str, files: list) -> list[tuple[str, list[int]]]: for index, start in enumerate(range(0, len(files), size))] -# From this many entries an unsplit emulator page lists them as one table, -# each row opening to its card (siteentries.py), in place of a summary -# table above the cards: GroovyMAME's 171 cards read 14,400 words open. -# A split emulator's card pages always do. -EMULATOR_TABLE_MIN = 150 +# From this many entries an emulator page lists them as one table, each +# row opening to its card (siteentries.py). Below it the page is one +# screen of open cards. A summary table above folded cards listed every +# file twice: PCSX-ReARMed's seven, once in the table and once in a closed +# "7 files" block. A split emulator's card pages always use the table. +EMULATOR_TABLE_MIN = 6 ENTRY_HEADS = ["File", "Contents", "Status", "Hash"] # The end of the badge _file_badges writes for an entry of each section. _SECTION_BADGE = {"required": ">required", "optional": ">optional", @@ -9283,22 +9280,14 @@ def _entries_table(files: list[dict], group: list[int], rows: list[siteentries.R searched=frozenset({"Status"})) -def _sectioned(files: list, indexes: list[int], render: Callable[[list[int]], list[str]], - fold: bool = False) -> list[str]: - """Each section's heading, then ``render`` of its entries among ``indexes``. - - ``fold``: every section after Required in one closed block, for a page - whose table above already lists each entry. - """ +def _sectioned(files: list, indexes: list[int], + render: Callable[[list[int]], list[str]]) -> list[str]: + """Each section's heading, then ``render`` of its entries among ``indexes``.""" lines: list[str] = [] for key, title in CARD_SECTIONS: group = [i for i in indexes if _card_section(files[i]) == key] - if not group: - continue - body = render(group) - if fold and key != "required": - body = markup.details("note", _count(len(group), "file"), body) - lines.extend([f"## {title}", "", *body]) + if group: + lines.extend([f"## {title}", "", *render(group)]) return lines @@ -9724,20 +9713,9 @@ def emulator_pages( lines.extend(_sectioned(files, _display_order(files), lambda group: _entries_table(files, group, rows))) elif not parts: - order = _display_order(files) - indexed = len(files) >= EMULATOR_SUMMARY_MIN - if indexed: - lines.extend(search_excluded(_summary_table( - files, order, lambda i, shown: f"[{_code(shown)}](#{anchors[i]})", - len(systems or []) > 1))) - lines.append("") lines.extend(held) - # Below the table, which indexes them, the cards past the - # required ones fold: DuckStation's page was 59 phone screens, - # 102 optional cards after its three BIOS. - lines.extend(_sectioned( - files, order, lambda group: [line for i in group for line in cards[i]], - fold=indexed)) + lines.extend(_sectioned(files, _display_order(files), + lambda group: [line for i in group for line in cards[i]])) else: lines.extend(_card_index(emu_name, files, anchors, aliases, parts, len(systems or []) > 1)) diff --git a/tests/test_site_entry_tables.py b/tests/test_site_entry_tables.py index ab8a733aa..fe05a2601 100644 --- a/tests/test_site_entry_tables.py +++ b/tests/test_site_entry_tables.py @@ -119,7 +119,7 @@ class ASplitEmulatorListsItsCardsAsRows(unittest.TestCase): class ALongEmulatorPageIsOneTable(unittest.TestCase): def test_rows_replace_the_index_and_the_cards(self): - files = ([_archive(i) for i in range(gs.EMULATOR_TABLE_MIN - 10)] + files = ([_archive(i) for i in range(gs.EMULATOR_TABLE_MIN)] + [{"name": f"opt{i}.bin", "md5": f"{i:032x}"} for i in range(10)]) page = gs.generate_emulator_page( "long", {"emulator": "Long", "type": "standalone", "files": files}, EMPTY_DB, None, set()) diff --git a/tests/test_site_r2_emulators.py b/tests/test_site_r2_emulators.py index 9a5c5916e..6cf729eed 100644 --- a/tests/test_site_r2_emulators.py +++ b/tests/test_site_r2_emulators.py @@ -118,22 +118,20 @@ class RequiredFilesComeFirst(unittest.TestCase): page = _page("duckstation", _profile("duckstation")) self.assertGreater(len(re.findall(r"^## ", page, re.M)), 2) - def test_a_summary_table_above_the_cards(self): - files = _files(gs.EMULATOR_SUMMARY_MIN - 1) + [ + def test_rows_carry_region_and_hash(self): + files = _files(gs.EMULATOR_TABLE_MIN - 1) + [ {"name": "req.bin", "required": True, "region": ["japan"], "md5": "ab" * 16}] page = _page("x", {"type": "standalone", "files": files}) - self.assertIn("| File | Region | Required | Hash |", page) - row = next(line for line in page.splitlines() if "req.bin" in line and "| yes |" in line) - self.assertIn("(#file-req-bin", row) - self.assertIn("| Japan |", row) - self.assertIn(f'MD5\u00a0{"ab" * 6}', row) - # The required row is the first one. - table = page.index("| File | Region | Required | Hash |") - self.assertLess(page.index("req.bin", table), page.index("f000.bin", table)) + required = page[page.index("## Required"):page.index("## Optional")] + self.assertIn('id="file-req-bin', required) + self.assertIn("Region: Japan", required) + self.assertIn(f'MD5\u00a0{"ab" * 6}', required) + self.assertNotIn("f000.bin", required) def test_no_table_for_a_few_cards(self): page = _page("x", {"type": "standalone", "files": _files(2)}) - self.assertNotIn("| File | Region | Required | Hash |", page) + self.assertNotIn('class="rb-entries"', page) + self.assertIn('class="rb-file-entry"', page) def test_a_split_page_puts_required_on_the_first_part(self): count = gs.EMULATOR_CARDS_PER_PAGE * 2 diff --git a/tests/test_site_r3_emulators.py b/tests/test_site_r3_emulators.py index 7424cb2de..e4a150187 100644 --- a/tests/test_site_r3_emulators.py +++ b/tests/test_site_r3_emulators.py @@ -102,12 +102,10 @@ class WhatTheAppShipsIsNoFileToGet(unittest.TestCase): section = self.page[self.page.index("## Required"):self.page.index("## Optional")] self.assertNotIn("Roboto-Regular.ttf", section) - def test_the_collection_counts_go_under_the_table(self): + def test_the_collection_counts_go_under_the_summary(self): held = self.page.index("

In this collection:") - table = self.page.index("| File |") - self.assertLess(table, held) self.assertLess(held, self.page.index("## Required")) - self.assertNotIn("in repo", self.page[self.page.index("## Files"):table]) + self.assertNotIn("in repo", self.page[self.page.index("## Files"):held]) def test_the_folder_is_the_apps_own(self): self.assertIn("| BIOS directory | `bios/`, inside the app's own data folder |", self.page) @@ -115,9 +113,8 @@ class WhatTheAppShipsIsNoFileToGet(unittest.TestCase): class TheSummaryTableFitsItsContent(unittest.TestCase): def test_an_empty_region_column_goes(self): - page = _page("aethersx2") - table = next(line for line in page.splitlines() if line.startswith("| File |")) - self.assertNotIn("Region", table) + rows = gs._summary_table(_files(3, md5="ab" * 16), [0, 1, 2], lambda _i, s: s) + self.assertNotIn("Region", rows[0]) def test_a_constant_required_column_goes(self): rows = gs._summary_table(_files(3, required=True), [0, 1, 2], lambda _i, s: s) @@ -138,14 +135,15 @@ class TheSummaryTableFitsItsContent(unittest.TestCase): class LongPagesFold(unittest.TestCase): - def test_optional_cards_fold_below_the_table(self): - files = _files(gs.EMULATOR_SUMMARY_MIN) + [{"name": "req.bin", "required": True}] + def test_each_entry_is_listed_once_as_a_row(self): + files = _files(gs.EMULATOR_TABLE_MIN) + [{"name": "req.bin", "required": True}] page = _page("x", {"type": "standalone", "files": files}) + self.assertNotIn("| File |", page) + self.assertNotIn(f"

{gs.EMULATOR_TABLE_MIN} files", page) + self.assertEqual(page.count('

{gs.EMULATOR_SUMMARY_MIN} files", optional) + self.assertEqual(required.count('

")] - self.assertIn(">required for Nintendo DSi", card) + row = page[page.index('id="file-dsi-nand-bin"'):] + row = row[:row.index("")] + self.assertIn(">required for Nintendo DSi", row) self.assertNotIn(">required", page) def test_one_system_says_required(self): + # The section heading says it: the rows under it repeat nothing. page = _pages("aethersx2")["aethersx2"] - self.assertIn(">required", page) + self.assertIn("## Required", page) self.assertNotIn("required for", page) diff --git a/tests/test_site_r5_search.py b/tests/test_site_r5_search.py index ccf666095..ac8c752f4 100644 --- a/tests/test_site_r5_search.py +++ b/tests/test_site_r5_search.py @@ -293,13 +293,17 @@ class _Head(HTMLParser): self._ld: list[str] | None = None self._label: list[str] | None = None self._tabs = False + # A summary in the page body (a file row) is no sidebar group. + self._main = False def handle_starttag(self, tag: str, attrs: list) -> None: a = dict(attrs) if tag == "script" and a.get("type") == "application/ld+json": self._ld = [] - if tag == "summary": + if tag == "summary" and self._main: self._label = [] + if tag == "nav" and a.get("aria-label") == "Main": + self._main = True if tag == "nav" and a.get("aria-label") == "Sections": self._tabs = True elif self._tabs and tag == "a" and a.get("aria-current"): @@ -308,6 +312,7 @@ class _Head(HTMLParser): def handle_endtag(self, tag: str) -> None: if tag == "nav": self._tabs = False + self._main = False if tag == "script" and self._ld is not None: self.ld.append(json.loads("".join(self._ld))) self._ld = None