diff --git a/docs/TASKS.md b/docs/TASKS.md
index 402fb39..d58e269 100644
--- a/docs/TASKS.md
+++ b/docs/TASKS.md
@@ -24,7 +24,11 @@ Abhaken beim Erledigen; neue Aufgaben unten anfügen.
`tests/render.test.js` (6 Tests, Snapshots). Anm.: der Modus
(horizontal/vertikal/kompakt) ist reine CSS-Container-Klasse und ändert
den Renderer-String nicht — ein Snapshot deckt alle drei Modi ab.
-- [ ] Warnungs-Modell vereinheitlichen (Zeilennummern, Typen).
+- [x] Warnungs-Modell vereinheitlichen (Zeilennummern, Typen).
+ → strukturierte Objekte `{type, line, ...}` (Renderer emittiert
+ `mixedGate`); `src/warnings.js` `formatWarning(w, t)` macht daraus den
+ lokalisierten, HTML-escapten Text an einer Stelle. Vorbereitet für
+ Phase 2 (`unknownStatus`). `tests/warnings.test.js`.
## Phase 2 — Qualität
- [ ] Barrierefreiheit: Fokusreihenfolge, aria-Labels für Status/Größe/Tags.
diff --git a/frontend/src/app.js b/frontend/src/app.js
index 472ea2f..44c8f67 100644
--- a/frontend/src/app.js
+++ b/frontend/src/app.js
@@ -2,6 +2,7 @@ import './style.css';
import { parse } from './parser.js';
import { computeCheapSet } from './model.js';
import { esc, renderTreeHtml } from './render.js';
+import { formatWarning } from './warnings.js';
const INITIAL = `%% Project structure – Sprint 14
[~] Website relaunch (XL) https://wiki.example.com/relaunch
@@ -57,7 +58,7 @@ function render(){
});
out.innerHTML = html;
- warnBox.innerHTML = warnings.map(w => `
⚠ ${w}
`).join('');
+ warnBox.innerHTML = warnings.map(w => `⚠ ${formatWarning(w, t)}
`).join('');
drawCheapPath();
}
diff --git a/frontend/src/render.js b/frontend/src/render.js
index 746c64e..b4f357f 100644
--- a/frontend/src/render.js
+++ b/frontend/src/render.js
@@ -47,7 +47,8 @@ function renderChildren(node, warnings, opts){
if(!kids.length) return '';
const types = new Set(kids.map(k => k.type));
if(types.size > 1){
- warnings.push(opts.t('mixedWarn', {line: kids[0].line, label: esc(node.label)}));
+ /* strukturierte Warnung (Typ + Zeile); Formatierung in warnings.js */
+ warnings.push({type: 'mixedGate', line: kids[0].line, label: node.label});
}
const gate = gateOf(kids);
const items = kids.map(k => {
@@ -62,7 +63,8 @@ function renderChildren(node, warnings, opts){
}
/* Baut den inneren HTML-String für #out aus (bereits gefilterten) Wurzeln und
- sammelt Warnungen (gemischte Gates). Leere Wurzelliste ⇒ leerer String. */
+ sammelt strukturierte Warnungen ({type, line, ...}, siehe warnings.js).
+ Leere Wurzelliste ⇒ leerer String. */
export function renderTreeHtml(roots, opts){
const warnings = [];
const html = roots.map(root => {
diff --git a/frontend/src/warnings.js b/frontend/src/warnings.js
new file mode 100644
index 0000000..2e60430
--- /dev/null
+++ b/frontend/src/warnings.js
@@ -0,0 +1,26 @@
+/* Vereinheitlichtes Warnungs-Modell.
+ Producer (Renderer, künftig auch Parser) liefern strukturierte Objekte
+ { type, line, ...data }
+ statt fertig formatierter Strings — so bleiben Typ und Zeilennummer
+ maschinenlesbar (sortierbar, filterbar, testbar). Die i18n-/HTML-Aufbereitung
+ passiert an genau einer Stelle: formatWarning(). Vgl. SPEC §3 (gemischte
+ Gates) und TASKS Phase 2 (unbekannte Statuszeichen).
+
+ Bekannte Typen:
+ - mixedGate { line, label } — Geschwister mit gemischtem Gate (SPEC §3)
+ - unknownStatus { line, code } — unbekanntes Statuszeichen (Phase 2) */
+
+import { esc } from './render.js';
+
+/* Strukturierte Warnung -> lokalisierter Anzeigetext (HTML-escaped Daten).
+ `t` ist die i18n-Funktion (key, vars) -> String. */
+export function formatWarning(w, t){
+ switch(w.type){
+ case 'mixedGate':
+ return t('mixedWarn', {line: w.line, label: esc(w.label)});
+ case 'unknownStatus':
+ return t('unknownStatusWarn', {line: w.line, code: esc(w.code)});
+ default:
+ return `${esc(String(w.type))} (${w.line ?? '?'})`;
+ }
+}
diff --git a/frontend/tests/render.test.js b/frontend/tests/render.test.js
index b6b064f..dcf67dc 100644
--- a/frontend/tests/render.test.js
+++ b/frontend/tests/render.test.js
@@ -79,6 +79,20 @@ describe('renderTreeHtml — „Untergliederung fehlt" (Geister-Knoten, SPEC §5
});
});
+describe('renderTreeHtml — strukturierte Warnungen', () => {
+ it('gemischte Gates ergeben eine {type:mixedGate, line, label}-Warnung', () => {
+ let {roots} = parse('- Eltern\n - und-Kind\n | oder-Kind');
+ const {warnings} = renderTreeHtml(roots, {t, showDiscarded: false, cheapPath: false, cheapSet: new Set()});
+ expect(warnings).toEqual([{type: 'mixedGate', line: 2, label: 'Eltern'}]);
+ });
+
+ it('einheitliche Gates ergeben keine Warnung', () => {
+ let {roots} = parse('- Eltern\n - a\n - b');
+ const {warnings} = renderTreeHtml(roots, {t, showDiscarded: false, cheapPath: false, cheapSet: new Set()});
+ expect(warnings).toEqual([]);
+ });
+});
+
describe('renderTreeHtml — Moduswechsel ist CSS, nicht Renderer', () => {
it('erzeugt nie eine Modus-Klasse (vertical/kompakt) — die setzt app.js am Container', () => {
const {html} = renderExample();
diff --git a/frontend/tests/warnings.test.js b/frontend/tests/warnings.test.js
new file mode 100644
index 0000000..456c51b
--- /dev/null
+++ b/frontend/tests/warnings.test.js
@@ -0,0 +1,23 @@
+import { describe, it, expect } from 'vitest';
+import { formatWarning } from '../src/warnings.js';
+
+/* Stub bildet key + interpolierte Variablen sichtbar ab, damit wir prüfen
+ können, welche Werte (inkl. Escaping) formatWarning durchreicht. */
+const t = (key, vars) => `${key}|${JSON.stringify(vars)}`;
+
+describe('formatWarning — vereinheitlichtes Warnungs-Modell', () => {
+ it('mixedGate reicht Zeile durch und HTML-escaped das Label', () => {
+ const out = formatWarning({type: 'mixedGate', line: 7, label: 'A & '}, t);
+ expect(out).toBe('mixedWarn|{"line":7,"label":"A & <B>"}');
+ });
+
+ it('unknownStatus reicht Zeile durch und escaped den Code', () => {
+ const out = formatWarning({type: 'unknownStatus', line: 3, code: '<'}, t);
+ expect(out).toBe('unknownStatusWarn|{"line":3,"code":"<"}');
+ });
+
+ it('unbekannter Typ fällt auf eine generische, escapte Meldung zurück', () => {
+ expect(formatWarning({type: 'was?', line: 9}, t)).toBe('was? (9)');
+ expect(formatWarning({type: 'x'}, t)).toBe('x (?)');
+ });
+});