Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 68 additions & 21 deletions gui/src/components/StyledMarkdownPreview/utils/remarkTables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,42 @@ import { visit } from "unist-util-visit";
export function remarkTables() {
return (tree: any) => {
visit(tree, "paragraph", (paragraphNode, index, parentOfParagraphNode) => {
let buffer = "";
visit(paragraphNode, "text", (textNode) => {
buffer += textNode.value;
// Collect all child nodes into a buffer, preserving their types
const buffer: any[] = [];
paragraphNode.children.forEach((child: any) => {
buffer.push(child);
});

// Flatten buffer to a string for regex matching, but keep track of positions
let bufferString = "";
const positions: { start: number; end: number; node: any }[] = [];

// Recursive renderer for inline nodes -> markdown-ish text
function renderInline(node: any): string {
if (!node) return "";
if (Array.isArray(node.children)) {
return node.children.map(renderInline).join("");
}
if (typeof node.value === "string") {
return node.value;
}
return "";
}

buffer.forEach((item) => {
const start = bufferString.length;
// renderInline returns a markdown-like string for inline nodes so decorations are preserved
const rendered = renderInline(item);
bufferString += rendered;
positions.push({ start, end: bufferString.length, node: item });
});

const tableRegex =
/((?:\| *[^|\r\n]+ *)+\|)(?:\r?\n)((?:\|[ :]?-+[ :]?)+\|)((?:(?:\r?\n)(?:\| *[^|\r\n]+ *)+\|)+)/g;
//// header // newline // |:---|----:| // new line // table rows

// prevent modifying if no markdown tables are present
if (!buffer.match(tableRegex)) {
if (!bufferString.match(tableRegex)) {
return;
}

Expand All @@ -42,7 +67,7 @@ export function remarkTables() {
const newNodes = [];
let failed = false;

while ((match = tableRegex.exec(buffer)) !== null) {
while ((match = tableRegex.exec(bufferString)) !== null) {
const fullTableString = match[0];
const headerGroup = match[1];
const separatorGroup = match[2];
Expand Down Expand Up @@ -104,16 +129,46 @@ export function remarkTables() {
],
};

// Add any text before the table as a text node
if (match.index > lastIndex) {
newNodes.push({
type: "text",
value: buffer.slice(lastIndex, match.index),
});
}
// Add any nodes before/after the table in one go
const tableStart = match.index;
const tableEnd = match.index + fullTableString.length;

// Process the text within the table and the surrounding text together
const beforeNodes: any[] = [];
const afterNodes: any[] = [];

positions.forEach((pos) => {
if (pos.end <= tableStart) {
beforeNodes.push(pos.node);
} else if (pos.start >= tableEnd) {
afterNodes.push(pos.node);
} else if (pos.node.type === "text") {
// Node is text and overlaps with table, may need splitting
const beforeText = pos.node.value.slice(
0,
Math.max(0, tableStart - pos.start),
);
const afterText = pos.node.value.slice(
Math.max(0, tableEnd - pos.start),
);
if (beforeText)
beforeNodes.push({ type: "text", value: beforeText });
if (afterText)
afterNodes.push({ type: "text", value: afterText });
} else {
// Node overlaps with table but isn't text (e.g., inlineCode, emphasis, etc.)
if (pos.start < tableStart) beforeNodes.push(pos.node);
if (pos.end > tableEnd) afterNodes.push(pos.node);
}
});

// Add table node
// Add to new nodes
newNodes.push(...beforeNodes);
newNodes.push(tableNode);
newNodes.push(...afterNodes);

// Mark positions as consumed
positions.length = 0;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearing the tracked child positions here means subsequent tables in the same paragraph keep their raw markdown text, so a second table renders twice (once as text, once as a table). This regresses multi-table paragraphs.

Prompt for AI agents
Address the following comment on gui/src/components/StyledMarkdownPreview/utils/remarkTables.tsx at line 171:

<comment>Clearing the tracked child positions here means subsequent tables in the same paragraph keep their raw markdown text, so a second table renders twice (once as text, once as a table). This regresses multi-table paragraphs.</comment>

<file context>
@@ -104,16 +129,46 @@ export function remarkTables() {
+          newNodes.push(...afterNodes);
+
+          // Mark positions as consumed
+          positions.length = 0;
         } catch (e) {
           console.error(&quot;Failed to parse markdown table after regex match&quot;, e);
</file context>
Fix with Cubic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional. All elements in positions are consumed at this stage, so the variable won’t be referenced again later. Thanks for pointing it out, though!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I've learnt: "In remarkTables, the positions array is fully consumed when constructing before/after nodes around a parsed table; it is not used later, so clearing positions.length = 0 at that point is intentional. In future reviews, verify whether a tracking array/variable is referenced later before flagging its clearing as a regression.".

Edit this learning here.

} catch (e) {
console.error("Failed to parse markdown table after regex match", e);
newNodes.push({
Expand All @@ -129,14 +184,6 @@ export function remarkTables() {
return;
}

// Add any remaining text after the last table
if (lastIndex < buffer.length) {
newNodes.push({
type: "text",
value: buffer.slice(lastIndex),
});
}

// Replace the original paragraph node with the new nodes
if (newNodes.length > 0) {
parentOfParagraphNode.children.splice(index, 1, ...newNodes);
Expand Down
Loading