|
|
|
|
@@ -1,35 +1,35 @@
|
|
|
|
|
// The docs pages, into site/dist/docs: the guides in docs.mjs, and an API reference for each
|
|
|
|
|
// service from api.mjs, laid out as docs.rs lays out a crate (#223): a root page listing its
|
|
|
|
|
// 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.mjs (site/build.sh runs it)
|
|
|
|
|
// 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 } from './api.mjs';
|
|
|
|
|
import { guides } from './docs.mjs';
|
|
|
|
|
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 => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
|
|
|
const esc = (s: unknown) => String(s).replace(/&/g, '&').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) {
|
|
|
|
|
function released(changelog: string) {
|
|
|
|
|
const m = fs.readFileSync(path.join(here, '..', changelog), 'utf8').match(/^## \[(\d+\.\d+\.\d+)\]/m);
|
|
|
|
|
return m ? m[1] : '';
|
|
|
|
|
return m?.[1] ?? '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function write(rel, html) {
|
|
|
|
|
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, current }) {
|
|
|
|
|
function shell({ title, description, sidebar, body }: { title: string, description: string, sidebar: string, body: string, current: string }) {
|
|
|
|
|
return `<!doctype html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
@@ -65,10 +65,10 @@ function shell({ title, description, sidebar, body, current }) {
|
|
|
|
|
|
|
|
|
|
/* ---------------- the guides ---------------- */
|
|
|
|
|
|
|
|
|
|
const REFS = [[ipx, 'ipx'], [directory, 'ipx-directory']];
|
|
|
|
|
const REFS: [Api, string][] = [[ipx, 'ipx'], [directory, 'ipx-directory']];
|
|
|
|
|
|
|
|
|
|
function guideSidebar(cur) {
|
|
|
|
|
const li = (href, name, on) => `<li><a href="${href}"${on ? ' aria-current="page"' : ''}>${name}</a></li>`;
|
|
|
|
|
function guideSidebar(cur: string) {
|
|
|
|
|
const li = (href: string, name: string, on: boolean) => `<li><a href="${href}"${on ? ' aria-current="page"' : ''}>${name}</a></li>`;
|
|
|
|
|
return `<nav aria-label="Docs">
|
|
|
|
|
<h2 class="side-h">Guides</h2>
|
|
|
|
|
<ul>${guides.map(g => li(g.href, g.name, g.href === cur)).join('')}</ul>
|
|
|
|
|
@@ -83,43 +83,46 @@ for (const g of guides) {
|
|
|
|
|
|
|
|
|
|
/* ---------------- the references ---------------- */
|
|
|
|
|
|
|
|
|
|
const METHOD_CLASS = { GET: 'm-get', POST: 'm-post', PATCH: 'm-patch', PUT: 'm-patch', DELETE: 'm-delete' };
|
|
|
|
|
const METHOD_CLASS: Record<Method, string> = { GET: 'm-get', POST: 'm-post', PATCH: 'm-patch', PUT: 'm-patch', DELETE: 'm-delete' };
|
|
|
|
|
|
|
|
|
|
function reference(api) {
|
|
|
|
|
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 => `${api.root}/types/${n}`;
|
|
|
|
|
const itemHref = (g, it) => `${api.root}/${g.id}/${it.slug}`;
|
|
|
|
|
const fnName = it => it.slug.replace(/-/g, '_');
|
|
|
|
|
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 => esc(s).replace(/\b[A-Z][A-Za-z]+\b/g, n => typeNames.has(n) ? `<a class="t" href="${typeHref(n)}">${n}</a>` : n);
|
|
|
|
|
const isType = s => /^[A-Z]/.test(s) && /[A-Za-z>]$/.test(s) && !/\s/.test(s.replace(/, /g, ','));
|
|
|
|
|
const access = it => it.access || 'signed in';
|
|
|
|
|
const ty = (s: string) => esc(s).replace(/\b[A-Z][A-Za-z]+\b/g, n => typeNames.has(n) ? `<a class="t" href="${typeHref(n)}">${n}</a>` : 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.
|
|
|
|
|
const usedBy = new Map();
|
|
|
|
|
// An endpoint, with its module, or a type that has it as a field (its module null).
|
|
|
|
|
const usedBy = new Map<string, ([Group, Item] | [null, ApiType])[]>();
|
|
|
|
|
for (const g of api.groups) for (const it of g.items) {
|
|
|
|
|
for (const s of [it.body, it.query, it.returns].filter(Boolean)) {
|
|
|
|
|
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)) {
|
|
|
|
|
if (!usedBy.has(n)) usedBy.set(n, []);
|
|
|
|
|
if (!usedBy.get(n).some(([, x]) => x === it)) usedBy.get(n).push([g, it]);
|
|
|
|
|
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) {
|
|
|
|
|
if (!usedBy.has(n)) usedBy.set(n, []);
|
|
|
|
|
if (!usedBy.get(n).some(([, x]) => x === t)) usedBy.get(n).push([null, t]);
|
|
|
|
|
const uses = usedBy.get(n) ?? [];
|
|
|
|
|
usedBy.set(n, uses);
|
|
|
|
|
if (!uses.some(([, x]) => x === t)) uses.push([null, t]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const crumbs = (...parts) => `<p class="path"><a href="${api.root}/">${esc(api.name)}</a>${parts.map(([h, n]) => `::<a href="${h}">${n}</a>`).join('')}</p>`;
|
|
|
|
|
const crumbs = (...parts: [href: string, name: string][]) => `<p class="path"><a href="${api.root}/">${esc(api.name)}</a>${parts.map(([h, n]) => `::<a href="${h}">${n}</a>`).join('')}</p>`;
|
|
|
|
|
const top = `<a class="crate" href="${api.root}/"><img src="/logo.svg" alt="" width="40" height="40"><span><b>${esc(label)}</b>${version ? `<span class="ver">${version}</span>` : ''}</span></a>`;
|
|
|
|
|
const back = `<p class="side-back"><a href="/docs/">All docs</a></p>`;
|
|
|
|
|
|
|
|
|
|
function sidebar({ group, item, type } = {}) {
|
|
|
|
|
function sidebar({ group, item, type }: { group?: Group, item?: Item, type?: ApiType } = {}) {
|
|
|
|
|
let s = top;
|
|
|
|
|
if (group) {
|
|
|
|
|
s += `<h2 class="side-h">Module <a href="${api.root}/${group.id}/">${group.name}</a></h2>
|
|
|
|
|
@@ -130,7 +133,7 @@ function reference(api) {
|
|
|
|
|
return `<nav aria-label="${esc(label)} reference">${s}</nav>${back}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const page = (rel, title, description, side, body) =>
|
|
|
|
|
const page = (rel: string, title: string, description: string, side: string, body: string) =>
|
|
|
|
|
write(rel, shell({ title, description, sidebar: side, body, current: 'docs' }));
|
|
|
|
|
|
|
|
|
|
// The root: what the crate page shows on docs.rs, its modules and types.
|
|
|
|
|
@@ -156,8 +159,8 @@ ${api.intro}
|
|
|
|
|
it.body && ` body ${ty(it.body)}`,
|
|
|
|
|
` -> ${isType(it.returns) ? ty(it.returns) : esc(it.returns)}`,
|
|
|
|
|
].filter(Boolean).join('\n');
|
|
|
|
|
const fieldsOf = n => api.types.find(t => t.name === n)?.fields || [];
|
|
|
|
|
const rows = list => `<dl class="fields">${list.map(([n, t, d]) => `<dt id="${it.slug}-${n}"><code>${esc(n)}: ${ty(t)}</code></dt><dd>${d || ''}</dd>`).join('')}</dl>`;
|
|
|
|
|
const fieldsOf = (n: string) => api.types.find(t => t.name === n)?.fields || [];
|
|
|
|
|
const rows = (list: Field[]) => `<dl class="fields">${list.map(([n, t, d]) => `<dt id="${it.slug}-${n}"><code>${esc(n)}: ${ty(t)}</code></dt><dd>${d || ''}</dd>`).join('')}</dl>`;
|
|
|
|
|
let body = `${crumbs([`${api.root}/${g.id}/`, g.name])}
|
|
|
|
|
<h1 class="item"><span class="kind">Endpoint</span> ${esc(api.name)}::${g.name}::<span class="name fn">${fnName(it)}</span><a class="srclink" href="${SRC}/${api.name === 'ipx' ? 'crates/ipx/src/web.rs' : 'crates/directory/src/lib.rs'}">Source</a></h1>
|
|
|
|
|
<pre class="decl"><code>${decl}</code></pre>
|
|
|
|
|
@@ -201,10 +204,10 @@ ${t.doc ? `<p class="summary">${t.doc}</p>` : ''}`;
|
|
|
|
|
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.mjs is otherwise a
|
|
|
|
|
// 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(Boolean)) {
|
|
|
|
|
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}`);
|
|
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|