mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 04:39:06 -05:00
A part page's parent is its last crumb and is marked in the sidebar. A Next steps link pointing back in the reading run gives way to the run order, so Start here reads every page once.
140 lines
6.2 KiB
TypeScript
140 lines
6.2 KiB
TypeScript
// 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;
|
|
// - 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;
|
|
// - a pruned sidebar: in the tabs listing every system and emulator, a
|
|
// group that does not hold the page becomes one link to its overview, so
|
|
// an emulator page does not carry the links of every other emulator. The
|
|
// groups of the other tabs stay groups;
|
|
// - a part page (one console of a setup's list) names the page it belongs
|
|
// to in its `parent:` front matter: that page is its last crumb, and its
|
|
// sidebar entry is marked when the part page has none of its own;
|
|
// - a page's "Next steps" link is its next only when it reads forward: one
|
|
// pointing back to an earlier page of the run made Start here loop.
|
|
import { defineRouteMiddleware, type StarlightRouteData } from '@astrojs/starlight/route-data';
|
|
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 };
|
|
export type Crumb = { label: string; href?: string };
|
|
|
|
const BASE = import.meta.env.BASE_URL.replace(/\/?$/, '/');
|
|
const withBase = (link: string) => (link.startsWith(BASE) ? link : BASE + link.replace(/^\//, ''));
|
|
|
|
const GROUPS = sidebar.groups as Record<string, Group>;
|
|
const RUNS = sidebar.reading as { label: string; link: string }[][];
|
|
|
|
const groupOf = (trail: string[]): Group | undefined => GROUPS[trail.join('/')];
|
|
|
|
const holdsCurrent = (entry: Entry): boolean =>
|
|
entry.type === 'link' ? entry.isCurrent : entry.entries.some(holdsCurrent);
|
|
|
|
const prune = (entries: Entry[], trail: string[]): Entry[] =>
|
|
entries.map((entry) => {
|
|
if (entry.type === 'link') return entry;
|
|
const here = [...trail, entry.label];
|
|
if (holdsCurrent(entry)) return { ...entry, collapsed: false, entries: prune(entry.entries, here) };
|
|
const group = groupOf(here);
|
|
if (group?.prune && group.href) {
|
|
return { type: 'link', label: entry.label, href: withBase(group.href), isCurrent: false, badge: entry.badge, attrs: {} };
|
|
}
|
|
return { ...entry, entries: prune(entry.entries, here) };
|
|
});
|
|
|
|
// The groups above the current page, outermost first.
|
|
const trail = (entries: Entry[], labels: string[]): Crumb[] | undefined => {
|
|
for (const entry of entries) {
|
|
if (entry.type === 'link') {
|
|
if (entry.isCurrent) return [];
|
|
continue;
|
|
}
|
|
const here = [...labels, entry.label];
|
|
const below = trail(entry.entries, here);
|
|
if (below) {
|
|
const href = groupOf(here)?.href;
|
|
return [{ label: entry.label, href: href ? withBase(href) : undefined }, ...below];
|
|
}
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
// A page the navigation does not list (part of a split page) sits under the
|
|
// group holding the pages of its folder.
|
|
const folderTrail = (entries: Entry[], labels: string[], folder: string): Crumb[] => {
|
|
const links = (entry: Entry): string[] => (entry.type === 'link' ? [entry.href] : entry.entries.flatMap(links));
|
|
for (const entry of entries) {
|
|
if (entry.type !== 'group' || !links(entry).some((href) => href.startsWith(folder))) continue;
|
|
const href = groupOf([...labels, entry.label])?.href;
|
|
return [{ label: entry.label, href: href ? withBase(href) : undefined }];
|
|
}
|
|
return [];
|
|
};
|
|
|
|
// The parent's entry marked current, for a page the sidebar does not list.
|
|
const markCurrent = (entries: Entry[], href: string): Entry[] =>
|
|
entries.map((entry) =>
|
|
entry.type === 'link'
|
|
? entry.href === href
|
|
? { ...entry, isCurrent: true }
|
|
: entry
|
|
: { ...entry, entries: markCurrent(entry.entries, href) },
|
|
);
|
|
|
|
const bare = (link: string) => withBase(link).replace(/[?#].*$/, '');
|
|
|
|
const asLink = (entry: { label: string; link: string }): Link => ({
|
|
type: 'link',
|
|
label: entry.label,
|
|
href: withBase(entry.link),
|
|
isCurrent: false,
|
|
badge: undefined,
|
|
attrs: {},
|
|
});
|
|
|
|
export const onRequest = defineRouteMiddleware(async (context, next) => {
|
|
await next();
|
|
const route = context.locals.starlightRoute;
|
|
const path = context.url.pathname;
|
|
|
|
const topic = context.locals.starlightSidebarTopics?.topics.find((t) => t.isCurrent);
|
|
const parent = route.entry.data.parent;
|
|
if (parent && !route.sidebar.some(holdsCurrent)) route.sidebar = markCurrent(route.sidebar, bare(parent.link));
|
|
const crumbs: Crumb[] = [];
|
|
if (topic && withBase(topic.link) !== path && route.id !== '404') {
|
|
crumbs.push({ label: topic.label, href: withBase(topic.link) });
|
|
const segments = path.slice(BASE.length).split('/').filter(Boolean);
|
|
const groups =
|
|
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, 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);
|
|
}
|
|
const parentHref = parent ? withBase(parent.link) : undefined;
|
|
if (parent && parentHref !== path && parentHref !== crumbs.at(-1)?.href) {
|
|
crumbs.push({ label: parent.label, href: parentHref });
|
|
}
|
|
}
|
|
context.locals.rbBreadcrumbs = crumbs;
|
|
|
|
const { prev, next: following } = route.entry.data;
|
|
const run = RUNS.find((pages) => pages.some((entry) => withBase(entry.link) === path));
|
|
const position = run ? run.findIndex((entry) => withBase(entry.link) === path) : -1;
|
|
if (prev === undefined) route.pagination.prev = run && position > 0 ? asLink(run[position - 1]) : undefined;
|
|
const inOrder = run && position < run.length - 1 ? asLink(run[position + 1]) : undefined;
|
|
if (following === undefined) route.pagination.next = inOrder;
|
|
else if (run && typeof following === 'object' && following.link) {
|
|
const target = run.findIndex((entry) => bare(entry.link) === bare(following.link));
|
|
if (target !== -1 && target <= position) route.pagination.next = inOrder;
|
|
}
|
|
|
|
route.sidebar = prune(route.sidebar, [topic?.label ?? '']);
|
|
});
|