feat: Knotenbeschreibungen — "-Zeilen und ----Beschreibungsteil (SPEC §1/§9, D40)

Parser: `"`-Zeilen (Leerraum-Regel, nur ohne Zerlegungszeichen) hängen am
vorangehenden Knoten; hinter einem `---`-Trenner eröffnen #id-Zeilen
Blöcke, deren eingerückte Zeilen der Text sind (Leerzeilen = Absätze,
weitere Trenner bedeutungslos). Freitext ohne §1-Extraktion, nur %% fällt
weiter weg. Warnungen: unknownDesc (ID ohne Knoten, Blocktext wird still
geschluckt) und descStray je verwaister Zeile — ein versehentlicher
Trenner meldet die verschluckten Knotenzeilen zeilengenau. Anzeige im
Tooltip (Text zuerst, mehrzeilig) + aria-label, ”-Marke am Knoten
(nicht im Export — eine Marke ohne Ziel wäre Rauschen). i18n in allen
9 Sprachen; SPEC-§11-Abschnitt in §1/§9 überführt; 16 neue Tests
(tests/desc.test.js).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-22 19:01:02 +02:00
co-authored by Claude Fable 5
parent c4abd24ce8
commit a42710b38f
10 changed files with 320 additions and 67 deletions
+38
View File
@@ -1909,3 +1909,41 @@ heißt Kosten investiert, und investiert ist investiert, auch wenn
Abhängigkeiten den Knoten zurückhalten; und „Was ist neu?“ (D28) — der gelbe Abhängigkeiten den Knoten zurückhalten; und „Was ist neu?“ (D28) — der gelbe
Kranz meldet das `[^]` im Text, also den Deploy des Knotens selbst. Beide Kranz meldet das `[^]` im Text, also den Deploy des Knotens selbst. Beide
Prüfungen laufen im Parser bzw. auf dem Text und bleiben unberührt. Prüfungen laufen im Parser bzw. auf dem Text und bleiben unberührt.
## D40 — Knotenbeschreibungen gebaut: Tooltip statt Pop-up, ”-Marke, laute Strays
Die in D34 entschiedene Schreibweise (`"`-Zeilen, `---`-Beschreibungsteil) ist
umgesetzt (SPEC §1/§9). Die Bau-Entscheidungen:
**Anzeige im Tooltip, kein eigenes Pop-up.** SPEC §11 ließ „Tooltip oder
Pop-up“ offen. Ein Pop-up bräuchte eine eigene Geste (Klick ist der Link,
Alt+Klick der Sprung, der lange Druck ebenso — es bliebe nur ein weiteres
Klickziel neben dem Falt-Zeichen), Positionierung, Schließen-Logik und
Mobil-Verhalten. Der Tooltip kostet nichts davon: Der Beschreibungstext steht
**zuerst** (mehrzeilig — `title` zeigt Zeilenumbrüche), danach die
Kurz-Fakten (ID, Abhängigkeiten, Status). Bekannte Grenze: Auf Touch-Geräten
gibt es keine Tooltips — dort bleibt der Text vorerst nur im `aria-label`;
ein Pop-up kann später ergänzt werden, die Syntax ändert sich dadurch nicht.
**Auffindbarkeit: die ”-Marke.** Eine Beschreibung, die nur im Tooltip lebt,
wäre unsichtbar (die D25-Lehre: was niemand sieht, ist keine Funktion). Ein
Knoten mit Beschreibung trägt deshalb ein kleines ” hinter dem Label — es
spiegelt das `"`-Zeichen der Notation, wie die `[x]`-Marke (D39) deren
Statusboxen spiegelt; kein neues Symbol zu lernen. **Nicht im Export**: Der
Text selbst kann im statischen Bild nicht erscheinen, eine Marke ohne Ziel
wäre Rauschen — anders als „▸ n“ (D38), das eine nachprüfbare Aussage über
verborgene Knoten trifft. Screenreader bekommen den Text im `aria-label`.
**Strays warnen einzeln, Blöcke unter unbekannter ID schlucken still.** Die
beiden Fehlerfälle sind verschieden: Eine verwaiste Zeile (uneingerückt und
keine ID-Zeile, oder eingerückt ohne offenen Block) ist wahrscheinlich ein
**verrutschter Knoten** — genau der Fall des versehentlichen Trenners mitten
im Plan — und meldet sich je Zeile (`descStray`), damit nichts still
verschwindet. Ein Block unter einer **unbekannten ID** dagegen ist als
Beschreibung erkennbar und schon mit `unknownDesc` gemeldet; seine Textzeilen
zusätzlich einzeln anzuprangern wäre nur Lärm (SKIP-Ziel im Parser).
**Freitext heißt Freitext:** In Beschreibungszeilen findet keine
§1-Extraktion statt — `(M)`, `@name`, `#id` oder URLs im Text bleiben Text.
Einzige Ausnahme ist `%%`: Der Kommentar fällt im ganzen Dokument als
Erstes weg (einheitliche Regel, §1) — so lassen sich auch Beschreibungen
kommentieren.
+42 -61
View File
@@ -94,6 +94,37 @@ hat etwas, das ein Cursor nicht hat: **alle** sehen dieselbe Stelle.
- Sie bleibt im Text stehen, bis jemand sie löscht; ein Werkzeug entfernt sie - Sie bleibt im Text stehen, bis jemand sie löscht; ein Werkzeug entfernt sie
nicht von selbst. nicht von selbst.
**Beschreibungszeilen (`"`) und Beschreibungsteil (`---`)** — erläuternder
Text zu einem Knoten (Anzeige: §9):
- **Kurzform:** Eine Zeile, deren erstes Zeichen nach der Einrückung `"` ist
(**mit folgendem Leerraum**, Leerraum-Regel wie bei `=` und `>`/`<` — ein
Label wie `"Zitat"` bleibt ein Label), ist **Beschreibung, kein Knoten**.
Sie gehört zum **vorangehenden Knoten**; mehrere `"`-Zeilen setzen dieselbe
Beschreibung fort. Die Einrückung der Zeile hat keine Bedeutung (Konvention:
wie ein Kind eingerückt); ohne vorangehenden Knoten: Warnung `descStray`.
Nur auf Zeilen **ohne** Zerlegungszeichen — `- " Zitat" …` bleibt ein
gewöhnlicher Knoten.
- **Langform:** Eine Zeile aus **drei oder mehr `-`** (nur Bindestriche,
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
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
Bedeutung.
- **Fehlertoleranz:** ID ohne Knoten → `unknownDesc`; uneingerückte
Nicht-ID-Zeilen und eingerückte Zeilen ohne offenen Block → `descStray`, je
mit Zeilennummer. Ein versehentlicher Trenner mitten im Plan meldet die
verschluckten Knotenzeilen so zeilengenau selbst.
- Der Inhalt ist **Freitext** — die Extraktion aus diesem Abschnitt findet
darin nicht statt; nur `%%`-Kommentare fallen weiterhin als Erstes weg.
Mehrere Blöcke und Kurz- und Langform zum selben Knoten werden in
Dokumentreihenfolge aneinandergehängt.
- Bewusste Verhaltensänderungen: `---` ergab früher einen Knoten mit Label
`--`; eine zeichenlose `" `-Zeile war früher ein Wurzelknoten.
Referenz-Regex der Implementierung: Referenz-Regex der Implementierung:
``` ```
@@ -259,6 +290,14 @@ Stück zwischen Elternknoten und erstem Abzweig — eine kleine **„1“-Plaket
(weißer Kreis mit grauem Rand, graue Ziffer): „genau eine“. Sie erscheint auch (weißer Kreis mit grauem Rand, graue Ziffer): „genau eine“. Sie erscheint auch
im Grafikexport. Siehe D35. im Grafikexport. Siehe D35.
**Knotenbeschreibungen (§1)** erscheinen im **Tooltip** des Knotens (zuerst
der Text, dann die Kurz-Fakten wie ID und Abhängigkeiten) und im
`aria-label`. Ein Knoten mit Beschreibung trägt eine kleine **”-Marke** hinter
dem Label — sie macht die sonst unsichtbare Beschreibung auffindbar und
spiegelt das `"`-Zeichen der Notation. Die Marke erscheint **nicht** im
Grafikexport: Der Text selbst kann dort nicht angezeigt werden, eine Marke
ohne Ziel wäre Rauschen. Siehe D40.
**Die Knotenfarbe zeigt den effektiven Status (§4)**, nicht den intrinsischen — **Die Knotenfarbe zeigt den effektiven Status (§4)**, nicht den intrinsischen —
das Diagramm beantwortet „wie weit ist das wirklich?“. Wo der eigene Status das Diagramm beantwortet „wie weit ist das wirklich?“. Wo der eigene Status
**weiter** ist als der effektive (der Knoten wird von Abhängigkeiten **weiter** ist als der effektive (der Knoten wird von Abhängigkeiten
@@ -713,67 +752,9 @@ die Rechnung schwerer als heute — siehe D34.
### Knotenbeschreibungen (`"` und `---`) ### Knotenbeschreibungen (`"` und `---`)
Erläuternder Text zu einem Knoten, im Diagramm als Tooltip oder Pop-up. **Umgesetzt** — Schreibweise in §1 (Kurzform als `"`-Zeile, Langform als
Beide Schreibweisen sind **entschieden** (D34-Nachtrag), noch nicht gebaut. ID-Blöcke im `---`-Beschreibungsteil), Anzeige in §9 (Tooltip, ”-Marke).
Der Kern des Problems war: Einrückung bedeutet Hierarchie (§2) — eine Entscheidung und verworfene Alternativen: D34-Nachtrag, D40.
eingerückte Folgezeile **ist** ein Kindknoten, und jede Zeile ohne Zeichen ist
ein Wurzelknoten. Die Kurzform löst das mit einem eigenen Zeichen, die
Langform, indem sie den Geltungsbereich dieser Regeln beendet.
**Kurzform — `"`-Zeile unter dem Knoten:**
```
- [~] Auth-Dienst #auth (L)
" Kapselt Login, Tokens und Rollen.
```
- Eine Zeile, deren erstes Zeichen nach der Einrückung `"` ist (**mit
folgendem Leerraum**, Leerraum-Regel wie bei `=` und `>`/`<` — ein Label wie
`"Zitat"` bleibt damit ein Label), ist **Beschreibung, kein Knoten**.
- Sie gehört zum **vorangehenden Knoten**; mehrere `"`-Zeilen hintereinander
setzen dieselbe Beschreibung fort. Die Einrückung der `"`-Zeile hat keine
Bedeutung (Konvention: wie ein Kind eingerückt). Eine `"`-Zeile ohne
vorangehenden Knoten ist eine Warnung mit Zeilennummer.
- Der Inhalt ist **Freitext**: Die §1-Extraktion (Größe, Tags, IDs, URL …)
findet darin nicht statt. Nur `%%`-Kommentare werden weiterhin entfernt —
der Kommentar fällt als Erstes (§1), einheitlich im ganzen Dokument.
- Nur auf Zeilen **ohne** Zerlegungszeichen: `- " Zitat" als Label` bleibt ein
gewöhnlicher Knoten.
**Langform — Beschreibungsteil hinter `---`:**
```
[~] Auth-Dienst #auth (L)
- [ ] Login
---
#auth
Hintergrund, Entscheidungen, Verweise — freier Text,
zeilenweise eingerückt. Leerzeilen trennen Absätze.
#api
Der nächste Block.
```
- Eine Zeile aus **drei oder mehr `-`** (nur Bindestriche, umgebender Leerraum
erlaubt) trennt den Baumteil vom **Beschreibungsteil**; alles danach gehört
zu ihm (YAML-/Frontmatter-Konvention: „ab hier etwas anderes“). Es gibt
keinen Schlusszaun — er kann also nicht vergessen werden.
- Im Beschreibungsteil eröffnet eine **uneingerückte Zeile mit genau einer
Knoten-ID** (`#auth`, allein) einen Block; die **eingerückten** Zeilen
darunter sind sein Text (um den Einzug gekürzt; Leerzeilen bleiben als
Absatztrenner erhalten). Anführungszeichen braucht es hier nicht — die
Wurzelknoten-Regel (§2) gilt hinter dem Trenner nicht mehr.
- **Fehlertoleranz:** Eine ID ohne zugehörigen Knoten → Warnung (wie
`unknownDep`). Eine uneingerückte Zeile, die keine ID-Zeile ist, oder
eingerückte Zeilen ohne eröffneten Block → Warnung mit Zeilennummer. Ein
versehentlicher Trenner mitten im Plan fällt dadurch sofort auf: Die
verschluckten Knotenzeilen werden zeilengenau gemeldet.
- Mehrere Blöcke zur selben ID werden in Dokumentreihenfolge aneinander-
gehängt; Kurz- und Langform zum selben Knoten dürfen koexistieren
(Anzeige-Details werden beim Bauen festgelegt). Weitere `---`-Zeilen im
Beschreibungsteil haben keine Bedeutung.
- Bewusste Verhaltensänderung: Eine Zeile `---` ergab bisher einen Knoten mit
Label `--`; sie ist jetzt der Trenner.
## 12. Dateiendung ## 12. Dateiendung
+4 -2
View File
@@ -111,8 +111,10 @@ entscheiden, **bevor** Code entsteht.
realisierter Alternative; Darstellung als any-of plus „1“-Plakette an der realisierter Alternative; Darstellung als any-of plus „1“-Plakette an der
Sammelleiste (auch im Grafikexport); Legende + i18n in 9 Sprachen; Sammelleiste (auch im Grafikexport); Legende + i18n in 9 Sprachen;
`tests/xor.test.js`. `tests/xor.test.js`.
- [ ] Knotenbeschreibungen: Schreibweise für kurze und lange Form festlegen - [x] Knotenbeschreibungen: Schreibweise für kurze und lange Form festlegen
(Einrückung ist bereits Hierarchie), dann Tooltip/Pop-up. (Einrückung ist bereits Hierarchie), dann Tooltip/Pop-up.
→ Schreibweise entschieden (D34-Nachtrag): Kurzform als `"`-Zeile unter → Schreibweise entschieden (D34-Nachtrag): Kurzform als `"`-Zeile unter
dem Knoten (Leerraum-Regel), Langform als eingerückte ID-Blöcke hinter dem Knoten (Leerraum-Regel), Langform als eingerückte ID-Blöcke hinter
einem `---`-Trenner (SPEC §11). Zu bauen: Parser + Anzeige. einem `---`-Trenner (SPEC §11). Umgesetzt (D40): Parser (`desc` am
Knoten, Warnungen `unknownDesc`/`descStray`), Anzeige im Tooltip +
`aria-label` mit ”-Marke am Knoten; `tests/desc.test.js`.
+1
View File
@@ -123,6 +123,7 @@
Knoten-ID mit <code>#name</code> — erscheint im Tooltip des Knotens. Knoten-ID mit <code>#name</code> — erscheint im Tooltip des Knotens.
Abhängigkeiten mit <code>:#name,#name</code> — erscheinen im Tooltip. 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. 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 (”).
Falten: <code>- &gt; [x] …</code> startet eingeklappt, <code>&lt;</code> holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→). Falten: <code>- &gt; [x] …</code> startet eingeklappt, <code>&lt;</code> holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→).
</div> </div>
</div> </div>
+29 -1
View File
@@ -403,7 +403,7 @@ function diagramToSvg(){
§9/D38 das Bild darf keine Vollständigkeit behaupten); das offener §9/D38 das Bild darf keine Vollständigkeit behaupten); das offener
Knoten ist Bedienelement und fällt weg. */ Knoten ist Bedienelement und fällt weg. */
const stripFold = node.classList.contains('folded') ? '' : ',.fold'; const stripFold = node.classList.contains('folded') ? '' : ',.fold';
clone.querySelectorAll('.size,.tags,.ext,.risk,.ownst' + stripFold).forEach(e => e.remove()); clone.querySelectorAll('.size,.tags,.ext,.risk,.ownst,.desc-mark' + stripFold).forEach(e => e.remove());
const label = clone.textContent.replace(/\s+/g,' ').trim(); const label = clone.textContent.replace(/\s+/g,' ').trim();
const deco = cs.textDecorationLine.includes('line-through') ? ' text-decoration="line-through"' : ''; const deco = cs.textDecorationLine.includes('line-through') ? ' text-decoration="line-through"' : '';
parts.push(`<text x="${b.cx.toFixed(1)}" y="${(b.cy+5).toFixed(1)}" text-anchor="middle" fill="${cs.color}" font-size="14" font-weight="${cs.fontWeight}"${deco}>${esc(label)}</text>`); parts.push(`<text x="${b.cx.toFixed(1)}" y="${(b.cy+5).toFixed(1)}" text-anchor="middle" fill="${cs.color}" font-size="14" font-weight="${cs.fontWeight}"${deco}>${esc(label)}</text>`);
@@ -1172,6 +1172,8 @@ const I18N = {
xorConflictWarn:"Zeile {line}: „{label}“ ist eine weitere realisierte Alternative — eine =-Gruppe erlaubt genau eine.", xorConflictWarn:"Zeile {line}: „{label}“ ist eine weitere realisierte Alternative — eine =-Gruppe erlaubt genau eine.",
duplicateIdWarn:"Zeile {line}: Die ID #{id} ist schon vergeben (Zeile {firstLine}).", duplicateIdWarn:"Zeile {line}: Die ID #{id} ist schon vergeben (Zeile {firstLine}).",
unknownDepWarn:"Zeile {line}: Abhängigkeit #{id} — es gibt keinen Knoten mit dieser ID.", unknownDepWarn:"Zeile {line}: Abhängigkeit #{id} — es gibt keinen Knoten mit dieser ID.",
unknownDescWarn:"Zeile {line}: Beschreibung für #{id} — es gibt keinen Knoten mit dieser ID.",
descStrayWarn:"Zeile {line}: Beschreibungszeile ohne Bezug — ihr fehlt der Knoten bzw. der #id-Block davor.",
st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich", st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich",
st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen", st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen",
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.", unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
@@ -1192,6 +1194,7 @@ const I18N = {
hint_id:"Knoten-ID mit #name — erscheint im Tooltip des Knotens.", hint_id:"Knoten-ID mit #name — erscheint im Tooltip des Knotens.",
hint_deps:"Abhängigkeiten mit :#name,#name — erscheinen im Tooltip.", 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_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." hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile; Alt+Klick im Text holt den Knoten ins Bild."
}, },
@@ -1244,6 +1247,8 @@ const I18N = {
xorConflictWarn:"Line {line}: “{label}” is another realized alternative — an = group allows exactly one.", xorConflictWarn:"Line {line}: “{label}” is another realized alternative — an = group allows exactly one.",
duplicateIdWarn:"Line {line}: ID #{id} is already taken (line {firstLine}).", duplicateIdWarn:"Line {line}: ID #{id} is already taken (line {firstLine}).",
unknownDepWarn:"Line {line}: dependency #{id} — no node has this ID.", unknownDepWarn:"Line {line}: dependency #{id} — no node has this ID.",
unknownDescWarn:"Line {line}: description for #{id} — no node has this ID.",
descStrayWarn:"Line {line}: description line with nothing to attach to — it needs a node or an #id block before it.",
st_idee:"idea", st_geplant:"planned", st_arbeit:"in progress", st_durchstich:"walking skeleton", st_idee:"idea", st_geplant:"planned", st_arbeit:"in progress", st_durchstich:"walking skeleton",
st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded", st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded",
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.", unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
@@ -1264,6 +1269,7 @@ const I18N = {
hint_id:"Node ID with #name — shown in the node's tooltip.", hint_id:"Node ID with #name — shown in the node's tooltip.",
hint_deps:"Dependencies with :#name,#name — shown in the 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_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." 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."
}, },
@@ -1316,6 +1322,8 @@ const I18N = {
xorConflictWarn:"Línea {line}: «{label}» es otra alternativa realizada — un grupo = permite exactamente una.", xorConflictWarn:"Línea {line}: «{label}» es otra alternativa realizada — un grupo = permite exactamente una.",
duplicateIdWarn:"Línea {line}: la ID #{id} ya está asignada (línea {firstLine}).", duplicateIdWarn:"Línea {line}: la ID #{id} ya está asignada (línea {firstLine}).",
unknownDepWarn:"Línea {line}: dependencia #{id} — ningún nodo tiene esta ID.", unknownDepWarn:"Línea {line}: dependencia #{id} — ningún nodo tiene esta ID.",
unknownDescWarn:"Línea {line}: descripción para #{id} — ningún nodo tiene esta ID.",
descStrayWarn:"Línea {line}: línea de descripción sin referencia — le falta un nodo o un bloque #id delante.",
st_idee:"idea", st_geplant:"planificado", st_arbeit:"en curso", st_durchstich:"prototipo funcional", st_idee:"idea", st_geplant:"planificado", st_arbeit:"en curso", st_durchstich:"prototipo funcional",
st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado", st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado",
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.", unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
@@ -1336,6 +1344,7 @@ const I18N = {
hint_id:"ID de nodo con #nombre — visible en el tooltip del nodo.", hint_id:"ID de nodo con #nombre — visible en el tooltip del nodo.",
hint_deps:"Dependencias con :#nombre,#nombre — visibles en el tooltip.", 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_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." 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."
}, },
@@ -1388,6 +1397,8 @@ const I18N = {
xorConflictWarn:"Ligne {line} : « {label} » est une alternative réalisée de plus — un groupe = nen autorise quune seule.", xorConflictWarn:"Ligne {line} : « {label} » est une alternative réalisée de plus — un groupe = nen autorise quune seule.",
duplicateIdWarn:"Ligne {line} : lID #{id} est déjà attribué (ligne {firstLine}).", duplicateIdWarn:"Ligne {line} : lID #{id} est déjà attribué (ligne {firstLine}).",
unknownDepWarn:"Ligne {line} : dépendance #{id} — aucun nœud ne porte cet ID.", unknownDepWarn:"Ligne {line} : dépendance #{id} — aucun nœud ne porte cet ID.",
unknownDescWarn:"Ligne {line} : description pour #{id} — aucun nœud ne porte cet ID.",
descStrayWarn:"Ligne {line} : ligne de description sans rattachement — il lui manque un nœud ou un bloc #id avant.",
st_idee:"idée", st_geplant:"planifié", st_arbeit:"en cours", st_durchstich:"squelette fonctionnel", st_idee:"idée", st_geplant:"planifié", st_arbeit:"en cours", st_durchstich:"squelette fonctionnel",
st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné", st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné",
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.", unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
@@ -1408,6 +1419,7 @@ const I18N = {
hint_id:"ID de nœud avec #nom — visible dans linfobulle du nœud.", hint_id:"ID de nœud avec #nom — visible dans linfobulle du nœud.",
hint_deps:"Dépendances avec :#nom,#nom — visibles dans linfobulle.", 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_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 (”).",
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." 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."
}, },
@@ -1460,6 +1472,8 @@ const I18N = {
xorConflictWarn:"Wiersz {line}: „{label}” to kolejna zrealizowana alternatywa — grupa = dopuszcza dokładnie jedną.", xorConflictWarn:"Wiersz {line}: „{label}” to kolejna zrealizowana alternatywa — grupa = dopuszcza dokładnie jedną.",
duplicateIdWarn:"Wiersz {line}: ID #{id} jest już zajęte (wiersz {firstLine}).", duplicateIdWarn:"Wiersz {line}: ID #{id} jest już zajęte (wiersz {firstLine}).",
unknownDepWarn:"Wiersz {line}: zależność #{id} — żaden węzeł nie ma tego ID.", unknownDepWarn:"Wiersz {line}: zależność #{id} — żaden węzeł nie ma tego ID.",
unknownDescWarn:"Wiersz {line}: opis dla #{id} — żaden węzeł nie ma tego ID.",
descStrayWarn:"Wiersz {line}: wiersz opisu bez odniesienia — brakuje węzła lub bloku #id przed nim.",
st_idee:"pomysł", st_geplant:"zaplanowane", st_arbeit:"w toku", st_durchstich:"działający szkielet", st_idee:"pomysł", st_geplant:"zaplanowane", st_arbeit:"w toku", st_durchstich:"działający szkielet",
st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone", st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone",
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.", unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
@@ -1480,6 +1494,7 @@ const I18N = {
hint_id:"ID węzła przez #nazwa — widoczne w podpowiedzi węzła.", hint_id:"ID węzła przez #nazwa — widoczne w podpowiedzi węzła.",
hint_deps:"Zależności przez :#nazwa,#nazwa — widoczne w podpowiedzi.", 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_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." 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."
}, },
@@ -1532,6 +1547,8 @@ const I18N = {
xorConflictWarn:"Строка {line}: «{label}» — ещё одна реализованная альтернатива, а группа = допускает ровно одну.", xorConflictWarn:"Строка {line}: «{label}» — ещё одна реализованная альтернатива, а группа = допускает ровно одну.",
duplicateIdWarn:"Строка {line}: ID #{id} уже занят (строка {firstLine}).", duplicateIdWarn:"Строка {line}: ID #{id} уже занят (строка {firstLine}).",
unknownDepWarn:"Строка {line}: зависимость #{id} — узла с таким ID нет.", unknownDepWarn:"Строка {line}: зависимость #{id} — узла с таким ID нет.",
unknownDescWarn:"Строка {line}: описание для #{id} — узла с таким ID нет.",
descStrayWarn:"Строка {line}: строка описания без привязки — перед ней нет узла или блока #id.",
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип", st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено", st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.", unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
@@ -1552,6 +1569,7 @@ const I18N = {
hint_id:"ID узла через #имя — виден во всплывающей подсказке узла.", hint_id:"ID узла через #имя — виден во всплывающей подсказке узла.",
hint_deps:"Зависимости через :#имя,#имя — видны в подсказке.", hint_deps:"Зависимости через :#имя,#имя — видны в подсказке.",
hint_eff:"Цвет узла показывает фактический статус (с учётом зависимостей); если собственный дальше, он показан меткой слева внизу.", hint_eff:"Цвет узла показывает фактический статус (с учётом зависимостей); если собственный дальше, он показан меткой слева внизу.",
hint_desc:"Описания: строка \" под узлом; длинный текст после --- как блок #id с отступом — оба в подсказке (”).",
hint_fold:"Сворачивание: - > [x] … открывается свёрнутым, < возвращает; ▾/▸ на узле переключает (клавиши: ←/→).", hint_fold:"Сворачивание: - > [x] … открывается свёрнутым, < возвращает; ▾/▸ на узле переключает (клавиши: ←/→).",
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме." hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме."
}, },
@@ -1604,6 +1622,8 @@ const I18N = {
xorConflictWarn:"पंक्ति {line}: „{label}“ एक और साकार विकल्प है — = समूह में केवल एक की अनुमति है।", xorConflictWarn:"पंक्ति {line}: „{label}“ एक और साकार विकल्प है — = समूह में केवल एक की अनुमति है।",
duplicateIdWarn:"पंक्ति {line}: आईडी #{id} पहले से प्रयुक्त है (पंक्ति {firstLine})।", duplicateIdWarn:"पंक्ति {line}: आईडी #{id} पहले से प्रयुक्त है (पंक्ति {firstLine})।",
unknownDepWarn:"पंक्ति {line}: निर्भरता #{id} — इस आईडी वाला कोई नोड नहीं है।", unknownDepWarn:"पंक्ति {line}: निर्भरता #{id} — इस आईडी वाला कोई नोड नहीं है।",
unknownDescWarn:"पंक्ति {line}: #{id} के लिए विवरण — इस आईडी वाला कोई नोड नहीं है।",
descStrayWarn:"पंक्ति {line}: विवरण पंक्ति बिना संदर्भ — इससे पहले कोई नोड या #id ब्लॉक नहीं है।",
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा", st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत", st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।", unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
@@ -1624,6 +1644,7 @@ const I18N = {
hint_id:"#नाम से नोड आईडी — नोड के टूलटिप में दिखती है।", hint_id:"#नाम से नोड आईडी — नोड के टूलटिप में दिखती है।",
hint_deps:":#नाम,#नाम से निर्भरताएँ — टूलटिप में दिखती हैं।", hint_deps:":#नाम,#नाम से निर्भरताएँ — टूलटिप में दिखती हैं।",
hint_eff:"नोड का रंग प्रभावी स्थिति दिखाता है (निर्भरताओं सहित); यदि अपनी स्थिति आगे है, तो वह नीचे-बाएँ चिह्न के रूप में दिखती है।", hint_eff:"नोड का रंग प्रभावी स्थिति दिखाता है (निर्भरताओं सहित); यदि अपनी स्थिति आगे है, तो वह नीचे-बाएँ चिह्न के रूप में दिखती है।",
hint_desc:"विवरण: नोड के नीचे \" पंक्ति; --- के बाद #id ब्लॉक में लंबा पाठ — दोनों टूलटिप में (”)।",
hint_fold:"फ़ोल्डिंग: - > [x] … समेटा हुआ खुलता है, < वापस लाता है; नोड पर ▾/▸ टॉगल करता है (कीबोर्ड: ←/→)।", hint_fold:"फ़ोल्डिंग: - > [x] … समेटा हुआ खुलता है, < वापस लाता है; नोड पर ▾/▸ टॉगल करता है (कीबोर्ड: ←/→)।",
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।" hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।"
}, },
@@ -1676,6 +1697,8 @@ const I18N = {
xorConflictWarn:"第 {line} 行:「{label}」是又一个已实现的备选项——= 组只允许恰好一个。", xorConflictWarn:"第 {line} 行:「{label}」是又一个已实现的备选项——= 组只允许恰好一个。",
duplicateIdWarn:"第 {line} 行:ID #{id} 已被占用(第 {firstLine} 行)。", duplicateIdWarn:"第 {line} 行:ID #{id} 已被占用(第 {firstLine} 行)。",
unknownDepWarn:"第 {line} 行:依赖 #{id}——没有节点使用此 ID。", unknownDepWarn:"第 {line} 行:依赖 #{id}——没有节点使用此 ID。",
unknownDescWarn:"第 {line} 行:#{id} 的描述——没有节点使用此 ID。",
descStrayWarn:"第 {line} 行:描述行没有归属——前面缺少节点或 #id 块。",
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架", st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃", st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。", unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
@@ -1696,6 +1719,7 @@ const I18N = {
hint_id:"用 #名称 指定节点 ID——显示在节点提示中。", hint_id:"用 #名称 指定节点 ID——显示在节点提示中。",
hint_deps:"用 :#名称,#名称 表示依赖——显示在提示中。", hint_deps:"用 :#名称,#名称 表示依赖——显示在提示中。",
hint_eff:"节点颜色显示实际状态(含依赖);若自身状态更靠前,会以左下角标记显示。", hint_eff:"节点颜色显示实际状态(含依赖);若自身状态更靠前,会以左下角标记显示。",
hint_desc:"描述:节点下方的 \" 行;--- 之后的缩进 #id 块为长文本——均显示在提示中(”)。",
hint_fold:"折叠:- > [x] … 打开时即折叠,< 将其展开;节点上的 ▾/▸ 切换(键盘:←/→)。", hint_fold:"折叠:- > [x] … 打开时即折叠,< 将其展开;节点上的 ▾/▸ 切换(键盘:←/→)。",
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。" hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。"
}, },
@@ -1748,6 +1772,8 @@ const I18N = {
xorConflictWarn:"{line} 行目:「{label}」も実現済みの選択肢です — = グループで実現できるのは 1 つだけです。", xorConflictWarn:"{line} 行目:「{label}」も実現済みの選択肢です — = グループで実現できるのは 1 つだけです。",
duplicateIdWarn:"{line} 行目:ID #{id} は既に使われています({firstLine} 行目)。", duplicateIdWarn:"{line} 行目:ID #{id} は既に使われています({firstLine} 行目)。",
unknownDepWarn:"{line} 行目:依存 #{id} — この ID を持つノードはありません。", unknownDepWarn:"{line} 行目:依存 #{id} — この ID を持つノードはありません。",
unknownDescWarn:"{line} 行目:#{id} の説明 — この ID を持つノードはありません。",
descStrayWarn:"{line} 行目:説明行の帰属先がありません — 直前にノードまたは #id ブロックが必要です。",
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン", st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄", st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。", unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
@@ -1768,6 +1794,7 @@ const I18N = {
hint_id:"#名前 でノード ID — ノードのツールチップに表示されます。", hint_id:"#名前 でノード ID — ノードのツールチップに表示されます。",
hint_deps:":#名前,#名前 で依存関係 — ツールチップに表示されます。", hint_deps:":#名前,#名前 で依存関係 — ツールチップに表示されます。",
hint_eff:"ノードの色は実効ステータス(依存関係込み)を示します。自身が先行している場合は左下のマークで表示されます。", hint_eff:"ノードの色は実効ステータス(依存関係込み)を示します。自身が先行している場合は左下のマークで表示されます。",
hint_desc:"説明:ノード直下の \" 行。--- 以降は #id ブロック(字下げ)で長文 — どちらもツールチップに表示(”)。",
hint_fold:"折りたたみ:- > [x] … は折りたたんだ状態で開き、< は呼び戻します。ノードの ▾/▸ で切替(キー:←/→)。", hint_fold:"折りたたみ:- > [x] … は折りたたんだ状態で開き、< は呼び戻します。ノードの ▾/▸ で切替(キー:←/→)。",
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。" hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。"
} }
@@ -1804,6 +1831,7 @@ function buildHint(){
${esc(t('hint_id'))} ${esc(t('hint_id'))}
${esc(t('hint_deps'))} ${esc(t('hint_deps'))}
${esc(t('hint_eff'))} ${esc(t('hint_eff'))}
${esc(t('hint_desc'))}
${esc(t('hint_fold'))} ${esc(t('hint_fold'))}
<code>!!!</code>&nbsp; ${esc(t('hint_focus'))} <code>!!!</code>&nbsp; ${esc(t('hint_focus'))}
<div class="hint-op">${esc(t('hint_jump'))}</div>`; <div class="hint-op">${esc(t('hint_jump'))}</div>`;
+67 -2
View File
@@ -27,7 +27,10 @@ const REALIZED = new Set(['arbeit', 'durchstich', 'fertig', 'prod']);
/* Parst den Notationstext zu { roots, warnings }. /* Parst den Notationstext zu { roots, warnings }.
Jeder Knoten: {label, type:'and'|'or'|'xor', optional, fold, status, url, Jeder Knoten: {label, type:'and'|'or'|'xor', optional, fold, status, url,
size, tags, id, deps, focus, children, line}. size, tags, id, deps, desc, focus, children, line}.
`desc` (SPEC §11/D40) ist der Beschreibungstext: `"`-Zeilen unter dem
Knoten (Kurzform) und ID-Blöcke aus dem `---`-Beschreibungsteil (Langform),
in Dokumentreihenfolge mit Zeilenumbrüchen zusammengefügt; null ohne.
`fold` ('>'|'<'|null, SPEC §1/D38) ist nur der ANFANGSZUSTAND der Faltung `fold` ('>'|'<'|null, SPEC §1/D38) ist nur der ANFANGSZUSTAND der Faltung
den wirksamen Zustand rechnet `initialCollapsed()` in model.js. den wirksamen Zustand rechnet `initialCollapsed()` in model.js.
`deps` sind ID-Strings, keine Knoten-Referenzen aufgelöst wird erst beim `deps` sind ID-Strings, keine Knoten-Referenzen aufgelöst wird erst beim
@@ -46,10 +49,62 @@ export function parse(text){
const stack = [{node:virtualRoot, width:-1}]; const stack = [{node:virtualRoot, width:-1}];
const warnings = []; const warnings = [];
const idLines = new Map(); /* Knoten-ID -> Zeile der ersten Vergabe (D36) */ const idLines = new Map(); /* Knoten-ID -> Zeile der ersten Vergabe (D36) */
const idNodes = new Map(); /* Knoten-ID -> Knoten der ersten Vergabe */
/* Beschreibungen (SPEC §11, D40): gesammelt je Knoten, am Ende zu `desc`
zusammengefügt. `lastNode` trägt die Kurzform-Zuordnung (vorangehender
Knoten"), `descTarget` den offenen Block des Beschreibungsteils; SKIP
schluckt Blocktext unter einer unbekannten ID, ohne je Zeile zu warnen. */
const descLines = new Map();
const SKIP = {};
let lastNode = null, inDesc = false, descTarget = null;
const addDesc = (node, text) => {
let arr = descLines.get(node);
if(!arr) descLines.set(node, arr = []);
if(text === '' && (!arr.length || arr[arr.length-1] === '')) return;
arr.push(text);
};
text.split('\n').forEach((raw, i) => { text.split('\n').forEach((raw, i) => {
raw = raw.replace(/%%.*$/, ''); /* %%-Kommentare entfernen (Mermaid-Konvention) */ raw = raw.replace(/%%.*$/, ''); /* %%-Kommentare entfernen (Mermaid-Konvention) */
/* Trenner `---` (SPEC §11, D40): drei oder mehr Bindestriche, umgebender
Leerraum erlaubt ab hier gilt der Beschreibungsteil. Es gibt keinen
Schlusszaun; weitere Trennzeilen darin haben keine Bedeutung. */
if(/^[ \t]*-{3,}[ \t]*$/.test(raw)){ inDesc = true; return; }
if(inDesc){
if(!raw.trim()){
if(descTarget && descTarget !== SKIP) addDesc(descTarget, ''); /* Absatztrenner */
return;
}
if(/^[ \t]/.test(raw)){ /* eingerückt: Blocktext */
if(descTarget == null){ warnings.push({type:'descStray', line:i+1}); return; }
if(descTarget !== SKIP) addDesc(descTarget, raw.trim());
return;
}
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
zeilengenau selbst (SPEC §11). */
warnings.push({type:'descStray', line:i+1});
descTarget = null;
return;
}
const target = idNodes.get(idm[1]);
if(!target){ warnings.push({type:'unknownDesc', line:i+1, id:idm[1]}); descTarget = SKIP; }
else descTarget = target;
return;
}
if(!raw.trim()) return; if(!raw.trim()) return;
/* Kurzform `"` (SPEC §11, D40): Beschreibung des VORANGEHENDEN Knotens,
nur mit folgendem Leerraum (Leerraum-Regel) und nur auf Zeilen ohne
Zerlegungszeichen `"Zitat"` und `- " Zitat" …` bleiben Labels.
Die Einrückung der Zeile hat keine Bedeutung. */
const ts = raw.replace(/^[ \t]*/, '');
if(ts[0] === '"' && /[ \t]/.test(ts[1] || '')){
if(lastNode) addDesc(lastNode, ts.slice(2).trim());
else warnings.push({type:'descStray', line:i+1});
return;
}
/* Statusbox tolerant erfassen: irgendein einzelnes Zeichen in [ ] an der /* Statusbox tolerant erfassen: irgendein einzelnes Zeichen in [ ] an der
Statusposition. Gültige Codes -> Status; unbekannte -> Warnung + neutral Statusposition. Gültige Codes -> Status; unbekannte -> Warnung + neutral
(fehlertolerant: die Zeile geht nicht verloren). */ (fehlertolerant: die Zeile geht nicht verloren). */
@@ -109,9 +164,19 @@ export function parse(text){
while(stack.length > 1 && stack[stack.length-1].width >= width) stack.pop(); while(stack.length > 1 && stack[stack.length-1].width >= width) stack.pop();
const parent = stack[stack.length-1].node; const parent = stack[stack.length-1].node;
const node = {label, type, optional, fold, status, url, size, tags, id, deps, focus, children:[], line:i+1}; const node = {label, type, optional, fold, status, url, size, tags, id, deps, desc:null, focus, children:[], line:i+1};
parent.children.push(node); parent.children.push(node);
stack.push({node, width}); stack.push({node, width});
lastNode = node;
if(id != null && !idNodes.has(id)) idNodes.set(id, node);
});
/* Beschreibungen zusammensetzen: Zeilen in Dokumentreihenfolge, Leerzeilen
bleiben als Absatztrenner, Ränder getrimmt (SPEC §11, D40). */
descLines.forEach((lines, node) => {
while(lines.length && lines[lines.length-1] === '') lines.pop();
while(lines.length && lines[0] === '') lines.shift();
if(lines.length) node.desc = lines.join('\n');
}); });
/* Unbekannte Abhängigkeits-IDs (SPEC §1): erst nach dem Einlesen prüfbar /* Unbekannte Abhängigkeits-IDs (SPEC §1): erst nach dem Einlesen prüfbar
+10 -1
View File
@@ -72,6 +72,8 @@ function nodeAria(n, opts, fold){
der eigenen Cursor-Zeile unterscheidet (SPEC §9). */ der eigenen Cursor-Zeile unterscheidet (SPEC §9). */
if(n.focus) parts.push(t('a11yFocusMark')); if(n.focus) parts.push(t('a11yFocusMark'));
if(n.url) parts.push(t('a11yLink')); if(n.url) parts.push(t('a11yLink'));
/* Beschreibung (SPEC §11/D40): der Text selbst — die ”-Marke ist aria-hidden. */
if(n.desc) parts.push(n.desc.replace(/\s+/g, ' '));
return parts.join(', '); return parts.join(', ');
} }
@@ -89,7 +91,9 @@ function nodeHtml(n, extra, opts, fold){
für die Gegenrichtung (Cursor-Zeile -> Knoten hervorheben). Der Hinweis im für die Gegenrichtung (Cursor-Zeile -> Knoten hervorheben). Der Hinweis im
Tooltip macht die sonst unsichtbare Alt-Klick-Geste auffindbar. */ Tooltip macht die sonst unsichtbare Alt-Klick-Geste auffindbar. */
const lineAttr = n.line ? ` data-line="${n.line}"` : ''; const lineAttr = n.line ? ` data-line="${n.line}"` : '';
const tip = [n.id ? '#' + n.id : '', /* Beschreibung zuerst im Tooltip (mehrzeilig, D40), dann die Kurz-Fakten. */
const tip = [n.desc || '',
n.id ? '#' + n.id : '',
n.deps && n.deps.length ? '→ ' + n.deps.map(d => '#' + d).join(', ') : '', n.deps && n.deps.length ? '→ ' + n.deps.map(d => '#' + d).join(', ') : '',
effKey effKey
? t('heldTooltip', {eff: t('st_' + effKey), own: t('st_' + n.status.key)}) ? t('heldTooltip', {eff: t('st_' + effKey), own: t('st_' + n.status.key)})
@@ -123,6 +127,11 @@ function nodeHtml(n, extra, opts, fold){
: ''; : '';
const inner = foldHtml + const inner = foldHtml +
esc(n.label) + esc(n.label) +
/* -Marke (D40): macht die sonst unsichtbare Beschreibung
auffindbar (Lehre aus D25) spiegelt das "-Zeichen der
Notation. Nicht im Export: Der Text selbst kann dort nicht
erscheinen, eine Marke ohne Ziel wäre Rauschen. */
(n.desc ? '<span class="desc-mark" aria-hidden="true">”</span>' : '') +
(n.url ? '<span class="ext" aria-hidden="true">↗</span>' : '') + (n.url ? '<span class="ext" aria-hidden="true">↗</span>' : '') +
riskMark + riskMark +
sizeBadge + sizeBadge +
+4
View File
@@ -561,6 +561,10 @@
/* Der dunkle Wurzelknoten braucht ein helleres Zeichen als das Grau. */ /* Der dunkle Wurzelknoten braucht ein helleres Zeichen als das Grau. */
.root-node .fold{color:rgba(255,255,255,.75)} .root-node .fold{color:rgba(255,255,255,.75)}
.root-node .fold:hover{color:#fff} .root-node .fold:hover{color:#fff}
/* -Marke (SPEC §9, D40): der Knoten trägt eine Beschreibung im Tooltip
nachlesbar. Spiegelt das "-Zeichen der Notation; gedeckt, kein Klickziel. */
.node .desc-mark{color:var(--muted);margin-left:4px;font-weight:600}
.root-node .desc-mark{color:rgba(255,255,255,.75)}
/* Sichtbarer Tastatur-Fokus (Knoten sind fokussierbar: Fokusreihenfolge = /* Sichtbarer Tastatur-Fokus (Knoten sind fokussierbar: Fokusreihenfolge =
Lese-/Dokumentreihenfolge; Screenreader liest den aria-label). */ Lese-/Dokumentreihenfolge; Screenreader liest den aria-label). */
.node:focus-visible{outline:2px solid var(--or);outline-offset:2px;box-shadow:0 2px 8px rgba(36,52,71,.22)} .node:focus-visible{outline:2px solid var(--or);outline-offset:2px;box-shadow:0 2px 8px rgba(36,52,71,.22)}
+11
View File
@@ -18,6 +18,13 @@
- unknownDep { line, id } Abhängigkeit auf eine ID ohne Knoten - unknownDep { line, id } Abhängigkeit auf eine ID ohne Knoten
(SPEC §1/D37); Zyklen sind dagegen (SPEC §1/D37); Zyklen sind dagegen
zulässig und warnen nie zulässig und warnen nie
- unknownDesc { line, id } Beschreibungsblock für eine ID ohne
Knoten (SPEC §11/D40)
- descStray { line } Beschreibungszeile ohne Bezug: `"`-Zeile
ohne Knoten davor bzw. Zeile im
Beschreibungsteil ohne #id-Block; meldet
auch die von einem versehentlichen
`---` verschluckten Knotenzeilen
- sourceLoad { url, error } ?sourceUrl= nicht ladbar (D23); ohne - sourceLoad { url, error } ?sourceUrl= nicht ladbar (D23); ohne
Zeilennummer, erscheint dadurch zuoberst Zeilennummer, erscheint dadurch zuoberst
- padRateLimit { seconds } zu früh nachgeladen; Werkbaum hat gar nicht - padRateLimit { seconds } zu früh nachgeladen; Werkbaum hat gar nicht
@@ -44,6 +51,10 @@ export function formatWarning(w, t){
return t('duplicateIdWarn', {line: w.line, id: esc(w.id), firstLine: w.firstLine}); return t('duplicateIdWarn', {line: w.line, id: esc(w.id), firstLine: w.firstLine});
case 'unknownDep': case 'unknownDep':
return t('unknownDepWarn', {line: w.line, id: esc(w.id)}); return t('unknownDepWarn', {line: w.line, id: esc(w.id)});
case 'unknownDesc':
return t('unknownDescWarn', {line: w.line, id: esc(w.id)});
case 'descStray':
return t('descStrayWarn', {line: w.line});
case 'sourceLoad': case 'sourceLoad':
return t('sourceLoadWarn', {url: esc(w.url), error: esc(w.error)}); return t('sourceLoadWarn', {url: esc(w.url), error: esc(w.error)});
case 'padRateLimit': case 'padRateLimit':
+114
View File
@@ -0,0 +1,114 @@
import { describe, it, expect } from 'vitest';
import { parse } from '../src/parser.js';
import { renderTreeHtml } from '../src/render.js';
const t = key => key;
const roots = txt => parse(txt).roots;
const render = txt => renderTreeHtml(roots(txt),
{t, showDiscarded: false, cheapPath: false, cheapSet: new Set()});
/* Knotenbeschreibungen (SPEC §1/§9, D40): `"`-Zeilen (Kurzform) und
ID-Blöcke hinter `---` (Langform). */
describe('Kurzform — `"`-Zeile unter dem Knoten', () => {
it('hängt die Zeile an den vorangehenden Knoten, ohne einen Knoten zu erzeugen', () => {
const [wurzel] = roots(`[ ] Auth\n " Kapselt Login und Tokens.`);
expect(wurzel.desc).toBe('Kapselt Login und Tokens.');
expect(wurzel.children).toEqual([]);
});
it('setzt mehrere `"`-Zeilen zur selben Beschreibung fort', () => {
const [wurzel] = roots(`[ ] Auth\n " Zeile eins.\n " Zeile zwei.`);
expect(wurzel.desc).toBe('Zeile eins.\nZeile zwei.');
});
it('verlangt folgenden Leerraum — `"Zitat"` bleibt ein Label', () => {
const [wurzel] = roots(`"Zitat" als Label`);
expect([wurzel.label, wurzel.desc]).toEqual(['"Zitat" als Label', null]);
});
it('gilt nicht auf Zeilen mit Zerlegungszeichen', () => {
const [wurzel] = roots(`[ ] W\n - " Zitat" als Label`);
expect(wurzel.children[0].label).toBe('" Zitat" als Label');
});
it('warnt bei einer `"`-Zeile ohne vorangehenden Knoten', () => {
const {warnings} = parse(`" verwaist`);
expect(warnings).toEqual([{type: 'descStray', line: 1}]);
});
it('extrahiert im Freitext nichts — (M), @name und #id bleiben Text', () => {
const [wurzel] = roots(`[ ] W\n " Kostet (M), fragt @ana wegen #auth.`);
expect(wurzel.desc).toBe('Kostet (M), fragt @ana wegen #auth.');
expect([wurzel.size, wurzel.tags, wurzel.id]).toEqual([null, [], null]);
});
it('entfernt %%-Kommentare auch in Beschreibungszeilen', () => {
const [wurzel] = roots(`[ ] W\n " Sichtbar. %% unsichtbar`);
expect(wurzel.desc).toBe('Sichtbar.');
});
});
describe('Langform — ID-Blöcke hinter `---`', () => {
const TXT = `[ ] Auth #auth
[ ] API #api
---
#auth
Erster Absatz.
Zweiter Absatz.
#api
Kurz.`;
it('ordnet Blöcke über die ID zu, mit Leerzeilen als Absatztrennern', () => {
const [auth, api] = roots(TXT);
expect(auth.desc).toBe('Erster Absatz.\n\nZweiter Absatz.');
expect(api.desc).toBe('Kurz.');
});
it('erzeugt hinter dem Trenner keine Knoten mehr', () => {
expect(roots(TXT).map(n => n.label)).toEqual(['Auth', 'API']);
});
it('akzeptiert auch längere Trenner und ignoriert weitere im Beschreibungsteil', () => {
const [w] = roots(`[ ] W #w\n-----\n#w\n Text.\n---\n#w\n Mehr.`);
expect(w.desc).toBe('Text.\nMehr.');
});
it('hängt Kurz- und Langform in Dokumentreihenfolge aneinander', () => {
const [w] = roots(`[ ] W #w\n " Kurz.\n---\n#w\n Lang.`);
expect(w.desc).toBe('Kurz.\nLang.');
});
it('warnt bei einer ID ohne Knoten — und schluckt deren Blocktext still', () => {
const {roots: r, warnings} = parse(`[ ] W\n---\n#fehlt\n Text dazu.\n Noch mehr.`);
expect(warnings).toEqual([{type: 'unknownDesc', line: 3, id: 'fehlt'}]);
expect(r[0].desc).toBe(null);
});
it('meldet verwaiste Zeilen einzeln — der versehentliche Trenner wird laut', () => {
const {warnings} = parse(`[ ] W\n---\n- [ ] Verschluckt\n - [ ] Kind`);
expect(warnings).toEqual([
{type: 'descStray', line: 3},
{type: 'descStray', line: 4}
]);
});
it('lässt `- --` als gewöhnlichen Knoten stehen', () => {
expect(roots(`- --`)[0].label).toBe('--');
});
});
describe('Darstellung — Tooltip, ”-Marke, aria', () => {
it('stellt die Beschreibung an den Anfang des Tooltips und setzt die Marke', () => {
const {html} = render(`[ ] Auth #auth\n " Kapselt Login.`);
expect(html).toContain('title="Kapselt Login. · #auth · st_geplant · jumpHint"');
expect(html).toContain('<span class="desc-mark" aria-hidden="true">”</span>');
expect(html).toContain('aria-label="Auth, a11yStatus, a11yId, Kapselt Login."');
});
it('ändert Knoten ohne Beschreibung nicht', () => {
const {html} = render(`[ ] Ohne`);
expect(html).not.toContain('desc-mark');
});
});