ribbit/test/hopdown.test.ts

152 lines
8.2 KiB
TypeScript
Raw Normal View History

2026-04-28 21:39:13 -07:00
import { ribbit } from './setup';
const r = ribbit();
const hopdown = new r.HopDown();
const H = (md: string) => hopdown.toHTML(md);
const M = (html: string) => hopdown.toMarkdown(html);
const rt = (md: string) => M(H(md));
describe('Markdown → HTML', () => {
describe('inline formatting', () => {
it('bold', () => expect(H('**bold**')).toBe('<p><strong>bold</strong></p>'));
it('italic', () => expect(H('*italic*')).toBe('<p><em>italic</em></p>'));
it('inline code', () => expect(H('`code`')).toBe('<p><code>code</code></p>'));
it('link', () => expect(H('[t](http://x)')).toBe('<p><a href="http://x">t</a></p>'));
it('bold+italic', () => expect(H('***bi***')).toBe('<p><em><strong>bi</strong></em></p>'));
it('mixed', () => expect(H('a **b** *c* `d`')).toBe('<p>a <strong>b</strong> <em>c</em> <code>d</code></p>'));
it('code before bold', () => expect(H('`a` **b**')).toBe('<p><code>a</code> <strong>b</strong></p>'));
});
describe('headings', () => {
it.each([1,2,3,4,5,6])('h%i', (n) => {
const prefix = '#'.repeat(n);
expect(H(`${prefix} Sub`)).toContain(`<h${n}`);
});
it('heading id', () => expect(H('## Hello World')).toContain("id='HelloWorld'"));
it('heading inline md', () => expect(H('## **Bold** text')).toContain('<strong>Bold</strong>'));
});
describe('horizontal rules', () => {
it('***', () => expect(H('***')).toBe('<hr>'));
it('---', () => expect(H('---')).toBe('<hr>'));
it('___', () => expect(H('___')).toBe('<hr>'));
});
describe('lists', () => {
it('ul *', () => expect(H('* a\n* b')).toBe('<ul><li>a</li><li>b</li></ul>'));
it('ul -', () => expect(H('- a\n- b')).toBe('<ul><li>a</li><li>b</li></ul>'));
it('ol', () => expect(H('1. a\n2. b')).toBe('<ol><li>a</li><li>b</li></ol>'));
it('ul inline', () => expect(H('* **bold** item')).toContain('<strong>bold</strong>'));
});
describe('blockquotes', () => {
it('basic', () => expect(H('> text')).toContain('<blockquote>'));
it('content', () => expect(H('> hello')).toContain('hello'));
it('multi-line', () => expect(H('> a\n> b')).toContain('a'));
});
describe('fenced code', () => {
it('basic', () => expect(H('```\nx = 1\n```')).toContain('<pre><code>'));
it('content', () => expect(H('```\nx = 1\n```')).toContain('x = 1'));
it('language', () => expect(H('```js\nvar x;\n```')).toContain('language-js'));
it('escapes html', () => expect(H('```\n<div>\n```')).toContain('&lt;div&gt;'));
it('no lang when none', () => expect(H('```\nplain\n```')).not.toContain('language-'));
});
describe('tables', () => {
const tbl = '| a | b |\n|---|---|\n| 1 | 2 |';
it('table tag', () => expect(H(tbl)).toContain('<table>'));
it('thead', () => expect(H(tbl)).toContain('<thead>'));
it('th cells', () => expect(H(tbl)).toContain('<th>a</th>'));
it('td cells', () => expect(H(tbl)).toContain('<td>1</td>'));
it('center align', () => expect(H('| C |\n|:--:|\n| x |')).toContain('text-align:center'));
it('right align', () => expect(H('| R |\n|--:|\n| x |')).toContain('text-align:right'));
it('inline md', () => expect(H('| **b** |\n|---|\n| x |')).toContain('<strong>b</strong>'));
});
describe('paragraphs', () => {
it('single', () => expect(H('hello')).toBe('<p>hello</p>'));
it('two', () => expect(H('a\n\nb')).toBe('<p>a</p>\n<p>b</p>'));
it('soft break', () => expect(H('a\nb')).toBe('<p>a\nb</p>'));
});
describe('edge cases', () => {
it('empty', () => expect(H('')).toBe(''));
it('whitespace', () => expect(H(' ')).toBe(''));
it('html entities', () => expect(H('a & b < c')).toContain('&amp;'));
it('html in code', () => expect(H('`<div>`')).toContain('&lt;div&gt;'));
it('para then heading', () => expect(H('text\n\n## H')).toContain('<h2'));
it('list then para', () => expect(H('- a\n\ntext')).toContain('<p>text</p>'));
});
});
describe('HTML → Markdown', () => {
it('strong→**', () => expect(M('<p><strong>b</strong></p>')).toBe('**b**'));
it('em→*', () => expect(M('<p><em>i</em></p>')).toBe('*i*'));
it('code→`', () => expect(M('<p><code>c</code></p>')).toBe('`c`'));
it('a→[]', () => expect(M('<a href="http://x">t</a>')).toBe('[t](http://x)'));
it('h1→#', () => expect(M('<h1>T</h1>')).toBe('# T'));
it('hr→---', () => expect(M('<hr>')).toBe('---'));
it('ul→-', () => expect(M('<ul><li>a</li><li>b</li></ul>')).toBe('- a\n- b'));
it('ol→1.', () => expect(M('<ol><li>a</li><li>b</li></ol>')).toBe('1. a\n2. b'));
it('bq→>', () => expect(M('<blockquote><p>q</p></blockquote>')).toContain('> '));
it('pre→```', () => expect(M('<pre><code>x</code></pre>')).toContain('```'));
it('pre lang', () => expect(M('<pre><code class="language-py">x</code></pre>')).toContain('```py'));
it('table→pipes', () => {
const html = '<table><thead><tr><th>a</th><th>b</th></tr></thead><tbody><tr><td>1</td><td>2</td></tr></tbody></table>';
expect(M(html)).toContain('| a | b |');
});
});
describe('Round-trips', () => {
it.each([
['paragraph', 'Hello world'],
['bold', '**bold**'],
['italic', '*italic*'],
['code', '`code`'],
['link', '[t](http://x)'],
['h1', '# Title'],
['h2', '## Sub'],
['ul', '- a\n- b'],
['ol', '1. a\n2. b'],
])('%s', (_, md) => expect(rt(md)).toBe(md));
it('hr', () => expect(rt('---')).toBe('---'));
it('blockquote', () => expect(rt('> quoted')).toContain('> '));
it('code block', () => expect(rt('```\nx = 1\n```')).toContain('```'));
it('table', () => expect(rt('| a | b |\n|---|---|\n| 1 | 2 |')).toContain('| a | b |'));
});
describe('Nested inline', () => {
it('bold wraps italic', () => expect(H('**a *b* c**')).toBe('<p><strong>a <em>b</em> c</strong></p>'));
it('italic wraps bold', () => expect(H('*a **b** c*')).toBe('<p><em>a <strong>b</strong> c</em></p>'));
it('bold wraps code', () => expect(H('**a `b` c**')).toBe('<p><strong>a <code>b</code> c</strong></p>'));
it('bold wraps link', () => expect(H('**[t](u)**')).toBe('<p><strong><a href="u">t</a></strong></p>'));
it('link with bold', () => expect(H('[**t**](u)')).toBe('<p><a href="u"><strong>t</strong></a></p>'));
it('link with code', () => expect(H('[`t`](u)')).toBe('<p><a href="u"><code>t</code></a></p>'));
});
describe('Nested blocks', () => {
it('bq > heading', () => expect(H('> # Title')).toContain('<h1'));
it('bq > list', () => expect(H('> - a\n> - b')).toContain('<ul>'));
it('bq > bq', () => expect(H('> > nested')).toContain('<blockquote>'));
it('li > bold', () => expect(H('- **bold**')).toContain('<strong>bold</strong>'));
it('heading > code', () => expect(H('## `code`')).toContain('<code>code</code>'));
it('table > bold', () => expect(H('| **b** |\n|---|\n| x |')).toContain('<strong>b</strong>'));
});
describe('Nested lists', () => {
it('ul > ul', () => expect(H('- a\n - b\n - c\n- d')).toBe('<ul><li>a<ul><li>b</li><li>c</li></ul></li><li>d</li></ul>'));
it('ol > ol', () => expect(H('1. a\n 1. b\n 1. c\n2. d')).toBe('<ol><li>a<ol><li>b</li><li>c</li></ol></li><li>d</li></ol>'));
it('ul > ol', () => expect(H('- a\n 1. b\n 2. c\n- d')).toBe('<ul><li>a<ol><li>b</li><li>c</li></ol></li><li>d</li></ul>'));
it('3-level', () => expect(H('- a\n - b\n - c\n- d')).toBe('<ul><li>a<ul><li>b<ul><li>c</li></ul></li></ul></li><li>d</li></ul>'));
it('ul>ul rt', () => expect(rt('- a\n - b\n - c\n- d')).toBe('- a\n - b\n - c\n- d'));
});
describe('Tables with nested markdown', () => {
it('td bold', () => expect(H('| h |\n|---|\n| **b** |')).toContain('<td><strong>b</strong></td>'));
it('td link>bold', () => expect(H('| h |\n|---|\n| [**t**](u) |')).toContain('<a href="u"><strong>t</strong></a>'));
it('td bold rt', () => expect(rt('| h |\n|---|\n| **b** |')).toBe('| h |\n| --- |\n| **b** |'));
it('multi-cell rt', () => expect(rt('| **a** | *b* |\n|---|---|\n| `c` | [d](e) |')).toBe('| **a** | *b* |\n| --- | --- |\n| `c` | [d](e) |'));
});