feat: Abhängigkeiten (:#a,#b) parsen — unbekannte ID warnt, Zyklen zulässig (SPEC §1, D37)

Extraktionsschritt 7: alle alleinstehend angesetzten `:#a,#b`-Token —
EIN zusammenhängendes Token ohne Leerraum, mehrere je Zeile werden
zusammengeführt; eingeklammerte Erwähnungen wie `(:#auth,#api)` bleiben
Label (Zitier-Konvention wie `(#auth)`, deshalb enger als die frühere
§11-Formulierung). `deps` sind ID-Strings am Knoten; der Parser prüft
nur Existenz (Warnung unknownDep, Vorwärts-Referenzen normal), Zyklen
werden bewusst nicht einmal erkannt. Sichtbar als `→ #a, #b` im Tooltip
und als a11yDeps; Legendenzeile + Warntext in allen 9 Sprachen. Im
mitgelieferten Plan: Zyklen-legal und unknown-id-Warnung auf [x],
Abhängigkeits-Knoten auf [~]. 12 neue Tests (tests/deps.test.js).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-22 16:25:09 +02:00
co-authored by Claude Fable 5
parent 6ecf3e4c69
commit 3ba37fc5a0
12 changed files with 239 additions and 39 deletions
+38
View File
@@ -1702,3 +1702,41 @@ welcher Knoten bei Verweisen „gewinnt“, entscheidet erst die
Abhängigkeits-Auflösung — dort ist die Warnung dann schon da. Eine Zeile, die Abhängigkeits-Auflösung — dort ist die Warnung dann schon da. Eine Zeile, die
**nur** aus einer ID besteht, wird wie jede leere Zeile ignoriert und belegt **nur** aus einer ID besteht, wird wie jede leere Zeile ignoriert und belegt
die ID nicht. die ID nicht.
## 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 —
bleiben eigene Schritte (§11). Vier Festlegungen:
**Die Liste ist ein zusammenhängendes Token, ohne Leerraum.** `:#a,#b` — jede
ID mit `#`, kommagetrennt, kein Leerzeichen. Das ist die wörtliche Lesart von
§11 („Doppelpunkt mit unmittelbar folgendem `#`“) und macht das Zeilenformat
robust: Wo das Token endet, beginnt wieder gewöhnlicher Text. Der bekannte
Stolperstein steht in der SPEC: `:#a, #b` liest nur `#a`, und das ` #b`
dahinter ist ein alleinstehendes Token — also die Knoten-ID. Ein Rest wie ein
verwaistes `,b` bleibt sichtbar im Label stehen und verrät den Tippfehler,
statt verschluckt zu werden.
**Nur alleinstehend angesetzt** (`(^|\s):#…`) — **enger als §11**, das nur den
folgenden `#` verlangte. Der Ausschlag: die **Zitier-Konvention**. Bei den IDs
(D36) wurden Erwähnungen im mitgelieferten Plan eingeklammert (`(#auth)`), und
genau das muss auch für Abhängigkeiten funktionieren — `(:#auth,#api)` als
Erwähnung im Label wäre mit der reinen `:#`-Regel trotzdem geparst worden
(die Klammer steht ja vor dem `:`, nicht vor dem `#`). Mit der
Alleinstehend-Regel schützen Klammern einheitlich alles: `(#auth)`,
`(:#a,#b)`, künftig `(&tag)`. Nebeneffekt: Konstrukte wie `1:#2` im Label
bleiben Label.
**`deps` sind ID-Strings, keine Knoten-Referenzen.** Der Parser prüft nur
**Existenz** (`unknownDep` je fehlender ID, an der Zeile des abhängigen
Knotens); aufgelöst wird erst beim Konsumenten. So bleibt der Parse-Baum
serialisierbar und die Auflösungsfragen (doppelte IDs — die `duplicateId`-
Warnung steht dann schon da) liegen dort, wo sie beantwortet werden können.
**Zyklen werden bewusst nicht einmal erkannt**: Sie sind zulässig (§1/D34,
„wird gemeinsam fertig“), eine Zyklenprüfung hätte also keinen Abnehmer —
auch die Selbst-Abhängigkeit ist stumm.
**Sichtbar als `→ #a, #b` im Tooltip und als `a11yDeps` im `aria-label`**
dieselbe Zurückhaltung wie bei den IDs (D36): keine eigene Diagramm-
Darstellung, bis die Querverbindungen (§11) gebaut sind; der Pfeil im Tooltip
sagt die Richtung („hängt ab von“), ohne ein neues Zeichen einzuführen.
+35 -16
View File
@@ -20,8 +20,9 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit):
4. Größe: erstes `(XS|S|M|L|XL|XXL)`, Groß-/Kleinschreibung egal. 4. Größe: erstes `(XS|S|M|L|XL|XXL)`, Groß-/Kleinschreibung egal.
5. Tags: alle `@name`-Vorkommen. 5. Tags: alle `@name`-Vorkommen.
6. Knoten-ID: das **erste** alleinstehend angesetzte `#name`-Token (siehe unten). 6. Knoten-ID: das **erste** alleinstehend angesetzte `#name`-Token (siehe unten).
7. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten). 7. Abhängigkeiten: alle alleinstehend angesetzten `:#a,#b`-Token (siehe unten).
8. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren. 8. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten).
9. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren.
**Knoten-ID `#name`** — benennt einen Knoten im **ganzen Dokument** eindeutig; **Knoten-ID `#name`** — benennt einen Knoten im **ganzen Dokument** eindeutig;
sie ist die Adresse für Abhängigkeiten und Beschreibungsblöcke (§11). sie ist die Adresse für Abhängigkeiten und Beschreibungsblöcke (§11).
@@ -41,6 +42,27 @@ sie ist die Adresse für Abhängigkeiten und Beschreibungsblöcke (§11).
ID gilt trotzdem am Knoten (fehlertolerant wie §4 — die Zeile geht nicht ID gilt trotzdem am Knoten (fehlertolerant wie §4 — die Zeile geht nicht
verloren). verloren).
**Abhängigkeiten `:#a,#b`** — der Knoten hängt von den Knoten mit diesen IDs
ab, auch außerhalb seines eigenen Teilbaums.
- Die Liste ist **ein zusammenhängendes Token**: Doppelpunkt, dann
kommagetrennt je ID mit `#`, **ohne Leerraum**. `:#a, #b` liest nur `#a`
das ` #b` dahinter ist ein alleinstehendes Token und damit die Knoten-ID
(siehe oben). Mehrere Listen je Zeile werden zusammengeführt.
- Erkannt nur **alleinstehend angesetzt** (`(^|\s):#…`, enger als die frühere
§11-Formulierung — Begründung: D37): Ein Doppelpunkt im Label bleibt Label,
und eine **eingeklammerte Erwähnung** wie `(:#auth,#api)` bleibt Label —
dieselbe Zitier-Konvention wie `(#auth)` bei der Knoten-ID.
- Abhängigkeiten sagen **nichts über Reihenfolge oder Startzeitpunkt** — sie
sagen etwas über den **Status** (effektiver Status, §11). Das trennt
Werkbaum von einem Netzplan.
- **Zyklen sind zulässig** und bedeuten: diese Knoten werden gemeinsam fertig.
Kein Fehler, keine Warnung — auch die Abhängigkeit auf sich selbst nicht.
Vorwärts-Referenzen (Ziel steht weiter unten) sind normal.
- Eine ID ohne zugehörigen Knoten: Warnung `unknownDep` mit Zeilennummer.
- Sichtbar im Tooltip (`→ #a, #b`) und im `aria-label`; die Querverbindungen
im Diagramm sind reserviert (§11).
**Fokusmarke `!!!`** — „schau hier hin": Der Knoten wird im Diagramm **Fokusmarke `!!!`** — „schau hier hin": Der Knoten wird im Diagramm
hervorgehoben und ins Bild geholt (§9). Gedacht für das gemeinsame Arbeiten an hervorgehoben und ins Bild geholt (§9). Gedacht für das gemeinsame Arbeiten an
einem Pad (§9, `?etherpad=`): Weil dort niemand den Cursor der anderen sieht, einem Pad (§9, `?etherpad=`): Weil dort niemand den Cursor der anderen sieht,
@@ -70,7 +92,13 @@ Für die Knoten-ID (Schritt 6, nur der erste Treffer):
(^|\s)#([\p{L}\p{N}._-]+) (^|\s)#([\p{L}\p{N}._-]+)
``` ```
Für die Fokusmarke (Schritt 7): Für die Abhängigkeiten (Schritt 7, alle Treffer):
```
(^|\s):#([\p{L}\p{N}._-]+(?:,#[\p{L}\p{N}._-]+)*)
```
Für die Fokusmarke (Schritt 8):
``` ```
(^|\s)!!!(?=\s|$) (^|\s)!!!(?=\s|$)
@@ -582,19 +610,10 @@ unten; damit ist die frühere Dreifach-Rolle von `#` aufgelöst (D34).
### Abhängigkeiten zwischen Knoten (`:#auth,#api`) ### Abhängigkeiten zwischen Knoten (`:#auth,#api`)
Ein Knoten kann von Knoten **außerhalb seines eigenen Teilbaums** abhängen; Die **Schreibweise ist umgesetzt** — Definition jetzt in §1 (Token-Vertrag,
notiert als Doppelpunkt mit kommagetrennter ID-Liste. Alleinstehend-Regel, Zyklen zulässig, Warnung `unknownDep`). Offen sind die
**Konsumenten**: der effektive Status, die Querverbindungen im Diagramm und
- Abhängigkeiten sagen **nichts über Reihenfolge oder Startzeitpunkt** — sie die Closure-Rechnung des günstigsten Pfads (alle drei unten).
sagen etwas über den **Status** (siehe unten). Das ist der Unterschied zu
einem Netzplan.
- **Zyklen sind zulässig** und bedeuten: diese Knoten werden gemeinsam fertig.
Sie sind also kein Fehler und bekommen keine Warnung.
- Eine ID ohne zugehörigen Knoten ist ein Fehler (Warnung mit Zeilennummer).
- Kollisionsfrei zur bestehenden Zeilenextraktion (§1): Gelesen wird nur ein
Doppelpunkt mit **unmittelbar folgendem `#`** — ein Doppelpunkt im Label
bleibt damit Label. Die URL wird ohnehin schon in Schritt 3 herausgenommen,
vor Größe, Tags und Label.
### Intrinsischer und effektiver Status ### Intrinsischer und effektiver Status
+6 -1
View File
@@ -77,7 +77,12 @@ entscheiden, **bevor** Code entsteht.
unberührt); Warnung `duplicateId` nennt beide Zeilen; sichtbar im unberührt); Warnung `duplicateId` nennt beide Zeilen; sichtbar im
Tooltip + `aria-label`; SPEC-§11-Teil nach §1 überführt; Tooltip + `aria-label`; SPEC-§11-Teil nach §1 überführt;
`tests/ids.test.js`. `tests/ids.test.js`.
- [ ] Abhängigkeiten `:#a,#b` parsen; unbekannte ID → Warnung, Zyklen erlaubt. - [x] Abhängigkeiten `:#a,#b` parsen; unbekannte ID → Warnung, Zyklen erlaubt.
→ Umgesetzt (D37): ein zusammenhängendes Token ohne Leerraum, nur
alleinstehend angesetzt (eingeklammerte Erwähnungen bleiben Label);
`deps` als ID-Strings am Knoten, Warnung `unknownDep`, Zyklen bewusst
ungeprüft; sichtbar als `→ #a, #b` im Tooltip + `a11yDeps`;
SPEC-§11-Teil nach §1 überführt; `tests/deps.test.js`.
- [ ] Effektiven Status rechnen (intrinsisch + Abhängigkeiten); Darstellung - [ ] Effektiven Status rechnen (intrinsisch + Abhängigkeiten); Darstellung
entscheiden — die Knotenfarbe zeigt heute den intrinsischen Status. entscheiden — die Knotenfarbe zeigt heute den intrinsischen Status.
- [ ] Günstigsten Pfad auf die Dependency Closure umstellen (gemeinsame - [ ] Günstigsten Pfad auf die Dependency Closure umstellen (gemeinsame
+3 -3
View File
@@ -12,11 +12,11 @@
- [x] Node IDs (#auth) (S) %% often just the ticket number, see SPEC §11 - [x] Node IDs (#auth) (S) %% often just the ticket number, see SPEC §11
+ [?] Free tags, &tag (M) %% only together with a consumer, see D34 + [?] Free tags, &tag (M) %% only together with a consumer, see D34
- [?] A lens: highlight every node tagged &x (S) - [?] A lens: highlight every node tagged &x (S)
- [?] Dependencies across the tree, :#auth,#api (M) - [~] Dependencies across the tree, (:#auth,#api) (M)
- [?] They constrain the status, not when work may start (S) - [?] They constrain the status, not when work may start (S)
- [?] Effective status: intrinsic, plus what it depends on (S) - [?] Effective status: intrinsic, plus what it depends on (S)
- [?] Cycles are legal — they mean "finished together" (XS) - [x] Cycles are legal — they mean "finished together" (XS)
- [ ] An id with no node behind it is a warning (XS) - [x] An id with no node behind it is a warning (XS)
- [x] XOR, = — exactly one alternative, not at least one (S) - [x] XOR, = — exactly one alternative, not at least one (S)
- [?] Node descriptions, shown as a tooltip or pop-up (M) - [?] Node descriptions, shown as a tooltip or pop-up (M)
- [?] A short text right at the node (S) %% indentation already means hierarchy - [?] A short text right at the node (S) %% indentation already means hierarchy
+8 -2
View File
@@ -47,8 +47,8 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
Alternative-Rahmen grau (Basis-CSS `ul.or`). Kein Petrol im Diagramm mehr; Alternative-Rahmen grau (Basis-CSS `ul.or`). Kein Petrol im Diagramm mehr;
`var(--or)` nur noch für UI-Akzente/Logo (SPEC §9, D15). `var(--or)` nur noch für UI-Akzente/Logo (SPEC §9, D15).
- Extraktionsreihenfolge im Parser nicht umstellen: Kommentar → Zeichen/ - Extraktionsreihenfolge im Parser nicht umstellen: Kommentar → Zeichen/
Status → URL → Größe → Tags → Knoten-ID → Fokusmarke (sonst kollidieren Status → URL → Größe → Tags → Knoten-ID → Abhängigkeiten → Fokusmarke
`@` und `#` in URLs). (sonst kollidieren `@` und `#` in URLs).
- Fehlertoleranz (SPEC §4): der Parser erfasst die Statusbox als *beliebiges* - Fehlertoleranz (SPEC §4): der Parser erfasst die Statusbox als *beliebiges*
Einzelzeichen `\[([^\]])\]` und validiert gegen `STATUS_BY_CODE`; unbekannte Einzelzeichen `\[([^\]])\]` und validiert gegen `STATUS_BY_CODE`; unbekannte
Codes → `parse().warnings` als `{type:'unknownStatus', line, code}`, Knoten Codes → `parse().warnings` als `{type:'unknownStatus', line, code}`, Knoten
@@ -273,6 +273,12 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
darf nicht gefressen werden. Zeichenmenge wie `@name`. Doppelte ID → darf nicht gefressen werden. Zeichenmenge wie `@name`. Doppelte ID →
`{type:'duplicateId', line, id, firstLine}`; die spätere gilt trotzdem. `{type:'duplicateId', line, id, firstLine}`; die spätere gilt trotzdem.
Keine eigene Darstellung — nur Tooltip (erste Position) und `a11yId`. Keine eigene Darstellung — nur Tooltip (erste Position) und `a11yId`.
- 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
Knoten-Referenzen; der Parser prüft nur Existenz (`unknownDep`), Zyklen
werden bewusst nicht einmal erkannt (zulässig, „gemeinsam fertig"). Keine
Diagramm-Darstellung — nur Tooltip (`→ #a, #b`) und `a11yDeps`.
- XOR-Gruppen `=` (SPEC §3/D35): Der Parser setzt `type:'xor'` (nur mit - XOR-Gruppen `=` (SPEC §3/D35): Der Parser setzt `type:'xor'` (nur mit
folgendem Leerraum — `=SUMME(…)` bleibt Label); der Renderer gibt folgendem Leerraum — `=SUMME(…)` bleibt Label); der Renderer gibt
`<ul class="or xor">` aus, damit die **gesamte** any-of-Geometrie (alle drei `<ul class="or xor">` aus, damit die **gesamte** any-of-Geometrie (alle drei
+1
View File
@@ -121,6 +121,7 @@
Kommentare mit <code>%%</code> — als ganze Zeile oder am Zeilenende. Kommentare mit <code>%%</code> — als ganze Zeile oder am Zeilenende.
Personen mit <code>@name</code> — erscheinen unten rechts am Knoten. 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> — erscheint im Tooltip des Knotens.
Abhängigkeiten mit <code>:#name,#name</code> — erscheinen im Tooltip.
</div> </div>
</div> </div>
</div> </div>
+28 -9
View File
@@ -1103,12 +1103,13 @@ const I18N = {
mixedWarn:"Zeile {line}: Unter „{label}“ sind - und | gemischt — dargestellt nach dem ersten Kind.", mixedWarn:"Zeile {line}: Unter „{label}“ sind - und | gemischt — dargestellt nach dem ersten Kind.",
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.",
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.",
sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).", sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"„{url}“ hat innerhalb von {seconds} s nicht geantwortet — der Abruf wurde abgebrochen. Etherpad begrenzt, wie oft der Export geholt werden darf (serienmäßig 10-mal pro 90 s); warte einen Moment und lade dann erneut.", sourceTimeoutWarn:"„{url}“ hat innerhalb von {seconds} s nicht geantwortet — der Abruf wurde abgebrochen. Etherpad begrenzt, wie oft der Export geholt werden darf (serienmäßig 10-mal pro 90 s); warte einen Moment und lade dann erneut.",
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt", a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.", hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen", hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
hint_xor:"Alternative, genau eine", hint_xor:"Alternative, genau eine",
@@ -1121,6 +1122,7 @@ const I18N = {
hint_comment:"Kommentare mit %% — als ganze Zeile oder am Zeilenende.", hint_comment:"Kommentare mit %% — als ganze Zeile oder am Zeilenende.",
hint_people:"Personen mit @name — erscheinen unten rechts am Knoten.", 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 — erscheint im Tooltip des Knotens.",
hint_deps:"Abhängigkeiten mit :#name,#name — erscheinen im Tooltip.",
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."
}, },
en: { en: {
@@ -1171,12 +1173,13 @@ const I18N = {
mixedWarn:"Line {line}: under “{label}”, - and | are mixed — rendered by the first child.", mixedWarn:"Line {line}: under “{label}”, - and | are mixed — rendered by the first child.",
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.",
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.",
sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).", sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"“{url}” did not answer within {seconds} s — the request was aborted. Etherpad limits how often the export may be fetched (10 times per 90 s by default); wait a moment, then reload.", sourceTimeoutWarn:"“{url}” did not answer within {seconds} s — the request was aborted. Etherpad limits how often the export may be fetched (10 times per 90 s by default); wait a moment, then reload.",
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link", a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.", hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
hint_all:"sub-task, all required", hint_any:"alternative, choose one", hint_all:"sub-task, all required", hint_any:"alternative, choose one",
hint_xor:"alternative, exactly one", hint_xor:"alternative, exactly one",
@@ -1189,6 +1192,7 @@ const I18N = {
hint_comment:"Comments with %% — whole line or at the end of a line.", 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_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 — shown in the node's tooltip.",
hint_deps:"Dependencies with :#name,#name — shown in the tooltip.",
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."
}, },
es: { es: {
@@ -1239,12 +1243,13 @@ const I18N = {
mixedWarn:"Línea {line}: bajo «{label}» se mezclan - y | — se representa según el primer hijo.", mixedWarn:"Línea {line}: bajo «{label}» se mezclan - y | — se representa según el primer hijo.",
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.",
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.",
sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).", sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"«{url}» no respondió en {seconds} s — se canceló la petición. Etherpad limita la frecuencia de descarga del export (10 veces por 90 s de forma predeterminada); espera un momento y vuelve a cargar.", sourceTimeoutWarn:"«{url}» no respondió en {seconds} s — se canceló la petición. Etherpad limita la frecuencia de descarga del export (10 veces por 90 s de forma predeterminada); espera un momento y vuelve a cargar.",
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace", a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.", hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.",
hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una", hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
hint_xor:"alternativa, exactamente una", hint_xor:"alternativa, exactamente una",
@@ -1257,6 +1262,7 @@ const I18N = {
hint_comment:"Comentarios con %% — línea completa o al final de la línea.", 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_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 — visible en el tooltip del nodo.",
hint_deps:"Dependencias con :#nombre,#nombre — visibles en el tooltip.",
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."
}, },
fr: { fr: {
@@ -1307,12 +1313,13 @@ const I18N = {
mixedWarn:"Ligne {line} : sous « {label} », - et | sont mélangés — rendu selon le premier enfant.", mixedWarn:"Ligne {line} : sous « {label} », - et | sont mélangés — rendu selon le premier enfant.",
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.",
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.",
sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).", sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"« {url} » na pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de lexport (10 fois par 90 s par défaut) ; attends un instant, puis recharge.", sourceTimeoutWarn:"« {url} » na pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de lexport (10 fois par 90 s par défaut) ; attends un instant, puis recharge.",
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien", a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.", hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.",
hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une", hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une",
hint_xor:"alternative, exactement une", hint_xor:"alternative, exactement une",
@@ -1325,6 +1332,7 @@ const I18N = {
hint_comment:"Commentaires avec %% — ligne entière ou en fin de ligne.", 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_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 — visible dans linfobulle du nœud.",
hint_deps:"Dépendances avec :#nom,#nom — visibles dans linfobulle.",
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."
}, },
pl: { pl: {
@@ -1375,12 +1383,13 @@ const I18N = {
mixedWarn:"Wiersz {line}: pod „{label}” mieszają się - i | — renderowane według pierwszego dziecka.", mixedWarn:"Wiersz {line}: pod „{label}” mieszają się - i | — renderowane według pierwszego dziecka.",
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.",
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.",
sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).", sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"„{url}” nie odpowiedział w ciągu {seconds} s — żądanie przerwano. Etherpad ogranicza częstość pobierania eksportu (domyślnie 10 razy na 90 s); odczekaj chwilę i wczytaj ponownie.", sourceTimeoutWarn:"„{url}” nie odpowiedział w ciągu {seconds} s — żądanie przerwano. Etherpad ogranicza częstość pobierania eksportu (domyślnie 10 razy na 90 s); odczekaj chwilę i wczytaj ponownie.",
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: M (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem", a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: M (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.", hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną", hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
hint_xor:"alternatywa, dokładnie jedna", hint_xor:"alternatywa, dokładnie jedna",
@@ -1393,6 +1402,7 @@ const I18N = {
hint_comment:"Komentarze z %% — cały wiersz lub na końcu wiersza.", 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_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 — widoczne w podpowiedzi węzła.",
hint_deps:"Zależności przez :#nazwa,#nazwa — widoczne w podpowiedzi.",
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."
}, },
ru: { ru: {
@@ -1443,12 +1453,13 @@ const I18N = {
mixedWarn:"Строка {line}: под «{label}» смешаны - и | — отображается по первому потомку.", mixedWarn:"Строка {line}: под «{label}» смешаны - и | — отображается по первому потомку.",
xorConflictWarn:"Строка {line}: «{label}» — ещё одна реализованная альтернатива, а группа = допускает ровно одну.", xorConflictWarn:"Строка {line}: «{label}» — ещё одна реализованная альтернатива, а группа = допускает ровно одну.",
duplicateIdWarn:"Строка {line}: ID #{id} уже занят (строка {firstLine}).", duplicateIdWarn:"Строка {line}: ID #{id} уже занят (строка {firstLine}).",
unknownDepWarn:"Строка {line}: зависимость #{id} — узла с таким 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}» — показан как нейтральный.",
sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).", sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"«{url}» не ответил за {seconds} с — запрос прерван. Etherpad ограничивает частоту загрузки экспорта (по умолчанию 10 раз за 90 с); подождите немного и обновите снова.", sourceTimeoutWarn:"«{url}» не ответил за {seconds} с — запрос прерван. Etherpad ограничивает частоту загрузки экспорта (по умолчанию 10 раз за 90 с); подождите немного и обновите снова.",
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой", a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.", hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну", hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
hint_xor:"альтернатива, ровно одна", hint_xor:"альтернатива, ровно одна",
@@ -1461,6 +1472,7 @@ const I18N = {
hint_comment:"Комментарии через %% — вся строка или в конце строки.", hint_comment:"Комментарии через %% — вся строка или в конце строки.",
hint_people:"Люди через @имя — показываются справа внизу узла.", hint_people:"Люди через @имя — показываются справа внизу узла.",
hint_id:"ID узла через #имя — виден во всплывающей подсказке узла.", hint_id:"ID узла через #имя — виден во всплывающей подсказке узла.",
hint_deps:"Зависимости через :#имя,#имя — видны в подсказке.",
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме." hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме."
}, },
hi: { hi: {
@@ -1511,12 +1523,13 @@ const I18N = {
mixedWarn:"पंक्ति {line}: „{label}“ के अंतर्गत - और | मिश्रित हैं — पहले चाइल्ड के अनुसार दिखाया गया।", mixedWarn:"पंक्ति {line}: „{label}“ के अंतर्गत - और | मिश्रित हैं — पहले चाइल्ड के अनुसार दिखाया गया।",
xorConflictWarn:"पंक्ति {line}: „{label}“ एक और साकार विकल्प है — = समूह में केवल एक की अनुमति है।", xorConflictWarn:"पंक्ति {line}: „{label}“ एक और साकार विकल्प है — = समूह में केवल एक की अनुमति है।",
duplicateIdWarn:"पंक्ति {line}: आईडी #{id} पहले से प्रयुक्त है (पंक्ति {firstLine})।", duplicateIdWarn:"पंक्ति {line}: आईडी #{id} पहले से प्रयुक्त है (पंक्ति {firstLine})।",
unknownDepWarn:"पंक्ति {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}“ — तटस्थ रूप में दिखाया गया।",
sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।", sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।",
sourceTimeoutWarn:"„{url}“ ने {seconds} स॰ में उत्तर नहीं दिया — अनुरोध रद्द कर दिया गया। Etherpad सीमित करता है कि एक्सपोर्ट कितनी बार लिया जा सके (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार); कुछ क्षण रुकें, फिर दोबारा लोड करें।", sourceTimeoutWarn:"„{url}“ ने {seconds} स॰ में उत्तर नहीं दिया — अनुरोध रद्द कर दिया गया। Etherpad सीमित करता है कि एक्सपोर्ट कितनी बार लिया जा सके (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार); कुछ क्षण रुकें, फिर दोबारा लोड करें।",
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित", a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।", hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें", hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
hint_xor:"विकल्प, ठीक एक", hint_xor:"विकल्प, ठीक एक",
@@ -1529,6 +1542,7 @@ const I18N = {
hint_comment:"%% से टिप्पणियाँ — पूरी पंक्ति या पंक्ति के अंत में।", hint_comment:"%% से टिप्पणियाँ — पूरी पंक्ति या पंक्ति के अंत में।",
hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।", hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।",
hint_id:"#नाम से नोड आईडी — नोड के टूलटिप में दिखती है।", hint_id:"#नाम से नोड आईडी — नोड के टूलटिप में दिखती है।",
hint_deps:":#नाम,#नाम से निर्भरताएँ — टूलटिप में दिखती हैं।",
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।" hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।"
}, },
zh: { zh: {
@@ -1579,12 +1593,13 @@ const I18N = {
mixedWarn:"第 {line} 行:在「{label}」下 - 与 | 混用——按第一个子项渲染。", mixedWarn:"第 {line} 行:在「{label}」下 - 与 | 混用——按第一个子项渲染。",
xorConflictWarn:"第 {line} 行:「{label}」是又一个已实现的备选项——= 组只允许恰好一个。", xorConflictWarn:"第 {line} 行:「{label}」是又一个已实现的备选项——= 组只允许恰好一个。",
duplicateIdWarn:"第 {line} 行:ID #{id} 已被占用(第 {firstLine} 行)。", duplicateIdWarn:"第 {line} 行:ID #{id} 已被占用(第 {firstLine} 行)。",
unknownDepWarn:"第 {line} 行:依赖 #{id}——没有节点使用此 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}”——显示为中性。",
sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORSAccess-Control-Allow-Origin)。", sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORSAccess-Control-Allow-Origin)。",
sourceTimeoutWarn:"“{url}” 在 {seconds} 秒内没有响应 — 请求已中止。Etherpad 会限制导出的获取频率(默认每 90 秒 10 次);请稍候再重新加载。", sourceTimeoutWarn:"“{url}” 在 {seconds} 秒内没有响应 — 请求已中止。Etherpad 会限制导出的获取频率(默认每 90 秒 10 次);请稍候再重新加载。",
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yId:"ID#{id}", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接", a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yId:"ID#{id}", a11yDeps:"依赖:{ids}", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
hint_indent:"缩进(2 个空格或制表符)定义层级。", hint_indent:"缩进(2 个空格或制表符)定义层级。",
hint_all:"子任务,全部必需", hint_any:"备选项,择其一", hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
hint_xor:"备选项,恰好一个", hint_xor:"备选项,恰好一个",
@@ -1597,6 +1612,7 @@ const I18N = {
hint_comment:"用 %% 注释——整行或行尾。", hint_comment:"用 %% 注释——整行或行尾。",
hint_people:"用 @姓名 表示人员——显示在节点右下角。", hint_people:"用 @姓名 表示人员——显示在节点右下角。",
hint_id:"用 #名称 指定节点 ID——显示在节点提示中。", hint_id:"用 #名称 指定节点 ID——显示在节点提示中。",
hint_deps:"用 :#名称,#名称 表示依赖——显示在提示中。",
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。" hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。"
}, },
ja: { ja: {
@@ -1647,12 +1663,13 @@ const I18N = {
mixedWarn:"{line} 行目:「{label}」の下で - と | が混在 — 最初の子に従って表示。", mixedWarn:"{line} 行目:「{label}」の下で - と | が混在 — 最初の子に従って表示。",
xorConflictWarn:"{line} 行目:「{label}」も実現済みの選択肢です — = グループで実現できるのは 1 つだけです。", xorConflictWarn:"{line} 行目:「{label}」も実現済みの選択肢です — = グループで実現できるのは 1 つだけです。",
duplicateIdWarn:"{line} 行目:ID #{id} は既に使われています({firstLine} 行目)。", duplicateIdWarn:"{line} 行目:ID #{id} は既に使われています({firstLine} 行目)。",
unknownDepWarn:"{line} 行目:依存 #{id} — この 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}」— 中立として表示。",
sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORSAccess-Control-Allow-Origin)を許可する必要があります。", sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORSAccess-Control-Allow-Origin)を許可する必要があります。",
sourceTimeoutWarn:"「{url}」が {seconds} 秒以内に応答しませんでした — 要求を中止しました。Etherpad はエクスポートの取得回数を制限します(既定で 90 秒あたり 10 回)。少し待ってから再読み込みしてください。", sourceTimeoutWarn:"「{url}」が {seconds} 秒以内に応答しませんでした — 要求を中止しました。Etherpad はエクスポートの取得回数を制限します(既定で 90 秒あたり 10 回)。少し待ってから再読み込みしてください。",
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり", a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。", hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ", hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
hint_xor:"選択肢、ちょうど1つ", hint_xor:"選択肢、ちょうど1つ",
@@ -1665,6 +1682,7 @@ const I18N = {
hint_comment:"%% でコメント — 行全体または行末。", hint_comment:"%% でコメント — 行全体または行末。",
hint_people:"@名前 で担当者 — ノードの右下に表示されます。", hint_people:"@名前 で担当者 — ノードの右下に表示されます。",
hint_id:"#名前 でノード ID — ノードのツールチップに表示されます。", hint_id:"#名前 でノード ID — ノードのツールチップに表示されます。",
hint_deps:":#名前,#名前 で依存関係 — ツールチップに表示されます。",
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。" hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。"
} }
}; };
@@ -1698,6 +1716,7 @@ function buildHint(){
${esc(t('hint_comment'))} ${esc(t('hint_comment'))}
${esc(t('hint_people'))} ${esc(t('hint_people'))}
${esc(t('hint_id'))} ${esc(t('hint_id'))}
${esc(t('hint_deps'))}
<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>`;
} }
+25 -4
View File
@@ -26,15 +26,17 @@ 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, status, url, size, Jeder Knoten: {label, type:'and'|'or'|'xor', optional, status, url, size,
tags, id, focus, children, line}. tags, id, deps, focus, children, line}.
`deps` sind ID-Strings, keine Knoten-Referenzen aufgelöst wird erst beim
Konsumenten (D37); der Parser prüft nur die Existenz (`unknownDep`).
`type` ist das Gate der Geschwistergruppe, `optional` (Zeichen `+`, SPEC §3) `type` ist das Gate der Geschwistergruppe, `optional` (Zeichen `+`, SPEC §3)
eine Eigenschaft des einzelnen Knotens: er hängt an derselben Und-Zerlegung eine Eigenschaft des einzelnen Knotens: er hängt an derselben Und-Zerlegung
(`type:'and'`), ist darin aber entbehrlich. Dadurch bleibt die (`type:'and'`), ist darin aber entbehrlich. Dadurch bleibt die
Gemischt-Warnung unverändert richtig sie schlägt an, wenn `|` oder `=` Gemischt-Warnung unverändert richtig sie schlägt an, wenn `|` oder `=`
mit `-`/`+` (oder untereinander) gemischt wird. mit `-`/`+` (oder untereinander) gemischt wird.
Extraktionsreihenfolge (SPEC §1): Kommentar -> Zeichen/Status -> URL -> Größe Extraktionsreihenfolge (SPEC §1): Kommentar -> Zeichen/Status -> URL -> Größe
-> Tags -> Knoten-ID -> Fokusmarke -> Label. Hierarchie über Einrückungsbreite -> Tags -> Knoten-ID -> Abhängigkeiten -> Fokusmarke -> Label. Hierarchie
(Tab = 2 Leerzeichen); über Einrückungsbreite (Tab = 2 Leerzeichen);
Elternknoten ist die nächste vorangehende Zeile mit kleinerer Breite. */ Elternknoten ist die nächste vorangehende Zeile mit kleinerer Breite. */
export function parse(text){ export function parse(text){
const virtualRoot = {label:'', type:'and', children:[]}; const virtualRoot = {label:'', type:'and', children:[]};
@@ -68,6 +70,13 @@ export function parse(text){
`@name`; kein Lookbehind (Safari erst ab 16.4). */ `@name`; kein Lookbehind (Safari erst ab 16.4). */
let id = null; 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}._-]+)/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
der ID). Mehrere Token je Zeile werden zusammengeführt. */
const deps = [];
rest = rest.replace(/(^|\s):#([\p{L}\p{N}._-]+(?:,#[\p{L}\p{N}._-]+)*)/gu,
(s, pre, list) => { for(const p of list.split(',')) deps.push(p.replace(/^#/, '')); return pre; });
/* Fokusmarke `!!!` (SPEC §1) nur ALLEINSTEHEND, damit Achtung!!!" ein /* Fokusmarke `!!!` (SPEC §1) nur ALLEINSTEHEND, damit Achtung!!!" ein
gewöhnliches Label bleibt. Kein Lookbehind (Safari kennt es erst ab 16.4): gewöhnliches Label bleibt. Kein Lookbehind (Safari kennt es erst ab 16.4):
der führende Leerraum wird mitgefangen und wieder eingesetzt. */ der führende Leerraum wird mitgefangen und wieder eingesetzt. */
@@ -93,11 +102,23 @@ 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, status, url, size, tags, id, focus, children:[], line:i+1}; const node = {label, type, optional, status, url, size, tags, id, deps, focus, children:[], line:i+1};
parent.children.push(node); parent.children.push(node);
stack.push({node, width}); stack.push({node, width});
}); });
/* Unbekannte Abhängigkeits-IDs (SPEC §1): erst nach dem Einlesen prüfbar
Vorwärts-Referenzen sind normal. Zyklen (auch auf sich selbst) werden
bewusst NICHT einmal erkannt: Sie sind zulässig und bedeuten wird
gemeinsam fertig" (D34/D37) eine Prüfung hätte keinen Abnehmer. */
(function checkDeps(nodes){
for(const n of nodes){
for(const d of n.deps)
if(!idLines.has(d)) warnings.push({type:'unknownDep', line:n.line, id:d});
checkDeps(n.children);
}
})(virtualRoot.children);
/* XOR-Regel (SPEC §3): In einer `=`-Gruppe darf genau EINE Alternative /* XOR-Regel (SPEC §3): In einer `=`-Gruppe darf genau EINE Alternative
realisiert sein. Jede weitere wird einzeln gemeldet die Warnung zeigt so realisiert sein. Jede weitere wird einzeln gemeldet die Warnung zeigt so
auf die Zeile, die man ansehen muss, statt pauschal auf die Gruppe (D35). auf die Zeile, die man ansehen muss, statt pauschal auf die Gruppe (D35).
+5 -2
View File
@@ -51,9 +51,11 @@ function nodeAria(n, opts){
if(n.size) parts.push(t('a11ySize', {size: n.size})); if(n.size) parts.push(t('a11ySize', {size: n.size}));
else if(cheapPath) parts.push(t('a11ySizeImplicit')); else if(cheapPath) parts.push(t('a11ySizeImplicit'));
if(n.tags && n.tags.length) parts.push(t('a11yTags', {names: n.tags.join(', ')})); if(n.tags && n.tags.length) parts.push(t('a11yTags', {names: n.tags.join(', ')}));
/* Knoten-ID (SPEC §1, D36): keine eigene Darstellung im Diagramm sichtbar /* Knoten-ID und Abhängigkeiten (SPEC §1, D36/D37): keine eigene Darstellung
nur im Tooltip und hier. */ im Diagramm sichtbar nur im Tooltip und hier. */
if(n.id) parts.push(t('a11yId', {id: n.id})); if(n.id) parts.push(t('a11yId', {id: n.id}));
if(n.deps && n.deps.length)
parts.push(t('a11yDeps', {ids: n.deps.map(d => '#' + d).join(', ')}));
if(n.optional) parts.push(t('a11yOptional')); if(n.optional) parts.push(t('a11yOptional'));
/* Die Fokusmarke ist rein als box-shadow sichtbar ohne diese Ansage wüsste /* Die Fokusmarke ist rein als box-shadow sichtbar ohne diese Ansage wüsste
ein Screenreader nichts davon. Zugleich der einzige Ort, an dem sie sich von ein Screenreader nichts davon. Zugleich der einzige Ort, an dem sie sich von
@@ -73,6 +75,7 @@ function nodeHtml(n, extra, opts){
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 : '', const tip = [n.id ? '#' + n.id : '',
n.deps && n.deps.length ? '→ ' + n.deps.map(d => '#' + d).join(', ') : '',
n.status ? t('st_' + n.status.key) : '', n.status ? t('st_' + n.status.key) : '',
n.optional ? t('a11yOptional') : '', t('jumpHint')] n.optional ? t('a11yOptional') : '', t('jumpHint')]
.filter(Boolean).join(' · '); .filter(Boolean).join(' · ');
+5
View File
@@ -15,6 +15,9 @@
- duplicateId { line, id, firstLine } Knoten-ID doppelt vergeben - duplicateId { line, id, firstLine } Knoten-ID doppelt vergeben
(SPEC §1/D36); gemeldet an der späteren (SPEC §1/D36); gemeldet an der späteren
Zeile, die erste wird genannt Zeile, die erste wird genannt
- unknownDep { line, id } Abhängigkeit auf eine ID ohne Knoten
(SPEC §1/D37); Zyklen sind dagegen
zulässig und warnen nie
- 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
@@ -39,6 +42,8 @@ export function formatWarning(w, t){
return t('xorConflictWarn', {line: w.line, label: esc(w.label)}); return t('xorConflictWarn', {line: w.line, label: esc(w.label)});
case 'duplicateId': case 'duplicateId':
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':
return t('unknownDepWarn', {line: w.line, id: esc(w.id)});
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':
+82
View File
@@ -0,0 +1,82 @@
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()});
/* Abhängigkeiten `:#a,#b` (SPEC §1, D37): der Knoten hängt von den Knoten mit
diesen IDs ab auch außerhalb seines eigenen Teilbaums. */
describe('Parser — `:#a,#b` als Abhängigkeits-Token', () => {
it('extrahiert die ID-Liste und nimmt sie aus dem Label', () => {
const [a, b] = roots(`[ ] Auth #auth\n[ ] Deploy :#auth (M)`);
expect([b.label, b.deps, b.size]).toEqual(['Deploy', ['auth'], 'M']);
expect(a.deps).toEqual([]);
});
it('liest mehrere IDs aus einem Token', () => {
const [wurzel] = roots(`[ ] A #a\n[ ] B #b\n[ ] C :#a,#b`);
expect(roots(`[ ] A #a\n[ ] B #b\n[ ] C :#a,#b`)[2].deps).toEqual(['a', 'b']);
});
it('führt mehrere Token je Zeile zusammen', () => {
const {roots: r, warnings} = parse(`[ ] A #a\n[ ] B #b\n[ ] C :#a später :#b`);
expect(r[2].deps).toEqual(['a', 'b']);
expect(r[2].label).toBe('C später');
expect(warnings).toEqual([]);
});
it('erkennt das Token nur alleinstehend — eingeklammerte Erwähnung bleibt Label', () => {
const [wurzel] = roots(`[ ] Dependencies across the tree, (:#auth,#api) (M)`);
expect([wurzel.label, wurzel.deps]).toEqual(
['Dependencies across the tree, (:#auth,#api)', []]);
});
it('lässt einen gewöhnlichen Doppelpunkt im Label stehen', () => {
const [wurzel] = roots(`[ ] Merke: wichtig`);
expect([wurzel.label, wurzel.deps]).toEqual(['Merke: wichtig', []]);
});
it('endet am Leerraum: `:#a, #b` liest nur #a — das #b wird Knoten-ID', () => {
/* Der dokumentierte Stolperstein (SPEC §1): die Liste ist EIN Token. */
const [a, b] = roots(`[ ] A #a\n[ ] B :#a, #b`);
expect([b.deps, b.id, b.label]).toEqual([['a'], 'b', 'B ,']);
});
it('verträgt sich mit der eigenen Knoten-ID auf derselben Zeile', () => {
const nodes = roots(`[ ] A #a\n[ ] B #b :#a`);
expect([nodes[1].id, nodes[1].deps]).toEqual(['b', ['a']]);
});
});
describe('Unbekannte IDs warnen — Zyklen nie', () => {
it('meldet eine Abhängigkeit ohne Zielknoten mit Zeilennummer', () => {
const {warnings} = parse(`[ ] A #a\n[ ] B :#a,#fehlt`);
expect(warnings).toEqual([{type: 'unknownDep', line: 2, id: 'fehlt'}]);
});
it('erlaubt Vorwärts-Referenzen (Ziel steht weiter unten)', () => {
expect(parse(`[ ] A :#b\n[ ] B #b`).warnings).toEqual([]);
});
it('warnt nicht bei Zyklen — auch nicht bei Selbst-Abhängigkeit', () => {
expect(parse(`[ ] A #a :#b\n[ ] B #b :#a`).warnings).toEqual([]);
expect(parse(`[ ] A #a :#a`).warnings).toEqual([]);
});
});
describe('Darstellung — Tooltip und aria, kein eigenes Zeichen', () => {
it('zeigt die Abhängigkeiten als → im Tooltip und als a11yDeps', () => {
const {html} = render(`[ ] A #a\n[ ] B #b\n[ ] C :#a,#b`);
expect(html).toContain('title="→ #a, #b · st_geplant · jumpHint"');
expect(html).toContain('aria-label="C, a11yStatus, a11yDeps"');
});
it('ändert Knoten ohne Abhängigkeiten nicht', () => {
const {html} = render(`[ ] Ohne`);
expect(html).not.toContain('a11yDeps');
expect(html).not.toContain('→');
});
});
+3 -2
View File
@@ -21,9 +21,10 @@ describe('Parser — `#name` als Knoten-ID', () => {
expect([wurzel.label, wurzel.id]).toEqual(['C# Kurs', null]); expect([wurzel.label, wurzel.id]).toEqual(['C# Kurs', null]);
}); });
it('frisst das reservierte `:#a,#b` (Abhängigkeiten, §11) nicht', () => { it('macht aus `:#a,#b` (Abhängigkeiten, D37) keine Knoten-ID', () => {
const [wurzel] = roots(`[ ] Deploy :#auth,#api`); const [wurzel] = roots(`[ ] Deploy :#auth,#api`);
expect([wurzel.label, wurzel.id]).toEqual(['Deploy :#auth,#api', null]); expect([wurzel.label, wurzel.id, wurzel.deps])
.toEqual(['Deploy', null, ['auth', 'api']]);
}); });
it('nimmt nur das ERSTE Token — weitere `#` bleiben im Label (Tickets)', () => { it('nimmt nur das ERSTE Token — weitere `#` bleiben im Label (Tickets)', () => {