Files
hopdown/test/hopdown.test.ts
2026-08-09 15:46:11 -07:00

234 lines
9.5 KiB
TypeScript

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*', '<p><em>italic</em></p>');
assert('italic _', '_italic_', '<p><em>italic</em></p>', '*italic*');
assert('bold *', '**bold**', '<p><strong>bold</strong></p>');
assert('bold _', '__bold__', '<p><strong>bold</strong></p>', '**bold**');
assert('strikethrough', '~~strike~~', '<p><del>strike</del></p>');
assert('inline code', '`code`', '<p><code>code</code></p>');
assert('bold+italic', '***bi***', '<p><strong><em>bi</em></strong></p>');
assert('link', '[t](http://x)', '<p><a href="http://x">t</a></p>');
assert('mixed', 'a **b** *c* `d`', '<p>a <strong>b</strong> <em>c</em> <code>d</code></p>');
assert('code then bold', '`a` **b**', '<p><code>a</code> <strong>b</strong></p>');
assert('nested, ummatched italic', '**i* b**', '<p><strong>i* b</strong></p>');
assert('nested, ummatched bold', '*b** i*', '<p><em>b** i</em></p>');
});
describe('block', () => {
assert('HR ***', '***', '<hr>');
assert('HR ******', '******', '<hr>', '***');
assert('HR ---', '---', '<hr>', '***');
assert('HR ___', '___', '<hr>', '***');
assert('H1', '# h', "<h1 id='h'>h</h1>");
assert('H2', '## h', "<h2 id='h'>h</h2>");
assert('H3', '### h', "<h3 id='h'>h</h3>");
assert('H4', '#### h', "<h4 id='h'>h</h4>");
assert('H5', '##### h', "<h5 id='h'>h</h5>");
assert('H6', '###### h', "<h6 id='h'>h</h6>");
assert('H1 with nested', '# h **bold**', "<h1 id='h_bold'>h <strong>bold</strong></h1>");
assert('Blockquote', '> foo\n> bar', "<blockquote>foo<br>bar</blockquote>");
assert('Blockquote+break', '> foo\n>\n> bar', "<blockquote>foo<p>bar</p></blockquote>");
assert('Blockquote+Bold`', '> **foo**\n> bar', "<blockquote><strong>foo</strong><br>bar</blockquote>");
assert('fenced code', '```\none\ntwo\n```', '<pre>one\ntwo</pre>');
assert('fenced code+lang', '```ts\nconst foo = "";\n```', '<pre class="language-ts">const foo = &quot;&quot;;</pre>');
assert('fenced code ~~~', '~~~\none\ntwo\n~~~', '<pre>one\ntwo</pre>', '```\none\ntwo\n```');
assert('UL *', '* foo\n* bar', '<ul><li>foo</li><li>bar</li></ul>');
assert('UL -', '- foo\n- bar', '<ul><li>foo</li><li>bar</li></ul>', '* foo\n* bar');
assert('OL 1. 2.', '1. foo\n2. bar', '<ol><li>foo</li><li>bar</li></ol>');
assert('OL 1. 1.', '1. foo\n1. bar', '<ol><li>foo</li><li>bar</li></ol>', '1. foo\n2. bar');
assert('UL Nested', '* foo\n * bar', '<ul><li>foo<ul><li>bar</li></ul></li></ul>');
assert('OL in UL', '* foo\n 1. bar\n 2. baz', '<ul><li>foo<ol><li>bar</li><li>baz</li></ol></li></ul>');
});
describe('links and anchors', () => {
assert('link', '[foo](http://x)', '<p><a href="http://x">foo</a></p>');
assert('link with title', '[foo](http://x "bar")', '<p><a href="http://x" title="bar">foo</a></p>');
assert('anchor', '[foo](#anchor)', '<p><a href="#anchor">foo</a></p>');
var reflinks = (
'\n\n' +
'[foo]: http://foo\n' +
'[bar]: http://bar "Bar"\n'
);
assert(
'reference link',
'[text][foo]' + reflinks,
'<p><a href="http://foo">text</a></p>',
'[text](http://foo)'
);
assert(
'shortcut link',
'[foo][]\n\n' + reflinks,
'<p><a href="http://foo">foo</a></p>',
'[foo](http://foo)'
);
assert(
'reference with title',
'[text][bar]' + reflinks,
'<p><a href="http://bar" title="Bar">text</a></p>',
'[text](http://bar "Bar")'
);
assert(
'autolink',
'foo http://x bar',
'<p>foo <a href="http://x">http://x</a> bar</p>',
'foo [http://x](http://x) bar',
);
assert(
'anchor wrapped with bold',
'**[bold](http://x)**',
'<p><strong><a href="http://x">bold</a></strong></p>',
);
});
describe('tables', () => {
// basic table
var md = (
'| Header 1 | Header 2 |\n' +
'| --- | --- |\n' +
'| Cell 1 | Cell 2 |'
);
var html = (
'<table><thead>' +
'<tr><th>Header 1</th><th>Header 2</th></tr>' +
'</thead><tbody>' +
'<tr><td>Cell 1</td><td>Cell 2</td></tr>' +
'</tbody></table>'
);
assert('Table', md, html);
// table with inline formatting
var md = (
'| Header 1 | Header 2 |\n' +
'| --- | --- |\n' +
'| *italic* | **bold** |\n' +
'| `code` | |'
);
var html = (
'<table><thead>' +
'<tr><th>Header 1</th><th>Header 2</th></tr>' +
'</thead><tbody>' +
'<tr><td><em>italic</em></td><td><strong>bold</strong></td></tr>' +
'<tr><td><code>code</code></td><td></td></tr>' +
'</tbody></table>'
);
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 = (
'<table><thead><tr>' +
'<th class="align-center">Header 1</th>' +
'<th>Header 2</th>' +
'<th class="align-right">Header 3</th>' +
'</tr></thead><tbody><tr>' +
'<td class="align-center">Cell 1</td>' +
'<td>Cell 2</td>' +
'<td class="align-right">Cell 3</td>' +
'</tr></tbody></table>'
);
assert('Table alignment', md, html, expected);
});
describe('Edge Cases', () => {
var html = 'a <span>SPAN</span>';
assert('HTML is stripped', html, '<p>' + html + '</p>', 'a SPAN')
assert('Bare brackets', '< foo', '<p>&lt; foo</p>', '< foo');
assert(
'HTML Entities',
'&lt; &gt; &amp; &#123; &#x7B; &unknown; foo',
'<p>&lt; &gt; &amp; { { &amp;unknown; foo</p>',
'< > & { { &unknown; foo'
);
assert('Escape sequence \\*', '\\*not italic\\*', '<p>\\*not italic\\*</p>');
assert('Escape sequence \\`', '\\`not code\\`', '<p>\\`not code\\`</p>');
});
});
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```',
(
'<div>' +
'<span class="delim pre">```</span>\n' +
'foo\n' +
'<span class="delim pre">```</span>' +
'</div>'
)
);
});
});