From 1de38b9b7ac3c50fcdbe6b31213fd61667812bf3 Mon Sep 17 00:00:00 2001
From: Florian Wilde
Date: Sun, 4 Oct 2026 22:28:42 +0200
Subject: [PATCH] Glossarverweise nicht in title-Attribute setzen
Nach dem Setzen eines Verweises liefen die folgenden Suchmuster ueber
denselben Text, einschliesslich des frisch eingefuegten title-Attributs.
Enthielt die Erklaerung einen weiteren Glossarbegriff, entstand mitten im
Attribut ein zweiter Verweis und das HTML brach auf - sichtbar im Handout
als `gewinnt.">Gruppenrichtlinie`.
Gesetzte Verweise werden jetzt als feste Stuecke herausgeloest; die
weiteren Muster durchsuchen nur den freien Text. Ein neuer Test prueft
alle Handouts gegen das echte Glossar auf wohlgeformte Verweise.
Co-Authored-By: Claude Opus 5.5
---
src/lib/inhalte/glossarverweise.ts | 38 ++++++++++++++++++++----------
src/lib/inhalte/suche.test.ts | 35 +++++++++++++++++++++++++++
2 files changed, 60 insertions(+), 13 deletions(-)
diff --git a/src/lib/inhalte/glossarverweise.ts b/src/lib/inhalte/glossarverweise.ts
index 942ff7b..fc95593 100644
--- a/src/lib/inhalte/glossarverweise.ts
+++ b/src/lib/inhalte/glossarverweise.ts
@@ -87,26 +87,38 @@ export function verlinkeGlossar(html: string, glossar: GlossarEintrag[]): string
if (teil.trim() === '') continue;
if (offen.some((element) => gemiedeneElemente.has(element))) continue;
- let text = teil;
+ // Ein gesetzter Verweis wird als festes Stück herausgelöst. Die folgenden
+ // Muster durchsuchen nur noch den freien Text – sonst fände etwa „OU" sein
+ // Gegenstück in der Erklärung von „Gruppenrichtlinie" im title-Attribut,
+ // und mitten im Attribut entstünde ein zweiter Verweis.
+ const stuecke: { text: string; fest: boolean }[] = [{ text: teil, fest: false }];
for (const { muster: regex, eintrag } of muster) {
if (bereitsVerlinkt.has(eintrag.begriff)) continue;
- const gefunden = regex.exec(text);
- if (!gefunden) continue;
+ for (let s = 0; s < stuecke.length; s += 1) {
+ const stueck = stuecke[s]!;
+ if (stueck.fest) continue;
- const erklaerung = eintrag.steht_fuer
- ? `${eintrag.steht_fuer} – ${eintrag.erklaerung}`
- : eintrag.erklaerung;
+ const gefunden = regex.exec(stueck.text);
+ if (!gefunden) continue;
- text =
- text.slice(0, gefunden.index) +
- gefunden[1] +
- `${gefunden[2]}` +
- text.slice(gefunden.index + gefunden[0].length);
+ const erklaerung = eintrag.steht_fuer
+ ? `${eintrag.steht_fuer} – ${eintrag.erklaerung}`
+ : eintrag.erklaerung;
+ const verweis = `${gefunden[2]}`;
- bereitsVerlinkt.add(eintrag.begriff);
+ stuecke.splice(
+ s,
+ 1,
+ { text: stueck.text.slice(0, gefunden.index) + gefunden[1], fest: false },
+ { text: verweis, fest: true },
+ { text: stueck.text.slice(gefunden.index + gefunden[0].length), fest: false },
+ );
+ bereitsVerlinkt.add(eintrag.begriff);
+ break;
+ }
}
- teile[i] = text;
+ teile[i] = stuecke.map((stueck) => stueck.text).join('');
}
return teile.join('');
diff --git a/src/lib/inhalte/suche.test.ts b/src/lib/inhalte/suche.test.ts
index a9c309e..f1b20a5 100644
--- a/src/lib/inhalte/suche.test.ts
+++ b/src/lib/inhalte/suche.test.ts
@@ -122,6 +122,41 @@ describe('Glossarbegriffe verlinken', () => {
expect(html).not.toContain('title="Ein "Anführungszeichen"');
});
+ it('verlinkt nicht in die Erklärung eines schon gesetzten Verweises hinein', () => {
+ // Die Erklärung von „Gruppenrichtlinie" enthält „OU". Ein zweiter Verweis
+ // darf nicht im title-Attribut des ersten landen – sonst bricht das HTML auf.
+ const eintraege = [
+ { begriff: 'Gruppenrichtlinie', formen: [], erklaerung: 'Verknüpft an eine OU – was zuletzt kommt, gewinnt.', siehe_auch: [] },
+ { begriff: 'OU', formen: [], erklaerung: 'Ein Ordner im Verzeichnis.', siehe_auch: [] },
+ ];
+ const html = verlinkeGlossar('Laufwerke per Gruppenrichtlinie zuordnen.
', eintraege);
+ expect(html.match(/Gruppenrichtlinie zuordnen.');
+ });
+
+ it('verlinkt beide Begriffe, wenn beide im Text stehen', () => {
+ const eintraege = [
+ { begriff: 'Gruppenrichtlinie', formen: [], erklaerung: 'Verknüpft an eine OU.', siehe_auch: [] },
+ { begriff: 'OU', formen: [], erklaerung: 'Ein Ordner im Verzeichnis.', siehe_auch: [] },
+ ];
+ const html = verlinkeGlossar('Die Gruppenrichtlinie hängt an der OU.
', eintraege);
+ expect(html.match(/OU.
');
+ expect(html).toContain('title="Verknüpft an eine OU."');
+ });
+
+ it('erzeugt für alle Handouts wohlgeformte Verweise', () => {
+ // Gegen das echte Glossar und die echten Handouts: In keinem title-Attribut
+ // darf ein weiterer Verweis stecken.
+ for (const handout of alle.handouts) {
+ if (!handout.wesentlich) continue;
+ const html = verlinkeGlossar(handout.wesentlich.html, glossar);
+ expect(html, handout.id).not.toMatch(/title="[^"]*/g) ?? []).length);
+ }
+ });
+
it('lässt HTML unverändert, wenn das Glossar leer ist', () => {
expect(verlinkeGlossar('DNS
', [])).toBe('DNS
');
});