mirror of
https://github.com/C4illin/ConvertX.git
synced 2026-03-02 22:47:01 +00:00
feat: add VCF to CSV converter (#497)
This commit is contained in:
@@ -38,6 +38,7 @@ A self-hosted online file converter. Supports over a thousand different formats.
|
||||
| [Dasel](https://github.com/TomWright/dasel) | Data Files | 5 | 4 |
|
||||
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
|
||||
| [msgconvert](https://github.com/mvz/email-outlook-message-perl) | Outlook | 1 | 1 |
|
||||
| VCF to CSV | Contacts | 1 | 1 |
|
||||
| [dvisvgm](https://dvisvgm.de/) | Vector images | 4 | 2 |
|
||||
| [ImageMagick](https://imagemagick.org/) | Images | 245 | 183 |
|
||||
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 167 | 130 |
|
||||
|
||||
@@ -22,6 +22,7 @@ import { convert as convertPotrace, properties as propertiesPotrace } from "./po
|
||||
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
|
||||
import { convert as convertImage, properties as propertiesImage } from "./vips";
|
||||
import { convert as convertVtracer, properties as propertiesVtracer } from "./vtracer";
|
||||
import { convert as convertVcf, properties as propertiesVcf } from "./vcf";
|
||||
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
|
||||
import { convert as convertMarkitdown, properties as propertiesMarkitdown } from "./markitdown";
|
||||
|
||||
@@ -128,6 +129,10 @@ const properties: Record<
|
||||
properties: propertiesVtracer,
|
||||
converter: convertVtracer,
|
||||
},
|
||||
vcf: {
|
||||
properties: propertiesVcf,
|
||||
converter: convertVcf,
|
||||
},
|
||||
markitDown: {
|
||||
properties: propertiesMarkitdown,
|
||||
converter: convertMarkitdown,
|
||||
|
||||
69
src/converters/vcf.ts
Normal file
69
src/converters/vcf.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
contacts: ["vcf"],
|
||||
},
|
||||
to: {
|
||||
contacts: ["csv"],
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
return cards
|
||||
.map((card) => {
|
||||
if (!card) return {};
|
||||
const lines = card.split("\n").filter((line) => line.trim());
|
||||
const contact: Record<string, string> = {};
|
||||
for (const line of lines) {
|
||||
const colonIndex = line.indexOf(":");
|
||||
if (colonIndex === -1) continue;
|
||||
const key = line.slice(0, colonIndex).trim();
|
||||
const value = line.slice(colonIndex + 1).trim();
|
||||
if (key === "FN") {
|
||||
contact["Full Name"] = value;
|
||||
} else if (key === "N") {
|
||||
const parts = value.split(";");
|
||||
contact["Last Name"] = parts[0] || "";
|
||||
contact["First Name"] = parts[1] || "";
|
||||
} 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;
|
||||
})
|
||||
.filter((contact) => Object.keys(contact).length > 0);
|
||||
}
|
||||
|
||||
export function toCSV(data: Record<string, string>[]): string {
|
||||
if (!data.length) return "";
|
||||
const first = data[0];
|
||||
if (!first) return "";
|
||||
const headers = Object.keys(first);
|
||||
const escape = (str: string) => `"${str.replace(/"/g, '""')}"`;
|
||||
const rows = data.map((row) => headers.map((h) => escape(row[h] || "")).join(","));
|
||||
return [headers.join(","), ...rows].join("\n");
|
||||
}
|
||||
|
||||
export async function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
options?: unknown, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
): Promise<string> {
|
||||
const vcfData = await readFile(filePath, "utf-8");
|
||||
const contacts = parseVCF(vcfData);
|
||||
const csvData = toCSV(contacts);
|
||||
await writeFile(targetPath, csvData, "utf-8");
|
||||
return "Done";
|
||||
}
|
||||
98
tests/converters/vcf.test.ts
Normal file
98
tests/converters/vcf.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { convert, parseVCF, toCSV } from "../../src/converters/vcf";
|
||||
|
||||
describe("parseVCF", () => {
|
||||
test("should parse a simple VCF card", () => {
|
||||
const vcfData = `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:John Doe
|
||||
N:Doe;John;;;
|
||||
TEL:+123456789
|
||||
EMAIL:john@example.com
|
||||
ORG:Example Corp
|
||||
END:VCARD`;
|
||||
|
||||
const result = parseVCF(vcfData);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
"Full Name": "John Doe",
|
||||
"Last Name": "Doe",
|
||||
"First Name": "John",
|
||||
Phone: "+123456789",
|
||||
Email: "john@example.com",
|
||||
Organization: "Example Corp",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle multiple cards", () => {
|
||||
const vcfData = `BEGIN:VCARD
|
||||
FN:John Doe
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
FN:Jane Smith
|
||||
END:VCARD`;
|
||||
|
||||
const result = parseVCF(vcfData);
|
||||
|
||||
expect(result).toEqual([{ "Full Name": "John Doe" }, { "Full Name": "Jane Smith" }]);
|
||||
});
|
||||
|
||||
test("should parse VCF with TYPE parameters", () => {
|
||||
const vcfData = `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:John Doe
|
||||
N:Doe;John;;;
|
||||
TEL;TYPE=WORK,VOICE:(111) 555-1212
|
||||
EMAIL;TYPE=PREF,INTERNET:john.doe@example.com
|
||||
END:VCARD`;
|
||||
|
||||
const result = parseVCF(vcfData);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
"Full Name": "John Doe",
|
||||
"Last Name": "Doe",
|
||||
"First Name": "John",
|
||||
Phone: "(111) 555-1212",
|
||||
Email: "john.doe@example.com",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toCSV", () => {
|
||||
test("should convert contacts to CSV", () => {
|
||||
const contacts = [
|
||||
{
|
||||
"Full Name": "John Doe",
|
||||
Phone: "+123",
|
||||
Email: "john@example.com",
|
||||
},
|
||||
];
|
||||
|
||||
const result = toCSV(contacts);
|
||||
|
||||
expect(result).toBe('Full Name,Phone,Email\n"John Doe","+123","john@example.com"');
|
||||
});
|
||||
|
||||
test("should escape quotes", () => {
|
||||
const contacts = [{ "Full Name": 'John "Johnny" Doe' }];
|
||||
|
||||
const result = toCSV(contacts);
|
||||
|
||||
expect(result).toBe('Full Name\n"John ""Johnny"" Doe"');
|
||||
});
|
||||
|
||||
test("should handle empty data", () => {
|
||||
const result = toCSV([]);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("convert", () => {
|
||||
test("should be a function", () => {
|
||||
expect(typeof convert).toBe("function");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user