regressions.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /**
  2. * Hand-written regression suite for the markdown engine.
  3. *
  4. * Covers the three reported bugs (emphasis across a line break, single
  5. * newline must not start a new paragraph, wrapped list-item lines must stay
  6. * in the item) plus the security posture (raw HTML neutralised, URL scheme
  7. * filtering, entity smuggling) and edge cases (CRLF, empty input, tight vs
  8. * loose lists, setext headings).
  9. *
  10. * Usage: qjs tests/markdown/regressions.js
  11. */
  12. import * as std from 'std';
  13. import { installShim } from './shim.js';
  14. function fail(msg) {
  15. std.err.puts(msg + '\n');
  16. std.exit(1);
  17. }
  18. installShim();
  19. // Load order matters: statum-core.js creates `window.statum` (and the module
  20. // seam), statum-markdown.js registers the engine + its bindings through it.
  21. // Loading exactly these two proves the core+markdown cherry-pick.
  22. (0, eval)(std.loadFile('js/statum-core.js'));
  23. (0, eval)(std.loadFile('js/statum-markdown.js'));
  24. var render = globalThis.statum._testMarkdown;
  25. if (typeof render !== 'function') fail('statum._testMarkdown is not exposed');
  26. var passed = 0, failed = 0;
  27. function eq(name, input, expected) {
  28. var got;
  29. try {
  30. got = render(input);
  31. } catch (e) {
  32. got = '<<threw ' + e + '>>';
  33. }
  34. if (got === expected) {
  35. passed++;
  36. } else {
  37. failed++;
  38. print('FAIL: ' + name);
  39. print(' input: ' + JSON.stringify(input));
  40. print(' expected: ' + JSON.stringify(expected));
  41. print(' got: ' + JSON.stringify(got));
  42. }
  43. }
  44. function ok(name, input, test, description) {
  45. var got;
  46. try {
  47. got = render(input);
  48. } catch (e) {
  49. got = '<<threw ' + e + '>>';
  50. }
  51. if (test(got)) {
  52. passed++;
  53. } else {
  54. failed++;
  55. print('FAIL: ' + name + (description ? ' — ' + description : ''));
  56. print(' input: ' + JSON.stringify(input));
  57. print(' got: ' + JSON.stringify(got));
  58. }
  59. }
  60. // ---------------------------------------------------------------------------
  61. // The three reported bugs
  62. // ---------------------------------------------------------------------------
  63. eq('emphasis wraps across a newline',
  64. '**bold text\nthat wraps**\n',
  65. '<p><strong>bold text\nthat wraps</strong></p>\n');
  66. eq('single newline is a soft break, not a new paragraph',
  67. 'one\ntwo\n',
  68. '<p>one\ntwo</p>\n');
  69. eq('paragraphs only split on blank lines',
  70. 'aaa\nbbb\n\nccc\nddd\n',
  71. '<p>aaa\nbbb</p>\n<p>ccc\nddd</p>\n');
  72. eq('wrapped line stays inside the list item',
  73. '- item one\n continued line\n- item two\n',
  74. '<ul>\n<li>item one\ncontinued line</li>\n<li>item two</li>\n</ul>\n');
  75. eq('wrapped line in ordered list stays in the item',
  76. '1. first\n wrapped\n2. second\n',
  77. '<ol>\n<li>first\nwrapped</li>\n<li>second</li>\n</ol>\n');
  78. // ---------------------------------------------------------------------------
  79. // Block constructs
  80. // ---------------------------------------------------------------------------
  81. eq('ATX headings', '# Title\n###### Deep\n', '<h1>Title</h1>\n<h6>Deep</h6>\n');
  82. eq('setext headings', 'Title\n=====\nSub\n---\n', '<h1>Title</h1>\n<h2>Sub</h2>\n');
  83. eq('thematic break', '---\n', '<hr />\n');
  84. eq('fenced code with language', '```js\nvar x = 1;\n```\n',
  85. '<pre><code class="language-js">var x = 1;\n</code></pre>\n');
  86. eq('tilde fence', '~~~\nplain\n~~~\n', '<pre><code>plain\n</code></pre>\n');
  87. eq('indented code', ' code\n', '<pre><code>code\n</code></pre>\n');
  88. eq('blockquote with lazy continuation', '> quoted\nlazy\n',
  89. '<blockquote>\n<p>quoted\nlazy</p>\n</blockquote>\n');
  90. eq('tight list stays tight', '- a\n- b\n', '<ul>\n<li>a</li>\n<li>b</li>\n</ul>\n');
  91. eq('loose list gets paragraphs', '- a\n\n- b\n',
  92. '<ul>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n</li>\n</ul>\n');
  93. eq('nested list', '- a\n - b\n', '<ul>\n<li>a\n<ul>\n<li>b</li>\n</ul>\n</li>\n</ul>\n');
  94. eq('ordered list start attribute', '3. c\n4. d\n',
  95. '<ol start="3">\n<li>c</li>\n<li>d</li>\n</ol>\n');
  96. // ---------------------------------------------------------------------------
  97. // Inlines
  98. // ---------------------------------------------------------------------------
  99. eq('code span', 'a `code` b\n', '<p>a <code>code</code> b</p>\n');
  100. eq('code span with backslash inside', '``a\\`b``\n', '<p><code>a\\`b</code></p>\n');
  101. eq('backslash escape', '\\*not em\\*\n', '<p>*not em*</p>\n');
  102. eq('named entity', '&copy; 2024\n', '<p>\u00A9 2024</p>\n');
  103. eq('numeric entity', '&#35;\n', '<p>#</p>\n');
  104. eq('invalid entity stays literal', '&madeup;\n', '<p>&amp;madeup;</p>\n');
  105. eq('intraword emphasis with underscores is not emphasis', 'foo_bar_baz\n',
  106. '<p>foo_bar_baz</p>\n');
  107. eq('intraword emphasis with asterisks', 'foo*bar*baz\n',
  108. '<p>foo<em>bar</em>baz</p>\n');
  109. eq('triple emphasis', '***both***\n', '<p><em><strong>both</strong></em></p>\n');
  110. eq('hard break with two spaces', 'foo \nbar\n', '<p>foo<br />\nbar</p>\n');
  111. eq('hard break with backslash', 'foo\\\nbar\n', '<p>foo<br />\nbar</p>\n');
  112. eq('inline link with title', '[t](/u "title")\n',
  113. '<p><a href="/u" title="title">t</a></p>\n');
  114. eq('image', '![alt](/img.png "cap")\n',
  115. '<p><img src="/img.png" alt="alt" title="cap" /></p>\n');
  116. eq('reference link', '[t][ref]\n\n[ref]: /u "ti"\n',
  117. '<p><a href="/u" title="ti">t</a></p>\n');
  118. eq('shortcut reference link', '[ref]\n\n[ref]: /u\n',
  119. '<p><a href="/u">ref</a></p>\n');
  120. eq('autolink http', '<https://example.com/>\n',
  121. '<p><a href="https://example.com/">https://example.com/</a></p>\n');
  122. eq('autolink mail', '<mail@example.com>\n',
  123. '<p><a href="mailto:mail@example.com">mail@example.com</a></p>\n');
  124. // ---------------------------------------------------------------------------
  125. // Security posture
  126. // ---------------------------------------------------------------------------
  127. ok('script tag is neutralised', '<script>alert(1)</script>\n',
  128. function (h) { return h.indexOf('<script') < 0 && h.indexOf('&lt;script') >= 0; });
  129. ok('img onerror is neutralised', '<img src=x onerror=alert(1)>\n',
  130. function (h) { return h.indexOf('<img') < 0 && h.indexOf('&lt;img') >= 0; },
  131. 'the tag must render as escaped text, not markup');
  132. ok('javascript: link href is replaced', '[click](javascript:alert(1))\n',
  133. function (h) { return h.indexOf('javascript:') < 0; },
  134. 'javascript: URLs must be filtered');
  135. ok('data: link href is replaced', '[x](data:text/html,<script>)\n',
  136. function (h) { return h.indexOf('data:') < 0; });
  137. ok('javascript: image src is replaced', '![x](javascript:alert(1))\n',
  138. function (h) { return h.indexOf('javascript:') < 0; });
  139. ok('vbscript href is replaced', '[x](vbscript:msgbox)\n',
  140. function (h) { return h.indexOf('vbscript:') < 0; });
  141. eq('allowed schemes pass through', '[a](https://x) [b](http://x) [c](mailto:a@b) [d](ftp://x) /rel\n',
  142. '<p><a href="https://x">a</a> <a href="http://x">b</a> <a href="mailto:a@b">c</a> <a href="ftp://x">d</a> /rel</p>\n');
  143. ok('entity smuggling is decoded then escaped', '&#60;script&#62;alert(1)&#60;/script&#62;\n',
  144. function (h) { return h.indexOf('<script') < 0 && h.indexOf('&lt;script') >= 0; },
  145. 'numeric refs must decode to text, then be escaped on output');
  146. ok('hex entity smuggling is neutralised', '&#x3c;script&#x3e;\n',
  147. function (h) { return h.indexOf('<script') < 0 && h.indexOf('&lt;script') >= 0; });
  148. ok('uppercase scheme javascript is filtered', '[x](JAVASCRIPT:alert(1))\n',
  149. function (h) { return h.toLowerCase().indexOf('javascript:') < 0; });
  150. ok('title attribute is escaped', '[x](/u ""><script>")\n',
  151. function (h) { return h.indexOf('<script') < 0; });
  152. // ---------------------------------------------------------------------------
  153. // Edge cases
  154. // ---------------------------------------------------------------------------
  155. eq('empty input renders empty', '', '');
  156. eq('blank-only input renders empty', '\n\n\n', '');
  157. eq('CRLF line endings', 'a\r\nb\r\n\r\nc\r\n', '<p>a\nb</p>\n<p>c</p>\n');
  158. eq('lone CR line endings', 'a\rb\r\rc\r', '<p>a\nb</p>\n<p>c</p>\n');
  159. // CommonMark: NUL is replaced by U+FFFD before parsing
  160. eq('NUL bytes are replaced', 'a\u0000b\n', '<p>a\uFFFDb</p>\n');
  161. eq('heading with trailing hashes', '## Title ##\n', '<h2>Title</h2>\n');
  162. eq('link label case-insensitive reference', '[Foo]\n\n[foo]: /u\n',
  163. '<p><a href="/u">Foo</a></p>\n');
  164. print('regressions: ' + passed + ' passed, ' + failed + ' failed');
  165. std.exit(failed ? 1 : 0);