From d1ac2e29ad333a2d187fcbf45a00a30f6e1d63e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Mart=C3=ADnez?= Date: Wed, 3 Jun 2026 15:44:46 +0200 Subject: [PATCH] feat: add markdown table renderer to the workbench ui (#2255) --- ui/index.html | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/ui/index.html b/ui/index.html index a614d2db..1a467496 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1369,10 +1369,42 @@ .markdown p, .markdown ul, .markdown ol, + .markdown table, .markdown pre { margin: 0; } + .markdown-table-scroll { + max-width: 100%; + overflow-x: auto; + } + + .markdown table { + width: 100%; + border-collapse: collapse; + overflow: hidden; + border: 1px solid var(--border-soft); + border-radius: 12px; + background: rgba(0, 0, 0, 0.14); + font-size: 0.94em; + white-space: normal; + } + + .markdown th, + .markdown td { + padding: 8px 10px; + border: 1px solid var(--border-soft); + text-align: left; + vertical-align: top; + overflow-wrap: break-word; + } + + .markdown th { + color: rgba(255, 255, 255, 0.96); + background: rgba(255, 255, 255, 0.08); + font-weight: 750; + } + .markdown ul, .markdown ol { padding-left: 22px; @@ -5022,12 +5054,32 @@ list = null; } }; - for (const raw of lines) { + for (let index = 0; index < lines.length; index += 1) { + const raw = lines[index]; const line = raw.trimEnd(); if (!line.trim()) { closeList(); continue; } + const nextLine = lines[index + 1]?.trimEnd() || ""; + if (isMarkdownTableHeader(line, nextLine)) { + closeList(); + const header = parseMarkdownTableRow(line); + const alignments = parseMarkdownTableAlignments(nextLine); + const rows = []; + index += 2; + while (index < lines.length) { + const rowLine = lines[index].trimEnd(); + if (!rowLine.trim() || !rowLine.includes("|")) break; + const row = parseMarkdownTableRow(rowLine); + if (row.length < 2) break; + rows.push(row); + index += 1; + } + index -= 1; + html.push(renderMarkdownTable(header, alignments, rows)); + continue; + } const bullet = line.match(/^\s*[-*]\s+(.+)$/); const ordered = line.match(/^\s*\d+[.)]\s+(.+)$/); const heading = line.match(/^(#{1,3})\s+(.+)$/); @@ -5050,6 +5102,49 @@ return html.join(""); } + function isMarkdownTableHeader(headerLine, dividerLine) { + const divider = parseMarkdownTableRow(dividerLine); + return headerLine.includes("|") && + parseMarkdownTableRow(headerLine).length >= 2 && + /^:?-{3,}:?$/.test(divider[0] || "") && + divider.every(cell => /^:?-{3,}:?$/.test(cell)); + } + + function parseMarkdownTableRow(line) { + let trimmed = String(line || "").trim(); + if (trimmed.startsWith("|")) trimmed = trimmed.slice(1); + if (trimmed.endsWith("|")) trimmed = trimmed.slice(0, -1); + return trimmed.split("|").map(cell => cell.trim()); + } + + function parseMarkdownTableAlignments(line) { + return parseMarkdownTableRow(line).map(cell => { + const left = cell.startsWith(":"); + const right = cell.endsWith(":"); + if (left && right) return "center"; + if (right) return "right"; + return "left"; + }); + } + + function renderMarkdownTable(header, alignments, rows) { + const cellStyle = index => alignments[index] && alignments[index] !== "left" + ? ` style="text-align:${alignments[index]}"` + : ""; + const columnCount = header.length; + const renderCells = (cells, tag) => Array.from({ length: columnCount }, (_unused, index) => + `<${tag}${cellStyle(index)}>${inlineMarkdown(cells[index] || "")}` + ).join(""); + return ` +
+ + ${renderCells(header, "th")} + ${rows.map(row => `${renderCells(row, "td")}`).join("")} +
+
+ `; + } + function inlineMarkdown(value) { let text = escapeHtml(value); text = text.replace(/`([^`]+)`/g, "$1");