Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 101 additions & 11 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ 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));
const dist = path.join(root, 'dist');
Expand Down Expand Up @@ -42,6 +44,43 @@ if (!existsSync(licensePath)) {
}
const licenseText = readFileSync(licensePath, 'utf8');

// --- schemas: published schemas, sitting next to docs/ in the tool repo ------

// Absent until the release that introduces schemas/, so a missing folder only
// warns - the rest of the site must keep deploying against older releases.
// Once the folder exists, anything wrong with its contents is fatal.
const schemasDir = locateSchemas(docsDir);
let schemas = [];
if (schemasDir) {
try {
schemas = collectSchemas(schemasDir);
} catch (err) {
console.error(`error: ${err.message}`);
process.exit(1);
}
if (schemas.length === 0) {
console.error(`error: ${schemasDir} holds no v<N>.<format> files - did the layout change?`);
process.exit(1);
}
}
if (!schemasDir) {
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 ------

function readVersion() {
Expand Down Expand Up @@ -141,34 +180,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,
});
Expand All @@ -193,9 +249,41 @@ for (const page of pages) {
writeFileSync(path.join(dir, 'index.html'), renderDoc(page));
}

// Schemas are machine-consumed artifacts, not pages: copied byte-for-byte and
// never through the markdown pipeline, whose link rewriting would corrupt the
// identifiers ($id, $ref, $schema) inside them.
if (schemasDir) {
cpSync(schemasDir, path.join(dist, 'schemas'), { recursive: true });
for (const schema of schemas) {
// A real file, not a redirect - GitHub Pages has no server-side redirects
// and a meta-refresh means nothing to a JSON fetch. The bytes are the
// highest version's verbatim, $id included: a copy fetched from
// latest.json must still say which version it actually is. One alias per
// format, so v1.xml would get latest.xml alongside latest.json.
writeFileSync(
path.join(dist, 'schemas', schema.name, `latest.${schema.format}`),
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/',
];
Expand All @@ -212,4 +300,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'})`,
);
15 changes: 12 additions & 3 deletions src/layout.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>`;
const moonIcon = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/></svg>`;
Expand Down Expand Up @@ -135,15 +138,21 @@ ${groupHtml}
<div class="sidebar-backdrop" hidden></div>`;
}

// 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 '<aside class="toc" aria-hidden="true"></aside>';
return `<aside class="toc">
<nav aria-label="On this page">
<p class="toc-label">On this page</p>
<ul>
${toc
.map((h) => `<li class="toc-l${h.level}"><a href="#${h.id}">${h.text}</a></li>`)
.map((h) => {
const v = h.version ? ` data-version="${esc(h.version)}"` : '';
const cur = h.current ? ' is-current' : '';
return `<li class="toc-l${h.level}${cur}"${v}><a href="#${h.id}">${h.text}</a></li>`;
})
.join('\n ')}
</ul>
</nav>
Expand Down
Loading