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
+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`))