diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 6c6c092..1eee53b 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -1958,6 +1958,10 @@ Kollision, wegen der D29 `*` verwarf — Markdown-Betonung). Wurzelknoten haben kein Gate — dort steht die Marke am Zeilenanfang; Wurzeln sind selten, und die Regel „hinter dem Zeichen“ bleibt trotzdem einheitlich formulierbar. +*(Der Satz „die Statusbox trägt keine Ausrichtungs-Information“ ist falsch — +sie trägt sie sehr wohl. Korrigiert in Nachtrag 2 am Ende dieses Eintrags; +die Stellung ist jetzt hinter der Statusbox.)* + **Export und Druck folgen der sichtbar eingeklappten Struktur — mit Kennzeichnung.** Der Präzedenzfall steht in §9: „Es wird genau die sichtbare Struktur exportiert (der ‚verworfene einblenden‘-Filter wirkt auch hier).“ @@ -2008,6 +2012,65 @@ Zwei bewusste Verhaltensänderungen, beide dokumentiert (§11): `---` ergab bisher einen Knoten mit Label `--`, und eine zeichenlose Zeile, die mit `" ` beginnt, war bisher ein Wurzelknoten mit `"`-Label. +**Nachtrag 2 — die Faltmarke rückt hinter die Statusbox: `- [x] > Backend`.** +Der erste Nachtrag oben stellte sie zwischen Zeichen und Box und begründete +das damit, dass sich dabei „nur die Statusbox verschiebt, und die trägt keine +Ausrichtungs-Information“. **Dieser Satz ist falsch**, und der Fehler war im +Text nicht zu sehen, sondern erst im gefalteten Plan: Die Marke ist zwei +Zeichen breit — genau eine Einrückungsstufe. Die Box einer gefalteten Zeile +rückt dadurch exakt in die Spalte der Boxen ihrer **eigenen Kinder**: + +``` + - > [ ] erster Schritt %% Box bei Spalte 6 + - [ ] Schritt 1a %% Box bei Spalte 6 ← dieselbe Spalte +``` + +Die Box-Spalte ist damit sehr wohl Ausrichtungs-Information: Sie ist die +zweite Spalte, an der das Auge die Ebene abliest, und der Fehler tritt +ausgerechnet dort auf, wo man ohnehin schon weniger sieht — an einem +eingeklappten Knoten, dessen Kinder gerade verborgen sind. Hinter der Box +steht die Marke dagegen vor dem **Label**, und Labels sind ohnehin ausgefranst +(unterschiedlich lange IDs, Größen, Tags) — dort kostet eine Verschiebung +nichts: + +``` + - [ ] > erster Schritt + - [ ] Schritt 1a +``` + +**Die Begründung des ersten Nachtrags bleibt im Übrigen gültig:** Die +Gate-Spalte ist der wichtigste Anker, `>` davor (`> - [x] …`) bliebe falsch, +und die Markdown-Blockquote-Kollision am Zeilenanfang ebenfalls. Die neue +Stellung greift keines der beiden an — sie verschiebt die Marke nur um eine +Position weiter nach rechts. + +**Nebengewinn: Die Regel wird einfacher, nicht komplizierter.** Bisher hieß +sie „zwischen Zeichen und Statusbox, bei Wurzelknoten am Zeilenanfang“ — mit +einer Ausnahme für Wurzeln. Jetzt heißt sie **„unmittelbar vor dem Label“**, +und das gilt ohne Ausnahme: Fehlt die Statusbox, rückt die Marke von selbst an +deren Stelle; fehlen Box und Zeichen (Wurzelknoten), steht sie am +Zeilenanfang. Für Zeilen **ohne** Statusbox ändert sich dadurch gar nichts. + +**Die alte Stellung wird weiter gelesen, aber nie mehr geschrieben.** Ein +harter Schnitt wäre vertretbar gewesen — die Faltmarken sind wenige Wochen alt +—, aber er träfe genau die Dokumente, die wir **nicht** migrieren können: +Pads (D31) und `?sourceUrl=`-Quellen (D23) liegen auf fremden Servern. Der +Parser hat deshalb zwei Marken-Gruppen (die erste gewinnt), `setFoldMark()` +schreibt immer die neue und löst eine alte dabei auf. Die Toleranz kostet eine +optionale Gruppe im Regex und eine Zeile in der SPEC; die Zeile *muss* dort +stehen, sonst ist die alte Schreibweise stillschweigend geduldete Magie statt +dokumentiertes Verhalten. + +**Der Preis, benannt:** Die Marke steht jetzt direkt vor dem Label, also dort, +wo ein Label anfangen kann. Ein Label wie `> 100 Nutzer` wird zur Marke plus +`100 Nutzer`. Neu ist das nicht — für Zeilen ohne Statusbox galt es schon +immer —, aber es ist jetzt der Regelfall statt der Ausnahme. Der Ausweg ist +derselbe wie bei `=SUMME(A1:B2)` (D34): kein Leerzeichen, also `>100 Nutzer`. + +Migriert sind die drei Marken in `docs/examples/werkbaum.werkbaum`, die eine +im `INITIAL`-Beispiel und das Beispiel in `llms.md`; die Legenden-Zeile +`hint_fold` nennt die neue Form in allen neun Sprachen. + ## D35 — XOR (`=`) umgesetzt: „realisiert“ definiert, „1“-Plakette, keine neue Linienart Das in D34 entschiedene XOR-Gate ist gebaut (SPEC §1/§3/§9); beim Bauen waren drei Dinge zu entscheiden, die die SPEC bis dahin offen ließ: @@ -2169,7 +2232,7 @@ Darstellung, bis die Querverbindungen (§11) gebaut sind; der Pfeil im Tooltip sagt die Richtung („hängt ab von“), ohne ein neues Zeichen einzuführen. ## D38 — Faltmarken gebaut: `<` wandert die Faltung hinunter, Eingriffe sind flüchtig -Die in D34 entschiedene Schreibweise (`- > [x] …`, Export folgt der Faltung) +Die in D34 entschiedene Schreibweise (`- [x] > …`, Export folgt der Faltung) ist umgesetzt (SPEC §1/§9). Die Bau-Entscheidungen: **`<` holt seinen Teilbaum hervor, indem die Faltung die Pfad-Ebenen diff --git a/docs/SPEC.md b/docs/SPEC.md index ec03029..580eb5e 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -7,15 +7,15 @@ Syntaxänderungen werden zuerst hier dokumentiert, dann implementiert. ## 1. Zeilenformat ``` -[Einrückung][Zeichen] [Faltmarke] [Statusbox] Label (Größe) URL @tag … !!! %% Kommentar +[Einrückung][Zeichen] [Statusbox] [Faltmarke] Label (Größe) URL @tag … !!! %% Kommentar ``` Alle Bestandteile außer dem Label sind optional. Die Extraktion erfolgt in dieser Reihenfolge (wichtig für Kollisionsfreiheit): 1. Kommentar entfernen: alles ab `%%` bis Zeilenende. -2. Einrückung, Zeichen (`-` / `+` / `|` / `=`), Faltmarke (`>` / `<`) und - Statusbox `[…]` per Zeilen-Regex; `=` nur mit folgendem Leerraum (§3), +2. Einrückung, Zeichen (`-` / `+` / `|` / `=`), Statusbox `[…]` und Faltmarke + (`>` / `<`) per Zeilen-Regex; `=` nur mit folgendem Leerraum (§3), die Faltmarke ebenso (siehe unten). 3. URL: erstes Token, das auf `https?://\S+` passt (dadurch stören `@` in URLs nicht). 4. Größe: erstes `(XS|S|M|L|XL|XXL)`, Groß-/Kleinschreibung egal. @@ -27,13 +27,22 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit): **Faltmarke `>` / `<`** — bestimmt, wie das Dokument **eröffnet** wird: -- Steht **zwischen** Zerlegungszeichen und Statusbox (`- > [x] Backend`); bei - Wurzelknoten (ohne Zeichen) am Zeilenanfang. Erkannt nur mit **folgendem - Leerraum** — `- >Achtung` bleibt damit ein Label. Begründung der Stellung: - D34-Nachtrag. +- Steht **unmittelbar vor dem Label**, also hinter der Statusbox + (`- [x] > Backend`); ohne Statusbox rückt sie an deren Stelle + (`- > Backend`, bei Wurzelknoten an den Zeilenanfang). Erkannt nur mit + **folgendem Leerraum** — `- [x] >Achtung` bleibt damit ein Label. Die + Stellung hält die Spalte der Statusboxen über die Ebenen hinweg bündig; + Begründung: D34-Nachtrag 2. +- Die frühere Stellung **zwischen Zeichen und Statusbox** (`- > [x] Backend`) + wird weiterhin **gelesen**, aber nie mehr geschrieben: Beim Zurückschreiben + (§9) wird sie in die neue aufgelöst. Stehen beide, gilt die erste. - `>` heißt: der Teilbaum dieses Knotens ist beim Öffnen **eingeklappt**. - `<` innerhalb eines eingeklappten Bereichs holt den **eigenen Teilbaum** - gezielt wieder hervor (Mechanik: §9). + gezielt wieder hervor (Mechanik: §9). Es ist eine **Schreibhilfe für + Autoren**: gelesen wird es unverändert, erzeugt wird es nie — das + Zurückschreiben (§9) setzt ausschließlich `>`. Ein von Hand gesetztes `<` + bleibt stehen, solange es den Zustand noch richtig beschreibt, und wird + aufgelöst, sobald alle Marken neu gesetzt werden. - 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 @@ -138,7 +147,15 @@ Text zu einem Knoten (Anzeige: §9): Referenz-Regex der Implementierung: ``` -^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([ ?~xX^/-])\]\s*)?(.*)$ +^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([ ?~xX^/-])\]\s*)?(?:([><])(?=[ \t])\s*)?(.*)$ +``` + +Die **zweite** Faltmarken-Gruppe ist die gültige Stellung, die erste die +weiterhin gelesene alte (siehe oben). Umkehrung fürs Zurückschreiben (§9) — +sie setzt die Marke immer in die zweite Stellung und löst die erste dabei auf: + +``` +^([ \t]*(?:[-|+]|=(?=[ \t]))?[ \t]*)(?:[><](?=[ \t])[ \t]*)?((?:\[[^\]]\][ \t]*)?)(?:[><](?=[ \t])[ \t]*)? ``` Für die Knoten-ID (Schritt 6, nur der erste Treffer; die letzte Gruppe ist der diff --git a/docs/examples/werkbaum.werkbaum b/docs/examples/werkbaum.werkbaum index e9d33bf..9961bb4 100644 --- a/docs/examples/werkbaum.werkbaum +++ b/docs/examples/werkbaum.werkbaum @@ -101,7 +101,7 @@ | [?] Ask the admin for cookie.sameSite = None (XS) | [ ] Live with it: edit in the pad's own tab (XS) - [^] A shared pointer: !!! focuses a node for everyone (XS) - - > [?] Update by itself, within about 2 s (M) + - [?] > Update by itself, within about 2 s (M) | [?] Our own Etherpad beside Werkbaum (M) %% then every setting below is ours - [ ] Node and a real database in userland (S) %% the flatfile store is not for production - [ ] Book the RAM quota for an own daemon (XS) @@ -141,7 +141,7 @@ - [?] Registration with Mermaid (M) | [?] External diagram, self-hosted pages only (M) | [?] Pull request into Mermaid itself (L) - - > [?] IntelliJ IDEA plugin (XL) + - [?] > IntelliJ IDEA plugin (XL) - [?] Show the diagram in the IDE (L) | [?] Through Mermaid, once Werkbaum is part of Mermaid itself (M) - [?] Fenced werkbaum blocks render in Markdown (S) @@ -157,7 +157,7 @@ - [?] Completion for status, size and tags (S) - [?] Structure view and folding (S) - [?] Kotlin with the Gradle IntelliJ plugin (S) - - < [!] A second parser that must not drift from the SPEC (M) %% exactly what D14 warns about + - [!] < A second parser that must not drift from the SPEC (M) %% exactly what D14 warns about | [?] Both parsers test against the same fixtures (S) | [?] Run the one JS parser inside the IDE (M) - [?] Tracker integration (XL) %% exactly one of these, hence = diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 03e07de..4111307 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -296,7 +296,12 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der `freshPrevRoots` (Basis, einmal geparst). Basis je Dokument in `werkbaum-seen`, fortgeschrieben **erst beim Bestätigen** über `#freshBtn`. - Faltmarken `>`/`<` (SPEC §1/§9, D38): Parser setzt nur `fold` ('>'|'<'|null, - Leerraum-Regel); den wirksamen Anfangszustand rechnet `initialCollapsed()` + Leerraum-Regel). **Stellung ist unmittelbar vor dem Label**, also hinter der + Statusbox (`- [x] > …`, D34-Nachtrag 2); die alte Stellung **davor** + (`- > [x] …`) wird weiter gelesen — daher zwei Marken-Gruppen im Zeilen-Regex, + die erste gewinnt —, aber `setFoldMark()` schreibt immer die neue und löst + eine alte dabei auf. Wer die Gruppen anfasst: Der Rest der Zeile ist `m[6]`, + nicht `m[5]`. Den wirksamen Anfangszustand rechnet `initialCollapsed()` in model.js — `<` (und die Fokusmarke) wandert die Faltung die Pfad-Ebenen **hinunter** statt Vorfahren bloß zu öffnen. `render()` überlagert ihn mit `foldOverrides` (Schlüssel = Label-Pfad via `nodeKeys()`, Sitzung, beim diff --git a/frontend/index.html b/frontend/index.html index 5b6ebce..2a3dce5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -131,7 +131,7 @@ Abhängigkeiten mit :#name,#name — erscheinen im Tooltip. Die Knotenfarbe zeigt den effektiven Status (mit Abhängigkeiten); ist der eigene weiter, steht er als Marke unten links. Beschreibungen: " Zeile unter dem Knoten; Langtext hinter --- als eingerückter #id-Block — beides im Tooltip (”). - Falten: - > [x] … startet eingeklappt, < holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→). + Falten: - [x] > … startet eingeklappt, < holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→). diff --git a/frontend/public/llms.md b/frontend/public/llms.md index 5320eaf..06646e5 100644 --- a/frontend/public/llms.md +++ b/frontend/public/llms.md @@ -14,7 +14,7 @@ https://werkbaum.javagil.de renders the notation as a diagram; ## Line format - [indent][gate] [fold] [status] Label (SIZE) URL @person #id :#id,#id !!! %% comment + [indent][gate] [status] [fold] Label (SIZE) URL @person #id :#id,#id !!! %% comment One node per line. Everything except the label is optional. @@ -46,12 +46,17 @@ One node per line. Everything except the label is optional. - Optionality (`+`) is orthogonal to status: an optional node can be long `[^]`. Optional nodes never count toward the cheapest path. -### Fold mark (between gate and status box; on root lines at line start) +### Fold mark (immediately before the label, i.e. after the status box) - `>` — this subtree starts **collapsed** when the document opens. - `<` — inside a collapsed region, bring this subtree back into view. -- Recognized only with following whitespace (`- >Careful` stays a label). - Presentation only: no effect on meaning, costs or warnings. +- Recognized only with following whitespace (`- [x] >Careful` stays a label). +- **Write it after the status box** (`- [x] > Concept`). The older position + between gate and box (`- > [x] Concept`) is still read, but never written: + it shifts the box by one indent step, so a folded parent's box lines up with + its children's boxes. +- Describes the document's fold state: opening restores it, and folding in the + diagram writes the mark back. No effect on meaning, costs or warnings. ### Status box @@ -163,7 +168,7 @@ One node per line. Everything except the label is optional. %% Project structure – Sprint 14 [~] Website relaunch (XL) https://wiki.example.com/relaunch " Folded chapters are done. - - > [x] Concept (M) + - [x] > Concept (M) - [x] Audience analysis (S) - [x] Sitemap (XS) - [~] Implementation (XL) diff --git a/frontend/src/app.js b/frontend/src/app.js index 3942849..700bd59 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -13,7 +13,7 @@ import WERKBAUM_DOC from '../../docs/examples/werkbaum.werkbaum?raw'; const INITIAL = `%% Project structure – Sprint 14 [~] Website relaunch (XL) https://wiki.example.com/relaunch " Folded chapters are done — click the ▸ to peek inside. - - > [x] Concept (M) + - [x] > Concept (M) - [x] Audience analysis (S) - [x] Sitemap (XS) - [~] Implementation (XL) @@ -1511,7 +1511,7 @@ const I18N = { hint_deps:"Abhängigkeiten mit :#name,#name — erscheinen im Tooltip.", hint_eff:"Die Knotenfarbe zeigt den effektiven Status (mit Abhängigkeiten); ist der eigene weiter, steht er als Marke unten links.", hint_desc:"Beschreibungen: \" Zeile unter dem Knoten; Langtext hinter --- als eingerückter #id-Block — beides im Tooltip (”).", - hint_fold:"Falten: - > [x] … startet eingeklappt, < holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→).", + hint_fold:"Falten: - [x] > … startet eingeklappt, < holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→).", hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile; Alt+Klick im Text holt den Knoten ins Bild." }, en: { @@ -1591,7 +1591,7 @@ const I18N = { hint_deps:"Dependencies with :#name,#name — shown in the tooltip.", hint_eff:"Node colour shows the effective status (with dependencies); if its own is further along, it appears as a mark at the bottom left.", hint_desc:"Descriptions: a \" line below the node; long text behind --- as an indented #id block — both in the tooltip (”).", - hint_fold:"Folding: - > [x] … starts collapsed, < brings it back; ▾/▸ on a node toggles (keyboard: ←/→).", + hint_fold:"Folding: - [x] > … starts collapsed, < brings it back; ▾/▸ on a node toggles (keyboard: ←/→).", hint_jump:"Alt+click a node (long press on touch) jumps to its line in the text; Alt+click in the text brings the node into view." }, es: { @@ -1671,7 +1671,7 @@ const I18N = { hint_deps:"Dependencias con :#nombre,#nombre — visibles en el tooltip.", hint_eff:"El color del nodo muestra el estado efectivo (con dependencias); si el propio va más adelante, aparece como marca abajo a la izquierda.", hint_desc:"Descripciones: línea \" bajo el nodo; texto largo tras --- como bloque #id sangrado — ambos en el tooltip (”).", - hint_fold:"Plegado: - > [x] … empieza plegado, < lo recupera; ▾/▸ en el nodo alterna (teclado: ←/→).", + hint_fold:"Plegado: - [x] > … empieza plegado, < lo recupera; ▾/▸ en el nodo alterna (teclado: ←/→).", hint_jump:"Alt+clic en un nodo (pulsación larga en táctil) salta a su línea en el texto; Alt+clic en el texto trae el nodo a la vista." }, fr: { @@ -1751,7 +1751,7 @@ const I18N = { hint_deps:"Dépendances avec :#nom,#nom — visibles dans l’infobulle.", hint_eff:"La couleur du nœud montre le statut effectif (avec dépendances) ; si le sien est plus avancé, il apparaît en marque en bas à gauche.", hint_desc:"Descriptions : ligne \" sous le nœud ; texte long après --- en bloc #id indenté — les deux dans l’infobulle (”).", - hint_fold:"Pliage : - > [x] … démarre replié, < le fait ressortir ; ▾/▸ sur le nœud bascule (clavier : ←/→).", + hint_fold:"Pliage : - [x] > … démarre replié, < le fait ressortir ; ▾/▸ sur le nœud bascule (clavier : ←/→).", hint_jump:"Alt+clic sur un nœud (appui long sur tactile) saute à sa ligne dans le texte ; Alt+clic dans le texte amène le nœud à l’écran." }, pl: { @@ -1831,7 +1831,7 @@ const I18N = { hint_deps:"Zależności przez :#nazwa,#nazwa — widoczne w podpowiedzi.", hint_eff:"Kolor węzła pokazuje status efektywny (z zależnościami); jeśli własny jest dalej, widnieje jako znacznik u dołu po lewej.", hint_desc:"Opisy: wiersz \" pod węzłem; dłuższy tekst za --- jako wcięty blok #id — oba w podpowiedzi (”).", - hint_fold:"Zwijanie: - > [x] … zaczyna zwinięte, < przywraca; ▾/▸ na węźle przełącza (klawiatura: ←/→).", + hint_fold:"Zwijanie: - [x] > … zaczyna zwinięte, < przywraca; ▾/▸ na węźle przełącza (klawiatura: ←/→).", hint_jump:"Alt+kliknięcie węzła (długie naciśnięcie na dotyku) przechodzi do jego wiersza w tekście; Alt+kliknięcie w tekście pokazuje węzeł na diagramie." }, ru: { @@ -1911,7 +1911,7 @@ const I18N = { hint_deps:"Зависимости через :#имя,#имя — видны в подсказке.", hint_eff:"Цвет узла показывает фактический статус (с учётом зависимостей); если собственный дальше, он показан меткой слева внизу.", hint_desc:"Описания: строка \" под узлом; длинный текст после --- как блок #id с отступом — оба в подсказке (”).", - hint_fold:"Сворачивание: - > [x] … открывается свёрнутым, < возвращает; ▾/▸ на узле переключает (клавиши: ←/→).", + hint_fold:"Сворачивание: - [x] > … открывается свёрнутым, < возвращает; ▾/▸ на узле переключает (клавиши: ←/→).", hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме." }, hi: { @@ -1991,7 +1991,7 @@ const I18N = { hint_deps:":#नाम,#नाम से निर्भरताएँ — टूलटिप में दिखती हैं।", hint_eff:"नोड का रंग प्रभावी स्थिति दिखाता है (निर्भरताओं सहित); यदि अपनी स्थिति आगे है, तो वह नीचे-बाएँ चिह्न के रूप में दिखती है।", hint_desc:"विवरण: नोड के नीचे \" पंक्ति; --- के बाद #id ब्लॉक में लंबा पाठ — दोनों टूलटिप में (”)।", - hint_fold:"फ़ोल्डिंग: - > [x] … समेटा हुआ खुलता है, < वापस लाता है; नोड पर ▾/▸ टॉगल करता है (कीबोर्ड: ←/→)।", + hint_fold:"फ़ोल्डिंग: - [x] > … समेटा हुआ खुलता है, < वापस लाता है; नोड पर ▾/▸ टॉगल करता है (कीबोर्ड: ←/→)।", hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।" }, zh: { @@ -2071,7 +2071,7 @@ const I18N = { hint_deps:"用 :#名称,#名称 表示依赖——显示在提示中。", hint_eff:"节点颜色显示实际状态(含依赖);若自身状态更靠前,会以左下角标记显示。", hint_desc:"描述:节点下方的 \" 行;--- 之后的缩进 #id 块为长文本——均显示在提示中(”)。", - hint_fold:"折叠:- > [x] … 打开时即折叠,< 将其展开;节点上的 ▾/▸ 切换(键盘:←/→)。", + hint_fold:"折叠:- [x] > … 打开时即折叠,< 将其展开;节点上的 ▾/▸ 切换(键盘:←/→)。", hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。" }, ja: { @@ -2151,7 +2151,7 @@ const I18N = { hint_deps:":#名前,#名前 で依存関係 — ツールチップに表示されます。", hint_eff:"ノードの色は実効ステータス(依存関係込み)を示します。自身が先行している場合は左下のマークで表示されます。", hint_desc:"説明:ノード直下の \" 行。--- 以降は #id ブロック(字下げ)で長文 — どちらもツールチップに表示(”)。", - hint_fold:"折りたたみ:- > [x] … は折りたたんだ状態で開き、< は呼び戻します。ノードの ▾/▸ で切替(キー:←/→)。", + hint_fold:"折りたたみ:- [x] > … は折りたたんだ状態で開き、< は呼び戻します。ノードの ▾/▸ で切替(キー:←/→)。", hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。" } }; diff --git a/frontend/src/parser.js b/frontend/src/parser.js index 87c5850..a7b0788 100644 --- a/frontend/src/parser.js +++ b/frontend/src/parser.js @@ -27,9 +27,13 @@ export const STATUS_BY_CODE = { 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); + /* Geschrieben wird IMMER die kanonische Stellung: unmittelbar vor dem Label, + also hinter der Statusbox (SPEC §1, D34-Nachtrag 2). Eine Marke in der + alten Stellung (zwischen Zeichen und Box) wird dabei aufgelöst — sie wird + weiterhin gelesen, aber nicht mehr erzeugt. */ + const m = line.match( + /^([ \t]*(?:[-|+]|=(?=[ \t]))?[ \t]*)(?:[><](?=[ \t])[ \t]*)?((?:\[[^\]]\][ \t]*)?)(?:[><](?=[ \t])[ \t]*)?/); + return m[1] + m[2] + (mark ? mark + ' ' : '') + line.slice(m[0].length); } /* Status, die als „realisiert" zählen (XOR-Regel, SPEC §3/D35): Kosten sind @@ -142,17 +146,19 @@ export function parse(text){ (fehlertolerant: die Zeile geht nicht verloren). */ /* `=` (XOR, SPEC §3) nur mit folgendem Leerraum — die Leerraum-Regel hält Labels wie `=SUMME(A1:B2)` heraus; `-`/`+`/`|` bleiben wie bisher. - Die Faltmarke `>`/`<` (SPEC §1, D38) steht zwischen Zeichen und - Statusbox (bei Wurzeln am Zeilenanfang), ebenfalls nur mit folgendem - Leerraum — `- >Achtung` bleibt ein Label. */ - const m = raw.match(/^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([^\]])\]\s*)?(.*)$/); + Die Faltmarke `>`/`<` (SPEC §1, D38) steht unmittelbar vor dem Label, + also hinter der Statusbox — ebenfalls nur mit folgendem Leerraum, + `- [x] >Achtung` bleibt ein Label. Die frühere Stellung ZWISCHEN Zeichen + und Box wird weiter gelesen (D34-Nachtrag 2), aber nicht mehr + geschrieben; deshalb zwei Marken-Gruppen, die erste gewinnt. */ + const m = raw.match(/^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([^\]])\]\s*)?(?:([><])(?=[ \t])\s*)?(.*)$/); const width = m[1].replace(/\t/g,' ').length; const type = m[2] === '|' ? 'or' : m[2] === '=' ? 'xor' : 'and'; const optional = m[2] === '+'; - const fold = m[3] || null; + const fold = m[3] || m[5] || null; const boxChar = m[4]; // undefined, wenn keine Statusbox - let rest = m[5], url = null, size = null; + let rest = m[6], url = null, size = null; const tags = []; rest = rest.replace(/https?:\/\/\S+/i, s => { url = s; return ''; }); rest = rest.replace(/\((XXL|XS|XL|S|M|L)\)/i, (s, g) => { size = g.toUpperCase(); return ''; }); diff --git a/frontend/tests/fold.test.js b/frontend/tests/fold.test.js index e4cfc9a..b745454 100644 --- a/frontend/tests/fold.test.js +++ b/frontend/tests/fold.test.js @@ -18,23 +18,31 @@ const collapsedLabels = (txt, rescueFocus = true) => [...initialCollapsed(roots(txt), rescueFocus)].map(n => n.label).sort(); /* Faltmarken `>`/`<` (SPEC §1/§9, D38): Anfangszustand der Faltung im Text. */ -describe('Parser — Faltmarke zwischen Zeichen und Statusbox', () => { - it('erkennt `>` und `<` und nimmt sie aus dem Label', () => { - const [wurzel] = roots(`[ ] Wurzel\n - > [x] Zu (M)\n - < [ ] Auf`); +describe('Parser — Faltmarke unmittelbar vor dem Label', () => { + it('erkennt `>` und `<` hinter der Statusbox und nimmt sie aus dem Label', () => { + const [wurzel] = roots(`[ ] Wurzel\n - [x] > Zu (M)\n - [ ] < Auf`); expect(wurzel.children.map(k => [k.label, k.fold, k.status?.key])) .toEqual([['Zu', '>', 'fertig'], ['Auf', '<', 'geplant']]); expect(wurzel.fold).toBe(null); }); - it('erkennt die Marke am Wurzelknoten (ohne Zeichen) am Zeilenanfang', () => { - const [wurzel] = roots(`> [~] Kapitel\n - [ ] Kind`); - expect([wurzel.fold, wurzel.label]).toEqual(['>', 'Kapitel']); + it('liest die alte Stellung (vor der Box) weiter — D34-Nachtrag 2', () => { + const [wurzel] = roots(`[ ] Wurzel\n - > [x] Zu (M)\n - < [ ] Auf`); + expect(wurzel.children.map(k => [k.label, k.fold, k.status?.key])) + .toEqual([['Zu', '>', 'fertig'], ['Auf', '<', 'geplant']]); }); - it('verlangt folgenden Leerraum — `- >Achtung` bleibt ein Label', () => { - const [wurzel] = roots(`[ ] Wurzel\n - >Achtung`); + it('erkennt die Marke ohne Statusbox direkt hinter dem Zeichen', () => { + const [wurzel] = roots(`> [~] Kapitel\n - > Kind ohne Box`); + expect([wurzel.fold, wurzel.label]).toEqual(['>', 'Kapitel']); expect(wurzel.children.map(k => [k.label, k.fold])) - .toEqual([['>Achtung', null]]); + .toEqual([['Kind ohne Box', '>']]); + }); + + it('verlangt folgenden Leerraum — `[x] >Achtung` bleibt ein Label', () => { + const [wurzel] = roots(`[ ] Wurzel\n - >Achtung\n - [x] >Achtung`); + expect(wurzel.children.map(k => [k.label, k.fold])) + .toEqual([['>Achtung', null], ['>Achtung', null]]); }); it('lässt ein `>` mitten im Label unberührt', () => { @@ -46,32 +54,51 @@ 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('setzt `>` hinter die Statusbox, unmittelbar vor das Label', () => { + expect(setFoldMark(' - [x] Concept (M)', '>')).toBe(' - [x] > Concept (M)'); }); it('entfernt eine vorhandene Marke', () => { - expect(setFoldMark(' - > [x] Concept (M)', null)).toBe(' - [x] Concept (M)'); + expect(setFoldMark(' - [x] > Concept (M)', null)).toBe(' - [x] Concept (M)'); }); it('ersetzt `<` durch `>`', () => { - expect(setFoldMark(' - < [ ] B1', '>')).toBe(' - > [ ] B1'); + 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öst die alte Stellung (vor der Box) in die neue auf', () => { + /* Gelesen wird sie weiter (D34-Nachtrag 2), geschrieben nie. */ + expect(setFoldMark(' - > [x] Concept (M)', '>')).toBe(' - [x] > Concept (M)'); + expect(setFoldMark(' - < [x] Concept (M)', '>')).toBe(' - [x] > Concept (M)'); + expect(setFoldMark(' - > [x] Concept (M)', null)).toBe(' - [x] Concept (M)'); + }); + + it('setzt die Marke ohne Statusbox direkt hinter das Zeichen', () => { + expect(setFoldMark(' - Konzeption', '>')).toBe(' - > Konzeption'); + expect(setFoldMark('[~] Wurzel', '>')).toBe('[~] > Wurzel'); + expect(setFoldMark('[~] > Wurzel', null)).toBe('[~] Wurzel'); + expect(setFoldMark('Wurzel ohne Box', '>')).toBe('> Wurzel ohne Box'); }); it('lässt ungewöhnliche Spaltung stehen', () => { - expect(setFoldMark(' - [x] X', '>')).toBe(' - > [x] X'); - expect(setFoldMark('\t= [ ] Cloud', '>')).toBe('\t= > [ ] Cloud'); + 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'); + /* `[x] >Achtung` ist ein Label, keine Marke — die neue Marke kommt davor. */ + expect(setFoldMark(' - [x] >Achtung', '>')).toBe(' - [x] > >Achtung'); + expect(setFoldMark(' - [x] >Achtung', null)).toBe(' - [x] >Achtung'); + }); + + it('hält die Statusbox-Spalte über die Ebenen hinweg bündig', () => { + /* Der Grund für die Stellung (D34-Nachtrag 2): Vor der Box geschrieben, + rückte die Box der gefalteten Zeile um genau eine Einrückungsstufe ein + und stand dann in der Spalte der Boxen ihrer eigenen Kinder. */ + const eltern = setFoldMark(' - [ ] erster Schritt', '>'); + const kind = ' - [ ] Schritt 1a'; + expect(eltern.indexOf('[')).toBe(4); + expect(kind.indexOf('[')).toBe(6); }); it('ist verlustfrei umkehrbar', () => {