fix(markdown): preserve URLs inside inline code spans (#4681)

Inline backtick spans were converted to <code> only at the end of
mdToHtml, after the bare-URL autolink and <a>/allowed-HTML passes. A URL
inside inline code is preceded by a space, so the autolink wrapped it in
an <a> tag and swapped it for an ___ALLOWED_HTML_ placeholder, corrupting
commands like `irm http://127.0.0.1:3000/x`.

Extract inline code into placeholders before the link passes, mirroring
the existing fenced-code-block handling, and restore them last so
placeholders carried inside restored <a> blocks resolve. Escape the code
at extraction time since it now bypasses the global escape pass.
This commit is contained in:
Ashvin
2026-06-22 20:53:55 +05:30
committed by GitHub
parent ca4973c41f
commit e812a29233
2 changed files with 52 additions and 6 deletions
+22 -6
View File
@@ -483,6 +483,7 @@ export function processWithThinking(text) {
export function mdToHtml(src, opts) {
const allowedHtmlBlocks = [];
const codeBlocks = [];
const inlineCodeBlocks = [];
const mermaidBlocks = [];
let s = (src ?? '');
@@ -521,6 +522,19 @@ export function mdToHtml(src, opts) {
return placeholder;
});
// Extract inline code spans before the link/autolink/HTML passes, mirroring
// the fenced-block handling above. A URL inside `inline code` (e.g.
// `irm http://127.0.0.1:3000/x`) is preceded by a space, so the bare-URL
// autolink matches it, wraps it in an <a> tag, and swaps that for an
// ___ALLOWED_HTML_ placeholder — corrupting the command. The old inline-code
// pass ran after those passes, too late to protect it.
s = s.replace(/`([^`]+?)`/g, (match, code) => {
if (code.startsWith('___CODE_BLOCK_') || code.startsWith('___MERMAID_BLOCK_')) return match;
const placeholder = `___INLINE_CODE_${inlineCodeBlocks.length}___`;
inlineCodeBlocks.push(`<code>${escapeHtml(code)}</code>`);
return placeholder;
});
// Repair common ways the agent mangles the entity-anchor convention
// (`[Name](#kind-<id>)`). Models reliably get the single-link case
// right but slip into other formats when listing many in a table.
@@ -678,12 +692,6 @@ export function mdToHtml(src, opts) {
return html;
});
// Inline code (but not placeholders)
s = s.replace(/`([^`]+?)`/g, (match, code) => {
if (code.startsWith('___CODE_BLOCK_') || code.startsWith('___ALLOWED_HTML_')) return match;
return `<code>${code}</code>`;
});
// Horizontal rules (must come before bold/italic to avoid * conflicts)
s = s.replace(/^(?:---|\*\*\*|___)\s*$/gm, '<hr>');
@@ -756,6 +764,14 @@ export function mdToHtml(src, opts) {
s = s.replace(`___CODE_BLOCK_${index}___`, block);
});
// Restore inline code spans last, so placeholders carried inside restored
// <a>/allowed-HTML blocks are resolved too. The function replacer keeps the
// escaped code literal — e.g. a shell snippet like `echo $1` is not treated
// as a regex back-reference.
inlineCodeBlocks.forEach((block, index) => {
s = s.replace(`___INLINE_CODE_${index}___`, () => block);
});
return _useSvgEmoji() ? svgifyEmoji(s, opts) : s;
}
+30
View File
@@ -170,6 +170,36 @@ def test_extract_thinking_blocks_handles_thought_tag(node_available):
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 <a> 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 "<code>$j = irm http://127.0.0.1:3000/x</code>" in html
assert "___ALLOWED_HTML_" not in html
assert "<a " not in html
assert 'href="http://127.0.0.1:3000/x"' not in html
def test_url_outside_inline_code_is_still_autolinked(node_available):
# Inline code must not disable autolinking for bare URLs elsewhere in the
# same line.
html = _run_markdown_case("Use `irm` then visit https://example.com/page now.")
assert "<code>irm</code>" 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 `<b>$1 & 'q'</b>` literally.")
assert "<code>&lt;b&gt;$1 &amp; &#39;q&#39;&lt;/b&gt;</code>" in html
assert "<b>" not in html
def test_dotted_python_import_paths_are_not_autolinked(node_available):
html = _run_markdown_case(
"from imblearn.combine import SMOTETomek\n"