78 lines
6.4 KiB
JavaScript
78 lines
6.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Every served page carries the slim bar (site-ui-5, the owner's rule of 7 October 2026: "easy access to all of our pages
|
|
// without a header that is crazy"): the mark and wordmark, the three groups (Mine, Network, Learn) as buttons with their
|
|
// aria, the Download button, and every route of the site reachable from the group panels AND from the phone sheet, each with
|
|
// its one-line description. A page with a shorter nav, a panel missing a route, or a route the panels do not name fails the
|
|
// build and the gate. The route list below is the contract: a new page adds its line here and in site/partials/nav.html in
|
|
// the same commit.
|
|
// node tools/ci/site-nav-check.mjs [site-dir]
|
|
// node tools/ci/site-nav-check.mjs --self-test a page with a route dropped from the sheet is caught; the tree passes
|
|
import { readdirSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const GROUPS = {
|
|
mine: ['/miner', '/download', '/app', '/wallet', '/miners', '/dev-fee', '/faucet', '/metamask'],
|
|
network: ['/live', '/explorer', '/journey', '/bench', '/evidence'],
|
|
learn: ['/litepaper', '/income', '/ledger', '/claims', '/randomx', '/provenance'],
|
|
};
|
|
const ROUTES = Object.values(GROUPS).flat();
|
|
// pages served without their own panel entry: the explorer's two rewrites, the 404 and the unlinked scenes lab
|
|
const UNLISTED = new Set(['block.html', 'address.html', '404.html', 'scenes.html']);
|
|
|
|
export function check(site) {
|
|
const pages = readdirSync(site).filter(f => f.endsWith('.html'));
|
|
const bad = [];
|
|
for (const page of pages) {
|
|
const html = readFileSync(join(site, page), 'utf8');
|
|
const nav = html.match(/<nav class="nav"[\s\S]*?<\/nav>/); if (!nav) { bad.push(`${page}: no <nav class="nav">`); continue; }
|
|
const n = nav[0];
|
|
if (!/<span class="word">IGNEUM<\/span>/.test(n)) bad.push(`${page}: no wordmark`);
|
|
if (!/<svg class="brand-mark"/.test(n)) bad.push(`${page}: no mark`);
|
|
for (const g of Object.keys(GROUPS)) {
|
|
if (!new RegExp(`<button type="button" class="group-btn" id="nav-btn-${g}" data-group="${g}"(?: data-active)? aria-expanded="(true|false)" aria-controls="nav-panel-${g}" aria-haspopup="true"`).test(n)) bad.push(`${page}: the ${g} group button or its aria is missing`);
|
|
const panel = n.match(new RegExp(`<div class="nav-panel" id="nav-panel-${g}"[\\s\\S]*?<\\/div>\\s*<\\/div>`));
|
|
if (!panel) { bad.push(`${page}: no panel for ${g}`); continue; }
|
|
for (const r of GROUPS[g]) if (!new RegExp(`<a href="${r}"[^>]*><b>[^<]+</b><span>[^<]+</span></a>`).test(panel[0])) bad.push(`${page}: the ${g} panel lacks ${r} with its description`);
|
|
}
|
|
const sheet = n.match(/<div class="nav-sheet" id="nav-sheet"[\s\S]*?<\/nav>/);
|
|
if (!sheet) { bad.push(`${page}: no phone sheet`); continue; }
|
|
for (const r of ROUTES) if (!new RegExp(`<a href="${r}"[^>]*><b>[^<]+</b><span>[^<]+</span></a>`).test(sheet[0])) bad.push(`${page}: the sheet lacks ${r} with its description`);
|
|
if (!/<a href="\/download" class="btn primary small cta" data-nav="download">Download<\/a>/.test(n)) bad.push(`${page}: no Download button in the bar`);
|
|
if (!/<a href="\/download" class="btn primary big" data-nav="download">Download<\/a>/.test(sheet[0])) bad.push(`${page}: no Download in the sheet`);
|
|
if (!/<button class="burger" id="nav-burger" type="button" aria-expanded="(true|false)" aria-controls="nav-sheet"/.test(n)) bad.push(`${page}: the burger lacks its aria attributes`);
|
|
if (!/role="dialog" aria-modal="true" aria-label="Menu"/.test(sheet[0])) bad.push(`${page}: the sheet is not a dialog`);
|
|
if (/\.(group-btn|panel-list a|sheet-group a|nav \.cta)[^{]*\{[^}]*display:\s*none/.test(html.replace(/@media \(max-width:860px\)\{[\s\S]*?\}\}/g, ''))) bad.push(`${page}: a page style hides the bar's items`);
|
|
// the page's own route is marked in the bar (aria-current) unless it is one of the unlisted views
|
|
const route = '/' + page.replace(/\.html$/, '').replace(/^index$/, '');
|
|
if (route !== '/' && !UNLISTED.has(page) && !ROUTES.includes(route)) bad.push(`${page}: served but named in no panel (add it to a group in nav.html and here)`);
|
|
}
|
|
// every panel route is a served page
|
|
for (const r of ROUTES) if (!pages.includes(r.slice(1) + '.html')) bad.push(`${r} is in a panel but has no page`);
|
|
return { pages: pages.length, bad };
|
|
}
|
|
|
|
const args = process.argv.slice(2);
|
|
if (args[0] === '--self-test') {
|
|
const site = join(here, '..', '..', 'site');
|
|
const fx = mkdtempSync(join(tmpdir(), 'nav-check-'));
|
|
try {
|
|
for (const f of readdirSync(site).filter(f => f.endsWith('.html'))) writeFileSync(join(fx, f), readFileSync(join(site, f), 'utf8'));
|
|
const clean = check(fx); if (clean.bad.length) { console.error('self-test: the tree copy should pass\n' + clean.bad.join('\n')); process.exit(1); }
|
|
// known failed: /ledger dropped from the sheet of one page
|
|
const p = join(fx, 'miner.html'); const h = readFileSync(p, 'utf8');
|
|
const sheetAt = h.indexOf('<div class="nav-sheet"'); const broken = h.slice(0, sheetAt) + h.slice(sheetAt).replace(/<a href="\/ledger"[^>]*>[\s\S]*?<\/a>/, '');
|
|
writeFileSync(p, broken);
|
|
const r = check(fx); if (!r.bad.some(b => /miner\.html: the sheet lacks \/ledger/.test(b))) { console.error('self-test: a route dropped from the sheet was not caught'); process.exit(1); }
|
|
// known failed: a group button without its aria
|
|
writeFileSync(p, h.replace('id="nav-btn-learn" data-group="learn" aria-expanded="false"', 'id="nav-btn-learn" data-group="learn"'));
|
|
const r2 = check(fx); if (!r2.bad.some(b => /miner\.html: the learn group button/.test(b))) { console.error('self-test: a group button without aria was not caught'); process.exit(1); }
|
|
console.log('self-test passed: a route dropped from the sheet and a group button without its aria are caught; the tree passes');
|
|
} finally { rmSync(fx, { recursive: true, force: true }); }
|
|
} else {
|
|
const site = args[0] || join(here, '..', '..', 'site');
|
|
const { pages, bad } = check(site);
|
|
if (bad.length) { console.error('site nav check: ' + bad.length + ' problems\n' + bad.join('\n')); process.exit(1); }
|
|
console.log(`site nav check: ${pages} pages carry the slim bar: the mark, three groups with ${ROUTES.length} routes in the panels and the sheet, Download, the burger`);
|
|
}
|