import { Marked, type Tokens } from "marked"; const EXTERNAL_LINK_PROTOCOLS = new Set(["http:", "https:"]); export interface AiMarkdownLinkClickEvent { target: unknown; currentTarget: unknown; preventDefault: () => void; stopPropagation: () => void; } export type AiMarkdownLinkOpener = (url: string) => void | Promise; const markedInstance = new Marked({ breaks: true, gfm: true, renderer: { codespan({ text }: Tokens.Codespan) { return `${escapeHtml(text)}`; }, html({ text }: Tokens.HTML | Tokens.Tag) { return escapeHtml(text); }, link({ href, title, tokens }: Tokens.Link) { const label = this.parser.parseInline(tokens); const safeHref = normalizeAiMarkdownLink(href); if (!safeHref) return label; const titleAttr = title ? ` title="${escapeHtml(title)}"` : ""; return `${label}`; }, }, }); export function formatAiInlineMarkdown(text: string): string { try { return markedInstance.parse(text) as string; } catch { return escapeHtml(text); } } export function normalizeAiMarkdownLink(href: string): string | null { try { const url = new URL(href); return EXTERNAL_LINK_PROTOCOLS.has(url.protocol) ? url.toString() : null; } catch { return null; } } export function aiMarkdownLinkUrlFromClick(target: unknown, currentTarget: unknown): string | null { const anchor = closestAnchor(target); if (!anchor) return null; if (hasContains(currentTarget) && !currentTarget.contains(anchor)) return null; const href = anchor.getAttribute("href"); return href ? normalizeAiMarkdownLink(href) : null; } export function handleAiMarkdownLinkClick(event: AiMarkdownLinkClickEvent, openUrl: AiMarkdownLinkOpener): boolean { const url = aiMarkdownLinkUrlFromClick(event.target, event.currentTarget); if (!url) return false; event.preventDefault(); event.stopPropagation(); void openUrl(url); return true; } function escapeHtml(value: string): string { return value.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } interface AnchorLike { getAttribute: (name: string) => string | null; } function closestAnchor(target: unknown): AnchorLike | null { const closestSource = hasClosest(target) ? target : parentWithClosest(target); if (!closestSource) return null; const anchor = closestSource.closest("a[href]"); return hasGetAttribute(anchor) ? anchor : null; } function parentWithClosest(target: unknown): { closest: (selector: string) => unknown } | null { if (!target || typeof target !== "object" || !("parentElement" in target)) return null; const parentElement = target.parentElement; return hasClosest(parentElement) ? parentElement : null; } function hasClosest(value: unknown): value is { closest: (selector: string) => unknown } { return !!value && typeof value === "object" && "closest" in value && typeof value.closest === "function"; } function hasGetAttribute(value: unknown): value is AnchorLike { return !!value && typeof value === "object" && "getAttribute" in value && typeof value.getAttribute === "function"; } function hasContains(value: unknown): value is { contains: (node: unknown) => boolean } { return !!value && typeof value === "object" && "contains" in value && typeof value.contains === "function"; }