feat(diagram): #-Umschalter blendet die Knoten-IDs vor dem Titel ein

Die ID ist die Adresse eines Knotens — Ziel der Abhängigkeiten, Schlüssel der
Beschreibungsblöcke —, stand im Diagramm aber nur im Tooltip (D36). Jetzt
umschaltbar und geschrieben wie im Text: `#some.id: Titel`.

- Als RENDERER-Option, nicht per CSS versteckt: `diagramToSvg()` zieht den
  Knotentext aus `textContent`, ein `display:none` hätte die IDs trotzdem in
  jeden Export geschrieben. Nachgemessen im SVG: `>#not.line: Line format<`
  mit Umschalter, `>Line format<` ohne.
- Zurückgenommen dargestellt (Mono wie im Textfeld, --muted), `aria-hidden` —
  der Screenreader bekommt die ID über a11yId (D36). Zustand persistiert.

Das zehnte Bedienelement hat die Kopfzeile gekippt, zweimal dieselbe Rechnung
wie D17-Nachtrag 5:
- Schreibtisch: 738 nötig, 728 da -> 82 statt 49 px. Nicht die Knöpfe (566 px
  für elf Elemente), sondern zehn Lücken à 14 px = 140 px. `gap:10` gibt
  40 px zurück; danach 698 von 728 und wieder 49 px mit UND ohne Knopf.
- Telefon (375 px): alle Ziele schon auf 29 px, Lücken auf 6 — ein zehntes
  passt arithmetisch nicht (386 gegen 359). D50s Umbruch-Schwelle wandert von
  360 auf 440 px. Greift nur mit dem „Was ist neu?"-Knopf: mit allen zehn
  78 px/zwei Reihen, ohne ihn 49 px/eine Reihe.

