Compare commits
3 Commits
docs/RUNBO
...
dc2826df08
| Author | SHA1 | Date | |
|---|---|---|---|
| dc2826df08 | |||
| 3e4df18b88 | |||
| a2d1df427d |
100
astro.config.mjs
100
astro.config.mjs
@@ -10,41 +10,101 @@ import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|||||||
import rehypeDetailsSections from "./scripts/rehype-details-sections.mjs";
|
import rehypeDetailsSections from "./scripts/rehype-details-sections.mjs";
|
||||||
import rehypeParagraphIds from "./src/plugins/rehype-paragraph-ids.js";
|
import rehypeParagraphIds from "./src/plugins/rehype-paragraph-ids.js";
|
||||||
|
|
||||||
const must = (name, fn) => {
|
/**
|
||||||
if (typeof fn !== "function") {
|
* Cast minimal pour satisfaire @ts-check sans dépendre de types internes Astro/Unified.
|
||||||
throw new Error(`[astro.config] rehype plugin "${name}" is not a function (export default vs named?)`);
|
* @param {unknown} x
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
const asAny = (x) => /** @type {any} */ (x);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} node
|
||||||
|
* @param {string} cls
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function hasClass(node, cls) {
|
||||||
|
const cn = node?.properties?.className;
|
||||||
|
if (Array.isArray(cn)) return cn.includes(cls);
|
||||||
|
if (typeof cn === "string") return cn.split(/\s+/).includes(cls);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rehype plugin: retire les ids dupliqués en gardant en priorité:
|
||||||
|
* 1) span.details-anchor
|
||||||
|
* 2) h1..h6
|
||||||
|
* 3) sinon: premier rencontré
|
||||||
|
* @returns {(tree: any) => void}
|
||||||
|
*/
|
||||||
|
function rehypeDedupeIds() {
|
||||||
|
/** @param {any} tree */
|
||||||
|
return (tree) => {
|
||||||
|
/** @type {Map<string, Array<{node:any, pref:number, idx:number}>>} */
|
||||||
|
const occ = new Map();
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
/** @param {any} node */
|
||||||
|
const walk = (node) => {
|
||||||
|
if (!node || typeof node !== "object") return;
|
||||||
|
|
||||||
|
if (node.type === "element") {
|
||||||
|
const id = node.properties?.id;
|
||||||
|
if (typeof id === "string" && id) {
|
||||||
|
let pref = 2;
|
||||||
|
if (node.tagName === "span" && hasClass(node, "details-anchor")) pref = 0;
|
||||||
|
else if (/^h[1-6]$/.test(String(node.tagName || ""))) pref = 1;
|
||||||
|
|
||||||
|
const arr = occ.get(id) || [];
|
||||||
|
arr.push({ node, pref, idx: idx++ });
|
||||||
|
occ.set(id, arr);
|
||||||
}
|
}
|
||||||
return fn;
|
|
||||||
};
|
const children = node.children;
|
||||||
|
if (Array.isArray(children)) for (const c of children) walk(c);
|
||||||
|
} else if (Array.isArray(node.children)) {
|
||||||
|
for (const c of node.children) walk(c);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
walk(tree);
|
||||||
|
|
||||||
|
for (const [id, items] of occ.entries()) {
|
||||||
|
if (items.length <= 1) continue;
|
||||||
|
|
||||||
|
items.sort((a, b) => (a.pref - b.pref) || (a.idx - b.idx));
|
||||||
|
const keep = items[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < items.length; i++) {
|
||||||
|
const n = items[i].node;
|
||||||
|
if (n?.properties?.id === id) delete n.properties.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// safety: on s'assure qu'un seul garde bien l'id
|
||||||
|
if (keep?.node?.properties) keep.node.properties.id = id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: "static",
|
output: "static",
|
||||||
trailingSlash: "always",
|
trailingSlash: "always",
|
||||||
|
|
||||||
site: process.env.PUBLIC_SITE ?? "http://localhost:4321",
|
site: process.env.PUBLIC_SITE ?? "http://localhost:4321",
|
||||||
|
|
||||||
integrations: [
|
integrations: [
|
||||||
mdx(),
|
// Important: MDX hérite du pipeline markdown (ids p-… + autres plugins)
|
||||||
|
mdx({ extendMarkdownConfig: true }),
|
||||||
sitemap({
|
sitemap({
|
||||||
filter: (page) => !page.includes("/api/") && !page.endsWith("/robots.txt"),
|
filter: (page) => !page.includes("/api/") && !page.endsWith("/robots.txt"),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
// ✅ Plugins appliqués AU MDX
|
|
||||||
mdx: {
|
|
||||||
// ✅ MDX hérite déjà de markdown.rehypePlugins
|
|
||||||
// donc ici on ne met QUE le spécifique MDX
|
|
||||||
rehypePlugins: [
|
|
||||||
must("rehype-details-sections", rehypeDetailsSections),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// ✅ Plugins appliqués au Markdown non-MDX
|
|
||||||
markdown: {
|
markdown: {
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
must("rehype-slug", rehypeSlug),
|
asAny(rehypeSlug),
|
||||||
[must("rehype-autolink-headings", rehypeAutolinkHeadings), { behavior: "append" }],
|
[asAny(rehypeAutolinkHeadings), { behavior: "append" }],
|
||||||
must("rehype-paragraph-ids", rehypeParagraphIds),
|
asAny(rehypeDetailsSections),
|
||||||
|
asAny(rehypeParagraphIds),
|
||||||
|
asAny(rehypeDedupeIds),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"build:clean": "npm run clean && npm run build",
|
"build:clean": "npm run clean && npm run build",
|
||||||
|
|
||||||
"postbuild": "node scripts/inject-anchor-aliases.mjs && npx pagefind --site dist",
|
"postbuild": "node scripts/inject-anchor-aliases.mjs && node scripts/dedupe-ids-dist.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",
|
||||||
|
|||||||
134
scripts/dedupe-ids-dist.mjs
Normal file
134
scripts/dedupe-ids-dist.mjs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
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: <tag ... id="X" ...>
|
||||||
|
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<string, Array<typeof occs[number]>>} */
|
||||||
|
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
|
||||||
|
// <tag{pre} id="X"{post}> ==> <tag{pre}{post}>
|
||||||
|
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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user