Skip to content

Commit 081304b

Browse files
authored
Support frontmatter contact file (#4)
1 parent b031ffa commit 081304b

File tree

9 files changed

+241
-106
lines changed

9 files changed

+241
-106
lines changed

README.md

+43
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,46 @@ You can use different sorting options to find the required contacts:
6666
- Use sorting by name to find a specific contact.
6767

6868
https://user-images.githubusercontent.com/9114994/209383369-d7fc0a42-d1df-4980-93e0-46a8541b00b5.mov
69+
70+
## Contact File Formats
71+
Any of the following formats can be used for storing contact data in Obsidian files. The default for new contacts is `Custom Format`, but this behavior can be changed in the settings using the `Contact File Template` menu item.
72+
73+
### (Default) Custom Format
74+
The default format used by this plugin is the markdown table for storing contact's data.
75+
```
76+
/---contact---/
77+
| key | value |
78+
| --------- | ------------------------ |
79+
| Name | carl |
80+
| Last Name | johnson |
81+
| Phone | +1 555 555 5555 |
82+
| Telegram | @carlj567 |
83+
| Linkedin | linkedin.com/in/carlj567 |
84+
| Birthday | 1966-12-06 |
85+
| Last chat | 2022-12-06 |
86+
| Friends | [[Bob]] [[Sue]] |
87+
/---contact---/
88+
```
89+
90+
### Frontmatter Format
91+
92+
The [Frontmatter](https://help.obsidian.md/Advanced+topics/YAML+front+matter) format is used by Obsidian as metadata for files and is also supported by the [Dataview](https://github.com/blacksmithgu/obsidian-dataview) plugin, allowing you to build queries for your contacts.
93+
94+
> :warning: **Do not change or remove `type` field**. It is used to detect if the current file is a contact.
95+
96+
> :warning: **It needs to be placed at the very top of the file**. Be very careful here!
97+
98+
```
99+
---
100+
name:
101+
first: carl
102+
last: johnson
103+
phone: +1 555 555 5555
104+
telegram: @carlj567
105+
linkedin: linkedin.com/in/carlj567
106+
birthday: 1966-12-06
107+
last_chat: 2022-12-06
108+
friends: "[[Bob]] [[Sue]]"
109+
type: contact
110+
---
111+
```

src/file/file.ts

+42-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
import { normalizePath, Notice, TFile, TFolder, Vault, Workspace } from "obsidian";
22
import { join } from "path";
3+
import { Template } from "src/settings/settings";
4+
5+
const customFormat =
6+
`/---contact---/
7+
| key | value |
8+
| --------- | ----- |
9+
| Name | |
10+
| Last Name | |
11+
| Phone | |
12+
| Telegram | |
13+
| Linkedin | |
14+
| Birthday | |
15+
| Last chat | |
16+
| Friends | |
17+
/---contact---/`
18+
19+
const frontmatterFormat =
20+
`---
21+
name:
22+
first:
23+
last:
24+
phone:
25+
telegram:
26+
linkedin:
27+
birthday:
28+
last_chat:
29+
friends:
30+
type: contact
31+
---`
332

433
export async function openFile(file: TFile, workspace: Workspace) {
534
const leaf = workspace.getLeaf()
@@ -16,26 +45,14 @@ export function findContactFiles(contactsFolder: TFolder) {
1645
return contactFiles;
1746
}
1847

19-
export function createContactFile(folderPath: string, vault: Vault, workspace: Workspace) {
48+
export function createContactFile(folderPath: string, template: Template, vault: Vault, workspace: Workspace) {
2049
const folder = vault.getAbstractFileByPath(folderPath)
2150
if (!folder) {
2251
new Notice(`Can not find path: '${folderPath}'. Please update "Contacts" plugin settings`);
2352
return;
2453
}
2554

26-
vault.create(normalizePath(join(folderPath, `Contact ${findNextFileNumber(folderPath, vault)}.md`)), `
27-
/---contact---/
28-
| key | value |
29-
| --------- | ----- |
30-
| Name | |
31-
| Last Name | |
32-
| Phone | |
33-
| Telegram | |
34-
| Linkedin | |
35-
| Birthday | |
36-
| Last chat | |
37-
| Friends | |
38-
/---contact---/`)
55+
vault.create(normalizePath(join(folderPath, `Contact ${findNextFileNumber(folderPath, vault)}.md`)), getNewFileContent(template))
3956
.then(createdFile => openFile(createdFile, workspace));
4057
}
4158

@@ -63,8 +80,18 @@ function findNextFileNumber(folderPath: string, vault: Vault) {
6380
const currentNumber = parseInt(currentNumberString);
6481
nextNumber = Math.max(nextNumber, (currentNumber + 1));
6582
}
66-
6783
}
6884
});
6985
return nextNumber === 0 ? "" : nextNumber.toString();
86+
}
87+
88+
function getNewFileContent(template: Template): string {
89+
switch (template) {
90+
case Template.CUSTOM:
91+
return customFormat;
92+
case Template.FRONTMATTER:
93+
return frontmatterFormat;
94+
default:
95+
return customFormat;
96+
}
7097
}

