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
+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