diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 22daac86..12d229fe 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -2199,6 +2199,7 @@ def decorate_markdown_pages(docs: Path) -> dict[str, str]: browser_title=browser_title, description=description, structured=structured, terms=terms, search=authored.get("search") != "exclude", + logo=str(authored.get("logo") or ""), ) write_decorated(docs / target, text) # One file per page: a page that gained or lost its components @@ -2921,6 +2922,7 @@ def generate_platform_page( # The mark is drawn beside the title from the front matter: floated # beside the first line at 48 px it read as a broken image. logo_url = registry.get(name, {}).get("logo", "") + logo_url = local_images().get(logo_url, logo_url) homepage = config.get("homepage", "") version = config.get("version", "") @@ -4543,6 +4545,16 @@ def _region_words(value: object, most: int = 0) -> str: _CATEGORY_WORDS = {"bios": "BIOS", "game_data": "game data", "bios_zip": "BIOS ZIP"} +INITIAL_DIGITS = "0 to 9" + + +def emulator_initial(display: str) -> str: + """The letter an emulator is filed under, in the navigation and on the + index alike. A name that does not open on a letter goes under "0 to 9".""" + initial = display[:1].upper() + return initial if "A" <= initial <= "Z" else INITIAL_DIGITS + + def generate_emulators_index(profiles: dict) -> str: unique = { k: v for k, v in profiles.items() if v.get("type") not in ("alias", "test") @@ -4562,13 +4574,13 @@ def generate_emulators_index(profiles: dict) -> str: "", "Each page is one emulator: the files it reads, whether it starts games " "without them, and the line of its source code that says so. The " - "classification below says how a core relates to the emulator it comes " - "from, which tells how far its own files can differ.", + "kind says how a core relates to the emulator it comes from, which " + "tells how far its own files can differ.", "", f"**{len(unique):,}** emulator profiles, **{total_files:,}** files total, **{len(aliases):,}** aliases.", "", - "| Classification | Count | Description |", - "|---------------|-------|-------------|", + "| Kind | Count | Description |", + "|------|-------|-------------|", ] cls_desc = { @@ -4601,28 +4613,25 @@ def generate_emulators_index(profiles: dict) -> str: continue label = CLS_LABELS.get(cls, cls) desc = cls_desc.get(cls, "") - lines.append(f"| [{label}](#{cls}) | {len(entries)} | {desc} |") + lines.append(f"| {label} | {len(entries)} | {desc} |") lines.append("") - for cls in cls_order: - entries = by_class.get(cls, []) - if not entries: - continue - label = CLS_LABELS.get(cls, cls) - desc = cls_desc.get(cls, "") - lines.extend( - [ - f"## {label} {{ #{cls} }}", - "", - f"{desc}. {_count(len(entries), 'profile')}.", - "", - "| Emulator | Systems | Files |", - "|--------|---------|-------|", - ] - ) - + # One section per initial, the groups the navigation draws: its letter + # links land on the section of the same name. + by_initial: dict[str, list[tuple[str, dict]]] = {} + for name, p in unique.items(): + by_initial.setdefault(emulator_initial(str(p.get("emulator", name))), []).append((name, p)) + for initial in sorted(by_initial, key=lambda g: (g != INITIAL_DIGITS, g)): + lines.extend([ + f"## {initial}", + "", + "| Emulator | Kind | Systems | Files |", + "|--------|------|---------|-------|", + ]) + entries = sorted(by_initial[initial], key=lambda e: str(e[1].get("emulator", e[0])).casefold()) for name, p in entries: emu_name = p.get("emulator", name) + cls = p.get("core_classification", "other") systems = list(dict.fromkeys( _named_system_link(s, "../") for s in p.get("systems") or [])) files = p.get("files", []) @@ -4631,7 +4640,7 @@ def generate_emulators_index(profiles: dict) -> str: sys_str += f" and {len(systems) - 3} more" file_count = len(files) file_str = f"{file_count:,}" if file_count else "-" - lines.append(f"| [{emu_name}]({name}.md) | {sys_str} | {file_str} |") + lines.append(f"| [{emu_name}]({name}.md) | {CLS_LABELS.get(cls, cls)} | {sys_str} | {file_str} |") lines.append("") if aliases: @@ -7348,11 +7357,9 @@ def _emulator_nav(profiles: dict) -> list: groups: dict[str, list[tuple[str, str]]] = {} for key, profile in nav_emulators(profiles).items(): display = str(profile.get("emulator", key)) - initial = display[:1].upper() - label = initial if "A" <= initial <= "Z" else "0 to 9" - groups.setdefault(label, []).append((display, f"emulators/{key}.md")) + groups.setdefault(emulator_initial(display), []).append((display, f"emulators/{key}.md")) nav: list = ["emulators/index.md"] - for label in sorted(groups, key=lambda g: (g != "0 to 9", g)): + for label in sorted(groups, key=lambda g: (g != INITIAL_DIGITS, g)): items = sorted(groups[label], key=lambda e: e[0].casefold()) nav.append({label: [{display: path} for display, path in items]}) return nav @@ -7895,7 +7902,7 @@ def _generate(args: argparse.Namespace) -> None: print(f"Removed {stale} page(s) nothing produced this run") -from sitediagrams import prepare_page, unresolved_images # noqa: E402,F401 +from sitediagrams import local_images, prepare_page, unresolved_images # noqa: E402,F401 from sitewrite import ( # noqa: E402,F401 _produced, _record, diff --git a/scripts/sitestarlight.py b/scripts/sitestarlight.py index 94bba427..0b10e2f0 100644 --- a/scripts/sitestarlight.py +++ b/scripts/sitestarlight.py @@ -406,6 +406,7 @@ def finalize_page( structured: dict, terms: dict[str, str], search: bool = True, + logo: str = "", ) -> tuple[str, str]: """The Starlight file for a page body: its path under the content collection and its text.""" @@ -446,6 +447,8 @@ def finalize_page( front["slug"] = stem if level: front["level"] = level + if logo: + front["logo"] = logo if weight: front["searchWeight"] = weight if rel == NOT_FOUND: diff --git a/tests/test_site_r2_emulators.py b/tests/test_site_r2_emulators.py index 71526367..48957536 100644 --- a/tests/test_site_r2_emulators.py +++ b/tests/test_site_r2_emulators.py @@ -172,10 +172,10 @@ class TheIndexNamesEmulatorsAndSystems(unittest.TestCase): "duckstation": {"emulator": "DuckStation", "core_classification": "official_port", "systems": ["sony-playstation"], "files": _files(3)}, }) - self.assertIn("| Emulator | Systems | Files |", page) + self.assertIn("| Emulator | Kind | Systems | Files |", page) + self.assertIn("\n## D\n", page) self.assertNotIn("| Engine |", page) self.assertNotIn(" -- ", page) - self.assertIn("1 profile.", page) row = next(line for line in page.splitlines() if line.startswith("| [DuckStation]")) self.assertIn(f"[{siterender.system_name('sony-playstation')}]", row) self.assertIn("(../systems/sony.md#playstation)", row) diff --git a/tests/test_site_r2_nav.py b/tests/test_site_r2_nav.py index 0b43406b..07a89c55 100644 --- a/tests/test_site_r2_nav.py +++ b/tests/test_site_r2_nav.py @@ -65,9 +65,20 @@ def _present(nav: list) -> set[str]: # follow the labels in platforms/_guides.yml instead of a copy of them. from siteguides import DEVICE_GROUPS # noqa: E402 +def _index_anchors(markdown: str) -> set[str]: + """The heading ids a generated page carries once written.""" + _title, title_id, body = sitestarlight.split_title(markdown) + return sitestarlight.heading_ids(sitestarlight.pin_heading_ids(body, title_id)) + + ANCHORS = { "platforms/index.md": {sitestarlight.toc_slug(label) for label, _ in DEVICE_GROUPS.values()}, "systems/index.md": {"consoles-and-computers"}, + "emulators/index.md": _index_anchors(generate_site.generate_emulators_index({ + "duckstation": {"emulator": "DuckStation", "systems": ["sony-playstation"]}, + "dolphin": {"emulator": "Dolphin", "systems": ["nintendo-gamecube"]}, + "mame": {"emulator": "MAME", "systems": ["arcade"]}, + })), } @@ -109,7 +120,7 @@ class GroupsAreGroups(unittest.TestCase): self.assertEqual(explore["Explore/Systems"], {"href": "/retrobios/systems/", "prune": True}) self.assertEqual(explore["Explore/Systems/Consoles and computers"]["href"], "/retrobios/systems/#consoles-and-computers") - self.assertEqual(explore["Explore/Emulators/D"], {"href": "/retrobios/emulators/", "prune": True}) + self.assertEqual(explore["Explore/Emulators/D"], {"href": "/retrobios/emulators/#d", "prune": True}) def test_no_group_points_at_one_of_its_own_pages(self): """The overview a group opens on is its own index page or a section of @@ -138,6 +149,17 @@ class EmulatorsOneLevelDeep(unittest.TestCase): self.assertEqual(nav, ["emulators/index.md", {"D": [ {"Dolphin": "emulators/dolphin.md"}, {"DuckStation": "emulators/duckstation.md"}]}]) + def test_every_initial_has_its_section_on_the_index(self): + """A letter in the sidebar lands on the section of that letter.""" + from common import load_emulator_profiles + + profiles = load_emulator_profiles(str(REPO_ROOT / "emulators")) + anchors = _index_anchors(generate_site.generate_emulators_index(profiles)) + for group in generate_site._emulator_nav(profiles)[1:]: + (label, _pages), = group.items() + with self.subTest(initial=label): + self.assertIn(sitestarlight.toc_slug(label), anchors) + def test_every_initial_holds_pages(self): from common import load_emulator_profiles diff --git a/website/src/routeData.ts b/website/src/routeData.ts index 0c4cd1f1..de7626fb 100644 --- a/website/src/routeData.ts +++ b/website/src/routeData.ts @@ -92,9 +92,8 @@ export const onRequest = defineRouteMiddleware(async (context, next) => { trail(route.sidebar, [topic.label]) ?? (segments.length > 1 ? folderTrail(route.sidebar, [topic.label], `${BASE}${segments[0]}/`) : []); for (const crumb of groups) { - // A group introduced by the page itself is the page; one with no - // section of its own (the initial D of the emulators) adds nothing - // to the crumb before it. + // A group introduced by the page itself is the page, and one with + // no section of its own adds nothing to the crumb before it. if (crumb.href === path) continue; if (crumb.href && crumb.href === crumbs.at(-1)?.href) continue; crumbs.push(crumb); diff --git a/website/src/styles/retrobios.css b/website/src/styles/retrobios.css index f937e088..727a5133 100644 --- a/website/src/styles/retrobios.css +++ b/website/src/styles/retrobios.css @@ -217,18 +217,6 @@ .rb-stat-value { display: block; font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; } .rb-stat-label { display: block; font-size: 0.75rem; color: var(--rb-text-secondary); } -/* Logos float right beside the opening paragraph */ -.sl-markdown-content img[align='right'] { - float: right; - width: 56px; - height: 56px; - object-fit: contain; - margin: 0 0 0.6rem 1rem; - padding: 4px; - border: 1px solid var(--rb-border); - border-radius: 4px; - background: #fff; -} .sl-markdown-content img[src*='assets/logos/'] { padding: 2px; border-radius: 2px; background: #fff; } /* A system icon beside a console heading holds its box before it loads: it pushed every card below it down once it arrived. */