mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-30 23:57:20 -05:00
feat: stack tables at build, add section tabs
This commit is contained in:
1 parent
72e7507ff9
commit
2c71f0d3da
3 files changed
+193
-2
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
// The RetroBIOS site. Pages, navigation and data files are written by
|
// The RetroBIOS site. Pages, navigation and data files are written by
|
||||||
// scripts/generate_site.py; this folder holds the engine's configuration,
|
// scripts/generate_site.py; this folder holds the engine's configuration,
|
||||||
// the components and the styles.
|
// the components and the styles.
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
import { copyFile } from 'node:fs/promises';
|
import { copyFile } from 'node:fs/promises';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
@@ -11,10 +12,27 @@ import starlightSidebarTopics from 'starlight-sidebar-topics';
|
|||||||
import starlightLinksValidator from 'starlight-links-validator';
|
import starlightLinksValidator from 'starlight-links-validator';
|
||||||
import sidebar from './src/generated/sidebar.json' with { type: 'json' };
|
import sidebar from './src/generated/sidebar.json' with { type: 'json' };
|
||||||
import lastmod from './src/generated/lastmod.json' with { type: 'json' };
|
import lastmod from './src/generated/lastmod.json' with { type: 'json' };
|
||||||
|
import stackTables from './src/plugins/rehype-stack-tables.mjs';
|
||||||
|
|
||||||
const SITE = 'https://abdess.github.io';
|
const SITE = 'https://abdess.github.io';
|
||||||
const BASE = '/retrobios/';
|
const BASE = '/retrobios/';
|
||||||
|
|
||||||
|
// Old addresses that lead on to a page: {"/from/": "/to/"}, site paths
|
||||||
|
// written by the generator. A checkout that has not run it builds with none.
|
||||||
|
// Astro puts the base before the old address but not before the new one.
|
||||||
|
const readRedirects = () => {
|
||||||
|
let redirects;
|
||||||
|
try {
|
||||||
|
redirects = JSON.parse(readFileSync(new URL('./src/generated/redirects.json', import.meta.url), 'utf-8'));
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') return {};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const withBase = (to) => (/^https?:/.test(to) || to.startsWith(BASE) ? to : BASE + to.replace(/^\//, ''));
|
||||||
|
const withoutBase = (from) => (from.startsWith(BASE) ? `/${from.slice(BASE.length)}` : from);
|
||||||
|
return Object.fromEntries(Object.entries(redirects).map(([from, to]) => [withoutBase(from), withBase(to)]));
|
||||||
|
};
|
||||||
|
|
||||||
// The sitemap keeps the address MkDocs gave it: sitemap.xml, beside the index.
|
// The sitemap keeps the address MkDocs gave it: sitemap.xml, beside the index.
|
||||||
const sitemapAlias = {
|
const sitemapAlias = {
|
||||||
name: 'retrobios-sitemap-alias',
|
name: 'retrobios-sitemap-alias',
|
||||||
@@ -30,8 +48,10 @@ export default defineConfig({
|
|||||||
site: SITE,
|
site: SITE,
|
||||||
base: BASE,
|
base: BASE,
|
||||||
trailingSlash: 'always',
|
trailingSlash: 'always',
|
||||||
|
redirects: readRedirects(),
|
||||||
markdown: {
|
markdown: {
|
||||||
processor: satteri({
|
processor: satteri({
|
||||||
|
hastPlugins: [stackTables()],
|
||||||
features: {
|
features: {
|
||||||
headingAttributes: true,
|
headingAttributes: true,
|
||||||
definitionList: true,
|
definitionList: true,
|
||||||
@@ -59,11 +79,21 @@ export default defineConfig({
|
|||||||
PageTitle: './src/components/PageTitle.astro',
|
PageTitle: './src/components/PageTitle.astro',
|
||||||
MarkdownContent: './src/components/MarkdownContent.astro',
|
MarkdownContent: './src/components/MarkdownContent.astro',
|
||||||
Footer: './src/components/Footer.astro',
|
Footer: './src/components/Footer.astro',
|
||||||
|
Header: './src/components/Header.astro',
|
||||||
},
|
},
|
||||||
routeMiddleware: ['./src/routeData.ts'],
|
routeMiddleware: ['./src/routeData.ts'],
|
||||||
tableOfContents: { minHeadingLevel: 2, maxHeadingLevel: 3 },
|
tableOfContents: { minHeadingLevel: 2, maxHeadingLevel: 3 },
|
||||||
// Commands wrap instead of scrolling, so the end of a one-liner stays visible.
|
// Commands wrap instead of scrolling, so the end of a one-liner stays
|
||||||
expressiveCode: { defaultProps: { wrap: true } },
|
// visible. A command block is titled (Terminal, PowerShell) and drawn
|
||||||
|
// without the three window dots. Starlight writes the dots into each
|
||||||
|
// theme after the global style overrides, so they are set here.
|
||||||
|
expressiveCode: {
|
||||||
|
defaultProps: { wrap: true },
|
||||||
|
customizeTheme(theme) {
|
||||||
|
theme.styleOverrides.frames = { ...theme.styleOverrides.frames, terminalTitlebarDotsOpacity: '0' };
|
||||||
|
return theme;
|
||||||
|
},
|
||||||
|
},
|
||||||
lastUpdated: false,
|
lastUpdated: false,
|
||||||
pagination: true,
|
pagination: true,
|
||||||
// Measured on the queries of tools/search_gate.py, at the centre of a
|
// Measured on the queries of tools/search_gate.py, at the centre of a
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
// Starlight's header bar, and under it the five sections of the site as
|
||||||
|
// tabs: the funnel (Start here to Contribute) is the site's structure, so it
|
||||||
|
// sits where every docs site puts its sections. On a phone the tabs stay the
|
||||||
|
// first row of the menu, where the sidebar lists them.
|
||||||
|
import Default from '@astrojs/starlight/components/Header.astro';
|
||||||
|
|
||||||
|
const topics = Astro.locals.starlightSidebarTopics?.topics ?? [];
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="rb-header-bar"><Default /></div>
|
||||||
|
{
|
||||||
|
topics.length > 0 && (
|
||||||
|
<nav class="rb-tabs print:hidden" aria-label="Sections">
|
||||||
|
<ul>
|
||||||
|
{topics.map((topic) => (
|
||||||
|
<li>
|
||||||
|
<a href={topic.link} aria-current={topic.isCurrent ? 'true' : undefined}>
|
||||||
|
{topic.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Global: the bar height and the sidebar's topic list belong to other components. -->
|
||||||
|
<style is:global>
|
||||||
|
.rb-header-bar { height: 100%; }
|
||||||
|
.rb-tabs { display: none; }
|
||||||
|
|
||||||
|
/* "Look up this hash", the first result of a hash typed in the search (site.ts). */
|
||||||
|
.rb-search-hash {
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.rb-search-hash a { font-weight: 600; }
|
||||||
|
/* Pagefind's own display rules would show what site.ts hides. */
|
||||||
|
#starlight__search [hidden] { display: none !important; }
|
||||||
|
|
||||||
|
@media (min-width: 50rem) {
|
||||||
|
:root {
|
||||||
|
--rb-nav-bar: 4rem;
|
||||||
|
--rb-tab-row: 2.75rem;
|
||||||
|
--sl-nav-height: calc(var(--rb-nav-bar) + var(--rb-tab-row));
|
||||||
|
}
|
||||||
|
.rb-header-bar { height: calc(100% - var(--rb-tab-row)); }
|
||||||
|
.site-title { height: calc(var(--rb-nav-bar) - 2 * var(--sl-nav-pad-y)); }
|
||||||
|
.rb-tabs {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
inset-inline: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: var(--rb-tab-row);
|
||||||
|
padding-inline: var(--sl-nav-pad-x);
|
||||||
|
}
|
||||||
|
.rb-tabs ul {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.75rem;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.rb-tabs li { display: flex; }
|
||||||
|
.rb-tabs a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
color: var(--sl-color-gray-2);
|
||||||
|
font-size: var(--sl-text-sm);
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rb-tabs a:hover { color: var(--sl-color-white); }
|
||||||
|
.rb-tabs a[aria-current] {
|
||||||
|
border-bottom-color: currentColor;
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
/* The header holds the sections: the sidebar lists the pages of one. */
|
||||||
|
.sidebar-content ul.starlight-sidebar-topics { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// Tables a phone reads one row at a time, marked when the page is built so
|
||||||
|
// the stacked layout is there on first paint (the CSS is retrobios.css,
|
||||||
|
// table[data-rb-stack]). The rule reads the table, never the page that
|
||||||
|
// wrote it:
|
||||||
|
// - a row that ends in an action (a release download, a button, the link to
|
||||||
|
// a setup's install command) or a header that names a ZIP column: one
|
||||||
|
// block per row, each cell under its column name;
|
||||||
|
// - two columns: a list of label and value, data-rb-stack="kv";
|
||||||
|
// - on a level 0 or 1 page (`level: start` or `setup`), a table short enough
|
||||||
|
// to read as records (under 20 rows, the size from which site.ts adds a
|
||||||
|
// filter): one block per row. A longer one is a list scanned by column and
|
||||||
|
// keeps its scroll.
|
||||||
|
// Satteri runs hast plugins (the rehype stage of its pipeline): this one is
|
||||||
|
// registered in astro.config.mjs through satteri({ hastPlugins }).
|
||||||
|
|
||||||
|
const ACTION_HREF = /\/releases\/(download\/|latest\/?$)|#install$/;
|
||||||
|
const ACTION_CLASS = /\bclass=["'][^"']*\brb-button\b/;
|
||||||
|
const DOWNLOAD_HEADER = /^(zip|download)$/i;
|
||||||
|
const READER_LEVELS = new Set(['start', 'setup']);
|
||||||
|
export const RECORD_ROWS = 20;
|
||||||
|
|
||||||
|
const elements = (node, tag, found = []) => {
|
||||||
|
for (const child of node.children ?? []) {
|
||||||
|
if (child.type !== 'element') continue;
|
||||||
|
if (child.tagName === tag) found.push(child);
|
||||||
|
// A table inside a cell is its own table.
|
||||||
|
if (child.tagName !== 'table') elements(child, tag, found);
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
};
|
||||||
|
|
||||||
|
const holdsAction = (node) =>
|
||||||
|
(node.children ?? []).some(
|
||||||
|
(child) =>
|
||||||
|
(child.type === 'element' &&
|
||||||
|
((child.tagName === 'a' && ACTION_HREF.test(String(child.properties?.href ?? ''))) ||
|
||||||
|
String(child.properties?.className ?? '').split(/[\s,]+/).includes('rb-button') ||
|
||||||
|
holdsAction(child))) ||
|
||||||
|
(child.type === 'raw' && ACTION_CLASS.test(child.value ?? '')),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const stackKind = ({ labels, rows, action, level }) => {
|
||||||
|
if (labels.length === 2) return 'kv';
|
||||||
|
if (action || DOWNLOAD_HEADER.test(labels.at(-1) ?? '')) return 'true';
|
||||||
|
if (READER_LEVELS.has(level) && labels.length > 2 && rows < RECORD_ROWS) return 'true';
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function stackTables() {
|
||||||
|
return {
|
||||||
|
name: 'rb-stack-tables',
|
||||||
|
element: {
|
||||||
|
filter: ['table'],
|
||||||
|
visit(table, ctx) {
|
||||||
|
const head = elements(table, 'thead')[0];
|
||||||
|
const body = elements(table, 'tbody')[0];
|
||||||
|
if (!head || !body) return;
|
||||||
|
const labels = elements(head, 'th').map((cell) => ctx.textContent(cell).trim());
|
||||||
|
const rows = elements(body, 'tr');
|
||||||
|
const level = ctx.data.astro?.frontmatter?.level;
|
||||||
|
const kind = stackKind({ labels, rows: rows.length, action: holdsAction(body), level });
|
||||||
|
if (!kind) return;
|
||||||
|
ctx.setProperty(table, 'dataRbStack', kind);
|
||||||
|
if (kind === 'kv') return;
|
||||||
|
for (const row of rows) {
|
||||||
|
elements(row, 'td').forEach((cell, column) => {
|
||||||
|
if (labels[column] && cell.properties?.dataLabel === undefined) ctx.setProperty(cell, 'dataLabel', labels[column]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user