import { expect, test, suite, describe } from 'vitest' import { HopDown } from '../dist/lib'; const hopdown = new HopDown(); suite('Markdown to HTML Round-Trips', () => { function assert(name: string, input: string, output: string, expected: string | undefined = undefined) { return test(name, () => { const html = hopdown.toHTML(input); expect(html.toLowerCase()).toBe(output.toLowerCase()); expect(hopdown.toMarkdown(html)).toBe(expected !== undefined ? expected : input); }); } describe('inline', () => { assert('italic *', '*italic*', '

italic

'); assert('italic _', '_italic_', '

italic

', '*italic*'); assert('bold *', '**bold**', '

bold

'); assert('bold _', '__bold__', '

bold

', '**bold**'); assert('strikethrough', '~~strike~~', '

strike

'); assert('inline code', '`code`', '

code

'); assert('bold+italic', '***bi***', '

bi

'); assert('link', '[t](http://x)', '

t

'); assert('mixed', 'a **b** *c* `d`', '

a b c d

'); assert('code then bold', '`a` **b**', '

a b

'); assert('nested, ummatched italic', '**i* b**', '

i* b

'); assert('nested, ummatched bold', '*b** i*', '

b** i

'); }); describe('block', () => { assert('HR ***', '***', '
'); assert('HR ******', '******', '
', '***'); assert('HR ---', '---', '
', '***'); assert('HR ___', '___', '
', '***'); assert('H1', '# h', "

h

"); assert('H2', '## h', "

h

"); assert('H3', '### h', "

h

"); assert('H4', '#### h', "

h

"); assert('H5', '##### h', "
h
"); assert('H6', '###### h', "
h
"); assert('H1 with nested', '# h **bold**', "

h bold

"); assert('Blockquote', '> foo\n> bar', "
foo
bar
"); assert('Blockquote+break', '> foo\n>\n> bar', "
foo

bar

"); assert('Blockquote+Bold`', '> **foo**\n> bar', "
foo
bar
"); assert('fenced code', '```\none\ntwo\n```', '
one\ntwo
'); assert('fenced code+lang', '```ts\nconst foo = "";\n```', '
const foo = "";
'); assert('fenced code ~~~', '~~~\none\ntwo\n~~~', '
one\ntwo
', '```\none\ntwo\n```'); assert('UL *', '* foo\n* bar', ''); assert('UL -', '- foo\n- bar', '', '* foo\n* bar'); assert('OL 1. 2.', '1. foo\n2. bar', '
  1. foo
  2. bar
'); assert('OL 1. 1.', '1. foo\n1. bar', '
  1. foo
  2. bar
', '1. foo\n2. bar'); assert('UL Nested', '* foo\n * bar', ''); assert('OL in UL', '* foo\n 1. bar\n 2. baz', ''); }); describe('links and anchors', () => { assert('link', '[foo](http://x)', '

foo

'); assert('link with title', '[foo](http://x "bar")', '

foo

'); assert('anchor', '[foo](#anchor)', '

foo

'); var reflinks = ( '\n\n' + '[foo]: http://foo\n' + '[bar]: http://bar "Bar"\n' ); assert( 'reference link', '[text][foo]' + reflinks, '

text

', '[text](http://foo)' ); assert( 'shortcut link', '[foo][]\n\n' + reflinks, '

foo

', '[foo](http://foo)' ); assert( 'reference with title', '[text][bar]' + reflinks, '

text

', '[text](http://bar "Bar")' ); assert( 'autolink', 'foo http://x bar', '

foo http://x bar

', 'foo [http://x](http://x) bar', ); assert( 'anchor wrapped with bold', '**[bold](http://x)**', '

bold

', ); }); describe('tables', () => { // basic table var md = ( '| Header 1 | Header 2 |\n' + '| --- | --- |\n' + '| Cell 1 | Cell 2 |' ); var html = ( '' + '' + '' + '' + '
Header 1Header 2
Cell 1Cell 2
' ); assert('Table', md, html); // table with inline formatting var md = ( '| Header 1 | Header 2 |\n' + '| --- | --- |\n' + '| *italic* | **bold** |\n' + '| `code` | |' ); var html = ( '' + '' + '' + '' + '' + '
Header 1Header 2
italicbold
code
' ); assert('Table + inline', md, html); // normalized markdown and cell alignment var expected = ( '| Header 1 | Header 2 | Header 3 |\n' + '| :---: | --- | ---: |\n' + '| Cell 1 | Cell 2 | Cell 3 |' ); var md = ( '| Header 1 | Header 2 |Header 3 |\n' + '| :---: | :--- | ----------: |\n' + '| Cell 1 | Cell 2 |Cell 3 |' ) var html = ( '' + '' + '' + '' + '' + '' + '' + '' + '
Header 1Header 2Header 3
Cell 1Cell 2Cell 3
' ); assert('Table alignment', md, html, expected); }); describe('Edge Cases', () => { var html = 'a SPAN'; assert('HTML is stripped', html, '

' + html + '

', 'a SPAN') assert('Bare brackets', '< foo', '

< foo

', '< foo'); assert( 'HTML Entities', '< > & { { &unknown; foo', '

< > & { { &unknown; foo

', '< > & { { &unknown; foo' ); assert('Escape sequence \\*', '\\*not italic\\*', '

\\*not italic\\*

'); assert('Escape sequence \\`', '\\`not code\\`', '

\\`not code\\`

'); }); }); suite('Markdown to Editor Nodes Round-Trips', () => { function span(name: string, delim: string) { const s = document.createElement('span'); s.className = `delim ${name.toLowerCase()}`; s.textContent = delim; return s; } function assertInline(name: string, delim: string) { return test(name, () => { const input = delim + name + delim; const expected = document.createDocumentFragment(); expected.appendChild(document.createElement('div'));; expected.childNodes[0].appendChild(span(name, delim)); expected.childNodes[0].appendChild(document.createTextNode(name)); expected.childNodes[0].appendChild(span(name, delim)); const node = hopdown.toEditorNode(input); expect(node).toStrictEqual(expected); expect(hopdown.toMarkdown(node)).toBe(input); }); } function assertBlock(name: string, input: string, output: string) { return test(name, () => { const div = document.createElement('div'); div.appendChild(hopdown.toEditorNode(input)); expect(div.innerHTML).toStrictEqual(output); }); } describe('inline', () => { assertInline('strong', '**'); assertInline('em', '*'); }); describe('block', () => { assertBlock( 'Fenced Code', '```\nfoo\n```', ( '
' + '```\n' + 'foo\n' + '```' + '
' ) ); }); });