// Card building core for the Chat Cards canvas extension. This module is // dependency-free so the extension folder stays self-contained for reuse // outside this repository. It knows nothing about the Copilot SDK or the // page server; extension.mjs wires those. Everything here is pure string // logic, so it can be exercised with plain Node without joining a session. // --------------------------------------------------------------------------- // Escaping and small string helpers // --------------------------------------------------------------------------- const HTML_ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; // Every text field an action accepts is PLAIN text and is escaped here, so a // caller must not pre-escape. Callers do it anyway, and turning their // "&" into "&amp;" shows the entity literally in the card. A "&" that // already begins a well-formed character reference is therefore left alone. // This costs nothing in safety: < > " and ' are still escaped // unconditionally, so no pre-escaped text can reopen a tag or an attribute. const ESCAPE_PATTERN = /[<>"']|&(?!#\d{1,7};|#[xX][0-9a-fA-F]{1,6};|[a-zA-Z][a-zA-Z0-9]{1,31};)/g; export function escapeHtml(value) { return String(value ?? "").replace(ESCAPE_PATTERN, (ch) => HTML_ESCAPES[ch]); } let uidCounter = 0; export function uid(prefix = "mcc") { uidCounter += 1; return `${prefix}-${Date.now().toString(36)}-${uidCounter.toString(36)}`; } export function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } export function isHttpUrl(value) { try { const url = new URL(value); return url.protocol === "http:" || url.protocol === "https:"; } catch { return false; } } export function looksLikeUrl(value) { return /^https?:\/\/\S+$/i.test(String(value ?? "").trim()); } export function isDataUrlOfType(value, type) { return new RegExp(`^data:${type}/[a-z0-9.+-]+\\s*[;,]`, "i").test(String(value ?? "").trim()); } export function isBlobUrl(value) { return /^blob:\S+$/i.test(String(value ?? "").trim()); } // Sources a