diff --git a/static/ui.js b/static/ui.js index 5275551..33cc417 100644 --- a/static/ui.js +++ b/static/ui.js @@ -459,10 +459,13 @@ function renderMd(raw){ // Used inside list items and blockquotes where the text may already contain // HTML from the pre-pass → bold pipeline, so we cannot call esc() directly. function inlineMd(t){ + // Stash backtick code spans first so bold/italic never esc() their content + const _code_stash=[]; + t=t.replace(/`([^`\n]+)`/g,(_,x)=>{_code_stash.push(`${esc(x)}`);return `\x00C${_code_stash.length-1}\x00`;}); t=t.replace(/\*\*\*(.+?)\*\*\*/g,(_,x)=>`${esc(x)}`); t=t.replace(/\*\*(.+?)\*\*/g,(_,x)=>`${esc(x)}`); t=t.replace(/\*([^*\n]+)\*/g,(_,x)=>`${esc(x)}`); - t=t.replace(/`([^`\n]+)`/g,(_,x)=>`${esc(x)}`); + t=t.replace(/\x00C(\d+)\x00/g,(_,i)=>_code_stash[+i]); // Stash [label](url) links before autolink so the URL in href= is not re-linked const _link_stash=[]; t=t.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,(_,lb,u)=>{_link_stash.push(`${esc(lb)}`);return `\x00L${_link_stash.length-1}\x00`;}); diff --git a/tests/test_issue470.py b/tests/test_issue470.py index 30c075b..b6f156d 100644 --- a/tests/test_issue470.py +++ b/tests/test_issue470.py @@ -260,3 +260,54 @@ def test_js_source_sanitizes_quotes_in_href(): assert "%22" in UI_JS, ( "URL placed in href should have double-quotes percent-encoded via .replace to %22" ) + +# ── Code-inside-bold tests (pre-existing bug, fixed in same PR) ─────────────── + +def test_js_inlinemd_stashes_code_before_bold(): + """Fixed inlineMd() must stash backtick code spans before bold/italic processing.""" + assert '_code_stash' in UI_JS, ( + "inlineMd() should use _code_stash to protect backtick spans from bold/italic esc()" + ) + + +def test_code_inside_bold_renders_correctly(): + """Inline code inside bold text must render as ..., + not with escaped <code> tags visible on screen.""" + # This was the pre-existing bug: **`esc()`** → <code>esc()</code> + text = '**`esc()` on `href`**: breaks URLs' + # Simulate the fixed inlineMd() + code_stash = [] + t = text + t = re.sub(r'`([^`\n]+)`', + lambda m: (code_stash.append(f'{esc(m.group(1))}') or f'\x00C{len(code_stash)-1}\x00'), t) + t = re.sub(r'\*\*(.+?)\*\*', lambda m: f'{esc(m.group(1))}', t) + t = re.sub(r'\x00C(\d+)\x00', lambda m: code_stash[int(m.group(1))], t) + assert '<code>' not in t, ( + f"Code tags should not be HTML-escaped inside bold. Got: {t}" + ) + assert 'esc()' in t, ( + f"Code tags should render as elements inside bold. Got: {t}" + ) + assert '' in t, "Bold should still render" + + +def test_code_and_bold_mixed_no_escaping(): + """Bold text containing multiple backtick spans must render all code tags correctly.""" + cases = [ + ('**`esc()` on `href`**', '', 'esc()', 'href'), + ('***`code` in bold-italic***', '', 'code'), + ('`code` then **bold**', 'code', 'bold'), + ] + for args in cases: + text = args[0] + expected_fragments = args[1:] + code_stash = [] + t = text + t = re.sub(r'`([^`\n]+)`', + lambda m: (code_stash.append(f'{esc(m.group(1))}') or f'\x00C{len(code_stash)-1}\x00'), t) + t = re.sub(r'\*\*\*(.+?)\*\*\*', lambda m: f'{esc(m.group(1))}', t) + t = re.sub(r'\*\*(.+?)\*\*', lambda m: f'{esc(m.group(1))}', t) + t = re.sub(r'\x00C(\d+)\x00', lambda m: code_stash[int(m.group(1))], t) + assert '<code>' not in t, f"Escaped code tag in: {text!r} → {t}" + for frag in expected_fragments: + assert frag in t, f"Expected {frag!r} in output of {text!r}, got: {t}"