Repository: renehernandez/obsidian-readwise (ts)

Issue:
Mapping of authors in json
## Problem

The `author` field in Readwise may be different based on the type of document that we are syncing. It has different values depending on the type of highlight:
- For websites, it is the domain name
- For tweets, it is usually `@handler on Twitter`
- etc.

## Proposal

I want to be able to relate different notes from same authors across different platforms by providing a mapping of authors leveraging a JSON file. For example:

```json
{
  "perell.com": "David Perell",
  "@david_perell on Twitter": "David Perell"
}
```

Pull request description:
Initial authors mapping
## Description

Fix #6

Patch:
diff --git a/src/authorsMapping.ts b/src/authorsMapping.ts
new file mode 100644
index 0000000..6255e89
--- /dev/null
+++ b/src/authorsMapping.ts
@@ -0,0 +1,31 @@
+import type { IFileSystemHandler } from "./fileSystem";
+import Log from "./log";
+
+
+export class AuthorsMapping {
+
+    fsHandler: IFileSystemHandler;
+    filename: string
+    path: string
+
+    constructor(filename: string, fsHandler: IFileSystemHandler) {
+        this.filename = filename;
+        this.fsHandler = fsHandler;
+    }
+
+    public async initialize(): Promise<void> {
+        this.path = this.fsHandler.normalizePath(`${this.fsHandler.pluginsDir()}/obsidian-readwise/${this.filename}`);
+
+        if (!(await this.fsHandler.exists(this.path))) {
+            Log.debug(`Creating authors mapping at ${this.path}`);
+            await this.fsHandler.write(this.path, "{}");
+        }
+    }
+
+    public async load(): Promise<Map<string, string>> {
+        let content = JSON.parse(await (this.fsHandler.read(this.path)));
+
+        return new Map(Object.entries(content));
+    }
+
+}
\ No newline at end of file
diff --git a/src/fileDoc.ts b/src/fileDoc.ts
index bd91b33..5d3a00d 100644
--- a/src/fileDoc.ts
+++ b/src/fileDoc.ts
@@ -20,7 +20,7 @@ export class FileDoc {
     }
 
     public async createOrUpdate(storagePath: string) {
-        const file = this.filePath(storagePath);
+        const file = this.fsHandler.normalizePath(this.preparePath(storagePath));
 
         var content = '';
 
@@ -43,11 +43,11 @@ export class FileDoc {
         await this.fsHandler.write(file, content);
     }
 
-    public filePath(storagePath: string = ''): string {
+    public preparePath(storagePath: string = ''): string {
         if (storagePath.length > 0 && storagePath.slice(-1) !== '/') {
             storagePath = storagePath + '/';
         }
-        return this.fsHandler.normalizePath(`${storagePath}${this.sanitizeName()}.md`)
+        return `${storagePath}${this.sanitizeName()}.md`
     }
 
     public sanitizeName(): string {
diff --git a/src/fileSystem/handler.ts b/src/fileSystem/handler.ts
index f618163..95aef81 100644
--- a/src/fileSystem/handler.ts
+++ b/src/fileSystem/handler.ts
@@ -3,9 +3,11 @@ import type { IFileSystemHandler } from "./interface";
 
 export class FileSystemHandler implements IFileSystemHandler {
     adapter: obsidian.FileSystemAdapter;
+    vault: obsidian.Vault;
 
-    constructor(adapter: obsidian.FileSystemAdapter) {
-        this.adapter = adapter;
+    constructor(vault: obsidian.Vault) {
+        this.vault = vault;
+        this.adapter = vault.adapter as obsidian.FileSystemAdapter;
     }
 
     public normalizePath(path: string): string {
@@ -23,4 +25,8 @@ export class FileSystemHandler implements IFileSystemHandler {
     public async exists(path: string): Promise<boolean> {
         return await this.adapter.exists(path);
     }
+
+    public pluginsDir(): string {
+        return this.normalizePath(`${this.vault.configDir}/plugins`);
+    }
 }
diff --git a/src/fileSystem/interface.ts b/src/fileSystem/interface.ts
index cc1527c..3abe3d2 100644
--- a/src/fileSystem/interface.ts
+++ b/src/fileSystem/interface.ts
@@ -6,4 +6,6 @@ export interface IFileSystemHandler {
     write(path: string, data: string): Promise<void>;
 
     exists(path: string): Promise<boolean>;
+
+    pluginsDir(): string;
 }
diff --git a/src/index.ts b/src/index.ts
index c604fde..aaf2e38 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -13,6 +13,7 @@ import { TokenManager } from "./tokenManager";
 import { FileSystemHandler } from "./fileSystem";
 import { DateFactory } from "./date";
 import PromiseQueue from "./promiseQueue";
+import { AuthorsMapping } from "./authorsMapping";
 
 
 export default class ObsidianReadwisePlugin extends Plugin {
@@ -24,6 +25,7 @@ export default class ObsidianReadwisePlugin extends Plugin {
     private api: ReadwiseApi;
     private mode: AppMode;
     private promiseQueue: PromiseQueue;
+    private authorsMapping: AuthorsMapping;
 
 
     setState(state: PluginState) {
@@ -52,6 +54,9 @@ export default class ObsidianReadwisePlugin extends Plugin {
             callback: () => this.promiseQueue.addTask(() => this.syncReadwise(this.settings.lastUpdate)),
         });
 
+        this.authorsMapping = new AuthorsMapping(this.settings.authorsMappingFilename, new FileSystemHandler(this.app.vault));
+        await this.authorsMapping.initialize();
+
 		if (this.settings.syncOnBoot) {
 			await this.syncReadwise(this.settings.lastUpdate);
 		}
@@ -116,11 +121,15 @@ export default class ObsidianReadwisePlugin extends Plugin {
 
     async updateNotes(documents: Document[]) {
         this.setState(PluginState.syncing)
-        const handler = new FileSystemHandler(this.app.vault.adapter as FileSystemAdapter);
+        const handler = new FileSystemHandler(this.app.vault);
         const header = await HeaderTemplateRenderer.create(this.settings.headerTemplatePath, handler);
         const highlight = await HighlightTemplateRenderer.create(this.settings.highlightTemplatePath, handler);
+        const mapping = await this.authorsMapping.load();
 
         documents.forEach(doc => {
+            if (mapping.has(doc.author)) {
+                doc.author = mapping.get(doc.author);
+            }
             const fileDoc = new FileDoc(doc, header, highlight, handler);
 
             fileDoc.createOrUpdate(this.settings.highlightStoragePath);
diff --git a/src/settings.ts b/src/settings.ts
index 5bbeb31..d6d2dc4 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -6,6 +6,7 @@ export interface ObsidianReadwiseSettings {
     headerTemplatePath: string;
     highlightStoragePath: string;
     highlightTemplatePath: string;
+    authorsMappingFilename: string;
 }
 
 export class ObsidianReadwiseSettingsGenerator {
@@ -22,6 +23,7 @@ export class ObsidianReadwiseSettingsGenerator {
             headerTemplatePath: "",
             highlightTemplatePath: "",
             highlightStoragePath: "",
+            authorsMappingFilename: "authors.json",
             lastUpdate: Date.now()
         }
     }


Test patch:
diff --git a/tests/authorsMapping.ts b/tests/authorsMapping.ts
new file mode 100644
index 0000000..89c6ec4
--- /dev/null
+++ b/tests/authorsMapping.ts
@@ -0,0 +1,18 @@
+import "mocha";
+import { assert } from "chai";
+import { AuthorsMapping } from "../src/authorsMapping";
+import { fileSystemHandler, resolvePathToData } from "./helpers";
+
+describe("AuthorsMapping", () => {
+    const handler = fileSystemHandler();
+
+    context("load", () => {
+        it('loads the authors data into object', async () => {
+            const authorsMapping = new AuthorsMapping('authors.json', handler);
+            await authorsMapping.initialize();
+            const mapping = await authorsMapping.load();
+
+            assert.equal(mapping.get("perell.com"), "David Perell");
+        });
+    });
+});
\ No newline at end of file
diff --git a/tests/data/plugins/obsidian-readwise/authors.json b/tests/data/plugins/obsidian-readwise/authors.json
new file mode 100644
index 0000000..f796fa0
--- /dev/null
+++ b/tests/data/plugins/obsidian-readwise/authors.json
@@ -0,0 +1,17 @@
+{
+    "perell.com": "David Perell",
+    "charity.wtf": "Charity Majors",
+    "paulgraham.com": "Paul Graham",
+    "fortelabs.co": "Tiago Forte",
+    "danluu.com": "Dan Luu",
+    "jamesclear.com": "James Clear",
+    "scottyoung.com": "Scott Young",
+    "@sama on Twitter": "Sam Altman",
+    "@GeePawHill on Twitter": "GeePaw Hill",
+    "@mipsytipsy on Twitter": "Charity Majors",
+    "@RealGeneKim on Twitter": "Gene Kim",
+    "@jbeda on Twitter": "Joe Beda",
+    "@david_perell on Twitter": "David Perell",
+    "@dollarsanddata on Twitter": "Nick Maggiulli",
+    "@cgst on Twitter": "Cristian Strat"
+}
\ No newline at end of file
diff --git a/tests/fileDoc.ts b/tests/fileDoc.ts
index ad1d359..28133de 100644
--- a/tests/fileDoc.ts
+++ b/tests/fileDoc.ts
@@ -54,10 +54,10 @@ describe("File Doc", () => {
             fileDoc.doc.title = "https://example.com/2021-04-26/article-name-12?foo=bar&key=value";
 
             assert.equal(fileDoc.sanitizeName(), "example_com-2021-04-26-article-name-12");
-    
+
             fileDoc.doc.title = "http://example.com/2021-04-26/article-name-13?foo=bar&key=value";
 
-            assert.equal(fileDoc.sanitizeName(), "example_com-2021-04-26-article-name-13");      
+            assert.equal(fileDoc.sanitizeName(), "example_com-2021-04-26-article-name-13");
         });
     });
 
@@ -83,15 +83,15 @@ describe("File Doc", () => {
         });
 
         it("generates the fileDoc path if unspecified", () => {
-            assert.equal(fileDoc.filePath(), "Hello World.md");
+            assert.equal(fileDoc.preparePath(), "Hello World.md");
         });
 
         it("generates a specified fileDoc path", () => {
-            assert.equal(fileDoc.filePath('foo/bar'), "foo/bar/Hello World.md");
+            assert.equal(fileDoc.preparePath('foo/bar'), "foo/bar/Hello World.md");
         });
 
         it("Handles trailing slash in a specified fileDoc path", () => {
-            assert.equal(fileDoc.filePath('foo/bar/'), "foo/bar/Hello World.md");
+            assert.equal(fileDoc.preparePath('foo/bar/'), "foo/bar/Hello World.md");
         });
 
     });
diff --git a/tests/helpers.ts b/tests/helpers.ts
index 82e74f7..3f473d6 100644
--- a/tests/helpers.ts
+++ b/tests/helpers.ts
@@ -1,7 +1,7 @@
 import type { IFileSystemHandler } from "../src/fileSystem";
 import type { IDateFactory, IDateHandler } from "../src/date";
-const moment = require("moment");
 
+const moment = require("moment");
 const fs = require('fs');
 const path = require('path');
 
@@ -12,7 +12,8 @@ export function fileSystemHandler(): IFileSystemHandler {
             return fs.readFileSync(path).toString();
         },
         write: async (path: string) => {},
-        exists: async (path: string) => true
+        exists: async (path: string) => true,
+        pluginsDir: () => resolvePathToData("plugins")
     }
 }