mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 04:39:06 -05:00
generate_site.py writes the Starlight project in website/ and nothing else: mkdocs.yml, the MkDocs hooks, theme overrides and stylesheet go, and the wiki is written in Starlight's syntax. Both workflows set up Node with the official actions/setup-node pinned by commit, run npm ci from the committed lockfile, build with the strict link validator and check the result with validate_site.py. The search weighs a page by what it answers: Start here pages first, a system page's cards and file names before a guide citing them, deep guides, launchers, aliases and frozen copies after. Status badges, hash blocks and the export change lists leave the index. The ranking sits at the centre of a range where the 13 queries of search_gate.py all answer, in the search box itself. 839 pages, every URL and 45,611 ids match the MkDocs build. The tests read the Starlight content and syntax.
178 lines
8.2 KiB
Python
178 lines
8.2 KiB
Python
"""Pages a phone can open: split pages, light navigation, short tables of contents.
|
|
|
|
systems/game-engines/ was one 10.5 MB page, emulators/mame/ 3 MB, and each
|
|
of 600 emulator pages carried 65 KB of navigation. A large page is now a
|
|
summary with pages of its own, and every address cited before the split
|
|
still lands: a console's heading stays on the summary, a card's sha1 anchor
|
|
is looked up by content, an emulator card's anchor sits on its row.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
|
|
import generate_site # noqa: E402
|
|
import sitestarlight # noqa: E402
|
|
import siterender # noqa: E402
|
|
import sitemarkup as markup # noqa: E402
|
|
from cross_reference import FileConsumers # noqa: E402
|
|
|
|
|
|
def _file(name: str, index: int, console: str, maker: str = "Maker") -> dict:
|
|
sha1 = f"{index:012x}" + "0" * 28
|
|
return {"name": name, "sha1": sha1, "md5": f"{index:032x}", "size": 10,
|
|
"path": f"bios/{maker}/{console}/{name}"}
|
|
|
|
|
|
class ASystemPageOverItsBudgetIsSplit(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
siterender.reset_console_pages()
|
|
self.addCleanup(siterender.reset_console_pages)
|
|
self.consoles = {
|
|
"Big": [_file(f"big{i:03d}.bin", i, "Big") for i in range(60)],
|
|
"Small": [_file("small.bin", 900, "Small")],
|
|
}
|
|
|
|
def _pages(self, budget: int) -> dict:
|
|
return generate_site.system_pages(
|
|
"Maker", self.consoles, FileConsumers({"files": {}}), budget=budget)
|
|
|
|
def test_a_page_within_its_budget_stays_whole(self):
|
|
pages = self._pages(10_000_000)
|
|
self.assertEqual(list(pages), ["maker"])
|
|
self.assertIn("## Big", pages["maker"][0])
|
|
|
|
def test_the_largest_section_moves_and_is_cut_in_parts(self):
|
|
pages = self._pages(20_000)
|
|
summary = pages["maker"][0]
|
|
# Both headings stay on the summary: systems/maker/#big still lands.
|
|
self.assertIn("## Big", summary)
|
|
self.assertIn("## Small", summary)
|
|
# The small section keeps its cards in place.
|
|
self.assertIn('id="sha1-' + f"{900:012x}", summary)
|
|
parts = [stem for stem in pages if stem.startswith("maker-big")]
|
|
self.assertGreater(len(parts), 1)
|
|
self.assertEqual(parts[0], "maker-big")
|
|
for stem in parts:
|
|
self.assertIn(f"]({stem}.md)", summary)
|
|
self.assertLessEqual(len(pages[stem][0]), 20_000 + 5_000)
|
|
self.assertIn("## Files", pages[stem][0])
|
|
# Each card is on exactly one part, and the links follow it.
|
|
held = [f["sha1"] for stem in parts for f in pages[stem][1]["Big"]]
|
|
self.assertEqual(sorted(held), sorted(f["sha1"] for f in self.consoles["Big"]))
|
|
last = self.consoles["Big"][-1]
|
|
self.assertEqual(siterender.card_page(last["path"], last["sha1"]), parts[-1])
|
|
small = self.consoles["Small"][0]
|
|
self.assertEqual(siterender.card_page(small["path"], small["sha1"]), "maker")
|
|
self.assertEqual(siterender.console_href("maker", "Big"), "systems/maker.md#big")
|
|
|
|
def test_a_console_on_one_page_is_linked_directly(self):
|
|
self.consoles["Big"] = self.consoles["Big"][:12]
|
|
# The budget is measured in the Markdown it was set on.
|
|
with markup.using("mkdocs"):
|
|
whole = self._pages(10**9)["maker"][0]
|
|
pages = self._pages(len(whole) - 1)
|
|
self.assertIn("maker-big", pages)
|
|
self.assertNotIn("maker-big-2", pages)
|
|
self.assertEqual(siterender.console_href("maker", "Big", "../"),
|
|
"../systems/maker-big.md")
|
|
|
|
|
|
class AnEmulatorWithTooManyCardsIsSplit(unittest.TestCase):
|
|
def test_parts_are_balanced_and_rows_keep_the_anchors(self):
|
|
files = [{"name": f"set{i:04d}.zip", "required": i % 2 == 0, "category": "bios_zip"}
|
|
for i in range(generate_site.EMULATOR_CARDS_PER_PAGE * 2 + 1)]
|
|
parts = generate_site.emulator_parts("big", files)
|
|
self.assertEqual([stem for stem, _ in parts], ["big-files-1", "big-files-2", "big-files-3"])
|
|
sizes = [len(entries) for _, entries in parts]
|
|
self.assertLessEqual(max(sizes) - min(sizes), 1)
|
|
self.assertEqual(generate_site.emulator_parts("small", files[:3]), [])
|
|
|
|
pages = generate_site.emulator_pages(
|
|
"big", {"emulator": "Big", "type": "standalone", "files": files}, {"files": {}})
|
|
self.assertEqual(sorted(pages), ["big", "big-files-1", "big-files-2", "big-files-3"])
|
|
main = pages["big"]
|
|
anchors = generate_site.emulator_file_anchors(files)
|
|
for anchor in anchors:
|
|
self.assertIn(f'<span id="{anchor}"></span>', main)
|
|
self.assertIn('class="rb-card-index"', main)
|
|
self.assertNotIn("rb-file-title", main)
|
|
self.assertIn(f'id="{anchors[0]}" class="rb-file-title"', pages["big-files-1"])
|
|
|
|
def test_cards_are_linked_on_the_page_that_holds_them(self):
|
|
files = [{"name": f"set{i:04d}.zip"} for i in range(generate_site.EMULATOR_CARDS_PER_PAGE + 1)]
|
|
generate_site.plan_emulator_pages({"big": {"files": files}})
|
|
anchors = generate_site.emulator_file_anchors(files)
|
|
self.assertEqual(siterender.emulator_card_page("big", anchors[-1]), "big-files-2")
|
|
self.assertEqual(siterender.emulator_card_page("other", "file-x"), "other")
|
|
|
|
|
|
class APlatformSystemOverItsBudgetMoves(unittest.TestCase):
|
|
def test_the_heading_stays_and_the_list_moves(self):
|
|
files = [{"name": f"f{i:04d}.zip", "status": "ok"} for i in range(40)]
|
|
config = {("arcade", f["name"]): [{"name": f["name"], "md5": "a" * 32}] for f in files}
|
|
moved: dict[str, str] = {}
|
|
budget = generate_site.PLATFORM_SECTION_BUDGET
|
|
generate_site.PLATFORM_SECTION_BUDGET = 500
|
|
try:
|
|
lines = generate_site._render_platform_systems(
|
|
{"arcade": files}, config, {}, ("deck", "Deck"), moved)
|
|
finally:
|
|
generate_site.PLATFORM_SECTION_BUDGET = budget
|
|
text = "\n".join(lines)
|
|
self.assertIn("{ #arcade }", text)
|
|
self.assertIn("](deck-arcade.md)", text)
|
|
self.assertNotIn("f0001.zip", text)
|
|
self.assertIn("f0001.zip", moved["deck-arcade"])
|
|
self.assertIn("[Deck page](deck.md#arcade)", moved["deck-arcade"])
|
|
|
|
|
|
class TheNavigationOpensOneInitial(unittest.TestCase):
|
|
def test_a_group_of_several_initials_is_nested(self):
|
|
profiles = {
|
|
key: {"emulator": name, "systems": ["s"]}
|
|
for key, name in (("a1", "Alpha"), ("b1", "Beta"), ("b2", "bravo"), ("z", "Zed"))
|
|
}
|
|
nav = generate_site._emulator_nav(profiles)
|
|
groups = {next(iter(g)): next(iter(g.values())) for g in nav[1:]}
|
|
self.assertEqual(groups["A to C"], [
|
|
{"A": [{"Alpha": "emulators/a1.md"}]},
|
|
{"B": [{"Beta": "emulators/b1.md"}, {"bravo": "emulators/b2.md"}]},
|
|
])
|
|
self.assertEqual(groups["V to Z"], [{"Zed": "emulators/z.md"}])
|
|
|
|
|
|
class TheTableOfContentsOfACardPageStopsAtLevelTwo(unittest.TestCase):
|
|
@staticmethod
|
|
@staticmethod
|
|
def _contents(rel: str, sections: int):
|
|
import yaml # noqa: PLC0415
|
|
|
|
body = "# T\n\n" + "".join(f"## S{i}\n\n### card\n\n" for i in range(sections))
|
|
_target, text = sitestarlight.finalize_page(
|
|
rel, body, level="", weight=None, browser_title="T", description="d",
|
|
structured={}, terms={})
|
|
return yaml.safe_load(text.split("---\n")[1]).get("tableOfContents", "default")
|
|
|
|
def test_cards_leave_the_toc_and_wiki_pages_keep_level_three(self):
|
|
level_two = {"minHeadingLevel": 2, "maxHeadingLevel": 2}
|
|
for rel, sections, expected in (
|
|
("systems/sony.md", 3, level_two), ("platforms/retroarch.md", 5, level_two),
|
|
("emulators/mame.md", 3, level_two), ("wiki/faq.md", 3, "default"),
|
|
# One or two headings over hundreds of cards: no table at all,
|
|
# whose script watched every block of the page.
|
|
("emulators/mame-files-2.md", 1, False), ("systems/arcade-arcade.md", 2, False),
|
|
):
|
|
with self.subTest(rel=rel):
|
|
self.assertEqual(self._contents(rel, sections), expected)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|