src/main.ts

+2-37
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
1-
import { App, Plugin, PluginSettingTab, Setting } from 'obsidian';
1+
import { Plugin } from 'obsidian';
22
import { ContactsView } from "src/ui/sidebar/sidebarView";
33
import { CONTACTS_VIEW_CONFIG } from "src/util/constants";
4-
5-
interface ContactsPluginSettings {
6-
contactsFolder: string;
7-
}
8-
9-
const DEFAULT_SETTINGS: ContactsPluginSettings = {
10-
contactsFolder: '/'
11-
}
4+
import { ContactsPluginSettings, ContactsSettingTab, DEFAULT_SETTINGS } from './settings/settings';
125

136
export default class ContactsPlugin extends Plugin {
147
settings: ContactsPluginSettings;
@@ -52,31 +45,3 @@ export default class ContactsPlugin extends Plugin {
5245
);
5346
}
5447
}
55-
56-
class ContactsSettingTab extends PluginSettingTab {
57-
plugin: ContactsPlugin;
58-
59-
constructor(app: App, plugin: ContactsPlugin) {
60-
super(app, plugin);
61-
this.plugin = plugin;
62-
}
63-
64-
display(): void {
65-
const { containerEl } = this;
66-
67-
containerEl.empty();
68-
69-
containerEl.createEl('h2', { text: 'Settings for "Contacts" plugin.' });
70-
71-
new Setting(containerEl)
72-
.setName('Contacts folder location')
73-
.setDesc('Files in this folder and all subfolders will be available as contacts')
74-
.addText(text => text
75-
.setPlaceholder('Personal/Contacts')
76-
.setValue(this.plugin.settings.contactsFolder)
77-
.onChange(async (value) => {
78-
this.plugin.settings.contactsFolder = value;
79-
await this.plugin.saveSettings();
80-
}));
81-
}
82-
}

