feat: add VCF to CSV converter
Add support for converting VCF contact files to CSV format - Implement TypeScript-based VCF parsing without external dependencies - Create CSV export with proper quote escaping - Add comprehensive test coverage for various VCF formats - Update documentation and converter registry Closes #396
This commit is contained in:
parent
d22f6ad213
commit
ddb8de4f96
2 changed files with 58 additions and 57 deletions
|
|
@ -10,42 +10,48 @@ export const properties = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseVCF(data: string): Record<string, string>[] {
|
export function parseVCF(data: string): Record<string, string>[] {
|
||||||
const cards = data.split(/BEGIN:VCARD/).slice(1).map(card => card.split(/END:VCARD/)[0]).filter(card => card);
|
const cards = data
|
||||||
return cards.map(card => {
|
.split(/BEGIN:VCARD/)
|
||||||
if (!card) return {};
|
.slice(1)
|
||||||
const lines = card.split('\n').filter(line => line.trim());
|
.map((card) => card.split(/END:VCARD/)[0])
|
||||||
const contact: Record<string, string> = {};
|
.filter((card) => card);
|
||||||
for (const line of lines) {
|
return cards
|
||||||
const colonIndex = line.indexOf(':');
|
.map((card) => {
|
||||||
if (colonIndex === -1) continue;
|
if (!card) return {};
|
||||||
const key = line.slice(0, colonIndex).trim();
|
const lines = card.split("\n").filter((line) => line.trim());
|
||||||
const value = line.slice(colonIndex + 1).trim();
|
const contact: Record<string, string> = {};
|
||||||
if (key === 'FN') {
|
for (const line of lines) {
|
||||||
contact['Full Name'] = value;
|
const colonIndex = line.indexOf(":");
|
||||||
} else if (key === 'N') {
|
if (colonIndex === -1) continue;
|
||||||
const parts = value.split(';');
|
const key = line.slice(0, colonIndex).trim();
|
||||||
contact['Last Name'] = parts[0] || '';
|
const value = line.slice(colonIndex + 1).trim();
|
||||||
contact['First Name'] = parts[1] || '';
|
if (key === "FN") {
|
||||||
} else if (key.startsWith('TEL')) {
|
contact["Full Name"] = value;
|
||||||
contact['Phone'] = value;
|
} else if (key === "N") {
|
||||||
} else if (key.startsWith('EMAIL')) {
|
const parts = value.split(";");
|
||||||
contact['Email'] = value;
|
contact["Last Name"] = parts[0] || "";
|
||||||
} else if (key === 'ORG') {
|
contact["First Name"] = parts[1] || "";
|
||||||
contact['Organization'] = value.split(';')[0] || '';
|
} else if (key.startsWith("TEL")) {
|
||||||
|
contact["Phone"] = value;
|
||||||
|
} else if (key.startsWith("EMAIL")) {
|
||||||
|
contact["Email"] = value;
|
||||||
|
} else if (key === "ORG") {
|
||||||
|
contact["Organization"] = value.split(";")[0] || "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return contact;
|
||||||
return contact;
|
})
|
||||||
}).filter(contact => Object.keys(contact).length > 0);
|
.filter((contact) => Object.keys(contact).length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toCSV(data: Record<string, string>[]): string {
|
export function toCSV(data: Record<string, string>[]): string {
|
||||||
if (!data.length) return '';
|
if (!data.length) return "";
|
||||||
const first = data[0];
|
const first = data[0];
|
||||||
if (!first) return '';
|
if (!first) return "";
|
||||||
const headers = Object.keys(first);
|
const headers = Object.keys(first);
|
||||||
const escape = (str: string) => `"${str.replace(/"/g, '""')}"`;
|
const escape = (str: string) => `"${str.replace(/"/g, '""')}"`;
|
||||||
const rows = data.map(row => headers.map(h => escape(row[h] || '')).join(','));
|
const rows = data.map((row) => headers.map((h) => escape(row[h] || "")).join(","));
|
||||||
return [headers.join(','), ...rows].join('\n');
|
return [headers.join(","), ...rows].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function convert(
|
export async function convert(
|
||||||
|
|
@ -53,11 +59,11 @@ export async function convert(
|
||||||
fileType: string,
|
fileType: string,
|
||||||
convertTo: string,
|
convertTo: string,
|
||||||
targetPath: string,
|
targetPath: string,
|
||||||
options?: unknown,
|
options?: unknown, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const vcfData = await readFile(filePath, 'utf-8');
|
const vcfData = await readFile(filePath, "utf-8");
|
||||||
const contacts = parseVCF(vcfData);
|
const contacts = parseVCF(vcfData);
|
||||||
const csvData = toCSV(contacts);
|
const csvData = toCSV(contacts);
|
||||||
await writeFile(targetPath, csvData, 'utf-8');
|
await writeFile(targetPath, csvData, "utf-8");
|
||||||
return "Done";
|
return "Done";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@ END:VCARD`;
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
'Full Name': 'John Doe',
|
"Full Name": "John Doe",
|
||||||
'Last Name': 'Doe',
|
"Last Name": "Doe",
|
||||||
'First Name': 'John',
|
"First Name": "John",
|
||||||
'Phone': '+123456789',
|
Phone: "+123456789",
|
||||||
'Email': 'john@example.com',
|
Email: "john@example.com",
|
||||||
'Organization': 'Example Corp'
|
Organization: "Example Corp",
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -36,10 +36,7 @@ END:VCARD`;
|
||||||
|
|
||||||
const result = parseVCF(vcfData);
|
const result = parseVCF(vcfData);
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([{ "Full Name": "John Doe" }, { "Full Name": "Jane Smith" }]);
|
||||||
{ 'Full Name': 'John Doe' },
|
|
||||||
{ 'Full Name': 'Jane Smith' }
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should parse VCF with TYPE parameters", () => {
|
test("should parse VCF with TYPE parameters", () => {
|
||||||
|
|
@ -55,12 +52,12 @@ END:VCARD`;
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
'Full Name': 'John Doe',
|
"Full Name": "John Doe",
|
||||||
'Last Name': 'Doe',
|
"Last Name": "Doe",
|
||||||
'First Name': 'John',
|
"First Name": "John",
|
||||||
'Phone': '(111) 555-1212',
|
Phone: "(111) 555-1212",
|
||||||
'Email': 'john.doe@example.com'
|
Email: "john.doe@example.com",
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -69,10 +66,10 @@ describe("toCSV", () => {
|
||||||
test("should convert contacts to CSV", () => {
|
test("should convert contacts to CSV", () => {
|
||||||
const contacts = [
|
const contacts = [
|
||||||
{
|
{
|
||||||
'Full Name': 'John Doe',
|
"Full Name": "John Doe",
|
||||||
'Phone': '+123',
|
Phone: "+123",
|
||||||
'Email': 'john@example.com'
|
Email: "john@example.com",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = toCSV(contacts);
|
const result = toCSV(contacts);
|
||||||
|
|
@ -81,9 +78,7 @@ describe("toCSV", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should escape quotes", () => {
|
test("should escape quotes", () => {
|
||||||
const contacts = [
|
const contacts = [{ "Full Name": 'John "Johnny" Doe' }];
|
||||||
{ 'Full Name': 'John "Johnny" Doe' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = toCSV(contacts);
|
const result = toCSV(contacts);
|
||||||
|
|
||||||
|
|
@ -92,7 +87,7 @@ describe("toCSV", () => {
|
||||||
|
|
||||||
test("should handle empty data", () => {
|
test("should handle empty data", () => {
|
||||||
const result = toCSV([]);
|
const result = toCSV([]);
|
||||||
expect(result).toBe('');
|
expect(result).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue