#!/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(`### \`${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();