feat: setup tab, letter crumbs, console words

get/ pages join the tab of the setup pages of the same name, so no page is excluded from every tab and the tabs keep their names. Groups filing emulators by first letter are no crumb. A console page carries "<name> BIOS" in its terms field. The search gate runs the console, checker, schema, license and iOS queries.
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 05:39:59 +01:00
1 parent f80dcb6d3c
commit 15f7c72077
5 files changed
+92 -22

No files matched your search

+62 -15
View File
@@ -417,10 +417,6 @@ def page_aliases(rel: str) -> list[str]:
return list(_page_aliases.get(rel, []))
# Search fields a page's own front matter fills, never typed here.
SEARCH_FIELDS = ("consoles", "terms")
def field_words(value: object) -> list[str]:
"""The words of a front matter search field: a list, or one string
with its words or phrases split at commas."""
@@ -431,6 +427,14 @@ def field_words(value: object) -> list[str]:
return []
def console_words(names: list[str]) -> list[str]:
"""What a visitor types for a console: its name with "BIOS". A page that
answers "No file needed" says BIOS only where the search does not read,
so "nes bios" found NES.emu and never the NES page. As a task word it
weighs like one: in the consoles field, NES.emu's title still won."""
return [f"{name} BIOS" for name in names]
def _neighbour(rel: str, value: object) -> object:
"""A `prev:` or `next:` the page states, its link resolved like any
other link of the page."""
@@ -503,8 +507,9 @@ def finalize_page(
r'<span data-pagefind-ignore class="\1"', body)
if rel.startswith("systems/"):
body += file_names_meta(body) + search_field("aliases", page_aliases(rel))
for key in SEARCH_FIELDS:
body += search_field(key, field_words(authored.get(key)))
consoles = field_words(authored.get("consoles"))
body += search_field("consoles", consoles)
body += search_field("terms", field_words(authored.get("terms")) + console_words(consoles))
components = used_components(body)
stem = rel.rsplit(".", 1)[0]
if components:
@@ -594,12 +599,33 @@ def reads_in_order(rel: str) -> bool:
return not rel.startswith(CARD_PAGES) or rel.endswith("/index.md")
_DIGIT_RANGE = re.compile(r"[0-9] to [0-9]")
def by_initial(label: str, children: list[dict]) -> bool:
"""A group that files its pages by the first character of their names
(A, B, "0 to 9"): a way through a long list, not a place, so it is no
crumb."""
if not children or any("items" in child for child in children):
return False
firsts = {str(child["label"])[:1].upper() for child in children}
if len(label) == 1:
return firsts == {label}
return bool(_DIGIT_RANGE.fullmatch(label)) and not any("A" <= first <= "Z" for first in firsts)
def sidebar(nav: list, present: set[str], anchors: dict[str, set[str]] | None = None) -> dict:
"""The navigation as Starlight topics, one per tab, with what the route
middleware (website/src/routeData.ts) reads beside it.
A page the navigation does not list (the parts of a split page) joins
the topic that holds most pages of its directory.
the topic that holds most pages of its directory. In a directory the
navigation lists nothing of (get/), it joins the topic of the pages of
the same name, and a page with neither joins the first topic: no page
is left out of every tab.
A group that files names by their first letter is marked `crumb: false`:
the breadcrumbs skip it.
`groups` gives each group, keyed by the labels from its tab down, the
page that introduces it: its own overview page, or the heading named
@@ -649,7 +675,8 @@ def sidebar(nav: list, present: set[str], anchors: dict[str, set[str]] | None =
children = [child for child in (item(v, topic, here, own or parent) for v in value) if child]
if not children:
return None
groups["/".join(here)] = {"href": href, "prune": topic in PRUNED_TABS and href is not None}
groups["/".join(here)] = {"href": href, "prune": topic in PRUNED_TABS and href is not None,
"crumb": not by_initial(label, children)}
return {"label": label, "items": children, "collapsed": True}
topics = []
@@ -670,15 +697,35 @@ def sidebar(nav: list, present: set[str], anchors: dict[str, set[str]] | None =
# is the way back in. Excluded from every tab, the theme named the tabs
# by their position, 0 to 4.
extra: dict[str, list[str]] = {topics[0]["id"]: ["/404"]} if topics else {}
exclude: list[str] = []
for rel in sorted(present - owner.keys()):
tally = votes.get(rel.rpartition("/")[0])
path = "/" + (_sidebar_link(rel).strip("/") or "index")
# A folder the navigation lists nothing of (get/, one download page per
# setup) has no vote, and excluded from every tab its pages had the
# tabs numbered 0 to 4 too. Its pages take the tab of the pages of the
# same name (get/batocera.md, platforms/batocera.md), and its overview
# the tab its pages took.
by_name: dict[str, dict[str, int]] = {}
for rel, topic in owner.items():
tally = by_name.setdefault(PurePosixPath(rel).name, {})
tally[topic] = tally.get(topic, 0) + 1
unlisted = sorted(present - owner.keys())
adopted: dict[str, dict[str, int]] = {}
for rel in unlisted:
folder, name = rel.rpartition("/")[0], PurePosixPath(rel).name
if folder in votes or name == "index.md":
continue
tally = adopted.setdefault(folder, {})
for topic, count in by_name.get(name, {}).items():
tally[topic] = tally.get(topic, 0) + count
for rel in unlisted:
folder = rel.rpartition("/")[0]
tally = votes.get(folder) or adopted.get(folder)
if tally:
extra.setdefault(max(tally, key=tally.get), []).append(path)
topic = max(tally, key=tally.get)
elif topics:
topic = topics[0]["id"]
else:
exclude.append(path)
return {"topics": topics, "options": {"topics": extra, "exclude": exclude},
continue
extra.setdefault(topic, []).append("/" + (_sidebar_link(rel).strip("/") or "index"))
return {"topics": topics, "options": {"topics": extra, "exclude": []},
"groups": groups, "reading": _reading(nav, present)}
+4 -2
View File
@@ -117,10 +117,12 @@ class GroupsAreGroups(unittest.TestCase):
def test_explore_groups_collapse_to_their_overview(self):
explore = {key: value for key, value in self.groups.items() if key.startswith("Explore/")}
self.assertEqual(explore["Explore/Systems"], {"href": "/retrobios/systems/", "prune": True})
self.assertEqual(explore["Explore/Systems"],
{"href": "/retrobios/systems/", "prune": True, "crumb": True})
self.assertEqual(explore["Explore/Systems/Consoles and computers"]["href"],
"/retrobios/systems/#consoles-and-computers")
self.assertEqual(explore["Explore/Emulators/D"], {"href": "/retrobios/emulators/#d", "prune": True})
self.assertEqual(explore["Explore/Emulators/D"],
{"href": "/retrobios/emulators/#d", "prune": True, "crumb": False})
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
+2 -1
View File
@@ -120,7 +120,8 @@ class FieldsFromFrontMatter(unittest.TestCase):
"terms": "bios not found, bios missing",
})["text"]
self.assertIn('data-pagefind-meta="consoles:Neo Geo Neo Geo Pocket Color"', text)
self.assertIn('data-pagefind-meta="terms:bios not found bios missing"', text)
self.assertIn('data-pagefind-meta="terms:bios not found bios missing Neo Geo BIOS '
'Neo Geo Pocket Color BIOS"', text)
# A field value is an attribute: none of it reaches an excerpt.
self.assertNotRegex(text, r">[^<]*bios missing[^<]*<")
+8 -3
View File
@@ -1,7 +1,8 @@
// What each page knows of its place in the site, computed once per page
// from what scripts/sitestarlight.py writes beside the sidebar:
// - breadcrumbs: the tab and the groups above the page, each linked to the
// page or the section that introduces it, never to a sibling page;
// page or the section that introduces it, never to a sibling page. A
// group of names by their first letter (A, B) is no crumb;
// - previous and next: the reading order of `reading`. Start here runs on
// into Your setup, every other tab is read on its own, and a system,
// emulator or platform page has none;
@@ -19,7 +20,7 @@ import sidebar from './generated/sidebar.json' with { type: 'json' };
type Entry = StarlightRouteData['sidebar'][number];
type Link = Extract<Entry, { type: 'link' }>;
type Group = { href: string | null; prune: boolean };
type Group = { href: string | null; prune: boolean; crumb?: boolean };
export type Crumb = { label: string; href?: string };
const BASE = import.meta.env.BASE_URL.replace(/\/?$/, '/');
@@ -55,7 +56,11 @@ const trail = (entries: Entry[], labels: string[]): Crumb[] | undefined => {
const here = [...labels, entry.label];
const below = trail(entry.entries, here);
if (below) {
const href = groupOf(here)?.href;
const group = groupOf(here);
// A group of names by their first letter is a way through the
// list, not a place: the crumb stops at the list.
if (group?.crumb === false) return below;
const href = group?.href;
return [{ label: entry.label, href: href ? withBase(href) : undefined }, ...below];
}
}
+16 -1
View File
@@ -63,6 +63,13 @@ QUERIES: dict[str, tuple[tuple[str, ...], int]] = {
"psx bios": (("systems/sony",), 3),
"dc bios missing": (("systems/sega", "emulators/flycast/"), 3),
"gba bios": (("systems/nintendo",), 3),
# A console whose page answers "No file needed" says BIOS only where the
# search does not read: its `terms` field carries "<name> BIOS".
"snes bios": (("systems/nintendo-snes/",), 1),
"nes bios": (("systems/nintendo-nes/",), 1),
"gamecube bios": (("systems/nintendo-gamecube/",), 1),
"game boy bios": (("systems/nintendo-game-boy/",), 1),
"3ds bios": (("systems/nintendo-3ds/",), 1),
# The words an emulator prints, and the page that answers them.
"bios not found": (("wiki/troubleshooting/",), 3),
"please insert playstation cd-rom": (("wiki/troubleshooting/",), 3),
@@ -74,12 +81,20 @@ QUERIES: dict[str, tuple[tuple[str, ...], int]] = {
# Checking a dump is what the Find a file page does.
"check my dump": (("files/",), 1),
"md5 check": (("files/",), 1),
"is my bios correct": (("files/",), 3),
# A folder is checked by the setup's command, which troubleshooting runs.
"check my bios folder": (("wiki/troubleshooting/",), 3),
# A tool author's words: the data reference or the installer's.
"manifest": (("data/", "wiki/installer/"), 3),
"events": (("data/", "wiki/installer/"), 3),
"exit code": (("data/", "wiki/installer/"), 3),
"rate limit": (("data/", "wiki/installer/"), 3),
"ios": (("wiki/faq/",), 3),
"schema": (("data/",), 3),
"json schema": (("data/",), 3),
"license": (("data/", "wiki/legal/"), 3),
"check bios folder script": (("data/",), 3),
# The iPhone and iPad answer: copy the files to the app, no terminal.
"ios": (("which-pack/", "wiki/faq/"), 3),
# A device name: the guide of the handhelds that run it.
"r36s": (("platforms/", "which-pack/"), 3),
"steam deck": (("platforms/emudeck/", "platforms/retrodeck/", "which-pack/"), 1),