/** * Hand-written regression suite for the markdown engine. * * Covers the three reported bugs (emphasis across a line break, single * newline must not start a new paragraph, wrapped list-item lines must stay * in the item) plus the security posture (raw HTML neutralised, URL scheme * filtering, entity smuggling) and edge cases (CRLF, empty input, tight vs * loose lists, setext headings). * * Usage: qjs tests/markdown/regressions.js */ import * as std from 'std'; import { installShim } from './shim.js'; function fail(msg) { std.err.puts(msg + '\n'); std.exit(1); } installShim(); (0, eval)(std.loadFile('js/statum.js')); var render = globalThis.statum._testMarkdown; if (typeof render !== 'function') fail('statum._testMarkdown is not exposed'); var passed = 0, failed = 0; function eq(name, input, expected) { var got; try { got = render(input); } catch (e) { got = '<>'; } if (got === expected) { passed++; } else { failed++; print('FAIL: ' + name); print(' input: ' + JSON.stringify(input)); print(' expected: ' + JSON.stringify(expected)); print(' got: ' + JSON.stringify(got)); } } function ok(name, input, test, description) { var got; try { got = render(input); } catch (e) { got = '<>'; } if (test(got)) { passed++; } else { failed++; print('FAIL: ' + name + (description ? ' — ' + description : '')); print(' input: ' + JSON.stringify(input)); print(' got: ' + JSON.stringify(got)); } } // --------------------------------------------------------------------------- // The three reported bugs // --------------------------------------------------------------------------- eq('emphasis wraps across a newline', '**bold text\nthat wraps**\n', '

bold text\nthat wraps

\n'); eq('single newline is a soft break, not a new paragraph', 'one\ntwo\n', '

one\ntwo

\n'); eq('paragraphs only split on blank lines', 'aaa\nbbb\n\nccc\nddd\n', '

aaa\nbbb

\n

ccc\nddd

\n'); eq('wrapped line stays inside the list item', '- item one\n continued line\n- item two\n', '\n'); eq('wrapped line in ordered list stays in the item', '1. first\n wrapped\n2. second\n', '
    \n
  1. first\nwrapped
  2. \n
  3. second
  4. \n
\n'); // --------------------------------------------------------------------------- // Block constructs // --------------------------------------------------------------------------- eq('ATX headings', '# Title\n###### Deep\n', '

Title

\n
Deep
\n'); eq('setext headings', 'Title\n=====\nSub\n---\n', '

Title

\n

Sub

\n'); eq('thematic break', '---\n', '
\n'); eq('fenced code with language', '```js\nvar x = 1;\n```\n', '
var x = 1;\n
\n'); eq('tilde fence', '~~~\nplain\n~~~\n', '
plain\n
\n'); eq('indented code', ' code\n', '
code\n
\n'); eq('blockquote with lazy continuation', '> quoted\nlazy\n', '
\n

quoted\nlazy

\n
\n'); eq('tight list stays tight', '- a\n- b\n', '\n'); eq('loose list gets paragraphs', '- a\n\n- b\n', '\n'); eq('nested list', '- a\n - b\n', '\n'); eq('ordered list start attribute', '3. c\n4. d\n', '
    \n
  1. c
  2. \n
  3. d
  4. \n
\n'); // --------------------------------------------------------------------------- // Inlines // --------------------------------------------------------------------------- eq('code span', 'a `code` b\n', '

a code b

\n'); eq('code span with backslash inside', '``a\\`b``\n', '

a\\`b

\n'); eq('backslash escape', '\\*not em\\*\n', '

*not em*

\n'); eq('named entity', '© 2024\n', '

\u00A9 2024

\n'); eq('numeric entity', '#\n', '

#

\n'); eq('invalid entity stays literal', '&madeup;\n', '

&madeup;

\n'); eq('intraword emphasis with underscores is not emphasis', 'foo_bar_baz\n', '

foo_bar_baz

\n'); eq('intraword emphasis with asterisks', 'foo*bar*baz\n', '

foobarbaz

\n'); eq('triple emphasis', '***both***\n', '

both

\n'); eq('hard break with two spaces', 'foo \nbar\n', '

foo
\nbar

\n'); eq('hard break with backslash', 'foo\\\nbar\n', '

foo
\nbar

\n'); eq('inline link with title', '[t](/u "title")\n', '

t

\n'); eq('image', '![alt](/img.png "cap")\n', '

alt

\n'); eq('reference link', '[t][ref]\n\n[ref]: /u "ti"\n', '

t

\n'); eq('shortcut reference link', '[ref]\n\n[ref]: /u\n', '

ref

\n'); eq('autolink http', '\n', '

https://example.com/

\n'); eq('autolink mail', '\n', '

mail@example.com

\n'); // --------------------------------------------------------------------------- // Security posture // --------------------------------------------------------------------------- ok('script tag is neutralised', '\n', function (h) { return h.indexOf('= 0; }); ok('img onerror is neutralised', '\n', function (h) { return h.indexOf('= 0; }, 'the tag must render as escaped text, not markup'); ok('javascript: link href is replaced', '[click](javascript:alert(1))\n', function (h) { return h.indexOf('javascript:') < 0; }, 'javascript: URLs must be filtered'); ok('data: link href is replaced', '[x](data:text/html,