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:
co-authored by
Claude Opus 5.5
parent
4611974494
commit
1de38b9b7a
@@ -87,26 +87,38 @@ export function verlinkeGlossar(html: string, glossar: GlossarEintrag[]): string
|
|||||||
if (teil.trim() === '') continue;
|
if (teil.trim() === '') continue;
|
||||||
if (offen.some((element) => gemiedeneElemente.has(element))) 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) {
|
for (const { muster: regex, eintrag } of muster) {
|
||||||
if (bereitsVerlinkt.has(eintrag.begriff)) continue;
|
if (bereitsVerlinkt.has(eintrag.begriff)) continue;
|
||||||
|
|
||||||
const gefunden = regex.exec(text);
|
for (let s = 0; s < stuecke.length; s += 1) {
|
||||||
if (!gefunden) continue;
|
const stueck = stuecke[s]!;
|
||||||
|
if (stueck.fest) continue;
|
||||||
|
|
||||||
const erklaerung = eintrag.steht_fuer
|
const gefunden = regex.exec(stueck.text);
|
||||||
? `${eintrag.steht_fuer} – ${eintrag.erklaerung}`
|
if (!gefunden) continue;
|
||||||
: eintrag.erklaerung;
|
|
||||||
|
|
||||||
text =
|
const erklaerung = eintrag.steht_fuer
|
||||||
text.slice(0, gefunden.index) +
|
? `${eintrag.steht_fuer} – ${eintrag.erklaerung}`
|
||||||
gefunden[1] +
|
: eintrag.erklaerung;
|
||||||
`<a class="glossarbegriff" href="/glossar#${glossarAnker(eintrag.begriff)}" title="${maskiere(erklaerung)}">${gefunden[2]}</a>` +
|
const verweis = `<a class="glossarbegriff" href="/glossar#${glossarAnker(eintrag.begriff)}" title="${maskiere(erklaerung)}">${gefunden[2]}</a>`;
|
||||||
text.slice(gefunden.index + gefunden[0].length);
|
|
||||||
|
|
||||||
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('');
|
return teile.join('');
|
||||||
|
|||||||
@@ -122,6 +122,41 @@ describe('Glossarbegriffe verlinken', () => {
|
|||||||
expect(html).not.toContain('title="Ein "Anführungszeichen"');
|
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', () => {
|
it('lässt HTML unverändert, wenn das Glossar leer ist', () => {
|
||||||
expect(verlinkeGlossar('<p>DNS</p>', [])).toBe('<p>DNS</p>');
|
expect(verlinkeGlossar('<p>DNS</p>', [])).toBe('<p>DNS</p>');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user