Skip to content

Commit

Permalink
improve vcard parsing and supported vcard fields
Browse files Browse the repository at this point in the history
refactored code
  • Loading branch information
motschel123 committed Nov 19, 2023
1 parent 4829080 commit aaf8ec4
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 89 deletions.
2 changes: 1 addition & 1 deletion esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "sync-contacts-macos",
"name": "Sync Contacts on macOS",
"version": "1.0.2",
"version": "1.0.3",
"minAppVersion": "0.15.0",
"description": "Sync your contacts from macOS to your Obsidian Vault.",
"author": "Marcel SchΓΆckel",
Expand Down
29 changes: 26 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "obsidian-sample-plugin",
"version": "1.0.2",
"version": "1.0.3",
"description": "Sync your contacts from macOS to your Obsidian Vault.",
"main": "main.js",
"scripts": {
Expand All @@ -22,6 +22,6 @@
"typescript": "4.7.4"
},
"dependencies": {
"vcard-parser": "^1.0.0"
"vcf": "^2.1.1"
}
}
136 changes: 54 additions & 82 deletions main.ts β†’ src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { App, Notice, Platform, Plugin, PluginSettingTab, Setting, TFile, TFolder, normalizePath } from 'obsidian';
const vCardParser = require('vcard-parser');
import { type } from 'os';
import { isFloat32Array } from 'util/types';
import VCardObject from './vcard-object';
const vCard = require('vcf');
const { spawn } = require('child_process');


Expand All @@ -10,19 +13,7 @@ interface ContactsPluginSettings {
autogenerationStartText: string
autogenerationEndTag: string
autogenerationEndText: string
}

interface VCard {
fn?: { value: string }[];
org?: { value: string[] }[];
email?: { value: string, meta: { type: string[] } }[];
tel?: { value: string, meta: { type: string[] } }[];
adr?: { value: string[], meta: { type: string[] } }[];
note?: { value: string }[];
url?: { value: string, meta: { type: string[] }, namespace?: string }[];
bday?: { value: string }[];
categories?: { value: string }[];
uid?: { value: string }[];
enabledContactFields: string
}

class SettingTab extends PluginSettingTab {
Expand Down Expand Up @@ -59,6 +50,33 @@ class SettingTab extends PluginSettingTab {
this.plugin.settings.contactsGroup = value;
await this.plugin.saveSettings();
}));

new Setting(containerEl)
.setName('Configure the shown contact fields below')
.setDesc('To update the shown contact fields, re-sync your contacts')

for (let attribute of VCardObject.getVCardFields()) {
new Setting(containerEl)
.setName(`${attribute}`)
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.enabledContactFields.includes(attribute))
toggle.onChange(async (value) => {
this.plugin.settings.enabledContactFields = this.toggleEnabledField(attribute, value);
await this.plugin.saveSettings();
console.debug(this.plugin.settings.enabledContactFields)
});
});
}
}

toggleEnabledField(field: string, value: boolean): string {
let enabledFields = this.plugin.settings.enabledContactFields.split(',');
if (value) {
enabledFields.push(field);
} else {
enabledFields = enabledFields.filter((enabledField) => enabledField != field);
}
return enabledFields.join(',');
}
}

Expand All @@ -68,7 +86,8 @@ const DEFAULT_SETTINGS: ContactsPluginSettings = {
autogenerationStartTag: "START",
autogenerationStartText: "Content BELOW this line is AUTOGENERATED and will be REPLACED.",
autogenerationEndTag: "END",
autogenerationEndText: "Content ABOVE this line is AUTOGENERATED and will be REPLACED."
autogenerationEndText: "Content ABOVE this line is AUTOGENERATED and will be REPLACED.",
enabledContactFields: 'nickname,emails,title,organization,telephones,addresses,birthdate,URLs,notes'
}

export default class ContactsPlugin extends Plugin {
Expand Down Expand Up @@ -98,7 +117,6 @@ export default class ContactsPlugin extends Plugin {

// Load contacts from MacOS "Contacts"
let markdownResults = await loadContactsLogic.loadContacts();

// Save all contacts into file
let successfulContacts = 0
let promises: Array<Promise<any>> = [];
Expand Down Expand Up @@ -181,28 +199,16 @@ class LoadContactsLogic {
}

async loadContacts(): Promise<Map<string, string>> {
// Get vCards from Contacts
let vCards: VCard[] = await this.getVCardStringsFromContactsApp();
// Inform user of any vCards without names
vCards.forEach((vcard) => {
if (vcard.fn?.[0]?.value == undefined) {
console.debug(`Found vCard without name: \n ${JSON.stringify(vcard, null, 2)}`);
new Notice('Contact without name found. Check developer console for details.')
}
});
let vCards: VCardObject[] = await this.getVCardStringsFromContactsApp();
// Filter out vCards without names
vCards = vCards.filter((vcard) => {
return vcard.fn?.[0]?.value != undefined;
return vcard.fn != undefined;
});

let filenameToMarkdown = new Map<string, string>();
const filenameToMarkdown = new Map<string, string>();
for (let vcard of vCards) {
let filename = vcard.fn![0].value!;
let markdown = this.vcardToMarkdown(vcard);

filenameToMarkdown.set(filename, markdown);
filenameToMarkdown.set(vcard.getFilename(), vcard.toMarkdown(this.settings.enabledContactFields));
}

return filenameToMarkdown;
}

Expand Down Expand Up @@ -246,11 +252,9 @@ class LoadContactsLogic {
});
}

