From d05b5186f5529fe257bfe0242e403449404ddc5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Randall=20Havens=20=E2=96=B3=20The=20Empathic=20Tec?= =?UTF-8?q?hnologist=20=E2=9F=81=20Doctor=20Who=2042?= Date: Sat, 8 Nov 2025 09:05:04 -0600 Subject: [PATCH] Create generate-index.mjs --- tools/generate-index.mjs | 125 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 tools/generate-index.mjs diff --git a/tools/generate-index.mjs b/tools/generate-index.mjs new file mode 100644 index 0000000..5e2f9aa --- /dev/null +++ b/tools/generate-index.mjs @@ -0,0 +1,125 @@ +/** + * generate-index.mjs (v2.0.0) + * Scans /public/{pinned,posts} for .html/.md files, emits public/index.json + * Deterministic, POSIX paths, reverse-chron default ordering handled client-side. + */ + +import { promises as fs } from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PUBLIC_DIR = path.resolve(__dirname, "../public"); +const ROOTS = ["pinned", "posts"]; +const ALLOWED = new Set([".html", ".md"]); +const MAX_TITLE_BYTES = 64 * 1024; + +const posix = path.posix; + +async function statSafe(p) { try { return await fs.stat(p); } catch { return null; } } +function isHidden(rel) { return /(^|\/)\./.test(rel); } // hide dotfiles/dirs +function toPosix(rel) { return rel.split(path.sep).join("/"); } + +async function readFirstChunk(abs) { + const fh = await fs.open(abs, "r"); + const { size } = await fh.stat(); + const len = Math.min(MAX_TITLE_BYTES, size); + const buf = Buffer.alloc(len); + await fh.read(buf, 0, len, 0); + await fh.close(); + return buf.toString("utf8"); +} + +function parseTitleFromHTML(raw) { + const m = raw.match(/]*>([\s\S]*?)<\/title>/i); + return m ? m[1].trim() : null; +} + +function parseTitleFromMD(raw) { + const m = raw.match(/^\s*#\s+(.+)\s*$/m); + return m ? m[1].trim() : null; +} + +async function walkDir(absRoot, relRoot) { + const st = await statSafe(absRoot); + const node = { + type: "dir", + name: path.basename(absRoot), + path: "/" + toPosix(relRoot), + mtime: st ? st.mtimeMs : 0, + children: [] + }; + if (!st?.isDirectory()) return node; + + const entries = await fs.readdir(absRoot, { withFileTypes: true }); + entries.sort((a, b) => a.name.localeCompare(b.name)); + + for (const e of entries) { + const abs = path.join(absRoot, e.name); + const rel = toPosix(path.join(relRoot, e.name)); + if (isHidden(rel)) continue; + + if (e.isDirectory()) { + node.children.push(await walkDir(abs, rel)); + } else { + const ext = path.extname(e.name).toLowerCase(); + if (!ALLOWED.has(ext)) continue; + const stFile = await statSafe(abs); + const raw = await readFirstChunk(abs); + let title = (ext === ".html") ? parseTitleFromHTML(raw) : parseTitleFromMD(raw); + title = title || e.name; + + node.children.push({ + type: "file", + name: e.name, + ext, + title, + path: "/" + rel, + mtime: stFile ? stFile.mtimeMs : 0, + size: stFile ? stFile.size : 0, + pinned: rel.startsWith("pinned/") + }); + } + } + + return node; +} + +function flatten(node, out = []) { + if (node.type === "file") { out.push(node); return out; } + for (const c of node.children || []) flatten(c, out); + return out; +} + +async function main() { + const index = { + generatedAt: new Date().toISOString(), + tree: { type: "dir", name: "/", path: "/", mtime: Date.now(), children: [] }, + flat: [] + }; + + for (const root of ROOTS) { + const abs = path.join(PUBLIC_DIR, root); + const st = await statSafe(abs); + if (!st?.isDirectory()) { + console.warn(`warning: /public/${root} missing — skipping`); + continue; + } + const node = await walkDir(abs, root); + node.name = root; + node.path = "/" + root; + index.tree.children.push(node); + } + + // Flatten in the order roots were added + for (const child of index.tree.children) flatten(child, index.flat); + + const outPath = path.join(PUBLIC_DIR, "index.json"); + await fs.writeFile(outPath, JSON.stringify(index, null, 2)); + console.log(`wrote ${posix.relative(PUBLIC_DIR, outPath)} with ${index.flat.length} files.`); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); \ No newline at end of file