src/parse/custom_format_parser.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { TFile, Vault } from "obsidian";
2+
import { Contact } from "./contact";
3+
import { parseDate } from "./parse_utils";
4+
5+
export async function isContactFile(
6+
file: TFile, vault: Vault
7+
): Promise<boolean> {
8+
const content = await vault.cachedRead(file);
9+
return (content.match(/\/---contact---\//g) || []).length === 2;
10+
}
11+
12+
export async function parseContactData(file: TFile, vault: Vault): Promise<Contact | null> {
13+
const fileContents = await vault.cachedRead(file);
14+
const regexpNames = /^\|(?<key>.+)\|(?<value>.+)\|$/gm;
15+
const contactsDict: { [key: string]: string } = {};
16+
for (const match of fileContents.matchAll(regexpNames)) {
17+
if (!match.groups) {
18+
continue;
19+
}
20+
const key = match.groups.key.trim()
21+
const value = match.groups.value.trim()
22+
if (key === "" || value === "") {
23+
continue;
24+
}
25+
contactsDict[key] = value;
26+
}
27+
28+
return {
29+
name: contactsDict['Name'],
30+
lastName: contactsDict['Last Name'],
31+
phone: contactsDict['Phone'],
32+
lastContact: parseDate(contactsDict['Last chat']),
33+
birthday: parseDate(contactsDict['Birthday']),
34+
file: file,
35+
}
36+
}
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { MetadataCache, TFile } from "obsidian";
2+
import { Contact } from "./contact";
3+
import { parseDate } from "./parse_utils";
4+
5+
export function isContactFile(
6+
file: TFile, metadataCache: MetadataCache
7+
): boolean {
8+
const type = metadataCache.getFileCache(file)?.frontmatter?.type;
9+
return type == 'contact';
10+
}
11+
12+
export async function parseContactData(file: TFile, metadataCache: MetadataCache): Promise<Contact | null> {
13+
const frontmatter = metadataCache.getFileCache(file)?.frontmatter;
14+
if (frontmatter == null) {
15+
return null;
16+
}
17+
18+
return {
19+
name: frontmatter['name']['first'],
20+
lastName: frontmatter['name']['last'],
21+
phone: frontmatter['phone'],
22+
lastContact: parseDate(frontmatter['last_chat']),
23+
birthday: parseDate(frontmatter['birthday']),
24+
file: file,
25+
}
26+
}

src/parse/parse.ts

+16-51
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,24 @@
1-
import { TFile, Vault } from "obsidian";
1+
import { MetadataCache, TFile, Vault } from "obsidian";
22
import { Contact } from "./contact";
3+
import { isContactFile as isContactFormatFile, parseContactData as parseContactFormatData } from "./custom_format_parser";
4+
import { isContactFile as isFrontmatterFormatFile, parseContactData as parseFrontmatterFormatData } from "./front_matter_format_parser";
35

4-
export async function parseContactFiles(files: TFile[], vault: Vault) {
6+
export async function parseContactFiles(files: TFile[], vault: Vault, metadataCache: MetadataCache) {
57
const contactsData: Contact[] = [];
6-
for (const contactFile of files) {
7-
const contact = await parseContactData(contactFile, vault);
8-
if (contact) {
8+
for (const file of files) {
9+
if (isFrontmatterFormatFile(file, metadataCache)) {
10+
const contact = await parseFrontmatterFormatData(file, metadataCache);
11+
if (!contact) {
12+
continue;
13+
}
14+
contactsData.push(contact);
15+
} else if (await isContactFormatFile(file, vault)) {
16+
const contact = await parseContactFormatData(file, vault);
17+
if (!contact) {
18+
continue;
19+
}
920
contactsData.push(contact);
1021
}
1122
}
1223
return contactsData;
13-
}
14-
15-
async function parseContactData(file: TFile, vault: Vault): Promise<Contact | null> {
16-
const fileContents = await vault.cachedRead(file);
17-
if (!isContactFile(fileContents)) {
18-
return null;
19-
}
20-
const regexpNames = /^\|(?<key>.+)\|(?<value>.+)\|$/gm;
21-
const contactsDict: { [key: string]: string } = {};
22-
for (const match of fileContents.matchAll(regexpNames)) {
23-
if (!match.groups) {
24-
continue;
25-
}
26-
const key = match.groups.key.trim()
27-
const value = match.groups.value.trim()
28-
if (key === "" || value === "") {
29-
continue;
30-
}
31-
contactsDict[key] = value;
32-
}
33-
34-
return {
35-
name: contactsDict['Name'],
36-
lastName: contactsDict['Last Name'],
37-
phone: contactsDict['Phone'],
38-
lastContact: parseDate(contactsDict['Last chat']),
39-
birthday: parseDate(contactsDict['Birthday']),
40-
file: file,
41-
}
42-
}
43-
44-
function parseDate(value: string): Date | undefined {
45-
if (!value) {
46-
return undefined;
47-
}
48-
const parsedDate = value.match(/(\[\[)?(?<date>[0-9-]+)(\]\])?/)
49-
if (!parsedDate || !parsedDate.groups) {
50-
return undefined;
51-
}
52-
return new Date(parsedDate.groups['date']);
53-
}
54-
55-
function isContactFile(
56-
content: string,
57-
): boolean {
58-
return (content.match(/\/---contact---\//g) || []).length === 2;
5924
}

src/parse/parse_utils.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function parseDate(value: string): Date | undefined {
2+
if (!value) {
3+
return undefined;
4+
}
5+
const parsedDate = value.match(/(\[\[)?(?<date>[0-9-]+)(\]\])?/)
6+
if (!parsedDate || !parsedDate.groups) {
7+
return undefined;
8+
}
9+
return new Date(parsedDate.groups['date']);
10+
}

src/settings/settings.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { App, PluginSettingTab, Setting } from "obsidian";
2+
import ContactsPlugin from "src/main";
3+
4+
export interface ContactsPluginSettings {
5+
contactsFolder: string;
6+
template: Template;
7+
}
8+
9+
export enum Template {
10+
CUSTOM = "custom", FRONTMATTER = "frontmatter"
11+
}
12+
13+
export const DEFAULT_SETTINGS: ContactsPluginSettings = {
14+
contactsFolder: '/',
15+
template: Template.CUSTOM
16+
}
17+
18+
export class ContactsSettingTab extends PluginSettingTab {
19+
plugin: ContactsPlugin;
20+
21+
constructor(app: App, plugin: ContactsPlugin) {
22+
super(app, plugin);
23+
this.plugin = plugin;
24+
}
25+
26+
display(): void {
27+
const { containerEl } = this;
28+
29+
containerEl.empty();
30+
31+
containerEl.createEl('h2', { text: 'Settings for "Contacts" plugin.' });
32+
33+
new Setting(containerEl)
34+
.setName('Contacts folder location')
35+
.setDesc('Files in this folder and all subfolders will be available as contacts')
36+
.addText(text => text
37+
.setPlaceholder('Personal/Contacts')
38+
.setValue(this.plugin.settings.contactsFolder)
39+
.onChange(async (value) => {
40+
this.plugin.settings.contactsFolder = value;
41+
await this.plugin.saveSettings();
42+
}));
43+
44+
new Setting(containerEl)
45+
.setName('Contact file template')
46+
.setDesc('Template to be used when creating a new contact file')
47+
.addDropdown(dropdown => dropdown
48+
.addOption(Template.CUSTOM, "Custom")
49+
.addOption(Template.FRONTMATTER, "Frontmatter (YAML Metadata)")
50+
.setValue(this.plugin.settings.template)
51+
.onChange(async (value) => {
52+
this.plugin.settings.template = value as Template;
53+
await this.plugin.saveSettings();
54+
}));
55+
}
56+
}

0 commit comments

Comments
 (0)