From b7c307020441dccced5b9897f52eef7622aa9781 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Tue, 16 Dec 2025 18:02:52 +0000 Subject: [PATCH] feat: Add markdown table conversion for Slack formatting --- .../MicrosoftTeams/MicrosoftTeams.ts | 79 +++++++++++++++++- Common/Server/Utils/Workspace/Slack/Slack.ts | 81 ++++++++++++++++++- 2 files changed, 158 insertions(+), 2 deletions(-) diff --git a/Common/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts b/Common/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts index 5ed57bde59..33eba2092f 100644 --- a/Common/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +++ b/Common/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts @@ -434,12 +434,89 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase { return { actionType: actionType as MicrosoftTeamsActionType, actionValue }; } + /** + * Converts markdown tables to HTML tables for Teams MessageCard. + * Teams MessageCard supports HTML in the text field. + */ + private static convertMarkdownTablesToHtml(markdown: string): string { + // Regular expression to match markdown tables + const tableRegex: RegExp = + /(?:^|\n)((?:\|[^\n]+\|\n)+(?:\|[-:\s|]+\|\n)(?:\|[^\n]+\|\n?)+)/g; + + return markdown.replace( + tableRegex, + (_match: string, table: string): string => { + const lines: Array = table.trim().split("\n"); + + if (lines.length < 2) { + return table; + } + + // Parse header row + const headerLine: string = lines[0] || ""; + const headers: Array = headerLine + .split("|") + .map((cell: string) => { + return cell.trim(); + }) + .filter((cell: string) => { + return cell.length > 0; + }); + + // Skip separator line (line with dashes) and get data rows + const dataRows: Array = lines.slice(2); + + // Build HTML table + let html: string = + ''; + + // Header row + html += ""; + for (const header of headers) { + html += ``; + } + html += ""; + + // Data rows + for (const row of dataRows) { + const cells: Array = row + .split("|") + .map((cell: string) => { + return cell.trim(); + }) + .filter((cell: string) => { + return cell.length > 0; + }); + + if (cells.length === 0) { + continue; + } + + html += ""; + for (const cell of cells) { + html += ``; + } + html += ""; + } + + html += "
${header}
${cell}
"; + + return "\n" + html + "\n"; + }, + ); + } + private static buildMessageCardFromMarkdown(markdown: string): JSONObject { /* * Teams MessageCard has limited markdown support. Headings like '##' are not supported * and single newlines can collapse. Convert common patterns to a structured card. */ - const lines: Array = markdown + + // First, convert markdown tables to HTML + const markdownWithHtmlTables: string = + this.convertMarkdownTablesToHtml(markdown); + + const lines: Array = markdownWithHtmlTables .split("\n") .map((l: string) => { return l.trim(); diff --git a/Common/Server/Utils/Workspace/Slack/Slack.ts b/Common/Server/Utils/Workspace/Slack/Slack.ts index aa03fd36de..a1c4d0bbac 100644 --- a/Common/Server/Utils/Workspace/Slack/Slack.ts +++ b/Common/Server/Utils/Workspace/Slack/Slack.ts @@ -1892,9 +1892,88 @@ export default class SlackUtil extends WorkspaceBase { return apiResult; } + /** + * Converts markdown tables to a Slack-friendly format. + * Since Slack's mrkdwn doesn't support tables, we convert them to + * a row-by-row format with bold headers. + */ + private static convertMarkdownTablesToSlackFormat(markdown: string): string { + // Regular expression to match markdown tables + const tableRegex: RegExp = + /(?:^|\n)((?:\|[^\n]+\|\n)+(?:\|[-:\s|]+\|\n)(?:\|[^\n]+\|\n?)+)/g; + + return markdown.replace( + tableRegex, + (_match: string, table: string): string => { + const lines: Array = table.trim().split("\n"); + + if (lines.length < 2) { + return table; + } + + // Parse header row + const headerLine: string = lines[0] || ""; + const headers: Array = headerLine + .split("|") + .map((cell: string) => { + return cell.trim(); + }) + .filter((cell: string) => { + return cell.length > 0; + }); + + /* + * Skip separator line (line with dashes) + * Find data rows (skip header and separator) + */ + const dataRows: Array = lines.slice(2); + const formattedRows: Array = []; + + for (let rowIndex: number = 0; rowIndex < dataRows.length; rowIndex++) { + const row: string = dataRows[rowIndex] || ""; + const cells: Array = row + .split("|") + .map((cell: string) => { + return cell.trim(); + }) + .filter((cell: string) => { + return cell.length > 0; + }); + + if (cells.length === 0) { + continue; + } + + const rowParts: Array = []; + for ( + let cellIndex: number = 0; + cellIndex < cells.length; + cellIndex++ + ) { + const header: string = + headers[cellIndex] || `Column ${cellIndex + 1}`; + const value: string = cells[cellIndex] || ""; + rowParts.push(`*${header}:* ${value}`); + } + + if (dataRows.length > 1) { + formattedRows.push(`_Row ${rowIndex + 1}_\n${rowParts.join("\n")}`); + } else { + formattedRows.push(rowParts.join("\n")); + } + } + + return "\n" + formattedRows.join("\n\n") + "\n"; + }, + ); + } + @CaptureSpan() public static convertMarkdownToSlackRichText(markdown: string): string { - return SlackifyMarkdown(markdown); + // First convert tables to Slack-friendly format + const markdownWithConvertedTables: string = + this.convertMarkdownTablesToSlackFormat(markdown); + return SlackifyMarkdown(markdownWithConvertedTables); } @CaptureSpan()