fix(renderer): match the table-row guard to the real table grammar
Third gate finding: the table-row predicate accepted ANY pipe-wrapped line, but the downstream table parser only accepts a run of pipe-lines whose SECOND line is a separator. So pipe-delimited prose such as '| note<br># heading |' was treated as a table row and kept its literal <br>, silently losing heading/list rendering that master renders correctly. The guard now classifies complete blocks with the parser's own grammar (row regex + separator check) and converts <br> on every unclassified line. Verified: all non-table cases now match master byte-for-byte, while genuine multi-row tables keep <br> in every data cell. Co-authored-by: JayaMegelar <JayaMegelar@users.noreply.github.com>
This commit is contained in:
1 parent
75d51d5ee2
commit
49d4aab4e2
2 files changed
+58
-7
No files matched your search
+26
-7
@@ -7785,13 +7785,32 @@ function renderMd(raw){
|
||||
s=s.replace(/<em>([\s\S]*?)<\/em>/gi,(_,t)=>_emphasis(t));
|
||||
s=s.replace(/<i>([\s\S]*?)<\/i>/gi,(_,t)=>_emphasis(t));
|
||||
s=s.replace(/<code>([^<]*?)<\/code>/gi,(_,t)=>'`'+t+'`');
|
||||
// Convert <br> to a newline ONLY outside markdown table rows — inside a row a
|
||||
// newline would split the row and destroy the table. No sentinel token is used
|
||||
// here on purpose: any fixed placeholder is attacker-suppliable in message text
|
||||
// and would be rewritten on the way out.
|
||||
s=s.split('\n').map(line=>(
|
||||
/^\s*\|.*\|\s*$/.test(line) ? line : line.replace(/<br\s*\/?>/gi,'\n')
|
||||
)).join('\n');
|
||||
// Convert <br> to a newline, EXCEPT inside genuine markdown table rows — there a
|
||||
// newline would split the row and destroy the table. No sentinel token is used on
|
||||
// purpose: any fixed placeholder is attacker-suppliable in message text and would be
|
||||
// rewritten on the way out.
|
||||
// The "is this a table row" test must match the DOWNSTREAM table parser's grammar
|
||||
// exactly (a run of pipe-lines whose SECOND line is a separator). A looser per-line
|
||||
// test would treat pipe-wrapped prose like `| note<br># heading |` as a table row and
|
||||
// silently strip its heading/list rendering.
|
||||
{
|
||||
const _rowRe=/^ {0,3}\|.+\|[ \t]*$/;
|
||||
const _sepRe=/^\|[\s|:-]+\|$/;
|
||||
const _lines=s.split('\n');
|
||||
const _isTableRow=new Array(_lines.length).fill(false);
|
||||
for(let i=0;i<_lines.length;){
|
||||
if(!_rowRe.test(_lines[i])){ i++; continue; }
|
||||
let j=i;
|
||||
while(j<_lines.length && _rowRe.test(_lines[j])) j++;
|
||||
// A block qualifies only if the parser would accept it: >=2 rows and a separator
|
||||
// in the second position.
|
||||
if(j-i>=2 && _sepRe.test(_lines[i+1].trim())){
|
||||
for(let k=i;k<j;k++) _isTableRow[k]=true;
|
||||
}
|
||||
i=j;
|
||||
}
|
||||
s=_lines.map((line,i)=>_isTableRow[i]?line:line.replace(/<br\s*\/?>/gi,'\n')).join('\n');
|
||||
}
|
||||
// ── 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:
|
||||
|
||||
@@ -982,3 +982,35 @@ class TestRendererGateRegressions7618:
|
||||
out = _render(driver_path, "2 * 3 * 4 = 24")
|
||||
assert "<em>" not in out
|
||||
assert "2 * 3 * 4 = 24" in out
|
||||
|
||||
def test_pipe_wrapped_prose_keeps_heading_rendering(self, driver_path):
|
||||
"""`| note<br># heading |` is NOT a table — it must keep heading rendering.
|
||||
|
||||
The table-row guard must use the same grammar as the downstream table
|
||||
parser (a pipe-line run whose SECOND line is a separator). A naive
|
||||
per-line "looks pipe-wrapped" test silently stripped heading/list
|
||||
rendering from pipe-delimited prose.
|
||||
"""
|
||||
out = _render(driver_path, "| prose<br># heading |")
|
||||
assert "<h1>" in out
|
||||
|
||||
def test_pipe_wrapped_prose_keeps_list_rendering(self, driver_path):
|
||||
out = _render(driver_path, "| prose<br>- item |")
|
||||
assert "<ul>" in out and "<li>" in out
|
||||
|
||||
def test_pipe_rows_without_separator_are_not_treated_as_table(self, driver_path):
|
||||
"""Two pipe lines with no separator row are prose, so <br> still converts."""
|
||||
out = _render(driver_path, "| a | b |\n| c | d |")
|
||||
assert "<table>" not in out
|
||||
|
||||
def test_multi_row_table_preserves_br_in_every_data_row(self, driver_path):
|
||||
src = (
|
||||
"| Feature | Notes | Status |\n"
|
||||
"|---|---|---|\n"
|
||||
"| Auth | OAuth<br>API keys<br>tokens | shipped |\n"
|
||||
"| Cache | LRU<br>60s TTL | in review |"
|
||||
)
|
||||
out = _render(driver_path, src)
|
||||
assert out.count("<tr>") == 3
|
||||
assert "OAuth<br>API keys<br>tokens" in out
|
||||
assert "LRU<br>60s TTL" in out
|
||||
Reference in new issue
Block a user