v1 checkpoint

This commit is the initial checkpoint for a functioning converter with
all tests passing.
This commit is contained in:
evilchili committed 2026-10-16 13:00:11 -07:00
1 parent 158c71bce6
commit 965fc09d99
58 files changed
+2921 -3224

No files matched your search

+164 -101
View File
@@ -1,6 +1,6 @@
import { expect, test, suite, describe } from 'vitest'
import { HopDown } from '../dist/lib';
import { HopDown } from '../dist';
const hopdown = new HopDown();
@@ -8,7 +8,7 @@ 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, () => {
return test(name, () => {
const html = hopdown.toHTML(input);
expect(html.toLowerCase()).toBe(output.toLowerCase());
expect(hopdown.toMarkdown(html)).toBe(expected !== undefined ? expected : input);
@@ -23,77 +23,52 @@ suite('Markdown to HTML Round-Trips', () => {
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('link', '[t](http://x)', '<p><a href="http://x" title="">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>');
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>');
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">foo</a></p>');
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">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('anchor', '[foo](#anchor)', '<p><a href="#anchor" title="">foo</a></p>');
assert(
'autolink',
'foo http://x bar',
'<p>foo <a href="http://x">http://x</a> bar</p>',
'<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">bold</a></strong></p>',
'<p><strong><a href="http://x" title="">bold</a></strong></p>',
);
});
@@ -101,68 +76,71 @@ suite('Markdown to HTML Round-Trips', () => {
// basic table
var md = (
'| Header 1 | Header 2 |\n' +
'| Header 1 | Header 2 |\n' +
'| --- | --- |\n' +
'| Cell 1 | Cell 2 |'
);
var html = (
'<table><thead>' +
'<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>'
'</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' +
'| Header 1 | Header 2 |\n' +
'| --- | --- |\n' +
'| *italic* | **bold** |\n' +
'| *italic* | **bold** |\n' +
'| `code` | |'
);
var html = (
'<table><thead>' +
'<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>'
'</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' +
'| Header 1 | Header 2 | Header 3 |\n' +
'| :---: | --- | ---: |\n' +
'| Cell 1 | Cell 2 | Cell 3 |'
);
var md = (
'| Header 1 | Header 2 |Header 3 |\n' +
'| 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>'
'<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', () => {
var html = 'a <span>SPAN</span>';
assert('HTML is stripped', html, '<p>' + html + '</p>', 'a SPAN')
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',
@@ -172,11 +150,12 @@ suite('Markdown to HTML Round-Trips', () => {
);
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) {
@@ -186,20 +165,14 @@ suite('Markdown to Editor Nodes Round-Trips', () => {
return s;
}
function assertInline(name: string, delim: string) {
function assertInline(name: string, markdown: string, expected: 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);
const node = hopdown.toEditorNode(markdown);
var html = '';
for (const child of Array.from(node.children)) {
html += child.outerHTML;
}
expect(html).toStrictEqual(expected);
});
}
@@ -211,23 +184,113 @@ suite('Markdown to Editor Nodes Round-Trips', () => {
});
}
describe('inline', () => {
assertInline('strong', '**');
assertInline('em', '*');
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```',
(
'<div>' +
'<span class="delim pre">```</span>\n' +
'foo\n' +
'<pre>' +
'<span class="delim pre">```</span>\n' +
'foo\n' +
'<span class="delim pre">```</span>' +
'</div>'
'</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>');
});
});