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

'); });