feat(notation): Schlagworte &tag gebaut — taiga.<slug> benennt das Projekt je Teilbaum, vererbt (SPEC §1, D91)
SPEC zuerst: §1 Schritt 5b samt Block und Referenz-Regex, §11 auf 'umgesetzt' umgestellt; llms.md nachgezogen. Parser extrahiert alleinstehende &-Token als node.marks (Zitier-Konvention gilt), taigaSlugs() in model.js vererbt das taiga.*-Schlagwort wie die @-Tags beim Engpass. Sichtbar im Tooltip und aria-label (a11yMarks, 9 Sprachen). Plan-Labels mit &-Erwähnungen eingeklammert; 12 neue Tests, Gegenprobe: ohne Alleinstehend-Anker fallen genau die zwei Zitier-Tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c712e81970
commit
5a18505571
@@ -19,6 +19,7 @@ reverse.
|
|||||||
|
|
||||||
## 2026-08-27
|
## 2026-08-27
|
||||||
|
|
||||||
|
- Free `&tag` keywords are part of the notation now: free-standing only, quoted mentions like `(&taiga.slug)` stay labels — and `&taiga.slug` names the Taiga project of a subtree, inherited downwards
|
||||||
- Plans spanning several Taiga projects: a `&taiga.slug` tag names the project per subtree, inherited downwards — the first consumer of the reserved free tags, recorded as planned
|
- Plans spanning several Taiga projects: a `&taiga.slug` tag names the project per subtree, inherited downwards — the first consumer of the reserved free tags, recorded as planned
|
||||||
- Creating Taiga tickets from nodes recorded as planned: a story per node from the node window, sub-packages as tasks picked in a dialog, through a backend proxy — the ref lands in the line as `#US-123`/`#T-1234`, beside the node id
|
- Creating Taiga tickets from nodes recorded as planned: a story per node from the node window, sub-packages as tasks picked in a dialog, through a backend proxy — the ref lands in the line as `#US-123`/`#T-1234`, beside the node id
|
||||||
- The unassigned pill in the people bar shows each language's short form now — German `N.N.`, English `TBD` — with the long wording in the tooltip
|
- The unassigned pill in the people bar shows each language's short form now — German `N.N.`, English `TBD` — with the long wording in the tooltip
|
||||||
|
|||||||
+40
-20
@@ -7,7 +7,7 @@ Syntaxänderungen werden zuerst hier dokumentiert, dann implementiert.
|
|||||||
## 1. Zeilenformat
|
## 1. Zeilenformat
|
||||||
|
|
||||||
```
|
```
|
||||||
[Einrückung][Zeichen] [Statusbox] [Faltmarke] Label (Größe) URL @tag … !!! %% Kommentar
|
[Einrückung][Zeichen] [Statusbox] [Faltmarke] Label (Größe) URL @tag &tag … !!! %% Kommentar
|
||||||
```
|
```
|
||||||
|
|
||||||
Alle Bestandteile außer dem Label sind optional. Die Extraktion erfolgt in
|
Alle Bestandteile außer dem Label sind optional. Die Extraktion erfolgt in
|
||||||
@@ -24,6 +24,7 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
|||||||
Groß-/Kleinschreibung egal (siehe unten). Frühere Vorkommen bleiben im
|
Groß-/Kleinschreibung egal (siehe unten). Frühere Vorkommen bleiben im
|
||||||
Label stehen.
|
Label stehen.
|
||||||
5. Tags: alle `@name`-Vorkommen.
|
5. Tags: alle `@name`-Vorkommen.
|
||||||
|
5b. Schlagworte: alle alleinstehend angesetzten `&name`-Token (siehe unten).
|
||||||
6. Knoten-ID: das **erste** alleinstehend angesetzte `#name`-Token (siehe unten).
|
6. Knoten-ID: das **erste** alleinstehend angesetzte `#name`-Token (siehe unten).
|
||||||
7. Abhängigkeiten: alle alleinstehend angesetzten `:#a,#b`-Token (siehe unten).
|
7. Abhängigkeiten: alle alleinstehend angesetzten `:#a,#b`-Token (siehe unten).
|
||||||
8. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten).
|
8. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten).
|
||||||
@@ -99,6 +100,30 @@ Referenz-Regex (Schritt 1b, geprüft auf der kommentarfreien Zeile):
|
|||||||
(Bis D68 galt das **erste** Token; die Umkehrung trifft nur Zeilen mit
|
(Bis D68 galt das **erste** Token; die Umkehrung trifft nur Zeilen mit
|
||||||
mehreren Kandidaten.)
|
mehreren Kandidaten.)
|
||||||
|
|
||||||
|
**Schlagworte `&tag`** — freie Schlagworte quer zur Hierarchie (die ID benennt
|
||||||
|
genau einen Knoten, ein Schlagwort eine **Menge**); das erste ausgewertete ist
|
||||||
|
das **Taiga-Projekt-Schlagwort** (§11, D91-Nachtrag 3):
|
||||||
|
|
||||||
|
- Zeichenmenge wie bei `@name` (§7): Unicode-Buchstaben, Ziffern, `.`, `_`,
|
||||||
|
`-`. Mehrere pro Zeile möglich, Position im Text egal.
|
||||||
|
- Erkannt nur **alleinstehend angesetzt** (`(^|\s)&…`): „R&D“ und
|
||||||
|
„Drag & Drop“ bleiben damit gewöhnliche Labels, und die Zitier-Konvention
|
||||||
|
gilt auch hier — `(&taiga.slug)` bleibt Label.
|
||||||
|
- Schlagworte gehören **nicht** zum Label. Sichtbar sind sie im
|
||||||
|
Knoten-Tooltip und im `aria-label`; eine eigene Diagramm-Darstellung haben
|
||||||
|
sie nicht.
|
||||||
|
- Semantik trägt allein das Präfix **`taiga.`**: `&taiga.<projekt-slug>`
|
||||||
|
benennt das Taiga-Projekt, zu dem der Teilbaum gehört, und **vererbt sich**
|
||||||
|
auf die Nachkommen wie die `@`-Tags beim Zuständigen-Engpass (§7/§9) — der
|
||||||
|
nächste Vorfahr mit einem `taiga.*`-Schlagwort gewinnt, ein eigenes am
|
||||||
|
Knoten übersteuert; trägt eine Zeile mehrere, gilt das **erste**. Konsument
|
||||||
|
ist die Ticket-Anlage (§11): Der Projekt-Dialog wird damit vorbelegt, und
|
||||||
|
die erste Anlage in einem Teilbaum ohne Zuordnung schreibt das Schlagwort
|
||||||
|
zurück.
|
||||||
|
- Alle übrigen Schlagworte sind **frei** und haben (noch) keinen Konsumenten.
|
||||||
|
Sie sagen nichts über Fortschritt (§4) oder Notwendigkeit (§3) und ändern
|
||||||
|
weder Kosten noch Warnungen.
|
||||||
|
|
||||||
**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).
|
||||||
|
|
||||||
@@ -223,6 +248,12 @@ bleibt stehen):
|
|||||||
(^|\s)\((XXL|XS|XL|S|M|L)\)
|
(^|\s)\((XXL|XS|XL|S|M|L)\)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Für die Schlagworte (Schritt 5b, alle Treffer):
|
||||||
|
|
||||||
|
```
|
||||||
|
(^|\s)&([\p{L}\p{N}._-]+)
|
||||||
|
```
|
||||||
|
|
||||||
Für die Knoten-ID (Schritt 6, nur der erste Treffer; die letzte Gruppe ist der
|
Für die Knoten-ID (Schritt 6, nur der erste Treffer; die letzte Gruppe ist der
|
||||||
optionale Trenn-Doppelpunkt, der mit entfällt):
|
optionale Trenn-Doppelpunkt, der mit entfällt):
|
||||||
|
|
||||||
@@ -1318,26 +1349,15 @@ einem Tag aufgelöst. Siehe D91-Nachträge 2 und 3.
|
|||||||
Freie Schlagworte liegen **nicht** mehr auf `#` — siehe `&tag` unten; damit
|
Freie Schlagworte liegen **nicht** mehr auf `#` — siehe `&tag` unten; damit
|
||||||
ist die frühere Dreifach-Rolle von `#` aufgelöst (D34).
|
ist die frühere Dreifach-Rolle von `#` aufgelöst (D34).
|
||||||
|
|
||||||
### Schlagworte (`&tag`) — reserviert, bewusst ungebaut
|
### Schlagworte (`&tag`)
|
||||||
|
|
||||||
- `&tag` — freies Schlagwort: benennt eine **Menge** von Knoten quer zur
|
Die **Schreibweise ist umgesetzt** — Definition jetzt in §1 (Zeichenmenge wie
|
||||||
Hierarchie (die ID benennt genau einen). Mehrere pro Zeile, Position egal,
|
`@name`, Alleinstehend-Regel, Zitier-Konvention, `taiga.*`-Vererbung); gebaut
|
||||||
gleiche Zeichenmenge wie `@name` (§7: Unicode-Buchstaben, Ziffern, `.`,
|
zusammen mit der Ticket-Anlage, wie D91-Nachtrag 3 es festlegte. Der erste
|
||||||
`_`, `-`).
|
Konsument ist das **Taiga-Projekt-Schlagwort** `&taiga.<projekt-slug>` (§1).
|
||||||
- Erkannt nur **alleinstehend** angesetzt (`(^|\s)&\w`, wie `!!!` in §1) —
|
Alle **übrigen** Schlagworte bleiben frei und ohne Konsumenten — künftige
|
||||||
„R&D“ und „Drag & Drop“ bleiben damit gewöhnliche Labels.
|
Auswerter (Filter-/Hervorheben-Linse, Label-Sync) sind weiterhin offen und
|
||||||
- **Niedrig priorisiert:** Ohne ein Feature, das Schlagworte auswertet
|
werden hier festgelegt, bevor sie gebaut werden.
|
||||||
(Filter-/Hervorheben-Linse, Taiga-Label-Sync), sind sie nur Kommentare mit
|
|
||||||
Extra-Syntax — gebaut werden sie erst **zusammen mit** dem ersten solchen
|
|
||||||
Konsumenten. Bis dahin gilt allein: `&` nicht anderweitig vergeben.
|
|
||||||
- **Der erste Konsument ist festgelegt** (D91-Nachtrag 3): das
|
|
||||||
**Taiga-Projekt-Schlagwort**. `&taiga.<projekt-slug>` benennt das
|
|
||||||
Taiga-Projekt, zu dem ein Teilbaum gehört, und **vererbt sich** auf die
|
|
||||||
Nachkommen wie die `@`-Tags beim Zuständigen-Engpass (§7/§9) — der
|
|
||||||
nächste Vorfahr mit einem `taiga.*`-Tag gewinnt, ein eigener Tag am
|
|
||||||
Knoten übersteuert. Alle übrigen Schlagworte bleiben frei; die Syntax
|
|
||||||
wird zusammen mit der Ticket-Anlage gebaut, und die Zitier-Konvention
|
|
||||||
gilt auch hier (`(&taiga.slug)` bleibt Label).
|
|
||||||
|
|
||||||
### Nutzen je Knoten (`(M/9)`) — festgelegt, noch nicht gebaut
|
### Nutzen je Knoten (`(M/9)`) — festgelegt, noch nicht gebaut
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
- [^] #not.gates: And/or decomposition (S)
|
- [^] #not.gates: And/or decomposition (S)
|
||||||
- [^] #not.opt: Optional nodes — neither required nor an alternative (S)
|
- [^] #not.opt: Optional nodes — neither required nor an alternative (S)
|
||||||
- [^] #not.id: Node IDs, (#auth:) before the title (S) %% often the ticket number, see SPEC §11
|
- [^] #not.id: Node IDs, (#auth:) before the title (S) %% often the ticket number, see SPEC §11
|
||||||
+ [ ] #not.tag: Free tags, &tag (M) %% first consumer found: the Taiga project tag, D91
|
+ [~] #not.tag: Free tags, (&tag) (M) %% first consumer found: the Taiga project tag, D91
|
||||||
- [?] #not.tag.lens: A lens: highlight every node tagged &x (S)
|
- [?] #not.tag.lens: A lens: highlight every node tagged (&x) (S)
|
||||||
- [ ] #not.tag.project: (&taiga.slug) assigns the Taiga project, inherited (S)
|
- [~] #not.tag.project: (&taiga.slug) assigns the Taiga project, inherited (S)
|
||||||
- [^] #not.dep: Dependencies across the tree, (:#auth,#api) (M)
|
- [^] #not.dep: Dependencies across the tree, (:#auth,#api) (M)
|
||||||
- [^] #not.dep.status: They constrain the status, not when work may start (S)
|
- [^] #not.dep.status: They constrain the status, not when work may start (S)
|
||||||
- [^] #not.dep.eff: Effective status: intrinsic, plus what it depends on (S)
|
- [^] #not.dep.eff: Effective status: intrinsic, plus what it depends on (S)
|
||||||
|
|||||||
+18
-7
@@ -15,7 +15,7 @@ people edit at once.
|
|||||||
|
|
||||||
## Line format
|
## Line format
|
||||||
|
|
||||||
[indent][gate] [status] [fold] Label (SIZE) URL @person #id :#id,#id !!! %% comment
|
[indent][gate] [status] [fold] Label (SIZE) URL @person &tag #id :#id,#id !!! %% comment
|
||||||
|
|
||||||
One node per line. Everything except the label is optional.
|
One node per line. Everything except the label is optional.
|
||||||
|
|
||||||
@@ -150,7 +150,15 @@ One node per line. Everything except the label is optional.
|
|||||||
size-like tokens stay in the label.
|
size-like tokens stay in the label.
|
||||||
4. `@name` — people tags; several per line, any position. Characters:
|
4. `@name` — people tags; several per line, any position. Characters:
|
||||||
Unicode letters, digits, `.`, `_`, `-`.
|
Unicode letters, digits, `.`, `_`, `-`.
|
||||||
5. `#id` — **node ID**: the *first* free-standing `#token` of the line
|
5. `&tag` — **free keywords**, free-standing only (so `R&D` and
|
||||||
|
`Drag & Drop` stay labels; `(&taiga.slug)` in parentheses mentions one
|
||||||
|
literally). Same character set as `@name`; several per line, any
|
||||||
|
position. Only the `taiga.` prefix carries meaning:
|
||||||
|
`&taiga.<project-slug>` names the Taiga project a subtree belongs to and
|
||||||
|
is **inherited downwards** — the nearest ancestor with a `taiga.*`
|
||||||
|
keyword wins, a node's own keyword overrides, the first of several on
|
||||||
|
one line counts. All other keywords are free and (so far) unconsumed.
|
||||||
|
6. `#id` — **node ID**: the *first* free-standing `#token` of the line
|
||||||
(preceded by start-of-line or whitespace). Uniquely names the node in the
|
(preceded by start-of-line or whitespace). Uniquely names the node in the
|
||||||
whole document; a duplicate ID warns, and references resolve to the first
|
whole document; a duplicate ID warns, and references resolve to the first
|
||||||
occurrence. Later `#tokens` stay label text; `C#` stays a label. Same
|
occurrence. Later `#tokens` stay label text; `C#` stays a label. Same
|
||||||
@@ -162,15 +170,15 @@ One node per line. Everything except the label is optional.
|
|||||||
whitespace or end of line — so a colon inside the label survives
|
whitespace or end of line — so a colon inside the label survives
|
||||||
(`#auth: Rule: token required`), and `#auth:#db` still reads as ID plus
|
(`#auth: Rule: token required`), and `#auth:#db` still reads as ID plus
|
||||||
dependency. Placement stays free: `Backend #auth` means the same thing.
|
dependency. Placement stays free: `Backend #auth` means the same thing.
|
||||||
6. `:#a,#b` — **dependencies**: one contiguous free-standing token — colon,
|
7. `:#a,#b` — **dependencies**: one contiguous free-standing token — colon,
|
||||||
then comma-separated IDs, each with `#`, **no spaces** (`:#a, #b` reads
|
then comma-separated IDs, each with `#`, **no spaces** (`:#a, #b` reads
|
||||||
only `#a`, and the free-standing ` #b` would become the node ID!).
|
only `#a`, and the free-standing ` #b` would become the node ID!).
|
||||||
Several tokens per line merge. `(:#a,#b)` in parentheses stays label text
|
Several tokens per line merge. `(:#a,#b)` in parentheses stays label text
|
||||||
(quoting convention, same as `(#id)`).
|
(quoting convention, same as `(#id)`).
|
||||||
7. `!!!` — **focus mark**, free-standing only: "everybody look here" — a
|
8. `!!!` — **focus mark**, free-standing only: "everybody look here" — a
|
||||||
shared pointer for collaborative editing. Independent of status and
|
shared pointer for collaborative editing. Independent of status and
|
||||||
necessity; `Wow!!!` stays a label.
|
necessity; `Wow!!!` stays a label.
|
||||||
8. Whatever remains, whitespace-normalized, is the **label**. An empty label
|
9. Whatever remains, whitespace-normalized, is the **label**. An empty label
|
||||||
means the line is ignored — **unless the line carries a node ID**: then
|
means the line is ignored — **unless the line carries a node ID**: then
|
||||||
`#id` becomes the label, with or without the trailing colon. `- #US-123`
|
`#id` becomes the label, with or without the trailing colon. `- #US-123`
|
||||||
and `- #US-123:` both render a node titled `#US-123`. Use this when the
|
and `- #US-123:` both render a node titled `#US-123`. Use this when the
|
||||||
@@ -264,5 +272,8 @@ One node per line. Everything except the label is optional.
|
|||||||
|
|
||||||
- Ticket references like `#123` or `#US-123` (Taiga user story, Jira
|
- Ticket references like `#123` or `#US-123` (Taiga user story, Jira
|
||||||
`#ABC-123`): planned tracker integration will resolve node IDs that match
|
`#ABC-123`): planned tracker integration will resolve node IDs that match
|
||||||
the connected tracker's reference pattern.
|
the connected tracker's reference pattern. Werkbaum writes the `US-`/`T-`
|
||||||
- Free-standing `&tag`: keywords across the hierarchy (reserved, unbuilt).
|
prefixes itself when it creates tickets; the ref is added to the line
|
||||||
|
**beside** an existing node ID (later `#tokens` stay label text).
|
||||||
|
- Free-standing `&tag` keywords other than `taiga.*` are parsed but have no
|
||||||
|
consumer yet — do not assign them a meaning.
|
||||||
|
|||||||
+9
-9
@@ -2506,7 +2506,7 @@ const I18N = {
|
|||||||
snapLocalHead:"Lokale Sicherungen (dieses Fenster)",
|
snapLocalHead:"Lokale Sicherungen (dieses Fenster)",
|
||||||
kindLiveDead:"getrennt — verbindet neu …",
|
kindLiveDead:"getrennt — verbindet neu …",
|
||||||
kindLiveUnsent:"ungesendete Änderungen!",
|
kindLiveUnsent:"ungesendete Änderungen!",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: mindestens {size} (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yFolded:"eingeklappt, {n} verborgen", a11yEffective:"effektiv: {status}", heldTooltip:"effektiv {eff} — selbst schon {own}, wartet auf Abhängigkeiten", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
|
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: mindestens {size} (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yMarks:"Schlagworte: {names}", a11yFolded:"eingeklappt, {n} verborgen", a11yEffective:"effektiv: {status}", heldTooltip:"effektiv {eff} — selbst schon {own}, wartet auf Abhängigkeiten", 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",
|
||||||
@@ -2641,7 +2641,7 @@ const I18N = {
|
|||||||
snapLocalHead:"Local backups (this window)",
|
snapLocalHead:"Local backups (this window)",
|
||||||
kindLiveDead:"disconnected — reconnecting …",
|
kindLiveDead:"disconnected — reconnecting …",
|
||||||
kindLiveUnsent:"unsent changes!",
|
kindLiveUnsent:"unsent changes!",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: at least {size} (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yFolded:"collapsed, {n} hidden", a11yEffective:"effective: {status}", heldTooltip:"effectively {eff} — itself already {own}, waiting on dependencies", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
|
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: at least {size} (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yMarks:"Keywords: {names}", a11yFolded:"collapsed, {n} hidden", a11yEffective:"effective: {status}", heldTooltip:"effectively {eff} — itself already {own}, waiting on dependencies", 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",
|
||||||
@@ -2776,7 +2776,7 @@ const I18N = {
|
|||||||
snapLocalHead:"Copias locales (esta ventana)",
|
snapLocalHead:"Copias locales (esta ventana)",
|
||||||
kindLiveDead:"desconectado — reconectando …",
|
kindLiveDead:"desconectado — reconectando …",
|
||||||
kindLiveUnsent:"¡cambios sin enviar!",
|
kindLiveUnsent:"¡cambios sin enviar!",
|
||||||
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: al menos {size} (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yFolded:"plegado, {n} ocultos", a11yEffective:"efectivo: {status}", heldTooltip:"efectivamente {eff} — por sí mismo ya {own}, espera dependencias", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
|
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: al menos {size} (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yMarks:"Palabras clave: {names}", a11yFolded:"plegado, {n} ocultos", a11yEffective:"efectivo: {status}", heldTooltip:"efectivamente {eff} — por sí mismo ya {own}, espera dependencias", 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",
|
||||||
@@ -2911,7 +2911,7 @@ const I18N = {
|
|||||||
snapLocalHead:"Sauvegardes locales (cette fenêtre)",
|
snapLocalHead:"Sauvegardes locales (cette fenêtre)",
|
||||||
kindLiveDead:"déconnecté — reconnexion …",
|
kindLiveDead:"déconnecté — reconnexion …",
|
||||||
kindLiveUnsent:"modifications non envoyées !",
|
kindLiveUnsent:"modifications non envoyées !",
|
||||||
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : au moins {size} (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yFolded:"replié, {n} masqués", a11yEffective:"effectif : {status}", heldTooltip:"effectivement {eff} — lui-même déjà {own}, en attente de dépendances", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
|
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : au moins {size} (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yMarks:"Mots-clés : {names}", a11yFolded:"replié, {n} masqués", a11yEffective:"effectif : {status}", heldTooltip:"effectivement {eff} — lui-même déjà {own}, en attente de dépendances", 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",
|
||||||
@@ -3046,7 +3046,7 @@ const I18N = {
|
|||||||
snapLocalHead:"Lokalne kopie (to okno)",
|
snapLocalHead:"Lokalne kopie (to okno)",
|
||||||
kindLiveDead:"rozłączono — ponowne łączenie …",
|
kindLiveDead:"rozłączono — ponowne łączenie …",
|
||||||
kindLiveUnsent:"niewysłane zmiany!",
|
kindLiveUnsent:"niewysłane zmiany!",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: co najmniej {size} (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yFolded:"zwinięte, ukrytych: {n}", a11yEffective:"efektywnie: {status}", heldTooltip:"efektywnie {eff} — sam już {own}, czeka na zależności", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
|
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: co najmniej {size} (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yMarks:"Słowa kluczowe: {names}", a11yFolded:"zwinięte, ukrytych: {n}", a11yEffective:"efektywnie: {status}", heldTooltip:"efektywnie {eff} — sam już {own}, czeka na zależności", 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",
|
||||||
@@ -3181,7 +3181,7 @@ const I18N = {
|
|||||||
snapLocalHead:"Локальные копии (это окно)",
|
snapLocalHead:"Локальные копии (это окно)",
|
||||||
kindLiveDead:"нет связи — переподключение …",
|
kindLiveDead:"нет связи — переподключение …",
|
||||||
kindLiveUnsent:"неотправленные изменения!",
|
kindLiveUnsent:"неотправленные изменения!",
|
||||||
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: не меньше {size} (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yFolded:"свёрнуто, скрыто: {n}", a11yEffective:"фактически: {status}", heldTooltip:"фактически {eff} — сам уже {own}, ждёт зависимости", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: не меньше {size} (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yMarks:"Метки: {names}", a11yFolded:"свёрнуто, скрыто: {n}", a11yEffective:"фактически: {status}", heldTooltip:"фактически {eff} — сам уже {own}, ждёт зависимости", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
||||||
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
||||||
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
||||||
hint_xor:"альтернатива, ровно одна",
|
hint_xor:"альтернатива, ровно одна",
|
||||||
@@ -3316,7 +3316,7 @@ const I18N = {
|
|||||||
snapLocalHead:"स्थानीय प्रतियाँ (यह विंडो)",
|
snapLocalHead:"स्थानीय प्रतियाँ (यह विंडो)",
|
||||||
kindLiveDead:"संपर्क टूटा — फिर जोड़ रहा है …",
|
kindLiveDead:"संपर्क टूटा — फिर जोड़ रहा है …",
|
||||||
kindLiveUnsent:"बिना भेजे बदलाव!",
|
kindLiveUnsent:"बिना भेजे बदलाव!",
|
||||||
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: कम से कम {size} (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yEffective:"प्रभावी: {status}", heldTooltip:"प्रभावी रूप से {eff} — स्वयं {own} है, निर्भरताओं की प्रतीक्षा में", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: कम से कम {size} (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yMarks:"कीवर्ड: {names}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yEffective:"प्रभावी: {status}", heldTooltip:"प्रभावी रूप से {eff} — स्वयं {own} है, निर्भरताओं की प्रतीक्षा में", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
||||||
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
||||||
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
||||||
hint_xor:"विकल्प, ठीक एक",
|
hint_xor:"विकल्प, ठीक एक",
|
||||||
@@ -3451,7 +3451,7 @@ const I18N = {
|
|||||||
snapLocalHead:"本地备份(此窗口)",
|
snapLocalHead:"本地备份(此窗口)",
|
||||||
kindLiveDead:"已断开——正在重连 …",
|
kindLiveDead:"已断开——正在重连 …",
|
||||||
kindLiveUnsent:"有未发送的更改!",
|
kindLiveUnsent:"有未发送的更改!",
|
||||||
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:至少 {size}(假定)", a11yTags:"负责人:{names}", a11yId:"ID:#{id}", a11yDeps:"依赖:{ids}", a11yFolded:"已折叠,隐藏 {n} 项", a11yEffective:"实际:{status}", heldTooltip:"实际为 {eff}——自身已是 {own},等待依赖完成", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:至少 {size}(假定)", a11yTags:"负责人:{names}", a11yId:"ID:#{id}", a11yDeps:"依赖:{ids}", a11yMarks:"关键词:{names}", a11yFolded:"已折叠,隐藏 {n} 项", a11yEffective:"实际:{status}", heldTooltip:"实际为 {eff}——自身已是 {own},等待依赖完成", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
||||||
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
||||||
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
||||||
hint_xor:"备选项,恰好一个",
|
hint_xor:"备选项,恰好一个",
|
||||||
@@ -3586,7 +3586,7 @@ const I18N = {
|
|||||||
snapLocalHead:"ローカルの控え(このウィンドウ)",
|
snapLocalHead:"ローカルの控え(このウィンドウ)",
|
||||||
kindLiveDead:"切断 — 再接続中 …",
|
kindLiveDead:"切断 — 再接続中 …",
|
||||||
kindLiveUnsent:"未送信の変更あり!",
|
kindLiveUnsent:"未送信の変更あり!",
|
||||||
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: 少なくとも {size}(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yEffective:"実効: {status}", heldTooltip:"実効では {eff} — 自身は既に {own}、依存待ち", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
|
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: 少なくとも {size}(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yMarks:"キーワード: {names}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yEffective:"実効: {status}", heldTooltip:"実効では {eff} — 自身は既に {own}、依存待ち", 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つ",
|
||||||
|
|||||||
@@ -565,6 +565,31 @@ export function personFoldSet(roots, tag){
|
|||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Taiga-Projekt-Schlagwort (SPEC §1/§11, D91) ----------
|
||||||
|
`&taiga.<slug>` benennt das Taiga-Projekt eines Teilbaums und vererbt sich
|
||||||
|
auf die Nachkommen wie die `@`-Tags beim Zuständigen-Engpass (D71): der
|
||||||
|
nächste Vorfahr mit einem `taiga.*`-Schlagwort gewinnt, ein eigenes am
|
||||||
|
Knoten übersteuert; trägt eine Zeile mehrere, gilt das erste. Map
|
||||||
|
Knoten -> Slug, nur für Knoten, die (geerbt) eines haben — Konsument ist
|
||||||
|
die Vorbelegung des Projekt-Dialogs der Ticket-Anlage. */
|
||||||
|
export function taigaSlugs(roots){
|
||||||
|
const map = new Map();
|
||||||
|
const own = n => {
|
||||||
|
for(const m of n.marks || [])
|
||||||
|
if(m.startsWith('taiga.') && m.length > 'taiga.'.length) return m.slice('taiga.'.length);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const walk = (ns, inherited) => {
|
||||||
|
for(const n of ns){
|
||||||
|
const slug = own(n) || inherited;
|
||||||
|
if(slug) map.set(n, slug);
|
||||||
|
walk(n.children, slug);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(roots, null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
/* Alle vergebenen Personen-Tags in Dokumentreihenfolge — die Einträge der
|
/* Alle vergebenen Personen-Tags in Dokumentreihenfolge — die Einträge der
|
||||||
Personen-Leiste (D87). */
|
Personen-Leiste (D87). */
|
||||||
export function allTags(roots){
|
export function allTags(roots){
|
||||||
|
|||||||
+13
-3
@@ -163,7 +163,9 @@ const REALIZED = new Set(['arbeit', 'durchstich', 'fertig', 'prod']);
|
|||||||
|
|
||||||
/* Parst den Notationstext zu { roots, warnings }.
|
/* Parst den Notationstext zu { roots, warnings }.
|
||||||
Jeder Knoten: {label, type:'and'|'or'|'xor', optional, fold, status, url,
|
Jeder Knoten: {label, type:'and'|'or'|'xor', optional, fold, status, url,
|
||||||
size, tags, id, deps, desc, focus, children, line}.
|
size, tags, marks, id, deps, desc, focus, children, line}.
|
||||||
|
`marks` (SPEC §1, D91) sind die freien Schlagworte (`&tag`, ohne `&`);
|
||||||
|
ausgewertet wird bisher nur das Präfix `taiga.` (model.js, `taigaSlugs`).
|
||||||
`desc` (SPEC §11/D40) ist der Beschreibungstext: `"`-Zeilen unter dem
|
`desc` (SPEC §11/D40) ist der Beschreibungstext: `"`-Zeilen unter dem
|
||||||
Knoten (Kurzform) und ID-Blöcke aus dem `---`-Beschreibungsteil (Langform),
|
Knoten (Kurzform) und ID-Blöcke aus dem `---`-Beschreibungsteil (Langform),
|
||||||
in Dokumentreihenfolge mit Zeilenumbrüchen zusammengefügt; null ohne.
|
in Dokumentreihenfolge mit Zeilenumbrüchen zusammengefügt; null ohne.
|
||||||
@@ -181,7 +183,8 @@ const REALIZED = new Set(['arbeit', 'durchstich', 'fertig', 'prod']);
|
|||||||
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 -> Abhängigkeiten -> Fokusmarke -> Label. Hierarchie
|
-> Tags -> Schlagworte -> Knoten-ID -> Abhängigkeiten -> Fokusmarke -> Label.
|
||||||
|
Hierarchie
|
||||||
über Einrückungsbreite (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){
|
||||||
@@ -302,6 +305,13 @@ export function parse(text){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 ''; });
|
||||||
|
/* Schlagworte `&name` (SPEC §1 Schritt 5b, D91): nur ALLEINSTEHEND
|
||||||
|
ANGESETZT — „R&D" und „Drag & Drop" bleiben Labels, und die
|
||||||
|
Zitier-Konvention gilt auch hier (`(&taiga.slug)` bleibt Label).
|
||||||
|
Semantik trägt allein das Präfix `taiga.` (Projekt-Vererbung rechnet
|
||||||
|
`taigaSlugs()` in model.js); alle übrigen sind frei. */
|
||||||
|
const marks = [];
|
||||||
|
rest = rest.replace(/(^|\s)&([\p{L}\p{N}._-]+)/gu, (s, pre, g) => { marks.push(g); return pre; });
|
||||||
/* Knoten-ID `#name` (SPEC §1, D36): nur ALLEINSTEHEND ANGESETZT — „C#"
|
/* Knoten-ID `#name` (SPEC §1, D36): nur ALLEINSTEHEND ANGESETZT — „C#"
|
||||||
bleibt Label, und das reservierte `:#a,#b` (§11) wird nicht gefressen.
|
bleibt Label, und das reservierte `:#a,#b` (§11) wird nicht gefressen.
|
||||||
Nur der ERSTE Treffer (kein /g): weitere `#`-Token bleiben im Label
|
Nur der ERSTE Treffer (kein /g): weitere `#`-Token bleiben im Label
|
||||||
@@ -353,7 +363,7 @@ 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, labelFromId, type, optional, fold, status, url, size, tags, id, deps, desc:null, descLines:null, focus, children:[], line:i+1};
|
const node = {label, labelFromId, type, optional, fold, status, url, size, tags, marks, id, deps, desc:null, descLines:null, focus, children:[], line:i+1};
|
||||||
parent.children.push(node);
|
parent.children.push(node);
|
||||||
stack.push({node, width});
|
stack.push({node, width});
|
||||||
lastNode = node;
|
lastNode = node;
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ function nodeAria(n, opts, fold){
|
|||||||
if(n.id && !n.labelFromId) parts.push(t('a11yId', {id: n.id}));
|
if(n.id && !n.labelFromId) parts.push(t('a11yId', {id: n.id}));
|
||||||
if(n.deps && n.deps.length)
|
if(n.deps && n.deps.length)
|
||||||
parts.push(t('a11yDeps', {ids: n.deps.map(d => '#' + d).join(', ')}));
|
parts.push(t('a11yDeps', {ids: n.deps.map(d => '#' + d).join(', ')}));
|
||||||
|
/* Schlagworte (SPEC §1, D91): keine eigene Diagramm-Darstellung — sichtbar
|
||||||
|
nur im Tooltip und hier. */
|
||||||
|
if(n.marks && n.marks.length)
|
||||||
|
parts.push(t('a11yMarks', {names: n.marks.map(m => '&' + m).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
|
||||||
@@ -181,6 +185,9 @@ function nodeHtml(n, extra, opts, fold){
|
|||||||
ein Screenreader läse die Striche einzeln vor (nodeAria oben). */
|
ein Screenreader läse die Striche einzeln vor (nodeAria oben). */
|
||||||
const facts = [(n.id && !n.labelFromId) ? '#' + n.id : '',
|
const facts = [(n.id && !n.labelFromId) ? '#' + n.id : '',
|
||||||
n.deps && n.deps.length ? '→ ' + n.deps.map(d => '#' + d).join(', ') : '',
|
n.deps && n.deps.length ? '→ ' + n.deps.map(d => '#' + d).join(', ') : '',
|
||||||
|
/* Schlagworte (SPEC §1, D91) — wörtlich, wie sie im Text
|
||||||
|
stehen; sie spiegeln die Notation und brauchen kein i18n. */
|
||||||
|
n.marks && n.marks.length ? n.marks.map(m => '&' + m).join(' ') : '',
|
||||||
effKey
|
effKey
|
||||||
? t('heldTooltip', {eff: t('st_' + effKey), own: t('st_' + n.status.key)})
|
? t('heldTooltip', {eff: t('st_' + effKey), own: t('st_' + n.status.key)})
|
||||||
: (n.status ? t('st_' + n.status.key) : ''),
|
: (n.status ? t('st_' + n.status.key) : ''),
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parse } from '../src/parser.js';
|
||||||
|
import { taigaSlugs } from '../src/model.js';
|
||||||
|
|
||||||
|
/* Schlagworte `&tag` (SPEC §1, D91): Extraktion im Parser und die
|
||||||
|
`taiga.*`-Vererbung in model.js — der erste Konsument der reservierten
|
||||||
|
freien Schlagworte (Projekt-Vorbelegung der Ticket-Anlage). */
|
||||||
|
|
||||||
|
describe('parse — Schlagworte &tag (SPEC §1, D91)', () => {
|
||||||
|
it('extrahiert alleinstehende &-Token aus dem Label', () => {
|
||||||
|
const { roots, warnings } = parse('- Backend &taiga.intern &infra (M)');
|
||||||
|
expect(roots[0].label).toBe('Backend');
|
||||||
|
expect(roots[0].marks).toEqual(['taiga.intern', 'infra']);
|
||||||
|
expect(roots[0].size).toBe('M');
|
||||||
|
expect(warnings).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Zeichenmenge wie @name: Punkt, Unterstrich, Bindestrich, Unicode', () => {
|
||||||
|
const { roots } = parse('- X &über_a.b-c');
|
||||||
|
expect(roots[0].marks).toEqual(['über_a.b-c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nur alleinstehend angesetzt: R&D und Drag & Drop bleiben Labels', () => {
|
||||||
|
const { roots } = parse('- R&D für Drag & Drop');
|
||||||
|
expect(roots[0].marks).toEqual([]);
|
||||||
|
expect(roots[0].label).toBe('R&D für Drag & Drop');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Zitier-Konvention: (&taiga.slug) bleibt Label', () => {
|
||||||
|
const { roots } = parse('- (&taiga.slug) benennt das Projekt');
|
||||||
|
expect(roots[0].marks).toEqual([]);
|
||||||
|
expect(roots[0].label).toBe('(&taiga.slug) benennt das Projekt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein & in einer URL bleibt Teil der URL (URL wird zuerst extrahiert)', () => {
|
||||||
|
const { roots } = parse('- Suche https://example.org/?a=1&b=2');
|
||||||
|
expect(roots[0].url).toBe('https://example.org/?a=1&b=2');
|
||||||
|
expect(roots[0].marks).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein Schlagwort hinter %% wirkt nicht (Kommentar fällt zuerst weg)', () => {
|
||||||
|
const { roots } = parse('- Backend %% &taiga.intern');
|
||||||
|
expect(roots[0].marks).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ändert weder Status noch Notwendigkeit noch Kosten-Felder', () => {
|
||||||
|
const { roots } = parse('+ [x] Zugabe &taiga.intern (S)');
|
||||||
|
const n = roots[0];
|
||||||
|
expect(n.optional).toBe(true);
|
||||||
|
expect(n.status.key).toBe('fertig');
|
||||||
|
expect(n.size).toBe('S');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('taigaSlugs — Projekt-Vererbung (SPEC §1, D91-Nachtrag 3)', () => {
|
||||||
|
const tree = text => parse(text).roots;
|
||||||
|
|
||||||
|
it('eigenes taiga.*-Schlagwort setzt den Slug, Nachkommen erben ihn', () => {
|
||||||
|
const roots = tree('- Wurzel &taiga.intern\n - Kind\n - Enkel');
|
||||||
|
const map = taigaSlugs(roots);
|
||||||
|
expect(map.get(roots[0])).toBe('intern');
|
||||||
|
expect(map.get(roots[0].children[0])).toBe('intern');
|
||||||
|
expect(map.get(roots[0].children[0].children[0])).toBe('intern');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein eigenes Schlagwort am Knoten übersteuert das geerbte', () => {
|
||||||
|
const r = tree('- Wurzel &taiga.a\n - Kind &taiga.b\n - Enkel\n - Bruder');
|
||||||
|
const m = taigaSlugs(r);
|
||||||
|
expect(m.get(r[0])).toBe('a');
|
||||||
|
expect(m.get(r[0].children[0])).toBe('b');
|
||||||
|
expect(m.get(r[0].children[0].children[0])).toBe('b');
|
||||||
|
expect(m.get(r[0].children[1])).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ohne taiga.*-Vorfahren gibt es keinen Eintrag', () => {
|
||||||
|
const r = tree('- Wurzel &infra\n - Kind');
|
||||||
|
const m = taigaSlugs(r);
|
||||||
|
expect(m.has(r[0])).toBe(false);
|
||||||
|
expect(m.has(r[0].children[0])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mehrere taiga.*-Schlagworte auf einer Zeile: das erste gilt', () => {
|
||||||
|
const r = tree('- Wurzel &taiga.a &taiga.b');
|
||||||
|
expect(taigaSlugs(r).get(r[0])).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('das nackte &taiga. ohne Slug zählt nicht', () => {
|
||||||
|
const r = tree('- Wurzel &taiga.');
|
||||||
|
expect(taigaSlugs(r).has(r[0])).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user