diff --git a/build.mjs b/build.mjs index e550f0f..22a34a3 100644 --- a/build.mjs +++ b/build.mjs @@ -11,6 +11,7 @@ import { docShell, esc, EXT_ATTRS, GITHUB_URL, highlightTokens, SITE_URL } from import { renderLanding } from './src/landing.mjs'; import { renderImpressum } from './src/impressum.mjs'; import { renderLicense } from './src/license.mjs'; +import { renderSchemaDoc } from './src/schema-doc.mjs'; import { collectSchemas, locateSchemas } from './src/schemas.mjs'; const root = path.dirname(fileURLToPath(import.meta.url)); @@ -62,7 +63,21 @@ if (schemasDir) { process.exit(1); } } else { - console.warn('warn: no schemas/ in the flashtrace checkout - skipping /schemas/.'); + console.warn('warn: no schemas/ in the flashtrace checkout - skipping /schemas/ and /docs/schemas/.'); +} + +// Serving is format-agnostic - every schema file is copied verbatim whatever +// its extension. Rendering is not: src/schema-doc.mjs reads JSON Schema. A +// format we cannot render is still published, just without a docs page, and +// says so loudly rather than vanishing. +const documented = schemas.filter((s) => s.format === 'json'); +for (const s of schemas) { + if (s.format !== 'json') { + console.warn( + `warn: ${s.title} (schemas/${s.name}/v*.${s.format}) is served but not documented - ` + + 'src/schema-doc.mjs renders JSON Schema only.', + ); + } } // --- version: release tag from env, else the tool repo's package.json ------ @@ -164,34 +179,51 @@ const marked = new Marked({ }, }); -function renderDoc(page) { - state.toc = []; - state.slugCounts = new Map(); - const md = readFileSync(path.join(docsDir, page.file), 'utf8'); - const content = marked.parse(md); - const navGroups = [ +// Shared by the markdown docs and the generated schema pages, so both render +// the same sidebar. `current` is { slug } for a doc page, { schema } for a +// schema page - a schema page has no slug, so no doc entry matches. +function navGroups(current) { + const groups = [ { label: 'Getting started', - items: [{ title: 'Usage Guide', href: '/docs/usage/', current: page.slug === 'usage' }], + items: [{ title: 'Usage Guide', href: '/docs/usage/', current: current.slug === 'usage' }], }, { label: 'Specification', items: [ - { title: 'Overview', href: '/docs/', current: page.slug === '' }, + { title: 'Overview', href: '/docs/', current: current.slug === '' }, ...specPages.map((p) => ({ title: p.title, href: `/docs/${p.slug}/`, - current: p.slug === page.slug, + current: p.slug === current.slug, })), ], }, ]; + if (documented.length > 0) { + groups.push({ + label: 'Schemas', + items: documented.map((s) => ({ + title: s.title, + href: `/docs/schemas/${s.slug}/`, + current: s.slug === current.schema, + })), + }); + } + return groups; +} + +function renderDoc(page) { + state.toc = []; + state.slugCounts = new Map(); + const md = readFileSync(path.join(docsDir, page.file), 'utf8'); + const content = marked.parse(md); return docShell({ title: `${page.title} · flashtrace`, description: `flashtrace documentation - ${page.title}.`, path: page.slug ? `/docs/${page.slug}/` : '/docs/', version, - navGroups, + navGroups: navGroups({ slug: page.slug }), toc: state.toc, content, }); @@ -232,11 +264,25 @@ if (schemasDir) { schema.latest.bytes, ); } + for (const schema of documented) { + const dir = path.join(dist, 'docs', 'schemas', schema.slug); + mkdirSync(dir, { recursive: true }); + try { + writeFileSync( + path.join(dir, 'index.html'), + renderSchemaDoc({ schema, version, navGroups: navGroups({ schema: schema.slug }) }), + ); + } catch (err) { + console.error(`error: rendering /docs/schemas/${schema.slug}/ failed.\n${err.message}`); + process.exit(1); + } + } } const sitePaths = [ '/', ...pages.map((p) => (p.slug ? `/docs/${p.slug}/` : '/docs/')), + ...documented.map((s) => `/docs/schemas/${s.slug}/`), '/license/', '/impressum/', ]; @@ -253,4 +299,6 @@ cpSync(path.join(root, 'public'), dist, { recursive: true }); cpSync(path.join(root, 'src', 'styles', 'site.css'), path.join(dist, 'site.css')); cpSync(path.join(root, 'src', 'scripts', 'site.js'), path.join(dist, 'site.js')); -console.log(`built ${pages.length + 3} pages into dist/ (flashtrace ${version || 'unknown version'})`); +console.log( + `built ${pages.length + documented.length + 3} pages into dist/ (flashtrace ${version || 'unknown version'})`, +); diff --git a/src/layout.mjs b/src/layout.mjs index 72a790f..433e8a7 100644 --- a/src/layout.mjs +++ b/src/layout.mjs @@ -36,7 +36,10 @@ export function highlightTokens(escaped) { ); } -const themeInit = `(function(){try{var t=localStorage.getItem('ft-theme');if(t!=='light'&&t!=='dark'){t=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}document.documentElement.dataset.theme=t;}catch(e){document.documentElement.dataset.theme='light';}})();`; +// Also marks the document as scripted before first paint, so a JS-only control +// (the schema version picker) can be styled away when scripting is off, and +// the version filtering it drives never flashes in. +const themeInit = `(function(){document.documentElement.classList.add('js');try{var t=localStorage.getItem('ft-theme');if(t!=='light'&&t!=='dark'){t=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}document.documentElement.dataset.theme=t;}catch(e){document.documentElement.dataset.theme='light';}})();`; const sunIcon = ``; const moonIcon = ``; @@ -135,7 +138,9 @@ ${groupHtml} `; } -// Right-rail "On this page" TOC from [{ id, text, level }] (h2/h3). +// Right-rail "On this page" TOC from [{ id, text, level }] (h2/h3). An entry +// may carry { version, current } to tie it to one section of a versioned page, +// so the rail lists only what is on screen. function tocRail(toc) { if (!toc.length) return ''; return `