78 lines
2.9 KiB
JavaScript
78 lines
2.9 KiB
JavaScript
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { relative, dirname, join, sep } from "node:path";
|
|
import { randomBytes } from "node:crypto";
|
|
|
|
const arg = (k, d) => { const i = process.argv.indexOf(k); return i > -1 ? process.argv[i + 1] : d; };
|
|
const SRC = arg("--src", "content");
|
|
const TARGET = arg("--target", "_manifest.js");
|
|
|
|
// --- uuid v7: 48-bit ms timestamp (time-ordered) + version/variant + random ---
|
|
function uuidv7(){
|
|
const ts = Date.now();
|
|
const b = randomBytes(16);
|
|
b[0]=(ts/2**40)&0xff; b[1]=(ts/2**32)&0xff; b[2]=(ts/2**24)&0xff;
|
|
b[3]=(ts/2**16)&0xff; b[4]=(ts/2**8)&0xff; b[5]=ts&0xff;
|
|
b[6]=(b[6]&0x0f)|0x70; // version 7
|
|
b[8]=(b[8]&0x3f)|0x80; // variant 10
|
|
const h = b.toString("hex");
|
|
return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`;
|
|
}
|
|
const ID_RE = /<!--\s*id:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s*-->/i;
|
|
const idLine = id => `<!-- id: ${id} -->`;
|
|
|
|
const files = readdirSync(SRC).filter(f => f.endsWith(".md")).sort();
|
|
|
|
// --- discovery: every card gets a stable v7 id. mint if absent (incl. user-removed),
|
|
// remint on collision (bootstrap audit). writes the id into the card at EOF. ---
|
|
const seen = new Set();
|
|
const idOf = {};
|
|
let minted = 0, reminted = 0;
|
|
for (const f of files) {
|
|
const p = join(SRC, f);
|
|
let md = readFileSync(p, "utf8");
|
|
const m = md.match(ID_RE);
|
|
let id = m ? m[1].toLowerCase() : null;
|
|
if (!id) { // absent → mint, append at bottom
|
|
id = uuidv7();
|
|
md = md.replace(/\s*$/, "") + `\n\n${idLine(id)}\n`;
|
|
writeFileSync(p, md); minted++;
|
|
} else if (seen.has(id)) { // collision → remint in place
|
|
id = uuidv7();
|
|
md = md.replace(ID_RE, idLine(id));
|
|
writeFileSync(p, md); reminted++;
|
|
}
|
|
seen.add(id);
|
|
idOf[f] = id;
|
|
}
|
|
|
|
// --- manifest ---
|
|
const parseTags = md => ((md.match(/^---\n([\s\S]*?)\n---/)?.[1]
|
|
.match(/tags:\s*\[([^\]]*)\]/)?.[1]) || "")
|
|
.split(",").map(s => s.trim()).filter(Boolean);
|
|
|
|
const allTags = new Set();
|
|
for (const f of files) parseTags(readFileSync(join(SRC, f), "utf8")).forEach(t => allTags.add(t));
|
|
|
|
const spec = f => {
|
|
let r = relative(dirname(TARGET) || ".", join(SRC, f)).split(sep).join("/");
|
|
return r.startsWith(".") ? r : "./" + r;
|
|
};
|
|
|
|
const imports = files.map((f, i) => `import c${i} from ${JSON.stringify(spec(f))};`).join("\n");
|
|
const items = files.map((f, i) =>
|
|
` { slug: ${JSON.stringify(f.replace(/\.md$/,""))}, id: ${JSON.stringify(idOf[f])}, md: c${i} }`).join(",\n");
|
|
const tags = [...allTags].sort();
|
|
|
|
writeFileSync(TARGET,
|
|
`// AUTO-GENERATED by gen.mjs — do not edit
|
|
${imports}
|
|
|
|
export const CARDS = [
|
|
${items}
|
|
];
|
|
|
|
// tag vocabulary, derived at build (not boot)
|
|
export const TAGS = ${JSON.stringify(tags)};
|
|
`);
|
|
console.log(`gen: ${files.length} card(s), ${tags.length} tag(s), +${minted} id(s), ${reminted} reminted -> ${TARGET}`);
|