Skip to content

Commit 984ddcc

Browse files
committed
Merge branch 'lindylearn-match-by-frontmatter'
2 parents 541b403 + 240fe48 commit 984ddcc

File tree

7 files changed

+97
-73
lines changed

7 files changed

+97
-73
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"license": "MIT",
1919
"dependencies": {
2020
"axios": "^0.26.0",
21+
"gray-matter": "^4.0.3",
2122
"lodash.pickby": "^4.6.0",
2223
"nunjucks": "^3.2.3"
2324
},
@@ -49,4 +50,4 @@
4950
"webpack-cli": "^4.6.0",
5051
"webpack-node-externals": "^2.5.2"
5152
}
52-
}
53+
}

src/fileManager.ts

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
import type { Vault } from 'obsidian';
1+
import type { Vault, MetadataCache, TFile } from 'obsidian';
22
import { get } from 'svelte/store';
33
import { Renderer } from '~/renderer';
44
import { settingsStore } from '~/store';
55
import { sanitizeTitle } from '~/utils/sanitizeTitle';
66
import type { Article } from '~/models';
7+
import { frontMatterDocType, addFrontMatter } from "~/utils/frontmatter"
8+
9+
type AnnotationFile = {
10+
articleUrl?: string;
11+
file: TFile;
12+
};
713

