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('