Chat models often emit GitHub/Slack-style :shortcode: text (e.g. 😊, 🎤) instead of the actual emoji. The renderer only converted real Unicode emoji to the monochrome line icons, so shortcodes rendered as literal text. Add a pure, browser-free shortcode->Unicode map (emojiShortcodes.js) and run it inside svgifyEmoji ahead of the existing Unicode->SVG pass, skipping <code>/<pre> so code stays literal. Covers ~430 common shortcodes plus common aliases (+1/thumbsup, etc.). Keep the conversion from touching anything it shouldn't: * Scope it to chat. mdToHtml/svgifyEmoji take a { shortcodes } option (default on); document and email body rendering (compose, export, preview) pass it as false so author-typed :shortcode: text stays literal. The Unicode->SVG pass still runs there exactly as before. * Only convert a :shortcode: that stands on its own. A word-boundary guard leaves embedded colon runs alone, so "1:100:2", "10:30:45", "16:9" and host:fire:port are never rewritten. Tests: extend the node-driven unit test with the boundary/false-positive cases, and fix the markdown-rendering test loader to resolve the new emojiShortcodes import.
162 lines
5.7 KiB
Python
162 lines
5.7 KiB
Python
"""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)"):
|
|
script = textwrap.dedent(
|
|
r"""
|
|
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['"];/,
|
|
''
|
|
);
|
|
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, '"')
|
|
.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)
|
|
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("<ol>") == 1
|
|
assert html.count("</ol>") == 1
|
|
assert html.count("<li>") == 5
|
|
assert "<ul>" not in html
|
|
assert "<oli>" not in html
|
|
assert "<uli>" not in html
|
|
assert "<p><ol>" not in html
|
|
assert "<p><li>" not in html
|
|
assert "<p>Before</p>" in html
|
|
assert "<p>After</p>" 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("<tr>") == 2
|
|
assert "<th" in html
|
|
assert "<td" in html
|
|
assert "---" not in html
|
|
|
|
|
|
def test_process_with_thinking_handles_gemma4_thought_channel(node_available):
|
|
html = _run_markdown_case(
|
|
"<|channel>thought\ninternal reasoning<channel|>Final 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\n<channel|>Final 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|><|channel>response\nFinal answer.<channel|>",
|
|
"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(
|
|
"<thought>internal reasoning</thought>Final answer.",
|
|
"mod.extractThinkingBlocks(input)",
|
|
)
|
|
|
|
assert result["thinkingBlocks"] == ["internal reasoning"]
|
|
assert result["content"] == "Final answer."
|