814
const articleFolderPath = (article: Article): string => {
915
const settings = get(settingsStore);
@@ -17,52 +23,68 @@ const articleFolderPath = (article: Article): string => {
1723

1824
export default class FileManager {
1925
private vault: Vault;
26+
private metadataCache: MetadataCache;
2027
private renderer: Renderer;
2128

22-
constructor(vault: Vault) {
29+
constructor(vault: Vault, metadataCache: MetadataCache) {
2330
this.vault = vault;
31+
this.metadataCache = metadataCache;
2432
this.renderer = new Renderer();
2533
}
2634

27-
public async createFolder(folderPath: string): Promise<void> {
28-
await this.vault.createFolder(folderPath);
29-
}
35+
// Save an article as markdown file, replacing its existing file if present
36+
public async saveArticle(article: Article): Promise<boolean> {
37+
const existingFile = await this.getArticleFile(article);
3038

31-
public async createFile(filePath: string, content: string): Promise<void> {
32-
await this.vault.create(filePath, content);
33-
}
39+
if (existingFile) {
40+
console.debug(`Updating ${existingFile.path}`);
3441

35-
public async createOrUpdate(article: Article): Promise<boolean> {
36-
const folderPath = articleFolderPath(article);
37-
const fileName = `${sanitizeTitle(article.metadata.title)}.md`;
38-
const filePath = `${folderPath}/${fileName}`
39-
let createdNewArticle = false;
42+
const newMarkdownContent = this.renderer.render(article, false);
43+
const existingFileContent = await this.vault.cachedRead(existingFile);
44+
const fileContent = existingFileContent + newMarkdownContent;
4045

41-
if (!(await this.vault.adapter.exists(folderPath))) {
42-
43-
console.info(`Folder ${folderPath} not found. Will be created`);
44-
await this.createFolder(folderPath);
45-
}
46+
await this.vault.modify(existingFile, fileContent);
47+
return false;
48+
} else {
49+
const newFilePath = this.getNewArticleFilePath(article);
50+
console.debug(`Creating ${newFilePath}`);
4651

47-
if (!(await this.vault.adapter.exists(filePath))) {
52+
const markdownContent = this.renderer.render(article, true);
53+
const fileContent = addFrontMatter(markdownContent, article);
4854

49-
console.info(`Document ${filePath} not found. Will be created`);
50-
const content = this.renderer.render(article);
51-
await this.createFile(filePath, content);
52-
createdNewArticle = true;
53-
await settingsStore.actions.addSyncedFile({filename: fileName, uri: encodeURIComponent(article.metadata.url)});
55+
await this.vault.create(newFilePath, fileContent);
56+
return true;
5457
}
55-
else {
58+
}
5659

57-
console.info(`Document ${article.metadata.title} found. Loading content and updating highlights`);
58-
const content = this.renderer.render(article, false);
59-
const fileContent = await this.vault.adapter.read(filePath);
60-
const contentToSave = fileContent + content;
61-
await this.vault.adapter.write(filePath, contentToSave);
60+
public async isArticleSaved(article: Article): Promise<boolean> {
61+
const file = await this.getArticleFile(article);
62+
return !!file
63+
}
6264

63-
}
65+
private async getArticleFile(article: Article): Promise<TFile | null> {
66+
const files = await this.getAnnotationFiles()
67+
return files.find((file) => file.articleUrl === article.metadata.url)?.file || null;
68+
}
6469

65-
return createdNewArticle;
70+
// TODO cache this method for performance?
71+
private async getAnnotationFiles(): Promise<AnnotationFile[]> {
72+
const files = this.vault.getMarkdownFiles();
73+
74+
return files
75+
.map((file) => {
76+
const cache = this.metadataCache.getFileCache(file);
77+
return { file, frontmatter: cache?.frontmatter };
78+
})
79+
.filter(({ frontmatter }) => frontmatter?.["doc_type"] === frontMatterDocType)
80+
.map(({ file, frontmatter }): AnnotationFile => ({ file, articleUrl: frontmatter["url"] }))
81+
}
82+
83+
public getNewArticleFilePath(article: Article): string {
84+
const folderPath = articleFolderPath(article);
85+
const fileName = `${sanitizeTitle(article.metadata.title)}.md`;
86+
const filePath = `${folderPath}/${fileName}`
87+
return filePath;
6688
}
6789

6890
}

src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default class HypothesisPlugin extends Plugin {
1818

1919
await initialise(this);
2020

21-
const fileManager = new FileManager(this.app.vault);
21+
const fileManager = new FileManager(this.app.vault, this.app.metadataCache);
2222

2323
this.syncHypothesis = new SyncHypothesis(fileManager);
2424

src/modals/resyncDelFileModal.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@ import { settingsStore } from '~/store';
44
import { get } from 'svelte/store';
55
import SyncHypothesis from '~/sync/syncHypothesis';
66
import FileManager from '~/fileManager';
7-
import type { SyncedFile } from '~/models';
7+
import type { Article, SyncedFile } from '~/models';
8+
import ApiManager from '~/api/api';
9+
import parseSyncResponse from '~/parser/parseSyncResponse';
810

911
export default class ResyncDelFileModal extends Modal {
1012
private syncHypothesis!: SyncHypothesis;
1113
public waitForClose: Promise<void>;
1214
private resolvePromise: () => void;
1315
private modalContent: ResyncDelFileModalContent;
14-
vault: Vault;
16+
private vault: Vault;
17+
private fileManager: FileManager
1518

1619
constructor(app: App) {
1720
super(app);
1821
this.vault = app.vault;
22+
this.fileManager = new FileManager(this.vault, this.app.metadataCache);
1923

2024
this.waitForClose = new Promise(
2125
(resolve) => (this.resolvePromise = resolve)
@@ -27,9 +31,8 @@ export default class ResyncDelFileModal extends Modal {
2731

2832
async onOpen() {
2933
super.onOpen()
30-
const fileManager = new FileManager(this.vault);
31-
this.syncHypothesis = new SyncHypothesis(fileManager);
32-
const deletedFiles = await this.retrieveDeletedFiles(this.vault);
34+
this.syncHypothesis = new SyncHypothesis(this.fileManager);
35+
const deletedFiles = await this.retrieveDeletedFiles();
3336

3437
this.titleEl.innerText = "Hypothes.is: Resync deleted file(s)";
3538

@@ -56,21 +59,20 @@ export default class ResyncDelFileModal extends Modal {
5659
this.resolvePromise();
5760
}
5861

59-
async retrieveDeletedFiles(vault:Vault) {
62+
async retrieveDeletedFiles(): Promise<SyncedFile[]> {
63+
const token = get(settingsStore).token;
64+
const userid = get(settingsStore).user;
65+
const apiManager = new ApiManager(token, userid);
6066

61-
const folder = get(settingsStore).highlightsFolder;
62-
const syncedFiles = get(settingsStore).syncedFiles;
67+
// Fetch all annotated articles that *should* be present
68+
const allAnnotations = await apiManager.getHighlights()
69+
const allArticles: [] = Object.values(await parseSyncResponse(allAnnotations));
6370

64-
const existingFiles = await vault.adapter.list(`/${folder}`);
65-
66-
// eslint-disable-next-line
67-
const r = /[^\/]*$/;
68-
69-
existingFiles.files.forEach(function(value, index) {
70-
this[index] = (value.match(r))[0]
71-
}, existingFiles.files);
72-
73-
return syncedFiles.filter((file) => !existingFiles.files.includes(file.filename));
71+
// Check which files are actually present
72+
const deletedArticles = await Promise.all(allArticles.filter(async article => !(await this.fileManager.isArticleSaved(article))));
73+
return deletedArticles.map((article: Article) =>
74+
({ uri: article.metadata.url, filename: this.fileManager.getNewArticleFilePath(article)})
75+
);
7476
}
7577

7678
async startResync(selectedFiles: SyncedFile[]): Promise<void> {

src/store/settings.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Group, SyncedFile } from '~/models';
1+
import type { Group } from '~/models';
22
import { writable } from 'svelte/store';
33
import defaultTemplate from '~/assets/defaultTemplate.njk';
44
import type HypothesisPlugin from '~/main';
@@ -19,7 +19,6 @@ type Settings = {
1919
history: SyncHistory;
2020
dateTimeFormat: string;
2121
autoSyncInterval: number;
22-
syncedFiles: SyncedFile[];
2322
groups: Group[];
2423
useDomainFolders: boolean;
2524
};
@@ -37,9 +36,8 @@ const DEFAULT_SETTINGS: Settings = {
3736
totalArticles: 0,
3837
totalHighlights: 0,
3938
},
40-
syncedFiles: [],
4139
groups: [],
42-
useDomainFolders: false,
40+
useDomainFolders: false
4341
};
4442

4543
const createSettingsStore = () => {
@@ -107,7 +105,6 @@ const createSettingsStore = () => {
107105
state.history.totalArticles = 0;
108106
state.history.totalHighlights = 0;
109107
state.lastSyncDate = undefined;
110-
state.syncedFiles = [];
111108
return state;
112109
});
113110
};
@@ -155,20 +152,6 @@ const createSettingsStore = () => {
155152
});
156153
};
157154

158-
const addSyncedFile = (value: SyncedFile) => {
159-
store.update((state) => {
160-
const uniqueValuesSet = new Set();
161-
const syncFiles = [...state.syncedFiles, value];
162-
state.syncedFiles = syncFiles.filter((obj) => {
163-
const isPresentInSet = uniqueValuesSet.has(obj.filename);
164-
uniqueValuesSet.add(obj.filename);
165-
return !isPresentInSet;
166-
});
167-
168-
return state;
169-
});
170-
}
171-
172155
const setGroups = async (value: Group[]) => {
173156
store.update((state) => {
174157
state.groups = value;
@@ -204,7 +187,6 @@ const createSettingsStore = () => {
204187
setSyncOnBoot,
205188
incrementHistory,
206189
setDateTimeFormat,
207-
addSyncedFile,
208190
setGroups,
209191
resetGroups,
210192
setUseDomainFolder,

src/sync/syncHypothesis.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export default class SyncHypothesis {
6868

6969
private async syncArticle(article: Article): Promise<void> {
7070

71-
const createdNewArticle = await this.fileManager.createOrUpdate(article);
71+
const createdNewArticle = await this.fileManager.saveArticle(article);
7272

7373
if (createdNewArticle) {
7474
this.syncState.newArticlesSynced += 1;

src/utils/frontmatter.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import matter from "gray-matter"
2+
import type { Article } from '~/models';
3+
4+
type FrontMatterContent = {
5+
doc_type?: string;
6+
url?: string;
7+
}
8+
9+
export const frontMatterDocType = "hypothesis-highlights"
10+
11+
export const addFrontMatter = (markdownContent: string, article: Article) => {
12+
const frontMatter: FrontMatterContent = {
13+
doc_type: frontMatterDocType,
14+
url: article.metadata.url,
15+
};
16+
return matter.stringify(markdownContent, frontMatter);
17+
}

0 commit comments

Comments
 (0)