Parse vCard
Parse vCard contact files into structured data
Source Code
import fs from "fs";
import path from "path";
const [inputPath, outputPath] = process.argv.slice(2);
if (!inputPath || !outputPath) {
console.error("Usage: inputPath outputPath");
process.exit(1);
}
/**
* Parse a single vCard block into structured data
*/
function parseVCard(vcardText) {
const contact = {
name: null,
firstName: null,
lastName: null,
organization: null,
title: null,
emails: [],
phones: [],
addresses: [],
urls: [],
notes: null,
birthday: null,
};
const lines = vcardText.split(/\r?\n/);
let currentLine = "";
for (const line of lines) {
// Handle line folding (lines starting with space/tab are continuations)
if (line.startsWith(" ") || line.startsWith("\t")) {
currentLine += line.slice(1);
continue;
}
if (currentLine) {
processLine(currentLine, contact);
}
currentLine = line;
}
if (currentLine) {
processLine(currentLine, contact);
}
return contact;
}
/**
* Process a single vCard property line
*/
function processLine(line, contact) {
const colonIndex = line.indexOf(":");
if (colonIndex === -1) return;
const propertyPart = line.slice(0, colonIndex);
const value = line.slice(colonIndex + 1);
// Parse property name and parameters
const [propName, ...params] = propertyPart.split(";");
const upperProp = propName.toUpperCase();
// Parse parameters into object
const paramObj = {};
for (const param of params) {
const [key, val] = param.split("=");
paramObj[key?.toUpperCase()] = val;
}
switch (upperProp) {
case "FN":
contact.name = decodeValue(value);
break;
case "N":
const nameParts = value.split(";");
contact.lastName = decodeValue(nameParts[0]) || null;
contact.firstName = decodeValue(nameParts[1]) || null;
break;
case "ORG":
contact.organization = decodeValue(value.split(";")[0]);
break;
case "TITLE":
contact.title = decodeValue(value);
break;
case "EMAIL":
contact.emails.push({
value: decodeValue(value),
type: paramObj.TYPE || "other",
});
break;
case "TEL":
contact.phones.push({
value: decodeValue(value),
type: paramObj.TYPE || "other",
});
break;
case "ADR":
const addrParts = value.split(";");
contact.addresses.push({
type: paramObj.TYPE || "other",
street: decodeValue(addrParts[2]) || null,
city: decodeValue(addrParts[3]) || null,
state: decodeValue(addrParts[4]) || null,
zip: decodeValue(addrParts[5]) || null,
country: decodeValue(addrParts[6]) || null,
});
break;
case "URL":
contact.urls.push(decodeValue(value));
break;
case "NOTE":
contact.notes = decodeValue(value);
break;
case "BDAY":
contact.birthday = value;
break;
}
}
/**
* Decode vCard encoded values
*/
function decodeValue(value) {
if (!value) return null;
// Handle quoted-printable encoding
value = value.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
// Handle escaped characters
value = value.replace(/\\n/gi, "\n").replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\");
return value.trim();
}
try {
console.log(`Parsing vCard: ${inputPath}...`);
const vcfContent = fs.readFileSync(inputPath, "utf-8");
// Split into individual vCards
const vcardBlocks = vcfContent.split(/(?=BEGIN:VCARD)/i).filter((block) => block.trim().toUpperCase().startsWith("BEGIN:VCARD"));
console.log(` Found ${vcardBlocks.length} contacts`);
const contacts = vcardBlocks.map(parseVCard);
// Ensure output directory exists
const dir = path.dirname(outputPath);
if (dir && dir !== ".") {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(outputPath, JSON.stringify(contacts, null, 2));
console.log(`\nā Parsed vCard`);
console.log(` Contacts: ${contacts.length}`);
console.log(` Written to: ${outputPath}`);
console.log(
JSON.stringify({
success: true,
inputPath,
outputPath,
contactCount: contacts.length,
})
);
} catch (error) {
console.error("Error:", error.message);
process.exit(1);
}