Spaces:
Sleeping
Sleeping
| import type { MarkdownTableMode } from "../config/types.base.js"; | |
| import { chunkMarkdownIR, markdownToIR, type MarkdownLinkSpan } from "../markdown/ir.js"; | |
| import { renderMarkdownWithMarkers } from "../markdown/render.js"; | |
| // Escape special characters for Slack mrkdwn format. | |
| // Preserve Slack's angle-bracket tokens so mentions and links stay intact. | |
| 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.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.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; | |
| }; | |
| 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, { | |
| styleMarkers: { | |
| bold: { open: "*", close: "*" }, | |
| italic: { open: "_", close: "_" }, | |
| strikethrough: { open: "~", close: "~" }, | |
| code: { open: "`", close: "`" }, | |
| code_block: { open: "```\n", close: "```" }, | |
| }, | |
| escapeText: escapeSlackMrkdwnText, | |
| buildLink: buildSlackLink, | |
| }); | |
| } | |
| 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); | |
| return chunks.map((chunk) => | |
| renderMarkdownWithMarkers(chunk, { | |
| styleMarkers: { | |
| bold: { open: "*", close: "*" }, | |
| italic: { open: "_", close: "_" }, | |
| strikethrough: { open: "~", close: "~" }, | |
| code: { open: "`", close: "`" }, | |
| code_block: { open: "```\n", close: "```" }, | |
| }, | |
| escapeText: escapeSlackMrkdwnText, | |
| buildLink: buildSlackLink, | |
| }), | |
| ); | |
| } | |