feat(editor): Warn-Zeilennummer trägt ihre Meldung als Tooltip

Der Streifen sagte bisher nur, DASS eine Zeile eine Warnung hat; welche,
stand allein unter dem Diagramm — man sah die orange Zahl und suchte dann
in der Liste die passende Zeilennummer.

- warnings.js: `warningText()` neben `formatWarning()`, beide aus demselben
  `switch` (`build(w, t, esc)`) — der Warnungsbereich ist HTML, ein `title`
  ist Klartext. Ohne die Trennung stünde dort „Drag & Drop“.
- app.js: `lineNoWarn` ist jetzt Map Zeile -> Text; mehrere Warnungen einer
  Zeile stehen im Tooltip untereinander. Titel verschwinden mit der Warnung.
- style.css: Der Kasten der Warn-Zahl reicht über die ganze Streifenbreite
  (Ziffern bleiben punktgenau stehen, nachgemessen 21,14 px), `cursor:help`.

Nachgemessen (4 Warnungen in 3 Zeilen): Tooltips wortgleich mit dem
Warnungsbereich, `&`/`"` unverfälscht, keine Zahl ohne Warnung mit `title`;
nach dem Beheben verschwinden Klasse und Titel mit. Werkbaum-Plan: 0.

SPEC §9 (Zeilennummern) + D33-Nachtrag; Plan: #ed.lineno.tip [x].
243 Tests grün (3 neue).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 10:41:05 +02:00
co-authored by Claude Opus 5
parent 0bead77e7f
commit cb966e4e20
8 changed files with 133 additions and 7 deletions
+4
View File
@@ -55,6 +55,10 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
neutral. `render()` in app.js führt Parser- und Renderer-Warnungen zusammen
(nach Zeile sortiert) und zeigt sie via `formatWarning` (warnings.js). Neue
Warnungstypen dort + i18n-Key in allen 9 Sprachen ergänzen.
- **Zwei Senken, ein `switch`:** `formatWarning()` liefert HTML (Warnungsbereich),
`warningText()` Klartext (der `title` der Zeilennummer, D33-Nachtrag); beide
bauen auf `build(w, t, esc)` auf. Einen neuen Typ **nur dort** eintragen —
einen zweiten Formatierer danebenzustellen heißt, dass einer veraltet.
- Modulteilung (D19): `parser.js` (Text→Baum, headless), `model.js` (Baum-/
Kostenlogik: `gateOf`, `needsBreakdown`, `visibleChildren(n, showDiscarded)`,
`computeCheapSet`, `cheapCls`), `render.js` (HTML-String via
+20 -5
View File
@@ -2,7 +2,7 @@ import './style.css';
import { parse, setFoldMark } from './parser.js';
import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM } from './model.js';
import { esc, renderTreeHtml, TIP_RULE } from './render.js';
import { formatWarning } from './warnings.js';
import { formatWarning, warningText } from './warnings.js';
import { padUrls } from './remote.js';
/* Werkbaum, mit Werkbaum geplant — als mitgeliefertes Dokument „Werkbank" (D27).
Dieselbe Datei, die auch per ?sourceUrl= geladen werden kann; `?raw` bettet
@@ -156,8 +156,16 @@ function render(){
warnings = warnings.slice().sort((a, b) => (a.line || 0) - (b.line || 0));
warnBox.innerHTML = warnings.map(w => `<div>⚠ ${formatWarning(w, t)}</div>`).join('');
/* Der Zeilennummern-Streifen zeigt genau die Zeilen an, die hier genannt
werden — deshalb hängt er an derselben Warnungsliste (D33). */
lineNoWarn = new Set(warnings.map(w => w.line).filter(Boolean));
werden — deshalb hängt er an derselben Warnungsliste (D33). Der Text
wandert gleich mit in den `title` der Zahl (D33-Nachtrag); mehrere
Warnungen einer Zeile stehen dort untereinander. */
lineNoWarn = new Map();
for(const w of warnings){
if(!w.line) continue;
const vorher = lineNoWarn.get(w.line);
const text = warningText(w, t);
lineNoWarn.set(w.line, vorher ? vorher + '\n' + text : text);
}
renderLineNos();
applyOptStairs(); /* muss vor dem Messen laufen — es verschiebt Knoten */
alignStems();
@@ -832,7 +840,7 @@ function lineTops(){
in den Textfluss lässt sich nichts einfügen, ein `<textarea>` kennt kein
Markup. Gescrollt wird er nicht selbst, sondern gegen `src.scrollTop`
verschoben; so kann er nie auseinanderlaufen. */
let lineNoWarn = new Set();
let lineNoWarn = new Map(); /* Zeile -> Warnungstext(e), siehe render() */
function renderLineNos(){
const tops = lineTops();
/* Der Marker ist ein Inline-Kasten und steht in seiner Zeilenbox mittig — sein
@@ -856,7 +864,14 @@ function renderLineNos(){
const s = lineNoInner.children[i], n = i + 1;
if(s.textContent !== String(n)) s.textContent = n;
s.style.top = (tops[i] - drop) + 'px';
s.classList.toggle('warn', lineNoWarn.has(n));
/* Die Warnung steht auch im Warnungsbereich unter dem Diagramm — der
Tooltip erspart nur den Weg dorthin und das Suchen der Zeilennummer.
Kein Ersatz: Der Streifen ist `aria-hidden`, für Screenreader bleibt die
Live-Region die Quelle (D33-Nachtrag). */
const warn = lineNoWarn.get(n);
s.classList.toggle('warn', !!warn);
if(warn){ if(s.title !== warn) s.title = warn; }
else if(s.title) s.removeAttribute('title');
}
markCurrentLineNo();
syncLineNoScroll();
+9 -1
View File
@@ -544,7 +544,15 @@
/* Cursor-Zeile und Zeilen mit Warnung heben sich ab — genau die beiden Fälle,
für die man im Streifen überhaupt nachsieht. */
.lineno-inner span.cur{opacity:1;color:var(--ink);font-weight:600}
.lineno-inner span.warn{opacity:1;color:var(--warn);font-weight:600}
/* Die Zahl einer Warn-Zeile trägt die Meldung als Tooltip (D33-Nachtrag).
Dafür reicht ihr Kasten über die **ganze** Streifenbreite statt nur über
die Ziffern: Ein zwei Zeichen breites Ziel trifft man sonst kaum. Die
Ziffern selbst bleiben, wo sie sind (rechtsbündig, 6 px Abstand), und
`cursor:help` sagt, dass es hier etwas zu lesen gibt. */
.lineno-inner span.warn{
opacity:1;color:var(--warn);font-weight:600;
left:0;right:0;padding-right:6px;text-align:right;cursor:help;
}
.hint{
padding:12px 16px;border-top:1px dashed rgba(36,52,71,.18);
font-size:.8rem;color:var(--muted);line-height:1.7;
+14
View File
@@ -43,6 +43,20 @@ 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){
return build(w, t, esc);
}
/* Dasselbe als **Klartext**, ohne HTML-Escaping — für Senken, die kein HTML
sind: der `title` der Zeilennummer (D33-Nachtrag). Dort stünde sonst
„Drag &amp; Drop“ statt „Drag & Drop“; die Vorlagen selbst enthalten kein
Markup, escaped werden ohnehin nur die eingesetzten Daten. */
export function warningText(w, t){
return build(w, t, s => String(s));
}
/* Der Parameter verdeckt das importierte `esc` bewusst: So bleibt der Rumpf
unverändert und es gibt nur **eine** Stelle, die die Typen kennt. */
function build(w, t, esc){
switch(w.type){
case 'mixedGate':
return t('mixedWarn', {line: w.line, label: esc(w.label)});
+22 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { formatWarning } from '../src/warnings.js';
import { formatWarning, warningText } from '../src/warnings.js';
/* Stub bildet key + interpolierte Variablen sichtbar ab, damit wir prüfen
können, welche Werte (inkl. Escaping) formatWarning durchreicht. */
@@ -40,3 +40,24 @@ describe('formatWarning — vereinheitlichtes Warnungs-Modell', () => {
expect(formatWarning({type: 'x'}, t)).toBe('x (?)');
});
});
/* Der `title` der Zeilennummer ist kein HTML — dort stünde sonst wörtlich
„Drag &amp; Drop“ (D33-Nachtrag). Dieselben Typen, dieselbe Vorlage, nur
ohne Escaping. */
describe('warningText — dieselbe Meldung als Klartext', () => {
it('lässt das Label unangetastet', () => {
expect(warningText({type: 'mixedGate', line: 7, label: 'A & <B>'}, t))
.toBe('mixedWarn|{"line":7,"label":"A & <B>"}');
});
it('lässt die URL unangetastet', () => {
expect(warningText({type: 'sourceLoad', url: 'https://a/?x=1&y=2', error: 'HTTP 404'}, t))
.toBe('sourceLoadWarn|{"url":"https://a/?x=1&y=2","error":"HTTP 404"}');
});
it('deckt dieselben Typen ab wie formatWarning', () => {
expect(warningText({type: 'descStray', line: 4}, t)).toBe('descStrayWarn|{"line":4}');
expect(warningText({type: 'cheapApprox'}, t)).toBe('cheapApproxWarn|undefined');
expect(warningText({type: 'was?', line: 9}, t)).toBe('was? (9)');
});
});