Usage:
const editor = new RibbitEditor({
themes: [
{ name: 'dark', features: { sourceMode: false } },
{ name: 'minimal', tags: minimalTags },
],
currentTheme: 'dark',
});
The built-in theme is 'ribbit-default' and is always available.
Additional themes from the themes array are registered on top.
417 lines
23 KiB
JavaScript
417 lines
23 KiB
JavaScript
const { JSDOM } = require('jsdom');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Set up a DOM environment and load the bundle
|
|
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
url: 'http://localhost',
|
|
pretendToBeVisual: true,
|
|
});
|
|
global.window = dom.window;
|
|
global.document = dom.window.document;
|
|
global.HTMLElement = dom.window.HTMLElement;
|
|
global.Node = dom.window.Node;
|
|
|
|
// Load the compiled bundle (attaches globals to window)
|
|
const bundle = fs.readFileSync(path.join(__dirname, '..', 'dist', 'ribbit', 'ribbit.js'), 'utf8');
|
|
dom.window.eval(bundle);
|
|
|
|
const hopdown = new dom.window.HopDown();
|
|
const H = hopdown.toHTML.bind(hopdown);
|
|
const M = hopdown.toMarkdown.bind(hopdown);
|
|
function rt(md) { return M(H(md)); }
|
|
|
|
// Test harness
|
|
let passed = 0, failed = 0, errors = [];
|
|
|
|
function norm(s) { return (s || '').replace(/\r\n/g, '\n').trim(); }
|
|
|
|
function eq(name, actual, expected) {
|
|
const a = norm(actual), e = norm(expected);
|
|
if (a === e) {
|
|
passed++;
|
|
} else {
|
|
failed++;
|
|
errors.push(name);
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` expected: ${e}`);
|
|
console.log(` actual: ${a}`);
|
|
}
|
|
}
|
|
|
|
function has(name, actual, sub) {
|
|
if (norm(actual).indexOf(norm(sub)) !== -1) {
|
|
passed++;
|
|
} else {
|
|
failed++;
|
|
errors.push(name);
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` expected to contain: ${sub}`);
|
|
console.log(` actual: ${actual}`);
|
|
}
|
|
}
|
|
|
|
function not(name, actual, sub) {
|
|
if (norm(actual).indexOf(norm(sub)) === -1) {
|
|
passed++;
|
|
} else {
|
|
failed++;
|
|
errors.push(name);
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` should NOT contain: ${sub}`);
|
|
console.log(` actual: ${actual}`);
|
|
}
|
|
}
|
|
|
|
function section(n) { /* silent */ }
|
|
|
|
// ── 1. Inline formatting ────────────────────────────────
|
|
section('1. Inline Formatting → HTML');
|
|
eq('bold', H('**bold**'), '<p><strong>bold</strong></p>');
|
|
eq('italic', H('*italic*'), '<p><em>italic</em></p>');
|
|
eq('inline code', H('`code`'), '<p><code>code</code></p>');
|
|
eq('link', H('[t](http://x)'), '<p><a href="http://x">t</a></p>');
|
|
eq('bold+italic', H('***bi***'), '<p><em><strong>bi</strong></em></p>');
|
|
eq('mixed inline', H('a **b** *c* `d`'), '<p>a <strong>b</strong> <em>c</em> <code>d</code></p>');
|
|
eq('code before bold', H('`a` **b**'), '<p><code>a</code> <strong>b</strong></p>');
|
|
|
|
// ── 2. Headings ─────────────────────────────────────────
|
|
eq('h1', H('# Title'), "<h1 id='Title'>Title</h1>");
|
|
eq('h2', H('## Sub'), "<h2 id='Sub'>Sub</h2>");
|
|
eq('h3', H('### Sub3'), "<h3 id='Sub3'>Sub3</h3>");
|
|
eq('h4', H('#### Sub4'), "<h4 id='Sub4'>Sub4</h4>");
|
|
eq('h5', H('##### Sub5'), "<h5 id='Sub5'>Sub5</h5>");
|
|
eq('h6', H('###### Sub6'), "<h6 id='Sub6'>Sub6</h6>");
|
|
has('heading id multi-word', H('## Hello World'), "id='HelloWorld'");
|
|
has('heading inline md', H('## **Bold** text'), '<strong>Bold</strong>');
|
|
|
|
// ── 3. Horizontal rules ─────────────────────────────────
|
|
eq('*** rule', H('***'), '<hr>');
|
|
eq('--- rule', H('---'), '<hr>');
|
|
eq('___ rule', H('___'), '<hr>');
|
|
|
|
// ── 4. Lists ────────────────────────────────────────────
|
|
eq('ul *', H('* a\n* b'), '<ul><li>a</li><li>b</li></ul>');
|
|
eq('ul -', H('- a\n- b'), '<ul><li>a</li><li>b</li></ul>');
|
|
eq('ol', H('1. a\n2. b'),'<ol><li>a</li><li>b</li></ol>');
|
|
has('ul inline', H('* **bold** item'), '<strong>bold</strong>');
|
|
has('ol inline', H('1. *em* item'), '<em>em</em>');
|
|
|
|
// ── 5. Blockquotes ──────────────────────────────────────
|
|
has('blockquote', H('> text'), '<blockquote>');
|
|
has('bq content', H('> hello'), 'hello');
|
|
has('multi-line bq', H('> a\n> b'), 'a');
|
|
|
|
// ── 6. Fenced code blocks ───────────────────────────────
|
|
has('code block', H('```\nx = 1\n```'), '<pre><code>');
|
|
has('code content', H('```\nx = 1\n```'), 'x = 1');
|
|
has('lang class', H('```js\nvar x;\n```'), 'language-js');
|
|
has('html escaped', H('```\n<div>\n```'), '<div>');
|
|
not('no lang attr when none', H('```\nplain\n```'), 'language-');
|
|
|
|
// ── 7. Tables ───────────────────────────────────────────
|
|
var tbl = '| a | b |\n|---|---|\n| 1 | 2 |';
|
|
has('table tag', H(tbl), '<table>');
|
|
has('thead', H(tbl), '<thead>');
|
|
has('tbody', H(tbl), '<tbody>');
|
|
has('th cells', H(tbl), '<th>a</th>');
|
|
has('td cells', H(tbl), '<td>1</td>');
|
|
var aligned = '| L | C | R |\n|:--|:--:|--:|\n| a | b | c |';
|
|
has('left align (default)', H(aligned), '<td>a</td>');
|
|
has('center align', H(aligned), 'text-align:center');
|
|
has('right align', H(aligned), 'text-align:right');
|
|
has('table inline md', H('| **b** | *i* |\n|---|---|\n| x | y |'), '<strong>b</strong>');
|
|
|
|
// ── 8. Paragraphs ───────────────────────────────────────
|
|
eq('single para', H('hello'), '<p>hello</p>');
|
|
eq('two paras', H('a\n\nb'), '<p>a</p>\n<p>b</p>');
|
|
eq('soft line break', H('a\nb'), '<p>a\nb</p>');
|
|
|
|
// ── 9. HTML → Markdown ──────────────────────────────────
|
|
eq('strong→**', M('<p><strong>b</strong></p>'), '**b**');
|
|
eq('em→*', M('<p><em>i</em></p>'), '*i*');
|
|
eq('code→`', M('<p><code>c</code></p>'), '`c`');
|
|
eq('a→[]', M('<a href="http://x">t</a>'), '[t](http://x)');
|
|
eq('p→text', M('<p>hello</p>'), 'hello');
|
|
eq('h1→#', M('<h1>T</h1>'), '# T');
|
|
eq('h2→##', M('<h2>T</h2>'), '## T');
|
|
eq('h3→###', M('<h3>T</h3>'), '### T');
|
|
eq('hr→---', M('<hr>'), '---');
|
|
eq('ul→-', M('<ul><li>a</li><li>b</li></ul>'), '- a\n- b');
|
|
eq('ol→1.', M('<ol><li>a</li><li>b</li></ol>'), '1. a\n2. b');
|
|
has('bq→>', M('<blockquote><p>q</p></blockquote>'), '> ');
|
|
has('pre→```', M('<pre><code>x</code></pre>'), '```');
|
|
has('pre content', M('<pre><code>x = 1</code></pre>'), 'x = 1');
|
|
has('pre lang', M('<pre><code class="language-py">x</code></pre>'), '```py');
|
|
var tableHtml = '<table><thead><tr><th>a</th><th>b</th></tr></thead><tbody><tr><td>1</td><td>2</td></tr></tbody></table>';
|
|
has('table→pipes', M(tableHtml), '| a | b |');
|
|
has('table separator', M(tableHtml), '| --- | --- |');
|
|
has('table body', M(tableHtml), '| 1 | 2 |');
|
|
|
|
// ── 10. Round-trip ──────────────────────────────────────
|
|
eq('para rt', rt('Hello world'), 'Hello world');
|
|
eq('bold rt', rt('**bold**'), '**bold**');
|
|
eq('italic rt', rt('*italic*'), '*italic*');
|
|
eq('code rt', rt('`code`'), '`code`');
|
|
eq('link rt', rt('[t](http://x)'), '[t](http://x)');
|
|
eq('h1 rt', rt('# Title'), '# Title');
|
|
eq('h2 rt', rt('## Sub'), '## Sub');
|
|
eq('hr rt', rt('---'), '---');
|
|
eq('ul rt', rt('- a\n- b'), '- a\n- b');
|
|
eq('ol rt', rt('1. a\n2. b'), '1. a\n2. b');
|
|
has('bq rt', rt('> quoted'), '> ');
|
|
has('code block rt', rt('```\nx = 1\n```'), '```');
|
|
has('code block rt content', rt('```\nx = 1\n```'), 'x = 1');
|
|
has('table rt', rt('| a | b |\n|---|---|\n| 1 | 2 |'), '| a | b |');
|
|
|
|
// ── 11. Edge cases ──────────────────────────────────────
|
|
eq('empty string', H(''), '');
|
|
eq('whitespace only', H(' '), '');
|
|
has('html entities', H('a & b < c'), '&');
|
|
has('html in code', H('`<div>`'), '<div>');
|
|
eq('empty html→md', M(''), '');
|
|
has('para then heading', H('text\n\n## H'), '<h2');
|
|
has('list then para', H('- a\n\ntext'), '<p>text</p>');
|
|
has('table no leading pipe', H('a | b\n---|---\n1 | 2'), '<table>');
|
|
|
|
// ── 12. Complex document ────────────────────────────────
|
|
var doc = '# Title\n\nSome **bold** and *italic* text with `code`.\n\n## Section One\n\n- item 1\n- item 2\n\n## Section Two\n\n| Col A | Col B |\n|-------|-------|\n| 1 | 2 |\n\n> A blockquote\n\n```js\nvar x = 1;\n```\n\n[A link](http://example.com)\n\n---';
|
|
var html = H(doc);
|
|
has('doc: h1', html, "<h1 id='Title'>Title</h1>");
|
|
has('doc: bold', html, '<strong>bold</strong>');
|
|
has('doc: italic', html, '<em>italic</em>');
|
|
has('doc: code', html, '<code>code</code>');
|
|
has('doc: h2', html, '<h2');
|
|
has('doc: ul', html, '<ul>');
|
|
has('doc: table', html, '<table>');
|
|
has('doc: blockquote', html, '<blockquote>');
|
|
has('doc: pre', html, '<pre>');
|
|
has('doc: link', html, '<a href="http://example.com">');
|
|
has('doc: hr', html, '<hr>');
|
|
var md = M(html);
|
|
has('doc rt: heading', md, '# Title');
|
|
has('doc rt: bold', md, '**bold**');
|
|
has('doc rt: italic', md, '*italic*');
|
|
has('doc rt: code', md, '`code`');
|
|
has('doc rt: list', md, '- item 1');
|
|
has('doc rt: table', md, '| Col A | Col B |');
|
|
has('doc rt: bq', md, '> ');
|
|
has('doc rt: fenced', md, '```');
|
|
has('doc rt: link', md, '[A link](http://example.com)');
|
|
has('doc rt: hr', md, '---');
|
|
|
|
// ── 13. Nested Inline ───────────────────────────────────
|
|
eq('bold wraps italic', H('**a *b* c**'), '<p><strong>a <em>b</em> c</strong></p>');
|
|
eq('italic wraps bold', H('*a **b** c*'), '<p><em>a <strong>b</strong> c</em></p>');
|
|
eq('bold wraps code', H('**a `b` c**'), '<p><strong>a <code>b</code> c</strong></p>');
|
|
eq('italic wraps code', H('*a `b` c*'), '<p><em>a <code>b</code> c</em></p>');
|
|
eq('bold wraps link', H('**[t](u)**'), '<p><strong><a href="u">t</a></strong></p>');
|
|
eq('italic wraps link', H('*[t](u)*'), '<p><em><a href="u">t</a></em></p>');
|
|
eq('link with bold text', H('[**t**](u)'), '<p><a href="u"><strong>t</strong></a></p>');
|
|
eq('link with italic text', H('[*t*](u)'), '<p><a href="u"><em>t</em></a></p>');
|
|
eq('link with code text', H('[`t`](u)'), '<p><a href="u"><code>t</code></a></p>');
|
|
eq('bold>italic>code', H('***`x`***'), '<p><em><strong><code>x</code></strong></em></p>');
|
|
eq('bold wraps bold-italic', H('**a ***b*** c**'), '<p><strong>a <em><strong>b</strong></em> c</strong></p>');
|
|
|
|
// ── 14. Nested Blocks ───────────────────────────────────
|
|
has('bq > heading', H('> # Title'), '<h1');
|
|
has('bq > heading content', H('> # Title'), 'Title');
|
|
has('bq > list', H('> - a\n> - b'), '<ul>');
|
|
has('bq > list items', H('> - a\n> - b'), '<li>a</li>');
|
|
has('bq > inline md', H('> **bold**'), '<strong>bold</strong>');
|
|
has('bq > code', H('> `code`'), '<code>code</code>');
|
|
has('bq > link', H('> [t](u)'), '<a href="u">');
|
|
has('bq > bq', H('> > nested'), '<blockquote>');
|
|
has('bq > fenced code', H('> ```\n> x\n> ```'), '<code>');
|
|
has('li > bold', H('- **bold**'), '<strong>bold</strong>');
|
|
has('li > italic', H('- *italic*'), '<em>italic</em>');
|
|
has('li > code', H('- `code`'), '<code>code</code>');
|
|
has('li > link', H('- [t](u)'), '<a href="u">');
|
|
has('heading > link', H('## [t](u)'), '<a href="u">');
|
|
has('heading > code', H('## `code`'), '<code>code</code>');
|
|
has('table > bold', H('| **b** |\n|---|\n| x |'), '<strong>b</strong>');
|
|
has('table > italic', H('| *i* |\n|---|\n| x |'), '<em>i</em>');
|
|
has('table > code', H('| `c` |\n|---|\n| x |'), '<code>c</code>');
|
|
has('table > link', H('| [t](u) |\n|---|\n| x |'), '<a href="u">');
|
|
|
|
// ── 15. Nested Round-Trips ──────────────────────────────
|
|
eq('bold>italic rt', rt('**a *b* c**'), '**a *b* c**');
|
|
eq('italic>bold rt', rt('*a **b** c*'), '*a **b** c*');
|
|
eq('bold>code rt', rt('**a `b` c**'), '**a `b` c**');
|
|
eq('bold>link rt', rt('**[t](u)**'), '**[t](u)**');
|
|
eq('link>bold rt', rt('[**t**](u)'), '[**t**](u)');
|
|
has('bq>heading rt', rt('> # Title'), '> ');
|
|
has('bq>heading rt title', rt('> # Title'), '# Title');
|
|
has('bq>list rt', rt('> - a\n> - b'), '> ');
|
|
has('li>bold rt', rt('- **bold**'), '**bold**');
|
|
has('heading>code rt', rt('## `code`'), '`code`');
|
|
|
|
// ── 16. Nested Lists ────────────────────────────────────
|
|
eq('ul > ul', H('- a\n - b\n - c\n- d'), '<ul><li>a<ul><li>b</li><li>c</li></ul></li><li>d</li></ul>');
|
|
eq('ol > ol', H('1. a\n 1. b\n 1. c\n2. d'), '<ol><li>a<ol><li>b</li><li>c</li></ol></li><li>d</li></ol>');
|
|
eq('ul > ol', H('- a\n 1. b\n 2. c\n- d'), '<ul><li>a<ol><li>b</li><li>c</li></ol></li><li>d</li></ul>');
|
|
eq('ol > ul', H('1. a\n - b\n - c\n2. d'), '<ol><li>a<ul><li>b</li><li>c</li></ul></li><li>d</li></ol>');
|
|
eq('3-level nesting', H('- a\n - b\n - c\n- d'), '<ul><li>a<ul><li>b<ul><li>c</li></ul></li></ul></li><li>d</li></ul>');
|
|
has('nested li > bold', H('- a\n - **bold**'), '<strong>bold</strong>');
|
|
has('nested li > link', H('- a\n - [t](u)'), '<a href="u">');
|
|
eq('ul>ul → md', M('<ul><li>a<ul><li>b</li><li>c</li></ul></li><li>d</li></ul>'), '- a\n - b\n - c\n- d');
|
|
eq('ol>ol → md', M('<ol><li>a<ol><li>b</li><li>c</li></ol></li><li>d</li></ol>'), '1. a\n 1. b\n 2. c\n2. d');
|
|
eq('ul>ol → md', M('<ul><li>a<ol><li>b</li><li>c</li></ol></li><li>d</li></ul>'), '- a\n 1. b\n 2. c\n- d');
|
|
eq('3-level → md', M('<ul><li>a<ul><li>b<ul><li>c</li></ul></li></ul></li><li>d</li></ul>'), '- a\n - b\n - c\n- d');
|
|
eq('ul>ul rt', rt('- a\n - b\n - c\n- d'), '- a\n - b\n - c\n- d');
|
|
eq('ol>ol rt', rt('1. a\n 1. b\n 1. c\n2. d'), '1. a\n 1. b\n 2. c\n2. d');
|
|
eq('ul>ol rt', rt('- a\n 1. b\n 2. c\n- d'), '- a\n 1. b\n 2. c\n- d');
|
|
eq('3-level rt', rt('- a\n - b\n - c\n- d'), '- a\n - b\n - c\n- d');
|
|
|
|
// ── 17. Tables with nested markdown ─────────────────────
|
|
has('td bold', H('| h |\n|---|\n| **b** |'), '<td><strong>b</strong></td>');
|
|
has('td italic', H('| h |\n|---|\n| *i* |'), '<td><em>i</em></td>');
|
|
has('td code', H('| h |\n|---|\n| `c` |'), '<td><code>c</code></td>');
|
|
has('td link', H('| h |\n|---|\n| [t](u) |'), '<td><a href="u">t</a></td>');
|
|
has('td bold+italic', H('| h |\n|---|\n| ***bi*** |'), '<td><em><strong>bi</strong></em></td>');
|
|
has('td bold>italic', H('| h |\n|---|\n| **a *b* c** |'), '<strong>a <em>b</em> c</strong>');
|
|
has('td link>bold', H('| h |\n|---|\n| [**t**](u) |'), '<a href="u"><strong>t</strong></a>');
|
|
has('td link>code', H('| h |\n|---|\n| [`c`](u) |'), '<a href="u"><code>c</code></a>');
|
|
has('multi-cell bold+italic', H('| **a** | *b* |\n|---|---|\n| `c` | [d](e) |'), '<strong>a</strong>');
|
|
has('multi-cell code+link', H('| **a** | *b* |\n|---|---|\n| `c` | [d](e) |'), '<a href="e">d</a>');
|
|
eq('td bold → md', M('<table><thead><tr><th>h</th></tr></thead><tbody><tr><td><strong>b</strong></td></tr></tbody></table>'), '| h |\n| --- |\n| **b** |');
|
|
eq('td italic → md', M('<table><thead><tr><th>h</th></tr></thead><tbody><tr><td><em>i</em></td></tr></tbody></table>'), '| h |\n| --- |\n| *i* |');
|
|
eq('td code → md', M('<table><thead><tr><th>h</th></tr></thead><tbody><tr><td><code>c</code></td></tr></tbody></table>'), '| h |\n| --- |\n| `c` |');
|
|
eq('td link → md', M('<table><thead><tr><th>h</th></tr></thead><tbody><tr><td><a href="u">t</a></td></tr></tbody></table>'), '| h |\n| --- |\n| [t](u) |');
|
|
eq('td bold rt', rt('| h |\n|---|\n| **b** |'), '| h |\n| --- |\n| **b** |');
|
|
eq('td italic rt', rt('| h |\n|---|\n| *i* |'), '| h |\n| --- |\n| *i* |');
|
|
eq('td code rt', rt('| h |\n|---|\n| `c` |'), '| h |\n| --- |\n| `c` |');
|
|
eq('td link rt', rt('| h |\n|---|\n| [t](u) |'), '| h |\n| --- |\n| [t](u) |');
|
|
eq('td bold+italic rt', rt('| h |\n|---|\n| ***bi*** |'), '| h |\n| --- |\n| ***bi*** |');
|
|
eq('td link>bold rt', rt('| h |\n|---|\n| [**t**](u) |'), '| h |\n| --- |\n| [**t**](u) |');
|
|
eq('multi-cell rt', rt('| **a** | *b* |\n|---|---|\n| `c` | [d](e) |'), '| **a** | *b* |\n| --- | --- |\n| `c` | [d](e) |');
|
|
|
|
// ── 18. inlineTag() factory ─────────────────────────────
|
|
const strikethrough = dom.window.inlineTag({
|
|
name: 'strikethrough',
|
|
delimiter: '~~',
|
|
htmlTag: 'del',
|
|
aliases: 'S,STRIKE',
|
|
precedence: 45,
|
|
});
|
|
const customInline = new dom.window.HopDown({
|
|
tags: { ...dom.window.defaultTags, 'DEL,S,STRIKE': strikethrough },
|
|
});
|
|
eq('factory: md→html', customInline.toHTML('~~struck~~'), '<p><del>struck</del></p>');
|
|
has('factory: html→md', customInline.toMarkdown('<p><del>struck</del></p>'), '~~struck~~');
|
|
eq('factory: round-trip', customInline.toMarkdown(customInline.toHTML('~~struck~~')), '~~struck~~');
|
|
has('factory: mixed with bold', customInline.toHTML('**bold** and ~~struck~~'), '<del>struck</del>');
|
|
has('factory: mixed with bold', customInline.toHTML('**bold** and ~~struck~~'), '<strong>bold</strong>');
|
|
eq('factory: non-recursive', dom.window.inlineTag({
|
|
name: 'test',
|
|
delimiter: '%%',
|
|
htmlTag: 'mark',
|
|
recursive: false,
|
|
}).toHTML({ content: '<b>x</b>', raw: '', consumed: 0 }, { inline: s => s, block: s => s, children: n => '', node: n => '' }),
|
|
'<mark><b>x</b></mark>');
|
|
|
|
// ── 19. Custom block tag ────────────────────────────────
|
|
const spoiler = {
|
|
name: 'spoiler',
|
|
match: (context) => {
|
|
if (!/^\|{3,}/.test(context.lines[context.index])) return null;
|
|
const content = [];
|
|
let i = context.index + 1;
|
|
while (i < context.lines.length && !/^\|{3,}/.test(context.lines[i])) content.push(context.lines[i++]);
|
|
return { content: content.join('\n'), raw: '', consumed: i + 1 - context.index };
|
|
},
|
|
toHTML: (token, convert) => '<details><summary>Spoiler</summary>' + convert.block(token.content) + '</details>',
|
|
selector: 'DETAILS',
|
|
toMarkdown: (element, convert) => '\n\n|||\n' + convert.children(element).trim() + '\n|||\n\n',
|
|
};
|
|
const customBlock = new dom.window.HopDown({
|
|
tags: { 'DETAILS': spoiler, ...dom.window.defaultTags },
|
|
});
|
|
has('custom block: md→html', customBlock.toHTML('|||\nhidden\n|||'), '<details>');
|
|
has('custom block: content', customBlock.toHTML('|||\nhidden\n|||'), 'hidden');
|
|
has('custom block: html→md', customBlock.toMarkdown('<details><summary>Spoiler</summary><p>hidden</p></details>'), '|||');
|
|
has('custom block: nested md', customBlock.toHTML('|||\n**bold** inside\n|||'), '<strong>bold</strong>');
|
|
|
|
// ── 20. HopDown({ exclude }) ────────────────────────────
|
|
const noTables = new dom.window.HopDown({ exclude: ['table'] });
|
|
// With table excluded, pipe lines fall through to paragraph but isBlockStart
|
|
// still detects table-like patterns, so lines are split across paragraphs.
|
|
has('exclude: table not rendered', noTables.toHTML('| a | b |\n|---|---|\n| 1 | 2 |'), '<p>');
|
|
not('exclude: no table tag', noTables.toHTML('| a | b |\n|---|---|\n| 1 | 2 |'), '<table>');
|
|
has('exclude: bold still works', noTables.toHTML('**bold**'), '<strong>bold</strong>');
|
|
|
|
const noCode = new dom.window.HopDown({ exclude: ['code'] });
|
|
eq('exclude: code not processed', noCode.toHTML('`code`'), '<p>`code`</p>');
|
|
has('exclude: bold still works', noCode.toHTML('**bold**'), '<strong>bold</strong>');
|
|
|
|
// ── 21. Collision detection: delimiter ───────────────────
|
|
let threw = false;
|
|
try {
|
|
const bad = dom.window.inlineTag({ name: 'bad', delimiter: '*', htmlTag: 'span', precedence: 10 });
|
|
new dom.window.HopDown({ tags: { ...dom.window.defaultTags, 'SPAN': bad } });
|
|
} catch (e) {
|
|
threw = true;
|
|
}
|
|
eq('delimiter collision throws', String(threw), 'true');
|
|
|
|
threw = false;
|
|
try {
|
|
// Same delimiter, higher precedence than existing — should throw
|
|
const bad = dom.window.inlineTag({ name: 'bad', delimiter: '**', htmlTag: 'span', precedence: 60 });
|
|
new dom.window.HopDown({ tags: { ...dom.window.defaultTags, 'SPAN': bad } });
|
|
} catch (e) {
|
|
threw = true;
|
|
}
|
|
eq('duplicate delimiter collision throws', String(threw), 'true');
|
|
|
|
// ── 22. Collision detection: selector ───────────────────
|
|
threw = false;
|
|
try {
|
|
const dup = { name: 'dup', match: () => null, toHTML: () => '', selector: 'STRONG', toMarkdown: () => '' };
|
|
new dom.window.HopDown({ tags: { ...dom.window.defaultTags, 'STRONG': dup } });
|
|
} catch (e) {
|
|
threw = true;
|
|
}
|
|
eq('selector collision throws', String(threw), 'true');
|
|
|
|
// ── 23. Precedence ordering ─────────────────────────────
|
|
// Longer delimiter with lower precedence should win
|
|
const tilde = dom.window.inlineTag({ name: 'tilde', delimiter: '~', htmlTag: 's', precedence: 45 });
|
|
const doubleTilde = dom.window.inlineTag({ name: 'doubleTilde', delimiter: '~~', htmlTag: 'del', precedence: 35 });
|
|
const precTest = new dom.window.HopDown({
|
|
tags: { ...dom.window.defaultTags, 'S': tilde, 'DEL': doubleTilde },
|
|
});
|
|
has('precedence: ~~ matches before ~', precTest.toHTML('~~struck~~'), '<del>struck</del>');
|
|
has('precedence: ~ still works', precTest.toHTML('~light~'), '<s>light</s>');
|
|
|
|
// Valid: longer delimiter has lower precedence
|
|
threw = false;
|
|
try {
|
|
const short = dom.window.inlineTag({ name: 'short', delimiter: '~', htmlTag: 's', precedence: 50 });
|
|
const long = dom.window.inlineTag({ name: 'long', delimiter: '~~', htmlTag: 'del', precedence: 40 });
|
|
new dom.window.HopDown({ tags: { ...dom.window.defaultTags, 'S': short, 'DEL': long } });
|
|
} catch (e) {
|
|
threw = true;
|
|
}
|
|
eq('valid precedence does not throw', String(threw), 'false');
|
|
|
|
// Invalid: longer delimiter has higher precedence
|
|
threw = false;
|
|
try {
|
|
const short = dom.window.inlineTag({ name: 'short', delimiter: '~', htmlTag: 's', precedence: 30 });
|
|
const long = dom.window.inlineTag({ name: 'long', delimiter: '~~', htmlTag: 'del', precedence: 50 });
|
|
new dom.window.HopDown({ tags: { ...dom.window.defaultTags, 'S': short, 'DEL': long } });
|
|
} catch (e) {
|
|
threw = true;
|
|
}
|
|
eq('invalid precedence throws', String(threw), 'true');
|
|
|
|
// ── Results ─────────────────────────────────────────────
|
|
const total = passed + failed;
|
|
console.log(`\n${passed}/${total} passed (${Math.round(100 * passed / total)}%) — ${failed} failed`);
|
|
if (errors.length) {
|
|
console.log('\nFailed:');
|
|
errors.forEach(e => console.log(` • ${e}`));
|
|
}
|
|
process.exit(failed > 0 ? 1 : 0);
|