import { promises as fs } from "node:fs"; import path from "node:path"; const DIST_DIR = path.resolve("dist"); /** @param {string} dir */ async function walkHtml(dir) { /** @type {string[]} */ const out = []; const entries = await fs.readdir(dir, { withFileTypes: true }); for (const e of entries) { const p = path.join(dir, e.name); if (e.isDirectory()) out.push(...(await walkHtml(p))); else if (e.isFile() && p.endsWith(".html")) out.push(p); } return out; } /** @param {string} attrs */ function getClass(attrs) { const m = attrs.match(/\bclass="([^"]*)"/i); return m ? m[1] : ""; } /** @param {{tag:string,id:string,cls:string}} occ */ function score(occ) { // plus petit = mieux (on garde) if (occ.tag === "span" && /\bdetails-anchor\b/.test(occ.cls)) return 0; if (/^h[1-6]$/.test(occ.tag)) return 1; if (occ.tag === "p" && occ.id.startsWith("p-")) return 2; return 10; // tout le reste (toc, nav, etc.) } async function main() { let changedFiles = 0; let removed = 0; const files = await walkHtml(DIST_DIR); for (const file of files) { let html = await fs.readFile(file, "utf8"); // capture: const re = /<([A-Za-z][\w:-]*)([^>]*?)\s+id="([^"]+)"([^>]*?)>/g; /** @type {Array<{id:string,tag:string,pre:string,post:string,start:number,end:number,cls:string,idx:number}>} */ const occs = []; let m; let idx = 0; while ((m = re.exec(html)) !== null) { const tag = m[1].toLowerCase(); const pre = m[2] || ""; const id = m[3] || ""; const post = m[4] || ""; const fullAttrs = `${pre}${post}`; const cls = getClass(fullAttrs); occs.push({ id, tag, pre, post, start: m.index, end: m.index + m[0].length, cls, idx: idx++, }); } if (occs.length === 0) continue; /** @type {Map>} */ const byId = new Map(); for (const o of occs) { if (!o.id) continue; const arr = byId.get(o.id) || []; arr.push(o); byId.set(o.id, arr); } /** @type {Array<{start:number,end:number,repl:string}>} */ const edits = []; for (const [id, arr] of byId.entries()) { if (arr.length <= 1) continue; // choisir le “meilleur” porteur d’id : details-anchor > h2/h3... > p-... > reste const sorted = [...arr].sort((a, b) => { const sa = score(a); const sb = score(b); if (sa !== sb) return sa - sb; return a.idx - b.idx; // stable: premier }); const keep = sorted[0]; for (const o of sorted.slice(1)) { // remplacer l’ouverture de tag en supprimant l’attribut id // ==> const repl = `<${o.tag}${o.pre}${o.post}>`; edits.push({ start: o.start, end: o.end, repl }); removed++; } // sécurité: on “force” l'id sur le keep (au cas où il aurait été modifié plus haut) // (on ne touche pas au keep ici, juste on ne le retire pas) void keep; void id; } if (edits.length === 0) continue; // appliquer de la fin vers le début edits.sort((a, b) => b.start - a.start); for (const e of edits) { html = html.slice(0, e.start) + e.repl + html.slice(e.end); } await fs.writeFile(file, html, "utf8"); changedFiles++; } if (changedFiles > 0) { console.log(`✅ dedupe-ids-dist: files_changed=${changedFiles} ids_removed=${removed}`); } else { console.log("ℹ️ dedupe-ids-dist: no duplicates found"); } } main().catch((err) => { console.error("❌ dedupe-ids-dist failed:", err); process.exit(1); });