From 5b1120aeb57d009211c92f2ab1645b5080e0d93e Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:36:04 +0200 Subject: [PATCH 1/3] feat: rank console pages above setup lists A setup's list for one console is a reference page kept out of the search index, naming its setup as parent. System pages carry file names and the platforms' own ids as attribute search fields, never as text. Arcade gets its own sidebar group. --- scripts/generate_site.py | 28 ++++++++++++++++++--- scripts/sitestarlight.py | 53 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index df4b4a93..21588d04 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -2205,6 +2205,7 @@ def decorate_markdown_pages(docs: Path) -> dict[str, str]: description=description, structured=structured, terms=terms, search=authored.get("search") != "exclude", logo=str(authored.get("logo") or ""), + parent=authored.get("parent"), ) write_decorated(docs / target, text) # One file per page: a page that gained or lost its components @@ -7509,12 +7510,19 @@ def _emulator_nav(profiles: dict) -> list: def _system_nav(manufacturers: dict) -> list: - """Manufacturer pages, the machines apart from the game and engine trees.""" - machines, games = [], [] + """Manufacturer pages, the machines apart from the arcade boards and the + game and engine trees: an arcade page's crumb read "Consoles and + computers".""" + machines, arcade, games = [], [], [] for mfr in sorted(manufacturers, key=str.casefold): entry = {mfr: f"systems/{_slug(mfr)}.md"} - (games if mfr in GAME_DATA_TOPS else machines).append(entry) + if mfr in GAME_DATA_TOPS: + games.append(entry) + else: + (arcade if mfr == "Arcade" else machines).append(entry) nav: list = ["systems/index.md", {"Consoles and computers": machines}] + if arcade: + nav.append({"Arcade": arcade}) if games: nav.append({"Game engines and PC games": games}) return nav @@ -7904,6 +7912,20 @@ def _generate(args: argparse.Namespace) -> None: if stem in coverages: raise RuntimeError(f"a system page takes the platform {stem}'s address") PART_PAGES.add(f"platforms/{stem}.md") + # One console of one setup's list, a reference: "playstation + # bios" found five setups' lists before the console's page. + # Weighed down, they still won on their title, which the page + # weight does not reach, and their file list is already kept out + # of the index: the title alone, found again on the setup's page + # and the console's, is all the search held of them. + SEARCH_DEMOTED.add(f"platforms/{stem}.md") + front = _front_matter(page) + if front: + page = page[page.find("\n---\n", 4) + 5:].lstrip("\n") + front.update(level="reference", search="exclude", + parent={"label": cov["platform"], "link": f"{name}.md"}) + page = "---\n" + yaml.safe_dump(front, sort_keys=False, allow_unicode=True) \ + + "---\n\n" + page write_if_changed(str(docs / "platforms" / f"{stem}.md"), page) PAGE_DATA[f"platforms/{name}.md"] = platform_structured_data(name, cov) diff --git a/scripts/sitestarlight.py b/scripts/sitestarlight.py index 0b10e2f0..5623c57b 100644 --- a/scripts/sitestarlight.py +++ b/scripts/sitestarlight.py @@ -368,6 +368,17 @@ def split_title(markdown: str) -> tuple[str, str, str]: _CARD_NAME = re.compile(r'
([^<]+)')
+def search_field(key: str, words: list[str]) -> str:
+ """A search field of the page, held in an attribute of an empty
+ element: the text of an element is indexed and shown as the result's
+ excerpt (a run of 250 file names). Pagefind splits several fields at a
+ comma, so a value carries none."""
+ value = " ".join(word.replace(",", " ") for word in words).strip()
+ if not value:
+ return ""
+ return f'\n\n'
+
+
def file_names_meta(body: str) -> str:
"""The names of the files a system page holds cards for, as a search
field of the page. A page of 250 cards is long, and the search weighs a
@@ -375,9 +386,35 @@ def file_names_meta(body: str) -> str:
after every short page citing the name. The field is weighted in the
site's ranking (astro.config.mjs, metaWeights.files)."""
names = sorted({unescape(name) for name in _CARD_NAME.findall(body)})
- if not names:
- return ""
- return f'\n{html.escape(" ".join(names))}\n'
+ return search_field("files", names)
+
+
+# systems/ page -> the platforms' own ids for the consoles it holds, built
+# on the first system page, once the generator has installed the sections.
+_page_aliases: dict[str, list[str]] = {}
+_page_aliases_built = [False]
+
+
+def page_aliases(rel: str) -> list[str]:
+ """The names the platforms file the consoles of a system page under
+ (psx, dreamcast, gba), from siterender.system_search_aliases(): "psx
+ bios" finds the PlayStation page though the page never says psx."""
+ if not _page_aliases_built[0]:
+ import siterender
+
+ by_page: dict[str, set[str]] = {}
+ for sys_id in list(siterender._system_page_map):
+ section = siterender.system_section(sys_id)
+ names = siterender.system_search_aliases(sys_id)
+ if not section or not names:
+ continue
+ target = siterender.console_href(*section).partition("#")[0]
+ by_page.setdefault(urllib.parse.unquote(target), set()).update(names)
+ _page_aliases.clear()
+ _page_aliases.update({page: sorted(names, key=str.casefold)
+ for page, names in by_page.items()})
+ _page_aliases_built[0] = bool(siterender._system_page_map)
+ return list(_page_aliases.get(rel, []))
_NEXT_STEPS = re.compile(r"^## Next steps\b[^\n]*\n+(?Pscph5501.bina,b.binno card
"), "") + + def test_system_pages_carry_the_platforms_names(self) -> None: + page = DOCS / "systems" / "sony-playstation.md" + if not page.is_file(): + self.skipTest("site pages not generated") + text = page.read_text(encoding="utf-8") + match = re.search(r'data-pagefind-meta="aliases:([^"]*)"', text) + self.assertIsNotNone(match, "no aliases field on the PlayStation page") + self.assertIn("psx", unescape(match[1]).split()) + dreamcast = (DOCS / "systems" / "sega-dreamcast.md").read_text(encoding="utf-8") + self.assertRegex(dreamcast, r'data-pagefind-meta="aliases:[^"]*\bdc\b') + + +class MovedPlatformPages(unittest.TestCase): + def test_a_setups_list_for_one_console_is_a_reference(self) -> None: + source = Path(generate_site.__file__).read_text(encoding="utf-8") + block = source[source.index("for stem, page in moved.items():"):] + block = block[: block.index("PAGE_DATA[f\"platforms/{name}.md\"]")] + self.assertIn('SEARCH_DEMOTED.add(f"platforms/{stem}.md")', block) + self.assertIn('level="reference"', block) + self.assertIn('search="exclude"', block) + self.assertIn("parent=", block) + + def test_generated_pages_weigh_as_references_and_name_their_setup(self) -> None: + pages = _moved_platform_pages() + if not pages: + self.skipTest("site pages not generated") + for path in pages: + front = _front(path.read_text(encoding="utf-8")) + with self.subTest(page=path.name): + self.assertIn("\nlevel: reference\n", f"\n{front}\n") + self.assertIn("\nsearchWeight: 0.5\n", f"\n{front}\n") + # The title alone was indexed, and it outranked the console. + self.assertIn("\npagefind: false\n", f"\n{front}\n") + self.assertRegex(front, r"parent:\n label: .+\n link: /retrobios/platforms/[a-z0-9_-]+/\n") + + def test_parent_link_resolves_against_the_page(self) -> None: + _target, text = sitestarlight.finalize_page( + "platforms/retroarch-sony-playstation.md", "# RetroArch: Sony PlayStation\n\nBody.\n", + level="reference", weight=0.5, browser_title="x", description="d", + structured={}, terms={}, parent={"label": "RetroArch", "link": "retroarch.md"}, + ) + self.assertIn("parent:\n label: RetroArch\n link: /retrobios/platforms/retroarch/\n", text) + + def test_the_schema_accepts_parent(self) -> None: + self.assertRegex(CONTENT_CONFIG.read_text(encoding="utf-8"), + r"parent: z\.object\(\{ label: z\.string\(\), link: z\.string\(\) \}\)") + + +class ArcadeGroup(unittest.TestCase): + def test_arcade_is_not_a_console_or_computer(self) -> None: + nav = generate_site._system_nav({"Sony": {}, "Arcade": {}, "Sega": {}}) + groups = {next(iter(entry)): entry[next(iter(entry))] for entry in nav if isinstance(entry, dict)} + self.assertEqual(groups["Arcade"], [{"Arcade": "systems/arcade.md"}]) + self.assertNotIn({"Arcade": "systems/arcade.md"}, groups["Consoles and computers"]) + + +class RouteMiddleware(unittest.TestCase): + def test_next_steps_link_is_kept_only_forward(self) -> None: + source = ROUTE.read_text(encoding="utf-8") + self.assertIn("target <= position", source) + + def test_parent_is_a_crumb_and_marked(self) -> None: + source = ROUTE.read_text(encoding="utf-8") + self.assertIn("route.entry.data.parent", source) + self.assertIn("markCurrent(route.sidebar", source) + + +def _gate_queries() -> dict: + """The search gate's QUERIES, read from its source: the tool is run + with node beside it, not imported as a package.""" + tree = ast.parse(SEARCH_GATE.read_text(encoding="utf-8")) + for node in tree.body: + if isinstance(node, ast.AnnAssign) and getattr(node.target, "id", "") == "QUERIES": + return ast.literal_eval(node.value) + raise AssertionError("no QUERIES in search_gate.py") + + +class SearchGateQueries(unittest.TestCase): + def test_the_audits_queries_are_gated(self) -> None: + for query in ("playstation bios", "dreamcast bios", "neo geo bios", "bios not found", + "offline install", "r36s", "wonderswan color", "dc bios missing", + "which mame version", "psx bios"): + self.assertIn(query, _gate_queries()) + + def test_prefixes_match_a_consoles_own_page(self) -> None: + pages, _rank = _gate_queries()["playstation bios"] + self.assertTrue(any("systems/sony-playstation/".startswith(p) for p in pages)) + + +def _html(url: str) -> str: + rel = url[len("/retrobios/"):] + path = DIST / rel / "index.html" + return path.read_text(encoding="utf-8") if path.is_file() else "" + + +def _pagination(text: str) -> tuple[str | None, str | None]: + start = text.find('