Files
hopdown/test/hopdown.test.ts
T
2026-10-16 13:55:16 -07:00

303 lines
12 KiB
TypeScript

import { expect, test, suite, describe } from 'vitest'
import { HopDown } from '../dist';
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" title="">t</a></p>');
assert('image', '![text](src.png "title")', '<p><img src="src.png" alt="text" title="title"></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 ***', '***', '<p><hr></p>');
assert('HR ---', '---', '<p><hr></p>', '***');
assert('HR ___', '___', '<p><hr></p>', '***');
//assert('HR ******', '******', '<p><hr></p>', '***');
assert('H1', '# h', "<p><h1 id='h'>h</h1></p>");
assert('H2', '## h', "<p><h2 id='h'>h</h2></p>");
assert('H3', '### h', "<p><h3 id='h'>h</h3></p>");
assert('H4', '#### h', "<p><h4 id='h'>h</h4></p>");
assert('H5', '##### h', "<p><h5 id='h'>h</h5></p>");
assert('H6', '###### h', "<p><h6 id='h'>h</h6></p>");
assert('H1 with nested', '# h **bold**', "<p><h1 id='h_bold'>h <strong>bold</strong></h1></p>");
assert('Blockquote+break', '> foo\n> \n> bar', "<p><blockquote>foo<br><p>bar</p></blockquote></p>");
assert('Blockquote', '> one\n> two', "<p><blockquote>one<br>two</blockquote></p>");
assert('Blockquote+Bold`', '> **foo**\n> bar', "<p><blockquote><strong>foo</strong><br>bar</blockquote></p>");
assert('fenced code', '```\none\ntwo\n```', '<p><pre>\none\ntwo\n</pre></p>');
assert('fenced code+lang', '```ts\nconst foo = "";\n```', '<p><pre class="language-ts">\nconst foo = &quot;&quot;;\n</pre></p>');
assert('UL *', '* foo\n* bar', '<p><ul><li>foo</li><li>bar</li></ul></p>');
assert('UL -', '- foo\n- **bar**', '<p><ul><li>foo</li><li><strong>bar</strong></li></ul></p>', '* foo\n* **bar**');
assert('OL 1. 2.', '1. foo\n2. bar', '<p><ol><li>foo</li><li>bar</li></ol></p>');
assert('OL 1. 1.', '1. foo\n1. bar', '<p><ol><li>foo</li><li>bar</li></ol></p>', '1. foo\n2. bar');
assert('UL Nested', '* foo\n * bar', '<p><ul><li>foo<ul><li>bar</li></ul></li></ul></p>');
assert('OL in UL', '* foo\n 1. bar\n 2. baz', '<p><ul><li>foo<ol><li>bar</li><li>baz</li></ol></li></ul></p>');
});
describe('links and anchors', () => {
assert('link', '[foo](http://x)', '<p><a href="http://x" title="">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" title="">foo</a></p>');
assert(
'autolink',
'foo http://x bar',
'<p>foo <a href="http://x" title="">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" title="">bold</a></strong></p>',
);
});
describe('tables', () => {
// basic table
var md = (
'| Header 1 | Header 2 |\n' +
'| --- | --- |\n' +
'| Cell 1 | Cell 2 |'
);
var html = (
'<p><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></p>'
);
assert('Table', md, html);
// table with inline formatting
var md = (
'| Header 1 | Header 2 |\n' +
'| --- | --- |\n' +
'| *italic* | **bold** |\n' +
'| `code` | |'
);
var html = (
'<p><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></p>'
);
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 = (
'<p><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></p>'
);
assert('Table alignment', md, html, expected);
});
describe('Edge Cases', () => {
assert('HTML is escaped',
'some <span>html</span>',
'<p>some &lt;span&gt;html&lt;/span&gt;</p>',
'some <span>html</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>');
assert('trailing space', '*i* ', '<p><em>i</em> </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, markdown: string, expected: string) {
return test(name, () => {
const node = hopdown.toEditorNode(markdown);
var html = '';
for (const child of Array.from(node.children)) {
html += child.outerHTML;
}
expect(html).toStrictEqual(expected);
});
}
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('trailing text', '*i* fnord', '<div><span class="delim em">*</span>i<span class="delim em">*</span></div><div> fnord</div>');
});
describe('block', () => {
assertBlock('hr ***', '***', '<hr>');
assertBlock('hr ---', '---', '<hr>');
assertBlock('hr ___', '___', '<hr>');
assertBlock('h1', '# one', '<h1 id="One">one</h1>');
assertBlock('h2', '## two', '<h2 id="Two">two</h2>');
assertBlock('h3', '### three', '<h3 id="Three">three</h3>');
assertBlock('h4', '#### four', '<h4 id="Four">four</h4>');
assertBlock('h5', '##### five', '<h5 id="Five">five</h5>');
assertBlock('h6', '###### six', '<h6 id="Six">six</h6>');
assertBlock('h1 with inline markup', '# *italic* one\nfoo', '<h1 id="Italic_One">*italic* one</h1><div>\nfoo</div>');
assertBlock(
'Fenced Code',
'```\nfoo\n```',
(
'<pre>' +
'<span class="delim pre">```</span>\n' +
'foo\n' +
'<span class="delim pre">```</span>' +
'</pre>'
)
);
assertBlock(
'Blockquote',
'> foo\n>\n> ***bar*** buz\nbaz',
(
'<blockquote>' +
'<p><span class="delim blockquote">&gt;</span>foo</p>' +
'<p><span class="delim blockquote">&gt;</span></p>' +
'<p><span class="delim blockquote">&gt;</span>' +
'<span class="delim em strong">***</span>' +
'bar' +
'<span class="delim em strong">***</span> buz</p>' +
'</blockquote><div>\nbaz</div>'
)
);
assertBlock(
'Unordered List',
'- foo\n - bar\n- **baz**\n',
(
'<ul>' +
'<li>foo<ul><li>bar</li></ul></li>' +
'<li><span class="delim strong">**</span>baz' +
'<span class="delim strong">**</span></li>' +
'</ul>'
)
);
assertBlock(
'Ordered List with Nesting',
'1. foo\n * ***bar***\n1. baz',
(
'<ol>' +
'<li>foo<ul><li>' +
'<span class="delim em strong">***</span>' +
'bar' +
'<span class="delim em strong">***</span>' +
'</li></ul></li>' +
'<li>baz</li>' +
'</ol>'
)
);
describe('tables', () => {
var md = (
'| Header 1 | Header 2 |\n' +
'| --- | ---: |\n' +
'| **Cell 1** | Cell 2 |'
);
var html = (
'<table>' +
'<thead>' +
'<tr>' +
'<th>Header 1</th>' +
'<th class="align-right">Header 2</th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
'<tr>' +
'<td>' +
'<span class="delim strong">**</span>' +
'Cell 1' +
'<span class="delim strong">**</span>' +
'</td>' +
'<td class="align-right">Cell 2</td>' +
'</tr>' +
'</tbody>' +
'</table>'
);
assertBlock('Table', md, html);
});
});
describe('link', () => {
assertInline('link', '[label](url)', '<a href="url" title="">label</a>');
assertInline('link', '[label](url "title")', '<a href="url" title="title">label</a>');
});
describe('image', () => {
assertInline('image', '![alt](url)', '<img src="url" alt="alt" title="">');
assertInline('image', '![alt](url "title")', '<img src="url" alt="alt" title="title">');
});
});