feat: Faltmarken (>/<) — Teilbäume ein-/ausklappbar (SPEC §1/§9, D38)

Parser liest die Marke zwischen Zeichen und Statusbox (Leerraum-Regel,
`- >Achtung` bleibt Label); den Anfangszustand rechnet initialCollapsed()
in model.js: `>` klappt ein, `<` (und die Fokusmarke !!!) holt seinen
Teilbaum hervor, indem die Faltung die Pfad-Ebenen hinunterwandert —
Geschwister stehen als einzelne eingeklappte Knoten da, jede Kante bleibt
eine echte. Im Diagramm: Falt-Zeichen ▾/„▸ n" vor dem Label (n = alle
verborgenen Knoten), Klick oder ←/→ am fokussierten Knoten klappt um;
Eingriffe je Label-Pfad, nur für die Sitzung, Dokumentwechsel setzt
zurück. Eingeklappte Kinder werden nicht gerendert (Export, Messungen,
Pfadlinie automatisch konsistent), ihre Warnungen aber weiter gemeldet.
Export/Druck: „▸ n" bleibt, das ▾ offener Knoten fällt weg. Legende +
a11y in allen 9 Sprachen; SPEC-§11-Abschnitt in §1/§9 überführt;
15 neue Tests (tests/fold.test.js), Snapshots aktualisiert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-22 16:47:34 +02:00
co-authored by Claude Fable 5
parent c778e3e267
commit cbe798d746
15 changed files with 480 additions and 76 deletions
+40
View File
@@ -100,6 +100,46 @@ function walkKeys(nodes, parentKey, fn){
walkKeys(n.children, key, fn);
}
}
/* Knoten -> stabiler Schlüssel (Label-Pfad) über den ganzen Baum. Dieselbe
Identität wie bei „Was ist neu?" — sie überlebt Umsortieren und Neu-Parsen;
genutzt für die interaktiven Falt-Eingriffe (D38). */
export function nodeKeys(roots){
const map = new Map();
walkKeys(roots, '', (key, n) => map.set(n, key));
return map;
}
/* ---------- Faltmarken (SPEC §1/§9, D38) ----------
Anfangszustand der Faltung aus den Textmarken: `>` klappt den Knoten ein.
`<` (und mit `rescueFocus` auch die Fokusmarke `!!!`) holt den eigenen
Teilbaum hervor, indem die Faltung die Pfad-Ebenen HINUNTERWANDERT: Jeder
eingeklappte Vorfahr wird geöffnet, seine Nicht-Pfad-Kinder werden
stattdessen eingeklappt. Sichtbar ist genau der Pfad samt Teilbaum, die
Geschwister stehen als einzelne eingeklappte Knoten da — und jede
gezeichnete Kante bleibt eine echte. Ein `>` innerhalb des hervorgeholten
Teilbaums bleibt respektiert. */
export function initialCollapsed(roots, rescueFocus){
const set = new Set();
const paths = [];
const walk = (n, path) => {
const p = path.concat(n);
if(n.fold === '>') set.add(n);
if(n.fold === '<' || (rescueFocus && n.focus)) paths.push(p);
n.children.forEach(c => walk(c, p));
};
roots.forEach(r => walk(r, []));
for(const p of paths){
for(let i = 0; i < p.length - 1; i++){
if(!set.has(p[i])) continue;
set.delete(p[i]);
for(const c of p[i].children)
if(c !== p[i+1] && c.children.length) set.add(c);
}
set.delete(p[p.length - 1]); /* der geholte Knoten selbst ist offen */
}
return set;
}
/* key -> Status-Schlüssel ('' für neutrale Knoten) über den ganzen Baum. */
export function statusByKey(roots){
const map = new Map();