mirror of
https://github.com/broekema41/obsidian-vcf-contacts.git
synced 2026-07-22 05:42:58 +00:00
extracted the sorting algorith and moved out of the vcard module at this is only important for the md render and we can guarantee the sorting is maintained this way
This commit is contained in:
parent
a01db62ede
commit
66eb591ced
8 changed files with 160 additions and 96 deletions
|
|
@ -1,7 +1,76 @@
|
|||
import { stringifyYaml } from "obsidian";
|
||||
|
||||
const nameKeys = ["N", "FN"];
|
||||
const priorityKeys = [
|
||||
"EMAIL", "TEL", "BDAY","URL",
|
||||
"ORG", "TITLE", "ROLE", "PHOTO"
|
||||
];
|
||||
const adrKeys = [
|
||||
"ADR"
|
||||
];
|
||||
|
||||
function extractBaseKey(key: string): string {
|
||||
if (key.includes("[")) {
|
||||
return key.split("[")[0];
|
||||
} else if (key.includes(".")) {
|
||||
return key.split(".")[0];
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
function groupVCardFields(record: Record<string, any>) {
|
||||
const groups:{
|
||||
name: { [key: string]: any };
|
||||
priority: { [key: string]: any };
|
||||
address: { [key: string]: any };
|
||||
other: { [key: string]: any };
|
||||
} = {
|
||||
name: {},
|
||||
priority: {},
|
||||
address: {},
|
||||
other: {}
|
||||
};
|
||||
|
||||
Object.entries(record).forEach(([key, value]) => {
|
||||
const baseKey = extractBaseKey(key);
|
||||
|
||||
if (nameKeys.includes(baseKey)) {
|
||||
groups.name[key] = value;
|
||||
} else if (priorityKeys.includes(baseKey)) {
|
||||
groups.priority[key] = value;
|
||||
} else if (adrKeys.includes(baseKey)) {
|
||||
groups.address[key] = value;
|
||||
} else {
|
||||
groups.other[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
|
||||
function sortNameItems(record: Record<string, any>) {
|
||||
const nameSortOrder = ['N.PREFIX', 'N.GN', 'N.MN', 'N.FN', 'N.SUFFIX', 'FN'];
|
||||
return stringifyYaml(Object.fromEntries(
|
||||
Object.entries(record).sort(([keyA], [keyB]) => {
|
||||
return nameSortOrder.indexOf(keyA) - nameKeys.indexOf(keyB);
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
function sortedPriorityItems(record: Record<string, any>) {
|
||||
return stringifyYaml(Object.fromEntries(
|
||||
Object.entries(record).sort(([keyA], [keyB]) => {
|
||||
const baseKeyA = extractBaseKey(keyA);
|
||||
const baseKeyB = extractBaseKey(keyB);
|
||||
return priorityKeys.indexOf(baseKeyA) - priorityKeys.indexOf(baseKeyB);
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
export function mdRender(record: Record<string, any>, hashtags: string): string {
|
||||
const { NOTE, ...recordWithoutNote } = record;
|
||||
const groups = groupVCardFields(recordWithoutNote)
|
||||
const myNote = NOTE ? NOTE.replace(/\\n/g, `
|
||||
`) : '';
|
||||
let additionalTags = ''
|
||||
|
|
@ -9,10 +78,12 @@ export function mdRender(record: Record<string, any>, hashtags: string): string
|
|||
const tempTags= recordWithoutNote.CATEGORIES.split(',')
|
||||
additionalTags = `#${tempTags.join(' #')}`
|
||||
}
|
||||
const yamlString = stringifyYaml(recordWithoutNote);
|
||||
|
||||
return `---
|
||||
${yamlString}
|
||||
${sortNameItems(groups.name)}
|
||||
${sortedPriorityItems(groups.priority)}
|
||||
${stringifyYaml(groups.address)}
|
||||
${stringifyYaml(groups.other)}
|
||||
---
|
||||
#### Notes
|
||||
${myNote}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { ensureHasName } from "src/contacts/vcard/shared/ensureHasName";
|
||||
import { sortVCardOFields } from "src/contacts/vcard/shared/sortVcardFields";
|
||||
|
||||
export async function createEmpty() {
|
||||
const vCardObject: Record<string, any> = {
|
||||
|
|
@ -24,6 +23,5 @@ export async function createEmpty() {
|
|||
"CATEGORIES": "",
|
||||
"VERSION": "4.0"
|
||||
}
|
||||
const checkedNameVCardObject = await ensureHasName(vCardObject);
|
||||
return sortVCardOFields(checkedNameVCardObject);
|
||||
return await ensureHasName(vCardObject);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { VCardForObsidianRecord, VCardSupportedKey } from "src/contacts/vcard";
|
||||
import { ensureHasName } from "src/contacts/vcard/shared/ensureHasName";
|
||||
import { sortVCardOFields } from "src/contacts/vcard/shared/sortVcardFields";
|
||||
import { sortVCardOFields } from "src/contacts/sortVcardFields";
|
||||
import { StructuredFields } from "src/contacts/vcard/shared/structuredFields";
|
||||
import { photoLineFromV3toV4 } from "src/util/photoLineFromV3toV4";
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ function parseVCardLine(line: string): VCardForObsidianRecord {
|
|||
let parsedData: Record<string, any> = {};
|
||||
|
||||
const typeValues:string = params["type"] ? `[${params["type"].join(",")}]` : "";
|
||||
if (key.contains('PHOTO') && key.contains('ENCODING=BASE64')) {
|
||||
if (key.includes('PHOTO') && key.includes('ENCODING=BASE64')) {
|
||||
parsedData['PHOTO'] = photoLineFromV3toV4(line);
|
||||
} else if (key === 'VERSION') {
|
||||
parsedData['VERSION'] = '4.0';
|
||||
|
|
@ -165,8 +165,7 @@ export async function parse(vCardData: string): Promise<VCardForObsidianRecord[]
|
|||
Object.assign(vCardObject, indexedParsedLine);
|
||||
}
|
||||
}
|
||||
const checkedNameVCardObject = await ensureHasName(vCardObject);
|
||||
return sortVCardOFields(checkedNameVCardObject);
|
||||
return await ensureHasName(vCardObject);
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,17 @@ export async function ensureHasName(vCardObject: VCardForObsidianRecord): Promis
|
|||
resolve(vCardObject);
|
||||
} else {
|
||||
new ContactNameModal(app, vCardObject['FN'], (givenName, familyName) => {
|
||||
if (vCardObject['N.PREFIX'] === undefined) {
|
||||
vCardObject['N.PREFIX'] = '';
|
||||
}
|
||||
vCardObject['N.GN'] = givenName;
|
||||
if (vCardObject['N.MN'] === undefined) {
|
||||
vCardObject['N.MN'] = '';
|
||||
}
|
||||
vCardObject['N.FN'] = familyName;
|
||||
if (vCardObject['N.SUFFIX'] === undefined) {
|
||||
vCardObject['N.SUFFIX'] = '';
|
||||
}
|
||||
resolve(vCardObject);
|
||||
}).open();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
import { VCardForObsidianRecord } from "src/contacts/vcard";
|
||||
|
||||
/**
|
||||
* Extracts the base key from a vCard field name.
|
||||
* - If the key contains `[`, extract everything before it.
|
||||
* - Else if the key contains `.`, extract everything before the first dot.
|
||||
* - Otherwise, return the key as is.
|
||||
* @param key The full vCard field key.
|
||||
* @returns The extracted base key.
|
||||
*/
|
||||
function extractBaseKey(key: string): string {
|
||||
if (key.includes("[")) {
|
||||
return key.split("[")[0];
|
||||
} else if (key.includes(".")) {
|
||||
return key.split(".")[0];
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sorts a vCard object:
|
||||
* - Moves priority fields (e.g., `N`, `FN`, `EMAIL`, `TEL`) to the top.
|
||||
* - Places `ADR` fields **after** `BDAY`.
|
||||
* - Sorts indexed fields (`[1:]`, `[1:TYPE]`) in order.
|
||||
* @param vCardObject The parsed vCard object.
|
||||
* @returns A sorted vCard object.
|
||||
*/
|
||||
export function sortVCardOFields(vCardObject: VCardForObsidianRecord): VCardForObsidianRecord {
|
||||
// Define sorting priority
|
||||
const priorityOrder = [
|
||||
"N", "FN", "PHOTO",
|
||||
"EMAIL", "TEL",
|
||||
"BDAY",
|
||||
"ADR", "URL",
|
||||
"ORG", "TITLE", "ROLE"
|
||||
];
|
||||
|
||||
// Separate priority and other fields
|
||||
const priorityEntries: VCardForObsidianRecord = {};
|
||||
const adrEntries: VCardForObsidianRecord = {};
|
||||
const otherEntries: VCardForObsidianRecord = {};
|
||||
|
||||
Object.entries(vCardObject).forEach(([key, value]) => {
|
||||
const baseKey = extractBaseKey(key);
|
||||
|
||||
if (priorityOrder.includes(baseKey)) {
|
||||
if (baseKey === "ADR") {
|
||||
adrEntries[key] = value; // Keep ADR separate to place after BDAY
|
||||
} else {
|
||||
priorityEntries[key] = value;
|
||||
}
|
||||
} else {
|
||||
otherEntries[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Sort priority entries based on priority order
|
||||
const sortedPriorityEntries = Object.fromEntries(
|
||||
Object.entries(priorityEntries).sort(([keyA], [keyB]) => {
|
||||
const baseKeyA = extractBaseKey(keyA);
|
||||
const baseKeyB = extractBaseKey(keyB);
|
||||
return priorityOrder.indexOf(baseKeyA) - priorityOrder.indexOf(baseKeyB);
|
||||
})
|
||||
);
|
||||
|
||||
// Sort non-priority fields alphabetically while preserving indexes
|
||||
const sortedOtherEntries = Object.fromEntries(
|
||||
Object.entries(otherEntries).sort(([a], [b]) => a.localeCompare(b))
|
||||
);
|
||||
|
||||
return { ...sortedPriorityEntries, ...adrEntries, ...sortedOtherEntries };
|
||||
}
|
||||
11
tests/fixtures/fixtures.ts
vendored
Normal file
11
tests/fixtures/fixtures.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export function readVcfFixture(fileName: string): string {
|
||||
const filePath = join(__dirname, fileName);
|
||||
return readFileSync(filePath, 'utf8');
|
||||
}
|
||||
|
||||
export const fixtures = {
|
||||
readVcfFixture
|
||||
}
|
||||
19
tests/fixtures/noFirstName.vcf
vendored
Normal file
19
tests/fixtures/noFirstName.vcf
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
BEGIN:VCARD
|
||||
N:;Zahra;;;
|
||||
ADR;TYPE=HOME:;;2 Sand Dune Street;Dubai;;00000;UAE
|
||||
FN:Zahra Ali
|
||||
FN:Zahra Ali
|
||||
EMAIL;TYPE=HOME:zahra.moon@miragemail.ae
|
||||
EMAIL;TYPE=WORK:zahra@dreambuild.ae
|
||||
TEL;TYPE=CELL:+971501234567
|
||||
TEL;TYPE=BOTIM:+971501234568
|
||||
TEL;TYPE=CANADA:+14165553333
|
||||
BDAY:1990-08-08
|
||||
URL;TYPE=HOME:https://zahra.ae
|
||||
URL;TYPE=WORK:https://dreambuild.ae/zahra
|
||||
ORG:Mirage Dream Builders
|
||||
ROLE:Lead Vision Architect
|
||||
CATEGORIES:Architecture, Stars
|
||||
VERSION:4.0
|
||||
UID:urn:uuid:019730a76c182-4191-824c-9d9607f52656
|
||||
END:VCARD
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { App } from "obsidian";
|
||||
import { vcard } from "src/contacts/vcard";
|
||||
import { setApp } from "src/context/sharedAppContext";
|
||||
import { fixtures } from "tests/fixtures/fixtures";
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
setApp({} as App);
|
||||
|
|
@ -26,22 +27,51 @@ vi.mock('src/ui/modals/contactNameModal', () => {
|
|||
|
||||
return { ContactNameModal };
|
||||
});
|
||||
|
||||
describe('vcard creatEmpty', () => {
|
||||
it('should ask for a firstname and lastname ', async () => {
|
||||
const empty = await vcard.createEmpty();
|
||||
expect(empty).toBeDefined();
|
||||
|
||||
expect(empty).toEqual(expect.objectContaining({
|
||||
'N.PREFIX': '',
|
||||
'N.GN': 'Foo',
|
||||
'N.MN': '',
|
||||
'N.FN': 'Bar',
|
||||
'N.SUFFIX': ''
|
||||
}));
|
||||
|
||||
const keys = Object.keys(empty);
|
||||
const expectedNFields = ['N.PREFIX', 'N.GN', 'N.MN', 'N.FN', 'N.SUFFIX'];
|
||||
expect(keys.slice(0, expectedNFields.length)).toEqual(expectedNFields);
|
||||
|
||||
const expectedFields = ['N.PREFIX', 'N.GN', 'N.MN', 'N.FN', 'N.SUFFIX'];
|
||||
expectedFields.forEach((field) => {
|
||||
expect(empty).toHaveProperty(field);
|
||||
});
|
||||
expect(empty['N.GN']).toBe('Foo');
|
||||
expect(empty['N.FN']).toBe('Bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('vcard parse', () => {
|
||||
|
||||
it('Should ensure all the name variables exist and first name and lastname are filled', async () => {
|
||||
const vcf = fixtures.readVcfFixture('noFirstName.vcf');
|
||||
const result = await vcard.parse(vcf);
|
||||
const expectedFields = ['N.PREFIX', 'N.GN', 'N.MN', 'N.FN', 'N.SUFFIX'];
|
||||
expectedFields.forEach((field) => {
|
||||
expect(result[0]).toHaveProperty(field);
|
||||
});
|
||||
expect(result[0]['N.GN']).toBe('Foo');
|
||||
expect(result[0]['N.FN']).toBe('Bar');
|
||||
});
|
||||
|
||||
it('should only import variables that are in a predifined list ', async () => {
|
||||
|
||||
});
|
||||
|
||||
it('should convert v3 internal phoro to a v4 version', async () => {
|
||||
|
||||
});
|
||||
|
||||
it('should be able to parse multiple cards from one file', async () => {
|
||||
|
||||
});
|
||||
|
||||
it('should add indexes to duplicate field names.', async () => {
|
||||
|
||||
});
|
||||
|
||||
it('should preform dome sorting and try to unify dates. ', async () => {
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue