From b2d74b525531fdaeb8f81ca1f2c73d7ec28258ce Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:00:46 +0200 Subject: [PATCH] feat: link emulator cards to the copy and section --- scripts/cross_reference.py | 12 +++++++++++- scripts/generate_site.py | 26 +++++++++++++++++++++++--- tests/test_audit_regressions.py | 26 ++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index 5ddbfaff..9b99d904 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -335,6 +335,9 @@ class FileConsumers: if path } self.platforms: dict[str, set[str]] = {} + # Under which of its systems each platform declares a file, so a + # link can land on that section rather than the page top. + self.platform_systems: dict[str, dict[str, set[str]]] = {} self.emulators: dict[str, set[str]] = {} def sha1_of(self, entry: dict, dest: str = "", owner: str = "") -> str | None: @@ -357,11 +360,18 @@ class FileConsumers: def add_platforms(self, configs: dict[str, dict]) -> None: for name, config in configs.items(): - for system in config.get("systems", {}).values(): + for system_id, system in config.get("systems", {}).items(): for entry in system.get("files", []): sha1 = self.sha1_of(entry, entry.get("destination", "")) if sha1: self.platforms.setdefault(sha1, set()).add(name) + self.platform_systems.setdefault(sha1, {}).setdefault( + name, set() + ).add(system_id) + + def path_of(self, sha1: str) -> str: + """The collection path the database records for a file.""" + return str(self._db.get("files", {}).get(sha1, {}).get("path", "")) def add_emulators(self, profiles: dict[str, dict]) -> None: for name, profile in profiles.items(): diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 03e8729d..b24133fc 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -1418,6 +1418,8 @@ def _render_platform_systems( lines.append("") pad = " " else: + # The same anchor in both layouts: emulator cards link here. + lines.append(f'') lines.append(f"## {sys_link}") lines.append("") pad = "" @@ -2180,6 +2182,7 @@ def _render_emulator_file( files: list, _file_available: Callable[[dict], bool], anchor: str = "", + owner: str = "", ) -> list[str]: """Render one row of an emulator profile's file table. @@ -2305,10 +2308,27 @@ def _render_emulator_file( f"Source: {_source_ref_markdown(profile, source_ref)}" ) if consumers: - sha1 = consumers.sha1_of(f, f.get("path") or "") + sha1 = consumers.sha1_of(f, f.get("path") or "", owner=owner) + if sha1 and in_repo: + # Which copy answers this entry, linked to its collection card + # (provenance, every hash, download): an entry declaring no hash + # said "in repo" without naming the file. + held = consumers.path_of(sha1) + parts = held.split("/") + if len(parts) >= 3 and parts[0] == "bios": + page = parts[1].lower().replace(" ", "-") + details.append( + f"Collection copy: [`{held}`](../systems/{page}.md#sha1-{sha1[:12]})" + ) plats = sorted(consumers.platforms.get(sha1, ())) if sha1 else [] if plats: - plat_links = [_platform_link(p, p, "../") for p in plats] + systems_of = consumers.platform_systems.get(sha1, {}) + plat_links = [] + for p in plats: + section = sorted(systems_of.get(p, ())) + # Land on the system section that gives the destination. + fragment = f"#{section[0].replace(' ', '-')}" if section else "" + plat_links.append(f"[{p}](../platforms/{p}.md{fragment})") details.append(f"Platforms: {', '.join(plat_links)}") if dest and dest != fname and dest != fpath: @@ -2712,7 +2732,7 @@ def generate_emulator_page( anchor = base if used[base] == 1 else f"{base}-{used[base]}" lines.extend( _render_emulator_file( - f, profile, consumers, files, _file_available, anchor + f, profile, consumers, files, _file_available, anchor, name ) ) diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 15078a14..348297f1 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -4581,5 +4581,31 @@ class NullReadsAsNone(unittest.TestCase): self.assertEqual(found, []) + +class EmulatorCardsLinkTheCopyAndTheSection(unittest.TestCase): + """Dolphin's IPL.bin (GC/USA) read "in repo" without naming the copy, + did not link its collection card, and its platform links landed on the + page top instead of the GameCube section.""" + + def test_copy_and_sections(self): + from types import SimpleNamespace + + from scripts.generate_site import _render_emulator_file + + sha1 = "ef9194ab4804aa0aa8540d846caf291b28331165" + consumers = SimpleNamespace( + sha1_of=lambda entry, dest="", owner="": sha1, + path_of=lambda value: "bios/Nintendo/GameCube/GC/USA/IPL.bin", + platforms={sha1: {"batocera"}}, + platform_systems={sha1: {"batocera": {"nintendo-gamecube"}}}, + ) + entry = {"name": "IPL.bin", "path": "GC/USA/IPL.bin"} + text = "\n".join(_render_emulator_file( + entry, {"files": [entry]}, consumers, [entry], lambda f: True, "file-ipl", "dolphin", + )) + self.assertIn("../systems/nintendo.md#sha1-ef9194ab4804", text) + self.assertIn("../platforms/batocera.md#nintendo-gamecube", text) + + if __name__ == "__main__": unittest.main()