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
|
||||
|
||||
- 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
|
||||
- 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
|
||||
|
||||
+40
-20
@@ -7,7 +7,7 @@ Syntaxänderungen werden zuerst hier dokumentiert, dann implementiert.
|
||||
## 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
|
||||
@@ -24,6 +24,7 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
||||
Groß-/Kleinschreibung egal (siehe unten). Frühere Vorkommen bleiben im
|
||||
Label stehen.
|
||||
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).
|
||||
7. Abhängigkeiten: alle alleinstehend angesetzten `:#a,#b`-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
|
||||
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;
|
||||
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)\)
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
Hierarchie (die ID benennt genau einen). Mehrere pro Zeile, Position egal,
|
||||
gleiche Zeichenmenge wie `@name` (§7: Unicode-Buchstaben, Ziffern, `.`,
|
||||
`_`, `-`).
|
||||
- Erkannt nur **alleinstehend** angesetzt (`(^|\s)&\w`, wie `!!!` in §1) —
|
||||
„R&D“ und „Drag & Drop“ bleiben damit gewöhnliche Labels.
|
||||
- **Niedrig priorisiert:** Ohne ein Feature, das Schlagworte auswertet
|
||||
(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).
|
||||
Die **Schreibweise ist umgesetzt** — Definition jetzt in §1 (Zeichenmenge wie
|
||||
`@name`, Alleinstehend-Regel, Zitier-Konvention, `taiga.*`-Vererbung); gebaut
|
||||
zusammen mit der Ticket-Anlage, wie D91-Nachtrag 3 es festlegte. Der erste
|
||||
Konsument ist das **Taiga-Projekt-Schlagwort** `&taiga.<projekt-slug>` (§1).
|
||||
Alle **übrigen** Schlagworte bleiben frei und ohne Konsumenten — künftige
|
||||
Auswerter (Filter-/Hervorheben-Linse, Label-Sync) sind weiterhin offen und
|
||||
werden hier festgelegt, bevor sie gebaut werden.
|
||||
|
||||
### Nutzen je Knoten (`(M/9)`) — festgelegt, noch nicht gebaut
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
- [^] #not.gates: And/or decomposition (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.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.project: (&taiga.slug) assigns the Taiga project, inherited (S)
|
||||
+ [~] #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.project: (&taiga.slug) assigns the Taiga project, inherited (S)
|
||||
- [^] #not.dep: Dependencies across the tree, (:#auth,#api) (M)
|
||||
- [^] #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)
|
||||
|
||||
+18
-7
@@ -15,7 +15,7 @@ people edit at once.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -150,7 +150,15 @@ One node per line. Everything except the label is optional.
|
||||
size-like tokens stay in the label.
|
||||
4. `@name` — people tags; several per line, any position. Characters:
|
||||
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
|
||||
whole document; a duplicate ID warns, and references resolve to the first
|
||||
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
|
||||
(`#auth: Rule: token required`), and `#auth:#db` still reads as ID plus
|
||||
dependency. Placement stays free: `Backend #auth` means the same thing.
|
||||
6. `:#a,#b` — **dependencies**: one contiguous free-standing token — colon,
|
||||
7. `:#a,#b` — **dependencies**: one contiguous free-standing token — colon,
|
||||
then comma-separated IDs, each with `#`, **no spaces** (`:#a, #b` reads
|
||||
only `#a`, and the free-standing ` #b` would become the node ID!).
|
||||
Several tokens per line merge. `(:#a,#b)` in parentheses stays label text
|
||||
(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
|
||||
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
|
||||
`#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
|
||||
@@ -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
|
||||
`#ABC-123`): planned tracker integration will resolve node IDs that match
|
||||
the connected tracker's reference pattern.
|
||||
- Free-standing `&tag`: keywords across the hierarchy (reserved, unbuilt).
|
||||
the connected tracker's reference pattern. Werkbaum writes the `US-`/`T-`
|
||||
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)",
|
||||
kindLiveDead:"getrennt — verbindet neu …",
|
||||
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_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
|
||||
hint_xor:"Alternative, genau eine",
|
||||
@@ -2641,7 +2641,7 @@ const I18N = {
|
||||
snapLocalHead:"Local backups (this window)",
|
||||
kindLiveDead:"disconnected — reconnecting …",
|
||||
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_all:"sub-task, all required", hint_any:"alternative, choose one",
|
||||
hint_xor:"alternative, exactly one",
|
||||
@@ -2776,7 +2776,7 @@ const I18N = {
|
||||
snapLocalHead:"Copias locales (esta ventana)",
|
||||
kindLiveDead:"desconectado — reconectando …",
|
||||
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_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
|
||||
hint_xor:"alternativa, exactamente una",
|
||||
@@ -2911,7 +2911,7 @@ const I18N = {
|
||||
snapLocalHead:"Sauvegardes locales (cette fenêtre)",
|
||||
kindLiveDead:"déconnecté — reconnexion …",
|
||||
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_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une",
|
||||
hint_xor:"alternative, exactement une",
|
||||
@@ -3046,7 +3046,7 @@ const I18N = {
|
||||
snapLocalHead:"Lokalne kopie (to okno)",
|
||||
kindLiveDead:"rozłączono — ponowne łączenie …",
|
||||
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_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
|
||||
hint_xor:"alternatywa, dokładnie jedna",
|
||||
@@ -3181,7 +3181,7 @@ const I18N = {
|
||||
snapLocalHead:"Локальные копии (это окно)",
|
||||
kindLiveDead:"нет связи — переподключение …",
|
||||
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_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
||||
hint_xor:"альтернатива, ровно одна",
|
||||
@@ -3316,7 +3316,7 @@ const I18N = {
|
||||
snapLocalHead:"स्थानीय प्रतियाँ (यह विंडो)",
|
||||
kindLiveDead:"संपर्क टूटा — फिर जोड़ रहा है …",
|
||||
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_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
||||
hint_xor:"विकल्प, ठीक एक",
|
||||
@@ -3451,7 +3451,7 @@ const I18N = {
|
||||
snapLocalHead:"本地备份(此窗口)",
|
||||
kindLiveDead:"已断开——正在重连 …",
|
||||
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_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
||||
hint_xor:"备选项,恰好一个",
|
||||
@@ -3586,7 +3586,7 @@ const I18N = {
|
||||
snapLocalHead:"ローカルの控え(このウィンドウ)",
|
||||
kindLiveDead:"切断 — 再接続中 …",
|
||||
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_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
|
||||
hint_xor:"選択肢、ちょうど1つ",
|
||||
|
||||
@@ -565,6 +565,31 @@ export function personFoldSet(roots, tag){
|
||||
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
|
||||
Personen-Leiste (D87). */
|
||||
export function allTags(roots){
|
||||
|
||||
+13
-3
@@ -163,7 +163,9 @@ const REALIZED = new Set(['arbeit', 'durchstich', 'fertig', 'prod']);
|
||||
|
||||
/* Parst den Notationstext zu { roots, warnings }.
|
||||
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
|
||||
Knoten (Kurzform) und ID-Blöcke aus dem `---`-Beschreibungsteil (Langform),
|
||||
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 `=`
|
||||
mit `-`/`+` (oder untereinander) gemischt wird.
|
||||
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);
|
||||
Elternknoten ist die nächste vorangehende Zeile mit kleinerer Breite. */
|
||||
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 ''; });
|
||||
/* 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#"
|
||||
bleibt Label, und das reservierte `:#a,#b` (§11) wird nicht gefressen.
|
||||
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();
|
||||
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);
|
||||
stack.push({node, width});
|
||||
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.deps && n.deps.length)
|
||||
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'));
|
||||
/* 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
|
||||
@@ -181,6 +185,9 @@ function nodeHtml(n, extra, opts, fold){
|
||||
ein Screenreader läse die Striche einzeln vor (nodeAria oben). */
|
||||
const facts = [(n.id && !n.labelFromId) ? '#' + n.id : '',
|
||||
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
|
||||
? t('heldTooltip', {eff: t('st_' + effKey), own: 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