Files
avcf/scripts/generate-docs.js
T
adrien b8e8ef9113 overhaul: modular architecture, tests, doc generation, warning system, and UX improvements
- Modular block files: split into blocks-algo, blocks-data, blocks-proc
- New modules: sections.js (labelled canvas areas), proc-browser.js (procedure tree modal), lib-loader.js (lazy library loading), templates.js (project templates), errno-data.js (errno table)
- Test infrastructure: browser test suite (28 tests) and Node pure-function tests (75 tests)
- Warning system: unified .block-warn for disconnected inputs, unconnected error hooks, and zero-exit-in-error-path
- Error path highlighting: red flow paths and block borders for error-only subgraphs
- Input hooks: hidden by default, shown during linking; outputs always at bottom
- Block editor modal: pencil icon, colour picker for accent, compact field labels
- Doc generation: standalone node script extracts JSDoc -> docs/API.md (124 declarations)
- Persistence: save/restore sections, errno selection, block colours
- Fixes: _segmentsIntersect bug, WCEnd crash from closest(this), drawpath guard on detached elements
2026-06-22 18:31:23 +02:00

159 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
// ── Standalone API documentation generator ────────────────────────────────
//
// Reads all production .js files, extracts JSDoc /** ... */ comment blocks
// together with the declaration they annotate, and writes a formatted
// Markdown file to docs/API.md.
//
// Zero dependencies - uses only Node.js built-in fs and path.
// Run: node scripts/generate-docs.js
//
// ────────────────────────────────────────────────────────────────────────────
const fs = require("fs");
const path = require("path");
const SOURCE_DIR = ".";
const OUTPUT_FILE = path.join("docs", "API.md");
const SOURCE_GLOB = [
"dom.js", "helpers.js", "error.js", "errno-data.js",
"draw.js", "sections.js", "wcblock.js", "blocks-algo.js",
"blocks-data.js", "blocks-proc.js", "wcprogram.js",
"persist.js", "lib-loader.js", "proc-browser.js",
"templates.js", "app.js"
];
// ── Parse helpers ────────────────────────────────────────────────────────
/** Extracts the first doc comment block preceding a declaration. */
function extractJSDoc(lines, startIdx) {
let i = startIdx;
while (i >= 0 && lines[i].trim() === "") i--;
if (i < 0) return null;
const line = lines[i].trim();
if (!line.endsWith("*/")) return null;
const end = i;
while (i >= 0 && !lines[i].trim().startsWith("/**")) i--;
if (i < 0) return null;
const raw = lines.slice(i, end + 1).join("\n");
return { raw, startLine: i + 1 };
}
/** Strips opening, closing, and leading star markers from a JSDoc block. */
function cleanJSDoc(raw) {
const singleLine = !raw.includes("\n");
if (singleLine) {
return raw.replace(/^\/\*\*\s*/, "").replace(/\s*\*\/$/, "").trim();
}
return raw
.replace(/^\/\*\*[\s\S]*?\n/, "")
.replace(/\s*\*\/\s*$/, "")
.split("\n")
.map(l => l.replace(/^\s*\* ?/, ""))
.join("\n")
.trim();
}
/** Extracts the name from a declaration line like class Foo, function foo, or const foo. */
function declName(line) {
const m = line.match(/^(?:class|function|const)\s+(\w+)/);
return m ? m[1] : null;
}
/** Determines the declaration kind (class/function/const) from the line. */
function declKind(line) {
if (/^class\b/.test(line)) return "class";
if (/^function\b/.test(line)) return "function";
if (/^const\b/.test(line)) return "const";
return "declaration";
}
// ── Generator ────────────────────────────────────────────────────────────
function generate() {
const blocks = [];
for (const filename of SOURCE_GLOB) {
const absPath = path.resolve(SOURCE_DIR, filename);
if (!fs.existsSync(absPath)) {
console.warn(` [warn] ${filename} not found, skipping`);
continue;
}
const src = fs.readFileSync(absPath, "utf-8");
const lines = src.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const name = declName(line);
if (!name) continue;
const jsdoc = extractJSDoc(lines, i - 1);
if (!jsdoc) continue;
const kind = declKind(line);
const desc = cleanJSDoc(jsdoc.raw);
// extract @param and @returns from the cleaned JSDoc
const params = [];
const returns = [];
const descLines = desc.split("\n");
const bodyLines = [];
for (const dl of descLines) {
const pm = dl.match(/^\s*@param\s+\{([^}]+)\}\s+(\S+)\s*(.*)/);
const rm = dl.match(/^\s*@returns\s+\{([^}]+)\}\s*(.*)/);
const tm = dl.match(/^\s*@type\s+\{([^}]+)\}\s*(.*)/);
if (pm) params.push({ type: pm[1], name: pm[2], desc: pm[3] });
else if (rm) returns.push({ type: rm[1], desc: rm[2] });
else if (tm) bodyLines.push(`Type: \`${tm[1]}\` ${tm[2]}`);
else bodyLines.push(dl);
}
const body = bodyLines.join("\n").trim();
blocks.push({ filename, name, kind, line: i + 1, body, params, returns });
}
}
// ── Write output ─────────────────────────────────────────────────────
fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true });
const out = fs.createWriteStream(OUTPUT_FILE);
out.write("# API Reference\n\n");
out.write(`Generated from JSDoc annotations in ${SOURCE_GLOB.length} source files.\n\n`);
// Group by file
let currentFile = "";
for (const b of blocks) {
if (b.filename !== currentFile) {
currentFile = b.filename;
out.write(`\n---\n## ${b.filename}\n\n`);
}
const anchor = `${b.kind}-${b.name}`;
out.write(`### <a name="${anchor}"></a>\`${b.kind}\` ${b.name} \n`);
out.write(`*Line ${b.line}*\n\n`);
if (b.body) out.write(b.body + "\n\n");
if (b.params.length > 0) {
out.write("**Parameters:**\n\n");
out.write("| Name | Type | Description |\n");
out.write("|------|------|------------|\n");
for (const p of b.params) {
out.write(`| \`${p.name}\` | \`${p.type}\` | ${p.desc} |\n`);
}
out.write("\n");
}
if (b.returns.length > 0) {
out.write("**Returns:** ");
for (const r of b.returns) {
out.write(`\`${r.type}\` — ${r.desc}`);
}
out.write("\n\n");
}
}
out.end();
console.log(`Wrote ${OUTPUT_FILE} (${blocks.length} documented declarations)`);
}
generate();