mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-09-27 04:35:35 +00:00
fix(markdown-editor): track fence length so a 4-backtick block isn't closed early
The escapeTablePipes fence tracker matched a fixed 3-char run (```/~~~) and compared truncated tokens, so a 4-backtick block — which renderTunedCodeBlock emits whenever the block's content holds a ``` line — was "closed" by that inner 3-backtick line. Two failures followed: pipes inside the real code block got `\|` injected into the saved bytes, and a genuine table AFTER the block lost its protection (the tracker thought it was still inside a fence), dropping cells. Capture the full run and its trailing text; close only on a same-char run of length >= the opener with no info string, mirroring CommonMark. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
81e448cf05
commit
19fc4b3092
@@ -143,6 +143,27 @@ describe('table cell with a pipe inside a URL — content survives load and conv
|
||||
});
|
||||
});
|
||||
|
||||
describe('fence length tracking — a longer fence is not closed by a shorter inner run', () => {
|
||||
it('keeps protecting a table after a 4-backtick block that contains a ``` line', () => {
|
||||
const src = '````\n```\ninner\n````\n\n| a | b |\n| --- | --- |\n| `x | y` | z |';
|
||||
|
||||
expect(escapeTablePipes(src)).toBe('````\n```\ninner\n````\n\n| a | b |\n| --- | --- |\n| `x \\| y` | z |');
|
||||
});
|
||||
|
||||
it('does not escape a table sitting inside a 4-backtick block that also holds a ``` line', () => {
|
||||
const src = '````\n```\n| a | b |\n| --- | --- |\n| `x | y` | z |\n````';
|
||||
|
||||
expect(escapeTablePipes(src)).toBe(src);
|
||||
});
|
||||
|
||||
it('round-trips a table after a fence-demonstrating code block without losing the cell', () => {
|
||||
const out = roundTrip('````\n```\ninner\n````\n\n| a | b |\n| --- | --- |\n| `x | y` | z |');
|
||||
|
||||
expect(out).toContain('z');
|
||||
expect(out).toContain('`x \\| y`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CRLF line endings — tables still protected', () => {
|
||||
it('escapes a code-span pipe in a CRLF table row', () => {
|
||||
expect(escapeTablePipes('| a | b |\r\n| --- | --- |\r\n| `x | y` | z |\r\n')).toBe(
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
// marked would end the table there; we do not replicate its HTML-block interrupt (vanishingly rare in this
|
||||
// content, and our html tokenizers render such lines as literal text anyway).
|
||||
|
||||
const FENCE_LINE = /^ {0,3}(```|~~~)/;
|
||||
// Capture the full fence run (not a fixed 3) plus the trailing text: renderTunedCodeBlock widens a fence to
|
||||
// 4+ backticks when its content holds a ``` line, and CommonMark closes a fence only with a run of the same
|
||||
// char, length >= the opener, and no info string — so length- and char-aware tracking is load-bearing.
|
||||
const FENCE_LINE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
||||
// Written to be linear: the trailing `(?: *\|)? *$` (not `\|? *$`) plus per-cell spacing keep any two space
|
||||
// runs from competing for the same characters, so a crafted delimiter-looking line can't force O(n²) backtracking.
|
||||
const TABLE_DELIMITER_LINE = /^ {0,3}\|? *:?-+:?(?: *\| *:?-+:?)*(?: *\|)? *$/;
|
||||
@@ -142,7 +145,7 @@ export const escapeTablePipes = (markdown: string): string => {
|
||||
// lose cells. Normalize first; the return below keeps the original bytes when nothing was escaped.
|
||||
const source = markdown.includes('\r') ? markdown.replace(/\r\n?/g, '\n') : markdown;
|
||||
const lines = source.split('\n');
|
||||
let openFence: null | string = null;
|
||||
let openFence: null | { char: string; length: number } = null;
|
||||
let isChanged = false;
|
||||
|
||||
const escapeRow = (row: number): void => {
|
||||
@@ -159,7 +162,14 @@ export const escapeTablePipes = (markdown: string): string => {
|
||||
const fence = FENCE_LINE.exec(line);
|
||||
|
||||
if (fence) {
|
||||
openFence = openFence === fence[1] ? null : (openFence ?? fence[1]!);
|
||||
const marker = fence[1]!;
|
||||
|
||||
if (openFence === null) {
|
||||
openFence = { char: marker[0]!, length: marker.length };
|
||||
} else if (marker[0] === openFence.char && marker.length >= openFence.length && !fence[2]!.trim()) {
|
||||
openFence = null;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user