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 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-10-04 22:28:42 +02:00
co-authored by Claude Opus 5.5
parent 4611974494
commit 1de38b9b7a
2 changed files with 60 additions and 13 deletions
+21 -9
View File
@@ -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);
for (let s = 0; s < stuecke.length; s += 1) {
const stueck = stuecke[s]!;
if (stueck.fest) continue;
const gefunden = regex.exec(stueck.text);
if (!gefunden) continue;
const erklaerung = eintrag.steht_fuer
? `${eintrag.steht_fuer} – ${eintrag.erklaerung}`
: eintrag.erklaerung;
const verweis = `<a class="glossarbegriff" href="/glossar#${glossarAnker(eintrag.begriff)}" title="${maskiere(erklaerung)}">${gefunden[2]}</a>`;
text =
text.slice(0, gefunden.index) +
gefunden[1] +
`<a class="glossarbegriff" href="/glossar#${glossarAnker(eintrag.begriff)}" title="${maskiere(erklaerung)}">${gefunden[2]}</a>` +
text.slice(gefunden.index + gefunden[0].length);
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('');
+35
View File
@@ -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('<p>Laufwerke per Gruppenrichtlinie zuordnen.</p>', eintraege);
expect(html.match(/<a /g)).toHaveLength(1);
expect(html).toContain('title="Verknüpft an eine OU – was zuletzt kommt, gewinnt."');
expect(html).toContain('>Gruppenrichtlinie</a> 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('<p>Die Gruppenrichtlinie hängt an der OU.</p>', eintraege);
expect(html.match(/<a /g)).toHaveLength(2);
expect(html).toContain('>OU</a>.</p>');
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="[^"]*<a /);
expect((html.match(/<a /g) ?? []).length, handout.id).toBe((html.match(/<\/a>/g) ?? []).length);
}
});
it('lässt HTML unverändert, wenn das Glossar leer ist', () => {
expect(verlinkeGlossar('<p>DNS</p>', [])).toBe('<p>DNS</p>');
});