export type MarkdownImageSegment = | { type: "text"; value: string } | { type: "image"; alt?: string; src: string }; // Markdown destinations may include balanced parentheses and backslash escapes. function readDestination(content: string, open: number) { if (content[open] !== "(") return null; let depth = 1; for (let index = open + 1; index < content.length; index += 1) { const character = content[index]; if (character === "\\") { index += 1; continue; } if (character === "(") depth += 1; if (character === ")" && --depth === 0) { const raw = content.slice(open + 1, index); if (!/^https?:\/\//i.test(raw) || /\s/.test(raw)) return null; return { url: raw.replace(/\\([()\\])/g, "$1"), end: index + 1 }; } } return null; } function readImage(content: string, start: number) { if (!content.startsWith("![", start)) return null; let depth = 1; for (let index = start + 2; index < content.length; index += 1) { const character = content[index]; if (character === "\\") { index += 1; continue; } if (character === "[") depth += 1; if (character !== "]" || --depth !== 0) continue; const destination = readDestination(content, index + 1); if (!destination) return null; return { alt: content.slice(start + 2, index).replace(/\\([\[\]\\])/g, "$1"), src: destination.url, end: destination.end, }; } return null; } export function splitMarkdownImages(content: string): MarkdownImageSegment[] { const segments: MarkdownImageSegment[] = []; let textStart = 0; let cursor = 0; while (cursor < content.length) { // Leave Markdown examples in code spans and fences untouched. if (content[cursor] === "`") { let endOfRun = cursor + 1; while (content[endOfRun] === "`") endOfRun += 1; const marker = content.slice(cursor, endOfRun); const close = content.indexOf(marker, endOfRun); cursor = close < 0 ? content.length : close + marker.length; continue; } const lineStart = cursor === 0 || content[cursor - 1] === "\n"; if (lineStart) { const fence = content.slice(cursor).match(/^ {0,3}(~{3,})[^\n]*(?:\n|$)/); if (fence) { const marker = fence[1]; const closing = new RegExp(`^ {0,3}~{${marker.length},}\\s*$`); let line = cursor + fence[0].length; while (line < content.length) { const next = content.indexOf("\n", line); const end = next < 0 ? content.length : next; if (closing.test(content.slice(line, end))) { line = next < 0 ? end : end + 1; break; } line = next < 0 ? end : end + 1; } cursor = line; continue; } } if (content[cursor] === "\\") { cursor += 2; continue; } const wrapped = content.startsWith("[![", cursor); const image = readImage(content, cursor + (wrapped ? 1 : 0)); if (!image) { cursor += 1; continue; } let end = image.end; if (wrapped) { const outer = content[end] === "]" ? readDestination(content, end + 1) : null; // Preserve incomplete, unsafe and different-target links as Markdown so // Streamdown applies its normal link sanitization and click behavior. if (!outer || outer.url !== image.src) { cursor = outer ? outer.end : end; continue; } end = outer.end; } if (cursor > textStart) { segments.push({ type: "text", value: content.slice(textStart, cursor) }); } segments.push({ type: "image", alt: image.alt, src: image.src }); cursor = end; textStart = end; } if (textStart < content.length) { segments.push({ type: "text", value: content.slice(textStart) }); } return segments; }