// The docs pages, into site/dist/docs: the guides in docs.mts, and an API reference for each
// service from api.mts, laid out as docs.rs lays out a crate (#223): a root page listing its
// modules and types, a page a module listing its endpoints, a page an endpoint and a page a type.
// No script: the site's Content-Security-Policy allows none of its own.
//
// node site/gen.mts (site/build.sh runs it)
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { ipx, directory, type Api, type ApiType, type Field, type Group, type Item, type Method } from './api.mts';
import { guides } from './docs.mts';
const here = path.dirname(fileURLToPath(import.meta.url));
const dist = path.join(here, 'dist');
const SRC = 'https://git.sdf1.net/rays/ipx/src/branch/main';
const esc = (s: unknown) => String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
/// The version a reference documents: its service's last release, from its changelog, not the
/// -dev version between releases.
function released(changelog: string) {
const m = fs.readFileSync(path.join(here, '..', changelog), 'utf8').match(/^## \[(\d+\.\d+\.\d+)\]/m);
return m?.[1] ?? '';
}
function write(rel: string, html: string) {
const file = path.join(dist, rel);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, html);
}
function shell({ title, description, sidebar, body }: { title: string, description: string, sidebar: string, body: string, current: string }) {
return `
`;
return ``;
}
for (const g of guides) {
write(g.file, shell({ title: g.title, description: g.description, sidebar: guideSidebar(g.href), body: g.body, current: 'docs' }));
}
/* ---------------- the references ---------------- */
const METHOD_CLASS: Record = { GET: 'm-get', POST: 'm-post', PATCH: 'm-patch', PUT: 'm-patch', DELETE: 'm-delete' };
function reference(api: Api) {
const label = api.label || api.name;
const version = released(api.changelog);
const typeNames = new Set(api.types.map(t => t.name));
const typeHref = (n: string) => `${api.root}/types/${n}`;
const itemHref = (g: Group, it: Item) => `${api.root}/${g.id}/${it.slug}`;
const fnName = (it: Item) => it.slug.replace(/-/g, '_');
// A Rust type, its names that are types of this API linked.
const ty = (s: string) => esc(s).replace(/\b[A-Z][A-Za-z]+\b/g, n => typeNames.has(n) ? `${n}` : n);
const isType = (s: string) => /^[A-Z]/.test(s) && /[A-Za-z>]$/.test(s) && !/\s/.test(s.replace(/, /g, ','));
const access = (it: Item) => it.access || 'signed in';
// Which endpoints mention each type, for its "Used by", as docs.rs lists where a type turns up.
// An endpoint, with its module, or a type that has it as a field (its module null).
const usedBy = new Map();
for (const g of api.groups) for (const it of g.items) {
for (const s of [it.body, it.query, it.returns].filter((s): s is string => !!s)) {
for (const n of s.match(/\b[A-Z][A-Za-z]+\b/g) || []) if (typeNames.has(n)) {
const uses = usedBy.get(n) ?? [];
usedBy.set(n, uses);
if (!uses.some(([, x]) => x === it)) uses.push([g, it]);
}
}
}
// Types inside types: a type used only as a field is reached from the type that holds it.
for (const t of api.types) for (const [, ft] of t.fields) {
for (const n of ft.match(/\b[A-Z][A-Za-z]+\b/g) || []) if (typeNames.has(n) && n !== t.name) {
const uses = usedBy.get(n) ?? [];
usedBy.set(n, uses);
if (!uses.some(([, x]) => x === t)) uses.push([null, t]);
}
}
const crumbs = (...parts: [href: string, name: string][]) => `
`);
for (const g of api.groups) {
page(`${api.root.slice(1)}/${g.id}/index.html`, `${api.name}::${g.name}`, g.title, sidebar({ group: g }),
`${crumbs()}
`;
}
page(`${api.root.slice(1)}/types/${t.name}.html`, `${t.name} - ${label}`, (t.doc || `${t.name} in ${label}'s API`).replace(/<[^>]+>/g, ''), sidebar({ type: t }), body);
}
// Every link to a type must name one there is: a misspelt type in api.mts is otherwise a
// name in plain text, which looks right and leads nowhere.
for (const g of api.groups) for (const it of g.items) for (const s of [it.body, it.query].filter((s): s is string => !!s)) {
for (const n of s.match(/\b[A-Z][A-Za-z]+\b/g) ?? []) if (!typeNames.has(n) && !['Vec', 'Option'].includes(n)) throw new Error(`${it.path}: no type ${n}`);
}
}
reference(ipx);
reference(directory);