diff --git a/docs/pr-media/1451/raw-pre-render-validation.png b/docs/pr-media/1451/raw-pre-render-validation.png new file mode 100644 index 00000000..d27ddc40 Binary files /dev/null and b/docs/pr-media/1451/raw-pre-render-validation.png differ diff --git a/static/ui.js b/static/ui.js index 73775d26..2e533896 100644 --- a/static/ui.js +++ b/static/ui.js @@ -1665,7 +1665,6 @@ function renderMd(raw){ s=s.replace(/([\s\S]*?)<\/i>/gi,(_,t)=>'*'+t+'*'); s=s.replace(/([^<]*?)<\/code>/gi,(_,t)=>'`'+t+'`'); s=s.replace(//gi,'\n'); - s=s.replace(/\x00R(\d+)\x00/g,(_,i)=>rawPreStash[+i]); // ── Glued-bold-heading lift (issue #1446) ──────────────────────────────── // LLMs in thinking/reasoning mode frequently emit a "section header" glued // to the end of the previous paragraph with no whitespace, like: @@ -1797,6 +1796,9 @@ function renderMd(raw){ s=s.replace(/(]*>[\s\S]*?<\/a>)/g,m=>{_a_stash.push(m);return `\x00A${_a_stash.length-1}\x00`;}); s=s.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,(_,label,url)=>`${esc(label)}`); s=s.replace(/\x00A(\d+)\x00/g,(_,i)=>_a_stash[+i]); + // Restore raw
 only after markdown rewrites so literal preformatted
+  // content stays placeholder-protected, then let the sanitizer normalize tags.
+  s=s.replace(/\x00R(\d+)\x00/g,(_,i)=>rawPreStash[+i]);
   // Sanitize any remaining HTML tags.  The renderer intentionally returns
   // HTML and inserts it with innerHTML later, so tag names alone are not enough:
   // raw/model-provided HTML like  or 
diff --git a/tests/test_issue1446_glued_heading_lift.py b/tests/test_issue1446_glued_heading_lift.py
index 5857a5b8..d760c34e 100644
--- a/tests/test_issue1446_glued_heading_lift.py
+++ b/tests/test_issue1446_glued_heading_lift.py
@@ -153,20 +153,21 @@ def test_chain_of_glued_headings_all_lifted():
 
 
 def test_lift_pass_present_in_ui_js_at_correct_position():
-    """The lift regex must be present in ui.js, between rawPreStash restore and fence_stash restore.
+    """The lift regex must be present in ui.js before protected-code restores.
 
     This pins the position so a future cleanup can't accidentally move the lift
-    to a place where it would corrupt fenced code blocks (which are stashed as
-    \\x00P / \\x00F tokens at this point and don't match the lift regex).
+    to a place where it would corrupt raw 
 HTML or fenced code blocks
+    (which are stashed as \x00R / \x00P / \x00F tokens at this point and don't
+    match the lift regex).
     """
     lift_idx = UI_JS.find(r'(/([.!?])\*\*([^*\n]{1,80})\*\*\n\n/g')
     assert lift_idx > 0, "Glued-bold-heading lift regex not found in static/ui.js"
     raw_pre_restore = UI_JS.find("rawPreStash[+i]")
     fence_restore = UI_JS.find("fence_stash[+i]")
     assert raw_pre_restore > 0 and fence_restore > 0, "stash restore landmarks missing"
-    assert raw_pre_restore < lift_idx < fence_restore, (
-        "Glued-bold lift must sit between rawPreStash restore and fence_stash restore "
-        "so fenced code is protected. Current ordering broken."
+    assert lift_idx < raw_pre_restore and lift_idx < fence_restore, (
+        "Glued-bold lift must run before rawPreStash and fence_stash restore "
+        "so raw 
 and fenced code are protected. Current ordering broken."
     )
 
 
@@ -254,6 +255,16 @@ def test_real_renderer_protects_fenced_code(driver_path):
     assert "**inside-code**" in out, out
 
 
+@pytest.mark.skipif(NODE is None, reason="node not on PATH")
+def test_real_renderer_protects_raw_pre_html(driver_path):
+    """Raw literal 
 content must stay byte-preserved when it contains the glued trigger."""
+    src = "
Para text.**Heading**\n\nNext.
\n" + out = _render(driver_path, src) + assert "
Para text.**Heading**\n\nNext.
" in out, out + assert "
Para text.\n\n**Heading**\n\nNext.
" not in out, out + assert "Heading" not in out, out + + @pytest.mark.skipif(NODE is None, reason="node not on PATH") def test_real_renderer_protects_inline_code(driver_path): """Glued pattern inside inline backticks must stay literal.""" diff --git a/tests/test_sprint16.py b/tests/test_sprint16.py index e273e7e8..0b7b309a 100644 --- a/tests/test_sprint16.py +++ b/tests/test_sprint16.py @@ -69,9 +69,9 @@ def render_md(raw): s = re.sub(r"([\s\S]*?)", lambda m: "*" + m.group(1) + "*", s, flags=re.I) s = re.sub(r"([^<]*?)", lambda m: "`" + m.group(1) + "`", s, flags=re.I) s = re.sub(r"", "\n", s, flags=re.I) - # Glued-bold-heading lift (issue #1446) — must mirror static/ui.js position: - # after raw
 restore, before fence_stash restore. Lifts a sentence-glued
-    # bold "stub heading" out into its own paragraph when followed by a blank line.
+    # Glued-bold-heading lift (issue #1446) — must mirror static/ui.js behavior:
+    # protected code/pre placeholders stay hidden while a sentence-glued bold
+    # "stub heading" is lifted into its own paragraph when followed by a blank line.
     s = re.sub(r"([.!?])\*\*([^*\n]{1,80})\*\*\n\n", r"\1\n\n**\2**\n\n", s)
     s = re.sub(r"\x00F(\d+)\x00", lambda m: fence_stash[int(m.group(1))], s)