"""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, render_expr: str = "mod.mdToHtml(input)", with_katex: bool = False): script = textwrap.dedent( r""" import fs from 'node:fs'; globalThis.window = { location: { origin: 'http://localhost' }, katex: null }; if (__WITH_KATEX__) { // Minimal stand-in for the CDN katex global: wraps the source so tests // can assert what was (or wasn't) handed to KaTeX. const katexStub = { renderToString(src, opts) { const display = !!(opts && opts.displayMode); return `${src}`; }, }; globalThis.window.katex = katexStub; globalThis.katex = katexStub; } globalThis.document = { readyState: 'loading', addEventListener() {}, createElement(tag) { if (tag !== 'template') throw new Error(`unsupported element: ${tag}`); return { _html: '', content: { querySelectorAll() { return []; } }, set innerHTML(value) { this._html = value; }, get innerHTML() { return this._html; }, }; }, }; globalThis.MutationObserver = class { observe() {} }; let source = fs.readFileSync('./static/js/markdown.js', 'utf8'); source = source.replace( /import uiModule from ['"]\.\/ui\.js['"];/, '' ); source = source.replace( /import \{ splitTableRow \} from ['"]\.\/markdown\/tableRow\.js['"];/, `function splitTableRow(row) { return (row || '').replace(/^\\s*\\|/, '').replace(/\\|\\s*$/, '').split('|').map(c => c.trim()); }` ); // markdown.js imports the emoji-shortcode helpers relatively (issue #345), // which a data: URL module can't resolve. Inline the REAL helpers (minus // their export keywords) so the renderer's shortcode pass behaves exactly // as it does in the browser. const emojiSource = fs.readFileSync('./static/js/emojiShortcodes.js', 'utf8') .replace(/^export default .*$/m, '') .replace(/export const /g, 'const ') .replace(/export function /g, 'function '); source = source.replace( /import \{ replaceEmojiShortcodes, hasEmojiShortcode \} from ['"]\.\/emojiShortcodes\.js['"];/, () => emojiSource ); source = source.replace( /var 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: __RENDER_EXPR__ })); """ ).replace("__RENDER_EXPR__", render_expr).replace( "__WITH_KATEX__", "true" if with_katex else "false" ) 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("
<svg></svg>" in html
assert 'Themed \n```",
render_expr="""(
globalThis.document.documentElement = {},
globalThis.getComputedStyle = () => ({ getPropertyValue: name => ({
'--bg': '#101214', '--panel': '#202428', '--fg': '#f1f5f9',
'--border': '#475569', '--red': '#22d3ee', '--color-muted': '#94a3b8',
'--color-success': '#4ade80', '--color-warning': '#facc15'
})[name] || '' }),
mod.mdToHtml(input)
)""",
)
assert '--bg:#101214' in html
assert '--panel:#202428' in html
assert '--fg:#f1f5f9' in html
assert '--accent:#22d3ee' in html
def test_more_list_expanders_render_as_clickable_links(node_available):
html = _run_markdown_case(
"Here are your notes:\n"
"\n"
"[...and 1 more notes](#notes-more-abc123)\n\n"
"Available skills:\n"
"\n"
"[...and 1 more skills](#skills-more-def456)\n\n"
"Memory:\n"
"\n"
"[...and 1 more saved memories](#memories-more-ghi789)\n\n"
"Calendar:\n"
"\n"
"[...and 1 more events](#events-more-jkl012)\n\n"
"Chats:\n"
"\n"
"[...and 1 more chats](#sessions-more-mno345)"
)
assert 'href="#notes-more-abc123"' in html
assert 'href="#skills-more-def456"' in html
assert 'href="#memories-more-ghi789"' in html
assert 'href="#events-more-jkl012"' in html
assert 'href="#sessions-more-mno345"' in html
assert "ody-more-" not in html
assert "") == 2
assert 'Drafts
' in html
assert 'href="#skill-last-published" class="chat-link"' in html
assert 'href="#skill-first-draft" class="chat-link"' in html
def test_session_titles_with_escaped_brackets_remain_clickable(node_available):
html = _run_markdown_case(
r"- [\[domain-audit\] theme](#session-ae3ee537-0a13-443f-a456-91e6fd061edf) "
"(last active just now)"
)
assert 'href="#session-ae3ee537-0a13-443f-a456-91e6fd061edf"' in html
assert "[domain-audit] theme" in html
assert "ody-math-pending" not in html
def test_table_separator_row_not_rendered_as_data(node_available):
html = _run_markdown_case("| A | B |\n|---|---|\n| 1 | 2 |")
assert html.count("") == 2
assert "thought\ninternal reasoningFinal answer.",
"mod.processWithThinking(input)",
)
assert "thinking-section" in html
assert "internal reasoning" in html
assert "Final answer." in html
assert "<|channel>" not in html
assert "<|channel>" not in html
def test_process_with_thinking_strips_empty_gemma4_thought_channel(node_available):
html = _run_markdown_case(
"<|channel>thought\nFinal answer.",
"mod.processWithThinking(input)",
)
assert "thinking-section" not in html
assert "Final answer." in html
assert "<|channel>" not in html
assert "<|channel>" not in html
def test_process_with_thinking_unwraps_gemma4_response_channel(node_available):
html = _run_markdown_case(
"<|channel>thought\ninternal reasoning<|channel>response\nFinal answer.",
"mod.processWithThinking(input)",
)
assert "thinking-section" in html
assert "internal reasoning" in html
assert "Final answer." in html
assert "<|channel>" not in html
assert "<|channel>" not in html
def test_extract_thinking_blocks_handles_thought_tag(node_available):
result = _run_markdown_case(
"internal reasoning Final answer.",
"mod.extractThinkingBlocks(input)",
)
assert result["thinkingBlocks"] == ["internal reasoning"]
assert result["content"] == "Final answer."
def test_url_inside_inline_code_is_not_autolinked(node_available):
# A URL inside a backtick span is preceded by a space, so the bare-URL
# autolink used to wrap it in an tag (then swap it for an
# ___ALLOWED_HTML_ placeholder), corrupting the command shown to the user.
html = _run_markdown_case("Run `$j = irm http://127.0.0.1:3000/x` to fetch.")
assert "$j = irm http://127.0.0.1:3000/x" in html
assert "___ALLOWED_HTML_" not in html
assert "irm" in html
assert 'href="https://example.com/page"' in html
def test_inline_code_content_is_html_escaped(node_available):
# Inline code is now extracted before the global escape pass, so it must be
# escaped at extraction time (matching the fenced-code-block handling).
html = _run_markdown_case("Render `$1 & 'q'` literally.")
assert "<b>$1 & 'q'</b>" in html
assert "" not in html
def test_fenced_code_keeps_dollar_ampersand(node_available):
# Issue #5663: the block-restore pass used a string replacement, so `$&` in a
# restored block was read as "the matched text" and re-inserted the
# placeholder. `perl -pe 's/world/$& again/'` rendered as
# "s/world/___CODE_BLOCK_0___amp; again/" — the trailing "amp;" is the orphan
# left behind after `$&` consumed the `$&` of the escaped `$&`.
html = _run_markdown_case(
"```sh\necho \"hello world\" | perl -pe 's/world/$& again/'\n```"
)
assert "___CODE_BLOCK_" not in html
assert "s/world/$& again/" in html
assert "amp; again" not in html.replace("$& again", "")
def test_fenced_code_keeps_dollar_backtick_and_quote(node_available):
# `` $` `` and `$'` splice the text before/after the placeholder into the
# block. Unlike `$&` these leave no placeholder behind — the characters just
# vanish — so assert the content survives verbatim.
html = _run_markdown_case("```sh\nsed \"s/$`/x/\" && sed \"s/$'/y/\"\n```")
assert "___CODE_BLOCK_" not in html
assert "s/$`/x/" in html
assert "s/$'/y/" in html
def test_fenced_code_keeps_double_dollar(node_available):
# `$$` collapsed to a single `$` in the restored block.
html = _run_markdown_case('```sh\necho "$$USD and $$"\n```')
assert "$$USD and $$" in html
def test_mermaid_block_keeps_dollar_ampersand(node_available):
# The mermaid restore site had the same hazard: a node label containing `$&`
# re-inserted the ___MERMAID_BLOCK_n___ placeholder into the diagram source,
# which then fails to parse. The math and allowed-HTML sites are fixed the
# same way; they need KaTeX/sanitizer conditions this harness doesn't set up.
html = _run_markdown_case('```mermaid\ngraph TD; A["$&"] --> B;\n```')
assert "___MERMAID_BLOCK_" not in html
assert "$&" in html
def test_currency_dollar_amounts_are_not_rendered_as_math(node_available):
# "$5 to $10" used to pair the two dollar signs as inline-math delimiters
# and render "5 to" through KaTeX. Pandoc-style rules now reject it: the
# closing $ is preceded by a space and followed by a digit.
html = _run_markdown_case(
"The price rose from $5 to $10 overnight.", with_katex=True
)
assert 'class="katex"' not in html
assert "$5" in html
assert "$10" in html
def test_inline_math_still_renders_through_katex(node_available):
html = _run_markdown_case("Pythagoras: $x^2 + y^2 = z^2$ holds.", with_katex=True)
assert 'x^2 + y^2 = z^2' in html
assert "$" not in html
def test_display_math_still_renders_through_katex(node_available):
html = _run_markdown_case("$$\\frac{a}{b}$$", with_katex=True)
assert 'data-display="true"' in html
assert "$$" not in html
def test_dotted_python_import_paths_are_not_autolinked(node_available):
html = _run_markdown_case(
"from imblearn.combine import SMOTETomek\n"
"from sklearn.metrics import f1_score\n"
"from sklearn.compose import ColumnTransformer\n\n"
"See example.com/docs for normal domain autolinking."
)
assert "___ALLOWED_HTML_" not in html
assert "imblearn.combine" in html
assert "sklearn.metrics" in html
assert "sklearn.compose" in html
assert 'href="https://imblearn.com' not in html
assert 'href="https://sklearn.me' not in html
assert 'href="https://example.com/docs"' in html