feat: Knotenbeschreibungen — "-Zeilen und ----Beschreibungsteil (SPEC §1/§9, D40)
Parser: `"`-Zeilen (Leerraum-Regel, nur ohne Zerlegungszeichen) hängen am vorangehenden Knoten; hinter einem `---`-Trenner eröffnen #id-Zeilen Blöcke, deren eingerückte Zeilen der Text sind (Leerzeilen = Absätze, weitere Trenner bedeutungslos). Freitext ohne §1-Extraktion, nur %% fällt weiter weg. Warnungen: unknownDesc (ID ohne Knoten, Blocktext wird still geschluckt) und descStray je verwaister Zeile — ein versehentlicher Trenner meldet die verschluckten Knotenzeilen zeilengenau. Anzeige im Tooltip (Text zuerst, mehrzeilig) + aria-label, ”-Marke am Knoten (nicht im Export — eine Marke ohne Ziel wäre Rauschen). i18n in allen 9 Sprachen; SPEC-§11-Abschnitt in §1/§9 überführt; 16 neue Tests (tests/desc.test.js). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c4abd24ce8
commit
a42710b38f
+67
-2
@@ -27,7 +27,10 @@ const REALIZED = new Set(['arbeit', 'durchstich', 'fertig', 'prod']);
|
||||
|
||||
/* Parst den Notationstext zu { roots, warnings }.
|
||||
Jeder Knoten: {label, type:'and'|'or'|'xor', optional, fold, status, url,
|
||||
size, tags, id, deps, focus, children, line}.
|
||||
size, tags, id, deps, desc, focus, children, line}.
|
||||
`desc` (SPEC §11/D40) ist der Beschreibungstext: `"`-Zeilen unter dem
|
||||
Knoten (Kurzform) und ID-Blöcke aus dem `---`-Beschreibungsteil (Langform),
|
||||
in Dokumentreihenfolge mit Zeilenumbrüchen zusammengefügt; null ohne.
|
||||
`fold` ('>'|'<'|null, SPEC §1/D38) ist nur der ANFANGSZUSTAND der Faltung —
|
||||
den wirksamen Zustand rechnet `initialCollapsed()` in model.js.
|
||||
`deps` sind ID-Strings, keine Knoten-Referenzen — aufgelöst wird erst beim
|
||||
@@ -46,10 +49,62 @@ export function parse(text){
|
||||
const stack = [{node:virtualRoot, width:-1}];
|
||||
const warnings = [];
|
||||
const idLines = new Map(); /* Knoten-ID -> Zeile der ersten Vergabe (D36) */
|
||||
const idNodes = new Map(); /* Knoten-ID -> Knoten der ersten Vergabe */
|
||||
/* Beschreibungen (SPEC §11, D40): gesammelt je Knoten, am Ende zu `desc`
|
||||
zusammengefügt. `lastNode` trägt die Kurzform-Zuordnung („vorangehender
|
||||
Knoten"), `descTarget` den offenen Block des Beschreibungsteils; SKIP
|
||||
schluckt Blocktext unter einer unbekannten ID, ohne je Zeile zu warnen. */
|
||||
const descLines = new Map();
|
||||
const SKIP = {};
|
||||
let lastNode = null, inDesc = false, descTarget = null;
|
||||
const addDesc = (node, text) => {
|
||||
let arr = descLines.get(node);
|
||||
if(!arr) descLines.set(node, arr = []);
|
||||
if(text === '' && (!arr.length || arr[arr.length-1] === '')) return;
|
||||
arr.push(text);
|
||||
};
|
||||
|
||||
text.split('\n').forEach((raw, i) => {
|
||||
raw = raw.replace(/%%.*$/, ''); /* %%-Kommentare entfernen (Mermaid-Konvention) */
|
||||
/* Trenner `---` (SPEC §11, D40): drei oder mehr Bindestriche, umgebender
|
||||
Leerraum erlaubt — ab hier gilt der Beschreibungsteil. Es gibt keinen
|
||||
Schlusszaun; weitere Trennzeilen darin haben keine Bedeutung. */
|
||||
if(/^[ \t]*-{3,}[ \t]*$/.test(raw)){ inDesc = true; return; }
|
||||
if(inDesc){
|
||||
if(!raw.trim()){
|
||||
if(descTarget && descTarget !== SKIP) addDesc(descTarget, ''); /* Absatztrenner */
|
||||
return;
|
||||
}
|
||||
if(/^[ \t]/.test(raw)){ /* eingerückt: Blocktext */
|
||||
if(descTarget == null){ warnings.push({type:'descStray', line:i+1}); return; }
|
||||
if(descTarget !== SKIP) addDesc(descTarget, raw.trim());
|
||||
return;
|
||||
}
|
||||
const idm = raw.match(/^#([\p{L}\p{N}._-]+)\s*$/u);
|
||||
if(!idm){
|
||||
/* Uneingerückt, keine ID-Zeile — bei einem versehentlichen Trenner
|
||||
mitten im Plan melden sich die verschluckten Knotenzeilen so
|
||||
zeilengenau selbst (SPEC §11). */
|
||||
warnings.push({type:'descStray', line:i+1});
|
||||
descTarget = null;
|
||||
return;
|
||||
}
|
||||
const target = idNodes.get(idm[1]);
|
||||
if(!target){ warnings.push({type:'unknownDesc', line:i+1, id:idm[1]}); descTarget = SKIP; }
|
||||
else descTarget = target;
|
||||
return;
|
||||
}
|
||||
if(!raw.trim()) return;
|
||||
/* Kurzform `"` (SPEC §11, D40): Beschreibung des VORANGEHENDEN Knotens,
|
||||
nur mit folgendem Leerraum (Leerraum-Regel) und nur auf Zeilen ohne
|
||||
Zerlegungszeichen — `"Zitat"` und `- " Zitat" …` bleiben Labels.
|
||||
Die Einrückung der Zeile hat keine Bedeutung. */
|
||||
const ts = raw.replace(/^[ \t]*/, '');
|
||||
if(ts[0] === '"' && /[ \t]/.test(ts[1] || '')){
|
||||
if(lastNode) addDesc(lastNode, ts.slice(2).trim());
|
||||
else warnings.push({type:'descStray', line:i+1});
|
||||
return;
|
||||
}
|
||||
/* Statusbox tolerant erfassen: irgendein einzelnes Zeichen in [ ] an der
|
||||
Statusposition. Gültige Codes -> Status; unbekannte -> Warnung + neutral
|
||||
(fehlertolerant: die Zeile geht nicht verloren). */
|
||||
@@ -109,9 +164,19 @@ export function parse(text){
|
||||
while(stack.length > 1 && stack[stack.length-1].width >= width) stack.pop();
|
||||
const parent = stack[stack.length-1].node;
|
||||
|
||||
const node = {label, type, optional, fold, status, url, size, tags, id, deps, focus, children:[], line:i+1};
|
||||
const node = {label, type, optional, fold, status, url, size, tags, id, deps, desc:null, focus, children:[], line:i+1};
|
||||
parent.children.push(node);
|
||||
stack.push({node, width});
|
||||
lastNode = node;
|
||||
if(id != null && !idNodes.has(id)) idNodes.set(id, node);
|
||||
});
|
||||
|
||||
/* Beschreibungen zusammensetzen: Zeilen in Dokumentreihenfolge, Leerzeilen
|
||||
bleiben als Absatztrenner, Ränder getrimmt (SPEC §11, D40). */
|
||||
descLines.forEach((lines, node) => {
|
||||
while(lines.length && lines[lines.length-1] === '') lines.pop();
|
||||
while(lines.length && lines[0] === '') lines.shift();
|
||||
if(lines.length) node.desc = lines.join('\n');
|
||||
});
|
||||
|
||||
/* Unbekannte Abhängigkeits-IDs (SPEC §1): erst nach dem Einlesen prüfbar —
|
||||
|
||||
Reference in New Issue
Block a user