Compare commits

...

3 Commits

Author SHA1 Message Date
Tom Moor 505b9e5dbb test: Cover math block table-cell serialization
Add a test mirroring the code fence case to guard the MathBlock half of the
table-cell <br> encoding and pipe/backslash escaping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:51:01 -04:00
Tom Moor aadf47f2d7 fix: Escape backslashes in table-cell code & math fences
CodeQL flagged incomplete string escaping: escaping pipes as \| without
escaping pre-existing backslashes leaves the encoding ambiguous. Escape the
backslash (the escape character) and pipe together in a single pass so cell
content cannot break out of the column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:44:49 -04:00
Tom Moor 4da4cd7f6a fix: Encode code & math fences in table cells with <br> to avoid breaking rows
Code and LaTeX fences inside table cells emitted literal newlines during
Markdown serialization, breaking the single-line table-row structure and
corrupting the whole table. Serialize them on a single line using <br> for
line breaks (and escaped pipes) when inside a table, matching the existing
handling for lists and hard breaks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:31:17 -04:00
3 changed files with 139 additions and 0 deletions
@@ -635,6 +635,121 @@ This is a [test paragraph](https://example.net)`,
expect(result).toContain("[x] done");
});
it("should export code fences inside table cells on a single line", async () => {
const document = await buildDocument({
content: {
type: "doc",
content: [
{
type: "table",
content: [
{
type: "tr",
content: [
{
type: "th",
attrs: { colspan: 1, rowspan: 1 },
content: [
{
type: "paragraph",
content: [{ type: "text", text: "Header" }],
},
],
},
],
},
{
type: "tr",
content: [
{
type: "td",
attrs: { colspan: 1, rowspan: 1 },
content: [
{
type: "code_fence",
attrs: { language: "abap" },
content: [
{ type: "text", text: "a | b\nc \\ d\nline 2" },
],
},
],
},
],
},
],
},
],
},
});
const result = await DocumentHelper.toMarkdown(document, {
includeTitle: false,
});
// Code fences inside tables should use <br> tags rather than literal
// newlines that would break the table row structure, with pipes and
// backslashes escaped so the content cannot break out of the column.
expect(result).toContain(
"```abap<br>a \\| b<br>c \\\\ d<br>line 2<br>```"
);
// The fence content must not introduce raw newlines inside the table.
expect(result).not.toMatch(/```abap\n/);
});
it("should export math blocks inside table cells on a single line", async () => {
const document = await buildDocument({
content: {
type: "doc",
content: [
{
type: "table",
content: [
{
type: "tr",
content: [
{
type: "th",
attrs: { colspan: 1, rowspan: 1 },
content: [
{
type: "paragraph",
content: [{ type: "text", text: "Header" }],
},
],
},
],
},
{
type: "tr",
content: [
{
type: "td",
attrs: { colspan: 1, rowspan: 1 },
content: [
{
type: "math_block",
content: [
{ type: "text", text: "a | b\n\\frac{1}{2}" },
],
},
],
},
],
},
],
},
],
},
});
const result = await DocumentHelper.toMarkdown(document, {
includeTitle: false,
});
// Math blocks inside tables should use <br> tags rather than literal
// newlines that would break the table row structure, with pipes and
// backslashes escaped so the content cannot break out of the column.
expect(result).toContain("$$<br>a \\| b<br>\\\\frac{1}{2}<br>$$");
// The block content must not introduce raw newlines inside the table.
expect(result).not.toMatch(/\$\$\n/);
});
it("should include collection title by default", async () => {
const collection = await buildCollection({
name: "Test Collection",
+13
View File
@@ -694,6 +694,19 @@ export default class CodeFence extends Node<CodeFenceOptions> {
}
toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) {
// Inside table cells literal newlines break the row structure, so encode
// the fence on a single line using <br> for line breaks. Backslashes and
// pipes are escaped so the cell content cannot break out of the column.
if (state.inTable) {
const code = node.textContent
.replace(/[\\|]/g, "\\$&")
.replace(/\n/g, "<br>");
state.write(
"```" + (node.attrs.language || "") + "<br>" + code + "<br>```"
);
return;
}
state.write("```" + (node.attrs.language || "") + "\n");
state.text(node.textContent, false);
state.ensureNewLine();
+11
View File
@@ -46,6 +46,17 @@ export default class MathBlock extends Node {
}
toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) {
// Inside table cells literal newlines break the row structure, so encode
// the block on a single line using <br> for line breaks. Backslashes and
// pipes are escaped so the cell content cannot break out of the column.
if (state.inTable) {
const math = node.textContent
.replace(/[\\|]/g, "\\$&")
.replace(/\n/g, "<br>");
state.write("$$<br>" + math + "<br>$$");
return;
}
state.write("$$\n");
state.text(node.textContent, false);
state.ensureNewLine();