feat: Labels brechen balanciert um, ID-Zeile über dem Titel, Falt-Chip — D64
Lange Knotentitel brechen bei ~40 Zeichen um, gleichmäßig auf die Zeilen verteilt — vom Renderer (wrapLabel, headless getestet) statt text-wrap:balance, damit der Kasten auf die längste balancierte Zeile schrumpft. Der 23-px-Abzweig ankert an der ersten Zeile; der Optional-Kreis sitzt fest auf der Abzweighöhe. Der Grafikexport misst die gerenderten Zeilen (labelLines) und gibt je Zeile ein <text> aus. Dazu: die eingeblendete ID (D56) steht in einer eigenen Zeile über dem Titel, und das Falt-Zeichen ist ein gerahmter 19×16-px-Chip mit gedeckelter Höhe. Plan: #ed.render.wrap, #ed.render M→L (D62-Prüfung schlug korrekt an). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1debb7d9da
commit
52232f2c20
@@ -19,6 +19,9 @@ reverse.
|
|||||||
|
|
||||||
## 2026-08-24
|
## 2026-08-24
|
||||||
|
|
||||||
|
- Long node titles wrap into evenly balanced lines of at most forty characters
|
||||||
|
- With ids shown, the id sits on its own line above the title
|
||||||
|
- The fold sign is a framed chip now — a click target you can actually hit
|
||||||
- Typing `:#` suggests the document's ids — arrows choose, Enter inserts
|
- Typing `:#` suggests the document's ids — arrows choose, Enter inserts
|
||||||
- When the caret's node is folded away, the nearest visible ancestor takes the highlight — also on Alt+click from the text
|
- When the caret's node is folded away, the nearest visible ancestor takes the highlight — also on Alt+click from the text
|
||||||
- A size that cannot hold its sub-packages turns its badge amber and gets a warning — sizes are read as ranges, so only sure conflicts are reported
|
- A size that cannot hold its sub-packages turns its badge amber and gets a warning — sizes are read as ranges, so only sure conflicts are reported
|
||||||
|
|||||||
@@ -4769,3 +4769,76 @@ Tests in `tests/autocomplete.test.js` (397 gesamt): Kontext-Erkennung
|
|||||||
D59, Ersetzen über die Schreibmarke hinaus), Sammeln (Dokumentreihenfolge,
|
D59, Ersetzen über die Schreibmarke hinaus), Sammeln (Dokumentreihenfolge,
|
||||||
verworfene, D60-Knoten ohne Titel) und Sortierung (Präfix vor Teilstring,
|
verworfene, D60-Knoten ohne Titel) und Sortierung (Präfix vor Teilstring,
|
||||||
case-insensitiv, exclude).
|
case-insensitiv, exclude).
|
||||||
|
|
||||||
|
## D64 — Lange Labels brechen um: balanciert, ~40 Zeichen, erste Zeile als Anker
|
||||||
|
Breite Knotenkästen trieben den Baum in die Breite — ein einziger langer Titel
|
||||||
|
kostete eine ganze Spalte davon. Jetzt brechen Labels um: höchstens ~40
|
||||||
|
Zeichen je Zeile (Nutzer-Vorgabe), zentriert, und die Zeichen **gleichmäßig
|
||||||
|
auf die Zeilen verteilt** (ausdrücklicher Nutzer-Nachtrag): Der gierige
|
||||||
|
Umbruch machte aus 44 Zeichen eine volle Zeile plus ein einsames Wort.
|
||||||
|
|
||||||
|
**Die Umbrüche setzt der Renderer, nicht CSS.** `text-wrap:balance` wäre eine
|
||||||
|
Zeile gewesen und ist verworfen: Es balanciert nur **innerhalb** der einmal
|
||||||
|
bestimmten Kastenbreite — der Kasten bliebe auf `max-width` stehen, mit
|
||||||
|
Leerraum um zwei kurze Zeilen. `wrapLabel()` in render.js bricht selbst
|
||||||
|
(Zeilenzahl = ⌈Länge/40⌉, Ziel = gleichmäßig, gebrochen wird an der Stelle,
|
||||||
|
die dem Ziel am nächsten kommt, nie mitten im Wort) und schreibt echte `\n`
|
||||||
|
ins Markup; `white-space:pre-line` macht sie sichtbar, und der Kasten
|
||||||
|
schrumpft auf die längste **balancierte** Zeile. Nebengewinn: Die Regel ist
|
||||||
|
headless testbar (Hausregel D54-Nachtrag 3) und in jedem Browser gleich.
|
||||||
|
`max-width:40ch` + `overflow-wrap` bleiben als Rückhalt für das einzelne Wort
|
||||||
|
über der Grenze. Bewusst `\n` statt `<br>`: Der `textContent` behielte sonst
|
||||||
|
keine Wortgrenze, und alles, was den Knotentext liest (Export,
|
||||||
|
Fokusmarken-Schlüssel), bekäme zusammengeklebte Wörter.
|
||||||
|
|
||||||
|
**Die Geometrie ankert an der ersten Zeile.** Der 23-px-Abzweig der
|
||||||
|
gestapelten Anordnungen (§9: 5 px Listenabstand + halbe einzeilige
|
||||||
|
Knotenhöhe) bleibt fest — bei mehrzeiligen Knoten trifft er damit die Mitte
|
||||||
|
der **ersten Zeile**, das gewohnte Idiom jeder Baumansicht. Eine gemessene
|
||||||
|
„wahre Mitte" hätte die 23-px-Konstante an einem Dutzend CSS-Stellen dynamisch
|
||||||
|
gemacht — für einen Unterschied, den das Auge als falsch gar nicht liest. Zwei
|
||||||
|
Stellen mussten mitziehen:
|
||||||
|
|
||||||
|
- Der **Optional-Kreis** (D29) saß bei `top:50%` der Knotenhöhe — für
|
||||||
|
einzeilige Knoten dasselbe wie die Abzweighöhe, für mehrzeilige nicht mehr.
|
||||||
|
Er sitzt jetzt fest bei 18 px (= Abzweighöhe), **auf dem Abzweig statt auf
|
||||||
|
der Knotenmitte**; die eine Ausnahme sind die vertikal zentrierten
|
||||||
|
all-of-Zwischenknoten, deren Abzweig wirklich die Mitte trifft (dort bleibt
|
||||||
|
50 %). Nachgemessen am dreizeiligen Optional-Knoten im vertikalen Modus:
|
||||||
|
Kreismitte = Abzweighöhe, 0 px Abweichung.
|
||||||
|
- Der **Grafikexport** zeichnete das Label als ein `<text>` — ein SVG-`<text>`
|
||||||
|
bricht nicht von selbst. `labelLines()` misst die gerenderten Zeilen am
|
||||||
|
Live-Knoten (zeichenweise per Range: neue Zeilen-Oberkante = neue Zeile —
|
||||||
|
das deckt auch den `overflow-wrap`-Bruch mitten im langen Wort ab, den eine
|
||||||
|
Nachbildung der Wortlogik verfehlte) und gibt je Zeile ein `<text>` an der
|
||||||
|
gemessenen Position aus. Die Linienführung des Exports zielte schon immer
|
||||||
|
auf die gemessene Knotenmitte und blieb unberührt.
|
||||||
|
|
||||||
|
**Zwei Nachträge aus derselben Runde, beide Nutzer-Wünsche:**
|
||||||
|
|
||||||
|
- **Die eingeblendete ID steht in einer eigenen Zeile ÜBER dem Titel**
|
||||||
|
(ändert D56). Vorher stand sie inline davor (`#some.id: Titel`) und machte
|
||||||
|
gerade die Knoten am breitesten, die ohnehin lange Titel tragen. Der
|
||||||
|
Trenn-Doppelpunkt entfällt: Er trennte ID und Titel in **derselben** Zeile —
|
||||||
|
hier trennt der Umbruch, und `#auth:` allein auf einer Zeile läse sich wie
|
||||||
|
ein Block-Kopf (§1). Der Export folgt von selbst (er misst Zeilen).
|
||||||
|
- **Das Falt-Zeichen ist jetzt ein gerahmter Chip** (ändert die D38-Optik):
|
||||||
|
Das nackte ▾ war ~10 px klein und schwer zu treffen. Der Chip (19×16 px,
|
||||||
|
Rahmen, dezente Füllung) ist das Klickziel, das er immer sein sollte. Seine
|
||||||
|
Höhe ist gedeckelt (`line-height:14px`, kein vertikales Padding), denn ein
|
||||||
|
Inline-Block, der höher ist als die Zeilenbox, höbe die feste Zeilenhöhe an,
|
||||||
|
an der die 23-px-Geometrie hängt — mit 15 px Zeilenhöhe wuchs der Knoten
|
||||||
|
nachgemessen von 34,3 auf 35,0 px, mit 14 px bleibt er exakt bei 34,3.
|
||||||
|
|
||||||
|
**Nachgemessen** am mitgelieferten Plan (horizontal, Pfad an): 166 Knoten,
|
||||||
|
0 Warnungen, 27 Stationen (unverändert), `--stem-x` weiterhin punktgenau auf
|
||||||
|
der Knotenmitte (130,8 = 130,8 px) — und am Testbaum: 79 Zeichen ergeben zwei
|
||||||
|
Zeilen zu ~40/39 statt 40+Rest, drei Zeilen bei ~100; im exportierten SVG
|
||||||
|
stehen die zwei Zeilen des langen Titels 18,3 px auseinander vollständig im
|
||||||
|
52,6-px-Kasten, die ID-Zeile als eigene Textzeile. Falten am Chip klappt
|
||||||
|
weiter um (166 → 140 → 166 Knoten). 405 Tests, davon 8 neue in
|
||||||
|
`tests/wrap.test.js` (Balance, keine Zeile über 40, kein Zeichenverlust, nie
|
||||||
|
mitten im Wort, `\n` im Knotentext); die showIds-Tests sind auf die eigene
|
||||||
|
Zeile umgeschrieben. Der neue Plan-Knoten `#ed.render.wrap (S)` kippte prompt
|
||||||
|
die Größenprüfung des eigenen Plans (`#ed.render (M)` mit nun 4×S, D62) —
|
||||||
|
ehrlich nachgezogen auf `(L)`, danach wieder 0 Warnungen.
|
||||||
|
|||||||
+19
-7
@@ -499,6 +499,15 @@ Siehe D29.
|
|||||||
deterministisch auf Knotenmitte liegen (Offset 23 px = 5 px Listenabstand
|
deterministisch auf Knotenmitte liegen (Offset 23 px = 5 px Listenabstand
|
||||||
+ halbe Knotenhöhe). Abzweige zielen auf den **Knoten**, nie auf die Mitte
|
+ halbe Knotenhöhe). Abzweige zielen auf den **Knoten**, nie auf die Mitte
|
||||||
des Teilbaums.
|
des Teilbaums.
|
||||||
|
- **Lange Labels brechen um** (D64): höchstens ~40 Zeichen je Zeile, die
|
||||||
|
Zeichen **gleichmäßig** auf die Zeilen verteilt (nicht gierig — sonst stünde
|
||||||
|
unter einer vollen Zeile ein einsames Wort). Die Umbrüche setzt der
|
||||||
|
**Renderer**, deterministisch und nur an Leerzeichen; der Kasten schrumpft
|
||||||
|
auf die längste Zeile, der Text steht zentriert. Bei **mehrzeiligen** Knoten
|
||||||
|
trifft der 23-px-Abzweig die Mitte der **ersten Zeile** (die Zeilenhöhe
|
||||||
|
bleibt fest); der Optional-Kreis (§9) sitzt auf dem Abzweig, nicht auf der
|
||||||
|
Knotenmitte. Der Grafikexport misst die gerenderten Zeilen und gibt sie als
|
||||||
|
einzelne Textzeilen aus.
|
||||||
- Dasselbe gilt **waagerecht**: Im horizontalen Fächer trifft der Stiel die
|
- Dasselbe gilt **waagerecht**: Im horizontalen Fächer trifft der Stiel die
|
||||||
**Knotenmitte**, nicht die Mitte der Zelle. Beides fällt nur zusammen, solange
|
**Knotenmitte**, nicht die Mitte der Zelle. Beides fällt nur zusammen, solange
|
||||||
der Knoten in seiner Zelle zentriert steht — ein Knoten mit any-of-Kindern
|
der Knoten in seiner Zelle zentriert steht — ein Knoten mit any-of-Kindern
|
||||||
@@ -837,10 +846,11 @@ verknüpft (siehe D25):
|
|||||||
Druck.
|
Druck.
|
||||||
|
|
||||||
### Knoten-IDs im Diagramm einblenden (`#`)
|
### Knoten-IDs im Diagramm einblenden (`#`)
|
||||||
Ein Umschalter im Diagramm-Kopf stellt die Knoten-ID **vor den Titel**, in
|
Ein Umschalter im Diagramm-Kopf stellt die Knoten-ID in eine **eigene Zeile
|
||||||
derselben Schreibweise wie im Text (§1/D36): `#some.id: Titel`. Dargestellt in
|
über den Titel** (`#some.id`, ohne den Trenn-Doppelpunkt — der trennt ID und
|
||||||
der Mono-Schrift des Textfelds und zurückgenommen gefärbt — die ID ist die
|
Titel in derselben Zeile, hier trennt der Umbruch; D56, geändert mit D64).
|
||||||
Adresse, der Titel führt die Zeile weiter an. Der Zustand wird gemerkt.
|
Dargestellt in der Mono-Schrift des Textfelds und zurückgenommen gefärbt — die
|
||||||
|
ID ist die Adresse, der Titel bleibt die Hauptzeile. Der Zustand wird gemerkt.
|
||||||
|
|
||||||
**Grafikexport und Druck folgen dem Umschalter** (wie beim „verworfene
|
**Grafikexport und Druck folgen dem Umschalter** (wie beim „verworfene
|
||||||
einblenden"-Filter und der Faltung): Ist er an, stehen die IDs auch im
|
einblenden"-Filter und der Faltung): Ist er an, stehen die IDs auch im
|
||||||
@@ -942,9 +952,11 @@ gehört und lesbar bleibt. Kein Blinken.
|
|||||||
Grafikexport: Sie sagt „schau jetzt hierhin", nicht „so ist der Plan".
|
Grafikexport: Sie sagt „schau jetzt hierhin", nicht „so ist der Plan".
|
||||||
|
|
||||||
### Ein- und ausklappbare Teilbäume (`>` / `<`, §1)
|
### Ein- und ausklappbare Teilbäume (`>` / `<`, §1)
|
||||||
Jeder Knoten mit sichtbaren Kindern trägt ein kleines **Falt-Zeichen** vor dem
|
Jeder Knoten mit sichtbaren Kindern trägt ein **Falt-Zeichen** vor dem
|
||||||
Label: **▾** offen, **„▸ n“** eingeklappt (n = Zahl der verborgenen Knoten).
|
Label: **▾** offen, **„▸ n“** eingeklappt (n = Zahl der verborgenen Knoten) —
|
||||||
Klick auf das Zeichen klappt um — der einfache Klick auf den Knoten selbst
|
als kleiner **gerahmter Chip**, damit das Klickziel zu treffen ist (D64; das
|
||||||
|
nackte Glyph war zu klein). Klick auf das Zeichen klappt um — der einfache
|
||||||
|
Klick auf den Knoten selbst
|
||||||
bleibt der Link (§6); Tastatur: **←** klappt zu, **→** klappt auf am
|
bleibt der Link (§6); Tastatur: **←** klappt zu, **→** klappt auf am
|
||||||
fokussierten Knoten (WAI-ARIA-Baum-Idiom).
|
fokussierten Knoten (WAI-ARIA-Baum-Idiom).
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,11 @@
|
|||||||
- [~] #ed: Editor (XXL) https://werkbaum.javagil.de
|
- [~] #ed: Editor (XXL) https://werkbaum.javagil.de
|
||||||
- [^] #ed.live: Live diagram from text (L)
|
- [^] #ed.live: Live diagram from text (L)
|
||||||
- [^] #ed.parser: Parser (S)
|
- [^] #ed.parser: Parser (S)
|
||||||
- [^] #ed.render: Renderer, three layout modes (M)
|
- [^] #ed.render: Renderer, three layout modes (L)
|
||||||
- [^] #ed.render.horiz: Horizontal fan (S)
|
- [^] #ed.render.horiz: Horizontal fan (S)
|
||||||
- [^] #ed.render.vert: Vertical, transposed (S)
|
- [^] #ed.render.vert: Vertical, transposed (S)
|
||||||
- [^] #ed.render.comp: Compact (S)
|
- [^] #ed.render.comp: Compact (S)
|
||||||
|
- [x] #ed.render.wrap: Long titles wrap into balanced lines (S) %% max ~40 chars, see D64
|
||||||
- [^] #ed.deplink: Cross links for dependencies (M)
|
- [^] #ed.deplink: Cross links for dependencies (M)
|
||||||
- [^] #ed.deplink.calm: Visually secondary — the tree carries the message (S)
|
- [^] #ed.deplink.calm: Visually secondary — the tree carries the message (S)
|
||||||
- [^] #ed.deplink.focus: Highlight what a selected node needs and feeds (S)
|
- [^] #ed.deplink.focus: Highlight what a selected node needs and feeds (S)
|
||||||
@@ -368,6 +369,11 @@
|
|||||||
Everything runs downward, so the tree stays narrow. Here the line style
|
Everything runs downward, so the tree stays narrow. Here the line style
|
||||||
alone carries the gate: solid for and, dashed for or.
|
alone carries the gate: solid for and, dashed for or.
|
||||||
|
|
||||||
|
#ed.render.wrap
|
||||||
|
A single long title used to cost a whole column of width. Titles beyond
|
||||||
|
forty characters now break into evenly balanced lines, set by the renderer
|
||||||
|
so the box shrinks to the longest line; connectors anchor on the first line.
|
||||||
|
|
||||||
#ed.deplink
|
#ed.deplink
|
||||||
Dependencies drawn as curved dotted edges on their own layer behind the
|
Dependencies drawn as curved dotted edges on their own layer behind the
|
||||||
nodes — the first line type that does not follow the decomposition.
|
nodes — the first line type that does not follow the decomposition.
|
||||||
|
|||||||
@@ -41,6 +41,15 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
- Abzweig-Linien zielen auf die **Knotenmitte** (fester 23-px-Offset,
|
- Abzweig-Linien zielen auf die **Knotenmitte** (fester 23-px-Offset,
|
||||||
`line-height: 1.3`), nicht auf die Mitte des Teilbaums — bei Layout-
|
`line-height: 1.3`), nicht auf die Mitte des Teilbaums — bei Layout-
|
||||||
Änderungen alle drei Modi (horizontal/vertikal/kompakt) prüfen.
|
Änderungen alle drei Modi (horizontal/vertikal/kompakt) prüfen.
|
||||||
|
**Seit D64 brechen lange Labels um** (`wrapLabel()` in render.js setzt `\n`,
|
||||||
|
`white-space:pre-line` macht sie sichtbar): Bei mehrzeiligen Knoten trifft
|
||||||
|
der 23-px-Abzweig die Mitte der **ersten Zeile**; der Optional-Kreis sitzt
|
||||||
|
deshalb fest bei `top:18px` (= Abzweighöhe), nicht bei 50 % (Ausnahme:
|
||||||
|
vertikal zentrierte all-of-Zwischenknoten). Nichts in den Knoten darf die
|
||||||
|
Zeilenbox über die festen ~18,3 px heben — der Falt-Chip hat darum
|
||||||
|
`line-height:14px` ohne vertikales Padding (mit 15 px wuchs der Knoten
|
||||||
|
gemessen um 0,7 px). Der Export zeichnet je gerenderter Zeile ein `<text>`
|
||||||
|
(`labelLines()`, misst zeichenweise per Range am Live-Knoten).
|
||||||
Vertikal + kompakt teilen die transponierte Basis-CSS; nur vertikal
|
Vertikal + kompakt teilen die transponierte Basis-CSS; nur vertikal
|
||||||
bekommt den Rechts-Ausgang für „all of“, kompakt führt auch „all of“
|
bekommt den Rechts-Ausgang für „all of“, kompakt führt auch „all of“
|
||||||
nach unten. Any-of ist in allen Modi grau: Linien gestrichelt grau und
|
nach unten. Any-of ist in allen Modi grau: Linien gestrichelt grau und
|
||||||
|
|||||||
+42
-4
@@ -470,6 +470,42 @@ out.addEventListener('focusout', () => setTimeout(drawDepLinks, 0));
|
|||||||
der Geister-Knoten werden übernommen; Verbindungslinien werden je Gate
|
der Geister-Knoten werden übernommen; Verbindungslinien werden je Gate
|
||||||
(und = durchgezogen Tinte, oder = gestrichelt Grau) neu gezogen und treffen
|
(und = durchgezogen Tinte, oder = gestrichelt Grau) neu gezogen und treffen
|
||||||
so garantiert die Knoten — unabhängig vom Darstellungsmodus. */
|
so garantiert die Knoten — unabhängig vom Darstellungsmodus. */
|
||||||
|
/* Die gerenderten Textzeilen eines Knotens, gemessen am LIVE-Element: je
|
||||||
|
Zeile der (whitespace-normalisierte) Text und die Box in Viewport-Pixeln.
|
||||||
|
Zeichenweise per Range — eine neue Zeilen-Oberkante beginnt eine neue Zeile;
|
||||||
|
das deckt auch den Umbruch INNERHALB eines langen Worts (overflow-wrap) ab,
|
||||||
|
den eine Nachbildung der Wort-Umbruchlogik verfehlte. Kollabierter Leerraum
|
||||||
|
(etwa das Leerzeichen, an dem umbrochen wurde) hat eine Null-Box und fällt
|
||||||
|
heraus. `excludeSel` sind die Elemente, die der Export nicht ins Label
|
||||||
|
nimmt (Badges, ↗, ”-Marke, je nach Faltzustand das Falt-Zeichen). */
|
||||||
|
function labelLines(node, excludeSel){
|
||||||
|
const range = document.createRange();
|
||||||
|
const lines = [];
|
||||||
|
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
|
||||||
|
acceptNode: tn => tn.parentElement.closest(excludeSel)
|
||||||
|
? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT,
|
||||||
|
});
|
||||||
|
for(let tn = walker.nextNode(); tn; tn = walker.nextNode()){
|
||||||
|
const s = tn.nodeValue;
|
||||||
|
for(let i = 0; i < s.length; i++){
|
||||||
|
range.setStart(tn, i); range.setEnd(tn, i + 1);
|
||||||
|
const r = range.getBoundingClientRect();
|
||||||
|
if(!r.width && !r.height) continue;
|
||||||
|
const last = lines[lines.length - 1];
|
||||||
|
if(last && Math.abs(r.top - last.top) < 2){
|
||||||
|
last.text += s[i];
|
||||||
|
last.left = Math.min(last.left, r.left);
|
||||||
|
last.right = Math.max(last.right, r.right);
|
||||||
|
last.bottom = Math.max(last.bottom, r.bottom);
|
||||||
|
} else {
|
||||||
|
lines.push({text: s[i], top: r.top, bottom: r.bottom, left: r.left, right: r.right});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(const l of lines) l.text = l.text.replace(/\s+/g, ' ').trim();
|
||||||
|
return lines.filter(l => l.text);
|
||||||
|
}
|
||||||
|
|
||||||
function diagramToSvg(){
|
function diagramToSvg(){
|
||||||
/* Der Export misst die **Live-Geometrie**. Den Ring der Cursor-Zeile liest er
|
/* Der Export misst die **Live-Geometrie**. Den Ring der Cursor-Zeile liest er
|
||||||
nie aus (`box-shadow` steht nicht in der Liste), ihre Erhebung
|
nie aus (`box-shadow` steht nicht in der Liste), ihre Erhebung
|
||||||
@@ -608,15 +644,17 @@ function diagramToSvg(){
|
|||||||
const b = R(node), cs = getComputedStyle(node);
|
const b = R(node), cs = getComputedStyle(node);
|
||||||
const dashed = cs.borderTopStyle === 'dashed';
|
const dashed = cs.borderTopStyle === 'dashed';
|
||||||
parts.push(`<rect x="${b.x.toFixed(1)}" y="${b.y.toFixed(1)}" width="${b.w.toFixed(1)}" height="${b.h.toFixed(1)}" rx="8" fill="${cs.backgroundColor}" stroke="${cs.borderTopColor}" stroke-width="${parseFloat(cs.borderTopWidth)||1.5}"${dashed?' stroke-dasharray="4 3"':''}/>`);
|
parts.push(`<rect x="${b.x.toFixed(1)}" y="${b.y.toFixed(1)}" width="${b.w.toFixed(1)}" height="${b.h.toFixed(1)}" rx="8" fill="${cs.backgroundColor}" stroke="${cs.borderTopColor}" stroke-width="${parseFloat(cs.borderTopWidth)||1.5}"${dashed?' stroke-dasharray="4 3"':''}/>`);
|
||||||
const clone = node.cloneNode(true);
|
|
||||||
/* Das „▸ n"-Kennzeichen eingeklappter Knoten gehört in den Export (SPEC
|
/* Das „▸ n"-Kennzeichen eingeklappter Knoten gehört in den Export (SPEC
|
||||||
§9/D38 — das Bild darf keine Vollständigkeit behaupten); das ▾ offener
|
§9/D38 — das Bild darf keine Vollständigkeit behaupten); das ▾ offener
|
||||||
Knoten ist Bedienelement und fällt weg. */
|
Knoten ist Bedienelement und fällt weg. */
|
||||||
const stripFold = node.classList.contains('folded') ? '' : ',.fold';
|
const stripFold = node.classList.contains('folded') ? '' : ',.fold';
|
||||||
clone.querySelectorAll('.size,.tags,.ext,.risk,.ownst,.desc-mark' + stripFold).forEach(e => e.remove());
|
|
||||||
const label = clone.textContent.replace(/\s+/g,' ').trim();
|
|
||||||
const deco = cs.textDecorationLine.includes('line-through') ? ' text-decoration="line-through"' : '';
|
const deco = cs.textDecorationLine.includes('line-through') ? ' text-decoration="line-through"' : '';
|
||||||
parts.push(`<text x="${b.cx.toFixed(1)}" y="${(b.cy+5).toFixed(1)}" text-anchor="middle" fill="${cs.color}" font-size="14" font-weight="${cs.fontWeight}"${deco}>${esc(label)}</text>`);
|
/* Seit Knoten umbrechen (D64), belegt ein Label mehrere Zeilenboxen — ein
|
||||||
|
SVG-<text> bricht nicht von selbst; je gemessene Zeile ein Element. */
|
||||||
|
for(const ln of labelLines(node, '.size,.tags,.ext,.risk,.ownst,.desc-mark' + stripFold)){
|
||||||
|
const cx = (ln.left + ln.right) / 2 + ox, cy = (ln.top + ln.bottom) / 2 + oy;
|
||||||
|
parts.push(`<text x="${cx.toFixed(1)}" y="${(cy+5).toFixed(1)}" text-anchor="middle" fill="${cs.color}" font-size="14" font-weight="${cs.fontWeight}"${deco}>${esc(ln.text)}</text>`);
|
||||||
|
}
|
||||||
const riskEl = node.querySelector('.risk');
|
const riskEl = node.querySelector('.risk');
|
||||||
if(riskEl){
|
if(riskEl){
|
||||||
const rb = R(riskEl);
|
const rb = R(riskEl);
|
||||||
|
|||||||
+50
-8
@@ -55,6 +55,40 @@ function attr(s){ return esc(String(s)).replace(/"/g,'"'); }
|
|||||||
Beschreibung als data-Attribut wäre der einzige andere Weg gewesen. */
|
Beschreibung als data-Attribut wäre der einzige andere Weg gewesen. */
|
||||||
export const TIP_RULE = '─'.repeat(24);
|
export const TIP_RULE = '─'.repeat(24);
|
||||||
|
|
||||||
|
/* Lange Labels umbrechen (SPEC §9/D64): höchstens ~40 Zeichen je Zeile, und
|
||||||
|
die Zeichen GLEICHMÄSSIG auf die Zeilen verteilt — der gierige CSS-Umbruch
|
||||||
|
machte aus 44 Zeichen eine volle Zeile plus ein einsames Wort. Bewusst im
|
||||||
|
Renderer statt per `text-wrap:balance`: So schrumpft der Knotenkasten auf
|
||||||
|
die längste BALANCIERTE Zeile (CSS balanciert nur innerhalb der einmal
|
||||||
|
bestimmten Kastenbreite und ließe ihn auf `max-width` stehen), und die
|
||||||
|
Regel ist headless testbar. Gebrochen wird nur an Leerzeichen; ein
|
||||||
|
einzelnes Wort über der Grenze bleibt stehen (das CSS fängt es mit
|
||||||
|
`max-width` + `overflow-wrap` ab). */
|
||||||
|
export function wrapLabel(label, max = 40){
|
||||||
|
const text = String(label);
|
||||||
|
if(text.length <= max) return [text];
|
||||||
|
const words = text.split(' ');
|
||||||
|
const n = Math.ceil(text.length / max); /* so viele Zeilen braucht es */
|
||||||
|
const target = Math.ceil(text.length / n); /* … und so voll wird jede */
|
||||||
|
const lines = [];
|
||||||
|
let cur = '';
|
||||||
|
for(const w of words){
|
||||||
|
if(!cur){ cur = w; continue; }
|
||||||
|
const withW = cur.length + 1 + w.length;
|
||||||
|
/* Gebrochen wird an der Stelle, die dem Ziel am nächsten kommt: wenn das
|
||||||
|
Wort die Zeile weiter über das Ziel höbe, als sie jetzt darunter liegt —
|
||||||
|
oder wenn es die harte Grenze risse. */
|
||||||
|
if(withW > max || (withW > target && withW - target >= target - cur.length)){
|
||||||
|
lines.push(cur);
|
||||||
|
cur = w;
|
||||||
|
} else {
|
||||||
|
cur = cur + ' ' + w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(cur);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
/* Barrierefreier Name eines Knotens: Label + Status + Aufwand + Zuständige +
|
/* Barrierefreier Name eines Knotens: Label + Status + Aufwand + Zuständige +
|
||||||
Link. Die visuellen Badges (Größe, Tags, ↗) sind aria-hidden — ihre
|
Link. Die visuellen Badges (Größe, Tags, ↗) sind aria-hidden — ihre
|
||||||
Information steckt hier, sonst würde der Screenreader Kryptisches („M",
|
Information steckt hier, sonst würde der Screenreader Kryptisches („M",
|
||||||
@@ -184,19 +218,27 @@ function nodeHtml(n, extra, opts, fold){
|
|||||||
const ownChip = effKey
|
const ownChip = effKey
|
||||||
? `<span class="chip ownst st-${n.status.key}" aria-hidden="true">[${n.status.code}]</span>`
|
? `<span class="chip ownst st-${n.status.key}" aria-hidden="true">[${n.status.code}]</span>`
|
||||||
: '';
|
: '';
|
||||||
/* ID vor dem Titel, mit `: ` abgetrennt — dieselbe Schreibweise wie im Text
|
/* ID in einer eigenen Zeile ÜBER dem Titel (D56, geändert mit D64): Das
|
||||||
(SPEC §1/D36), damit man beides nebeneinander lesen kann. Umschaltbar im
|
`\n` trennt sie, `white-space:pre-line` macht es sichtbar. Ohne den
|
||||||
Diagramm-Kopf; als Renderer-Option und nicht per CSS versteckt, damit der
|
Trenn-Doppelpunkt — der trennte ID und Titel in DERSELBEN Zeile, hier
|
||||||
Grafikexport (er liest den Knotentext) von selbst folgt. `aria-hidden`:
|
trennt der Umbruch; mit Doppelpunkt läse sich die Zeile wie ein
|
||||||
Der Screenreader bekommt die ID schon über `a11yId` (D56). */
|
Block-Kopf (`#auth:`). Umschaltbar im Diagramm-Kopf; als Renderer-Option
|
||||||
|
und nicht per CSS versteckt, damit der Grafikexport (er misst die
|
||||||
|
gerenderten Zeilen) von selbst folgt. `aria-hidden`: Der Screenreader
|
||||||
|
bekommt die ID schon über `a11yId` (D56). */
|
||||||
/* Nicht bei einem Knoten, dessen Label die ID selbst IST (SPEC §1) — sonst
|
/* Nicht bei einem Knoten, dessen Label die ID selbst IST (SPEC §1) — sonst
|
||||||
stünde dort `#US-123: #US-123`. */
|
stünde dort `#US-123` zweimal untereinander. */
|
||||||
const idHtml = showIds && n.id && !n.labelFromId
|
const idHtml = showIds && n.id && !n.labelFromId
|
||||||
? `<span class="nid" aria-hidden="true">#${esc(n.id)}:</span> `
|
? `<span class="nid" aria-hidden="true">#${esc(n.id)}</span>\n`
|
||||||
: '';
|
: '';
|
||||||
const inner = foldHtml +
|
const inner = foldHtml +
|
||||||
idHtml +
|
idHtml +
|
||||||
esc(n.label) +
|
/* Die Umbrüche kommen als echte `\n` ins Markup, das CSS macht
|
||||||
|
sie mit `white-space:pre-line` sichtbar (D64). Kein `<br>`:
|
||||||
|
Der `textContent` behielte sonst keine Wortgrenze, und alles,
|
||||||
|
was den Knotentext liest (Export, Fokusmarken-Schlüssel),
|
||||||
|
bekäme zusammengeklebte Wörter. */
|
||||||
|
esc(wrapLabel(n.label).join('\n')) +
|
||||||
/* ”-Marke (D40): macht die sonst unsichtbare Beschreibung
|
/* ”-Marke (D40): macht die sonst unsichtbare Beschreibung
|
||||||
auffindbar (Lehre aus D25) — spiegelt das "-Zeichen der
|
auffindbar (Lehre aus D25) — spiegelt das "-Zeichen der
|
||||||
Notation. Nicht im Export: Der Text selbst kann dort nicht
|
Notation. Nicht im Export: Der Text selbst kann dort nicht
|
||||||
|
|||||||
+34
-7
@@ -684,7 +684,18 @@
|
|||||||
border-radius:var(--r);
|
border-radius:var(--r);
|
||||||
padding:7px 14px;
|
padding:7px 14px;
|
||||||
font-size:.88rem;font-weight:500;line-height:1.3;
|
font-size:.88rem;font-weight:500;line-height:1.3;
|
||||||
white-space:nowrap;
|
/* Lange Labels brechen um, statt den Baum in die Breite zu treiben (D64).
|
||||||
|
Die Umbrüche setzt der RENDERER (wrapLabel: ~40 Zeichen je Zeile,
|
||||||
|
gleichmäßig verteilt) als `\n` — pre-line macht sie sichtbar, und der
|
||||||
|
Kasten schrumpft auf die längste balancierte Zeile. `max-width` +
|
||||||
|
`overflow-wrap` sind nur der Rückhalt für ein einzelnes Wort über der
|
||||||
|
Grenze. Zentriert (Organigramm-Konvention). Die feste Zeilenhöhe
|
||||||
|
bleibt — der 23-px-Abzweig (§9) trifft bei mehrzeiligen Knoten
|
||||||
|
deterministisch die Mitte der ERSTEN Zeile. */
|
||||||
|
white-space:pre-line;
|
||||||
|
max-width:40ch;
|
||||||
|
text-align:center;
|
||||||
|
overflow-wrap:break-word;
|
||||||
box-shadow:0 1px 2px rgba(36,52,71,.08);
|
box-shadow:0 1px 2px rgba(36,52,71,.08);
|
||||||
}
|
}
|
||||||
a.node{text-decoration:none;cursor:pointer}
|
a.node{text-decoration:none;cursor:pointer}
|
||||||
@@ -692,15 +703,23 @@
|
|||||||
/* Falt-Zeichen (SPEC §9, D38): ▾ offen / „▸ n" eingeklappt, Klickziel fürs
|
/* Falt-Zeichen (SPEC §9, D38): ▾ offen / „▸ n" eingeklappt, Klickziel fürs
|
||||||
Umklappen. Gedeckt in Grau — es ist Bedienelement, keine Statusaussage;
|
Umklappen. Gedeckt in Grau — es ist Bedienelement, keine Statusaussage;
|
||||||
beim Zeigen färbt es sich zur Tinte. */
|
beim Zeigen färbt es sich zur Tinte. */
|
||||||
|
/* Als gerahmter Chip statt bloßem Glyph (D64): das nackte ▾ war ~10px klein
|
||||||
|
und kaum zu treffen. Die Höhe ist bewusst gedeckelt (feste line-height +
|
||||||
|
kein vertikales Padding): Der Chip darf die Zeilenbox des Knotens NICHT
|
||||||
|
über die festen 18,3px heben, sonst wandert die 23-px-Abzweig-Geometrie
|
||||||
|
(§9) um Halbpixel. */
|
||||||
.node .fold{
|
.node .fold{
|
||||||
cursor:pointer;color:var(--muted);
|
cursor:pointer;color:var(--muted);
|
||||||
font-size:.72em;margin-right:6px;
|
font-size:.75em;margin-right:6px;
|
||||||
|
display:inline-block;padding:0 6px;line-height:14px;
|
||||||
|
border:1px solid rgba(36,52,71,.28);border-radius:5px;
|
||||||
|
background:rgba(36,52,71,.04);
|
||||||
user-select:none;-webkit-user-select:none;
|
user-select:none;-webkit-user-select:none;
|
||||||
}
|
}
|
||||||
.node .fold:hover{color:var(--line)}
|
.node .fold:hover{color:var(--line);border-color:var(--line);background:rgba(36,52,71,.09)}
|
||||||
/* Der dunkle Wurzelknoten braucht ein helleres Zeichen als das Grau. */
|
/* Der dunkle Wurzelknoten braucht ein helleres Zeichen als das Grau. */
|
||||||
.root-node .fold{color:rgba(255,255,255,.75)}
|
.root-node .fold{color:rgba(255,255,255,.85);border-color:rgba(255,255,255,.45);background:rgba(255,255,255,.08)}
|
||||||
.root-node .fold:hover{color:#fff}
|
.root-node .fold:hover{color:#fff;border-color:#fff}
|
||||||
/* ”-Marke (SPEC §9, D40): der Knoten trägt eine Beschreibung — im Tooltip
|
/* ”-Marke (SPEC §9, D40): der Knoten trägt eine Beschreibung — im Tooltip
|
||||||
nachlesbar. Spiegelt das "-Zeichen der Notation; gedeckt, kein Klickziel. */
|
nachlesbar. Spiegelt das "-Zeichen der Notation; gedeckt, kein Klickziel. */
|
||||||
.node .desc-mark{color:var(--muted);margin-left:4px;font-weight:600}
|
.node .desc-mark{color:var(--muted);margin-left:4px;font-weight:600}
|
||||||
@@ -1165,7 +1184,12 @@
|
|||||||
Abzweig kommt von links auf halber Knotenhöhe. */
|
Abzweig kommt von links auf halber Knotenhöhe. */
|
||||||
.node.opt::before{
|
.node.opt::before{
|
||||||
content:'';position:absolute;
|
content:'';position:absolute;
|
||||||
left:-5px;top:50%;transform:translateY(-50%);
|
/* 18px statt 50%: Der gestapelte Abzweig sitzt fest bei 23px im <li>
|
||||||
|
(= 5px Listenabstand + halbe EINZEILIGE Knotenhöhe). Seit Knoten
|
||||||
|
umbrechen (D64), ist 50% bei mehrzeiligen nicht mehr diese Höhe —
|
||||||
|
der Kreis muss auf dem Abzweig sitzen, nicht auf der Knotenmitte.
|
||||||
|
Für einzeilige Knoten sind 18px und 50% dasselbe. */
|
||||||
|
left:-5px;top:18px;transform:translateY(-50%);
|
||||||
width:10px;height:10px;border-radius:50%;
|
width:10px;height:10px;border-radius:50%;
|
||||||
background:var(--card);border:2px solid var(--line);
|
background:var(--card);border:2px solid var(--line);
|
||||||
box-sizing:border-box;
|
box-sizing:border-box;
|
||||||
@@ -1177,8 +1201,11 @@
|
|||||||
/* …ausgenommen der gestapelte all-of-Teilbaum unter einer any-of-Gruppe
|
/* …ausgenommen der gestapelte all-of-Teilbaum unter einer any-of-Gruppe
|
||||||
(siehe oben): dort kommt der Abzweig wieder von links. */
|
(siehe oben): dort kommt der Abzweig wieder von links. */
|
||||||
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li>.node.opt::before{
|
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li>.node.opt::before{
|
||||||
left:-5px;top:50%;transform:translateY(-50%);
|
left:-5px;top:18px;transform:translateY(-50%);
|
||||||
}
|
}
|
||||||
|
/* Vertikal zentrierte all-of-Zwischenknoten (unten): dort trifft der Abzweig
|
||||||
|
wirklich die Knotenmitte (50%) — der Kreis geht mit. */
|
||||||
|
.tree.vertical li.has-and>ul.and>li.has-and>.node.opt::before{top:50%}
|
||||||
|
|
||||||
/* --- TREPPE: aufeinanderfolgende optionale Endknoten (D29, Nachtrag 3) ---
|
/* --- TREPPE: aufeinanderfolgende optionale Endknoten (D29, Nachtrag 3) ---
|
||||||
Nur im horizontalen Fächer, wo jedes Geschwister sonst eine eigene Spalte
|
Nur im horizontalen Fächer, wo jedes Geschwister sonst eine eigene Spalte
|
||||||
|
|||||||
@@ -143,8 +143,10 @@ describe('renderTreeHtml — data-line je Knoten (D25)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* Knoten-IDs im Diagramm (D56). Als Renderer-Option gebaut und nicht per CSS
|
/* Knoten-IDs im Diagramm (D56). Als Renderer-Option gebaut und nicht per CSS
|
||||||
versteckt — nur so folgt der Grafikexport, der den Knotentext ausliest. */
|
versteckt — nur so folgt der Grafikexport, der den Knotentext ausliest.
|
||||||
describe('showIds — die ID vor dem Titel, geschrieben wie im Text', () => {
|
Seit D64 steht die ID in einer eigenen Zeile ÜBER dem Titel (Trenner ist
|
||||||
|
der Umbruch, nicht mehr der Doppelpunkt). */
|
||||||
|
describe('showIds — die ID in einer eigenen Zeile über dem Titel', () => {
|
||||||
const ids = (text, showIds) => renderTreeHtml(parse(text).roots,
|
const ids = (text, showIds) => renderTreeHtml(parse(text).roots,
|
||||||
{t, showDiscarded: false, cheapPath: false, cheapSet: new Set(), showIds}).html;
|
{t, showDiscarded: false, cheapPath: false, cheapSet: new Set(), showIds}).html;
|
||||||
|
|
||||||
@@ -154,9 +156,9 @@ describe('showIds — die ID vor dem Titel, geschrieben wie im Text', () => {
|
|||||||
expect(html).not.toContain('nid');
|
expect(html).not.toContain('nid');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stellt die ID mit Doppelpunkt und Leerzeichen davor', () => {
|
it('stellt die ID in eine eigene Zeile über den Titel', () => {
|
||||||
expect(ids('#auth: Backend', true))
|
expect(ids('#auth: Backend', true))
|
||||||
.toContain('<span class="nid" aria-hidden="true">#auth:</span> Backend');
|
.toContain('<span class="nid" aria-hidden="true">#auth</span>\nBackend');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('lässt Knoten ohne ID unangetastet', () => {
|
it('lässt Knoten ohne ID unangetastet', () => {
|
||||||
@@ -176,7 +178,7 @@ describe('showIds — die ID vor dem Titel, geschrieben wie im Text', () => {
|
|||||||
genau das: `#a<b` ergibt die ID `a`, der Rest bleibt (escaped) im Label. */
|
genau das: `#a<b` ergibt die ID `a`, der Rest bleibt (escaped) im Label. */
|
||||||
it('endet an einem Zeichen außerhalb der ID-Zeichenmenge', () => {
|
it('endet an einem Zeichen außerhalb der ID-Zeichenmenge', () => {
|
||||||
const html = ids('#a<b: X', true);
|
const html = ids('#a<b: X', true);
|
||||||
expect(html).toContain('<span class="nid" aria-hidden="true">#a:</span>');
|
expect(html).toContain('<span class="nid" aria-hidden="true">#a</span>');
|
||||||
expect(html).toContain('<b: X');
|
expect(html).toContain('<b: X');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// Umbruch langer Labels (SPEC §9/D64): höchstens ~40 Zeichen je Zeile und
|
||||||
|
// gleichmäßig verteilt — nicht gierig (volle Zeile plus einsames Wort).
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { wrapLabel, renderTreeHtml } from '../src/render.js';
|
||||||
|
import { parse } from '../src/parser.js';
|
||||||
|
|
||||||
|
const t = key => key;
|
||||||
|
|
||||||
|
describe('wrapLabel: balanciert statt gierig', () => {
|
||||||
|
it('bis 40 Zeichen bleibt es eine Zeile, unverändert', () => {
|
||||||
|
const s = 'Ein gewöhnlicher Knotentitel (39 Zeich.)';
|
||||||
|
expect(s.length).toBe(40);
|
||||||
|
expect(wrapLabel(s)).toEqual([s]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('44 Zeichen werden zwei etwa gleiche Zeilen — nicht 40 plus Rest', () => {
|
||||||
|
const s = 'Update the agents version of the spec please';
|
||||||
|
expect(s.length).toBe(44);
|
||||||
|
const lines = wrapLabel(s);
|
||||||
|
expect(lines.length).toBe(2);
|
||||||
|
for(const l of lines){
|
||||||
|
expect(l.length).toBeGreaterThanOrEqual(15);
|
||||||
|
expect(l.length).toBeLessThanOrEqual(29);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verliert kein Zeichen: die Zeilen ergeben zusammengefügt das Label', () => {
|
||||||
|
const s = 'Offering the status codes, the sizes and the known tags while typing';
|
||||||
|
expect(wrapLabel(s).join(' ')).toBe(s);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keine Zeile über 40 Zeichen, solange die Wörter es hergeben', () => {
|
||||||
|
const s = 'A dependency needs its target id and nobody remembers a hundred and eighty of them by heart';
|
||||||
|
for(const l of wrapLabel(s)) expect(l.length).toBeLessThanOrEqual(40);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rund 100 Zeichen ergeben drei gleichmäßige Zeilen', () => {
|
||||||
|
const s = 'The list at the caret offers the document ids filtered by what is typed with the node title as ctx';
|
||||||
|
expect(s.length).toBe(98);
|
||||||
|
const lines = wrapLabel(s);
|
||||||
|
expect(lines.length).toBe(3);
|
||||||
|
for(const l of lines){
|
||||||
|
expect(l.length).toBeGreaterThanOrEqual(25);
|
||||||
|
expect(l.length).toBeLessThanOrEqual(40);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein einzelnes Wort über der Grenze bleibt eine Zeile (CSS fängt es ab)', () => {
|
||||||
|
const w = 'x'.repeat(55);
|
||||||
|
expect(wrapLabel(w)).toEqual([w]);
|
||||||
|
expect(wrapLabel('kurz ' + w)).toEqual(['kurz', w]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bricht nie mitten im Wort', () => {
|
||||||
|
const s = 'einige ziemlich lange zusammengesetzte Bandwurmwortgebilde nebeneinander aufgereiht';
|
||||||
|
const words = s.split(' ');
|
||||||
|
for(const l of wrapLabel(s)){
|
||||||
|
for(const w of l.split(' ')) expect(words).toContain(w);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Renderer: die Umbrüche stehen als \\n im Knotentext', () => {
|
||||||
|
it('ein langer Titel bekommt sein \\n, ein kurzer nicht', () => {
|
||||||
|
const { roots } = parse(
|
||||||
|
'- Kurzer Titel\n' +
|
||||||
|
'- Ein wirklich sehr langer Titel, der die vierzig Zeichen deutlich reisst');
|
||||||
|
const { html } = renderTreeHtml(roots, {t, showDiscarded: false, cheapPath: false,
|
||||||
|
cheapSet: new Set()});
|
||||||
|
expect(html).toContain('Kurzer Titel');
|
||||||
|
expect(html).not.toContain('Kurzer\nTitel');
|
||||||
|
expect(html).toMatch(/Ein wirklich sehr langer Titel,[^<]*\n[^<]*reisst/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user