feat(editor): ID-Kurzschreibweise #.kc als Eingabehilfe

Gefragt war, ob `#.kc` unter `#prod-stage` als Notation `#prod-stage.kc`
bedeuten könnte. Als Notation wäre es falsch, und das zeigt der eigene Plan:
181 IDs im Baumteil, davon 50 (28 %), deren gepunktetes Präfix gar nicht die
Eltern-ID ist (`#ed.parser` hängt unter `#ed.live`) — die Punkte benennen den
BEREICH, nicht den Pfad, genau wie D48 es vorsieht. Dazu: der
Beschreibungsteil hinter `---` hat keinen Baum (zwei Schreibweisen für eine
ID), Einrücken änderte die Identität und bräche `:#…`-Verweise, und aus einer
Konvention würde Grammatik. Gewinn wären 802 Zeichen in 40 kB.

Also als Eingabehilfe: getippt wird `#.kc`, beim Verlassen der Zeile steht
`#prod-stage.kc` im Text — derselbe Griff, mit dem das Umklappen im Diagramm
seine Faltmarke zurückschreibt (D38-Nachtrag 2). Die Datei bleibt eindeutig,
durchsuchbar und umsortierbar; llms.md ist unberührt, weil sich an der
Notation nichts ändert.

- parser.js: `expandShortIds(text)` neben `setFoldMark`, 17 Tests.
- app.js: löst beim Zeilenwechsel auf, aber nur in der Zeile, in der auch
  getippt wurde — ein bloß durchgeklicktes fremdes Dokument bleibt
  zeichengenau stehen (sonst fiele es aus dem Nachziehen, D27).

Falle, zum zweiten Mal dieselbe: `execCommand` verweigert re-entrant im
`input`-Ereignis den Dienst, `replaceTextUndoable` fällt dann auf
`src.value =` zurück und löscht die Undo-Historie. Gemessen: erstes
Rückgängig ohne Wirkung, jedes weitere `false`. Jetzt einen Zug später
(`setTimeout 0`) und nur mit Fokus im Textfeld; danach nimmt ein Rückgängig
genau die Auflösung zurück.

SPEC §9 + D55; Plan: #ed.shortid [x] (159 Knoten, 0 Warnungen).
288 Tests grün (271+17).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 11:47:16 +02:00
co-authored by Claude Opus 5
parent e937e1bbbb
commit 285d86a2e1
6 changed files with 310 additions and 2 deletions
+53
View File
@@ -36,6 +36,59 @@ export function setFoldMark(line, mark){
return m[1] + m[2] + (mark ? mark + ' ' : '') + line.slice(m[0].length);
}
const RE_LINE = /^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([^\]])\]\s*)?(?:([><])(?=[ \t])\s*)?(.*)$/;
const RE_ID_TOKEN = /(^|\s)#([\p{L}\p{N}._-]+)/u;
/* Kurzschreibweise der Knoten-ID auflösen: `#.kc` unter `#prod-stage` wird zu
`#prod-stage.kc` (D55). Das ist eine **Eingabehilfe**, keine Notation — die
Datei enthält am Ende immer die volle ID. Deshalb steht sie hier als
Text→Text-Funktion neben `setFoldMark`: Der Editor ruft sie beim Verlassen
der Zeile auf und schreibt das Ergebnis zurück, wie das Umklappen im
Diagramm seine Faltmarke zurückschreibt (D38-Nachtrag 2).
Aufgelöst wird gegen den **nächsten Vorfahren mit ID** — nicht zwingend den
direkten Elternknoten, der kann selbst ohne ID sein. Findet sich keiner
(Wurzelzeile) oder trägt er selbst noch eine Kurzform, bleibt die Zeile
unangetastet: Lieber `#.kc` stehen lassen, als etwas Falsches hineinschreiben.
Der Beschreibungsteil hinter `---` hat keinen Baum und wird nicht angefasst. */
export function expandShortIds(text){
const lines = text.split('\n');
const stack = []; /* {width, id} — auch Knoten OHNE ID stehen drin */
let changed = false;
for(let i = 0; i < lines.length; i++){
const raw = lines[i];
if(/^\s*-{3,}\s*$/.test(raw)) break;
const k = raw.indexOf('%%');
const head = k === -1 ? raw : raw.slice(0, k);
const tail = k === -1 ? '' : raw.slice(k);
const m = head.match(RE_LINE);
if(!m) continue;
const body = m[6];
if(!body.trim()) continue; /* leer oder nur Kommentar */
if(/^"(\s|$)/.test(body)) continue; /* Beschreibungszeile, kein Knoten */
const width = m[1].replace(/\t/g, ' ').length;
while(stack.length && stack[stack.length - 1].width >= width) stack.pop();
const t = body.match(RE_ID_TOKEN);
let id = t ? t[2] : null;
/* `.kc` ja, `..kc` nein — zwei Punkte sind keine vereinbarte Bedeutung. */
if(id && /^\.[^.]/.test(id)){
let anc = null;
for(let j = stack.length - 1; j >= 0; j--){
if(stack[j].id && stack[j].id[0] !== '.'){ anc = stack[j].id; break; }
}
if(anc){
id = anc + id;
lines[i] = head.slice(0, head.length - body.length)
+ body.replace(RE_ID_TOKEN, (s, pre) => pre + '#' + id)
+ tail;
changed = true;
}
}
stack.push({width, id});
}
return changed ? lines.join('\n') : text;
}
/* Status, die als „realisiert" zählen (XOR-Regel, SPEC §3/D35): Kosten sind
investiert oder mehr. Absicht (`[?]`, `[ ]`, `[!]`), Ablehnung (`[-]`) und
neutrale Knoten zählen nicht. */