feat: Umklappen im Diagramm schreibt die Faltmarke in den Text zurück

Erster Schritt der Schreibrichtung Diagramm -> Text. Damit ist der Text auch für
die Faltung die eine Quelle der Wahrheit (D14): Was man sieht, steht
geschrieben, und ein Neuladen stellt es wieder her. Kehrt die bisherige
Festlegung aus D34/D38 um („die Marken bestimmen nur den Anfangszustand").

Verfahren: minimal patchen, dann nachrechnen. Die Ableitung Text -> Zustand ist
nicht umkehrbar — mehrere Markensätze ergeben denselben Zustand, und `<` faltet
Knoten, die gar keine Marke tragen. Statt zu invertieren wird der Kandidat
befragt: `>` an die eine Zeile, initialCollapsed() darauf, mit dem Soll
vergleichen. Passt es, bleibt alles andere unangetastet — so überleben von Hand
gesetzte `<`. Passt es nicht, werden alle Marken neu gesetzt (das `<` löst sich
dabei auf). Passt auch das nicht (ein `!!!` holt seinen Knoten immer wieder
hervor), wird gar nicht geschrieben: Ein Text, der etwas anderes sagt als das
Bild, wäre schlimmer als eine fehlende Marke. initialCollapsed() bleibt die
einzige Stelle, die die Marken versteht.

Undo-fähig über execCommand('insertText') — gemessen: value= UND setRangeText
machen Strg+Z wirkungslos, wer tippt und dann faltet käme an sein Getipptes
nicht mehr heran. Falten ist damit ein eigener Undo-Schritt (geprüft: einmal
Strg+Z nimmt die Faltung, ein zweites das Getippte).

Zwei gemessene Fallen: Ein Textfeld mit display:none-Vorfahr nimmt kein
execCommand an (liefert false, obwohl activeElement es meldet) — auf Mobil ist
der Editor beim Falten genau das, deshalb schaltet ihn `writing-fold` für die
Dauer des synchronen Schreibens aus dem Bild heraus sichtbar. Und der nötige
Fokus zöge dort die Tastatur hoch, dagegen inputmode="none" wie beim Sprung
(D25); danach geht der Fokus an den Knoten zurück.

Bei schreibgeschütztem Text (Pad, D31) wird nicht geschrieben — dort trägt
weiter die Sitzungs-Überlagerung. Schreibmarke und Scrollstand bleiben erhalten.

Im Browser geprüft: `<` bleibt stehen, wenn es noch stimmt, und löst sich auf,
wenn nicht; Falten bei verborgenem Editor auf Mobil; Pad-Fall sitzungsweise;
Hin- und Herklappen führt zeichengenau zum Ausgangstext zurück.

7 neue Tests für setFoldMark (215 gesamt). SPEC §1/§9, D38 (Nachtrag 2) und
frontend/CLAUDE.md nachgezogen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-23 09:25:32 +02:00
co-authored by Claude Fable 5
parent 38104a4c5d
commit 321bdc9dfa
7 changed files with 286 additions and 14 deletions
+59
View File
@@ -2232,6 +2232,65 @@ per CSS zu verstecken). Die Kennzeichnung „▸ n“ ist Teil des Knotentexts u
wandert von selbst in den SVG-Export; das ▾ offener Knoten wird dort und im
Druck entfernt — es ist Bedienelement, keine Aussage über den Plan.
**Nachtrag 2 — Umklappen im Diagramm schreibt jetzt in den Text.** Damit wird
die oben getroffene Festlegung („der Eingriff gilt für die Sitzung; er wird
nicht gespeichert — die dauerhafte Aussage steht im Text") **umgekehrt**, und
mit ihr die D34-Formulierung, die Marken bestimmten nur den Anfangszustand.
Anlass ist der Wunsch nach bidirektionaler Interaktion zwischen Diagramm und
Text; der Faltzustand ist dafür der richtige erste Fall: verlustfrei umkehrbar,
ohne inhaltliche Aussage, und die Notation dafür gibt es schon.
Die Entscheidungen im Einzelnen, alle vom Nutzer getroffen:
- **Bei jedem Umschalten**, nicht auf Ansage. Der direkteste Zusammenhang
zwischen Bild und Text; der Preis (unten) wurde bewusst in Kauf genommen.
- **`<` bleibt erhalten, wo es noch stimmt.** Nicht auf reine `>`
normalisieren: Handgeschriebene Marken sollen stehen bleiben.
- **Geschrieben wird, wo der Text beschreibbar ist** — auch bei `?sourceUrl=`,
wo es wie jede lokale Änderung bis zum nächsten Laden hält (D23). Nur beim
Pad bleibt es sitzungsweise, weil das Textfeld dort schreibgeschützt ist
(D31).
- **Falten gilt als Änderung.** Ein mitgeliefertes Dokument wird dadurch als
bearbeitet geführt, bekommt also keine neuen Fassungen mehr und zeigt
„Original wiederherstellen" (D27). Bewusst keine Sonderregel: Wer faltet,
ändert den Text, und der Text ist der Vertrag (D14).
- **Undo-fähig geschrieben.** Nachgemessen: `value =` **und** `setRangeText`
machen Strg+Z wirkungslos — wer tippt und dann faltet, käme an sein
Getipptes nicht mehr heran. Nur `document.execCommand('insertText')` erhält
die Historie; die API gilt als veraltet, funktioniert aber überall. Falten
ist damit ein eigener Undo-Schritt (geprüft: Strg+Z nimmt die Faltung
zurück, ein zweites das Getippte).
**Das Verfahren: minimal patchen, dann nachrechnen.** Die Ableitung Text →
Zustand ist **nicht umkehrbar** — mehrere Markensätze ergeben denselben
Zustand, und `<` erzeugt Faltungen an Knoten, die gar keine Marke tragen
(oben). Statt sie zu invertieren, wird der Kandidat schlicht **befragt**:
`>` an der einen Zeile setzen oder entfernen, `initialCollapsed()` darauf
laufen lassen, mit dem Sollzustand vergleichen. Stimmt es, bleibt alles andere
unangetastet — das ist der `<`-Erhalt. Stimmt es nicht, werden alle Marken neu
gesetzt und erneut geprüft. Stimmt auch das nicht — etwa weil ein `!!!` seinen
Knoten immer wieder hervorholt und „eingeklappt" dort gar nicht ausdrückbar
ist —, wird **nicht geschrieben** und die Sitzungs-Überlagerung bleibt stehen.
So kann nie ein Text entstehen, der etwas anderes sagt als das Bild.
`initialCollapsed()` bleibt dadurch die einzige Stelle, die die Bedeutung der
Marken kennt.
**Zwei Fallen, beide gemessen statt vermutet.** Erstens: Auf kleinem
Bildschirm ist der Editor `display:none`, wenn das Diagramm vorn ist — und
`execCommand` tut dann **nichts**, es liefert `false`, obwohl `activeElement`
das Textfeld meldet. Für die Dauer des synchronen Schreibens wird der Editor
deshalb absolut positioniert aus dem Bild geschoben sichtbar geschaltet
(Klasse `writing-fold`, derselbe Griff wie `exporting` im Grafikexport);
gezeichnet wird davon nichts. Zweitens: `execCommand` braucht den Fokus im
Textfeld, und der zöge auf dem Telefon die Bildschirmtastatur hoch — dagegen
`inputmode="none"` wie beim Sprung (D25), das der erste echte Tipp ins Feld
wieder aufhebt. Anschließend geht der Fokus an den Knoten zurück.
Schreibmarke und Scrollstand werden gesichert und zurückgesetzt; verschoben
wird nur, was **hinter** der Änderung lag. Gelingt das Schreiben, werden die
Sitzungs-Überlagerungen geleert — sonst könnten sie den Text maskieren, der
jetzt die Wahrheit ist.
**Nachtrag — der eingeklappte Knoten vertritt seinen Teilbaum auch auf dem
günstigsten Pfad.** Oben steht, ein eingeklappter Zweig dürfe Pfadknoten
verbergen, „die Inversion an den sichtbaren Knoten bleibt richtig". Das war zu
+20 -6
View File
@@ -34,9 +34,10 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit):
- `>` heißt: der Teilbaum dieses Knotens ist beim Öffnen **eingeklappt**.
- `<` innerhalb eines eingeklappten Bereichs holt den **eigenen Teilbaum**
gezielt wieder hervor (Mechanik: §9).
- Die Marken bestimmen nur den **Anfangszustand** — im Diagramm wird danach
unabhängig vom Text gefaltet (§9). Sie sagen nichts über Fortschritt (§4)
oder Notwendigkeit (§3) und ändern weder Kosten noch Warnungen.
- Die Marken beschreiben den **Faltzustand des Dokuments**: Beim Öffnen stellen
sie ihn her, und Umklappen im Diagramm schreibt sie zurück (§9) — Text und
Bild sagen dasselbe. Sie sagen nichts über Fortschritt (§4) oder
Notwendigkeit (§3) und ändern weder Kosten noch Warnungen.
**Knoten-ID `#name`** — benennt einen Knoten im **ganzen Dokument** eindeutig;
sie ist die Adresse für Abhängigkeiten und Beschreibungsblöcke (§11).
@@ -665,9 +666,22 @@ fokussierten Knoten (WAI-ARIA-Baum-Idiom).
hervorgeholten Teilbaums bleibt respektiert.
- Auch ein **`!!!`-markierter Knoten** (§1) holt sich auf diese Weise hervor —
ein Zeigefinger auf etwas Unsichtbares zeigte ins Leere.
- Der interaktive Eingriff gilt **je Knoten** (Identität = Label-Pfad, wie bei
„Was ist neu?“) und **für die Sitzung**; er wird nicht gespeichert — die
dauerhafte Aussage steht im Text (D34). Ein Dokumentwechsel setzt zurück.
- **Umklappen im Diagramm schreibt die Marke in den Text zurück.** Damit ist
der Text auch für die Faltung die eine Quelle der Wahrheit: Was du siehst,
steht geschrieben, und ein Neuladen stellt es wieder her. Die Änderung ist
eine gewöhnliche Textänderung — sie lässt sich mit **Rückgängig** zurücknehmen
und macht ein mitgeliefertes Dokument zu einem bearbeiteten (§9, D27).
Geschrieben wird **minimal**: Nur die Zeile des umgeklappten Knotens wird
angefasst, solange die übrigen Marken den Zustand noch richtig beschreiben —
ein von Hand gesetztes `<` bleibt also stehen. Trifft es nicht mehr zu, werden
alle Marken neu gesetzt und das `<` dabei aufgelöst.
- Wo der Text **nicht beschreibbar** ist — bei einem Pad-Dokument (§9) —, gilt
der Eingriff **je Knoten** (Identität = Label-Pfad, wie bei „Was ist neu?“)
und **nur für die Sitzung**; ein Dokumentwechsel setzt ihn zurück. Dasselbe
gilt für einen Zustand, der sich in Marken gar nicht ausdrücken lässt (etwa
weil eine Fokusmarke `!!!` ihren Knoten immer wieder hervorholt): Dann wird
lieber nichts geschrieben, als einen Text zu hinterlassen, der etwas anderes
sagt als das Bild.
- Faltung ist **reine Ansicht**: Warnungen aus eingeklappten Teilbäumen werden
weiter gemeldet (sie gelten dem Text), und der günstigste Pfad rechnet
unverändert über den ganzen Baum.
+17 -1
View File
@@ -311,7 +311,23 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
Pfad-Mitgliedschaft (ein per `:#…` gezogenes Ziel unter einem `+`-Knoten):
Dann ist er der einzige sichtbare Griff darauf und zählt als `cheap`.
`extraCls()` muss dieselbe Eingeklappt-Bedingung bilden wie `itemHtml`
(nur mit sichtbaren Kindern). Umklappen: Klick aufs
(nur mit sichtbaren Kindern).
**Umklappen schreibt in den TEXT zurück** (`writeFoldToText`, D38-Nachtrag 2):
Die Ableitung Text → Zustand ist nicht umkehrbar (mehrere Markensätze ergeben
denselben Zustand; `<` faltet Knoten ohne eigene Marke), deshalb **minimal
patchen und nachrechnen** — `setFoldMark()` auf die eine Zeile, dann
`initialCollapsed()` auf dem Kandidaten gegen den Soll vergleichen; erst bei
Abweichung alle Marken neu setzen; passt auch das nicht (z. B. `!!!` holt den
Knoten immer hervor), gar nicht schreiben. Nie eine eigene Umkehrfunktion
bauen — `initialCollapsed()` bleibt die einzige Stelle, die die Marken
versteht. Geschrieben wird mit `execCommand('insertText')`: `value =` und
`setRangeText` zerstören die Undo-Historie (gemessen). Zwei Fallen: Ein
Textfeld mit `display:none`-Vorfahr nimmt **kein** `execCommand` an (Klasse
`writing-fold` schaltet es kurz sichtbar), und der nötige Fokus zieht auf
Mobil die Tastatur hoch (`keyboardOnJump(true)`). Bei `src.readOnly` (Pad,
D31) wird nicht geschrieben, dort trägt weiter `foldOverrides` — die
Überlagerungen werden nach erfolgreichem Schreiben geleert, sonst maskieren
sie den Text. Umklappen: Klick aufs
`.fold`-Zeichen (preventDefault — es sitzt bei Link-Knoten im `<a>`) oder
←/→ am fokussierten Knoten; nach `render()` den Fokus per `data-line`
wiederherstellen. Export/Druck: „▸ n" bleibt, das ▾ offener Knoten fällt weg
+127 -6
View File
@@ -1,5 +1,5 @@
import './style.css';
import { parse } from './parser.js';
import { parse, setFoldMark } from './parser.js';
import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus } from './model.js';
import { esc, renderTreeHtml } from './render.js';
import { formatWarning } from './warnings.js';
@@ -856,15 +856,136 @@ function nodeFromEvent(e){
return el && out.contains(el) ? el : null;
}
/* Faltung umklappen (SPEC §9, D38). Der Eingriff überlagert den Anfangszustand
aus dem Text; nach dem Neubau bekommt derselbe Knoten den Fokus zurück,
/* ---------- Faltung zurück in den Text (D38-Nachtrag 2) ----------
Umklappen im Diagramm ändert den Notationstext. Der Text ist damit auch für
die Faltung die eine Quelle der Wahrheit (D14) die Sitzungs-Überlagerung
`foldOverrides` bleibt nur für Dokumente, in die nicht geschrieben werden
kann (Pad, schreibgeschützt nach D31). */
/* Der Sollzustand als Menge von Label-Pfad-Schlüsseln: heutiger Anfangszustand
aus dem Text, überlagert von den Eingriffen dieselbe Rechnung wie in
render(), damit beide nie auseinanderlaufen. */
function desiredFoldKeys(roots){
const initFold = initialCollapsed(roots, true);
const keys = nodeKeys(roots);
const want = new Set();
keys.forEach((key, n) => {
const ov = foldOverrides.get(key);
if((ov !== undefined ? ov : initFold.has(n)) && n.children.length > 0) want.add(key);
});
return want;
}
/* Erzeugt ein Text ZWEIMAL denselben Faltzustand? Statt die Ableitung
Text -> Zustand umzukehren (sie ist nicht eindeutig: mehrere Markensätze
ergeben denselben Zustand), wird der Kandidat schlicht nachgerechnet.
`initialCollapsed()` bleibt so die einzige Wahrheit über die Bedeutung der
Marken die Umkehrung muss sie nicht kennen, nur befragen. */
function foldStateMatches(txt, want){
const r = parse(txt).roots;
const keys = nodeKeys(r);
const got = new Set();
initialCollapsed(r, true).forEach(n => { if(n.children.length > 0) got.add(keys.get(n)); });
if(got.size !== want.size) return false;
for(const k of want) if(!got.has(k)) return false;
return true;
}
/* Schreibt den Text undo-fähig. `execCommand` ist die EINZIGE Art, ein Textfeld
programmatisch zu ändern, ohne dessen Rückgängig-Historie zu zerstören
(nachgemessen: `value =` und `setRangeText` machen Strg+Z wirkungslos). Sie
ersetzt die Auswahl geändert wird deshalb nur das wirklich abweichende
Stück zwischen gemeinsamem Anfang und Ende. */
function replaceTextUndoable(neu){
const alt = src.value;
if(alt === neu) return true;
let s = 0;
while(s < alt.length && s < neu.length && alt[s] === neu[s]) s++;
let e = 0;
while(e < alt.length - s && e < neu.length - s &&
alt[alt.length-1-e] === neu[neu.length-1-e]) e++;
const von = s, bis = alt.length - e, ein = neu.slice(s, neu.length - e);
const cs = src.selectionStart, ce = src.selectionEnd, top = src.scrollTop;
/* Der Fokus muss ins Textfeld sonst greift execCommand nicht. Auf dem
Telefon zöge das die Bildschirmtastatur hoch; `inputmode="none"` hält sie
unten (derselbe Griff wie beim Sprung, D25), und der erste echte Tipp ins
Feld hebt die Sperre wieder auf. */
keyboardOnJump(true);
src.focus({preventScroll: true});
src.setSelectionRange(von, bis);
let ok = false;
try{ ok = document.execCommand('insertText', false, ein); }catch(_){}
if(!ok){
/* Rückfall: Der richtige Zustand geht vor der Undo-Historie (D14). */
src.value = neu;
src.dispatchEvent(new Event('input', {bubbles: true}));
}
/* Schreibmarke und Scrollstand zurück sonst risse das Falten den Nutzer
aus seiner Textstelle. Nur was HINTER der Änderung lag, verschiebt sich. */
const d = ein.length - (bis - von);
const fix = p => p >= bis ? p + d : Math.min(p, von + ein.length);
src.setSelectionRange(fix(cs), fix(ce));
src.scrollTop = top;
return true;
}
/* Auf kleinem Bildschirm ist der Editor `display:none`, wenn das Diagramm vorn
ist und dann tut `execCommand` NICHTS (gemessen: liefert `false`, obwohl
`activeElement` das Feld meldet). Für die Dauer des synchronen Schreibens
wird er deshalb aus dem Fluss heraus sichtbar geschaltet; gezeichnet wird
davon nichts, wie bei `exporting` im Grafikexport. */
function withEditorWritable(fn){
const versteckt = editorPanel.offsetParent === null;
if(versteckt) document.body.classList.add('writing-fold');
try{ return fn(); }
finally{ if(versteckt) document.body.classList.remove('writing-fold'); }
}
/* Schreibt den Faltzustand in den Text. Liefert false, wenn nicht geschrieben
werden konnte dann bleibt die Sitzungs-Überlagerung stehen. */
function writeFoldToText(line, collapsed){
if(src.readOnly) return false; /* Pad-Dokument (D31) */
const roots = parse(src.value).roots;
if(!roots.length) return false;
const want = desiredFoldKeys(roots);
const zeilen = src.value.split('\n');
/* 1) Minimal: nur die umgeklappte Zeile anfassen. So bleiben von Hand
gesetzte `<` stehen, solange sie den Zustand noch richtig beschreiben. */
const klein = zeilen.slice();
klein[line-1] = setFoldMark(klein[line-1], collapsed ? '>' : null);
let neu = klein.join('\n');
if(!foldStateMatches(neu, want)){
/* 2) Vollständig: alle Marken neu setzen. Nötig, wenn ein `<` den Zustand
nicht mehr trifft dann wird es hier aufgelöst. */
const keys = nodeKeys(roots);
const ganz = zeilen.slice();
keys.forEach((key, n) => {
ganz[n.line-1] = setFoldMark(ganz[n.line-1], want.has(key) ? '>' : null);
});
neu = ganz.join('\n');
/* 3) Nicht ausdrückbar etwa weil ein `!!!` im Zweig seinen Knoten immer
wieder hervorholt (SPEC §9). Dann NICHT schreiben: ein Text, der
etwas anderes sagt als das Bild, wäre schlimmer als keine Marke. */
if(!foldStateMatches(neu, want)) return false;
}
return withEditorWritable(() => replaceTextUndoable(neu));
}
/* Faltung umklappen (SPEC §9, D38). Gelingt das Zurückschreiben, ist der Text
der Zustand die Überlagerungen werden dann geleert, damit sie ihn nicht
maskieren können. Nach dem Neubau bekommt derselbe Knoten den Fokus zurück,
sonst risse die Tastaturbedienung ab (das alte Element ist weg). */
function toggleFold(el){
const st = foldByLine.get(+el.dataset.line);
const line = +el.dataset.line;
const st = foldByLine.get(line);
if(!st || !st.canFold) return;
foldOverrides.set(st.key, !st.collapsed);
render();
const again = out.querySelector('.node[data-line="' + el.dataset.line + '"]');
/* Das Schreiben löst per `input`-Ereignis schon ein render() aus. */
if(writeFoldToText(line, !st.collapsed)) foldOverrides.clear();
else render();
const again = out.querySelector('.node[data-line="' + line + '"]');
if(again) again.focus({preventScroll: true});
}
+12
View File
@@ -20,6 +20,18 @@ export const STATUS_BY_CODE = {
'!': {code:'!', key:'highrisk', name:'High Risk Aufwand unklar'}
};
/* Setzt (`'>'`) oder entfernt (`null`) die Faltmarke einer Zeile die Umkehrung
der Extraktion aus §1, gebraucht fürs Zurückschreiben aus dem Diagramm
(D38-Nachtrag). Angefasst wird NUR die Marke samt ihrem Leerraum: Einrückung,
Zerlegungszeichen, Statusbox und Label bleiben zeichengenau stehen, auch bei
ungewöhnlicher Spaltung wie `- [x] X`. Wurzelzeilen (ohne Zeichen) bekommen
die Marke am Zeilenanfang, wie SPEC §1 es verlangt. */
export function setFoldMark(line, mark){
const m = line.match(/^([ \t]*(?:[-|+]|=(?=[ \t]))?[ \t]*)((?:[><](?=[ \t])[ \t]*)?)/);
const head = m[1], alt = m[2];
return head + (mark ? mark + ' ' : '') + line.slice(head.length + alt.length);
}
/* Status, die als realisiert" zählen (XOR-Regel, SPEC §3/D35): Kosten sind
investiert oder mehr. Absicht (`[?]`, `[ ]`, `[!]`), Ablehnung (`[-]`) und
neutrale Knoten zählen nicht. */
+12
View File
@@ -399,6 +399,18 @@
.dltrigger{display:none}
.legendbtn{display:none} /* nur auf kleinem Bildschirm sichtbar */
.panetoggle{display:none} /* ebenso: dort gibt es nur einen Bereich */
/* Zurückschreiben der Faltung (D38-Nachtrag 2): Für die Dauer des SYNCHRONEN
Schreibens braucht das Textfeld einen Kasten ohne ihn tut `execCommand`
nichts (gemessen: liefert `false`, obwohl `activeElement` das Feld meldet).
Auf kleinem Bildschirm ist der Editor `display:none`, wenn das Diagramm
vorn ist. Absolut aus dem Fluss genommen und aus dem Bild geschoben, damit
der sichtbare Bereich seine Größe behält; gezeichnet wird davon nichts
derselbe Griff wie `exporting` im Grafikexport. */
body.writing-fold .panel.editor{
display:flex!important;
position:absolute!important;left:-10000px!important;top:0!important;
width:360px!important;height:400px!important;
}
/* Dokument-Wähler: der Name des aktiven Dokuments in der Editor-Titelzeile ist
zugleich der Auslöser eines Dropdowns zum Wechseln/Anlegen/Umbenennen/Löschen
(localStorage-Dokumente, noch kein Backend). */
+39 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { parse } from '../src/parser.js';
import { parse, setFoldMark } from '../src/parser.js';
import { initialCollapsed, computeCheapSet } from '../src/model.js';
import { renderTreeHtml } from '../src/render.js';
@@ -43,6 +43,44 @@ describe('Parser — Faltmarke zwischen Zeichen und Statusbox', () => {
});
});
/* Umkehrung der Marken-Extraktion fürs Zurückschreiben aus dem Diagramm
(SPEC §1, D38-Nachtrag 2): Angefasst wird nur die Marke samt Leerraum. */
describe('setFoldMark — Faltmarke setzen und entfernen', () => {
it('setzt `>` hinter das Zerlegungszeichen', () => {
expect(setFoldMark(' - [x] Concept (M)', '>')).toBe(' - > [x] Concept (M)');
});
it('entfernt eine vorhandene Marke', () => {
expect(setFoldMark(' - > [x] Concept (M)', null)).toBe(' - [x] Concept (M)');
});
it('ersetzt `<` durch `>`', () => {
expect(setFoldMark(' - < [ ] B1', '>')).toBe(' - > [ ] B1');
});
it('setzt die Marke bei Wurzelzeilen an den Zeilenanfang', () => {
expect(setFoldMark('[~] Wurzel', '>')).toBe('> [~] Wurzel');
expect(setFoldMark('> [~] Wurzel', null)).toBe('[~] Wurzel');
});
it('lässt ungewöhnliche Spaltung stehen', () => {
expect(setFoldMark(' - [x] X', '>')).toBe(' - > [x] X');
expect(setFoldMark('\t= [ ] Cloud', '>')).toBe('\t= > [ ] Cloud');
});
it('fasst ein `>` im Label nicht an (Leerraum-Regel)', () => {
/* `- >Achtung` ist ein Label, keine Marke — die neue Marke kommt davor. */
expect(setFoldMark(' - >Achtung', '>')).toBe(' - > >Achtung');
expect(setFoldMark(' - >Achtung', null)).toBe(' - >Achtung');
});
it('ist verlustfrei umkehrbar', () => {
for(const l of ['[ ] W', ' - [x] A (M) @anna', ' | [?] B', ' + [-] C']){
expect(setFoldMark(setFoldMark(l, '>'), null)).toBe(l);
}
});
});
describe('initialCollapsed — `>` klappt ein, `<` wandert die Faltung hinunter', () => {
it('klappt `>`-Knoten ein', () => {
expect(collapsedLabels(`[ ] W\n - > [ ] A\n - [ ] A1\n - [ ] B`))