Seed from NAS prod snapshot 20260130-190531
All checks were successful
CI / build-and-anchors (push) Successful in 1m25s
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (pull_request) Successful in 1m20s

This commit is contained in:
archicratia
2026-01-31 10:51:38 +00:00
commit 60d88939b0
142 changed files with 33443 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
// scripts/rehype-details-sections.mjs
// Rehype plugin: regroupe chaque section H2 en <details>/<summary> (accordion).
// FIX: anti-récursion / anti-boucle infinie -> post-order traversal (children first, then transform)
// et on ne retraverse pas les <details> insérés.
function isElement(node) {
return node && node.type === "element" && typeof node.tagName === "string";
}
function headingLevel(node) {
if (!isElement(node)) return null;
const m = node.tagName.match(/^h([1-6])$/i);
return m ? Number(m[1]) : null;
}
function nodeText(node) {
if (!node) return "";
if (node.type === "text") return String(node.value || "");
if (isElement(node) && Array.isArray(node.children)) {
return node.children.map(nodeText).join("");
}
return "";
}
function classList(v) {
if (!v) return [];
if (Array.isArray(v)) return v.map(String);
if (typeof v === "string") return v.split(/\s+/).filter(Boolean);
return [];
}
export default function rehypeDetailsSections(options = {}) {
const {
// Par défaut : sections sur H2 uniquement
minDepth = 2,
maxDepth = 2,
openByDefault = false,
detailsClass = "details-section",
summaryClass = "details-summary",
bodyClass = "details-body",
anchorClass = "details-anchor",
} = options;
const DETAILS_SENTINEL_ATTR = "data-details-sections";
function transformChildren(parent) {
if (!parent || !Array.isArray(parent.children) || parent.children.length === 0) return;
const children = parent.children;
const out = [];
for (let i = 0; i < children.length; i++) {
const n = children[i];
const lvl = headingLevel(n);
// Wrap uniquement les headings min/max
if (lvl && lvl >= minDepth && lvl <= maxDepth) {
const heading = n;
const props = heading.properties || {};
const id = typeof props.id === "string" ? props.id : "";
const title = nodeText(heading).trim() || "Section";
// Capture jusquau prochain heading de niveau <= lvl
const bodyNodes = [];
i++;
for (; i < children.length; i++) {
const nxt = children[i];
const nlvl = headingLevel(nxt);
if (nlvl && nlvl <= lvl) {
i--; // rendre la main au for principal
break;
}
bodyNodes.push(nxt);
}
// On garde le heading dans le body mais on retire l'id
// (lancre est portée par un <span id=...> au-dessus)
const headingClone = {
...heading,
properties: { ...(heading.properties || {}) },
};
if (headingClone.properties) delete headingClone.properties.id;
const details = {
type: "element",
tagName: "details",
properties: {
[DETAILS_SENTINEL_ATTR]: "1",
className: [detailsClass],
...(openByDefault ? { open: true } : {}),
"data-section-level": String(lvl),
},
children: [
{
type: "element",
tagName: "summary",
properties: { className: [summaryClass] },
children: [
...(id ? [{
type: "element",
tagName: "span",
properties: { id, className: [anchorClass], "aria-hidden": "true" },
children: [],
}] : []),
(id
? {
type: "element",
tagName: "a",
properties: { href: `#${id}` },
children: [{ type: "text", value: title }],
}
: { type: "text", value: title }),
],
},
{
type: "element",
tagName: "div",
properties: { className: [bodyClass] },
children: [headingClone, ...bodyNodes],
},
],
};
out.push(details);
continue;
}
out.push(n);
}
parent.children = out;
}
function walk(node) {
if (!node) return;
// IMPORTANT: si cest un <details> déjà produit par nous,
// on ne descend pas dedans (sinon boucle infinie).
if (isElement(node) && node.tagName === "details") {
const props = node.properties || {};
if (props[DETAILS_SENTINEL_ATTR] === "1") return;
// Si jamais quelquun a mis une class identique manuellement
const cls = classList(props.className);
if (cls.includes(detailsClass)) return;
}
// post-order : dabord les enfants
if (node.type === "root" || isElement(node)) {
const kids = node.children || [];
for (const k of kids) walk(k);
}
// puis on transforme le parent
transformChildren(node);
}
return function transformer(tree) {
walk(tree);
};
}