diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf82c4c..b0516d95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.580] — 2026-06-22 — Release UM (wrap markdown source previews) + +### Fixed + +- **Markdown source previews now wrap normal prose instead of forcing horizontal scroll.** Fenced code blocks tagged `md`, `markdown`, or `mdx` (the inline viewer with the small `MD` header and Copy button) now render in a dedicated wrapping source-preview style, so ordinary markdown text flows to the message width while true code/diff blocks keep their existing horizontal-scroll behavior. Thanks @TomBanksAU. (#4669) + ## [v0.51.579] — 2026-06-22 — Release UL (deflake TLS end-to-end tests) ### Fixed diff --git a/static/style.css b/static/style.css index b81ce66e..4ee51c2b 100644 --- a/static/style.css +++ b/static/style.css @@ -1687,6 +1687,9 @@ .msg-body code{font-family:"SF Mono","Fira Code",ui-monospace,monospace;font-size:var(--message-code-font-size);background:var(--code-inline-bg);padding:1px 5px;border-radius:4px;color:var(--code-text);} .msg-body pre{background:var(--code-bg);border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;margin:10px 0;} .msg-body pre code{background:none;padding:0;border-radius:0;color:var(--pre-text);font-size:var(--message-pre-code-font-size);line-height:1.6;} + .msg-body pre.md-source-block,.preview-md pre.md-source-block{white-space:pre-wrap;overflow-x:hidden;overflow-wrap:anywhere;} + .msg-body pre.md-source-block code,.preview-md pre.md-source-block code{display:block;white-space:inherit;overflow-wrap:anywhere;word-break:break-word;} + .msg-body pre.md-source-block code .token,.preview-md pre.md-source-block code .token{white-space:inherit;overflow-wrap:anywhere;word-break:inherit;} .provider-error-details{margin:12px 0 0;border:1px solid var(--border);border-radius:10px;background:var(--surface);overflow:hidden;} .provider-error-details>summary{cursor:pointer;color:var(--muted);font-size:12px;font-weight:600;padding:8px 12px;} .provider-error-details>pre{margin:0;border:0;border-top:1px solid var(--border);border-radius:0;max-height:220px;} diff --git a/static/ui.js b/static/ui.js index d530bb47..7db23b26 100644 --- a/static/ui.js +++ b/static/ui.js @@ -349,6 +349,7 @@ function _renderUserFencedBlocks(text){ if(code.endsWith('\n')) code=code.slice(0,-1); const h=lang?`
${esc(lang)}
`:''; const langAttr=lang?` class="language-${esc(lang)}"`:''; + const preClass=/^(md|markdown|mdx)$/.test(lang)?' class="md-source-block"':''; if(lang==='diff'||lang==='patch'){ const colored=esc(code).split('\n').map(line=>{ if(line.startsWith('@@')) return `${line}`; @@ -358,7 +359,7 @@ function _renderUserFencedBlocks(text){ }).join('\n'); stash.push(`${h}
${colored}
`); } else { - stash.push(`${h}
${esc(code)}
`); + stash.push(`${h}${esc(code)}`); } return lead+'\x00UF'+(stash.length-1)+'\x00'; }); @@ -4856,6 +4857,7 @@ function renderMd(raw){ } else { const h=lang?`
${esc(lang)}
`:''; const langAttr=lang?` class="language-${esc(lang)}"`:''; + const preClass=/^(md|markdown|mdx)$/.test(lang)?' class="md-source-block"':''; // For diff/patch blocks, wrap each line in a colored span if(lang==='diff'||lang==='patch'){ const colored=esc(code.replace(/\n$/,'')).split('\n').map(line=>{ @@ -4881,10 +4883,10 @@ function renderMd(raw){ const body=rows.slice(1).map(r=>''+r.split(',').map(c=>`${esc(c.trim())}`).join('')+'').join(''); _preBlock_stash.push(`${h}
${headers.map(h=>``).join('')}${body}
${esc(h)}
`); } else { - _preBlock_stash.push(`${h}
${esc(code.replace(/\n$/,''))}
`); + _preBlock_stash.push(`${h}${esc(code.replace(/\n$/,''))}`); } } else { - _preBlock_stash.push(`${h}
${esc(code.replace(/\n$/,''))}
`); + _preBlock_stash.push(`${h}${esc(code.replace(/\n$/,''))}`); } } return lead+'\x00P'+(_preBlock_stash.length-1)+'\x00'; diff --git a/tests/test_1325_user_fenced_code.py b/tests/test_1325_user_fenced_code.py index be918a4b..240e4560 100644 --- a/tests/test_1325_user_fenced_code.py +++ b/tests/test_1325_user_fenced_code.py @@ -126,7 +126,7 @@ class TestUserFencedBlocks: def test_four_backtick_outer_fence_preserves_inner_triple_fence(self): """User-message code fences should follow CommonMark fence-length matching too.""" out = _run_user_render("````md\n```inner\nfoo\n```\n````") - assert out.count("
") == 1
+        assert out.count("
") == 1
         assert out.count("
") == 1 assert '
md
' in out assert "```inner" in out diff --git a/tests/test_csv_table_rendering.py b/tests/test_csv_table_rendering.py index a2afc3da..b86c413f 100644 --- a/tests/test_csv_table_rendering.py +++ b/tests/test_csv_table_rendering.py @@ -41,7 +41,9 @@ def test_csv_fence_fallback_for_insufficient_rows(): src = f.read() fence_section = src[src.find("lang==='csv'"):src.find("lang==='csv'") + 800] assert 'rows.length>=2' in fence_section, "Should check for at least 2 rows" - assert '
"
+    assert '' in fence_section, (
+        "Fallback should render via the shared preClass-aware code-block template"
+    )
 
 
 def test_csv_media_file_handler():
