import { strict as assert } from 'node:assert'; import { test } from 'node:test'; import { markdownFromDom, markdownToHtml } from '../src/http/app/markdown.js'; import { parseHtml } from './mini-dom.ts'; /** * The notes editor's Markdown bridge. * * The editor shows a note as formatted text and writes it back as Markdown, so * every save runs the note through `markdownToHtml` and `markdownFromDom`. If * that pair loses anything, it loses a lesson — these notes are the only record * of what was actually said in the room, and there is no second copy to restore * from. Hence the shape of almost every test here: put Markdown in, get the * same Markdown back. */ /** Markdown → HTML → Markdown, the trip a note takes on every edit. */ function back(markdown: string): string { return markdownFromDom(parseHtml(markdownToHtml(markdown))); } /** * Asserts the trip is idempotent, and returns what it settles on. * * One pass may tidy — `*a*` becomes `_a_`, a ragged table lines up — and that * is allowed, because the editor only rewrites a file the person has edited. * A second pass changing anything is not: it would mean every save mangles the * note a little further, which is how a term's notes rot into nothing. */ function settles(markdown: string): string { const once = back(markdown); assert.equal(back(once), once, 'the round trip is not stable'); return once; } /** Markdown that must survive the trip exactly as written. */ function unchanged(markdown: string): void { assert.equal(settles(markdown), markdown); } test('headings keep their level', () => { unchanged('## 1. Deutsch — 08:00–08:45 · MEI · R 204'); unchanged('# Eins\n\n## Zwei\n\n### Drei\n\n#### Vier'); assert.match(markdownToHtml('## Deutsch'), /
a
b
fett
'); }); test('emphasis markers hug their text', () => { // `** fett **` is four literal stars in every renderer there is. const html = 'Merke: fett rest
'; assert.equal(markdownFromDom(parseHtml(html)), 'Merke: **fett** rest'); }); test('inline code keeps what is inside it literal', () => { unchanged('Der Platzhalter `**nicht fett**` bleibt stehen.'); unchanged('`a | b`'); assert.equal(settles('``ein ` backtick``'), '``ein ` backtick``'); }); test('links keep their target, and a dangerous scheme is dropped', () => { unchanged('[Arbeitsblatt](https://example.org/ab.pdf)'); // The label survives; only the target goes. A note is never worth less than // its words, and nothing here should render a tappable `javascript:`. assert.equal(back('[hier](javascript:alert)'), 'hier'); // A target with parentheses in it is a link, not a broken one. unchanged('[Erörterung](https://de.wikipedia.org/wiki/Erörterung_(Textsorte))'); }); test('bullet lists nest', () => { unchanged('- eins\n- zwei\n - zwei a\n - zwei b\n- drei'); }); test('numbered lists keep their numbering', () => { unchanged('1. eins\n2. zwei\n3. drei'); // A list that starts elsewhere keeps its first number and renumbers the rest. assert.equal(settles('3. drei\n4. vier'), '3. drei\n4. vier'); }); test('task lists keep their boxes', () => { unchanged('- [ ] offen\n- [x] erledigt'); assert.match(markdownToHtml('- [x] fertig'), //); }); test('tables round-trip and line up', () => { unchanged('| Präfix | Adressen |\n| --- | --- |\n| /24 | 254 |\n| /25 | 126 |'); // A ragged table is tidied once, then left alone. assert.equal(settles('|a|b|\n|-|-|\n|1|2|'), '| a | b |\n| --- | --- |\n| 1 | 2 |'); }); test('a pipe inside a cell stays inside the cell', () => { const md = '| Zeichen | Bedeutung |\n| --- | --- |\n| \\| | oder |'; assert.equal(settles(md), md); }); test('blockquotes and rules survive', () => { unchanged('> Merksatz des Lehrers\n> über zwei Zeilen'); unchanged('---'); }); test('fenced code keeps its language and its contents verbatim', () => { unchanged('```bash\nip route add 10.0.0.0/8 via 10.1.1.1\n```'); // Indentation inside a fence is content, not structure. unchanged('```\nif x:\n y = 1\n```'); }); test('underscores inside words are not emphasis', () => { unchanged('snake_case_name bleibt ein Wort'); // `__` is bold in Markdown, though, and is normalised to the one spelling. assert.equal(settles('__wirklich fett__'), '**wirklich fett**'); }); test('a note cannot smuggle HTML into the editor', () => { const html = markdownToHtml(' & nicht fett'); assert.equal(html.includes('