// configuration.md as list + inspector. its tables become one entry per setting at build
// time, so the html has every setting written out (ctrl-f, no js, search all work) and the
// script only picks which entry shows. a table that doesn't look like a settings table
// fails the build instead of quietly losing settings.
import type { Root, Element, ElementContent, RootContent } from "hast";
import { toString } from "hast-util-to-string";
export interface Setting {
key: string;
/** section heading id */
section: string;
defaultText: string;
firstLine: string;
}
export interface SettingsSection {
id: string;
title: string;
settings: Setting[];
}
const HEADER = ["variable", "default", "description"];
const el = (tagName: string, className: string | null, children: ElementContent[], props: Record<string, string> = {}): Element => ({
type: "element",
tagName,
properties: { ...(className ? { className: [className] } : {}), ...props },
children,
});
const text = (value: string): ElementContent => ({ type: "text", value });
const isEl = (n: unknown, tag: string): n is Element => (n as Element)?.type === "element" && (n as Element).tagName === tag;
const kids = (n: Element, tag: string) => n.children.filter((c): c is Element => isEl(c, tag));
function rows(table: Element): Element[][] {
return [...kids(table, "thead"), ...kids(table, "tbody")].flatMap((part) => kids(part, "tr").map((tr) => kids(tr, "th").concat(kids(tr, "td"))));
}
const isSettingsTable = (table: Element) => {
const head = rows(table)[0];
return head?.length === 3 && head.every((c, i) => toString(c).trim().toLowerCase() === HEADER[i]);
};
export function buildSettings(tree: Root, docId: string): SettingsSection[] {
const sections: SettingsSection[] = [];
const intro: RootContent[] = [];
const out: RootContent[] = [];
const seen = new Set<string>();
let current: { h2: Element; body: RootContent[]; entries: Element[]; s: SettingsSection } | null = null;
const flush = () => {
if (!current) return;
const { h2, body, entries, s } = current;
const summary = s.settings.length
? [el("ul", "cfg-sum", s.settings.map((st) => el("li", null, [el("a", null, [
el("code", null, [text(st.key)]),
el("span", "d", [text(st.defaultText)]),
el("span", "first", [text(st.firstLine)]),
], { href: `#${st.key}` })])))]
: [];
out.push(el("section", "cfg-item cfg-sec", [h2 as ElementContent, ...(body as ElementContent[]), ...summary], { "data-item": s.id }));
out.push(...entries);
current = null;
};
for (const node of tree.children) {
if (isEl(node, "h2")) {
flush();
const id = String(node.properties.id);
const s = { id, title: toString(node), settings: [] };
sections.push(s);
current = { h2: node, body: [], entries: [], s };
continue;
}
if (!current) {
intro.push(node);
continue;
}
if (!isEl(node, "table") || !isSettingsTable(node)) {
if (isEl(node, "table")) throw new Error(`${docId}.md: a table in "${current.s.title}" isn't a variable | default | description table`);
current.body.push(node);
continue;
}
for (const [name, dflt, desc] of rows(node).slice(1)) {
const code = name?.children.filter((c) => !(c.type === "text" && !c.value.trim()));
if (!code || code.length !== 1 || !isEl(code[0], "code") || !desc) {
throw new Error(`${docId}.md: a row in "${current.s.title}" doesn't start with one \`VARIABLE\` cell: ${name ? toString(name) : "(empty)"}`);
}
const key = toString(code[0]).trim();
if (seen.has(key)) throw new Error(`${docId}.md lists ${key} twice`);
seen.add(key);
const st: Setting = { key, section: current.s.id, defaultText: toString(dflt).trim(), firstLine: toString(desc).trim() };
current.s.settings.push(st);
current.entries.push(el("article", "cfg-item cfg-var", [
el("h3", "cfg-name", [el("code", null, [text(key)])], { id: key }),
...(st.defaultText ? [el("p", "cfg-default", [text("default "), ...dflt.children])] : []),
el("div", "cfg-desc", desc.children),
], { "data-item": key, "data-sec": current.s.id }));
}
}
flush();
tree.children = [el("div", "cfg", [
el("section", "cfg-item cfg-intro", intro as ElementContent[], { "data-item": "" }),
...(out as ElementContent[]),
], { "data-cfg": "" })];
return sections;
}