| import type { MarkdownTableMode } from "../config/types.base.js"; |
| import { chunkMarkdownIR, markdownToIR, type MarkdownLinkSpan } from "../markdown/ir.js"; |
| import { renderMarkdownWithMarkers } from "../markdown/render.js"; |
|
|
| |
| |
| function escapeSlackMrkdwnSegment(text: string): string { |
| return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); |
| } |
|
|
| const SLACK_ANGLE_TOKEN_RE = /<[^>\n]+>/g; |
|
|
| function isAllowedSlackAngleToken(token: string): boolean { |
| if (!token.startsWith("<") || !token.endsWith(">")) { |
| return false; |
| } |
| const inner = token.slice(1, -1); |
| return ( |
| inner.startsWith("@") || |
| inner.startsWith("#") || |
| inner.startsWith("!") || |
| inner.startsWith("mailto:") || |
| inner.startsWith("tel:") || |
| inner.startsWith("http://") || |
| inner.startsWith("https://") || |
| inner.startsWith("slack://") |
| ); |
| } |
|
|
| function escapeSlackMrkdwnContent(text: string): string { |
| if (!text) { |
| return ""; |
| } |
| if (!text.includes("&") && !text.includes("<") && !text.includes(">")) { |
| return text; |
| } |
|
|
| SLACK_ANGLE_TOKEN_RE.lastIndex = 0; |
| const out: string[] = []; |
| let lastIndex = 0; |
|
|
| for ( |
| let match = SLACK_ANGLE_TOKEN_RE.exec(text); |
| match; |
| match = SLACK_ANGLE_TOKEN_RE.exec(text) |
| ) { |
| const matchIndex = match.index ?? 0; |
| out.push(escapeSlackMrkdwnSegment(text.slice(lastIndex, matchIndex))); |
| const token = match[0] ?? ""; |
| out.push(isAllowedSlackAngleToken(token) ? token : escapeSlackMrkdwnSegment(token)); |
| lastIndex = matchIndex + token.length; |
| } |
|
|
| out.push(escapeSlackMrkdwnSegment(text.slice(lastIndex))); |
| return out.join(""); |
| } |
|
|
| function escapeSlackMrkdwnText(text: string): string { |
| if (!text) { |
| return ""; |
| } |
| if (!text.includes("&") && !text.includes("<") && !text.includes(">")) { |
| return text; |
| } |
|
|
| return text |
| .split("\n") |
| .map((line) => { |
| if (line.startsWith("> ")) { |
| return `> ${escapeSlackMrkdwnContent(line.slice(2))}`; |
| } |
| return escapeSlackMrkdwnContent(line); |
| }) |
| .join("\n"); |
| } |
|
|
| function buildSlackLink(link: MarkdownLinkSpan, text: string) { |
| const href = link.href.trim(); |
| if (!href) { |
| return null; |
| } |
| const label = text.slice(link.start, link.end); |
| const trimmedLabel = label.trim(); |
| const comparableHref = href.startsWith("mailto:") ? href.slice("mailto:".length) : href; |
| const useMarkup = |
| trimmedLabel.length > 0 && trimmedLabel !== href && trimmedLabel !== comparableHref; |
| if (!useMarkup) { |
| return null; |
| } |
| const safeHref = escapeSlackMrkdwnSegment(href); |
| return { |
| start: link.start, |
| end: link.end, |
| open: `<${safeHref}|`, |
| close: ">", |
| }; |
| } |
|
|
| type SlackMarkdownOptions = { |
| tableMode?: MarkdownTableMode; |
| }; |
|
|
| function buildSlackRenderOptions() { |
| return { |
| styleMarkers: { |
| bold: { open: "*", close: "*" }, |
| italic: { open: "_", close: "_" }, |
| strikethrough: { open: "~", close: "~" }, |
| code: { open: "`", close: "`" }, |
| code_block: { open: "```\n", close: "```" }, |
| }, |
| escapeText: escapeSlackMrkdwnText, |
| buildLink: buildSlackLink, |
| }; |
| } |
|
|
| export function markdownToSlackMrkdwn( |
| markdown: string, |
| options: SlackMarkdownOptions = {}, |
| ): string { |
| const ir = markdownToIR(markdown ?? "", { |
| linkify: false, |
| autolink: false, |
| headingStyle: "bold", |
| blockquotePrefix: "> ", |
| tableMode: options.tableMode, |
| }); |
| return renderMarkdownWithMarkers(ir, buildSlackRenderOptions()); |
| } |
|
|
| export function normalizeSlackOutboundText(markdown: string): string { |
| return markdownToSlackMrkdwn(markdown ?? ""); |
| } |
|
|
| export function markdownToSlackMrkdwnChunks( |
| markdown: string, |
| limit: number, |
| options: SlackMarkdownOptions = {}, |
| ): string[] { |
| const ir = markdownToIR(markdown ?? "", { |
| linkify: false, |
| autolink: false, |
| headingStyle: "bold", |
| blockquotePrefix: "> ", |
| tableMode: options.tableMode, |
| }); |
| const chunks = chunkMarkdownIR(ir, limit); |
| const renderOptions = buildSlackRenderOptions(); |
| return chunks.map((chunk) => renderMarkdownWithMarkers(chunk, renderOptions)); |
| } |
|
|