From ad2f3bab14102ba7be9b8878320b662308064306 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Sun, 23 Aug 2026 07:31:45 +0200 Subject: [PATCH] feat: Knoten-ID vor dem Titel mit optionalem Doppelpunkt; Alt+Klick pulst mit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Übliche Schreibweise ist jetzt `#auth: Backend` — die ID benennt den Knoten, der Titel erläutert sie; vorn hat sie eine feste Stelle, an der das Auge sie sucht. Der Doppelpunkt ist optional und reines Trennzeichen: Er gehört weder zur ID noch zum Label und fällt beim Parsen weg, sonst wären `#auth` und `#auth:` zwei verschiedene Adressen. Geschluckt wird er nur mit folgendem Leerraum oder Zeilenende (Leerraum-Regel wie bei `=`, `>`/`<`, `"`). Damit bleibt `#auth:#db` ID plus Abhängigkeit, und ein Doppelpunkt im Titel bleibt stehen. Die ID-Erkennung selbst ist unverändert — die Doppelpunkt-Gruppe ist optional; ein verlangtes `(?=\s|$)` hinter der ID hätte bestehende Zeilen umgedeutet. Der Block-Kopf im Beschreibungsteil nimmt den Doppelpunkt ebenfalls an. Alle mitgelieferten Beispiele umgestellt (docs/examples/, INITIAL, llms.md); SPEC §10 hat keine IDs. Legenden-Zeile hint_id in allen neun Sprachen. Dazu: Alt+Klick im Textfeld zeigt jetzt dieselbe Hervorhebung wie der Zeilenwechsel, Puls eingeschlossen — bisher unterdrückte focusNodeOfCaret() ihn, um doppeltes Scrollen zu vermeiden, und ausgerechnet die ausdrückliche Geste kam stiller an als das beiläufige Tippen. Getrennt über den neuen Parameter highlightCurrentNode(moved, scroll): 'nearest' | 'center' | false. 7 neue Parser-Tests (197 gesamt). SPEC §1, D36 (Nachtrag), D25 (Nachtrag 2) und llms.md nachgezogen. Co-Authored-By: Claude Fable 5 --- docs/DECISIONS.md | 66 +++++++++++++++++++++++++++++++++ docs/SPEC.md | 15 ++++++-- docs/examples/werkbaum.werkbaum | 10 ++--- frontend/CLAUDE.md | 17 +++++++-- frontend/index.html | 2 +- frontend/public/llms.md | 13 ++++++- frontend/src/app.js | 38 +++++++++++-------- frontend/src/parser.js | 15 ++++++-- frontend/tests/ids.test.js | 46 +++++++++++++++++++++++ 9 files changed, 190 insertions(+), 32 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index e1f52f8..d461a04 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -710,6 +710,28 @@ Nebenbefund beim Aufräumen: Der Kommentar über der Regel behauptete noch, die Fokusmarke `!!!` trage „bewusst DIESELBE Hervorhebung" — überholt, seit der D32-Nachtrag ihr den eigenen Petrol-Kranz gegeben hat. Ersetzt. +**Nachtrag 2 — Alt+Klick im Textfeld pulst jetzt auch.** Die erste Fassung des +Pulses hing an derselben Bedingung wie das Scrollen, und `focusNodeOfCaret()` +gab dort bewusst `false` weiter, um ein doppeltes Scrollen zu vermeiden +(erst `nearest`, dann `center`). Folge: Ausgerechnet die **ausdrückliche** +Geste — „ich will diesen Knoten jetzt sehen" — kam stiller an als das +beiläufige Tippen. Das ist verkehrt herum. + +Getrennt sind die beiden Dinge jetzt sauber: `highlightCurrentNode(moved, +scroll)` nimmt zusätzlich, **wie** ins Bild geholt wird — `'nearest'` beim +gewöhnlichen Zeilenwechsel, `'center'` beim Alt+Klick, `false` beim Neubau. +Die Hervorhebung ist in allen Fällen dieselbe, Puls eingeschlossen; der +Unterschied liegt allein im Scroll-Modus. Der Fokus wandert weiter mit +(D25-Nachtrag 1, `focus({preventScroll:true})` **vor** dem Scrollen) — er +verändert das Bild nicht, weil `:focus-visible` nach einem Mausklick nicht +greift (nachgemessen: `false`). + +Der Fall, den man beim Bauen leicht übersieht: Alt+Klick trifft oft den Knoten, +der **schon** die Cursor-Zeile ist. Dann ist es dasselbe DOM-Element, und der +Puls muss trotzdem neu anlaufen — genau dafür steht das Lesen von +`offsetWidth` zwischen Entfernen und Setzen der Klasse. Nachgemessen: vor dem +Klick keine laufende Animation, 80 ms danach beide bei `currentTime ≈ 67 ms`. + ## D26 — Legende scrollbar: eigener Container statt `
`, plus Splitter Die Legende („Agenda") im Editor-Panel war zu hoch für ihren Platz und wurde **abgeschnitten** statt scrollbar zu sein — obwohl `.hint` seit jeher @@ -1884,6 +1906,50 @@ Abhängigkeits-Auflösung — dort ist die Warnung dann schon da. Eine Zeile, di **nur** aus einer ID besteht, wird wie jede leere Zeile ignoriert und belegt die ID nicht. +**Nachtrag — die übliche Stellung ist vor dem Titel, mit trennendem +Doppelpunkt: `#auth: Backend`.** Die Stellung war frei und blieb es auch; was +fehlte, war eine **Konvention**. In den Beispielen stand die ID mal hinten +(`Documents on the server #docs (L)`), mal irgendwo dazwischen — lesbar, aber +ohne feste Stelle, an der das Auge sie sucht. Vorn ist sie dort, wo sie +hingehört: Sie **benennt** den Knoten, und der Titel ist die Erläuterung dazu. +Damit liest sich eine Zeile wie ein Wörterbucheintrag, und Zeilen mit ID +richten sich untereinander aus. + +**Der Doppelpunkt ist optional und reines Trennzeichen.** Ohne ihn stünden +zwei Bezeichner unmittelbar nebeneinander (`#auth Backend`) — das liest sich +wie ein zweiteiliger Name, nicht wie Adresse plus Titel. Er gehört weder zur +ID noch zum Label und wird nie gerendert; im Diagramm steht genau wie bisher +nur der Titel. Ein Werkzeug, das ihn als Teil der ID führte, machte aus +`#auth` und `#auth:` zwei verschiedene Adressen — deshalb fällt er beim Parsen +weg, nicht erst beim Anzeigen. + +**Geschluckt wird er nur mit folgendem Leerraum oder Zeilenende** — dieselbe +Leerraum-Regel wie bei `=`, `>`/`<` und `"` (D34), hier aber mit einem +konkreten Grund: `#auth:#db` bliebe sonst nicht als ID plus Abhängigkeit +erhalten. Mit der Regel liest der Parser dort `#auth` als ID, lässt den +Doppelpunkt stehen, und die Abhängigkeits-Extraktion (§1, Schritt 7) findet +`:#db` — genau wie ohne die Neuerung. Ein Doppelpunkt **im Titel** bleibt +ebenfalls unangetastet: `#auth: Regel: nur mit Token` ergibt das Label +„Regel: nur mit Token“, weil nur der **unmittelbar** an die ID anschließende +Doppelpunkt gemeint ist. + +**Die ID-Erkennung selbst ist unverändert geblieben** — die Doppelpunkt-Gruppe +im Regex ist optional und verlangt nichts. Ein zusätzlich verlangtes +`(?=\s|$)` **hinter der ID** wäre der naheliegende, aber falsche Weg gewesen: +Es hätte bestehende Zeilen umgedeutet, weil der Ausdruck bei einem Fehlschlag +weiterwandert und dann ein *späteres* `#`-Token zur ID erklärt hätte. + +Im **Beschreibungsteil** (`---`, §1) ist ein angehängter Doppelpunkt ebenfalls +zugelassen (`#auth:` als Block-Kopf). Dort folgt kein Titel, die Konvention +greift also nicht — aber wer sie gewohnt ist, soll nicht über eine +`descStray`-Warnung stolpern. Die mitgelieferten Beispiele schreiben +Block-Köpfe weiterhin ohne Doppelpunkt. + +Umgestellt sind alle mitgelieferten Beispiele (`docs/examples/`, das +`INITIAL`-Dokument und das Beispiel in `llms.md`); SPEC §10 hat keine IDs und +bleibt unberührt. Die Legenden-Zeile `hint_id` nennt die neue Form in allen +neun Sprachen. + ## D37 — Abhängigkeiten (`:#a,#b`) geparst: ein Token, alleinstehend, IDs als Strings Der zweite Baustein der Phase-4-Kette. Gebaut ist die **Schreibweise** (SPEC §1); die Konsumenten — effektiver Status, Querverbindungen, Closure-Rechnung — diff --git a/docs/SPEC.md b/docs/SPEC.md index 720cc39..2500466 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -51,6 +51,13 @@ sie ist die Adresse für Abhängigkeiten und Beschreibungsblöcke (§11). dem Muster des angebundenen Trackers entspricht (`#123`, `#US-123`), ist zugleich die künftige Ticket-Referenz — oft ist die Ticket-Kennung die natürliche Knoten-ID (D34). +- **Übliche Schreibweise ist die ID vor dem Titel, mit Doppelpunkt:** + `#auth: Backend`. Der Doppelpunkt ist **optional** und reines Trennzeichen im + Text — er gehört weder zur ID noch zum Label und erscheint nicht im Diagramm. + Geschluckt wird er nur, wenn er **unmittelbar** auf die ID folgt und ihm + **Leerraum oder Zeilenende** folgt; ein Doppelpunkt im Label (`#auth: Regel: + nur mit Token`) bleibt also stehen, und `#auth:#db` bleibt ID plus + Abhängigkeit. Die Stellung ist frei — `Backend #auth` bedeutet dasselbe. - Die ID gehört **nicht** zum Label. Eine eigene Darstellung im Diagramm hat sie (noch) nicht; sichtbar ist sie im Knoten-Tooltip und im `aria-label`. - **Doppelte ID:** Warnung `duplicateId` mit beiden Zeilennummern; die spätere @@ -110,7 +117,8 @@ Text zu einem Knoten (Anzeige: §9): umgebender Leerraum erlaubt) trennt den Baumteil vom **Beschreibungsteil**; alles danach gehört zu ihm (YAML-/Frontmatter-Konvention). Es gibt keinen Schlusszaun — er kann also nicht vergessen werden. Dort eröffnet eine - **uneingerückte Zeile mit genau einer Knoten-ID** (`#auth`, allein) einen + **uneingerückte Zeile mit genau einer Knoten-ID** (`#auth`, allein — ein + angehängter Doppelpunkt ist auch hier zugelassen) einen Block; die **eingerückten** Zeilen darunter sind sein Text (um den Einzug gekürzt, Leerzeilen bleiben als Absatztrenner). Die Wurzelknoten-Regel (§2) gilt hinter dem Trenner nicht mehr. Weitere `---`-Zeilen dort sind ohne @@ -132,10 +140,11 @@ Referenz-Regex der Implementierung: ^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([ ?~xX^/-])\]\s*)?(.*)$ ``` -Für die Knoten-ID (Schritt 6, nur der erste Treffer): +Für die Knoten-ID (Schritt 6, nur der erste Treffer; die letzte Gruppe ist der +optionale Trenn-Doppelpunkt, der mit entfällt): ``` -(^|\s)#([\p{L}\p{N}._-]+) +(^|\s)#([\p{L}\p{N}._-]+)(?::(?=\s|$))? ``` Für die Abhängigkeiten (Schritt 7, alle Treffer): diff --git a/docs/examples/werkbaum.werkbaum b/docs/examples/werkbaum.werkbaum index 10cef34..e9d33bf 100644 --- a/docs/examples/werkbaum.werkbaum +++ b/docs/examples/werkbaum.werkbaum @@ -9,7 +9,7 @@ - [^] People tags, bare URLs, %% comments (XS) - [^] And/or decomposition (S) - [^] Optional nodes — neither required nor an alternative (S) - - [^] Node IDs (#auth) (S) %% often just the ticket number, see SPEC §11 + - [^] Node IDs, (#auth:) before the title (S) %% often the ticket number, see SPEC §11 + [?] Free tags, &tag (M) %% only together with a consumer, see D34 - [?] A lens: highlight every node tagged &x (S) - [^] Dependencies across the tree, (:#auth,#api) (M) @@ -40,7 +40,7 @@ - [^] Cheapest path (M) - [^] Cost model, missing size counts as M (S) - [^] Metro-map line through the leaves (S) - - [^] Count the whole dependency closure #closure (M) + - [^] #closure: Count the whole dependency closure (M) - [^] Shared dependencies counted only once (S) - [^] The cheapest alternative is no longer the local one (M) %% see D34, D42 | [^] Search exactly, the trees are small enough (M) @@ -78,10 +78,10 @@ - [-] Deploy through a gh-pages branch (S) %% GitHub Actions instead - [-] Fonts from Google (XS) %% self-hosted, no third-party request - [ ] Backend (XL) - - [ ] Kotlin and Spring Boot scaffold #scaffold (M) + - [ ] #scaffold: Kotlin and Spring Boot scaffold (M) - [ ] Gradle Kotlin DSL, JDK 21 (S) - [ ] Health endpoint and CI (S) - - [ ] Documents on the server #docs (L) :#scaffold + - [ ] #docs: Documents on the server (L) :#scaffold - [ ] Text plus metadata, no structure format (M) - [ ] REST endpoints (S) - [ ] Persistence (S) @@ -162,7 +162,7 @@ | [?] Run the one JS parser inside the IDE (M) - [?] Tracker integration (XL) %% exactly one of these, hence = = [?] Taiga (L) https://taiga.io - - [ ] Resolve "#123" over the REST API #resolve (M) + - [ ] #resolve: Resolve "#123" over the REST API (M) - [ ] Read title, link and status (S) - [ ] Map the workflow onto the states (S) - [?] Write the status back (M) :#resolve diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index ba658a5..967ceae 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -232,9 +232,12 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der `jumpHint` im Knoten-Tooltip und `hint_jump` als letzte Zeile der Legende. - Alt+Klick **im Textfeld** (D25, Nachtrag): `focusNodeOfCaret()` zentriert den Knoten der Cursor-Zeile und gibt ihm den Fokus (`focus({preventScroll:true})` - **vor** `scrollIntoView({block:'center'})` — sonst scrollt der Browser zweimal). - Das ist nicht dasselbe wie das Mitlaufen der Cursor-Zeile: `syncCaret()` scrollt - absichtlich nur `nearest` und nur beim Zeilenwechsel. Beim Tastaturweg + **vor** dem Scrollen — sonst scrollt der Browser zweimal). Die Hervorhebung ist + **dieselbe wie beim Zeilenwechsel**, Puls eingeschlossen; den Unterschied trägt + allein der zweite Parameter `highlightCurrentNode(moved, scroll)`: + `'nearest'` beim gewöhnlichen Zeilenwechsel (`syncCaret()`), `'center'` hier, + `false` beim Neubau aus `render()`. Nicht wieder `false` für den Puls + mitgeben — dann kommt die ausdrückliche Geste stiller an als das Tippen. Beim Tastaturweg (Alt+Enter) ist `preventDefault()` Pflicht, sonst bekommt der Text einen Umbruch. Die Legenden-Zeile `hint_jump` nennt **beide** Richtungen — kein eigener i18n-Schlüssel. @@ -307,6 +310,14 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der darf nicht gefressen werden. Zeichenmenge wie `@name`. Doppelte ID → `{type:'duplicateId', line, id, firstLine}`; die spätere gilt trotzdem. Keine eigene Darstellung — nur Tooltip (erste Position) und `a11yId`. + **Übliche Schreibweise ist `#id: Titel`** (D36-Nachtrag): Der trennende + Doppelpunkt ist optional, gehört weder zur ID noch zum Label und fällt beim + Parsen weg — sonst wären `#auth` und `#auth:` zwei Adressen. Geschluckt nur + mit folgendem **Leerraum oder Zeilenende**, sonst bliebe von `#auth:#db` die + Abhängigkeit nicht übrig. Die ID-Erkennung selbst NICHT verschärfen: ein + verlangtes `(?=\s|$)` hinter der ID deutet bestehende Zeilen um (der Ausdruck + wandert bei Fehlschlag weiter und erklärt ein späteres `#`-Token zur ID). + Der Block-Kopf im Beschreibungsteil nimmt den Doppelpunkt ebenfalls an. - Abhängigkeiten `:#a,#b` (SPEC §1/D37): EIN zusammenhängendes Token ohne Leerraum, nur **alleinstehend angesetzt** — `(:#a,#b)` bleibt Label (Zitier-Konvention wie `(#auth)`). `node.deps` sind **ID-Strings**, keine diff --git a/frontend/index.html b/frontend/index.html index 1893fbb..4779ce7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -121,7 +121,7 @@ Ab (M) gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.
Kommentare mit %% — als ganze Zeile oder am Zeilenende. Personen mit @name — erscheinen unten rechts am Knoten. - Knoten-ID mit #name — erscheint im Tooltip des Knotens. + Knoten-ID mit #name: vor dem Titel — erscheint im Tooltip des Knotens. 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 (”). diff --git a/frontend/public/llms.md b/frontend/public/llms.md index e0d1aac..5320eaf 100644 --- a/frontend/public/llms.md +++ b/frontend/public/llms.md @@ -91,6 +91,13 @@ One node per line. Everything except the label is optional. whole document; a duplicate ID warns, and references resolve to the first occurrence. Later `#tokens` stay label text; `C#` stays a label. Same character set as `@name`. + **Write it before the title, separated by a colon — `#auth: Backend`.** That + is the customary form. The colon is optional and is a separator in the text + only: it belongs neither to the ID nor to the label and is never rendered. + It is consumed only when it directly follows the ID *and* is followed by + whitespace or end of line — so a colon inside the label survives + (`#auth: Rule: token required`), and `#auth:#db` still reads as ID plus + dependency. Placement stays free: `Backend #auth` means the same thing. 6. `:#a,#b` — **dependencies**: one contiguous free-standing token — colon, then comma-separated IDs, each with `#`, **no spaces** (`:#a, #b` reads only `#a`, and the free-standing ` #b` would become the node ID!). @@ -129,7 +136,8 @@ One node per line. Everything except the label is optional. lines without a gate character. - **Long form:** a separator line of three or more dashes (`---`) ends the tree part. After it, an **unindented** line holding exactly one `#id` - opens a block; the **indented** lines below it are its text (blank lines = + (a trailing colon is tolerated) opens a block; the **indented** lines below + it are its text (blank lines = paragraph breaks). There is no closing fence — the description section runs to end of file. Stray lines there produce warnings, never silent loss. - Inside descriptions no token extraction happens — `(M)`, `@name`, `#id` @@ -141,6 +149,7 @@ One node per line. Everything except the label is optional. - Let indentation carry the hierarchy; keep sibling gates aligned. - Never mix `|` or `=` with other gates in one sibling group. - Decompose everything `(M)` or larger. +- Put a node ID in front of the title, separated by a colon: `#auth: Backend`. - Give a node its own status only; express "blocked by" with `:#…` dependencies instead of understating the status. - Mark nice-to-haves with `+` — otherwise they inflate the cheapest path. @@ -165,7 +174,7 @@ One node per line. Everything except the label is optional. - [ ] Android (M) - [ ] iOS (M) - [!] Backend (L) @ben @carla - - [ ] CMS integration #cms (M) + - [ ] #cms: CMS integration (M) | [ ] WordPress | [?] Headless CMS | [-] Custom build %% too much effort diff --git a/frontend/src/app.js b/frontend/src/app.js index 145276c..eba4bcd 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -24,7 +24,7 @@ const INITIAL = `%% Project structure – Sprint 14 - [ ] Android (M) - [ ] iOS (M) - [!] Backend (L) @ben @carla - - [ ] CMS integration #cms (M) + - [ ] #cms: CMS integration (M) | [ ] WordPress | [?] Headless CMS | [-] Custom build %% too much effort @@ -933,7 +933,11 @@ out.addEventListener('touchcancel', disarmPress); out.addEventListener('contextmenu', e => { if(pressTimer || armedEl) e.preventDefault(); }); /* Text -> Diagramm: Knoten der Cursor-Zeile hervorheben (`caretLine` steht oben). */ -function highlightCurrentNode(moved){ +/* `scroll` sagt, WIE ins Bild geholt wird: 'nearest' beim gewöhnlichen + Zeilenwechsel (nur so weit wie nötig — sonst ruckelte das Diagramm), + 'center' beim ausdrücklichen Alt+Klick, `false` gar nicht (Neubau). Die + Hervorhebung selbst ist in allen Fällen dieselbe, Puls eingeschlossen. */ +function highlightCurrentNode(moved, scroll = 'nearest'){ markCurrentLineNo(); /* die eine Stelle, an der die Cursor-Zeile neu gesetzt wird */ if(currentNodeEl) currentNodeEl.classList.remove('current', 'pulse'); currentNodeEl = caretLine == null @@ -954,7 +958,7 @@ function highlightCurrentNode(moved){ currentNodeEl.classList.remove('pulse'); void currentNodeEl.offsetWidth; currentNodeEl.classList.add('pulse'); - currentNodeEl.scrollIntoView({block:'nearest', inline:'nearest', behavior:'smooth'}); + if(scroll) currentNodeEl.scrollIntoView({block:scroll, inline:scroll, behavior:'smooth'}); } function caretLineOf(){ return src.value.slice(0, src.selectionStart).split('\n').length; @@ -979,10 +983,14 @@ function focusNodeOfCaret(){ const el = out.querySelector('.node[data-line="' + line + '"]'); if(!el) return; /* Kommentar, Leerzeile, ausgeblendet Verworfenes */ caretLine = line; - highlightCurrentNode(false); - /* Erst den Fokus (ohne eigenes Scrollen), dann bewusst zentrieren. */ + /* Erst den Fokus (ohne eigenes Scrollen), dann hervorheben und zentrieren. + Die Hervorhebung ist **dieselbe wie beim Zeilenwechsel** — Puls + eingeschlossen; der Unterschied ist allein, dass hier bewusst zentriert + wird statt nur so weit zu scrollen wie nötig. Deshalb `true` statt `false`: + die erste Fassung unterdrückte den Puls, und der Knoten kam ausgerechnet + bei der ausdrücklichen Geste stiller an als beim beiläufigen Tippen. */ el.focus({preventScroll: true}); - el.scrollIntoView({block:'center', inline:'center', behavior:'smooth'}); + highlightCurrentNode(true, 'center'); } src.addEventListener('click', e => { if(e.altKey) focusNodeOfCaret(); }); src.addEventListener('keydown', e => { @@ -1332,7 +1340,7 @@ const I18N = { hint_break:"Ab (M) gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.", hint_comment:"Kommentare mit %% — als ganze Zeile oder am Zeilenende.", hint_people:"Personen mit @name — erscheinen unten rechts am Knoten.", - hint_id:"Knoten-ID mit #name — erscheint im Tooltip des Knotens.", + hint_id:"Knoten-ID mit #name: vor dem Titel — erscheint im Tooltip des Knotens.", 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 (”).", @@ -1410,7 +1418,7 @@ const I18N = { hint_break:"From (M) on: break it down further — if the breakdown is missing, a placeholder appears in the diagram.", hint_comment:"Comments with %% — whole line or at the end of a line.", hint_people:"People with @name — shown at the bottom-right of the node.", - hint_id:"Node ID with #name — shown in the node's tooltip.", + hint_id:"Node ID with #name: before the title — shown in the node's tooltip.", 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 (”).", @@ -1488,7 +1496,7 @@ const I18N = { hint_break:"A partir de (M): sigue desglosando — si falta el desglose, aparece un marcador de posición en el diagrama.", hint_comment:"Comentarios con %% — línea completa o al final de la línea.", hint_people:"Personas con @nombre — aparecen abajo a la derecha del nodo.", - hint_id:"ID de nodo con #nombre — visible en el tooltip del nodo.", + hint_id:"ID de nodo con #nombre: delante del título — visible en el tooltip del nodo.", 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 (”).", @@ -1566,7 +1574,7 @@ const I18N = { hint_break:"À partir de (M) : décomposer davantage — si la décomposition manque, un espace réservé apparaît dans le diagramme.", hint_comment:"Commentaires avec %% — ligne entière ou en fin de ligne.", hint_people:"Personnes avec @nom — affichées en bas à droite du nœud.", - hint_id:"ID de nœud avec #nom — visible dans l’infobulle du nœud.", + hint_id:"ID de nœud avec #nom : devant le titre — visible dans l’infobulle du nœud.", 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 (”).", @@ -1644,7 +1652,7 @@ const I18N = { hint_break:"Od (M): dziel dalej — gdy brakuje podziału, w diagramie pojawia się symbol zastępczy.", hint_comment:"Komentarze z %% — cały wiersz lub na końcu wiersza.", hint_people:"Osoby z @nazwa — pokazywane w prawym dolnym rogu węzła.", - hint_id:"ID węzła przez #nazwa — widoczne w podpowiedzi węzła.", + hint_id:"ID węzła przez #nazwa: przed tytułem — widoczne w podpowiedzi węzła.", 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 (”).", @@ -1722,7 +1730,7 @@ const I18N = { hint_break:"С (M): дробите дальше — если декомпозиции нет, в диаграмме появляется заполнитель.", hint_comment:"Комментарии через %% — вся строка или в конце строки.", hint_people:"Люди через @имя — показываются справа внизу узла.", - hint_id:"ID узла через #имя — виден во всплывающей подсказке узла.", + hint_id:"ID узла через #имя: перед заголовком — виден во всплывающей подсказке узла.", hint_deps:"Зависимости через :#имя,#имя — видны в подсказке.", hint_eff:"Цвет узла показывает фактический статус (с учётом зависимостей); если собственный дальше, он показан меткой слева внизу.", hint_desc:"Описания: строка \" под узлом; длинный текст после --- как блок #id с отступом — оба в подсказке (”).", @@ -1800,7 +1808,7 @@ const I18N = { hint_break:"(M) से आगे: और विभाजित करें — विभाजन न होने पर आरेख में प्लेसहोल्डर दिखता है।", hint_comment:"%% से टिप्पणियाँ — पूरी पंक्ति या पंक्ति के अंत में।", hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।", - hint_id:"#नाम से नोड आईडी — नोड के टूलटिप में दिखती है।", + hint_id:"शीर्षक से पहले #नाम: के रूप में नोड आईडी — नोड के टूलटिप में दिखती है।", hint_deps:":#नाम,#नाम से निर्भरताएँ — टूलटिप में दिखती हैं।", hint_eff:"नोड का रंग प्रभावी स्थिति दिखाता है (निर्भरताओं सहित); यदि अपनी स्थिति आगे है, तो वह नीचे-बाएँ चिह्न के रूप में दिखती है।", hint_desc:"विवरण: नोड के नीचे \" पंक्ति; --- के बाद #id ब्लॉक में लंबा पाठ — दोनों टूलटिप में (”)।", @@ -1878,7 +1886,7 @@ const I18N = { hint_break:"从 (M) 起:继续细分——若缺少细分,图表中会出现占位符。", hint_comment:"用 %% 注释——整行或行尾。", hint_people:"用 @姓名 表示人员——显示在节点右下角。", - hint_id:"用 #名称 指定节点 ID——显示在节点提示中。", + hint_id:"用 #名称: 写在标题前指定节点 ID——显示在节点提示中。", hint_deps:"用 :#名称,#名称 表示依赖——显示在提示中。", hint_eff:"节点颜色显示实际状态(含依赖);若自身状态更靠前,会以左下角标记显示。", hint_desc:"描述:节点下方的 \" 行;--- 之后的缩进 #id 块为长文本——均显示在提示中(”)。", @@ -1956,7 +1964,7 @@ const I18N = { hint_break:"(M) 以上:さらに分解 — 分解がないと図にプレースホルダーが表示されます。", hint_comment:"%% でコメント — 行全体または行末。", hint_people:"@名前 で担当者 — ノードの右下に表示されます。", - hint_id:"#名前 でノード ID — ノードのツールチップに表示されます。", + hint_id:"タイトルの前に #名前: でノード ID — ノードのツールチップに表示されます。", hint_deps:":#名前,#名前 で依存関係 — ツールチップに表示されます。", hint_eff:"ノードの色は実効ステータス(依存関係込み)を示します。自身が先行している場合は左下のマークで表示されます。", hint_desc:"説明:ノード直下の \" 行。--- 以降は #id ブロック(字下げ)で長文 — どちらもツールチップに表示(”)。", diff --git a/frontend/src/parser.js b/frontend/src/parser.js index 2b1a153..e2328fc 100644 --- a/frontend/src/parser.js +++ b/frontend/src/parser.js @@ -80,7 +80,10 @@ export function parse(text){ if(descTarget !== SKIP) addDesc(descTarget, raw.trim()); return; } - const idm = raw.match(/^#([\p{L}\p{N}._-]+)\s*$/u); + /* Der trennende Doppelpunkt (siehe Knoten-ID unten) ist auch hier + zugelassen — ein Block-Kopf hat zwar keinen Titel dahinter, aber wer + die Schreibweise `#auth:` gewohnt ist, soll nicht darüber stolpern. */ + const idm = raw.match(/^#([\p{L}\p{N}._-]+):?\s*$/u); if(!idm){ /* Uneingerückt, keine ID-Zeile — bei einem versehentlichen Trenner mitten im Plan melden sich die verschluckten Knotenzeilen so @@ -129,9 +132,15 @@ export function parse(text){ bleibt Label, und das reservierte `:#a,#b` (§11) wird nicht gefressen. Nur der ERSTE Treffer (kein /g): weitere `#`-Token bleiben im Label stehen, dort wohnt die reservierte Ticket-Referenz. Zeichenmenge wie bei - `@name`; kein Lookbehind (Safari erst ab 16.4). */ + `@name`; kein Lookbehind (Safari erst ab 16.4). + Übliche Schreibweise ist die ID **vor** dem Titel, abgetrennt durch einen + Doppelpunkt: `#auth: Backend`. Der Doppelpunkt ist optional, gehört weder + zur ID noch zum Label und verschwindet hier. Er wird nur geschluckt, wenn + **Leerraum oder Zeilenende** folgt — sonst bliebe von `#auth:#db` nicht + die Abhängigkeit `:#db` übrig. Die ID-Erkennung selbst bleibt unberührt + (die Doppelpunkt-Gruppe ist optional, verlangt also nichts). */ let id = null; - rest = rest.replace(/(^|\s)#([\p{L}\p{N}._-]+)/u, (s, pre, g) => { id = g; return pre; }); + rest = rest.replace(/(^|\s)#([\p{L}\p{N}._-]+)(?::(?=\s|$))?/u, (s, pre, g) => { id = g; return pre; }); /* Abhängigkeiten `:#a,#b` (SPEC §1, D37): EIN zusammenhängendes Token ohne Leerraum, nur ALLEINSTEHEND ANGESETZT — eingeklammerte Erwähnungen wie `(:#auth,#api)` bleiben damit Label (dieselbe Zitier-Konvention wie bei diff --git a/frontend/tests/ids.test.js b/frontend/tests/ids.test.js index f8344aa..e1d20b5 100644 --- a/frontend/tests/ids.test.js +++ b/frontend/tests/ids.test.js @@ -49,6 +49,52 @@ describe('Parser — `#name` als Knoten-ID', () => { }); }); +/* Übliche Schreibweise: ID vor dem Titel, abgetrennt durch einen Doppelpunkt. + Der Doppelpunkt ist optional, gehört weder zur ID noch zum Label. */ +describe('ID vor dem Titel, `#id: Titel`', () => { + it('schluckt den trennenden Doppelpunkt', () => { + const [n] = roots(`[ ] #auth: Auth-Modul (M)`); + expect([n.id, n.label, n.size]).toEqual(['auth', 'Auth-Modul', 'M']); + }); + + it('kommt ohne Doppelpunkt zum selben Ergebnis', () => { + const mit = roots(`[ ] #auth: Auth-Modul (M)`)[0]; + const ohne = roots(`[ ] #auth Auth-Modul (M)`)[0]; + expect([mit.id, mit.label]).toEqual([ohne.id, ohne.label]); + }); + + it('lässt den Doppelpunkt auch am Zeilenende weg', () => { + const [n] = roots(`[ ] Titel danach #auth:`); + expect([n.id, n.label]).toEqual(['auth', 'Titel danach']); + }); + + it('vertägt sich mit Abhängigkeiten in derselben Zeile', () => { + const {roots: r, warnings} = parse(`[ ] #api: API\n[ ] #ui: Oberfläche :#api`); + expect(r.map(n => [n.id, n.label, n.deps])).toEqual([ + ['api', 'API', []], ['ui', 'Oberfläche', ['api']], + ]); + expect(warnings).toEqual([]); + }); + + it('lässt einen Doppelpunkt IM Label unberührt', () => { + const [n] = roots(`[ ] #auth: Regel: nur mit Token`); + expect([n.id, n.label]).toEqual(['auth', 'Regel: nur mit Token']); + }); + + it('schluckt nur einen Doppelpunkt mit folgendem Leerraum — `#auth:#db` bleibt Abhängigkeit', () => { + const {roots: r} = parse(`[ ] #db: Datenbank\n[ ] #auth:#db Login`); + expect(r[1].id).toBe('auth'); + expect(r[1].deps).toEqual(['db']); + expect(r[1].label).toBe('Login'); + }); + + it('nimmt den Doppelpunkt auch im Beschreibungsteil an', () => { + const {roots: r, warnings} = parse(`[ ] #auth: Auth\n---\n#auth:\n Erklärung.`); + expect(r[0].desc).toBe('Erklärung.'); + expect(warnings).toEqual([]); + }); +}); + describe('Doppelte IDs — Warnung an der späteren Zeile', () => { it('meldet die spätere Zeile und nennt die erste', () => { const {warnings} = parse(`[ ] A #auth\n[ ] B\n[ ] C #auth`);