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:
co-authored by
Claude Opus 5
parent
e937e1bbbb
commit
285d86a2e1
@@ -3842,3 +3842,83 @@ als der Fehler längst weg war. Auseinandergehalten hat es der `?t=`-Stempel
|
|||||||
im Stacktrace gegen den der geladenen Datei (`performance.getEntriesByType`):
|
im Stacktrace gegen den der geladenen Datei (`performance.getEntriesByType`):
|
||||||
`…961240` gegen `…049984`. Dieselbe Sorte Lehre wie D25 und D17-Nachtrag 4 —
|
`…961240` gegen `…049984`. Dieselbe Sorte Lehre wie D25 und D17-Nachtrag 4 —
|
||||||
die Meldung des Werkzeugs ist noch kein Befund.
|
die Meldung des Werkzeugs ist noch kein Befund.
|
||||||
|
|
||||||
|
## D55 — ID-Kurzschreibweise: Eingabehilfe statt Notation
|
||||||
|
Gefragt war, ob man unter `#prod-stage` einfach `#.kc` schreiben könnte, das
|
||||||
|
dann `#prod-stage.kc` bedeutet. Die Beobachtung dahinter stimmt: Die
|
||||||
|
D48-Konvention `#bereich.task` wiederholt das Präfix auf jeder Zeile, und die
|
||||||
|
Einrückung sagt dasselbe noch einmal.
|
||||||
|
|
||||||
|
**Als Notation wäre es trotzdem falsch — das zeigt der eigene Plan.** Gemessen
|
||||||
|
an `docs/examples/werkbaum.werkbaum`: 181 IDs im Baumteil, davon **50 (28 %),
|
||||||
|
deren gepunktetes Präfix gar nicht die Eltern-ID ist** — `#ed.parser` hängt
|
||||||
|
unter `#ed.live`, `#not.status` unter `#not.line`. Das ist kein Schlendrian,
|
||||||
|
sondern D48 wörtlich („dritte Stufe nur, wo es sonst kryptisch würde"): Die
|
||||||
|
Punkte benennen den **Bereich**, nicht den Pfad. Eine Kurzform, die gegen den
|
||||||
|
Elternknoten auflöst, wäre also für über ein Viertel der IDs des Dokuments
|
||||||
|
falsch, das die Konvention erfunden hat. Der Gewinn wäre klein: 802 Zeichen in
|
||||||
|
einer 40-kB-Datei, rund 2 %.
|
||||||
|
|
||||||
|
Drei weitere Kosten, jede für sich schwerer als das Tippen:
|
||||||
|
|
||||||
|
- **Der Beschreibungsteil hat keinen Baum.** Hinter `---` steht der Block-Kopf
|
||||||
|
uneingerückt und ohne Vorfahren; dort müsste weiterhin die volle ID stehen.
|
||||||
|
Eine ID, zwei Schreibweisen — je nachdem, wo man sie hinschreibt.
|
||||||
|
- **Die ID wäre keine Adresse mehr.** Heute überlebt sie das Umsortieren, und
|
||||||
|
genau darauf bauen die Abhängigkeiten `:#…`, die bewusst quer zum Baum zeigen
|
||||||
|
(D34). Mit Kurzform ändert **Einrücken** die Identität, und Verweise zeigen
|
||||||
|
still ins Leere. Dieselbe Zerbrechlichkeit, wegen der D34 den Verweis auf den
|
||||||
|
*Titel* verworfen hat.
|
||||||
|
- **Aus einer Konvention würde Grammatik.** Der Punkt ist heute ein gewöhnliches
|
||||||
|
Zeichen der ID-Zeichenmenge (SPEC §1); dass `#ed.closure` unter `#ed` hängt,
|
||||||
|
prüft niemand. Machte man ihn strukturell, wären die 50 Fälle oben
|
||||||
|
Regelverstöße.
|
||||||
|
|
||||||
|
**Entschieden (Nutzer): die Kurzform als Eingabehilfe.** Getippt wird `#.kc`,
|
||||||
|
und beim **Verlassen der Zeile** steht `#prod-stage.kc` im Text. Der
|
||||||
|
Präzedenzfall liegt im Haus: Umklappen im Diagramm schreibt seine Faltmarke
|
||||||
|
ebenso zurück (D38-Nachtrag 2). Damit bleibt die Datei eindeutig, durchsuchbar
|
||||||
|
und umsortierbar — die Ersparnis beim Tippen gibt es trotzdem, und keine der
|
||||||
|
drei Kosten oben fällt an. Dass die Auflösung den **Baum**-Vorfahren nimmt und
|
||||||
|
nicht das Bereichs-Präfix, ist dabei kein Mangel: Man sieht das Ergebnis sofort
|
||||||
|
im Text und kann es hinschreiben, wie man es will.
|
||||||
|
|
||||||
|
**Aufgelöst wird gegen den nächsten Vorfahren MIT ID**, nicht gegen den
|
||||||
|
direkten Elternknoten — der kann selbst keine haben. Findet sich keiner
|
||||||
|
(Wurzelzeile) oder trägt er selbst noch eine Kurzform, bleibt die Zeile
|
||||||
|
unangetastet: lieber `#.kc` stehen lassen als etwas Falsches hineinschreiben.
|
||||||
|
Das ist auch deshalb ungefährlich, weil `#.kc` schon heute eine **gültige** ID
|
||||||
|
ist (der Punkt gehört zur Zeichenmenge) — es geht also nichts verloren, es ist
|
||||||
|
nur nicht aufgelöst. `#..x` wird nicht angefasst: zwei Punkte haben keine
|
||||||
|
vereinbarte Bedeutung.
|
||||||
|
|
||||||
|
**Angefasst wird nur die eine Zeile, in der auch getippt wurde.** Beides ist
|
||||||
|
nötig: Wer ein fremdes Dokument bloß durchklickt, darf es nicht umgeschrieben
|
||||||
|
bekommen — es fiele sonst aus dem Nachziehen mitgelieferter Fassungen (D27)
|
||||||
|
und würde als bearbeitet geführt. Nachgemessen: Ein per Dokumentwechsel
|
||||||
|
geladener Text mit zwei Kurzformen bleibt beim Durchklicken zeichengenau
|
||||||
|
stehen.
|
||||||
|
|
||||||
|
**Die Falle beim Bauen, zum zweiten Mal dieselbe:** Der Zeilenwechsel kommt oft
|
||||||
|
aus dem `input`-Ereignis der Enter-Taste, und `execCommand` verweigert den
|
||||||
|
Dienst, wenn es **re-entrant** darin aufgerufen wird. `replaceTextUndoable`
|
||||||
|
fällt dann auf `src.value =` zurück — und das löscht die Undo-Historie
|
||||||
|
(D38-Nachtrag 2, D53). Gemessen: erstes Rückgängig ohne Wirkung, jedes weitere
|
||||||
|
`false`. Geschrieben wird deshalb einen Zug später (`setTimeout(…, 0)`), und
|
||||||
|
nur, solange der Fokus im Textfeld steht: Wer die Zeile per Klick ins Diagramm
|
||||||
|
verlässt, soll nicht zurückgerissen werden (`replaceTextUndoable` fokussiert
|
||||||
|
selbst). Danach nimmt ein Rückgängig genau die Auflösung zurück
|
||||||
|
(`#stage.kc` → `#.kc`), und der Stapel lebt weiter.
|
||||||
|
|
||||||
|
**Abhängigkeiten bleiben außen vor.** `:#.kc` wäre eine eigene Entscheidung —
|
||||||
|
relativ wozu, zum verweisenden Knoten? Das ist selten nützlich und mehrdeutig;
|
||||||
|
die Regel bleibt vorerst auf die Knoten-ID beschränkt.
|
||||||
|
|
||||||
|
**Getestet ist die Regel, nicht die Verdrahtung** — `expandShortIds()` liegt
|
||||||
|
als Text→Text-Funktion in `parser.js` neben `setFoldMark` und hat 17
|
||||||
|
Zusicherungen (Auflösen über mehrere Ebenen, Vorfahren ohne ID überspringen,
|
||||||
|
Kommentar/Beschreibungsteil/`"`-Zeilen/Abhängigkeiten unangetastet, nur das
|
||||||
|
erste `#`-Token, zeichengenaue Erhaltung von Einrückung, Zeichen, Statusbox
|
||||||
|
und Faltmarke, und dass das Ergebnis denselben Baum ergibt wie die von Hand
|
||||||
|
ausgeschriebene Fassung). Das Zusammenspiel mit dem Textfeld bleibt
|
||||||
|
Browser-Sache — die Lehre aus D54-Nachtrag 3.
|
||||||
|
|||||||
@@ -703,6 +703,21 @@ verknüpft (siehe D25):
|
|||||||
- Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im
|
- Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im
|
||||||
Druck.
|
Druck.
|
||||||
|
|
||||||
|
### Kurzschreibweise der Knoten-ID beim Tippen (`#.name`)
|
||||||
|
Eine **Eingabehilfe**, keine Notation: Wer `#.kc` unter einem Knoten mit der ID
|
||||||
|
`#prod-stage` schreibt, findet beim **Verlassen der Zeile** `#prod-stage.kc` im
|
||||||
|
Text. Aufgelöst wird gegen den nächsten Vorfahren **mit** ID; gibt es keinen
|
||||||
|
oder trägt er selbst noch eine Kurzform, bleibt die Zeile stehen (`#.kc` ist
|
||||||
|
ohnehin eine gültige ID — der Punkt gehört zur Zeichenmenge, §1). `#..x` wird
|
||||||
|
nicht angefasst.
|
||||||
|
|
||||||
|
Die Datei enthält danach immer die volle ID: Sie bleibt durchsuchbar und
|
||||||
|
überlebt das Umsortieren, worauf die Abhängigkeiten `:#…` bauen. **An der
|
||||||
|
Notation ändert sich dadurch nichts** — der Parser sieht nie eine Kurzform,
|
||||||
|
und `llms.md` (§13) bleibt unberührt. Angefasst wird nur die Zeile, in der auch
|
||||||
|
getippt wurde; Abhängigkeiten (`:#…`) und der Beschreibungsteil hinter `---`
|
||||||
|
bleiben außen vor. Siehe D55.
|
||||||
|
|
||||||
### Zeilennummern im Texteditor
|
### Zeilennummern im Texteditor
|
||||||
Links neben dem Textfeld steht ein schmaler Streifen mit den **Zeilennummern** —
|
Links neben dem Textfeld steht ein schmaler Streifen mit den **Zeilennummern** —
|
||||||
dieselben, die die Warnungen nennen („Zeile 12: …", §4). Ohne ihn muss man sie
|
dieselben, die die Warnungen nennen („Zeile 12: …", §4). Ohne ihn muss man sie
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
- [^] #ed.docs.restore: Restore a shipped document from the menu (XS)
|
- [^] #ed.docs.restore: Restore a shipped document from the menu (XS)
|
||||||
- [^] #ed.jump: Jump between diagram and text (S)
|
- [^] #ed.jump: Jump between diagram and text (S)
|
||||||
- [^] #ed.lineno: Line numbers in the text editor (XS) %% the warnings name them
|
- [^] #ed.lineno: Line numbers in the text editor (XS) %% the warnings name them
|
||||||
|
- [x] #ed.shortid: Type #.kc under #prod-stage, get #prod-stage.kc (XS) %% an input aid, not notation
|
||||||
- [^] #ed.lineno.tip: The warning is the tooltip of its number (XS) %% no hunting in the list
|
- [^] #ed.lineno.tip: The warning is the tooltip of its number (XS) %% no hunting in the list
|
||||||
- [^] #ed.fold: Collapsible subtrees (M)
|
- [^] #ed.fold: Collapsible subtrees (M)
|
||||||
- [^] #ed.fold.marks: Initial state from the text: > collapses, < re-opens (S)
|
- [^] #ed.fold.marks: Initial state from the text: > collapses, < re-opens (S)
|
||||||
@@ -492,6 +493,11 @@
|
|||||||
word for word as it stands below the diagram. Otherwise you read the number,
|
word for word as it stands below the diagram. Otherwise you read the number,
|
||||||
then hunt for it in the list.
|
then hunt for it in the list.
|
||||||
|
|
||||||
|
#ed.shortid
|
||||||
|
The dotted prefix repeats what the indentation already says. Resolved while
|
||||||
|
typing, not while parsing: the file keeps the full id, so it stays greppable
|
||||||
|
and survives re-ordering, which is what the :#... references rely on.
|
||||||
|
|
||||||
#ed.fold
|
#ed.fold
|
||||||
A large plan is unreadable unless parts of it can be put away. Folding is
|
A large plan is unreadable unless parts of it can be put away. Folding is
|
||||||
view only — warnings from folded subtrees are still reported, because they
|
view only — warnings from folded subtrees are still reported, because they
|
||||||
|
|||||||
+39
-2
@@ -1,5 +1,5 @@
|
|||||||
import './style.css';
|
import './style.css';
|
||||||
import { parse, setFoldMark } from './parser.js';
|
import { parse, setFoldMark, expandShortIds } from './parser.js';
|
||||||
import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM } from './model.js';
|
import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM } from './model.js';
|
||||||
import { esc, renderTreeHtml, TIP_RULE } from './render.js';
|
import { esc, renderTreeHtml, TIP_RULE } from './render.js';
|
||||||
import { formatWarning, warningText } from './warnings.js';
|
import { formatWarning, warningText } from './warnings.js';
|
||||||
@@ -1348,11 +1348,48 @@ function caretLineOf(){
|
|||||||
function syncCaret(){
|
function syncCaret(){
|
||||||
const line = caretLineOf();
|
const line = caretLineOf();
|
||||||
const moved = line !== caretLine;
|
const moved = line !== caretLine;
|
||||||
caretLine = line;
|
if(moved) resolveShortId(caretLine);
|
||||||
|
caretLine = caretLineOf(); /* neu lesen: das Auflösen kann den Text ändern */
|
||||||
highlightCurrentNode(moved);
|
highlightCurrentNode(moved);
|
||||||
}
|
}
|
||||||
for(const ev of ['click','keyup','input','focus']) src.addEventListener(ev, syncCaret);
|
for(const ev of ['click','keyup','input','focus']) src.addEventListener(ev, syncCaret);
|
||||||
|
|
||||||
|
/* Kurzschreibweise der ID auflösen (D55): `#.kc` wird beim **Verlassen der
|
||||||
|
Zeile** zu `#prod-stage.kc`. Eingabehilfe, keine Notation — in der Datei
|
||||||
|
steht danach die volle ID, sie bleibt also durchsuchbar und überlebt das
|
||||||
|
Umsortieren.
|
||||||
|
|
||||||
|
Angefasst wird nur die **eine** Zeile, in der auch getippt wurde. Beides ist
|
||||||
|
nötig: `#.foo` ist schon heute eine gültige ID, und wer ein fremdes Dokument
|
||||||
|
bloß durchklickt, darf es nicht umgeschrieben bekommen (und damit aus dem
|
||||||
|
Nachziehen mitgelieferter Fassungen fallen, D27). Geschrieben wird
|
||||||
|
undo-fähig — ein Griff daneben kostet ein Strg+Z (D53). */
|
||||||
|
let touchedLine = null;
|
||||||
|
src.addEventListener('input', () => { touchedLine = caretLineOf(); });
|
||||||
|
function resolveShortId(line){
|
||||||
|
if(src.readOnly || line == null || line !== touchedLine) return;
|
||||||
|
touchedLine = null;
|
||||||
|
/* **Nicht** sofort schreiben: Der Zeilenwechsel kommt oft aus dem
|
||||||
|
`input`-Ereignis der Enter-Taste, und `execCommand` verweigert den Dienst,
|
||||||
|
wenn es re-entrant darin aufgerufen wird. `replaceTextUndoable` fiele dann
|
||||||
|
auf `src.value =` zurück — und das löscht die Undo-Historie (D38-Nachtrag
|
||||||
|
2). Gemessen: erstes Rückgängig ohne Wirkung, jedes weitere `false`.
|
||||||
|
Deshalb ein Zug später, wenn das Ereignis zugestellt ist.
|
||||||
|
|
||||||
|
Nur mit Fokus im Textfeld: Wer die Zeile per Klick ins Diagramm verlässt,
|
||||||
|
soll nicht zurückgerissen werden (`replaceTextUndoable` fokussiert selbst).
|
||||||
|
Die Kurzform bleibt dann stehen — sie ist eine gültige ID, es geht nichts
|
||||||
|
verloren, und beim nächsten Bearbeiten der Zeile wird sie aufgelöst. */
|
||||||
|
setTimeout(() => {
|
||||||
|
if(document.activeElement !== src) return;
|
||||||
|
const lines = src.value.split('\n');
|
||||||
|
const neu = expandShortIds(src.value).split('\n');
|
||||||
|
if(line > lines.length || neu[line-1] === lines[line-1]) return;
|
||||||
|
lines[line-1] = neu[line-1];
|
||||||
|
replaceTextUndoable(lines.join('\n'));
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/* Gegenstück zum Alt+Klick am Knoten (D25, Nachtrag): Alt+Klick im Textfeld —
|
/* Gegenstück zum Alt+Klick am Knoten (D25, Nachtrag): Alt+Klick im Textfeld —
|
||||||
Tastatur Alt+Enter — holt den Knoten der Cursor-Zeile in die **Mitte** des
|
Tastatur Alt+Enter — holt den Knoten der Cursor-Zeile in die **Mitte** des
|
||||||
Diagramms und gibt ihm den Fokus. Der gewöhnliche Klick markiert ihn zwar
|
Diagramms und gibt ihm den Fokus. Der gewöhnliche Klick markiert ihn zwar
|
||||||
|
|||||||
@@ -36,6 +36,59 @@ export function setFoldMark(line, mark){
|
|||||||
return m[1] + m[2] + (mark ? mark + ' ' : '') + line.slice(m[0].length);
|
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
|
/* Status, die als „realisiert" zählen (XOR-Regel, SPEC §3/D35): Kosten sind
|
||||||
investiert oder mehr. Absicht (`[?]`, `[ ]`, `[!]`), Ablehnung (`[-]`) und
|
investiert oder mehr. Absicht (`[?]`, `[ ]`, `[!]`), Ablehnung (`[-]`) und
|
||||||
neutrale Knoten zählen nicht. */
|
neutrale Knoten zählen nicht. */
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { expandShortIds, parse } from '../src/parser.js';
|
||||||
|
|
||||||
|
/* Kurzschreibweise der Knoten-ID (D55): `#.kc` unter `#prod-stage` wird beim
|
||||||
|
Verlassen der Zeile zu `#prod-stage.kc`. Eingabehilfe, keine Notation — in
|
||||||
|
der Datei steht danach immer die volle ID. */
|
||||||
|
|
||||||
|
describe('expandShortIds — auflösen gegen den nächsten Vorfahren mit ID', () => {
|
||||||
|
it('löst gegen den Elternknoten auf', () => {
|
||||||
|
const text = ['#prod-stage: Stage', ' - #.kc: Keycloak'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toBe(
|
||||||
|
['#prod-stage: Stage', ' - #prod-stage.kc: Keycloak'].join('\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('löst über mehrere Ebenen jeweils gegen die nächste auf', () => {
|
||||||
|
const text = ['#a: A', ' - #.b: B', ' - #.c: C'].join('\n');
|
||||||
|
/* Die zweite Zeile ist beim Auflösen der dritten bereits `#a.b`. */
|
||||||
|
expect(expandShortIds(text)).toBe(
|
||||||
|
['#a: A', ' - #a.b: B', ' - #a.b.c: C'].join('\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Der direkte Elternknoten muss keine ID haben — dann zählt der nächste
|
||||||
|
Vorfahre, der eine hat. */
|
||||||
|
it('überspringt Vorfahren ohne ID', () => {
|
||||||
|
const text = ['#a: A', ' - Zwischenknoten ohne ID', ' - #.c: C'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toContain('#a.c: C');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nimmt mehrere Stufen auf einmal', () => {
|
||||||
|
const text = ['#a: A', ' - #.b.c: tief'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toContain('#a.b.c: tief');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt volle IDs unangetastet', () => {
|
||||||
|
const text = ['#a: A', ' - #ganz.anders: X'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toBe(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gibt denselben String zurück, wenn nichts aufzulösen ist', () => {
|
||||||
|
const text = '- [x] Nichts zu tun (M)';
|
||||||
|
expect(expandShortIds(text)).toBe(text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('expandShortIds — wo nicht aufgelöst werden kann, bleibt der Text stehen', () => {
|
||||||
|
it('lässt eine Wurzelzeile in Ruhe (kein Vorfahre)', () => {
|
||||||
|
const text = '#.kc: Keycloak';
|
||||||
|
expect(expandShortIds(text)).toBe(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('löst nicht gegen einen Vorfahren auf, der selbst noch kurz ist', () => {
|
||||||
|
const text = ['#.a: A', ' - #.b: B'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toBe(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Zwei Punkte sind keine vereinbarte Bedeutung — also nichts erfinden. */
|
||||||
|
it('rührt `#..x` nicht an', () => {
|
||||||
|
const text = ['#a: A', ' - #..x: X'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toBe(text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('expandShortIds — fasst nur an, was die ID ist', () => {
|
||||||
|
it('lässt den Beschreibungsteil hinter --- unberührt', () => {
|
||||||
|
const text = ['#a: A', ' - #.b: B', '---', '#.b', ' Text mit #.b darin'].join('\n');
|
||||||
|
const out = expandShortIds(text).split('\n');
|
||||||
|
expect(out[1]).toContain('#a.b: B');
|
||||||
|
expect(out[3]).toBe('#.b');
|
||||||
|
expect(out[4]).toBe(' Text mit #.b darin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt eine Kurzform im Kommentar stehen', () => {
|
||||||
|
const text = ['#a: A', ' - #.b: B %% später vielleicht #.c'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toBe(
|
||||||
|
['#a: A', ' - #a.b: B %% später vielleicht #.c'].join('\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fasst nur das ERSTE #-Token an (SPEC §1)', () => {
|
||||||
|
const text = ['#a: A', ' - #.b: siehe auch #.c'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toContain('#a.b: siehe auch #.c');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt eine `"`-Beschreibungszeile aus', () => {
|
||||||
|
const text = ['#a: A', ' " Zitat mit #.b', ' - #.c: C'].join('\n');
|
||||||
|
const out = expandShortIds(text).split('\n');
|
||||||
|
expect(out[1]).toBe(' " Zitat mit #.b');
|
||||||
|
expect(out[2]).toContain('#a.c: C');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('erhält Einrückung, Zeichen, Statusbox und Faltmarke zeichengenau', () => {
|
||||||
|
const text = ['#a: A', ' = [x] > #.b: B (M) @anna :#a !!!'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toBe(
|
||||||
|
['#a: A', ' = [x] > #a.b: B (M) @anna :#a !!!'].join('\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Die Abhängigkeits-Schreibweise beginnt mit `:#` und ist damit kein
|
||||||
|
alleinstehendes `#`-Token — sie bleibt außen vor (Kurzform dort wäre eine
|
||||||
|
eigene Entscheidung, D55). */
|
||||||
|
it('rührt Abhängigkeiten nicht an', () => {
|
||||||
|
const text = ['#a: A', ' - Kind :#.b'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toBe(text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('expandShortIds — das Ergebnis parst wie eine von Hand geschriebene ID', () => {
|
||||||
|
it('ergibt denselben Baum wie die ausgeschriebene Fassung', () => {
|
||||||
|
const kurz = ['#prod-stage: Stage', ' - #.kc: Keycloak'].join('\n');
|
||||||
|
const lang = ['#prod-stage: Stage', ' - #prod-stage.kc: Keycloak'].join('\n');
|
||||||
|
expect(parse(expandShortIds(kurz))).toEqual(parse(lang));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('macht aus der Kurzform eine auflösbare Abhängigkeit', () => {
|
||||||
|
const text = ['#a: A', ' - #.b: B', ' - #.c: C :#a.b'].join('\n');
|
||||||
|
const r = parse(expandShortIds(text));
|
||||||
|
expect(r.warnings).toEqual([]); /* kein unknownDep mehr */
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user