feat: Knoten-IDs (#auth) parsen — doppelte ID warnt (SPEC §1, D36)
Extraktionsschritt 6 (nach den Tags): das erste alleinstehend angesetzte `#name`-Token wird die Knoten-ID (Zeichenmenge wie `@name`); weitere `#`-Token bleiben im Label (reservierte Ticket-Referenzen), `:#a,#b` und `C#` werden nicht gefressen. Doppelte ID → Warnung duplicateId an der späteren Zeile mit Nennung der ersten; die spätere gilt trotzdem. Sichtbar im Tooltip (erste Position) und als a11yId im aria-label — noch keine eigene Diagramm-Darstellung. Legendenzeile + Warntext in allen 9 Sprachen; die drei `#`-Erwähnungen im mitgelieferten Plan sind eingeklammert, damit sie Erwähnungen bleiben. 12 neue Tests (tests/ids.test.js). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d276c94840
commit
6ecf3e4c69
@@ -1660,3 +1660,45 @@ kopierten Regeln — hätte jede künftige Layoutänderung doppelt pflegen lasse
|
|||||||
Im Modell ist `'xor'` ein eigener Gate-Wert (`gateOf`), damit die
|
Im Modell ist `'xor'` ein eigener Gate-Wert (`gateOf`), damit die
|
||||||
`mixedGate`-Warnung Mischungen mit `|` von selbst meldet; alle
|
`mixedGate`-Warnung Mischungen mit `|` von selbst meldet; alle
|
||||||
Disjunktiv-Abfragen prüfen `!== 'and'`.
|
Disjunktiv-Abfragen prüfen `!== 'and'`.
|
||||||
|
|
||||||
|
## D36 — Knoten-IDs (`#auth`) umgesetzt: eng gefasst, sichtbar nur im Tooltip
|
||||||
|
Der erste Baustein der Phase-4-Kette (ohne IDs keine Abhängigkeiten, ohne die
|
||||||
|
kein effektiver Status). Vier Festlegungen, die §11 offen ließ:
|
||||||
|
|
||||||
|
**Zeichenmenge wie `@name`, nicht „whitespace-frei“.** §11 sagte „ein
|
||||||
|
whitespace-freier Bezeichner“; umgesetzt ist die engere Menge aus §7
|
||||||
|
(Unicode-Buchstaben, Ziffern, `.`, `_`, `-`). Drei Gründe: Konsistenz mit den
|
||||||
|
beiden Nachbarn (`@name` heute, `&tag` reserviert mit derselben Menge, D34);
|
||||||
|
ein `#a/b` oder `#a:b` liefe sonst in dieselben Kollisionen, vor denen §11 bei
|
||||||
|
`:` und Pfaden gerade ausweicht; und enger → weiter ist später kompatibel
|
||||||
|
möglich, umgekehrt nicht.
|
||||||
|
|
||||||
|
**Nur alleinstehend angesetzt** (`(^|\s)#…`, wie beim reservierten `&tag`):
|
||||||
|
„C#“ bleibt ein Label, und — entscheidend für den nächsten Schritt — die
|
||||||
|
Abhängigkeits-Schreibweise `:#a,#b` wird **nicht** als ID gefressen, weil dort
|
||||||
|
`:` bzw. `,` vor dem `#` steht. Die ID-Extraktion muss beim Bau der
|
||||||
|
Abhängigkeiten also nicht angefasst werden.
|
||||||
|
|
||||||
|
**Das erste Token ist die ID, weitere bleiben im Label.** Die ID benennt genau
|
||||||
|
einen Knoten — mehr als eine pro Zeile ergibt keinen Sinn. Alles nach dem
|
||||||
|
ersten Treffer bleibt unangetastet stehen, denn dort wohnt die reservierte
|
||||||
|
Ticket-Referenz (`… #123 …`, §11): Sie soll sichtbar im Label bleiben, bis das
|
||||||
|
Taiga-Feature sie auflöst. Deshalb wurden auch die drei `#`-Vorkommen im
|
||||||
|
mitgelieferten Plan eingeklammert (`(#auth)`, `(#123)`) — als Erwähnungen sind
|
||||||
|
sie keine IDs, und `#123` wäre sonst doppelt vergeben gewesen (Zeile 25/162).
|
||||||
|
|
||||||
|
**Sichtbar im Tooltip und `aria-label`, sonst nirgends.** Die ID gehört nicht
|
||||||
|
zum Label (sonst änderte das spätere Entfernen die Knoten-Identität der
|
||||||
|
„Was ist neu?“-Anzeige, D28). Ganz unsichtbar wäre aber nutzerfeindlich —
|
||||||
|
getippter Text verschwände spurlos. Der Tooltip zeigt `#id` als erste Zeile,
|
||||||
|
der Screenreader bekommt `a11yId`; ein eigenes Badge bekommt sie erst, wenn
|
||||||
|
etwas darauf zeigt (Querverbindungen, §11) — die Knoten-Ecken sind belegt
|
||||||
|
(D18).
|
||||||
|
|
||||||
|
**Doppelte ID: Warnung an der späteren Zeile, mit Nennung der ersten.** Die
|
||||||
|
Meldung zeigt dorthin, wo man eingreifen muss, und `{firstLine}` erspart das
|
||||||
|
Suchen. Die spätere ID gilt trotzdem am Knoten (fehlertolerant wie §4);
|
||||||
|
welcher Knoten bei Verweisen „gewinnt“, entscheidet erst 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
|
||||||
|
die ID nicht.
|
||||||
|
|||||||
+31
-7
@@ -19,8 +19,27 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
|||||||
3. URL: erstes Token, das auf `https?://\S+` passt (dadurch stören `@` in URLs nicht).
|
3. URL: erstes Token, das auf `https?://\S+` passt (dadurch stören `@` in URLs nicht).
|
||||||
4. Größe: erstes `(XS|S|M|L|XL|XXL)`, Groß-/Kleinschreibung egal.
|
4. Größe: erstes `(XS|S|M|L|XL|XXL)`, Groß-/Kleinschreibung egal.
|
||||||
5. Tags: alle `@name`-Vorkommen.
|
5. Tags: alle `@name`-Vorkommen.
|
||||||
6. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten).
|
6. Knoten-ID: das **erste** alleinstehend angesetzte `#name`-Token (siehe unten).
|
||||||
7. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren.
|
7. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten).
|
||||||
|
8. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren.
|
||||||
|
|
||||||
|
**Knoten-ID `#name`** — benennt einen Knoten im **ganzen Dokument** eindeutig;
|
||||||
|
sie ist die Adresse für Abhängigkeiten und Beschreibungsblöcke (§11).
|
||||||
|
|
||||||
|
- Zeichenmenge wie bei `@name` (§7): Unicode-Buchstaben, Ziffern, `.`, `_`, `-`.
|
||||||
|
(Enger als das frühere „whitespace-frei“ aus §11 — Begründung: D36.)
|
||||||
|
- Erkannt nur **alleinstehend angesetzt** (`(^|\s)#…`): „C#“ bleibt damit ein
|
||||||
|
Label, und der für Abhängigkeiten reservierte Doppelpunkt `:#a,#b` (§11)
|
||||||
|
kollidiert nicht.
|
||||||
|
- Das **erste** solche Token der Zeile ist die ID; weitere `#`-Token bleiben im
|
||||||
|
Label stehen (dort liegt die reservierte Ticket-Referenz `#123`, §11). Eine
|
||||||
|
rein numerische ID ist zugleich die künftige Ticket-Referenz — oft ist die
|
||||||
|
Ticket-Nummer die natürliche Knoten-ID (D34).
|
||||||
|
- Die ID gehört **nicht** zum Label. Eine eigene Darstellung im Diagramm hat
|
||||||
|
sie (noch) nicht; sichtbar ist sie im Knoten-Tooltip und im `aria-label`.
|
||||||
|
- **Doppelte ID:** Warnung `duplicateId` mit beiden Zeilennummern; die spätere
|
||||||
|
ID gilt trotzdem am Knoten (fehlertolerant wie §4 — die Zeile geht nicht
|
||||||
|
verloren).
|
||||||
|
|
||||||
**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
|
||||||
@@ -45,7 +64,13 @@ Referenz-Regex der Implementierung:
|
|||||||
^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:\[([ ?~xX^/-])\]\s*)?(.*)$
|
^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:\[([ ?~xX^/-])\]\s*)?(.*)$
|
||||||
```
|
```
|
||||||
|
|
||||||
Für die Fokusmarke (Schritt 6):
|
Für die Knoten-ID (Schritt 6, nur der erste Treffer):
|
||||||
|
|
||||||
|
```
|
||||||
|
(^|\s)#([\p{L}\p{N}._-]+)
|
||||||
|
```
|
||||||
|
|
||||||
|
Für die Fokusmarke (Schritt 7):
|
||||||
|
|
||||||
```
|
```
|
||||||
(^|\s)!!!(?=\s|$)
|
(^|\s)!!!(?=\s|$)
|
||||||
@@ -532,10 +557,9 @@ das auch. Begründung und Zusammenhang: D34.
|
|||||||
- `#123` — Referenz auf externe Tickets (geplant für Taiga-Integration).
|
- `#123` — Referenz auf externe Tickets (geplant für Taiga-Integration).
|
||||||
Ticket-Referenzen werden **so** notiert, weil es die etablierte
|
Ticket-Referenzen werden **so** notiert, weil es die etablierte
|
||||||
Kurzschreibweise ist; sie haben unter den `#`-Verwendungen Vorrang.
|
Kurzschreibweise ist; sie haben unter den `#`-Verwendungen Vorrang.
|
||||||
- `#auth` — **Knoten-ID**: ein whitespace-freier Bezeichner, der einen Knoten im
|
- `#auth` — **Knoten-ID**: **umgesetzt**, Definition jetzt in §1 (Zeichenmenge,
|
||||||
ganzen Dokument eindeutig benennt. Ziel für Abhängigkeiten und
|
Alleinstehend-Regel, Warnung `duplicateId`). Ziel für Abhängigkeiten und
|
||||||
Beschreibungsblöcke (siehe unten). Zwei Knoten mit derselben ID sind ein
|
Beschreibungsblöcke (siehe unten).
|
||||||
Fehler und bekommen eine Warnung mit Zeilennummer (§4).
|
|
||||||
|
|
||||||
Beide Rollen vertragen sich: Oft **ist** die Ticket-Nummer die natürliche
|
Beide Rollen vertragen sich: Oft **ist** die Ticket-Nummer die natürliche
|
||||||
Knoten-ID. Als Ticket-Link behandelt wird heuristisch das rein **numerische**
|
Knoten-ID. Als Ticket-Link behandelt wird heuristisch das rein **numerische**
|
||||||
|
|||||||
+6
-1
@@ -71,7 +71,12 @@ entscheiden, **bevor** Code entsteht.
|
|||||||
numerisch, oft zugleich die natürliche Knoten-ID; notfalls Präfix wie
|
numerisch, oft zugleich die natürliche Knoten-ID; notfalls Präfix wie
|
||||||
`#t123`); Schlagworte gehen auf `&tag` — niedrig priorisiert, gebaut
|
`#t123`); Schlagworte gehen auf `&tag` — niedrig priorisiert, gebaut
|
||||||
erst mit dem ersten Konsumenten (D34-Nachtrag).
|
erst mit dem ersten Konsumenten (D34-Nachtrag).
|
||||||
- [ ] Knoten-IDs parsen; doppelte ID → Warnung mit Zeilennummer.
|
- [x] Knoten-IDs parsen; doppelte ID → Warnung mit Zeilennummer.
|
||||||
|
→ Umgesetzt (D36): Zeichenmenge wie `@name`, nur alleinstehend angesetzt
|
||||||
|
und nur das erste Token je Zeile (`:#a,#b` und Ticket-Erwähnungen bleiben
|
||||||
|
unberührt); Warnung `duplicateId` nennt beide Zeilen; sichtbar im
|
||||||
|
Tooltip + `aria-label`; SPEC-§11-Teil nach §1 überführt;
|
||||||
|
`tests/ids.test.js`.
|
||||||
- [ ] Abhängigkeiten `:#a,#b` parsen; unbekannte ID → Warnung, Zyklen erlaubt.
|
- [ ] Abhängigkeiten `:#a,#b` parsen; unbekannte ID → Warnung, Zyklen erlaubt.
|
||||||
- [ ] 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.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
- [^] People tags, bare URLs, %% comments (XS)
|
- [^] People tags, bare URLs, %% comments (XS)
|
||||||
- [^] And/or decomposition (S)
|
- [^] And/or decomposition (S)
|
||||||
- [^] Optional nodes — neither required nor an alternative (S)
|
- [^] Optional nodes — neither required nor an alternative (S)
|
||||||
- [?] 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)
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
- [?] A short text right at the node (S) %% indentation already means hierarchy
|
- [?] A short text right at the node (S) %% indentation already means hierarchy
|
||||||
- [?] A long block at the end, addressed by its id (S)
|
- [?] A long block at the end, addressed by its id (S)
|
||||||
- [-] A separate storage format for the structure (L) %% the text is the format
|
- [-] A separate storage format for the structure (L) %% the text is the format
|
||||||
- [ ] Ticket references #123 (S)
|
- [ ] Ticket references (#123) (S)
|
||||||
- [?] Benefit per node, not only cost (M)
|
- [?] Benefit per node, not only cost (M)
|
||||||
| [?] Another sigil next to the size (S)
|
| [?] Another sigil next to the size (S)
|
||||||
| [?] Story points behind the T-shirt sizes (M)
|
| [?] Story points behind the T-shirt sizes (M)
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
| [?] Run the one JS parser inside the IDE (M)
|
| [?] Run the one JS parser inside the IDE (M)
|
||||||
- [?] Tracker integration (XL)
|
- [?] Tracker integration (XL)
|
||||||
| [?] Taiga (L) https://taiga.io
|
| [?] Taiga (L) https://taiga.io
|
||||||
- [ ] Resolve #123 over the REST API (M)
|
- [ ] Resolve "#123" over the REST API (M)
|
||||||
- [ ] Read title, link and status (S)
|
- [ ] Read title, link and status (S)
|
||||||
- [ ] Map the workflow onto the states (S)
|
- [ ] Map the workflow onto the states (S)
|
||||||
- [?] Write the status back (M)
|
- [?] Write the status back (M)
|
||||||
|
|||||||
+8
-1
@@ -47,7 +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 (sonst kollidiert `@` in URLs).
|
Status → URL → Größe → Tags → Knoten-ID → Fokusmarke (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
|
||||||
@@ -266,6 +267,12 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
passiert: Zähler stimmte, nichts leuchtete). Vorgehalten wird nur
|
passiert: Zähler stimmte, nichts leuchtete). Vorgehalten wird nur
|
||||||
`freshPrevRoots` (Basis, einmal geparst). Basis je Dokument in `werkbaum-seen`,
|
`freshPrevRoots` (Basis, einmal geparst). Basis je Dokument in `werkbaum-seen`,
|
||||||
fortgeschrieben **erst beim Bestätigen** über `#freshBtn`.
|
fortgeschrieben **erst beim Bestätigen** über `#freshBtn`.
|
||||||
|
- Knoten-IDs `#name` (SPEC §1/D36): nur **alleinstehend angesetzt** und nur
|
||||||
|
der **erste** Treffer der Zeile (kein `/g`!) — weitere `#`-Token bleiben im
|
||||||
|
Label (reservierte Ticket-Referenzen), und `:#a,#b` (künftige Abhängigkeiten)
|
||||||
|
darf nicht gefressen werden. Zeichenmenge wie `@name`. Doppelte ID →
|
||||||
|
`{type:'duplicateId', line, id, firstLine}`; die spätere gilt trotzdem.
|
||||||
|
Keine eigene Darstellung — nur Tooltip (erste Position) und `a11yId`.
|
||||||
- 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
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
Ab <code>(M)</code> gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.<br>
|
Ab <code>(M)</code> gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.<br>
|
||||||
Kommentare mit <code>%%</code> — als ganze Zeile oder am Zeilenende.
|
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.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+28
-9
@@ -1102,12 +1102,13 @@ const I18N = {
|
|||||||
empty:"Noch keine Struktur — einfach lostippen.", ghost:"…",
|
empty:"Noch keine Struktur — einfach lostippen.", ghost:"…",
|
||||||
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}).",
|
||||||
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}", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
|
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", 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",
|
||||||
@@ -1119,6 +1120,7 @@ const I18N = {
|
|||||||
hint_break:"Ab (M) gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.",
|
hint_break:"Ab (M) gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.",
|
||||||
hint_comment:"Kommentare mit %% — als ganze Zeile oder am Zeilenende.",
|
hint_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_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: {
|
||||||
@@ -1168,12 +1170,13 @@ const I18N = {
|
|||||||
empty:"No structure yet — just start typing.", ghost:"…",
|
empty:"No structure yet — just start typing.", ghost:"…",
|
||||||
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}).",
|
||||||
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}", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
|
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", 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",
|
||||||
@@ -1185,6 +1188,7 @@ const I18N = {
|
|||||||
hint_break:"From (M) on: break it down further — if the breakdown is missing, a placeholder appears in the diagram.",
|
hint_break:"From (M) on: break it down further — if the breakdown is missing, a placeholder appears in the diagram.",
|
||||||
hint_comment:"Comments with %% — whole line or at the end of a line.",
|
hint_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_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: {
|
||||||
@@ -1234,12 +1238,13 @@ const I18N = {
|
|||||||
empty:"Aún no hay estructura — simplemente empieza a escribir.", ghost:"…",
|
empty:"Aún no hay estructura — simplemente empieza a escribir.", ghost:"…",
|
||||||
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}).",
|
||||||
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}", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
|
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", 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",
|
||||||
@@ -1251,6 +1256,7 @@ const I18N = {
|
|||||||
hint_break:"A partir de (M): sigue desglosando — si falta el desglose, aparece un marcador de posición en el diagrama.",
|
hint_break:"A partir de (M): sigue desglosando — si falta el desglose, aparece un marcador de posición en el diagrama.",
|
||||||
hint_comment:"Comentarios con %% — línea completa o al final de la línea.",
|
hint_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_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: {
|
||||||
@@ -1300,12 +1306,13 @@ const I18N = {
|
|||||||
empty:"Pas encore de structure — commencez à taper.", ghost:"…",
|
empty:"Pas encore de structure — commencez à taper.", ghost:"…",
|
||||||
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 = n’en autorise qu’une seule.",
|
xorConflictWarn:"Ligne {line} : « {label} » est une alternative réalisée de plus — un groupe = n’en autorise qu’une seule.",
|
||||||
|
duplicateIdWarn:"Ligne {line} : l’ID #{id} est déjà attribué (ligne {firstLine}).",
|
||||||
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} » n’a pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de l’export (10 fois par 90 s par défaut) ; attends un instant, puis recharge.",
|
sourceTimeoutWarn:"« {url} » n’a pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de l’export (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}", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
|
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", 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",
|
||||||
@@ -1317,6 +1324,7 @@ const I18N = {
|
|||||||
hint_break:"À partir de (M) : décomposer davantage — si la décomposition manque, un espace réservé apparaît dans le diagramme.",
|
hint_break:"À partir de (M) : décomposer davantage — si la décomposition manque, un espace réservé apparaît dans le diagramme.",
|
||||||
hint_comment:"Commentaires avec %% — ligne entière ou en fin de ligne.",
|
hint_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 l’infobulle du nœud.",
|
||||||
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: {
|
||||||
@@ -1366,12 +1374,13 @@ const I18N = {
|
|||||||
empty:"Brak struktury — zacznij pisać.", ghost:"…",
|
empty:"Brak struktury — zacznij pisać.", ghost:"…",
|
||||||
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}).",
|
||||||
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}", 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}", 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",
|
||||||
@@ -1383,6 +1392,7 @@ const I18N = {
|
|||||||
hint_break:"Od (M): dziel dalej — gdy brakuje podziału, w diagramie pojawia się symbol zastępczy.",
|
hint_break:"Od (M): dziel dalej — gdy brakuje podziału, w diagramie pojawia się symbol zastępczy.",
|
||||||
hint_comment:"Komentarze z %% — cały wiersz lub na końcu wiersza.",
|
hint_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_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: {
|
||||||
@@ -1432,12 +1442,13 @@ const I18N = {
|
|||||||
empty:"Пока нет структуры — просто начните печатать.", ghost:"…",
|
empty:"Пока нет структуры — просто начните печатать.", ghost:"…",
|
||||||
mixedWarn:"Строка {line}: под «{label}» смешаны - и | — отображается по первому потомку.",
|
mixedWarn:"Строка {line}: под «{label}» смешаны - и | — отображается по первому потомку.",
|
||||||
xorConflictWarn:"Строка {line}: «{label}» — ещё одна реализованная альтернатива, а группа = допускает ровно одну.",
|
xorConflictWarn:"Строка {line}: «{label}» — ещё одна реализованная альтернатива, а группа = допускает ровно одну.",
|
||||||
|
duplicateIdWarn:"Строка {line}: ID #{id} уже занят (строка {firstLine}).",
|
||||||
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}", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
||||||
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
||||||
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
||||||
hint_xor:"альтернатива, ровно одна",
|
hint_xor:"альтернатива, ровно одна",
|
||||||
@@ -1449,6 +1460,7 @@ const I18N = {
|
|||||||
hint_break:"С (M): дробите дальше — если декомпозиции нет, в диаграмме появляется заполнитель.",
|
hint_break:"С (M): дробите дальше — если декомпозиции нет, в диаграмме появляется заполнитель.",
|
||||||
hint_comment:"Комментарии через %% — вся строка или в конце строки.",
|
hint_comment:"Комментарии через %% — вся строка или в конце строки.",
|
||||||
hint_people:"Люди через @имя — показываются справа внизу узла.",
|
hint_people:"Люди через @имя — показываются справа внизу узла.",
|
||||||
|
hint_id:"ID узла через #имя — виден во всплывающей подсказке узла.",
|
||||||
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме."
|
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме."
|
||||||
},
|
},
|
||||||
hi: {
|
hi: {
|
||||||
@@ -1498,12 +1510,13 @@ const I18N = {
|
|||||||
empty:"अभी कोई संरचना नहीं — बस टाइप करना शुरू करें।", ghost:"…",
|
empty:"अभी कोई संरचना नहीं — बस टाइप करना शुरू करें।", ghost:"…",
|
||||||
mixedWarn:"पंक्ति {line}: „{label}“ के अंतर्गत - और | मिश्रित हैं — पहले चाइल्ड के अनुसार दिखाया गया।",
|
mixedWarn:"पंक्ति {line}: „{label}“ के अंतर्गत - और | मिश्रित हैं — पहले चाइल्ड के अनुसार दिखाया गया।",
|
||||||
xorConflictWarn:"पंक्ति {line}: „{label}“ एक और साकार विकल्प है — = समूह में केवल एक की अनुमति है।",
|
xorConflictWarn:"पंक्ति {line}: „{label}“ एक और साकार विकल्प है — = समूह में केवल एक की अनुमति है।",
|
||||||
|
duplicateIdWarn:"पंक्ति {line}: आईडी #{id} पहले से प्रयुक्त है (पंक्ति {firstLine})।",
|
||||||
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}", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
||||||
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
||||||
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
||||||
hint_xor:"विकल्प, ठीक एक",
|
hint_xor:"विकल्प, ठीक एक",
|
||||||
@@ -1515,6 +1528,7 @@ const I18N = {
|
|||||||
hint_break:"(M) से आगे: और विभाजित करें — विभाजन न होने पर आरेख में प्लेसहोल्डर दिखता है।",
|
hint_break:"(M) से आगे: और विभाजित करें — विभाजन न होने पर आरेख में प्लेसहोल्डर दिखता है।",
|
||||||
hint_comment:"%% से टिप्पणियाँ — पूरी पंक्ति या पंक्ति के अंत में।",
|
hint_comment:"%% से टिप्पणियाँ — पूरी पंक्ति या पंक्ति के अंत में।",
|
||||||
hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।",
|
hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।",
|
||||||
|
hint_id:"#नाम से नोड आईडी — नोड के टूलटिप में दिखती है।",
|
||||||
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।"
|
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।"
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
@@ -1564,12 +1578,13 @@ const I18N = {
|
|||||||
empty:"还没有结构——直接开始输入吧。", ghost:"…",
|
empty:"还没有结构——直接开始输入吧。", ghost:"…",
|
||||||
mixedWarn:"第 {line} 行:在「{label}」下 - 与 | 混用——按第一个子项渲染。",
|
mixedWarn:"第 {line} 行:在「{label}」下 - 与 | 混用——按第一个子项渲染。",
|
||||||
xorConflictWarn:"第 {line} 行:「{label}」是又一个已实现的备选项——= 组只允许恰好一个。",
|
xorConflictWarn:"第 {line} 行:「{label}」是又一个已实现的备选项——= 组只允许恰好一个。",
|
||||||
|
duplicateIdWarn:"第 {line} 行:ID #{id} 已被占用(第 {firstLine} 行)。",
|
||||||
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}", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yId:"ID:#{id}", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
||||||
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
||||||
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
||||||
hint_xor:"备选项,恰好一个",
|
hint_xor:"备选项,恰好一个",
|
||||||
@@ -1581,6 +1596,7 @@ const I18N = {
|
|||||||
hint_break:"从 (M) 起:继续细分——若缺少细分,图表中会出现占位符。",
|
hint_break:"从 (M) 起:继续细分——若缺少细分,图表中会出现占位符。",
|
||||||
hint_comment:"用 %% 注释——整行或行尾。",
|
hint_comment:"用 %% 注释——整行或行尾。",
|
||||||
hint_people:"用 @姓名 表示人员——显示在节点右下角。",
|
hint_people:"用 @姓名 表示人员——显示在节点右下角。",
|
||||||
|
hint_id:"用 #名称 指定节点 ID——显示在节点提示中。",
|
||||||
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。"
|
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。"
|
||||||
},
|
},
|
||||||
ja: {
|
ja: {
|
||||||
@@ -1630,12 +1646,13 @@ const I18N = {
|
|||||||
empty:"まだ構造がありません — 入力を始めてください。", ghost:"…",
|
empty:"まだ構造がありません — 入力を始めてください。", ghost:"…",
|
||||||
mixedWarn:"{line} 行目:「{label}」の下で - と | が混在 — 最初の子に従って表示。",
|
mixedWarn:"{line} 行目:「{label}」の下で - と | が混在 — 最初の子に従って表示。",
|
||||||
xorConflictWarn:"{line} 行目:「{label}」も実現済みの選択肢です — = グループで実現できるのは 1 つだけです。",
|
xorConflictWarn:"{line} 行目:「{label}」も実現済みの選択肢です — = グループで実現できるのは 1 つだけです。",
|
||||||
|
duplicateIdWarn:"{line} 行目:ID #{id} は既に使われています({firstLine} 行目)。",
|
||||||
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}", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
|
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", 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つ",
|
||||||
@@ -1647,6 +1664,7 @@ const I18N = {
|
|||||||
hint_break:"(M) 以上:さらに分解 — 分解がないと図にプレースホルダーが表示されます。",
|
hint_break:"(M) 以上:さらに分解 — 分解がないと図にプレースホルダーが表示されます。",
|
||||||
hint_comment:"%% でコメント — 行全体または行末。",
|
hint_comment:"%% でコメント — 行全体または行末。",
|
||||||
hint_people:"@名前 で担当者 — ノードの右下に表示されます。",
|
hint_people:"@名前 で担当者 — ノードの右下に表示されます。",
|
||||||
|
hint_id:"#名前 でノード ID — ノードのツールチップに表示されます。",
|
||||||
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。"
|
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1679,6 +1697,7 @@ function buildHint(){
|
|||||||
${esc(t('hint_break'))}<br>
|
${esc(t('hint_break'))}<br>
|
||||||
${esc(t('hint_comment'))}
|
${esc(t('hint_comment'))}
|
||||||
${esc(t('hint_people'))}
|
${esc(t('hint_people'))}
|
||||||
|
${esc(t('hint_id'))}
|
||||||
<code>!!!</code> ${esc(t('hint_focus'))}
|
<code>!!!</code> ${esc(t('hint_focus'))}
|
||||||
<div class="hint-op">${esc(t('hint_jump'))}</div>`;
|
<div class="hint-op">${esc(t('hint_jump'))}</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -26,19 +26,21 @@ 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, focus, children, line}.
|
tags, id, focus, children, line}.
|
||||||
`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 -> Fokusmarke -> Label. Hierarchie über Einrückungsbreite (Tab = 2 Leerzeichen);
|
-> Tags -> Knoten-ID -> Fokusmarke -> Label. Hierarchie ü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:[]};
|
||||||
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) */
|
||||||
|
|
||||||
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) */
|
||||||
@@ -59,6 +61,13 @@ export function parse(text){
|
|||||||
rest = rest.replace(/https?:\/\/\S+/i, s => { url = s; return ''; });
|
rest = rest.replace(/https?:\/\/\S+/i, s => { url = s; return ''; });
|
||||||
rest = rest.replace(/\((XXL|XS|XL|S|M|L)\)/i, (s, g) => { size = g.toUpperCase(); return ''; });
|
rest = rest.replace(/\((XXL|XS|XL|S|M|L)\)/i, (s, g) => { size = g.toUpperCase(); return ''; });
|
||||||
rest = rest.replace(/@([\p{L}\p{N}._-]+)/gu, (s, g) => { tags.push(g); return ''; });
|
rest = rest.replace(/@([\p{L}\p{N}._-]+)/gu, (s, g) => { tags.push(g); return ''; });
|
||||||
|
/* Knoten-ID `#name` (SPEC §1, D36): nur ALLEINSTEHEND ANGESETZT — „C#"
|
||||||
|
bleibt Label, und das reservierte `:#a,#b` (§11) wird nicht gefressen.
|
||||||
|
Nur der ERSTE Treffer (kein /g): weitere `#`-Token bleiben im Label
|
||||||
|
stehen, dort wohnt die reservierte Ticket-Referenz. Zeichenmenge wie bei
|
||||||
|
`@name`; kein Lookbehind (Safari erst ab 16.4). */
|
||||||
|
let id = null;
|
||||||
|
rest = rest.replace(/(^|\s)#([\p{L}\p{N}._-]+)/u, (s, pre, g) => { id = g; 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. */
|
||||||
@@ -73,10 +82,18 @@ export function parse(text){
|
|||||||
if(!status) warnings.push({type:'unknownStatus', line:i+1, code:boxChar});
|
if(!status) warnings.push({type:'unknownStatus', line:i+1, code:boxChar});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Doppelte ID (SPEC §1): Warnung an der späteren Zeile, mit Nennung der
|
||||||
|
ersten; die spätere ID gilt trotzdem am Knoten (fehlertolerant). Erst
|
||||||
|
hier — eine Zeile ohne Label ist schon zurückgekehrt und belegt nichts. */
|
||||||
|
if(id != null){
|
||||||
|
if(idLines.has(id)) warnings.push({type:'duplicateId', line:i+1, id, firstLine:idLines.get(id)});
|
||||||
|
else idLines.set(id, i+1);
|
||||||
|
}
|
||||||
|
|
||||||
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, focus, children:[], line:i+1};
|
const node = {label, type, optional, status, url, size, tags, id, focus, children:[], line:i+1};
|
||||||
parent.children.push(node);
|
parent.children.push(node);
|
||||||
stack.push({node, width});
|
stack.push({node, width});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ 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
|
||||||
|
nur im Tooltip und hier. */
|
||||||
|
if(n.id) parts.push(t('a11yId', {id: n.id}));
|
||||||
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
|
||||||
@@ -69,7 +72,8 @@ function nodeHtml(n, extra, opts){
|
|||||||
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.status ? t('st_' + n.status.key) : '',
|
const tip = [n.id ? '#' + n.id : '',
|
||||||
|
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(' · ');
|
||||||
const title = ` title="${attr(tip)}"`;
|
const title = ` title="${attr(tip)}"`;
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
- xorConflict { line, label } — weitere realisierte Alternative in einer
|
- xorConflict { line, label } — weitere realisierte Alternative in einer
|
||||||
`=`-Gruppe (SPEC §3/D35); je Zeile eine
|
`=`-Gruppe (SPEC §3/D35); je Zeile eine
|
||||||
Warnung, damit sie dorthin zeigt
|
Warnung, damit sie dorthin zeigt
|
||||||
|
- duplicateId { line, id, firstLine } — Knoten-ID doppelt vergeben
|
||||||
|
(SPEC §1/D36); gemeldet an der späteren
|
||||||
|
Zeile, die erste wird genannt
|
||||||
- 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
|
||||||
@@ -34,6 +37,8 @@ export function formatWarning(w, t){
|
|||||||
return t('unknownStatusWarn', {line: w.line, code: esc(w.code)});
|
return t('unknownStatusWarn', {line: w.line, code: esc(w.code)});
|
||||||
case 'xorConflict':
|
case 'xorConflict':
|
||||||
return t('xorConflictWarn', {line: w.line, label: esc(w.label)});
|
return t('xorConflictWarn', {line: w.line, label: esc(w.label)});
|
||||||
|
case 'duplicateId':
|
||||||
|
return t('duplicateIdWarn', {line: w.line, id: esc(w.id), firstLine: w.firstLine});
|
||||||
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':
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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()});
|
||||||
|
|
||||||
|
/* Knoten-IDs `#name` (SPEC §1, D36): Adresse für Abhängigkeiten und
|
||||||
|
Beschreibungsblöcke; eindeutig im ganzen Dokument. */
|
||||||
|
describe('Parser — `#name` als Knoten-ID', () => {
|
||||||
|
it('extrahiert die ID und nimmt sie aus dem Label', () => {
|
||||||
|
const [wurzel] = roots(`[ ] Auth-Modul #auth (M)\n - [ ] Login`);
|
||||||
|
expect([wurzel.label, wurzel.id, wurzel.size]).toEqual(['Auth-Modul', 'auth', 'M']);
|
||||||
|
expect(wurzel.children[0].id).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('erkennt IDs nur alleinstehend angesetzt — „C#" bleibt Label', () => {
|
||||||
|
const [wurzel] = roots(`[ ] C# Kurs`);
|
||||||
|
expect([wurzel.label, wurzel.id]).toEqual(['C# Kurs', null]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('frisst das reservierte `:#a,#b` (Abhängigkeiten, §11) nicht', () => {
|
||||||
|
const [wurzel] = roots(`[ ] Deploy :#auth,#api`);
|
||||||
|
expect([wurzel.label, wurzel.id]).toEqual(['Deploy :#auth,#api', null]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nimmt nur das ERSTE Token — weitere `#` bleiben im Label (Tickets)', () => {
|
||||||
|
const [wurzel] = roots(`[ ] Auth #auth siehe #123`);
|
||||||
|
expect([wurzel.label, wurzel.id]).toEqual(['Auth siehe #123', 'auth']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt `#` in URLs unberührt (URL wird vorher extrahiert)', () => {
|
||||||
|
const [wurzel] = roots(`[ ] Doku https://example.org/seite#abschnitt`);
|
||||||
|
expect([wurzel.id, wurzel.url]).toEqual([null, 'https://example.org/seite#abschnitt']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('erlaubt numerische und Unicode-IDs', () => {
|
||||||
|
const nodes = roots(`[ ] Ticket #123\n[ ] Umlaut #größe-1`);
|
||||||
|
expect(nodes.map(n => n.id)).toEqual(['123', 'größe-1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignoriert eine Zeile, die nur aus einer ID besteht — die ID bleibt frei', () => {
|
||||||
|
const {roots: r, warnings} = parse(`- #auth\n- [ ] Echt #auth`);
|
||||||
|
expect(r.map(n => [n.label, n.id])).toEqual([['Echt', 'auth']]);
|
||||||
|
expect(warnings).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Doppelte IDs — Warnung an der späteren Zeile', () => {
|
||||||
|
it('meldet die spätere Zeile und nennt die erste', () => {
|
||||||
|
const {warnings} = parse(`[ ] A #auth\n[ ] B\n[ ] C #auth`);
|
||||||
|
expect(warnings).toEqual([{type: 'duplicateId', line: 3, id: 'auth', firstLine: 1}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt die spätere ID trotzdem am Knoten stehen (fehlertolerant)', () => {
|
||||||
|
const nodes = roots(`[ ] A #x\n[ ] B #x`);
|
||||||
|
expect(nodes.map(n => n.id)).toEqual(['x', 'x']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('meldet jede weitere Doppelvergabe einzeln', () => {
|
||||||
|
const {warnings} = parse(`[ ] A #x\n[ ] B #x\n[ ] C #x`);
|
||||||
|
expect(warnings.map(w => w.line)).toEqual([2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Darstellung — Tooltip und aria, kein Badge', () => {
|
||||||
|
it('zeigt die ID im Tooltip und im aria-label', () => {
|
||||||
|
const {html} = render(`[ ] Auth #auth`);
|
||||||
|
expect(html).toContain('title="#auth · st_geplant · jumpHint"');
|
||||||
|
expect(html).toContain('aria-label="Auth, a11yStatus, a11yId"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ändert Knoten ohne ID nicht', () => {
|
||||||
|
const {html} = render(`[ ] Ohne`);
|
||||||
|
expect(html).not.toContain('a11yId');
|
||||||
|
expect(html).toContain('title="st_geplant · jumpHint"');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user