export function inferAttachmentMimeType(fileName: string, declaredType?: string): string { const safeDeclaredType = typeof declaredType === "string" ? declaredType.trim() : "" if (safeDeclaredType && safeDeclaredType !== "application/octet-stream") { return safeDeclaredType } const extension = fileName.toLowerCase().split(".").pop() || "" const extensionToMime: Record = { md: "text/plain", markdown: "text/plain", txt: "text/plain", log: "text/plain", json: "application/json", csv: "text/csv", yml: "text/plain", yaml: "text/plain", html: "text/html", htm: "text/html", xml: "application/xml", js: "text/javascript", mjs: "text/javascript", cjs: "text/javascript", ts: "text/plain", tsx: "text/plain", jsx: "text/plain", swift: "text/plain", m: "text/plain", mm: "text/plain", pbxproj: "text/plain", xcconfig: "text/plain", strings: "text/plain", plist: "application/xml", stringsdict: "application/xml", entitlements: "application/xml", storyboard: "application/xml", xib: "application/xml", metal: "text/plain", css: "text/css", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", pdf: "application/pdf", } return extensionToMime[extension] || "application/octet-stream" } export function isMarkdownAttachment(fileName: string, mimeType?: string): boolean { const extension = fileName.toLowerCase().split(".").pop() || "" if (extension === "md" || extension === "markdown") return true return (mimeType || "").toLowerCase() === "text/markdown" } export function isTextPreviewAttachment(fileName: string, mimeType?: string): boolean { const extension = fileName.toLowerCase().split(".").pop() || "" const textLikeExtensions = new Set([ "txt", "log", "json", "csv", "xml", "yml", "yaml", "ini", "cfg", "conf", "env", "toml", "properties", "sql", "sh", "bash", "zsh", "fish", "ps1", "py", "rb", "php", "go", "rs", "java", "kt", "swift", "pbxproj", "plist", "strings", "stringsdict", "xcconfig", "entitlements", "storyboard", "xib", "m", "mm", "metal", "c", "h", "cpp", "hpp", "cs", "js", "mjs", "cjs", "ts", "tsx", "jsx", "css", "scss", "sass", "less", "vue", "svelte", "html", "htm", ]) const safeMimeType = (mimeType || "").toLowerCase() if (textLikeExtensions.has(extension)) return true if (safeMimeType.startsWith("text/")) return true if (safeMimeType === "application/json" || safeMimeType === "application/xml") return true return false } export function coerceDataUrlMimeType(dataUrl: string, mimeType: string): string { if (!dataUrl.startsWith("data:")) return dataUrl const commaIndex = dataUrl.indexOf(",") if (commaIndex === -1) return dataUrl const meta = dataUrl.slice(0, commaIndex) const payload = dataUrl.slice(commaIndex + 1) const hasBase64 = meta.includes(";base64") const encodingSuffix = hasBase64 ? ";base64" : "" return `data:${mimeType}${encodingSuffix},${payload}` } export async function blobFromDataUrl(dataUrl: string, mimeTypeOverride?: string): Promise { const response = await fetch(dataUrl) const blob = await response.blob() if (!mimeTypeOverride || blob.type === mimeTypeOverride) return blob const buffer = await blob.arrayBuffer() return new Blob([buffer], { type: mimeTypeOverride }) } export async function textFromDataUrl(dataUrl: string): Promise { const response = await fetch(dataUrl) return response.text() } function safeScriptString(value: string): string { return JSON.stringify(value).replace(/<\/script/gi, "<\\/script") } function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) } function getExtension(fileName: string): string { return fileName.toLowerCase().split(".").pop() || "" } function isJsonAttachment(fileName: string, mimeType?: string): boolean { const extension = getExtension(fileName) return extension === "json" || (mimeType || "").toLowerCase() === "application/json" } function isCsvAttachment(fileName: string, mimeType?: string): boolean { const extension = getExtension(fileName) return extension === "csv" || (mimeType || "").toLowerCase() === "text/csv" } function isHtmlAttachment(fileName: string, mimeType?: string): boolean { const extension = getExtension(fileName) const safeMimeType = (mimeType || "").toLowerCase() return extension === "html" || extension === "htm" || safeMimeType === "text/html" } function inferCodeLanguage(fileName: string, mimeType?: string): string { const extension = getExtension(fileName) const safeMimeType = (mimeType || "").toLowerCase() const extensionToLanguage: Record = { swift: "swift", m: "objectivec", mm: "objectivec", h: "objectivec", pbxproj: "ini", xcconfig: "ini", strings: "ini", stringsdict: "xml", plist: "xml", entitlements: "xml", storyboard: "xml", xib: "xml", metal: "cpp", json: "json", csv: "plaintext", yml: "yaml", yaml: "yaml", xml: "xml", html: "xml", htm: "xml", js: "javascript", mjs: "javascript", cjs: "javascript", ts: "typescript", tsx: "typescript", jsx: "javascript", css: "css", scss: "scss", sass: "scss", less: "less", sh: "bash", bash: "bash", zsh: "bash", fish: "bash", ps1: "powershell", py: "python", rb: "ruby", php: "php", go: "go", rs: "rust", java: "java", kt: "kotlin", c: "c", cpp: "cpp", hpp: "cpp", cs: "csharp", sql: "sql", vue: "xml", svelte: "xml", md: "markdown", markdown: "markdown", txt: "plaintext", log: "plaintext", ini: "ini", cfg: "ini", conf: "ini", env: "ini", toml: "ini", properties: "ini", } const fromExtension = extensionToLanguage[extension] if (fromExtension) return fromExtension if (safeMimeType === "application/json") return "json" if (safeMimeType === "application/xml" || safeMimeType === "text/xml") return "xml" if (safeMimeType === "text/html") return "xml" if (safeMimeType === "text/css") return "css" if (safeMimeType === "text/javascript") return "javascript" return "plaintext" } function parseCsvLine(line: string): string[] { const cells: string[] = [] let current = "" let index = 0 let inQuotes = false while (index < line.length) { const char = line[index] if (char === '"') { const next = line[index + 1] if (inQuotes && next === '"') { current += '"' index += 2 continue } inQuotes = !inQuotes index += 1 continue } if (char === "," && !inQuotes) { cells.push(current) current = "" index += 1 continue } current += char index += 1 } cells.push(current) return cells } function csvTableHtml(content: string): string { const lines = content.split(/\r?\n/).filter((line) => line.trim().length > 0).slice(0, 200) if (lines.length === 0) return '

No CSV rows to preview.

' const rows = lines.map(parseCsvLine) const maxColumns = rows.reduce((max, row) => Math.max(max, row.length), 0) const normalizedRows = rows.map((row) => [...row, ...Array.from({ length: maxColumns - row.length }, () => "")]) const header = normalizedRows[0] const body = normalizedRows.slice(1) const headerCells = header.map((cell) => `${escapeHtml(cell || "Column")}`).join("") const bodyRows = body .map((row) => `${row.map((cell) => `${escapeHtml(cell)}`).join("")}`) .join("") return `${headerCells}${bodyRows}
` } function prettyJsonOrRaw(content: string): string { try { return JSON.stringify(JSON.parse(content), null, 2) } catch { return content } } export function createTextPreviewHtml(fileName: string, content: string, mimeType?: string): string { const safeTitle = escapeHtml(fileName) const safeMimeType = (mimeType || "").toLowerCase() const isHtml = isHtmlAttachment(fileName, safeMimeType) const isJson = isJsonAttachment(fileName, safeMimeType) const isCsv = isCsvAttachment(fileName, safeMimeType) const language = inferCodeLanguage(fileName, safeMimeType) const displayedText = isJson ? prettyJsonOrRaw(content) : content const sourceLiteral = safeScriptString(displayedText) const htmlLiteral = safeScriptString(content) const renderedCsv = isCsv ? csvTableHtml(content) : "" return ` ${safeTitle}
Attachment Preview: ${safeTitle}${safeMimeType ? ` · ${escapeHtml(safeMimeType)}` : ""}
${isHtml ? `
` : ""} ${isCsv ? `
${renderedCsv}
` : ""} ${isHtml ? '
View source' : ""}
${isHtml ? '
' : ""}
` } export function createMarkdownPreviewHtml(fileName: string, markdown: string): string { const safeTitle = fileName.replace(/[<>&"]/g, (char) => { if (char === "<") return "<" if (char === ">") return ">" if (char === "&") return "&" return """ }) const markdownLiteral = safeScriptString(markdown) return ` ${safeTitle}
Markdown Preview: ${safeTitle}
`; } export async function markdownPreviewObjectUrl(fileName: string, dataUrl: string): Promise { const markdown = await textFromDataUrl(dataUrl) const html = createMarkdownPreviewHtml(fileName, markdown) return URL.createObjectURL(new Blob([html], { type: "text/html" })) } export async function textPreviewObjectUrl(fileName: string, dataUrl: string, mimeType?: string): Promise { const content = await textFromDataUrl(dataUrl) const html = createTextPreviewHtml(fileName, content, mimeType) return URL.createObjectURL(new Blob([html], { type: "text/html" })) }