From 3cbe206832047389cf243c57ef788bc0931fcbbd Mon Sep 17 00:00:00 2001 From: Michael Lam Date: Sat, 16 May 2026 02:12:11 -0700 Subject: [PATCH] fix: keep markdown tables block-level --- CHANGELOG.md | 4 ++++ static/ui.js | 7 +++++-- tests/test_renderer_js_behaviour.py | 31 +++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa25bab..ae569eec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- **PR #2375** by @Michaelyklam (closes #2374) — Markdown tables rendered by chat messages now stay as block-level `` elements instead of being wrapped in paragraph tags by the renderer's final paragraph pass. This keeps CommonMark-style pipe tables visible as tables across browsers. + ## [v0.51.74] — 2026-05-16 — Release AX (stage-367 — 4-PR safe-lane batch — #2362 table-cell spacing + #2363 run-state-consistency RFC + #2365 custom_providers list-format + #2367 settings sidebar i18n) ### Added diff --git a/static/ui.js b/static/ui.js index c050811c..253dd86a 100644 --- a/static/ui.js +++ b/static/ui.js @@ -2614,7 +2614,10 @@ function renderMd(raw){ const parseHeader=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>``).join(''); const header=`${parseHeader(rows[0])}`; const body=rows.slice(2).map(r=>`${parseRow(r)}`).join(''); - return `
${inlineMd(c.trim())}
${header}${body}
`; + // Surround with blank lines so the final paragraph splitter treats the + // generated table as its own block even when the regex consumes one of the + // markdown block's trailing newlines. + return `\n\n${header}${body}
\n\n`; }); // #487: Outer image pass — handles ![alt](url) in plain paragraphs (outside tables/lists). // Runs AFTER the table pass (images in table cells are handled by inlineMd() above). @@ -2757,7 +2760,7 @@ function renderMd(raw){ return '\x00E'+(_pre_stash.length-1)+'\x00'; }); const parts=s.split(/\n{2,}/); - s=parts.map(p=>{p=p.trim();if(!p)return '';if(/^<(h[1-6]|ul|ol|pre|hr|blockquote)|^\x00[EQ]/.test(p))return p;return `

${p.replace(/\n/g,'
')}

`;}).join('\n'); + s=parts.map(p=>{p=p.trim();if(!p)return '';if(/^<(h[1-6]|ul|ol|table|pre|hr|blockquote)|^\x00[EQ]/.test(p))return p;return `

${p.replace(/\n/g,'
')}

`;}).join('\n'); s=s.replace(/\x00E(\d+)\x00/g,(_,i)=>_pre_stash[+i]); // ── Restore MEDIA stash → inline images or download links ───────────────── s=s.replace(/\x00D(\d+)\x00/g,(_,i)=>{ diff --git a/tests/test_renderer_js_behaviour.py b/tests/test_renderer_js_behaviour.py index 22a831b7..509b1d72 100644 --- a/tests/test_renderer_js_behaviour.py +++ b/tests/test_renderer_js_behaviour.py @@ -187,6 +187,37 @@ class TestRendererSanitization: class TestCommonLLMShapes: + def test_commonmark_table_is_not_wrapped_in_paragraph(self, driver_path): + src = ( + "| 升级时段 | 人数 |\n" + "|---------|------|\n" + "| 5/15(发布当天) | ~30 人 |\n" + "| 5/16(今天) | ~10 人 |" + ) + out = _render(driver_path, src) + assert "" in out + assert "" in out + assert "" in out + assert "" in out + assert "

Before the table.

" in out + assert "
升级时段5/15(发布当天)~10 人
" in out + assert "

After the table.

" in out + assert "

" not in out + def test_strikethrough_outside_quote(self, driver_path): out = _render(driver_path, "This was ~~outdated~~ but is now fine.") assert "outdated" in out