76 lines
3.6 KiB
JavaScript
76 lines
3.6 KiB
JavaScript
// Check generated HTML, so Markdown links and MDX components are both covered.
|
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
import { join, relative, sep } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = fileURLToPath(new URL('../dist/', import.meta.url));
|
|
if (!existsSync(root)) throw new Error('Missing dist/. Run npm run build first.');
|
|
const origin = 'https://docs.simba-robotics.com';
|
|
const files = [];
|
|
function walk(dir) {
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const path = join(dir, entry.name);
|
|
if (entry.isDirectory()) walk(path);
|
|
else files.push(path);
|
|
}
|
|
}
|
|
walk(root);
|
|
const pages = new Map(files.filter((path) => path.endsWith('.html'))
|
|
.map((path) => [path, readFileSync(path, 'utf8')]));
|
|
const errors = [];
|
|
let checked = 0;
|
|
let diagrams = 0;
|
|
|
|
for (const [path, html] of pages) {
|
|
const page = relative(root, path).split(sep).join('/');
|
|
const base = new URL(page.replace(/index\.html$/, ''), origin);
|
|
if (html.includes('class="katex-error"')) errors.push(`${page}: invalid math`);
|
|
if (/<style\b[^>]*\sset:html=/.test(html)) errors.push(`${page}: MDX emitted inert SVG styles`);
|
|
const diagramCount = [...html.matchAll(/class="mermaid-diagram"/g)].length;
|
|
if (diagramCount) {
|
|
diagrams += diagramCount;
|
|
const staticCount = [...html.matchAll(/data-rendered="static"/g)].length;
|
|
const lightCount = [...html.matchAll(/class="mermaid-variant mermaid-light"/g)].length;
|
|
const darkCount = [...html.matchAll(/class="mermaid-variant mermaid-dark"/g)].length;
|
|
const svgCount = [...html.matchAll(/<svg\b[^>]*\bid="mermaid-[^"]+"/g)].length;
|
|
if ([staticCount, lightCount, darkCount].some((count) => count !== diagramCount) || svgCount !== diagramCount * 2) {
|
|
errors.push(`${page}: Mermaid must contain both pre-rendered SVG themes`);
|
|
}
|
|
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]);
|
|
if (ids.length !== new Set(ids).size) errors.push(`${page}: duplicate IDs may break inline SVG references`);
|
|
}
|
|
// Astro emits quoted, escaped attributes. Code examples escape '<', so they
|
|
// are not mistaken for real links here. External URLs are not fetched.
|
|
for (const tag of html.matchAll(/<(?:a|link|script|img)\b[^>]*>/g)) {
|
|
// Canonical/hreflang are SEO metadata, not navigation or loaded assets.
|
|
if (/^<link\b/.test(tag[0]) && /\srel="(?:canonical|alternate)"/.test(tag[0])) continue;
|
|
for (const attribute of tag[0].matchAll(/\s(?:href|src)="([^"]*)"/g)) {
|
|
const ref = attribute[1].replaceAll('&', '&');
|
|
const url = new URL(ref, base);
|
|
if (url.origin !== base.origin) continue;
|
|
let target = join(root, decodeURIComponent(url.pathname));
|
|
if (existsSync(target) && statSync(target).isDirectory()) target = join(target, 'index.html');
|
|
if (!existsSync(target)) {
|
|
errors.push(`${page}: missing ${ref}`);
|
|
continue;
|
|
}
|
|
checked++;
|
|
if (url.hash && pages.has(target)) {
|
|
const id = decodeURIComponent(url.hash.slice(1));
|
|
if (!pages.get(target).includes(`id="${id}"`)) errors.push(`${page}: missing anchor ${ref}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!existsSync(join(root, '404.html'))) errors.push('Missing 404.html');
|
|
for (const path of files) {
|
|
if (/\.(?:woff2?|ttf|otf|eot)$/i.test(path)) errors.push(`Unexpected bundled font: ${relative(root, path)}`);
|
|
}
|
|
if (errors.length) {
|
|
console.error(errors.join('\n'));
|
|
process.exitCode = 1;
|
|
} else {
|
|
console.log(`Checked ${pages.size} HTML pages, ${checked} local links/assets, and ${diagrams} static Mermaid diagrams; no broken anchors, math errors, or bundled fonts.`);
|
|
}
|