diff --git a/tests/test_issue_code_syntax_highlight.py b/tests/test_issue_code_syntax_highlight.py
index d83617a6..d6dedd3d 100644
--- a/tests/test_issue_code_syntax_highlight.py
+++ b/tests/test_issue_code_syntax_highlight.py
@@ -17,9 +17,10 @@ def test_fenced_code_blocks_add_prism_language_class():
 def test_fenced_code_blocks_keep_existing_pre_header_layout():
     js = _read_ui_js()
     # The fenced code rendering was moved into the stash callback (#1154 fix).
-    # The template string now uses `lang` instead of `normalizedLang`.
-    assert '${h}
${esc(code.replace(/\\n$/,' in js, (
-        "The syntax-highlight fix should preserve the existing fenced code block layout"
+    # The template string now uses `lang` instead of `normalizedLang`, and
+    # markdown-source fences may add a dedicated md-source-block class.
+    assert '${h}${esc(code.replace(/\\n$/,' in js, (
+        "The syntax-highlight fix should preserve the shared fenced code block layout"
     )
     assert '
' not in js, ( "This fix should not introduce a new wrapper around fenced code blocks" diff --git a/tests/test_markdown_source_code_blocks_wrap.py b/tests/test_markdown_source_code_blocks_wrap.py new file mode 100644 index 00000000..14c5e603 --- /dev/null +++ b/tests/test_markdown_source_code_blocks_wrap.py @@ -0,0 +1,18 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8") +CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8") + + +def test_markdown_source_code_blocks_get_dedicated_pre_class_in_both_renderers(): + expected = "const preClass=/^(md|markdown|mdx)$/.test(lang)?' class=\"md-source-block\"':'';" + + assert UI_JS.count(expected) >= 2, "Expected both fenced-code renderers to tag markdown source blocks" + + +def test_markdown_source_code_blocks_have_desktop_wrap_css(): + assert ".msg-body pre.md-source-block,.preview-md pre.md-source-block{white-space:pre-wrap;overflow-x:hidden;overflow-wrap:anywhere;}" in CSS + assert ".msg-body pre.md-source-block code,.preview-md pre.md-source-block code{display:block;white-space:inherit;overflow-wrap:anywhere;word-break:break-word;}" in CSS + assert ".msg-body pre.md-source-block code .token,.preview-md pre.md-source-block code .token{white-space:inherit;overflow-wrap:anywhere;word-break:inherit;}" in CSS diff --git a/tests/test_renderer_js_behaviour.py b/tests/test_renderer_js_behaviour.py index 2a84905a..0c63cc6e 100644 --- a/tests/test_renderer_js_behaviour.py +++ b/tests/test_renderer_js_behaviour.py @@ -397,7 +397,7 @@ class TestFencedCodeFenceLength: "That is much more correct than pretending" ) out = _render(driver_path, src) - assert out.count("
") == 1
+        assert out.count("
") == 1
         assert out.count("
") == 1 assert '
md
' in out assert "```novelcrafter" in out @@ -408,7 +408,7 @@ class TestFencedCodeFenceLength: def test_four_backtick_outer_fence_preserves_inner_triple_fence(self, driver_path): out = _render(driver_path, "````md\n```inner\nfoo\n```\n````\n") - assert out.count("
") == 1
+        assert out.count("
") == 1
         assert out.count("
") == 1 assert '
md
' in out assert "```inner" in out