import { describe, expect, it } from 'vitest';
import { gliedereText, textbloeckeAlsKnoten, type Textblock } from '@/ui/datenschutzText';
/**
* Der kleine Leser fuer docs/datenschutz.md - als Einheit, ohne Fenster.
*
* Die Faelle folgen den Zusagen im Kopf von src/ui/datenschutzText.ts: das
* Format der Datei, die drei Abweichungen, die der Leser wie CommonMark liest,
* und die, die er anders liest. Am echten Fenster mit der echten Datei - und
* die Wache, dass die Datei keine der anders gelesenen enthaelt - prueft
* tests/ui/datenschutzAnsicht.test.ts.
*/
const ERSATZ = 'Ersatztitel';
function absatz(text: string): Textblock {
return { art: 'absatz', text };
}
function abschnitt(text: string): Textblock {
return { art: 'abschnitt', text };
}
function aufzaehlung(...punkte: string[]): Textblock {
return { art: 'aufzaehlung', punkte };
}
describe('gliedereText: Titel', () => {
it('nimmt die Titelzeile als Titel und nicht als Block', () => {
const text = gliedereText('# Hinweise\n\nErster Absatz.\n', ERSATZ);
expect(text.titel).toBe('Hinweise');
expect(text.bloecke).toEqual([absatz('Erster Absatz.')]);
});
it('nimmt den Ersatztitel, wenn die Titelzeile fehlt', () => {
const text = gliedereText('## Abschnitt\n\nAbsatz.', ERSATZ);
expect(text.titel).toBe(ERSATZ);
expect(text.bloecke).toEqual([abschnitt('Abschnitt'), absatz('Absatz.')]);
});
it('verwirft einen zweiten Titel nicht, sondern fuehrt ihn als Abschnitt', () => {
const text = gliedereText('# Erster\n\n# Zweiter\n\nText.', ERSATZ);
expect(text.titel).toBe('Erster');
expect(text.bloecke).toEqual([abschnitt('Zweiter'), absatz('Text.')]);
});
it('liest ein Nummernzeichen ohne Leerzeichen nicht als Ueberschrift', () => {
// "#Hinweise" ist kein Titel und "###" keine Ebene, die der Leser kennt -
// beides bleibt sichtbar Text, statt still verschluckt zu werden.
const text = gliedereText('#Hinweise\n\n### Dritte Ebene', ERSATZ);
expect(text.titel).toBe(ERSATZ);
expect(text.bloecke).toEqual([absatz('#Hinweise'), absatz('### Dritte Ebene')]);
});
});
describe('gliedereText: Absaetze', () => {
it('fuegt die Zeilen eines Absatzes mit einem Leerzeichen zusammen', () => {
const text = gliedereText('Die erste Zeile\nlaeuft hier weiter\nund endet hier.', ERSATZ);
expect(text.bloecke).toEqual([absatz('Die erste Zeile laeuft hier weiter und endet hier.')]);
});
it('trennt Absaetze an der Leerzeile und macht aus mehreren Leerzeilen keinen leeren Absatz', () => {
const text = gliedereText('Eins.\n\n\n \nZwei\nZeilen.', ERSATZ);
expect(text.bloecke).toEqual([absatz('Eins.'), absatz('Zwei Zeilen.')]);
});
it('laesst Leerzeilen und Leerzeichen am Anfang und am Ende nicht als Block stehen', () => {
const text = gliedereText('\n\n Absatz mit Rand. \n\n\n\n', ERSATZ);
expect(text.bloecke).toEqual([absatz('Absatz mit Rand.')]);
});
it('liest Wagenruecklaeufe am Zeilenende nicht mit', () => {
const text = gliedereText('# Titel\r\n\r\nZeile eins\r\nZeile zwei\r\n\r\n- Punkt\r\n', ERSATZ);
expect(text.titel).toBe('Titel');
expect(text.bloecke).toEqual([absatz('Zeile eins Zeile zwei'), aufzaehlung('Punkt')]);
});
it('beendet einen Absatz an einer Ueberschrift auch ohne Leerzeile', () => {
const text = gliedereText('Absatz.\n## Abschnitt\nWeiter.', ERSATZ);
expect(text.bloecke).toEqual([absatz('Absatz.'), abschnitt('Abschnitt'), absatz('Weiter.')]);
});
});
describe('gliedereText: Aufzaehlungen', () => {
it('fasst aufeinanderfolgende Punkte zu einer Aufzaehlung zusammen', () => {
const text = gliedereText('- eins\n- zwei\n- drei', ERSATZ);
expect(text.bloecke).toEqual([aufzaehlung('eins', 'zwei', 'drei')]);
});
it('beginnt nach einem Absatz auch ohne Leerzeile eine Aufzaehlung', () => {
const text = gliedereText('Einleitung:\n- erster Punkt\n- zweiter Punkt', ERSATZ);
expect(text.bloecke).toEqual([
absatz('Einleitung:'),
aufzaehlung('erster Punkt', 'zweiter Punkt'),
]);
});
it('setzt einen Punkt mit der unmittelbar folgenden Zeile fort', () => {
const text = gliedereText('- erster Punkt,\n umbrochen\n- zweiter Punkt', ERSATZ);
expect(text.bloecke).toEqual([aufzaehlung('erster Punkt, umbrochen', 'zweiter Punkt')]);
});
it('beendet eine Aufzaehlung an der Leerzeile, der Absatz danach steht fuer sich', () => {
const text = gliedereText('- Punkt\n\nAbsatz danach.', ERSATZ);
expect(text.bloecke).toEqual([aufzaehlung('Punkt'), absatz('Absatz danach.')]);
});
it('beendet eine Aufzaehlung an einer Ueberschrift', () => {
const text = gliedereText('- Punkt\n## Abschnitt', ERSATZ);
expect(text.bloecke).toEqual([aufzaehlung('Punkt'), abschnitt('Abschnitt')]);
});
it('schliesst eine Aufzaehlung am Dateiende ohne Zeilenumbruch ab', () => {
const text = gliedereText('Absatz.\n\n- letzter Punkt', ERSATZ);
expect(text.bloecke).toEqual([absatz('Absatz.'), aufzaehlung('letzter Punkt')]);
});
it('liest einen Bindestrich ohne Leerzeichen nicht als Punkt', () => {
const text = gliedereText('-kein Punkt', ERSATZ);
expect(text.bloecke).toEqual([absatz('-kein Punkt')]);
});
});
describe('gliedereText: anders als CommonMark', () => {
/*
* Die Liste im Kopf von src/ui/datenschutzText.ts, Fall fuer Fall: was dieser
* Leser liest. Nach CommonMark gliedert sich jeder dieser Texte anders, und
* deshalb darf docs/datenschutz.md keinen davon enthalten
* (`gliederungsabweichungen` in tests/ui/datenschutzAnsicht.test.ts). Liest
* der Leser einen davon eines Tages anders, gehoeren Kopf und Wache mit
* geaendert.
*/
it.each<[string, string, Textblock[]]>([
[
'zwei Punkte mit einer Leerzeile dazwischen: zwei Aufzaehlungen',
'- a\n\n- b',
[aufzaehlung('a'), aufzaehlung('b')],
],
[
'eine eingerueckte Zeile nach Leerzeile hinter einem Punkt: ein eigener Absatz',
'- a\n\n b',
[aufzaehlung('a'), absatz('b')],
],
['Bindestriche unter einer Absatzzeile: Text', 'Titel\n---', [absatz('Titel ---')]],
['Gleichheitszeichen unter einer Absatzzeile: Text', 'Titel\n===', [absatz('Titel ===')]],
[
'eine Zeile nur aus Sternen oder Unterstrichen: ein Absatz',
'Eins.\n\n***\n\n___',
[absatz('Eins.'), absatz('***'), absatz('___')],
],
['ein Bindestrich allein hinter einem Punkt: Fortsetzung', '- a\n-', [aufzaehlung('a -')]],
['ein Zitat: Text', '> Zitat', [absatz('> Zitat')]],
['Tilden: Text', '~~~\nCode\n~~~', [absatz('~~~ Code ~~~')]],
[
'eine Verweisdefinition: sichtbarer Text',
'[name]: https://example.org',
[absatz('[name]: https://example.org')],
],
['ein Tabulator nach dem Bindestrich: kein Punkt', '-\tPunkt', [absatz('-\tPunkt')]],
])('%s', (_fall, wortlaut, erwartet) => {
expect(gliedereText(wortlaut, ERSATZ).bloecke).toEqual(erwartet);
});
});
describe('textbloeckeAlsKnoten', () => {
it('macht aus einem Abschnitt h3, aus einem Absatz p und aus einer Aufzaehlung ul mit li', () => {
const knoten = textbloeckeAlsKnoten(
[abschnitt('Abschnitt'), absatz('Absatz.'), aufzaehlung('eins', 'zwei')],
'de',
);
expect(knoten.map((k) => k.tagName)).toEqual(['H3', 'P', 'UL']);
expect(knoten.map((k) => k.textContent)).toEqual(['Abschnitt', 'Absatz.', 'einszwei']);
const punkte = [...knoten[2]!.children];
expect(punkte.map((p) => p.tagName)).toEqual(['LI', 'LI']);
expect(punkte.map((p) => p.textContent)).toEqual(['eins', 'zwei']);
});
it('zeichnet jeden Block mit der uebergebenen Sprache aus', () => {
for (const sprache of ['de', 'en'] as const) {
const knoten = textbloeckeAlsKnoten([abschnitt('A'), absatz('B'), aufzaehlung('C')], sprache);
expect(knoten.map((k) => k.getAttribute('lang'))).toEqual([sprache, sprache, sprache]);
}
});
it('setzt Text als Text: Eine spitze Klammer wird kein Element', () => {
const knoten = textbloeckeAlsKnoten(
[
abschnitt('Abschnitt'),
absatz('
'),
aufzaehlung('fett'),
],
'de',
);
expect(knoten[0]!.children).toHaveLength(0);
expect(knoten[0]!.textContent).toBe('Abschnitt');
expect(knoten[1]!.children).toHaveLength(0);
expect(knoten[1]!.textContent).toBe('
');
expect(knoten[2]!.querySelector('b')).toBeNull();
expect(knoten[2]!.textContent).toBe('fett');
});
it('liefert fuer keine Bloecke keine Knoten', () => {
expect(textbloeckeAlsKnoten([], 'de')).toEqual([]);
});
});