mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 07:33:45 -06:00
chore: merge main
This commit is contained in:
commit
dd64434122
782 files changed
+13985
-1777
No files matched your search
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"platforms/batocera-acorn-bbc-micro.md": "platforms/batocera.md#acorn-bbc-micro",
|
||||
"platforms/batocera-apple-ii.md": "platforms/batocera.md#apple-ii",
|
||||
"platforms/batocera-apple-macintosh-ii.md": "platforms/batocera.md#apple-macintosh-ii",
|
||||
"platforms/batocera-archimedes.md": "platforms/batocera.md#archimedes",
|
||||
"platforms/batocera-atari-st.md": "platforms/batocera.md#atari-st",
|
||||
"platforms/batocera-commodore-amiga.md": "platforms/batocera.md#commodore-amiga",
|
||||
"platforms/batocera-elektronika-bk.md": "platforms/batocera.md#elektronika-bk",
|
||||
"platforms/batocera-microsoft-msx.md": "platforms/batocera.md#microsoft-msx",
|
||||
"platforms/batocera-nintendo-ds.md": "platforms/batocera.md#nintendo-ds",
|
||||
"platforms/batocera-sega-dreamcast-arcade.md": "platforms/batocera.md#sega-dreamcast-arcade",
|
||||
"platforms/batocera-sony-playstation.md": "platforms/batocera.md#sony-playstation",
|
||||
"platforms/emuelec-microsoft-msx.md": "platforms/emuelec.md#microsoft-msx",
|
||||
"platforms/knulli-acorn-bbc-micro.md": "platforms/knulli.md#acorn-bbc-micro",
|
||||
"platforms/knulli-apple-macintosh-ii.md": "platforms/knulli.md#apple-macintosh-ii",
|
||||
"platforms/knulli-archimedes.md": "platforms/knulli.md#archimedes",
|
||||
"platforms/knulli-atari-st.md": "platforms/knulli.md#atari-st",
|
||||
"platforms/knulli-commodore-amiga.md": "platforms/knulli.md#commodore-amiga",
|
||||
"platforms/knulli-sega-dreamcast-arcade.md": "platforms/knulli.md#sega-dreamcast-arcade",
|
||||
"platforms/lakka-arcade.md": "platforms/lakka.md#arcade",
|
||||
"platforms/lakka-commodore-amiga.md": "platforms/lakka.md#commodore-amiga",
|
||||
"platforms/lakka-nec-pc-98.md": "platforms/lakka.md#nec-pc-98",
|
||||
"platforms/lakka-nintendo-ds.md": "platforms/lakka.md#nintendo-ds",
|
||||
"platforms/lakka-nintendo-gamecube.md": "platforms/lakka.md#nintendo-gamecube",
|
||||
"platforms/lakka-nintendo-sgb.md": "platforms/lakka.md#nintendo-sgb",
|
||||
"platforms/lakka-nintendo-snes.md": "platforms/lakka.md#nintendo-snes",
|
||||
"platforms/lakka-scummvm.md": "platforms/lakka.md#scummvm",
|
||||
"platforms/lakka-sega-dreamcast-arcade.md": "platforms/lakka.md#sega-dreamcast-arcade",
|
||||
"platforms/lakka-sega-saturn.md": "platforms/lakka.md#sega-saturn",
|
||||
"platforms/lakka-sinclair-zx-spectrum.md": "platforms/lakka.md#sinclair-zx-spectrum",
|
||||
"platforms/lakka-sony-playstation-2.md": "platforms/lakka.md#sony-playstation-2",
|
||||
"platforms/lakka-sony-playstation.md": "platforms/lakka.md#sony-playstation",
|
||||
"platforms/recalbox-atari-st.md": "platforms/recalbox.md#atari-st",
|
||||
"platforms/recalbox-commodore-amiga.md": "platforms/recalbox.md#commodore-amiga",
|
||||
"platforms/recalbox-microsoft-msx.md": "platforms/recalbox.md#microsoft-msx",
|
||||
"platforms/retroarch-arcade.md": "platforms/retroarch.md#arcade",
|
||||
"platforms/retroarch-commodore-amiga.md": "platforms/retroarch.md#commodore-amiga",
|
||||
"platforms/retroarch-nec-pc-engine.md": "platforms/retroarch.md#nec-pc-engine",
|
||||
"platforms/retroarch-nintendo-ds.md": "platforms/retroarch.md#nintendo-ds",
|
||||
"platforms/retroarch-nintendo-gamecube.md": "platforms/retroarch.md#nintendo-gamecube",
|
||||
"platforms/retroarch-nintendo-sgb.md": "platforms/retroarch.md#nintendo-sgb",
|
||||
"platforms/retroarch-nintendo-snes.md": "platforms/retroarch.md#nintendo-snes",
|
||||
"platforms/retroarch-scummvm.md": "platforms/retroarch.md#scummvm",
|
||||
"platforms/retroarch-sega-dreamcast-arcade.md": "platforms/retroarch.md#sega-dreamcast-arcade",
|
||||
"platforms/retroarch-sega-saturn.md": "platforms/retroarch.md#sega-saturn",
|
||||
"platforms/retroarch-sinclair-zx-spectrum.md": "platforms/retroarch.md#sinclair-zx-spectrum",
|
||||
"platforms/retroarch-sony-playstation-2.md": "platforms/retroarch.md#sony-playstation-2",
|
||||
"platforms/retroarch-sony-playstation.md": "platforms/retroarch.md#sony-playstation",
|
||||
"platforms/retrobat-atari-st.md": "platforms/retrobat.md#atari-st",
|
||||
"platforms/retrobat-commodore-amiga.md": "platforms/retrobat.md#commodore-amiga",
|
||||
"platforms/retrobat-microsoft-msx.md": "platforms/retrobat.md#microsoft-msx",
|
||||
"platforms/retrobat-nec-pc-engine.md": "platforms/retrobat.md#nec-pc-engine",
|
||||
"platforms/retrobat-nintendo-ds.md": "platforms/retrobat.md#nintendo-ds",
|
||||
"platforms/retrobat-sega-saturn.md": "platforms/retrobat.md#sega-saturn",
|
||||
"platforms/retrodeck-arcade-2.md": "platforms/retrodeck.md#arcade",
|
||||
"platforms/retrodeck-arcade-3.md": "platforms/retrodeck.md#arcade",
|
||||
"platforms/retrodeck-arcade-4.md": "platforms/retrodeck.md#arcade",
|
||||
"platforms/retrodeck-arcade.md": "platforms/retrodeck.md#arcade",
|
||||
"platforms/retrodeck-atari-400-800.md": "platforms/retrodeck.md#atari-400-800",
|
||||
"platforms/retrodeck-microsoft-msx.md": "platforms/retrodeck.md#microsoft-msx",
|
||||
"platforms/retrodeck-nintendo-snes.md": "platforms/retrodeck.md#nintendo-snes",
|
||||
"platforms/retrodeck-sega-saturn.md": "platforms/retrodeck.md#sega-saturn",
|
||||
"platforms/retrodeck-sinclair-zx-spectrum.md": "platforms/retrodeck.md#sinclair-zx-spectrum",
|
||||
"platforms/retrodeck-sony-playstation-2.md": "platforms/retrodeck.md#sony-playstation-2",
|
||||
"platforms/retrodeck-sony-playstation.md": "platforms/retrodeck.md#sony-playstation",
|
||||
"platforms/retropie-arcade.md": "platforms/retropie.md#arcade",
|
||||
"platforms/retropie-commodore-amiga.md": "platforms/retropie.md#commodore-amiga",
|
||||
"platforms/retropie-nec-pc-98.md": "platforms/retropie.md#nec-pc-98",
|
||||
"platforms/retropie-nec-pc-engine.md": "platforms/retropie.md#nec-pc-engine",
|
||||
"platforms/retropie-nintendo-ds.md": "platforms/retropie.md#nintendo-ds",
|
||||
"platforms/retropie-nintendo-gamecube.md": "platforms/retropie.md#nintendo-gamecube",
|
||||
"platforms/retropie-nintendo-sgb.md": "platforms/retropie.md#nintendo-sgb",
|
||||
"platforms/retropie-nintendo-snes.md": "platforms/retropie.md#nintendo-snes",
|
||||
"platforms/retropie-scummvm.md": "platforms/retropie.md#scummvm",
|
||||
"platforms/retropie-sega-dreamcast-arcade.md": "platforms/retropie.md#sega-dreamcast-arcade",
|
||||
"platforms/retropie-sega-saturn.md": "platforms/retropie.md#sega-saturn",
|
||||
"platforms/retropie-sinclair-zx-spectrum.md": "platforms/retropie.md#sinclair-zx-spectrum",
|
||||
"platforms/retropie-snk-neogeo-cd.md": "platforms/retropie.md#snk-neogeo-cd",
|
||||
"platforms/retropie-sony-playstation-2.md": "platforms/retropie.md#sony-playstation-2",
|
||||
"platforms/retropie-sony-playstation.md": "platforms/retropie.md#sony-playstation",
|
||||
"platforms/romm-sony-playstation-2.md": "platforms/romm.md#sony-playstation-2"
|
||||
}
|
||||
+280
-3
@@ -1,7 +1,8 @@
|
||||
// Progressive enhancement of the generated pages: table captions, filters
|
||||
// and keyboard scrolling, the hash lookup on files/ (a pasted hash or a file
|
||||
// hashed on this device), moved cards, a hash typed in the search, glossary
|
||||
// terms on a tap, the OS tab of the visitor's system, the current page in
|
||||
// terms on a tap, the OS tab of the visitor's system, the setup picked in an
|
||||
// install box, copy buttons on hashes, the current page in
|
||||
// the sidebar, and the 404 page's hints.
|
||||
// Which tables stack on a phone is decided when the page is built
|
||||
// (src/plugins/rehype-stack-tables.mjs), so the layout is right on first paint.
|
||||
@@ -87,9 +88,19 @@ const setScrollable = (table: HTMLTableElement, overflows: boolean) => {
|
||||
// The filter box above a long table is written when the page is built
|
||||
// (src/plugins/rehype-stack-tables.mjs), so the table never moves down once
|
||||
// drawn. Here it only filters.
|
||||
const filterTable = (table: HTMLTableElement) => {
|
||||
// A table folded in a closed block can have its box above the block
|
||||
// (the provenance lists): typing opens the block on the rows it keeps.
|
||||
const filterControls = (table: HTMLTableElement) => {
|
||||
const controls = table.previousElementSibling;
|
||||
if (!controls?.matches('.rb-table-filter')) return;
|
||||
if (controls?.matches('.rb-table-filter')) return controls;
|
||||
const block = table.closest('details');
|
||||
return block?.previousElementSibling?.matches('.rb-table-filter') ? block.previousElementSibling : null;
|
||||
};
|
||||
|
||||
const filterTable = (table: HTMLTableElement) => {
|
||||
const controls = filterControls(table);
|
||||
if (!controls) return;
|
||||
const block = controls.nextElementSibling instanceof HTMLDetailsElement ? controls.nextElementSibling : null;
|
||||
const input = controls.querySelector('input');
|
||||
const count = controls.querySelector('.rb-table-filter-count');
|
||||
if (!input || !count || input.dataset.rbWired) return;
|
||||
@@ -100,6 +111,7 @@ const filterTable = (table: HTMLTableElement) => {
|
||||
input.addEventListener('input', () => {
|
||||
searchable ??= rows.map((row) => ({ row, text: row.textContent!.normalize('NFKD').toLocaleLowerCase() }));
|
||||
const query = input.value.trim().normalize('NFKD').toLocaleLowerCase();
|
||||
if (block && query) block.open = true;
|
||||
let visible = 0;
|
||||
for (const entry of searchable) {
|
||||
entry.row.hidden = !!query && !entry.text.includes(query);
|
||||
@@ -171,6 +183,9 @@ const enhanceTables = () => {
|
||||
});
|
||||
if (document.readyState === 'complete') start();
|
||||
else addEventListener('load', start, { once: true });
|
||||
// A box above a closed block is typed in before its table is ever near
|
||||
// the screen: it is wired now.
|
||||
for (const table of tables) if (table.closest('details:not([open])') && filterControls(table)) filterTable(table);
|
||||
let timer = 0;
|
||||
addEventListener('resize', () => {
|
||||
clearTimeout(timer);
|
||||
@@ -743,6 +758,18 @@ const followMovedCard = () => {
|
||||
if (match && !target) location.replace(new URL(`files/?sha1=${match[1]}`, siteRoot()));
|
||||
};
|
||||
|
||||
// A link to a card folded in a table row (emulators with many entries)
|
||||
// opens its row. Chrome does it on its own. Where the browser left the
|
||||
// target hidden, the same address is followed again once the row is open,
|
||||
// so the browser brings the card into view as for any link.
|
||||
const openTargetBlock = () => {
|
||||
const id = decodeURIComponent(location.hash.slice(1));
|
||||
const block = id ? document.getElementById(id)?.closest('details') : null;
|
||||
if (!block || block.open) return;
|
||||
block.open = true;
|
||||
location.replace(location.hash);
|
||||
};
|
||||
|
||||
// The site search holds no hashes. A hash typed in it gets "Look up this
|
||||
// hash" as its first result, and Enter opens the lookup. The pages Pagefind
|
||||
// finds for the same characters (a file name that holds them, a word that
|
||||
@@ -931,6 +958,251 @@ const glossaryTerms = () => {
|
||||
document.addEventListener('click', close);
|
||||
};
|
||||
|
||||
// Copy buttons. A system card's hash cell (dd.rb-hash) gets one: in its
|
||||
// fold, each hash copies itself, and the short SHA1 shown open copies the
|
||||
// whole value, the fold's first hash. An emulator card's hash and each
|
||||
// command of an install box's list carry data-copy: the value to copy, or
|
||||
// empty when the text shown is the value. The buttons are added where the
|
||||
// reader sees the values, and when a fold opens for the ones inside it, so a
|
||||
// page of 250 closed folds holds none of them.
|
||||
// A platform row's button carries its own data-copy: it is not a value to
|
||||
// give a button.
|
||||
const COPYABLE = '[data-copy]:not(button), .rb-sys-file dd.rb-hash';
|
||||
|
||||
// What the button copies, in words: the label of its grid row, else the
|
||||
// words before the value ("MD5: ", ", SHA1 ", "RetroArch: "), else the
|
||||
// label its list item opens on.
|
||||
const copyLabel = (value: HTMLElement) => {
|
||||
const term = (value.tagName === 'DD' ? value : value.closest('dd'))?.previousElementSibling;
|
||||
if (term?.tagName === 'DT') return term.textContent?.trim() ?? '';
|
||||
const before = (value.previousSibling?.textContent ?? '').replace(/[:\s]+$/, '').split(',').pop()?.trim();
|
||||
if (before) return before;
|
||||
return /^\s*([^:]+):/.exec(value.closest('li')?.textContent ?? '')?.[1]?.trim() ?? '';
|
||||
};
|
||||
|
||||
// The value's own text, without the button a cell holds.
|
||||
const shownText = (value: Element) =>
|
||||
[...value.childNodes].filter((node) => !(node instanceof HTMLButtonElement)).map((node) => node.textContent).join('').trim();
|
||||
|
||||
const copyText = (value: HTMLElement) => {
|
||||
const own = value.dataset.copy;
|
||||
// An MDX page writes an empty attribute as "true".
|
||||
if (own && own !== 'true') return own;
|
||||
if (value.matches('dd.rb-hash') && !value.closest('details')) {
|
||||
const whole = value.closest('.rb-sys-file')?.querySelector('details dd.rb-hash');
|
||||
if (whole) return shownText(whole);
|
||||
}
|
||||
return shownText(value);
|
||||
};
|
||||
|
||||
const addCopyButtons = (root: ParentNode) => {
|
||||
for (const value of root.querySelectorAll<HTMLElement>(COPYABLE)) {
|
||||
if (value.hasAttribute('data-copy-ready') || value.closest('details:not([open])')) continue;
|
||||
value.setAttribute('data-copy-ready', '');
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'rb-copy';
|
||||
button.textContent = 'Copy';
|
||||
// The word that replaces "Copy" is read out.
|
||||
button.setAttribute('aria-live', 'polite');
|
||||
const label = copyLabel(value);
|
||||
button.setAttribute('aria-label', label ? `Copy ${label}` : 'Copy');
|
||||
// A grid cell holds its button, a value elsewhere is followed by it.
|
||||
if (value.tagName === 'DD') value.append(button);
|
||||
else value.after(button);
|
||||
}
|
||||
};
|
||||
|
||||
const copyButtons = () => {
|
||||
if (!document.querySelector(`${COPYABLE}, button.rb-copy[data-copy]`)) return;
|
||||
addCopyButtons(document);
|
||||
document.addEventListener('toggle', (event) => {
|
||||
const fold = event.target as HTMLDetailsElement;
|
||||
if (fold.open) addCopyButtons(fold);
|
||||
}, true);
|
||||
document.addEventListener('click', async (event) => {
|
||||
const button = (event.target as Element).closest?.<HTMLButtonElement>('button.rb-copy');
|
||||
if (!button) return;
|
||||
// A platform row's button shows a short hash and copies the whole one
|
||||
// from data-copy: its text stays, a mark shows for two seconds.
|
||||
const own = button.dataset.copy;
|
||||
if (own && own !== 'true') {
|
||||
try {
|
||||
await navigator.clipboard.writeText(own);
|
||||
let status = document.querySelector<HTMLElement>('#rb-copy-status');
|
||||
if (!status) {
|
||||
status = document.createElement('p');
|
||||
status.id = 'rb-copy-status';
|
||||
status.className = 'rb-visually-hidden';
|
||||
status.setAttribute('role', 'status');
|
||||
document.body.append(status);
|
||||
}
|
||||
status.textContent = `Copied ${own}`;
|
||||
button.dataset.copied = '';
|
||||
window.setTimeout(() => delete button.dataset.copied, 2000);
|
||||
} catch {
|
||||
// No clipboard: the full value is in the row's details.
|
||||
}
|
||||
return;
|
||||
}
|
||||
const value = button.parentElement?.matches(COPYABLE)
|
||||
? button.parentElement
|
||||
: (button.previousElementSibling as HTMLElement | null);
|
||||
if (!value?.matches(COPYABLE)) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(copyText(value));
|
||||
button.textContent = 'Copied';
|
||||
} catch {
|
||||
// No clipboard (an insecure page, a refused permission): the value
|
||||
// is selected, for Ctrl+C.
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(value.tagName === 'DD' && value.firstChild ? value.firstChild : value);
|
||||
getSelection()?.removeAllRanges();
|
||||
getSelection()?.addRange(range);
|
||||
button.textContent = 'Selected';
|
||||
}
|
||||
window.setTimeout(() => { button.textContent = 'Copy'; }, 1500);
|
||||
});
|
||||
};
|
||||
|
||||
// A system's install box: one command, for the setup picked in its list,
|
||||
// under the site's OS tabs. The box is built with the first setup's command;
|
||||
// the choice rewrites the options of each command and its copy text, and is
|
||||
// remembered for the next box and the next page. Without this script the
|
||||
// picker stays hidden and the list of every command shows instead.
|
||||
const SETUP_KEY = 'rb-setup';
|
||||
|
||||
const savedSetup = (): string | null => {
|
||||
try {
|
||||
return localStorage.getItem(SETUP_KEY);
|
||||
} catch {
|
||||
// Storage refused (a private window, blocked site data): no saved setup.
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// The colours a flag and a value take in the highlighted sh command, read
|
||||
// from the box's own first command so a rewritten one looks the same.
|
||||
type ArgStyles = { flag: string; value: string; space: string };
|
||||
|
||||
const argStyles = (block: Element): ArgStyles | null => {
|
||||
const leaves = [...block.querySelectorAll<HTMLElement>('pre[data-language="sh"] .ec-line .code span')].filter((span) => !span.children.length);
|
||||
const at = leaves.findIndex((leaf) => leaf.textContent === '--platform');
|
||||
if (at < 0) return null;
|
||||
const value = leaves.slice(at + 1).find((leaf) => leaf.textContent?.trim());
|
||||
const space = leaves.find((leaf) => leaf.textContent === ' ');
|
||||
return { flag: leaves[at].getAttribute('style') ?? '', value: value?.getAttribute('style') ?? '', space: space?.getAttribute('style') ?? '' };
|
||||
};
|
||||
|
||||
// Rewrites the options of a highlighted command, from " --platform": the
|
||||
// installer's own part keeps its colours, the options take *styles*.
|
||||
const setCommandArgs = (frame: Element, args: string, styles: ArgStyles | null) => {
|
||||
const line = frame.querySelector<HTMLElement>('.ec-line .code');
|
||||
if (!line) return;
|
||||
const text = line.textContent ?? '';
|
||||
const start = text.indexOf(' --platform ');
|
||||
if (start < 0) return;
|
||||
const walker = document.createTreeWalker(line, NodeFilter.SHOW_TEXT);
|
||||
let seen = 0;
|
||||
const range = document.createRange();
|
||||
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
||||
const length = node.textContent?.length ?? 0;
|
||||
if (seen + length > start) {
|
||||
range.setStart(node, start - seen);
|
||||
break;
|
||||
}
|
||||
seen += length;
|
||||
}
|
||||
range.setEndAfter(line.lastChild!);
|
||||
range.deleteContents();
|
||||
for (const part of ` ${args}`.split(/(\s+)/)) {
|
||||
if (!part) continue;
|
||||
const span = document.createElement('span');
|
||||
const space = /^\s+$/.test(part);
|
||||
const style = space ? styles?.space : part.startsWith('-') ? styles?.flag : styles?.value;
|
||||
if (style) span.setAttribute('style', style);
|
||||
span.textContent = part;
|
||||
// The theme colours a span that has no class: a word kept whole is
|
||||
// wrapped, as the build wraps it (ec-unbroken-tokens.mjs).
|
||||
if (space) {
|
||||
line.append(span);
|
||||
} else {
|
||||
const whole = document.createElement('span');
|
||||
whole.className = 'rb-nb';
|
||||
whole.append(span);
|
||||
line.append(whole);
|
||||
}
|
||||
}
|
||||
frame.querySelector('.copy button')?.setAttribute('data-code', text.slice(0, start) + ` ${args}`);
|
||||
};
|
||||
|
||||
const installPickers = () => {
|
||||
const blocks = [...document.querySelectorAll<HTMLElement>('[data-rb-install]')];
|
||||
if (!blocks.length) return;
|
||||
const pick = (setup: string | null) => {
|
||||
for (const block of blocks) {
|
||||
const select = block.querySelector('select');
|
||||
if (!select) continue;
|
||||
if (setup && [...select.options].some((option) => option.value === setup)) select.value = setup;
|
||||
const option = select.selectedOptions[0];
|
||||
if (!option) continue;
|
||||
const styles = (block as HTMLElement & { rbStyles?: ArgStyles | null }).rbStyles;
|
||||
for (const frame of block.querySelectorAll('.expressive-code')) {
|
||||
const shell = frame.querySelector('pre')?.dataset.language === 'powershell' ? 'ps' : 'sh';
|
||||
setCommandArgs(frame, option.dataset[shell] ?? '', styles ?? null);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const block of blocks) {
|
||||
(block as HTMLElement & { rbStyles?: ArgStyles | null }).rbStyles = argStyles(block);
|
||||
block.hidden = false;
|
||||
const list = block.nextElementSibling;
|
||||
if (list instanceof HTMLDetailsElement && list.classList.contains('rb-install-all')) list.open = false;
|
||||
block.querySelector('select')?.addEventListener('change', (event) => {
|
||||
const setup = (event.target as HTMLSelectElement).value;
|
||||
try {
|
||||
localStorage.setItem(SETUP_KEY, setup);
|
||||
} catch {
|
||||
// Storage refused: the choice holds for this page only.
|
||||
}
|
||||
pick(setup);
|
||||
});
|
||||
}
|
||||
pick(savedSetup());
|
||||
};
|
||||
|
||||
// A platform's file list (div.rb-files) is rows of a details and its copy
|
||||
// button. The filter box written above a long one hides both of a row.
|
||||
const filterFiles = () => {
|
||||
for (const list of document.querySelectorAll<HTMLElement>('.rb-table-filter + .rb-files')) {
|
||||
const controls = list.previousElementSibling!;
|
||||
const input = controls.querySelector('input');
|
||||
const count = controls.querySelector('.rb-table-filter-count');
|
||||
if (!input || !count) continue;
|
||||
const rows = [...list.querySelectorAll<HTMLElement>(':scope > details.rb-file')];
|
||||
let searchable: { row: HTMLElement; text: string }[] | null = null;
|
||||
input.addEventListener('input', () => {
|
||||
searchable ??= rows.map((row) => ({
|
||||
row,
|
||||
text: `${row.querySelector('summary')?.textContent ?? ''} ${row.nextElementSibling?.matches('button.rb-copy') ? row.nextElementSibling.textContent : ''}`
|
||||
.normalize('NFKD')
|
||||
.toLocaleLowerCase(),
|
||||
}));
|
||||
const query = input.value.trim().normalize('NFKD').toLocaleLowerCase();
|
||||
let visible = 0;
|
||||
for (const { row, text } of searchable) {
|
||||
row.hidden = !!query && !text.includes(query);
|
||||
const button = row.nextElementSibling;
|
||||
if (button?.matches('button.rb-copy')) (button as HTMLElement).hidden = row.hidden;
|
||||
if (!row.hidden) visible += 1;
|
||||
}
|
||||
count.textContent = query
|
||||
? `${visible.toLocaleString('en')} of ${rows.length.toLocaleString('en')} rows`
|
||||
: `${rows.length.toLocaleString('en')} rows`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// The 404 page: a search button, and the list a moved page belongs to.
|
||||
const notFound = () => {
|
||||
document.querySelector('[data-rb-open-search]')?.addEventListener('click', () => {
|
||||
@@ -948,10 +1220,15 @@ const notFound = () => {
|
||||
|
||||
resolveFileAddress();
|
||||
followMovedCard();
|
||||
openTargetBlock();
|
||||
addEventListener('hashchange', openTargetBlock);
|
||||
enhanceTables();
|
||||
breakPaths();
|
||||
hashSearch();
|
||||
glossaryTerms();
|
||||
copyButtons();
|
||||
filterFiles();
|
||||
osTabs();
|
||||
installPickers();
|
||||
showCurrentPage();
|
||||
notFound();
|
||||
@@ -17,6 +17,9 @@
|
||||
.rb-bar inline coverage bar, width from --v
|
||||
.rb-details-* collapsible block, neutral unless caution or danger
|
||||
dl.rb-facts the facts of a file card, label and value side by side
|
||||
.rb-install-all the list of every command in a system's install box
|
||||
.rb-copy the copy button site.ts adds after a hash
|
||||
.rb-install-setup the setup list of a system's install box
|
||||
abbr.rb-term a glossary term, its definition on hover or tap
|
||||
*/
|
||||
|
||||
@@ -530,6 +533,88 @@ body:has(.rb-sys-file, .rb-file-entry) { --sl-content-width: var(--rb-grid-width
|
||||
}
|
||||
:is(.rb-sys-file, .rb-file-entry):has(> .rb-file-title:target) { background: var(--rb-surface); }
|
||||
|
||||
/* Entries as table rows (scripts/siteentries.py): the name takes the room
|
||||
and its card opens under it. On a phone the row stacks like any table
|
||||
with a hash column, the name on the full width. */
|
||||
body:has(table.rb-entries) { --sl-content-width: var(--rb-grid-width); }
|
||||
/* A row the filter leaves out stays out on a phone, where a stacked row is
|
||||
a block: `display: block` outweighed the hidden attribute, and every
|
||||
stacked table showed all its rows under "11 of 1,037 rows". */
|
||||
.sl-markdown-content table[data-rb-stack] tr[hidden] { display: none; }
|
||||
@media (min-width: 50rem) {
|
||||
.sl-markdown-content table.rb-wanted td.rb-hash { min-width: 10rem; }
|
||||
}
|
||||
.sl-markdown-content .rb-entries-wrap { container-type: inline-size; }
|
||||
.sl-markdown-content table.rb-entries { width: 100%; }
|
||||
@media (min-width: 50rem) {
|
||||
.sl-markdown-content table.rb-entries { display: table; }
|
||||
.sl-markdown-content table.rb-entries td:first-child { width: 100%; }
|
||||
/* An open card takes the table's width, not its column's: the other
|
||||
cells of its row keep to one line, so the room under them is free. */
|
||||
.sl-markdown-content table.rb-entries tr:has(> td > details[open]) > td:not(:first-child) { white-space: nowrap; }
|
||||
.sl-markdown-content details.rb-entry[open] { contain: inline-size; }
|
||||
.sl-markdown-content details.rb-entry[open] > :not(summary) { width: 100cqi; }
|
||||
}
|
||||
.sl-markdown-content details.rb-entry { border: 0; padding: 0; }
|
||||
.sl-markdown-content details.rb-entry > summary { min-height: 24px; font-weight: 400; }
|
||||
.sl-markdown-content details.rb-entry > summary > .rb-file-title { display: inline; margin: 0; }
|
||||
.sl-markdown-content details.rb-entry[open] > summary { margin-bottom: 0.4rem; }
|
||||
.sl-markdown-content details.rb-entry ul { margin: 0.3rem 0 0; padding-left: 1.2rem; font-size: 0.85rem; }
|
||||
.sl-markdown-content details.rb-entry li { margin: 0.1rem 0; }
|
||||
.sl-markdown-content details.rb-entry:has(.rb-file-title:target) { background: var(--rb-surface); }
|
||||
@media (max-width: 50rem) {
|
||||
.sl-markdown-content table.rb-entries td:first-child { padding-left: 0.6rem; font-size: 1rem; }
|
||||
.sl-markdown-content table.rb-entries td:first-child::before { content: none; }
|
||||
/* "required for Data East DECO Cassette System" ran 12 px past a phone
|
||||
beside its label: a long badge wraps in its cell. */
|
||||
.sl-markdown-content table.rb-entries .rb-badge { white-space: normal; }
|
||||
.sl-markdown-content details.rb-entry ul { font-size: 0.75rem; }
|
||||
}
|
||||
|
||||
/* A fold of a file card (Hashes, the rest): no frame, a small summary in
|
||||
the muted colour, a 24 px target (WCAG 2.5.8). */
|
||||
.sl-markdown-content :is(:is(.rb-sys-file, .rb-file-entry) details, details.rb-install-all) { margin: 0.3rem 0 0; padding: 0; border: 0; font-size: 0.85rem; }
|
||||
.sl-markdown-content :is(:is(.rb-sys-file, .rb-file-entry) details, details.rb-install-all) > summary {
|
||||
min-height: 24px;
|
||||
line-height: 24px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--rb-text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
.sl-markdown-content :is(:is(.rb-sys-file, .rb-file-entry) details, details.rb-install-all) > summary::before { background-color: var(--rb-text-secondary); }
|
||||
.sl-markdown-content :is(:is(.rb-sys-file, .rb-file-entry) details, details.rb-install-all) > summary:hover { color: var(--sl-color-white); }
|
||||
.sl-markdown-content :is(:is(.rb-sys-file, .rb-file-entry) details, details.rb-install-all) > :not(summary) { margin-top: 0.2rem; }
|
||||
|
||||
/* The copy button after a hash (site.ts): the Download button's frame,
|
||||
smaller, a 24 px target that stays inside a 24 px line, so it never
|
||||
covers the fold below it (WCAG 2.5.8). */
|
||||
.rb-copy {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
height: 24px;
|
||||
margin-left: 0.4rem;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid var(--rb-border-strong);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-family: var(--sl-font);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
color: var(--rb-link);
|
||||
vertical-align: top;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rb-copy:hover { background: var(--sl-color-accent-low); }
|
||||
|
||||
/* A system's install box: the setup list above the OS tabs, and the list
|
||||
of every command, which wraps anywhere rather than scroll a phone. */
|
||||
.sl-markdown-content .rb-install-all code { overflow-wrap: anywhere; }
|
||||
.sl-markdown-content .rb-install-setup { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-size: 0.9rem; }
|
||||
/* Its select is styled as every other choice list on the site (the
|
||||
arrow of --rb-select-arrow, below): a rule of its own hid the arrow. */
|
||||
|
||||
/* Collapsible blocks are quiet: a closed block must not outweigh the
|
||||
command it explains. Colour is kept for caution and danger. */
|
||||
.sl-markdown-content details.rb-details {
|
||||
@@ -551,6 +636,95 @@ body:has(.rb-sys-file, .rb-file-entry) { --sl-content-width: var(--rb-grid-width
|
||||
.sl-markdown-content details.rb-details > summary::before { background-color: var(--rb-text-secondary); }
|
||||
.sl-markdown-content details.rb-details[open] > summary { margin-bottom: 0.6rem; }
|
||||
|
||||
/* A platform's file list (generate_site._platform_file_list), on the
|
||||
pattern of the Tested block: each row a details whose summary is the
|
||||
row (name, folder, status), its body the evidence across the whole
|
||||
list. The short hash sits on its copy button (site.ts) beside the
|
||||
summary, since a summary is itself a control. The details reaches under
|
||||
the button column, so the row rule and an opened body run full width. */
|
||||
.sl-markdown-content .rb-files {
|
||||
--rb-hash-column: 6.75rem;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) var(--rb-hash-column);
|
||||
margin-top: 0.75rem;
|
||||
border-top: 1px solid var(--rb-border-strong);
|
||||
}
|
||||
.sl-markdown-content .rb-files > * { margin: 0; }
|
||||
.sl-markdown-content .rb-files > details.rb-file {
|
||||
grid-column: 1;
|
||||
margin-right: calc(-1 * var(--rb-hash-column));
|
||||
padding: 0 0 0 1.75rem;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--rb-border);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.sl-markdown-content .rb-files > details.rb-file[open] { padding-bottom: 0.6rem; background: var(--rb-surface); }
|
||||
.sl-markdown-content .rb-files > details.rb-file > summary {
|
||||
display: grid;
|
||||
grid-template-columns: 1.25rem minmax(0, 1fr) 9rem 6rem;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
min-height: 2.25rem;
|
||||
margin-left: -1.75rem;
|
||||
padding: 0.3rem calc(var(--rb-hash-column) + 0.5rem) 0.3rem 0;
|
||||
font: 0.8125rem/1.4 var(--sl-font-mono);
|
||||
cursor: pointer;
|
||||
}
|
||||
.sl-markdown-content .rb-files:not(.rb-files-path) > details.rb-file > summary { grid-template-columns: 1.25rem minmax(0, 1fr) 6rem; }
|
||||
.sl-markdown-content .rb-files > details.rb-file > summary:hover { background: var(--rb-surface); }
|
||||
.sl-markdown-content .rb-files > details.rb-file > summary::before { margin: 0; background-color: var(--rb-text-secondary); }
|
||||
.sl-markdown-content .rb-files > details.rb-file > summary > code { grid-column: 3; padding: 0; background: none; color: var(--rb-text-secondary); }
|
||||
.sl-markdown-content .rb-files > details.rb-file > summary > .rb-badge { grid-column: -2; justify-self: start; font-family: var(--sl-font); }
|
||||
.sl-markdown-content .rb-files > button.rb-copy {
|
||||
position: relative;
|
||||
grid-column: 2;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
.sl-markdown-content .rb-files > [hidden] { display: none; }
|
||||
|
||||
.sl-markdown-content .rb-files button.rb-copy {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
min-height: 24px;
|
||||
padding: 0 0.35em;
|
||||
border: 1px solid var(--rb-border-strong);
|
||||
border-radius: 3px;
|
||||
background: var(--sl-color-bg);
|
||||
color: var(--sl-color-white);
|
||||
font: 0.8125rem/1.4 var(--sl-font-mono);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sl-markdown-content .rb-files button.rb-copy::after {
|
||||
content: '';
|
||||
width: 0.9em;
|
||||
height: 0.9em;
|
||||
background: var(--rb-text-secondary);
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M5 1h8a2 2 0 0 1 2 2v8h-2V3H5zM3 5h7a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2zm0 2v7h7V7z'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
}
|
||||
.sl-markdown-content .rb-files button.rb-copy[data-copied]::after {
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 11.2 2.8 8 1.4 9.4 6 14l8.6-8.6-1.4-1.4z'/%3E%3C/svg%3E");
|
||||
background: var(--rb-ok-fg);
|
||||
}
|
||||
.sl-markdown-content .rb-files button.rb-copy:hover { border-color: var(--rb-link); }
|
||||
@media (max-width: 50rem) {
|
||||
/* On a phone the folder goes under the name, the status stays on the
|
||||
first line. A row with no folder prints nothing for it. */
|
||||
.sl-markdown-content .rb-files > details.rb-file > summary,
|
||||
.sl-markdown-content .rb-files:not(.rb-files-path) > details.rb-file > summary {
|
||||
grid-template-columns: 1.25rem minmax(0, 1fr) auto;
|
||||
row-gap: 0.1rem;
|
||||
}
|
||||
.sl-markdown-content .rb-files > details.rb-file > summary::before { grid-row: 1; }
|
||||
.sl-markdown-content .rb-files > details.rb-file > summary > code { grid-column: 2; grid-row: 2; }
|
||||
.sl-markdown-content .rb-files > details.rb-file > summary > .rb-badge { grid-column: 3; grid-row: 1; }
|
||||
}
|
||||
|
||||
/* Asides: a neutral box for notes and tips, no side stripe, no icon.
|
||||
Caution and danger keep their colour, as a 1 px border all round. */
|
||||
.sl-markdown-content .starlight-aside {
|
||||
|
||||
@@ -68,6 +68,27 @@ if (mode === 'weight') {
|
||||
if (n === 0) await page.click('button[data-open-modal]');
|
||||
const input = page.locator('dialog input[type="text"], dialog input[type="search"]').first();
|
||||
await input.fill(query);
|
||||
if (/^[0-9a-f]+$/.test(query) && [8, 32, 40, 64].includes(query.length)) {
|
||||
// A pasted hash: the search offers "Look up this hash" first, and
|
||||
// the lookup names the cards that hold the value. Its results
|
||||
// are those cards.
|
||||
const lookup = await page.waitForSelector('.rb-search-hash a', { timeout: 15000 });
|
||||
const found = await context.newPage();
|
||||
await found.goto(await lookup.evaluate((a) => a.href), { waitUntil: 'networkidle' });
|
||||
await found.waitForFunction(() => /^Found|^No file/.test(document.getElementById('rb-file-lookup-status')?.textContent ?? ''), null, { timeout: 15000 });
|
||||
results = await found.evaluate(() => {
|
||||
const cards = [...document.querySelectorAll('#rb-file-lookup-result a')].filter((a) => a.textContent === 'Open its card');
|
||||
return {
|
||||
count: document.getElementById('rb-file-lookup-status')?.textContent ?? '',
|
||||
top: cards.slice(0, 5).map((a) => `Open its card | ${new URL(a.href).pathname}${new URL(a.href).hash}`),
|
||||
};
|
||||
});
|
||||
await found.close();
|
||||
await page.waitForTimeout(300);
|
||||
const transfer = sum(rows.slice(mark), () => true);
|
||||
console.log(JSON.stringify({ query, transfer_kb: (transfer / 1024).toFixed(1), ...results }));
|
||||
continue;
|
||||
}
|
||||
await page.waitForSelector('.pagefind-ui__result', { timeout: 15000 });
|
||||
await page.waitForTimeout(1200);
|
||||
results = await page.evaluate(() => {
|
||||
|
||||
@@ -112,6 +112,19 @@ QUERIES: dict[str, tuple[tuple[str, ...], int]] = {
|
||||
"r36s": (("platforms/", "which-pack/"), 3),
|
||||
"steam deck": (("platforms/emudeck/", "platforms/retrodeck/", "which-pack/"), 1),
|
||||
"knulli": (("platforms/knulli/",), 1),
|
||||
# A card folded in a table row, on the last part of MAME's cards.
|
||||
"bbc_flop_torch.xml": (("emulators/mame-files-",), 3),
|
||||
# FinalBurn Neo has no pgm.zip entry: the archive is named by the badge
|
||||
# of each member it reads there, in the Status column of its table.
|
||||
"pgm.zip": (("emulators/fbneo/",), 3),
|
||||
# A dump name in a wanted list folded under its filter box.
|
||||
"speeddos": (("provenance-tosec/",), 1),
|
||||
# A pasted hash of any kind finds the file's card, its hashes now in a
|
||||
# closed fold: the MD5, CRC32 and SHA256 of scph5501.bin. The search box
|
||||
# offers "Look up this hash", and the lookup opens the card.
|
||||
"490f666e1afb15b7362b406ed1cea246": (("systems/sony-playstation/",), 1),
|
||||
"8d8cb7e4": (("systems/sony-playstation/",), 1),
|
||||
"11052b6499e466bbf0a709b1f9cb6834a9418e66680387912451e971cf8a1fef": (("systems/sony-playstation/",), 1),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user