import { fullContactDisplayName, type FullContact } from "./types" function escapeVCardValue(value: string): string { return value .replace(/\\/g, "\\\\") .replace(/;/g, "\\;") .replace(/,/g, "\\,") .replace(/\n/g, "\\n") } function escapeCsvField(value: string): string { if (/[",\n\r]/.test(value)) { return `"${value.replace(/"/g, '""')}"` } return value } function sanitizeFilename(name: string): string { return name.replace(/[/\\?%*:|"<>]/g, "-").trim() || "contact" } export function contactToVCard(contact: FullContact): string { const lines: string[] = ["BEGIN:VCARD", "VERSION:3.0"] const last = contact.lastName ?? "" const first = contact.firstName ?? "" const middle = contact.middleName ?? "" const prefix = contact.namePrefix ?? "" const suffix = contact.nameSuffix ?? "" lines.push( `N:${escapeVCardValue(last)};${escapeVCardValue(first)};${escapeVCardValue(middle)};${escapeVCardValue(prefix)};${escapeVCardValue(suffix)}` ) const fn = fullContactDisplayName(contact) || contact.emails[0]?.value || contact.phones[0]?.value if (fn) lines.push(`FN:${escapeVCardValue(fn)}`) for (const e of contact.emails) { if (e.value.trim()) { lines.push(`EMAIL;TYPE=${escapeVCardValue(e.label || "INTERNET")}:${escapeVCardValue(e.value.trim())}`) } } for (const p of contact.phones) { if (p.value.trim()) { lines.push(`TEL;TYPE=${escapeVCardValue(p.label || "VOICE")}:${escapeVCardValue(p.value.trim())}`) } } if (contact.company?.trim()) { const org = contact.jobTitle?.trim() ? `${escapeVCardValue(contact.company.trim())};${escapeVCardValue(contact.jobTitle.trim())}` : escapeVCardValue(contact.company.trim()) lines.push(`ORG:${org}`) } else if (contact.jobTitle?.trim()) { lines.push(`TITLE:${escapeVCardValue(contact.jobTitle.trim())}`) } if (contact.department?.trim()) { lines.push(`X-ABLabel:${escapeVCardValue(contact.department.trim())}`) } if (contact.birthday?.month && contact.birthday?.day) { const y = contact.birthday.year ?? 1900 const m = String(contact.birthday.month).padStart(2, "0") const d = String(contact.birthday.day).padStart(2, "0") lines.push(`BDAY:${y}${m}${d}`) } if (contact.notes?.trim()) { lines.push(`NOTE:${escapeVCardValue(contact.notes.trim())}`) } lines.push("END:VCARD") return lines.join("\r\n") } export function contactsToVCard(contacts: FullContact[]): string { return contacts.map(contactToVCard).join("\r\n") } export function contactsToCsv(contacts: FullContact[]): string { const header = ["Name", "Email", "Phone", "Company", "Job Title", "Notes"] const rows = contacts.map((c) => { const name = fullContactDisplayName(c) const email = c.emails.map((e) => e.value).join("; ") const phone = c.phones.map((p) => p.value).join("; ") const company = c.company ?? "" const jobTitle = c.jobTitle ?? "" const notes = c.notes ?? "" return [name, email, phone, company, jobTitle, notes].map(escapeCsvField).join(",") }) return [header.join(","), ...rows].join("\r\n") } export function downloadTextFile(content: string, filename: string, mimeType: string): void { const blob = new Blob([content], { type: mimeType }) const url = URL.createObjectURL(blob) const link = document.createElement("a") link.href = url link.download = filename link.click() URL.revokeObjectURL(url) } export function downloadContactVCard(contact: FullContact): void { const base = sanitizeFilename( fullContactDisplayName(contact) || contact.emails[0]?.value || "contact" ) downloadTextFile(contactToVCard(contact), `${base}.vcf`, "text/vcard;charset=utf-8") } export function downloadContactsVCard(contacts: FullContact[], filename = "contacts.vcf"): void { downloadTextFile(contactsToVCard(contacts), filename, "text/vcard;charset=utf-8") } export function downloadContactsCsv(contacts: FullContact[], filename = "contacts.csv"): void { downloadTextFile(contactsToCsv(contacts), filename, "text/csv;charset=utf-8") }