getVCardStringsFromContactsApp(): Promise<VCard[]> {
getVCardStringsFromContactsApp(): Promise<VCardObject[]> {
const groupName = this.settings.contactsGroup;
const GROUP_NOT_DEFINED_ERROR = "GROUP NOT DEFINED";


const JXA_SCRIPT = `
ObjC.import('Foundation');
const stdout = $.NSFileHandle.fileHandleWithStandardOutput;
Expand All @@ -270,24 +274,34 @@ class LoadContactsLogic {
}
`;

return new Promise((resolve, reject) => {
return new Promise<VCardObject[]>((resolve, reject) => {
let vCardStrBuffer = "";
const vCards: VCardObject[]= [];
// Start JXA Script
const osascript = spawn('osascript', ['-l', 'JavaScript', '-e', JXA_SCRIPT]);

let vCardStrBuffer = "";
const vCards: Array<VCard> = [];
// Handle JXA Script output (vCard strings)
osascript.stdout.on('data', (data: Buffer) => {
vCardStrBuffer += data.toString('utf-8');
// Check if vCard is complete
const regex = /BEGIN:VCARD[\s\S]*?END:VCARD/g;
const matches = vCardStrBuffer.match(regex);
for (let match of matches ?? []) {
const vCard = vCardParser.parse(match) as VCard;
vCards.push(vCard);
const card = new vCard().parse(match);
const vCardObj = new VCardObject(card);
vCards.push(vCardObj);
}
vCardStrBuffer = vCardStrBuffer.replace(regex, "");
});

osascript.on('close', (code: any) => {
if ((vCardStrBuffer = vCardStrBuffer.trim()).length > 0) {
console.error(`JXA Script Error: Possibly Incomplete vCard: ${vCardStrBuffer}`);
console.debug(`Leftover vCardBuffer Length: ${vCardStrBuffer.length}`);
}
console.debug(vCards)
resolve(vCards);
});

osascript.stderr.on('data', (data: Buffer) => {
const errorMsg = data.toString('utf-8');
Expand All @@ -301,48 +315,6 @@ class LoadContactsLogic {

new Notice(`Error retrieving contacts: \n${data}`);
});

osascript.on('close', (code: any) => {
if ((vCardStrBuffer = vCardStrBuffer.trim()).length > 0) {
console.error(`JXA Script Error: Possibly Incomplete vCard: ${vCardStrBuffer}`);
console.debug(`Leftover vCardBuffer Length: ${vCardStrBuffer.length}`);
}
resolve(vCards);
});
});
}

vcardToMarkdown(vcard: VCard): string {
let markdown = `## πŸ‘€ ${vcard.fn?.[0]?.value ?? 'Unknown'}\n`;

if (vcard.tel) markdown += vcard.tel.map(tel => `- ☎️ ${tel.meta?.type?.[0]?.toLowerCase() ?? 'Phone'}: [${tel.value}](tel:${tel.value})\n`).join('');
if (vcard.email) markdown += `- πŸ“§ Email: [${vcard.email[0]?.value}](mailto:${vcard.email[0]?.value})\n`;
if (vcard.url) markdown += `- 🌐 Website: [${vcard.url[0]?.value}](${vcard.url[0]?.value})\n`;
if (vcard.adr) markdown += vcard.adr.map(adr => `- 🏠 Address: ${adr.value.filter(str => str.length > 0).join(", ")}\n`).join(''); // Joining address components by ', '
if (vcard.bday) {
const date = new Date(Date.parse(vcard.bday[0]?.value));
markdown += `- πŸŽ‚ Birthday: ${date.toLocaleDateString()}\n`;
}
if (vcard.org) markdown += `- 🏒 Organization: ${vcard.org[0]?.value?.[0]}\n`;
if (vcard.note) {
let notes_lines = vcard.note[0]?.value.split("\n");
markdown += `- πŸ“ Notes: ${notes_lines[0]}\n`;
for ( let line of notes_lines.slice(1))
markdown += `\t${line}\n`;
}
//if (vcard.x_anniversary) markdown += `- πŸ’ Anniversary: [[${vcard.x_anniversary[0]?.value}]]\n`; // Make sure this field exists in your VCard interface
//if (vcard.geo) markdown += `- πŸ“ Location: ${vcard.geo[0]?.value}\n`;
//if (vcard.role) markdown += `- πŸ’Ό Role: ${vcard.role[0]?.value}\n`;
//if (vcard.title) markdown += `- πŸ“› Title: ${vcard.title[0]?.value}\n`;

//if (vcard.x_gender) {
// const genderEmoji = vcard.x_gender[0]?.value?.toLowerCase() === 'f' ? "\u2640\ufe0f" : "\u2642\ufe0f";
// markdown += `- ${genderEmoji} Gender: ${vcard.x_gender[0]?.value}\n`;
//}

//if (vcard.lang) markdown += `- πŸ—£οΈ Language: ${vcard.lang[0]?.value}\n`;

return markdown.trim();
}

}
Loading

0 comments on commit aaf8ec4

Please sign in to comment.