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([]); }); });