199 lines
5.7 KiB
JavaScript
199 lines
5.7 KiB
JavaScript
#!/usr/bin/env node
|
||
import fs from "node:fs/promises";
|
||
import path from "node:path";
|
||
import process from "node:process";
|
||
|
||
const CWD = process.cwd();
|
||
const DIST_ROOT = path.join(CWD, "dist");
|
||
const ALIASES_PATH = path.join(CWD, "src", "anchors", "anchor-aliases.json");
|
||
|
||
const argv = process.argv.slice(2);
|
||
const DRY_RUN = argv.includes("--dry-run");
|
||
const STRICT = argv.includes("--strict") || process.env.CI === "1" || process.env.CI === "true";
|
||
|
||
function escRe(s) {
|
||
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
}
|
||
|
||
async function exists(p) {
|
||
try {
|
||
await fs.access(p);
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function normalizeRoute(route) {
|
||
let r = String(route || "").trim();
|
||
if (!r.startsWith("/")) r = "/" + r;
|
||
if (!r.endsWith("/")) r = r + "/";
|
||
r = r.replace(/\/{2,}/g, "/");
|
||
return r;
|
||
}
|
||
|
||
function countIdAttr(html, id) {
|
||
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "gi");
|
||
let c = 0;
|
||
while (re.exec(html)) c++;
|
||
return c;
|
||
}
|
||
|
||
function findStartTagWithId(html, id) {
|
||
const re = new RegExp(
|
||
`<([a-zA-Z0-9:-]+)\\b[^>]*\\bid=(["'])${escRe(id)}\\2[^>]*>`,
|
||
"i"
|
||
);
|
||
const m = re.exec(html);
|
||
if (!m) return null;
|
||
return { tagName: String(m[1]).toLowerCase(), tag: m[0] };
|
||
}
|
||
|
||
function isInjectedAliasSpan(html, id) {
|
||
const found = findStartTagWithId(html, id);
|
||
if (!found) return false;
|
||
if (found.tagName !== "span") return false;
|
||
return /\bclass=(["'])(?:(?!\1).)*\bpara-alias\b(?:(?!\1).)*\1/i.test(found.tag);
|
||
}
|
||
|
||
function injectBeforeId(html, newId, injectHtml) {
|
||
const re = new RegExp(
|
||
`(<[^>]+\\bid=(["'])${escRe(newId)}\\2[^>]*>)`,
|
||
"i"
|
||
);
|
||
const m = html.match(re);
|
||
if (!m || m.index == null) return { html, injected: false };
|
||
const i = m.index;
|
||
const out = html.slice(0, i) + injectHtml + "\n" + html.slice(i);
|
||
return { html: out, injected: true };
|
||
}
|
||
|
||
async function main() {
|
||
if (!(await exists(ALIASES_PATH))) {
|
||
console.log("ℹ️ Aucun fichier d'aliases (src/anchors/anchor-aliases.json). Skip.");
|
||
return;
|
||
}
|
||
|
||
const raw = await fs.readFile(ALIASES_PATH, "utf-8");
|
||
|
||
/** @type {Record<string, Record<string,string>>} */
|
||
let aliases;
|
||
try {
|
||
aliases = JSON.parse(raw);
|
||
} catch (e) {
|
||
throw new Error(`JSON invalide: ${ALIASES_PATH} (${e?.message || e})`);
|
||
}
|
||
|
||
if (!aliases || typeof aliases !== "object" || Array.isArray(aliases)) {
|
||
throw new Error(`Format invalide: attendu { route: { oldId: newId } } dans ${ALIASES_PATH}`);
|
||
}
|
||
|
||
const routes = Object.keys(aliases || {});
|
||
if (routes.length === 0) {
|
||
console.log("ℹ️ Aliases vides. Rien à injecter.");
|
||
return;
|
||
}
|
||
|
||
let changedFiles = 0;
|
||
let injectedCount = 0;
|
||
let warnCount = 0;
|
||
|
||
for (const routeKey of routes) {
|
||
const route = normalizeRoute(routeKey);
|
||
const map = aliases[routeKey] || {};
|
||
const entries = Object.entries(map);
|
||
|
||
if (route !== routeKey) {
|
||
const msg = `⚠️ routeKey non normalisée: "${routeKey}" → "${route}" (corrige anchor-aliases.json)`;
|
||
if (STRICT) throw new Error(msg);
|
||
console.log(msg);
|
||
warnCount++;
|
||
}
|
||
|
||
if (entries.length === 0) continue;
|
||
|
||
const rel = route.replace(/^\/+|\/+$/g, "");
|
||
const htmlPath = path.join(DIST_ROOT, rel, "index.html");
|
||
|
||
if (!(await exists(htmlPath))) {
|
||
const msg = `⚠️ dist introuvable pour route=${route} (${htmlPath})`;
|
||
if (STRICT) throw new Error(msg);
|
||
console.log(msg);
|
||
warnCount++;
|
||
continue;
|
||
}
|
||
|
||
let html = await fs.readFile(htmlPath, "utf-8");
|
||
let fileChanged = false;
|
||
|
||
for (const [oldId, newId] of entries) {
|
||
if (!oldId || !newId) continue;
|
||
|
||
const oldCount = countIdAttr(html, oldId);
|
||
|
||
// ✅ déjà injecté => idempotent
|
||
if (oldCount > 0 && isInjectedAliasSpan(html, oldId)) {
|
||
if (STRICT && oldCount !== 1) {
|
||
throw new Error(`oldId dupliqué (${oldCount}) alors qu'il est censé être unique: ${route} id=${oldId}`);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// ⛔️ oldId existe déjà "en vrai" => alias inutile/inversé
|
||
if (oldCount > 0) {
|
||
const found = findStartTagWithId(html, oldId);
|
||
const where = found ? `<${found.tagName} … id="${oldId}" …>` : `id="${oldId}"`;
|
||
const msg =
|
||
`⚠️ alias inutile/inversé: oldId déjà présent (${where}). ` +
|
||
`Supprime ${oldId} -> ${newId} (ou corrige le sens) pour route=${route}`;
|
||
if (STRICT) throw new Error(msg);
|
||
console.log(msg);
|
||
warnCount++;
|
||
continue;
|
||
}
|
||
|
||
// newId doit exister UNE fois (sinon injection ambiguë)
|
||
const newCount = countIdAttr(html, newId);
|
||
if (newCount !== 1) {
|
||
const msg = `⚠️ newId non-unique (${newCount}) : ${route} new=${newId} (injection ambiguë)`;
|
||
if (STRICT) throw new Error(msg);
|
||
console.log(msg);
|
||
warnCount++;
|
||
continue;
|
||
}
|
||
|
||
const aliasSpan = `<span id="${oldId}" class="para-alias" aria-hidden="true"></span>`;
|
||
const r = injectBeforeId(html, newId, aliasSpan);
|
||
|
||
if (!r.injected) {
|
||
const msg = `⚠️ injection impossible (pattern non trouvé) : ${route} new=${newId}`;
|
||
if (STRICT) throw new Error(msg);
|
||
console.log(msg);
|
||
warnCount++;
|
||
continue;
|
||
}
|
||
|
||
html = r.html;
|
||
fileChanged = true;
|
||
injectedCount++;
|
||
}
|
||
|
||
if (fileChanged) {
|
||
changedFiles++;
|
||
if (!DRY_RUN) await fs.writeFile(htmlPath, html, "utf-8");
|
||
}
|
||
}
|
||
|
||
console.log(
|
||
`✅ inject-anchor-aliases: files_changed=${changedFiles} aliases_injected=${injectedCount} warnings=${warnCount}` +
|
||
(DRY_RUN ? " (dry-run)" : "")
|
||
);
|
||
|
||
if (STRICT && warnCount > 0) process.exit(2);
|
||
}
|
||
|
||
main().catch((e) => {
|
||
console.error("💥 inject-anchor-aliases:", e?.message || e);
|
||
process.exit(1);
|
||
});
|