export type ChatMarkdownProps = { children: string; streaming?: boolean; }; const protocolPattern = /^([a-z][a-z\d+.-]*):/i; const safeProtocols = new Set(["http", "https", "mailto", "tel"]); const hexColorPattern = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/; const numericCitationPattern = /^\[?(\d{1,3})\]?$/; export function sanitizeMarkdownUrl(url: string, allowRelative = false): string | undefined { const value = url.trim(); const protocol = value.match(protocolPattern)?.[1]?.toLowerCase(); if (protocol) return safeProtocols.has(protocol) ? value : undefined; if ( allowRelative && (value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("#")) ) { return value; } return undefined; } export function isExternalMarkdownUrl(url: string | undefined): boolean { if (!url) return false; const protocol = url.trim().match(protocolPattern)?.[1]?.toLowerCase(); return Boolean(protocol && safeProtocols.has(protocol)); } export function parseCitationBadgeLabel( label: string | undefined, href: string | undefined, ): string | undefined { if (!label || !href || !isExternalMarkdownUrl(href)) return undefined; const trimmed = label.trim(); const numeric = trimmed.match(numericCitationPattern); if (numeric?.[1]) return numeric[1]; return undefined; } export function citationDomainLabel(href: string | undefined): string | undefined { if (!href) return undefined; try { const parsed = new URL(href.trim()); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return undefined; return parsed.hostname.toLowerCase().replace(/^www\./, ""); } catch { return undefined; } } export function parseHexColorToken(value: string | undefined): string | undefined { if (!value) return undefined; const trimmed = value.trim(); return hexColorPattern.test(trimmed) ? trimmed : undefined; } export function closeUnterminatedFence(markdown: string): string { let openFence: { marker: "`" | "~"; length: number } | undefined; for (const line of markdown.split("\n")) { const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/); if (!match?.[1]) continue; const marker = match[1][0] as "`" | "~"; if (!openFence) { openFence = { marker, length: match[1].length }; continue; } if ( marker === openFence.marker && match[1].length >= openFence.length && (match[2] ?? "").trim() === "" ) { openFence = undefined; } } return openFence ? `${markdown}\n${openFence.marker.repeat(openFence.length)}` : markdown; }