SPEC §9 + D56; Plan: #ed.showids [x] (160 Knoten, 0 Warnungen).
293 Tests grün (288+5); i18n `idsTooltip` in 9 Sprachen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 11:55:04 +02:00
co-authored by Claude Opus 5
parent 285d86a2e1
commit f0fe877d45
8 changed files with 183 additions and 8 deletions
+52
View File
@@ -3922,3 +3922,55 @@ erste `#`-Token, zeichengenaue Erhaltung von Einrückung, Zeichen, Statusbox
und Faltmarke, und dass das Ergebnis denselben Baum ergibt wie die von Hand
ausgeschriebene Fassung). Das Zusammenspiel mit dem Textfeld bleibt
Browser-Sache — die Lehre aus D54-Nachtrag 3.
## D56 — `#`-Umschalter: die Knoten-ID vor dem Titel, geschrieben wie im Text
Die ID ist die **Adresse** eines Knotens — Ziel der Abhängigkeiten (§1), Schlüssel
der Beschreibungsblöcke, und seit D55 auch das, was man beim Tippen abkürzt. Im
Diagramm stand sie bisher nur im Tooltip (D36: „eine eigene Darstellung hat sie
(noch) nicht"). Wer Text und Bild nebeneinander liest, musste jeden Knoten
antippen, um zu wissen, welcher er ist.
Ein Umschalter im Diagramm-Kopf blendet sie ein, **geschrieben wie im Text**:
`#some.id: Titel`, mit Doppelpunkt und Leerzeichen. Dieselbe Schreibweise auf
beiden Seiten ist der ganze Zweck — eine eigene Darstellung (Badge in der Ecke,
Klammern, Kapitälchen) wäre eine zweite Konvention für dieselbe Sache.
**Als Renderer-Option, nicht per CSS versteckt.** Der naheliegende Weg wäre eine
Klasse an `#out` und `display:none` gewesen. Dann aber stünde die ID trotzdem im
`textContent` — und genau daraus zieht `diagramToSvg()` den Knotentext. Der
Export hätte die IDs also **immer** enthalten, egal was auf dem Schirm steht.
Als Option in `renderTreeHtml(roots, {…, showIds})` folgt er von selbst;
nachgemessen im ausgegebenen SVG: `>#not.line: Line format</text>` mit
Umschalter, `>Line format</text>` ohne. Das entspricht der Hausregel, UI-State
als **Parameter** zu übergeben (frontend/CLAUDE.md), und der Linie von D38/D44:
Export und Druck zeigen, was sichtbar ist.
**Zurückgenommen dargestellt:** Mono-Schrift wie im Textfeld, 0,86 em, in
`--muted` (am dunklen Wurzelknoten in Weiß mit 75 %). Die ID ist die Adresse,
nicht der Name — der Titel soll die Zeile weiter anführen. `aria-hidden`, weil
der Screenreader sie über `a11yId` ohnehin bekommt (D36) und sie sonst doppelt
vorläse. Der Zustand gehört zur Ansicht und wird wie Modus, Zoom und Aufteilung
global gemerkt (D22).
**Der Knopf trägt das Zeichen selbst.** Ein gezeichnetes Icon sagte hier
weniger als `#` — dasselbe Argument wie beim Fenster-Wähler in D31, nur
umgekehrt: Wo es ein etabliertes Schriftzeichen gibt, ist es das beste Symbol.
**Das zehnte Bedienelement hat die Kopfzeile gekippt — zweimal dieselbe
Rechnung wie D17-Nachtrag 5.** Nachgemessen statt geschätzt:
- **Schreibtisch:** 738 px nötig, 728 verfügbar — 10 px zu wenig, die Zeile ging
auf 82 statt 49 px. Nicht die Knöpfe waren das Problem (566 px für elf
Elemente), sondern der Weißraum: zehn Lücken à 14 px sind 140 px. `gap:10`
statt `14` gibt 40 px zurück, also fast genau die Breite des neuen Knopfes;
danach 698 von 728 und wieder 49 px — mit **und** ohne den Knopf.
- **Telefon (375 px):** Dort sind alle Ziele schon auf dem 29-px-Fingerminimum
und die Lücken auf 6 px; ein zehntes passt arithmetisch nicht (386 gegen
359 px). Die Umbruch-Schwelle aus D50 wandert deshalb von 360 auf 440 px —
dieselbe Rechnung, ein Element mehr. Sie greift auch nur dann wirklich: Der
„Was ist neu?"-Knopf (44 px) erscheint nur bei Neuigkeiten, ohne ihn bleiben
336 px und damit eine Reihe. Gemessen: mit allen zehn 78 px und zwei Reihen,
alles innerhalb; ohne den Neuigkeiten-Knopf 49 px und eine Reihe.
Ein `overflow` an der Kopfzeile bleibt weiterhin ausgeschlossen — es klippt die
beiden Aufklapp-Menüs (D50).
+11
View File
@@ -703,6 +703,17 @@ verknüpft (siehe D25):
- Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im
Druck.
### Knoten-IDs im Diagramm einblenden (`#`)
Ein Umschalter im Diagramm-Kopf stellt die Knoten-ID **vor den Titel**, in
derselben Schreibweise wie im Text (§1/D36): `#some.id: Titel`. Dargestellt in
der Mono-Schrift des Textfelds und zurückgenommen gefärbt — die ID ist die
Adresse, der Titel führt die Zeile weiter an. Der Zustand wird gemerkt.
**Grafikexport und Druck folgen dem Umschalter** (wie beim „verworfene
einblenden"-Filter und der Faltung): Ist er an, stehen die IDs auch im
ausgegebenen Bild. Für Screenreader ändert sich nichts — sie bekommen die ID
ohnehin über das `aria-label`. Siehe D56.
### Kurzschreibweise der Knoten-ID beim Tippen (`#.name`)
Eine **Eingabehilfe**, keine Notation: Wer `#.kc` unter einem Knoten mit der ID
`#prod-stage` schreibt, findet beim **Verlassen der Zeile** `#prod-stage.kc` im
+6
View File
@@ -68,6 +68,7 @@
- [^] #ed.jump: Jump between diagram and text (S)
- [^] #ed.lineno: Line numbers in the text editor (XS) %% the warnings name them
- [x] #ed.shortid: Type #.kc under #prod-stage, get #prod-stage.kc (XS) %% an input aid, not notation
- [x] #ed.showids: A # button shows the ids before the titles (XS) %% written as in the text, with a colon
- [^] #ed.lineno.tip: The warning is the tooltip of its number (XS) %% no hunting in the list
- [^] #ed.fold: Collapsible subtrees (M)
- [^] #ed.fold.marks: Initial state from the text: > collapses, < re-opens (S)
@@ -498,6 +499,11 @@
typing, not while parsing: the file keeps the full id, so it stays greppable
and survives re-ordering, which is what the :#... references rely on.
#ed.showids
The id is the address of a node, but the diagram only carried it in the
tooltip. The switch writes it before the title the same way the text does,
so both can be read side by side; the graphic export follows the switch.
#ed.fold
A large plan is unreadable unless parts of it can be put away. Folding is
view only — warnings from folded subtrees are still reported, because they
+6
View File
@@ -192,6 +192,12 @@
<button type="button" class="copybtn togglebtn" id="foldSmallBtn" aria-pressed="false" data-i18n-title="foldSmallTooltip" data-i18n-aria="foldSmallTooltip" title="Knoten der Größe M und kleiner zuklappen erneut drücken klappt alle wieder auf" aria-label="Knoten der Größe M und kleiner zuklappen erneut drücken klappt alle wieder auf">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21V3M8 8l4 4 4-4M8 16l4-4 4 4"/></svg>
</button>
<!-- Knoten-IDs vor dem Titel einblenden (SPEC §9, D56) — geschrieben wie
im Text: `#some.id: Titel`. Das Zeichen selbst ist das Symbol; ein
gezeichnetes Icon sagte hier weniger als das `#`. -->
<button type="button" class="copybtn togglebtn idsbtn" id="idsBtn" aria-pressed="false" data-i18n-title="idsTooltip" data-i18n-aria="idsTooltip" title="Knoten-IDs vor dem Titel einblenden" aria-label="Knoten-IDs vor dem Titel einblenden">
<span aria-hidden="true">#</span>
</button>
<div class="seg" role="radiogroup" aria-label="Darstellung">
<label data-i18n-title="modeHorizontal" title="Horizontal Organigramm, Diagramm über dem Editor">
<input type="radio" name="layout" value="horizontal" checked data-i18n-aria="modeHorizontal" aria-label="Horizontal Organigramm">
+28 -1
View File
@@ -53,6 +53,7 @@ const lineNoInner = lineNoBox.firstElementChild;
Pfad) lebt headless in model.js, das HTML-Erzeugen in render.js. Hier bleibt
nur der UI-State des Günstigster-Pfad-Toggles (persistiert). */
let cheapPathOn = true;
let showIds = false; /* Knoten-IDs im Diagramm einblenden (D56) */
/* Ansicht bei einem Pad-Dokument (D31): beide | nur Pad | nur Spiegel. Hier oben
deklariert, weil saveUI() sie liest und schon aus applySplit() heraus laufen
kann weiter unten stünde sie dann noch in der temporalen Todeszone. */
@@ -148,7 +149,7 @@ function render(){
alles offen" derselbe Zustand der Knopf säße dann gedrückt da, ohne
dass etwas zugeklappt ist. */
foldSmallBtn.setAttribute('aria-pressed', foldSmallExact && foldSmallAny ? 'true' : 'false');
const r = renderTreeHtml(roots, {t, showDiscarded, cheapPath: cheapPathOn, cheapSet,
const r = renderTreeHtml(roots, {t, showDiscarded, cheapPath: cheapPathOn, cheapSet, showIds,
freshSet, collapsedSet,
effStatus: effectiveStatus(roots)});
out.innerHTML = r.html;
@@ -1662,6 +1663,18 @@ cheapBtn.addEventListener('click', () => {
saveUI();
});
/* Knoten-IDs im Diagramm an/aus (D56). Wie die Nachbarn ein Umschalter mit
`aria-pressed`; der Zustand gehört zur Ansicht und wird wie Modus, Zoom und
Aufteilung global gemerkt (D22). Neu gerendert statt per CSS versteckt
dann folgt der Grafikexport von selbst. */
const idsBtn = document.getElementById('idsBtn');
idsBtn.addEventListener('click', () => {
showIds = !showIds;
idsBtn.setAttribute('aria-pressed', showIds ? 'true' : 'false');
render();
saveUI();
});
/* ---------- Von Station zu Station (SPEC §9, D47) ----------
Kein Umschalter, sondern eine Bewegung: Jeder Druck holt die nächste noch
offene Station des günstigsten Pfads in die Mitte beim ersten Druck die
@@ -1802,6 +1815,7 @@ const I18N = {
snapTooltip:"Frühere Stände dieses Dokuments", snapMenuAria:"Frühere Stände",
snapNone:"Noch keine früheren Stände gesichert wird auf Knopfdruck und alle 10 Minuten, sobald sich etwas geändert hat.",
snapAddTooltip:"Aktuellen Stand jetzt sichern",
idsTooltip:"Knoten-IDs vor dem Titel einblenden",
snapLines:"{n} Zeilen",
docNew:"Neues Dokument", docRename:"Umbenennen", docDelete:"Löschen",
docNewName:"Unbenannt",
@@ -1891,6 +1905,7 @@ const I18N = {
snapTooltip:"Earlier states of this document", snapMenuAria:"Earlier states",
snapNone:"No earlier states yet one is kept on demand and every 10 minutes, once something has changed.",
snapAddTooltip:"Save the current state now",
idsTooltip:"Show node ids before the title",
snapLines:"{n} lines",
docNew:"New document", docRename:"Rename", docDelete:"Delete",
docNewName:"Untitled",
@@ -1980,6 +1995,7 @@ const I18N = {
snapTooltip:"Estados anteriores de este documento", snapMenuAria:"Estados anteriores",
snapNone:"Aún no hay estados anteriores: se guarda uno al pulsar el botón y cada 10 minutos, cuando algo ha cambiado.",
snapAddTooltip:"Guardar ahora el estado actual",
idsTooltip:"Mostrar los id de nodo antes del título",
snapLines:"{n} líneas",
docNew:"Nuevo documento", docRename:"Renombrar", docDelete:"Eliminar",
docNewName:"Sin título",
@@ -2069,6 +2085,7 @@ const I18N = {
snapTooltip:"États antérieurs de ce document", snapMenuAria:"États antérieurs",
snapNone:"Pas encore d’état antérieur : un état est conservé sur demande et toutes les 10 minutes, dès que quelque chose a changé.",
snapAddTooltip:"Enregistrer l’état actuel maintenant",
idsTooltip:"Afficher les identifiants de nœud avant le titre",
snapLines:"{n} lignes",
docNew:"Nouveau document", docRename:"Renommer", docDelete:"Supprimer",
docNewName:"Sans titre",
@@ -2158,6 +2175,7 @@ const I18N = {
snapTooltip:"Wcześniejsze stany tego dokumentu", snapMenuAria:"Wcześniejsze stany",
snapNone:"Brak wcześniejszych stanów zapisywany jest na żądanie i co 10 minut, gdy coś się zmieni.",
snapAddTooltip:"Zapisz bieżący stan teraz",
idsTooltip:"Pokaż identyfikatory węzłów przed tytułem",
snapLines:"wierszy: {n}",
docNew:"Nowy dokument", docRename:"Zmień nazwę", docDelete:"Usuń",
docNewName:"Bez nazwy",
@@ -2247,6 +2265,7 @@ const I18N = {
snapTooltip:"Прежние состояния этого документа", snapMenuAria:"Прежние состояния",
snapNone:"Прежних состояний пока нет — они сохраняются по нажатию кнопки и каждые 10 минут, если что-то изменилось.",
snapAddTooltip:"Сохранить текущее состояние сейчас",
idsTooltip:"Показывать идентификаторы узлов перед заголовком",
snapLines:"строк: {n}",
docNew:"Новый документ", docRename:"Переименовать", docDelete:"Удалить",
docNewName:"Без названия",
@@ -2336,6 +2355,7 @@ const I18N = {
snapTooltip:"इस दस्तावेज़ की पिछली स्थितियाँ", snapMenuAria:"पिछली स्थितियाँ",
snapNone:"अभी कोई पिछली स्थिति नहीं — बटन दबाने पर और कुछ बदलने पर हर 10 मिनट में एक सहेजी जाती है।",
snapAddTooltip:"मौजूदा स्थिति अभी सहेजें",
idsTooltip:"शीर्षक से पहले नोड आईडी दिखाएँ",
snapLines:"{n} पंक्तियाँ",
docNew:"नया दस्तावेज़", docRename:"नाम बदलें", docDelete:"हटाएँ",
docNewName:"बिना शीर्षक",
@@ -2425,6 +2445,7 @@ const I18N = {
snapTooltip:"此文档的早期状态", snapMenuAria:"早期状态",
snapNone:"暂无早期状态——可随时手动保存;内容有变动时,也会每 10 分钟保存一次。",
snapAddTooltip:"立即保存当前状态",
idsTooltip:"在标题前显示节点 ID",
snapLines:"{n} 行",
docNew:"新建文档", docRename:"重命名", docDelete:"删除",
docNewName:"未命名",
@@ -2514,6 +2535,7 @@ const I18N = {
snapTooltip:"このドキュメントの以前の状態", snapMenuAria:"以前の状態",
snapNone:"以前の状態はまだありません — ボタンを押したとき、および変更があれば 10 分ごとに保存されます。",
snapAddTooltip:"現在の状態を今すぐ保存",
idsTooltip:"タイトルの前にノード ID を表示",
snapLines:"{n} 行",
docNew:"新規ドキュメント", docRename:"名前を変更", docDelete:"削除",
docNewName:"無題",
@@ -2808,6 +2830,7 @@ function saveUI(){
mode: modeEl ? modeEl.value : 'horizontal',
discarded: discardedShown(),
cheapPath: cheapPathOn,
showIds,
split: splitState,
col: app.style.getPropertyValue('--col') || null,
drow: app.style.getPropertyValue('--drow') || null,
@@ -2846,6 +2869,10 @@ function restoreState(){
cheapPathOn = ui.cheapPath;
cheapBtn.setAttribute('aria-pressed', cheapPathOn ? 'true' : 'false');
}
if(typeof ui.showIds === 'boolean'){
showIds = ui.showIds;
idsBtn.setAttribute('aria-pressed', showIds ? 'true' : 'false');
}
if(typeof ui.zoom === 'number') zoom = ui.zoom;
if(ui.split) splitState = ui.split;
if(ui.fullscreen){
+10 -1
View File
@@ -90,7 +90,7 @@ function nodeAria(n, opts, fold){
}
function nodeHtml(n, extra, opts, fold){
const { t, cheapPath } = opts;
const { t, cheapPath, showIds } = opts;
const need = needsBreakdown(n);
/* Die Knotenfarbe zeigt den EFFEKTIVEN Status (SPEC §9/D39); bei Diskrepanz
trägt die Marke unten links die eigene Statusbox in den eigenen Farben. */
@@ -163,7 +163,16 @@ function nodeHtml(n, extra, opts, fold){
const ownChip = effKey
? `<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
(SPEC §1/D36), damit man beides nebeneinander lesen kann. Umschaltbar im
Diagramm-Kopf; als Renderer-Option und nicht per CSS versteckt, damit der
Grafikexport (er liest den Knotentext) von selbst folgt. `aria-hidden`:
Der Screenreader bekommt die ID schon über `a11yId` (D56). */
const idHtml = showIds && n.id
? `<span class="nid" aria-hidden="true">#${esc(n.id)}:</span> `
: '';
const inner = foldHtml +
idHtml +
esc(n.label) +
/* -Marke (D40): macht die sonst unsichtbare Beschreibung
auffindbar (Lehre aus D25) spiegelt das "-Zeichen der
+31 -6
View File
@@ -305,7 +305,12 @@
.panel-head{
flex:0 0 auto;position:relative;
padding:10px 16px;
display:flex;justify-content:space-between;align-items:center;gap:14px;
/* gap:10 statt 14 dieselbe Rechnung wie in D17-Nachtrag 5, nur für den
Schreibtisch: Beim zehnten Bedienelement (D56) fehlten 10 px, und zehn
Lücken à 14 px machen 140 px Weißraum für 566 px Knöpfe. Verkleinert
wird der Zwischenraum, nicht das Ziel; 4 px weniger je Lücke geben die
Breite des neuen Knopfes zurück. */
display:flex;justify-content:space-between;align-items:center;gap:10px;
font-family:'IBM Plex Mono',monospace;
font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;
color:var(--muted);
@@ -635,6 +640,21 @@
nachlesbar. Spiegelt das "-Zeichen der Notation; gedeckt, kein Klickziel. */
.node .desc-mark{color:var(--muted);margin-left:4px;font-weight:600}
.root-node .desc-mark{color:rgba(255,255,255,.75)}
/* Knoten-ID vor dem Titel (D56). Zurückgenommen und in der Mono-Schrift des
Textfelds: Sie ist die Adresse, nicht der Name der Titel soll die Zeile
weiter anführen. Kein `margin-right`; das trennende Leerzeichen steht im
Markup, damit der Grafikexport (er liest den Knotentext) es mitbekommt. */
.node .nid{
font-family:'IBM Plex Mono',monospace;font-size:.86em;font-weight:500;
color:var(--muted);
}
.root-node .nid{color:rgba(255,255,255,.75)}
/* Der Knopf trägt das Zeichen selbst statt eines Icons in der Größe der
SVGs daneben, damit die Kopfzeile ihre Höhe behält. */
.copybtn.idsbtn span{
display:block;width:15px;height:15px;line-height:15px;text-align:center;
font-family:'IBM Plex Mono',monospace;font-size:14px;font-weight:600;
}
/* Sichtbarer Tastatur-Fokus (Knoten sind fokussierbar: Fokusreihenfolge =
Lese-/Dokumentreihenfolge; Screenreader liest den aria-label). */
.node:focus-visible{outline:2px solid var(--or);outline-offset:2px;box-shadow:0 2px 8px rgba(36,52,71,.22)}
@@ -1235,17 +1255,22 @@
ein sichtbarer Überlauf (D17-Nachtrag 5). */
body.mobile .panel-head{gap:6px;flex-wrap:nowrap;padding-inline:8px}
body.mobile .panel-head>*{flex:0 0 auto}
/* Unterhalb von rund 360 px ist es reine Arithmetik: neun Bedienelemente à
29 px plus Lücken brauchen mehr, als dort überhaupt da ist (bei 320 px
fehlen 41 px). Kleiner als 29 px darf ein Fingerziel nicht werden
(D17-Nachtrag 5) dort darf die Zeile deshalb umbrechen.
/* Unterhalb von rund 440 px ist es reine Arithmetik: **zehn** Bedienelemente
à 29 px (D56 hat den ID-Umschalter hinzugefügt) plus Lücken brauchen
386 px, bei 375 px stehen 359 zur Verfügung. Kleiner als 29 px darf ein
Fingerziel nicht werden (D17-Nachtrag 5) dort darf die Zeile deshalb
umbrechen. Sie tut es aber nur, wenn wirklich alle zehn da sind: Der
Was ist neu?"-Knopf (44 px) erscheint nur bei Neuigkeiten, ohne ihn
bleiben 336 px und damit eine Reihe. Die Schwelle stand vorher bei 360 px
und ist mit dem zehnten Element mitgewandert dieselbe Rechnung, ein
Element mehr.
**Kein `overflow` an dieser Stelle** (D50): Die Kopfzeile trägt die beiden
Aufklapp-Menüs (Dokumente, Download) als absolut positionierte Kinder. Ein
Scroll-Container klippt sie und weil `overflow-x` das `visible` der
anderen Achse auf `auto` hebt, klippt er auch nach unten, wo die Menüs
stehen. Sie waren damit auf **jeder** Mobilbreite unerreichbar, nicht nur
auf den schmalen. */
@media (max-width:360px){
@media (max-width:440px){
body.mobile .panel-head{flex-wrap:wrap}
}
/* Inhalte rund 25 % kleiner, damit mehr Plan auf die Fläche passt
+39
View File
@@ -141,3 +141,42 @@ describe('renderTreeHtml — data-line je Knoten (D25)', () => {
expect(html).not.toMatch(/ghost-node[^>]*data-line/);
});
});
/* Knoten-IDs im Diagramm (D56). Als Renderer-Option gebaut und nicht per CSS
versteckt nur so folgt der Grafikexport, der den Knotentext ausliest. */
describe('showIds — die ID vor dem Titel, geschrieben wie im Text', () => {
const ids = (text, showIds) => renderTreeHtml(parse(text).roots,
{t, showDiscarded: false, cheapPath: false, cheapSet: new Set(), showIds}).html;
it('bleibt ohne Umschalter aus dem Knotentext heraus', () => {
const html = ids('#auth: Backend', false);
expect(html).toContain('>Backend<');
expect(html).not.toContain('nid');
});
it('stellt die ID mit Doppelpunkt und Leerzeichen davor', () => {
expect(ids('#auth: Backend', true))
.toContain('<span class="nid" aria-hidden="true">#auth:</span> Backend');
});
it('lässt Knoten ohne ID unangetastet', () => {
const html = ids('- [x] Ohne ID', true);
expect(html).toContain('>Ohne ID<');
expect(html).not.toContain('nid');
});
/* Die ID steht ohnehin im aria-label (D36) sichtbar doppelt vorgelesen
würde sie sonst. */
it('versteckt die sichtbare ID vor dem Screenreader', () => {
expect(ids('#auth: Backend', true)).toContain('<span class="nid" aria-hidden="true">');
});
/* Markup kann in einer ID gar nicht vorkommen `<`, `&` und `"` gehören
nicht zur Zeichenmenge (SPEC §1), die ID endet dort. Geprüft wird deshalb
genau das: `#a<b` ergibt die ID `a`, der Rest bleibt (escaped) im Label. */
it('endet an einem Zeichen außerhalb der ID-Zeichenmenge', () => {
const html = ids('#a<b: X', true);
expect(html).toContain('<span class="nid" aria-hidden="true">#a:</span>');
expect(html).toContain('&lt;b: X');
});
});