feat: Knoten-ID vor dem Titel mit optionalem Doppelpunkt; Alt+Klick pulst mit

Ü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 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-23 07:31:45 +02:00
co-authored by Claude Fable 5
parent 099ee87bc1
commit ad2f3bab14
9 changed files with 190 additions and 32 deletions
+66
View File
@@ -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 `<details>`, 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 —
+12 -3
View File
@@ -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):
+5 -5
View File
@@ -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
+14 -3
View File
@@ -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
+1 -1
View File
@@ -121,7 +121,7 @@
Ab <code>(M)</code> gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.<br>
Kommentare mit <code>%%</code> — als ganze Zeile oder am Zeilenende.
Personen mit <code>@name</code> — erscheinen unten rechts am Knoten.
Knoten-ID mit <code>#name</code> — erscheint im Tooltip des Knotens.
Knoten-ID mit <code>#name:</code> vor dem Titel — erscheint im Tooltip des Knotens.
Abhängigkeiten mit <code>:#name,#name</code> — erscheinen im Tooltip.
Die Knotenfarbe zeigt den effektiven Status (mit Abhängigkeiten); ist der eigene weiter, steht er als Marke unten links.
Beschreibungen: <code>"</code> Zeile unter dem Knoten; Langtext hinter <code>---</code> als eingerückter <code>#id</code>-Block — beides im Tooltip (”).
+11 -2
View File
@@ -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
+23 -15
View File
@@ -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 linfobulle du nœud.",
hint_id:"ID de nœud avec #nom : devant le titre — visible dans linfobulle du nœud.",
hint_deps:"Dépendances avec :#nom,#nom — visibles dans linfobulle.",
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 linfobulle (”).",
@@ -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 ブロック(字下げ)で長文 — どちらもツールチップに表示(”)。",
+12 -3
View File
@@ -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
+46
View File
@@ -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`);