diff --git a/static/js/markdown.js b/static/js/markdown.js index 622a166..2d2f6ca 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -580,8 +580,9 @@ export function mdToHtml(src) { s = s.replace(/(?:^|\n)([\s\S]*?)(?=\n(?!)|$)/g, m => `
    ${m.trim().replace(/<\/?oli>/g, (t) => t === '' ? '
  1. ' : '
  2. ')}
`); // Unordered lists - s = s.replace(/^(?:- |\* )(.*)$/gm, '
  • $1
  • '); - s = s.replace(/(?:^|\n)(
  • [\s\S]*?)(?=\n(?!
  • )|$)/g, m => `
      ${m.trim()}
    `); + s = s.replace(/^(?:- |\* )(.*)$/gm, '$1'); + s = s.replace(/(^|\n)((?:[^\n]*<\/uli>(?:\n|$))+)/g, (_, prefix, block) => + `${prefix}
      ${block.trim().replace(/<\/?uli>/g, (t) => t === '' ? '
    • ' : '
    • ')}
    `); // Blockquotes s = s.replace(/^> (.*)$/gm, '$1'); diff --git a/tests/test_markdown_rendering_js.py b/tests/test_markdown_rendering_js.py new file mode 100644 index 0000000..f606fdd --- /dev/null +++ b/tests/test_markdown_rendering_js.py @@ -0,0 +1,82 @@ +"""Regression coverage for the browser markdown renderer.""" + +import json +import shutil +import subprocess +import textwrap +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parent.parent +_HAS_NODE = shutil.which("node") is not None + + +@pytest.fixture(scope="module") +def node_available(): + if not _HAS_NODE: + pytest.skip("node binary not on PATH") + + +def _run_markdown_case(markdown: str) -> str: + script = textwrap.dedent( + """ + import fs from 'node:fs'; + + globalThis.window = { location: { origin: 'http://localhost' }, katex: null }; + globalThis.document = { + readyState: 'loading', + addEventListener() {}, + }; + globalThis.MutationObserver = class { observe() {} }; + + let source = fs.readFileSync('./static/js/markdown.js', 'utf8'); + source = source.replace( + "import uiModule from './ui.js';\\n\\nvar escapeHtml = uiModule.esc;", + `var escapeHtml = (value) => String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''');` + ); + + const moduleUrl = 'data:text/javascript;base64,' + Buffer.from(source).toString('base64'); + const mod = await import(moduleUrl); + const input = JSON.parse(process.argv[1]); + console.log(JSON.stringify({ html: mod.mdToHtml(input) })); + """ + ) + result = subprocess.run( + ["node", "--input-type=module", "-e", script, json.dumps(markdown)], + cwd=_REPO, + capture_output=True, + timeout=15, + text=True, + ) + if result.returncode != 0: + raise AssertionError(f"node failed:\nSTDERR:\n{result.stderr}\nSTDOUT:\n{result.stdout}") + return json.loads(result.stdout.splitlines()[-1])["html"] + + +def test_ordered_lists_render_as_one_unwrapped_ol(node_available): + html = _run_markdown_case( + "Before\n\n" + "1. **Check against the home page** — that's the visual reference for how things should feel.\n" + "2. **Open DevTools** and inspect the element — check fonts, colors, and spacing against this guide.\n" + "3. **Flag it** — note the page, the section, what's wrong, and what CSS rule you suspect.\n" + "4. **Small fixes** — if you know the fix (e.g. wrong CSS variable, wrong font), go ahead and change it in the CSS Module file.\n" + "5. **Big changes** — Talk it through before making wide changes across many pages.\n\n" + "After" + ) + + assert html.count("
      ") == 1 + assert html.count("
    ") == 1 + assert html.count("
  • ") == 5 + assert "
      " not in html + assert "" not in html + assert "" not in html + assert "

        " not in html + assert "

      1. " not in html + assert "

        Before

        " in html + assert "

        After

        " in html