ribbit/test/test_hopdown.js
gsb df49ce7545 Single ribbit namespace instead of window globals
Use esbuild --global-name=ribbit to expose a single namespace.
2026-04-29 01:40:18 +00:00

418 lines
24 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 — esbuild IIFE assigns to var ribbit,
// but eval in jsdom doesn't attach vars to window, so we patch it.
const bundle = fs.readFileSync(path.join(__dirname, '..', 'dist', 'ribbit', 'ribbit.js'), 'utf8');
dom.window.eval(bundle.replace('var ribbit =', 'window.ribbit ='));
const hopdown = new dom.window.ribbit.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```'), '&lt;div&gt;');
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'), '&amp;');
has('html in code', H('`<div>`'), '&lt;div&gt;');
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.ribbit.inlineTag({
name: 'strikethrough',
delimiter: '~~',
htmlTag: 'del',
aliases: 'S,STRIKE',
precedence: 45,
});
const customInline = new dom.window.ribbit.HopDown({
tags: { ...dom.window.ribbit.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.ribbit.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>&lt;b&gt;x&lt;/b&gt;</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.ribbit.HopDown({
tags: { 'DETAILS': spoiler, ...dom.window.ribbit.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.ribbit.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.ribbit.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.ribbit.inlineTag({ name: 'bad', delimiter: '*', htmlTag: 'span', precedence: 10 });
new dom.window.ribbit.HopDown({ tags: { ...dom.window.ribbit.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.ribbit.inlineTag({ name: 'bad', delimiter: '**', htmlTag: 'span', precedence: 60 });
new dom.window.ribbit.HopDown({ tags: { ...dom.window.ribbit.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.ribbit.HopDown({ tags: { ...dom.window.ribbit.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.ribbit.inlineTag({ name: 'tilde', delimiter: '~', htmlTag: 's', precedence: 45 });
const doubleTilde = dom.window.ribbit.inlineTag({ name: 'doubleTilde', delimiter: '~~', htmlTag: 'del', precedence: 35 });
const precTest = new dom.window.ribbit.HopDown({
tags: { ...dom.window.ribbit.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.ribbit.inlineTag({ name: 'short', delimiter: '~', htmlTag: 's', precedence: 50 });
const long = dom.window.ribbit.inlineTag({ name: 'long', delimiter: '~~', htmlTag: 'del', precedence: 40 });
new dom.window.ribbit.HopDown({ tags: { ...dom.window.ribbit.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.ribbit.inlineTag({ name: 'short', delimiter: '~', htmlTag: 's', precedence: 30 });
const long = dom.window.ribbit.inlineTag({ name: 'long', delimiter: '~~', htmlTag: 'del', precedence: 50 });
new dom.window.ribbit.HopDown({ tags: { ...dom.window.ribbit.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);