p0: build-time anchor aliases (web-native)
Some checks failed
CI / build-and-anchors (push) Failing after 34s
Some checks failed
CI / build-and-anchors (push) Failing after 34s
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"postbuild": "npx pagefind --site dist",
|
"postbuild": "node scripts/inject-anchor-aliases.mjs && npx pagefind --site dist",
|
||||||
"import": "node scripts/import-docx.mjs",
|
"import": "node scripts/import-docx.mjs",
|
||||||
"apply:ticket": "node scripts/apply-ticket.mjs",
|
"apply:ticket": "node scripts/apply-ticket.mjs",
|
||||||
"test": "npm run build && npm run test:anchors && node scripts/check-inline-js.mjs",
|
"test": "npm run build && npm run test:anchors && node scripts/check-inline-js.mjs",
|
||||||
|
|||||||
143
scripts/inject-anchor-aliases.mjs
Normal file
143
scripts/inject-anchor-aliases.mjs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
#!/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");
|
||||||
|
|
||||||
|
function escRe(s) {
|
||||||
|
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exists(p) {
|
||||||
|
try {
|
||||||
|
await fs.access(p);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasId(html, id) {
|
||||||
|
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "i");
|
||||||
|
return re.test(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectBeforeId(html, newId, injectHtml) {
|
||||||
|
// insère juste avant la balise qui porte id="newId"
|
||||||
|
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>>} */
|
||||||
|
const aliases = JSON.parse(raw);
|
||||||
|
|
||||||
|
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 (entries.length === 0) continue;
|
||||||
|
|
||||||
|
const rel = route.replace(/^\/+|\/+$/g, ""); // sans slash
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (hasId(html, oldId)) {
|
||||||
|
// alias déjà présent → idempotent
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasId(html, newId)) {
|
||||||
|
const msg = `⚠️ newId introuvable: ${route} old=${oldId} -> new=${newId}`;
|
||||||
|
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);
|
||||||
|
});
|
||||||
6
src/anchors/anchor-aliases.json
Normal file
6
src/anchors/anchor-aliases.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"/archicratie/prologue/": {
|
||||||
|
"p-8-e7075fe3": "p-8-0e65838d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -121,3 +121,10 @@ body[data-reading-level="2"] .level-3 { display: none; }
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.para-alias {
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
/* ajuste si header sticky : */
|
||||||
|
scroll-margin-top: var(--scroll-margin-top, 96px);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user