// scripts/fix-pandoc-false-ordered-lists.mjs // Convertit les "fausses listes" Pandoc (1. gros paragraphe / 2. gros paragraphe / ...) // en paragraphes normaux, sans toucher aux petites listes légitimes. // Sécurités : // - ignore les blocs code fences // - n'agit que sur des listes top-level // - heuristique (taille/volume) + backup .bak import fs from "node:fs/promises"; import path from "node:path"; const args = process.argv.slice(2); const DRY = args.includes("--dry-run"); const files = args.filter((a) => !a.startsWith("--")); if (files.length === 0) { console.error("Usage: node scripts/fix-pandoc-false-ordered-lists.mjs [file2.mdx ...] [--dry-run]"); process.exit(1); } const MIN_ITEMS = 6; // en dessous -> on ne touche pas const MIN_AVG_LEN = 140; // moyenne chars/item -> prose const MAX_SHORT_RATIO = 0.25; // si trop d'items courts -> c'est une vraie liste function splitFrontmatter(txt) { const m = txt.match(/^---\n[\s\S]*?\n---\n/); if (!m) return { front: "", body: txt }; return { front: m[0], body: txt.slice(m[0].length) }; } function isFence(line) { const t = line.trim(); return t.startsWith("```") || t.startsWith("~~~"); } function isOlItemStart(line) { // top-level only (pas d'indent) return /^(\d{1,3})([.)])\s+/.test(line); } function stripOlMarker(line) { return line.replace(/^(\d{1,3})([.)])\s+/, ""); } function visibleLen(s) { // approx : retire les backticks et liens markdown return String(s) .replace(/`+/g, "") .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") .replace(/\s+/g, " ") .trim().length; } function looksLikeFalseList(items) { if (items.length < MIN_ITEMS) return false; const lens = items.map((it) => visibleLen(it.join("\n"))); const avg = lens.reduce((a, b) => a + b, 0) / Math.max(1, lens.length); const short = lens.filter((n) => n < 60).length / Math.max(1, lens.length); // si ça ressemble à une liste de “vrais points” (beaucoup de courts), on laisse if (short > MAX_SHORT_RATIO) return false; return avg >= MIN_AVG_LEN; } function rewriteFalseList(items) { // paragraphes séparés const out = []; for (const it of items) { const txt = it.join("\n").trimEnd(); if (txt) out.push(txt, ""); // blank line } return out.join("\n"); } function processBody(body) { const lines = body.split(/\r?\n/); let inFence = false; let changed = false; const out = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (isFence(line)) { inFence = !inFence; out.push(line); continue; } if (inFence) { out.push(line); continue; } if (!isOlItemStart(line)) { out.push(line); continue; } // Parse bloc de liste ordonnée top-level const items = []; let cur = [stripOlMarker(line)]; i++; for (; i < lines.length; i++) { const l = lines[i]; if (isFence(l)) { // fin de bloc list (safe) i--; // reprocess fence in outer loop break; } if (isOlItemStart(l)) { items.push(cur); cur = [stripOlMarker(l)]; continue; } // continuation: indent => appartient à l'item if (/^\s{2,}\S/.test(l)) { cur.push(l.replace(/^\s{2}/, "")); continue; } // ligne vide => conserve dans item (rare) mais ne termine pas forcément if (l.trim() === "") { cur.push(""); continue; } // ligne non indentée => fin de bloc de liste i--; // reprocess this line outside break; } items.push(cur); // Décision heuristique if (looksLikeFalseList(items)) { changed = true; out.push(rewriteFalseList(items)); } else { // on remet tel quel (reconstruit en markdown list) for (let k = 0; k < items.length; k++) { out.push(`${k + 1}. ${items[k][0]}`); for (const extra of items[k].slice(1)) out.push(extra === "" ? "" : ` ${extra}`); } } } return { text: out.join("\n"), changed }; } for (const file of files) { const p = path.resolve(file); const raw = await fs.readFile(p, "utf8"); const { front, body } = splitFrontmatter(raw); const res = processBody(body); if (!res.changed) { console.log(`✅ ${file}: no false ordered-lists detected`); continue; } if (DRY) { console.log(`🟡 ${file}: would rewrite false ordered-lists (dry-run)`); continue; } const bak = `${p}.bak`; await fs.writeFile(bak, raw, "utf8"); await fs.writeFile(p, front + res.text, "utf8"); console.log(`✅ ${file}: rewritten (backup -> ${path.basename(bak)})`); }