From ff77f3628d253f36d69ec095d8f81ff8387f57a9 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 14 Nov 2023 16:35:49 +0800 Subject: [PATCH] fix: lint code in CI --- .eslintignore | 4 +- .eslintrc | 44 ++- .github/workflows/test.yml | 6 +- .prettierrc | 4 +- package.json | 3 + src/__mocks__/obsidian.ts | 12 +- src/__tests__/formatDate.spec.ts | 144 ++++---- src/__tests__/path_validation.spec.ts | 132 ++++---- src/api.ts | 20 +- src/main.ts | 460 +++++++++++++------------- src/settings/file-suggest.ts | 46 +-- src/settings/index.ts | 120 +++---- src/settings/suggest.ts | 198 +++++------ src/settings/template.ts | 240 +++++++------- src/util.ts | 138 ++++---- 15 files changed, 789 insertions(+), 782 deletions(-) diff --git a/.eslintignore b/.eslintignore index 32909b2..dd87e2d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ -npm node_modules -build \ No newline at end of file +node_modules +build diff --git a/.eslintrc b/.eslintrc index 766e9cf..58a795d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,24 +1,22 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "env": { "node": true }, - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], - "@typescript-eslint/ban-ts-comment": "off", - "no-prototype-builtins": "off", - "@typescript-eslint/no-empty-function": "off", - "semi": ["error", "always"] - } - } + "root": true, + "parser": "@typescript-eslint/parser", + "env": { "node": true }, + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "@typescript-eslint/ban-ts-comment": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-empty-function": "off", + "semi": [2, "never"] + } +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 22a15f8..763cf1d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] - node-version: [ 14.x, 16.x, 18.x, 19.x ] + node-version: [ 16.x, 18.x, 19.x ] max-parallel: 24 steps: - uses: actions/checkout@v3 @@ -26,7 +26,5 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: yarn - # before the first 'run' each "run" is a script from your project: - # - run: npm run prettier - # - run: npm run linter + - run: yarn lint - run: yarn test diff --git a/.prettierrc b/.prettierrc index 8d95c2d..1ccc975 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,4 @@ { - "semi": true, - "singleQuote": false + "semi": false, + "singleQuote": true } diff --git a/package.json b/package.json index 99202ec..937c0af 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "dev": "node esbuild.config.mjs", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "version": "node version-bump.mjs && git add manifest.json versions.json", + "lint": "eslint ./src --ext .ts", + "lint:fix": "eslint ./src --ext .ts --fix", + "format": "prettier --write ./src/**/*.ts", "test": "jest", "test:watch": "jest --watch", "test:ci": "jest --ci --reporters='default' --reporters='./github-actions-reporter'", diff --git a/src/__mocks__/obsidian.ts b/src/__mocks__/obsidian.ts index a1ae754..8a3460f 100644 --- a/src/__mocks__/obsidian.ts +++ b/src/__mocks__/obsidian.ts @@ -1,7 +1,7 @@ export const mockObsidianApp = { // Mock implementation of the App API app: { - platform: () => "desktop", + platform: () => 'desktop', plugins: { getPlugins: () => [], isEnabled: () => true, @@ -11,7 +11,7 @@ export const mockObsidianApp = { // Mock implementation of the Workspace API workspace: { onLayoutReady: (callback: () => void) => { - setTimeout(callback, 0); + setTimeout(callback, 0) }, getLeavesOfType: () => [], getConfig: () => ({}), @@ -19,12 +19,12 @@ export const mockObsidianApp = { // Mock implementation of the MarkdownView API markdownView: { - getMode: () => "source", - getMarkdown: () => "", + getMode: () => 'source', + getMarkdown: () => '', }, -}; +} // Mock implementation of the Obsidian global object export const obsidian = { ...mockObsidianApp, -}; +} diff --git a/src/__tests__/formatDate.spec.ts b/src/__tests__/formatDate.spec.ts index 1090f53..22525aa 100644 --- a/src/__tests__/formatDate.spec.ts +++ b/src/__tests__/formatDate.spec.ts @@ -1,70 +1,70 @@ -import { DateTime } from "luxon"; -import { DEFAULT_SETTINGS } from "../settings"; -import { formatDate } from "../util"; +import { DateTime } from 'luxon' +import { DEFAULT_SETTINGS } from '../settings' +import { formatDate } from '../util' -const jsDate = new Date("2023-02-18 13:02:08.169"); -const apiDate = jsDate.toISOString(); // API returns ISO 8601 date strings +const jsDate = new Date('2023-02-18 13:02:08.169') +const apiDate = jsDate.toISOString() // API returns ISO 8601 date strings type testCase = { - format: string; - date: string; - expected: string; -}; + format: string + date: string + expected: string +} const luxonHierarchicalFormatWithTime = { date: apiDate, - expected: "2023/2023-02/2023-02-18/130208", - format: "yyyy/yyyy-MM/yyyy-MM-dd/HHmmss", -}; + expected: '2023/2023-02/2023-02-18/130208', + format: 'yyyy/yyyy-MM/yyyy-MM-dd/HHmmss', +} const luxonHierarchicalFormat = { date: apiDate, - expected: "2023/2023-02/2023-02-18", - format: "yyyy/yyyy-MM/yyyy-MM-dd", -}; + expected: '2023/2023-02/2023-02-18', + format: 'yyyy/yyyy-MM/yyyy-MM-dd', +} const defaultDateHighlightedFormatTestCase: testCase = { date: apiDate, - expected: "2023-02-18 13:02:08", + expected: '2023-02-18 13:02:08', format: DEFAULT_SETTINGS.dateHighlightedFormat, -}; +} const defaultDateSavedFormatTestCase: testCase = { date: apiDate, - expected: "2023-02-18 13:02:08", + expected: '2023-02-18 13:02:08', format: DEFAULT_SETTINGS.dateSavedFormat, -}; +} const defaultFolderDateFormatTestCase: testCase = { date: apiDate, - expected: "2023-02-18", + expected: '2023-02-18', format: DEFAULT_SETTINGS.folderDateFormat, -}; +} const testCases: testCase[] = [ defaultDateHighlightedFormatTestCase, defaultDateSavedFormatTestCase, defaultFolderDateFormatTestCase, luxonHierarchicalFormat, luxonHierarchicalFormatWithTime, -]; -describe("ensure default formats are as expected", () => { - test("dateHighlightedFormat", () => { - expect(DEFAULT_SETTINGS.dateHighlightedFormat).toBe("yyyy-MM-dd HH:mm:ss"); - }); - test("dateSavedFormat", () => { - expect(DEFAULT_SETTINGS.dateSavedFormat).toBe("yyyy-MM-dd HH:mm:ss"); - }); - test("folderDateFormat", () => { - expect(DEFAULT_SETTINGS.folderDateFormat).toBe("yyyy-MM-dd"); - }); -}); +] +describe('ensure default formats are as expected', () => { + test('dateHighlightedFormat', () => { + expect(DEFAULT_SETTINGS.dateHighlightedFormat).toBe('yyyy-MM-dd HH:mm:ss') + }) + test('dateSavedFormat', () => { + expect(DEFAULT_SETTINGS.dateSavedFormat).toBe('yyyy-MM-dd HH:mm:ss') + }) + test('folderDateFormat', () => { + expect(DEFAULT_SETTINGS.folderDateFormat).toBe('yyyy-MM-dd') + }) +}) -describe("formatDate on known formats", () => { - test.each(testCases)("should correctly format %s", (testCase) => { - const result = formatDate(testCase.date, testCase.format); - expect(result).toBe(testCase.expected); - }); -}); +describe('formatDate on known formats', () => { + test.each(testCases)('should correctly format %s', (testCase) => { + const result = formatDate(testCase.date, testCase.format) + expect(result).toBe(testCase.expected) + }) +}) function generateRandomISODateStrings(quantity: number): string[] { - const randomISODateStrings: string[] = []; - const timeZones = Intl.DateTimeFormat().resolvedOptions().timeZone.split(","); + const randomISODateStrings: string[] = [] + const timeZones = Intl.DateTimeFormat().resolvedOptions().timeZone.split(',') for (let i = 0; i < quantity; i++) { const date = new Date( @@ -77,80 +77,80 @@ function generateRandomISODateStrings(quantity: number): string[] { Math.floor(Math.random() * 60), Math.floor(Math.random() * 1000) ) - ); + ) // Randomly select a timezone from the available time zones const randomTimeZone = - timeZones[Math.floor(Math.random() * timeZones.length)]; + timeZones[Math.floor(Math.random() * timeZones.length)] // Convert the generated date to the randomly selected timezone // const dateTimeWithZone = DateTime.fromJSDate(date, { zone: randomTimeZone }).toUTC(); const jsDateTimeWithZone = new Date( - date.toLocaleString("en-US", { timeZone: randomTimeZone }) - ); - const luxonDate = DateTime.fromJSDate(jsDateTimeWithZone); - randomISODateStrings.push(luxonDate.toISO() as string); + date.toLocaleString('en-US', { timeZone: randomTimeZone }) + ) + const luxonDate = DateTime.fromJSDate(jsDateTimeWithZone) + randomISODateStrings.push(luxonDate.toISO() as string) } - return randomISODateStrings; + return randomISODateStrings } -describe("formatDate on random dates", () => { +describe('formatDate on random dates', () => { test.each(generateRandomISODateStrings(100))( - "should correctly format %s", + 'should correctly format %s', (date) => { - const result = formatDate(date, "yyyy-MM-dd HH:mm:ss"); + const result = formatDate(date, 'yyyy-MM-dd HH:mm:ss') // test with regex to ensure the format is correct - expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) } - ); -}); + ) +}) function getCasesWithRandomDates( testFormats: string[], quantity = 10 ): { - date: string; - luxonFormat: string; + date: string + luxonFormat: string }[] { return testFormats.flatMap((luxonFormat) => generateRandomISODateStrings(quantity).map((date) => ({ date, luxonFormat, })) - ); + ) } -describe("round trip on random dates", () => { +describe('round trip on random dates', () => { const testFormats = [ defaultDateHighlightedFormatTestCase.format, defaultDateSavedFormatTestCase.format, defaultFolderDateFormatTestCase.format, - ]; + ] // generate permutations of testCases.formats and 10 generated each - const casesWithRandomDates = getCasesWithRandomDates(testFormats); + const casesWithRandomDates = getCasesWithRandomDates(testFormats) test.each(casesWithRandomDates)( - "should be unchanged after round trip %s", + 'should be unchanged after round trip %s', (testCase) => { - const result = formatDate(testCase.date, testCase.luxonFormat); - const result2 = formatDate(result, testCase.luxonFormat); - expect(result2).toBe(result); + const result = formatDate(testCase.date, testCase.luxonFormat) + const result2 = formatDate(result, testCase.luxonFormat) + expect(result2).toBe(result) } - ); + ) const atypicalFormats = [ luxonHierarchicalFormat.format, luxonHierarchicalFormatWithTime.format, - ]; + ] test.each(getCasesWithRandomDates(atypicalFormats))( - "should be unchanged after round trip with atypical format %s", + 'should be unchanged after round trip with atypical format %s', (testCase) => { - const formattedDate = formatDate(testCase.date, testCase.luxonFormat); + const formattedDate = formatDate(testCase.date, testCase.luxonFormat) const parsedDate = DateTime.fromFormat( formattedDate, testCase.luxonFormat - ); - expect(parsedDate.isValid).toBe(true); + ) + expect(parsedDate.isValid).toBe(true) } - ); -}); + ) +}) diff --git a/src/__tests__/path_validation.spec.ts b/src/__tests__/path_validation.spec.ts index 87c8287..1454553 100644 --- a/src/__tests__/path_validation.spec.ts +++ b/src/__tests__/path_validation.spec.ts @@ -1,115 +1,115 @@ -import * as fs from "fs"; +import * as fs from 'fs' import { ILLEGAL_CHAR_REGEX, replaceIllegalChars, REPLACEMENT_CHAR, -} from "../util"; +} from '../util' const expectedManualIllegalChars: string[] = [ - "/", - "\\", - "?", - "*", - ":", - "|", + '/', + '\\', + '?', + '*', + ':', + '|', '"', - "<", - ">", - "\u0000", - "\u001F", -]; + '<', + '>', + '\u0000', + '\u001F', +] // ZERO WIDTH JOINER and SOFT HYPHEN -const expectedInvisibleChars: string[] = ["­", "‍"]; +const expectedInvisibleChars: string[] = ['­', '‍'] -describe("replaceIllegalChars() removes all expected characters", () => { +describe('replaceIllegalChars() removes all expected characters', () => { test.each(expectedManualIllegalChars)( 'Illegal character "%s" is removed', (character) => { - const input = `this${character}string`; - const output = replaceIllegalChars(input); - expect(output).not.toContain(character); + const input = `this${character}string` + const output = replaceIllegalChars(input) + expect(output).not.toContain(character) } - ); -}); + ) +}) -describe("replaceIllegalChars() function replaces illegal characters with replacement char", () => { +describe('replaceIllegalChars() function replaces illegal characters with replacement char', () => { test.each(expectedManualIllegalChars)( "Illegal character '%s' is replaced", (char) => { - const input = `this${char}string`; - const expectedOutput = `this${REPLACEMENT_CHAR}string`; - const output = replaceIllegalChars(input); - expect(output).toEqual(expectedOutput); + const input = `this${char}string` + const expectedOutput = `this${REPLACEMENT_CHAR}string` + const output = replaceIllegalChars(input) + expect(output).toEqual(expectedOutput) } - ); -}); + ) +}) -describe("replaceIllegalChars() function does not modify string without illegal characters", () => { - test.each(["this_is_a_valid_string", "this is a valid string"])( +describe('replaceIllegalChars() function does not modify string without illegal characters', () => { + test.each(['this_is_a_valid_string', 'this is a valid string'])( "String '%s' is not modified", (input) => { - const output = replaceIllegalChars(input); - expect(output).toEqual(input); + const output = replaceIllegalChars(input) + expect(output).toEqual(input) } - ); -}); + ) +}) -describe("replaceIllegalChars() function handles empty string", () => { - test("Empty string is not modified", () => { - const input = ""; - const output = replaceIllegalChars(input); - expect(output).toEqual(input); - }); -}); +describe('replaceIllegalChars() function handles empty string', () => { + test('Empty string is not modified', () => { + const input = '' + const output = replaceIllegalChars(input) + expect(output).toEqual(input) + }) +}) -describe("replaceIllegalChars() function replaces all occurrences of illegal characters", () => { +describe('replaceIllegalChars() function replaces all occurrences of illegal characters', () => { test.each(expectedManualIllegalChars)( "Illegal character '%s' is replaced", (char) => { - const input = `${char}foo${char}bar`; - const expectedOutput = `${REPLACEMENT_CHAR}foo${REPLACEMENT_CHAR}bar`; - const output = replaceIllegalChars(input); - expect(output).toEqual(expectedOutput); - expect(output.match(ILLEGAL_CHAR_REGEX)).toBeNull(); + const input = `${char}foo${char}bar` + const expectedOutput = `${REPLACEMENT_CHAR}foo${REPLACEMENT_CHAR}bar` + const output = replaceIllegalChars(input) + expect(output).toEqual(expectedOutput) + expect(output.match(ILLEGAL_CHAR_REGEX)).toBeNull() } - ); -}); + ) +}) -describe("file system behavior with non-alphanumeric characters not in the illegal character list", () => { +describe('file system behavior with non-alphanumeric characters not in the illegal character list', () => { const nonAlphanumericCharactersWithoutIllegal: string[] = Array.from( { length: 127 - 32 }, (_, i) => String.fromCharCode(i + 32) ) .filter((char) => !/^[a-zA-Z0-9]+$/.test(char)) - .map(replaceIllegalChars); + .map(replaceIllegalChars) test.each(nonAlphanumericCharactersWithoutIllegal)( "File system allows creation of file with character '%s'", (char) => { - const input = `test${char}test.txt`; + const input = `test${char}test.txt` // verify file does not already exist - expect(fs.existsSync(input)).toBe(false); - fs.writeFileSync(input, "test"); + expect(fs.existsSync(input)).toBe(false) + fs.writeFileSync(input, 'test') // verify the file exists - expect(fs.existsSync(input)).toBe(true); + expect(fs.existsSync(input)).toBe(true) // remove the file - fs.unlinkSync(input); + fs.unlinkSync(input) // verify the file has been deleted - expect(fs.existsSync(input)).toBe(false); + expect(fs.existsSync(input)).toBe(false) } - ); -}); + ) +}) -describe("replaceIllegalChars() function removes all occurrences of invisible characters", () => { +describe('replaceIllegalChars() function removes all occurrences of invisible characters', () => { test.each(expectedInvisibleChars)( "Invisible character '%s' is replaced", (char) => { - const input = `${char}foo${char}bar`; - const expectedOutput = "foobar"; - const output = replaceIllegalChars(input); - expect(output).toEqual(expectedOutput); - expect(output.match(ILLEGAL_CHAR_REGEX)).toBeNull(); + const input = `${char}foo${char}bar` + const expectedOutput = 'foobar' + const output = replaceIllegalChars(input) + expect(output).toEqual(expectedOutput) + expect(output.match(ILLEGAL_CHAR_REGEX)).toBeNull() } - ); -}); + ) +}) diff --git a/src/api.ts b/src/api.ts index 4ba915b..3d5ebf2 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,4 +1,4 @@ -import { requestUrl } from "obsidian"; +import { requestUrl } from "obsidian" export interface SearchResponse { data: { @@ -84,7 +84,7 @@ const requestHeaders = (apiKey: string) => ({ "Content-Type": "application/json", authorization: apiKey, "X-OmnivoreClient": "obsidian-plugin", -}); +}) export const loadArticles = async ( endpoint: string, @@ -163,13 +163,13 @@ export const loadArticles = async ( }, }), method: "POST", - }); + }) - const jsonRes = res.json as SearchResponse; - const articles = jsonRes.data.search.edges.map((e) => e.node); + const jsonRes = res.json as SearchResponse + const articles = jsonRes.data.search.edges.map((e) => e.node) - return [articles, jsonRes.data.search.pageInfo.hasNextPage]; -}; + return [articles, jsonRes.data.search.pageInfo.hasNextPage] +} export const deleteArticleById = async (endpoint: string, apiKey: string, articleId: string) => { @@ -198,11 +198,11 @@ export const deleteArticleById = async (endpoint: string, apiKey: string, articl }, }), method: "POST", - }); + }) - const jsonRes = res.json as DeleteArticleResponse; + const jsonRes = res.json as DeleteArticleResponse if (jsonRes.data.setBookmarkArticle.bookmarkedArticle.id === articleId) { - return true; + return true } return false diff --git a/src/main.ts b/src/main.ts index e7f01c6..9348706 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { DateTime } from "luxon"; +import { DateTime } from "luxon" import { addIcon, App, @@ -11,22 +11,22 @@ import { stringifyYaml, TFile, TFolder, -} from "obsidian"; -import { Article, deleteArticleById, loadArticles, PageType } from "./api"; +} from "obsidian" +import { Article, deleteArticleById, loadArticles, PageType } from "./api" import { DEFAULT_SETTINGS, Filter, FRONT_MATTER_VARIABLES, HighlightOrder, OmnivoreSettings, -} from "./settings"; -import { FolderSuggest } from "./settings/file-suggest"; +} from "./settings" +import { FolderSuggest } from "./settings/file-suggest" import { preParseTemplate, renderArticleContnet, renderFilename, renderFolderName, -} from "./settings/template"; +} from "./settings/template" import { DATE_FORMAT, findFrontMatterIndex, @@ -35,42 +35,42 @@ import { parseFrontMatterFromContent, removeFrontMatterFromContent, replaceIllegalChars, -} from "./util"; +} from "./util" export default class OmnivorePlugin extends Plugin { - settings: OmnivoreSettings; + settings: OmnivoreSettings async onload() { - await this.loadSettings(); - await this.resetSyncingStateSetting(); + await this.loadSettings() + await this.resetSyncingStateSetting() // update version if needed - const latestVersion = this.manifest.version; - const currentVersion = this.settings.version; + const latestVersion = this.manifest.version + const currentVersion = this.settings.version if (latestVersion !== currentVersion) { - this.settings.version = latestVersion; - this.saveSettings(); + this.settings.version = latestVersion + this.saveSettings() // show release notes const releaseNotes = `Omnivore plugin is upgraded to ${latestVersion}. What's new: https://github.com/omnivore-app/obsidian-omnivore/blob/main/CHANGELOG.md - `; - new Notice(releaseNotes, 10000); + ` + new Notice(releaseNotes, 10000) } this.addCommand({ id: "sync", name: "Sync", callback: () => { - this.fetchOmnivore(); + this.fetchOmnivore() }, - }); + }) this.addCommand({ id: "deleteArticle", name: "Delete Current Article from Omnivore", callback: () => { - this.deleteCurrentArticle(this.app.workspace.getActiveFile()); + this.deleteCurrentArticle(this.app.workspace.getActiveFile()) } }) @@ -78,89 +78,89 @@ export default class OmnivorePlugin extends Plugin { id: "resync", name: "Resync all articles", callback: () => { - this.settings.syncAt = ""; - this.saveSettings(); - new Notice("Omnivore Last Sync reset"); - this.fetchOmnivore(); + this.settings.syncAt = "" + this.saveSettings() + new Notice("Omnivore Last Sync reset") + this.fetchOmnivore() }, - }); + }) - const iconId = "Omnivore"; + const iconId = "Omnivore" // add icon addIcon( iconId, `` - ); + ) // This creates an icon in the left ribbon. this.addRibbonIcon(iconId, iconId, async (evt: MouseEvent) => { // Called when the user clicks the icon. - await this.fetchOmnivore(); - }); + await this.fetchOmnivore() + }) // This adds a settings tab so the user can configure various aspects of the plugin - this.addSettingTab(new OmnivoreSettingTab(this.app, this)); + this.addSettingTab(new OmnivoreSettingTab(this.app, this)) - this.scheduleSync(); + this.scheduleSync() } onunload() {} async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) } async saveSettings() { - await this.saveData(this.settings); + await this.saveData(this.settings) } scheduleSync() { // clear previous interval if (this.settings.intervalId > 0) { - window.clearInterval(this.settings.intervalId); + window.clearInterval(this.settings.intervalId) } - const frequency = this.settings.frequency; + const frequency = this.settings.frequency if (frequency > 0) { // schedule new interval const intervalId = window.setInterval(async () => { - await this.fetchOmnivore(false); - }, frequency * 60 * 1000); + await this.fetchOmnivore(false) + }, frequency * 60 * 1000) // save new interval id - this.settings.intervalId = intervalId; - this.saveSettings(); + this.settings.intervalId = intervalId + this.saveSettings() // clear interval when plugin is unloaded - this.registerInterval(intervalId); + this.registerInterval(intervalId) } } async downloadFileAsAttachment(article: Article): Promise { // download pdf from the URL to the attachment folder - const url = article.url; + const url = article.url const response = await requestUrl({ url, contentType: "application/pdf", - }); + }) const folderName = normalizePath( renderFolderName( article, this.settings.attachmentFolder, this.settings.folderDateFormat ) - ); - const folder = app.vault.getAbstractFileByPath(folderName); + ) + const folder = app.vault.getAbstractFileByPath(folderName) if (!(folder instanceof TFolder)) { - await app.vault.createFolder(folderName); + await app.vault.createFolder(folderName) } - const fileName = normalizePath(`${folderName}/${article.id}.pdf`); - const file = app.vault.getAbstractFileByPath(fileName); + const fileName = normalizePath(`${folderName}/${article.id}.pdf`) + const file = app.vault.getAbstractFileByPath(fileName) if (!(file instanceof TFile)) { const newFile = await app.vault.createBinary( fileName, response.arrayBuffer - ); - return newFile.path; + ) + return newFile.path } - return file.path; + return file.path } async fetchOmnivore(manualSync = true) { @@ -177,38 +177,38 @@ export default class OmnivorePlugin extends Plugin { isSingleFile, frontMatterVariables, frontMatterTemplate, - } = this.settings; + } = this.settings if (syncing) { - new Notice("🐢 Already syncing ..."); - return; + new Notice("🐢 Already syncing ...") + return } if (!apiKey) { - new Notice("Missing Omnivore api key"); - return; + new Notice("Missing Omnivore api key") + return } - this.settings.syncing = true; - await this.saveSettings(); + this.settings.syncing = true + await this.saveSettings() try { - console.log(`obsidian-omnivore starting sync since: '${syncAt}'`); + console.log(`obsidian-omnivore starting sync since: '${syncAt}'`) - manualSync && new Notice("🚀 Fetching articles ..."); + manualSync && new Notice("🚀 Fetching articles ...") // pre-parse template - frontMatterTemplate && preParseTemplate(frontMatterTemplate); - const templateSpans = preParseTemplate(template); + frontMatterTemplate && preParseTemplate(frontMatterTemplate) + const templateSpans = preParseTemplate(template) // check if we need to include content or file attachment const includeContent = templateSpans.some( (templateSpan) => templateSpan[1] === "content" - ); + ) const includeFileAttachment = templateSpans.some( (templateSpan) => templateSpan[1] === "fileAttachment" - ); + ) - const size = 50; + const size = 50 for ( let hasNextPage = true, articles: Article[] = [], after = 0; hasNextPage; @@ -223,21 +223,21 @@ export default class OmnivorePlugin extends Plugin { getQueryFromFilter(filter, customQuery), includeContent, "highlightedMarkdown" - ); + ) for (const article of articles) { const folderName = normalizePath( renderFolderName(article, folder, this.settings.folderDateFormat) - ); + ) const omnivoreFolder = - this.app.vault.getAbstractFileByPath(folderName); + this.app.vault.getAbstractFileByPath(folderName) if (!(omnivoreFolder instanceof TFolder)) { - await this.app.vault.createFolder(folderName); + await this.app.vault.createFolder(folderName) } const fileAttachment = article.pageType === PageType.File && includeFileAttachment ? await this.downloadFileAsAttachment(article) - : undefined; + : undefined const content = await renderArticleContnet( article, template, @@ -248,139 +248,139 @@ export default class OmnivorePlugin extends Plugin { frontMatterVariables, frontMatterTemplate, fileAttachment - ); + ) // use the custom filename const customFilename = replaceIllegalChars( renderFilename(article, filename, this.settings.filenameDateFormat) - ); - const pageName = `${folderName}/${customFilename}.md`; - const normalizedPath = normalizePath(pageName); + ) + const pageName = `${folderName}/${customFilename}.md` + const normalizedPath = normalizePath(pageName) const omnivoreFile = - this.app.vault.getAbstractFileByPath(normalizedPath); + this.app.vault.getAbstractFileByPath(normalizedPath) if (omnivoreFile instanceof TFile) { // file exists, so we might need to update it if (isSingleFile) { // sync into a single file - const existingContent = await this.app.vault.read(omnivoreFile); + const existingContent = await this.app.vault.read(omnivoreFile) // we need to remove the front matter const contentWithoutFrontmatter = - removeFrontMatterFromContent(content); + removeFrontMatterFromContent(content) const existingContentWithoutFrontmatter = - removeFrontMatterFromContent(existingContent); + removeFrontMatterFromContent(existingContent) // get front matter from content let existingFrontMatter = - parseFrontMatterFromContent(existingContent) || []; + parseFrontMatterFromContent(existingContent) || [] if (!Array.isArray(existingFrontMatter)) { // convert front matter to array - existingFrontMatter = [existingFrontMatter]; + existingFrontMatter = [existingFrontMatter] } - const newFrontMatter = parseFrontMatterFromContent(content); + const newFrontMatter = parseFrontMatterFromContent(content) if ( !newFrontMatter || !Array.isArray(newFrontMatter) || newFrontMatter.length === 0 ) { - throw new Error("Front matter does not exist in the template"); + throw new Error("Front matter does not exist in the template") } - let newContentWithoutFrontMatter: string; + let newContentWithoutFrontMatter: string // find the front matter with the same id const frontMatterIdx = findFrontMatterIndex( existingFrontMatter, article.id - ); + ) if (frontMatterIdx >= 0) { // this article already exists in the file // we need to locate the article which is wrapped in comments // and replace the content - const sectionStart = `%%${article.id}_start%%`; - const sectionEnd = `%%${article.id}_end%%`; + const sectionStart = `%%${article.id}_start%%` + const sectionEnd = `%%${article.id}_end%%` const existingContentRegex = new RegExp( `${sectionStart}.*?${sectionEnd}`, "s" - ); + ) newContentWithoutFrontMatter = existingContentWithoutFrontmatter.replace( existingContentRegex, contentWithoutFrontmatter - ); + ) - existingFrontMatter[frontMatterIdx] = newFrontMatter[0]; + existingFrontMatter[frontMatterIdx] = newFrontMatter[0] } else { // this article doesn't exist in the file // prepend the article - newContentWithoutFrontMatter = `${contentWithoutFrontmatter}\n\n${existingContentWithoutFrontmatter}`; + newContentWithoutFrontMatter = `${contentWithoutFrontmatter}\n\n${existingContentWithoutFrontmatter}` // prepend new front matter which is an array - existingFrontMatter.unshift(newFrontMatter[0]); + existingFrontMatter.unshift(newFrontMatter[0]) } const newFrontMatterStr = `---\n${stringifyYaml( existingFrontMatter - )}---`; + )}---` await this.app.vault.modify( omnivoreFile, `${newFrontMatterStr}\n\n${newContentWithoutFrontMatter}` - ); - continue; + ) + continue } // sync into separate files await this.app.fileManager.processFrontMatter( omnivoreFile, async (frontMatter) => { - const id = frontMatter.id; + const id = frontMatter.id if (id && id !== article.id) { // this article has the same name but different id - const newPageName = `${folderName}/${customFilename}-${article.id}.md`; - const newNormalizedPath = normalizePath(newPageName); + const newPageName = `${folderName}/${customFilename}-${article.id}.md` + const newNormalizedPath = normalizePath(newPageName) const newOmnivoreFile = - this.app.vault.getAbstractFileByPath(newNormalizedPath); + this.app.vault.getAbstractFileByPath(newNormalizedPath) if (newOmnivoreFile instanceof TFile) { // a file with the same name and id already exists, so we need to update it const existingContent = await this.app.vault.read( newOmnivoreFile - ); + ) if (existingContent !== content) { - await this.app.vault.modify(newOmnivoreFile, content); + await this.app.vault.modify(newOmnivoreFile, content) } - return; + return } // a file with the same name but different id already exists, so we need to create it - await this.app.vault.create(newNormalizedPath, content); - return; + await this.app.vault.create(newNormalizedPath, content) + return } // a file with the same id already exists, so we might need to update it - const existingContent = await this.app.vault.read(omnivoreFile); + const existingContent = await this.app.vault.read(omnivoreFile) if (existingContent !== content) { - await this.app.vault.modify(omnivoreFile, content); + await this.app.vault.modify(omnivoreFile, content) } } - ); - continue; + ) + continue } // file doesn't exist, so we need to create it try { - await this.app.vault.create(normalizedPath, content); + await this.app.vault.create(normalizedPath, content) } catch (error) { if (error.toString().includes("File already exists")) { new Notice( `Skipping file creation: ${normalizedPath}. Please check if you have duplicated article titles and delete the file if needed.` - ); + ) } else { - throw error; + throw error } } } } - manualSync && new Notice("🔖 Articles fetched"); - this.settings.syncAt = DateTime.local().toFormat(DATE_FORMAT); + manualSync && new Notice("🔖 Articles fetched") + this.settings.syncAt = DateTime.local().toFormat(DATE_FORMAT) } catch (e) { - new Notice("Failed to fetch articles"); - console.error(e); + new Notice("Failed to fetch articles") + console.error(e) } finally { - this.settings.syncing = false; - await this.saveSettings(); + this.settings.syncing = false + await this.saveSettings() } } @@ -391,43 +391,43 @@ export default class OmnivorePlugin extends Plugin { //use frontmatter id to find the file const articleId = this.app.metadataCache.getFileCache(file)?.frontmatter?.id if (!articleId) { - new Notice("Failed to delete article: article id not found"); + new Notice("Failed to delete article: article id not found") } try{ const isDeleted = deleteArticleById(this.settings.endpoint, this.settings.apiKey, articleId) if(!isDeleted) { - new Notice("Failed to delete article in Omnivore"); + new Notice("Failed to delete article in Omnivore") } } catch (e) { - new Notice("Failed to delete article in Omnivore"); - console.error(e); + new Notice("Failed to delete article in Omnivore") + console.error(e) } await this.app.vault.delete(file) } private async resetSyncingStateSetting() { - this.settings.syncing = false; - this.settings.intervalId = 0; - await this.saveSettings(); + this.settings.syncing = false + this.settings.intervalId = 0 + await this.saveSettings() } } class OmnivoreSettingTab extends PluginSettingTab { - plugin: OmnivorePlugin; + plugin: OmnivorePlugin constructor(app: App, plugin: OmnivorePlugin) { - super(app, plugin); - this.plugin = plugin; + super(app, plugin) + this.plugin = plugin } display(): void { - const { containerEl } = this; + const { containerEl } = this - containerEl.empty(); + containerEl.empty() - containerEl.createEl("h2", { text: "Settings for Omnivore plugin" }); + containerEl.createEl("h2", { text: "Settings for Omnivore plugin" }) new Setting(containerEl) .setName("API Key") @@ -439,7 +439,7 @@ class OmnivoreSettingTab extends PluginSettingTab { text: "https://omnivore.app/settings/api", href: "https://omnivore.app/settings/api", }) - ); + ) }) ) .addText((text) => @@ -447,23 +447,23 @@ class OmnivoreSettingTab extends PluginSettingTab { .setPlaceholder("Enter your Omnivore Api Key") .setValue(this.plugin.settings.apiKey) .onChange(async (value) => { - this.plugin.settings.apiKey = value; - await this.plugin.saveSettings(); + this.plugin.settings.apiKey = value + await this.plugin.saveSettings() }) - ); + ) new Setting(containerEl) .setName("Filter") .setDesc("Select an Omnivore search filter type") .addDropdown((dropdown) => { - dropdown.addOptions(Filter); + dropdown.addOptions(Filter) dropdown .setValue(this.plugin.settings.filter) .onChange(async (value) => { - this.plugin.settings.filter = value; - await this.plugin.saveSettings(); - }); - }); + this.plugin.settings.filter = value + await this.plugin.saveSettings() + }) + }) new Setting(containerEl) .setName("Custom query") @@ -476,7 +476,7 @@ class OmnivoreSettingTab extends PluginSettingTab { href: "https://docs.omnivore.app/using/search", }), " for more info on search query syntax. Make sure your Filter (in the section above) is set to advanced when using a custom query." - ); + ) }) ) .addText((text) => @@ -486,10 +486,10 @@ class OmnivoreSettingTab extends PluginSettingTab { ) .setValue(this.plugin.settings.customQuery) .onChange(async (value) => { - this.plugin.settings.customQuery = value; - await this.plugin.saveSettings(); + this.plugin.settings.customQuery = value + await this.plugin.saveSettings() }) - ); + ) new Setting(containerEl) .setName("Last Sync") @@ -500,23 +500,23 @@ class OmnivoreSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.syncAt) .setDefaultFormat("yyyy-MM-dd'T'HH:mm:ss") .onChange(async (value) => { - this.plugin.settings.syncAt = value; - await this.plugin.saveSettings(); + this.plugin.settings.syncAt = value + await this.plugin.saveSettings() }) - ); + ) new Setting(containerEl) .setName("Highlight Order") .setDesc("Select the order in which highlights are applied") .addDropdown((dropdown) => { - dropdown.addOptions(HighlightOrder); + dropdown.addOptions(HighlightOrder) dropdown .setValue(this.plugin.settings.highlightOrder) .onChange(async (value) => { - this.plugin.settings.highlightOrder = value; - await this.plugin.saveSettings(); - }); - }); + this.plugin.settings.highlightOrder = value + await this.plugin.saveSettings() + }) + }) new Setting(containerEl) .setName("Front Matter") @@ -534,7 +534,7 @@ class OmnivoreSettingTab extends PluginSettingTab { fragment.createEl("br"), fragment.createEl("br"), "If you want to use a custom front matter template, you can enter it below under the advanced settings" - ); + ) }) ) .addTextArea((text) => { @@ -550,12 +550,12 @@ class OmnivoreSettingTab extends PluginSettingTab { (v, i, a) => FRONT_MATTER_VARIABLES.includes(v.split("::")[0]) && a.indexOf(v) === i - ); - await this.plugin.saveSettings(); - }); - text.inputEl.setAttr("rows", 4); - text.inputEl.setAttr("cols", 30); - }); + ) + await this.plugin.saveSettings() + }) + text.inputEl.setAttr("rows", 4) + text.inputEl.setAttr("cols", 30) + }) new Setting(containerEl) .setName("Article Template") @@ -570,7 +570,7 @@ class OmnivoreSettingTab extends PluginSettingTab { fragment.createEl("br"), fragment.createEl("br"), "If you want to use a custom front matter template, you can enter it below under the advanced settings" - ); + ) }) ) .addTextArea((text) => { @@ -581,11 +581,11 @@ class OmnivoreSettingTab extends PluginSettingTab { // if template is empty, use default template this.plugin.settings.template = value ? value - : DEFAULT_SETTINGS.template; - await this.plugin.saveSettings(); - }); - text.inputEl.setAttr("rows", 25); - text.inputEl.setAttr("cols", 50); + : DEFAULT_SETTINGS.template + await this.plugin.saveSettings() + }) + text.inputEl.setAttr("rows", 25) + text.inputEl.setAttr("cols", 50) }) .addExtraButton((button) => { // add a button to reset template @@ -593,12 +593,12 @@ class OmnivoreSettingTab extends PluginSettingTab { .setIcon("reset") .setTooltip("Reset template") .onClick(async () => { - this.plugin.settings.template = DEFAULT_SETTINGS.template; - await this.plugin.saveSettings(); - this.display(); - new Notice("Template reset"); - }); - }); + this.plugin.settings.template = DEFAULT_SETTINGS.template + await this.plugin.saveSettings() + this.display() + new Notice("Template reset") + }) + }) new Setting(containerEl) .setName("Frequency") @@ -611,18 +611,18 @@ class OmnivoreSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.frequency.toString()) .onChange(async (value) => { // validate frequency - const frequency = parseInt(value); + const frequency = parseInt(value) if (isNaN(frequency)) { - new Notice("Frequency must be a positive integer"); - return; + new Notice("Frequency must be a positive integer") + return } // save frequency - this.plugin.settings.frequency = frequency; - await this.plugin.saveSettings(); + this.plugin.settings.frequency = frequency + await this.plugin.saveSettings() - this.plugin.scheduleSync(); + this.plugin.scheduleSync() }) - ); + ) new Setting(containerEl) .setName("Folder") @@ -630,30 +630,30 @@ class OmnivoreSettingTab extends PluginSettingTab { "Enter the folder where the data will be stored. {{{title}}}, {{{dateSaved}}} and {{{datePublished}}} could be used in the folder name" ) .addSearch((search) => { - new FolderSuggest(this.app, search.inputEl); + new FolderSuggest(this.app, search.inputEl) search .setPlaceholder("Enter the folder") .setValue(this.plugin.settings.folder) .onChange(async (value) => { - this.plugin.settings.folder = value; - await this.plugin.saveSettings(); - }); - }); + this.plugin.settings.folder = value + await this.plugin.saveSettings() + }) + }) new Setting(containerEl) .setName("Attachment Folder") .setDesc( "Enter the folder where the attachment will be downloaded to. {{{title}}}, {{{dateSaved}}} and {{{datePublished}}} could be used in the folder name" ) .addSearch((search) => { - new FolderSuggest(this.app, search.inputEl); + new FolderSuggest(this.app, search.inputEl) search .setPlaceholder("Enter the attachment folder") .setValue(this.plugin.settings.attachmentFolder) .onChange(async (value) => { - this.plugin.settings.attachmentFolder = value; - await this.plugin.saveSettings(); - }); - }); + this.plugin.settings.attachmentFolder = value + await this.plugin.saveSettings() + }) + }) new Setting(containerEl) .setName("Is Single File") @@ -664,10 +664,10 @@ class OmnivoreSettingTab extends PluginSettingTab { toggle .setValue(this.plugin.settings.isSingleFile) .onChange(async (value) => { - this.plugin.settings.isSingleFile = value; - await this.plugin.saveSettings(); + this.plugin.settings.isSingleFile = value + await this.plugin.saveSettings() }) - ); + ) new Setting(containerEl) .setName("Filename") @@ -679,10 +679,10 @@ class OmnivoreSettingTab extends PluginSettingTab { .setPlaceholder("Enter the filename") .setValue(this.plugin.settings.filename) .onChange(async (value) => { - this.plugin.settings.filename = value; - await this.plugin.saveSettings(); + this.plugin.settings.filename = value + await this.plugin.saveSettings() }) - ); + ) new Setting(containerEl) .setName("Filename Date Format") @@ -694,7 +694,7 @@ class OmnivoreSettingTab extends PluginSettingTab { text: "reference", href: "https://moment.github.io/luxon/#/formatting?id=table-of-tokens", }) - ); + ) }) ) .addText((text) => @@ -702,10 +702,10 @@ class OmnivoreSettingTab extends PluginSettingTab { .setPlaceholder("yyyy-MM-dd") .setValue(this.plugin.settings.filenameDateFormat) .onChange(async (value) => { - this.plugin.settings.filenameDateFormat = value; - await this.plugin.saveSettings(); + this.plugin.settings.filenameDateFormat = value + await this.plugin.saveSettings() }) - ); + ) new Setting(containerEl) .setName("Folder Date Format") @@ -717,7 +717,7 @@ class OmnivoreSettingTab extends PluginSettingTab { text: "reference", href: "https://moment.github.io/luxon/#/formatting?id=table-of-tokens", }) - ); + ) }) ) .addText((text) => @@ -725,10 +725,10 @@ class OmnivoreSettingTab extends PluginSettingTab { .setPlaceholder("yyyy-MM-dd") .setValue(this.plugin.settings.folderDateFormat) .onChange(async (value) => { - this.plugin.settings.folderDateFormat = value; - await this.plugin.saveSettings(); + this.plugin.settings.folderDateFormat = value + await this.plugin.saveSettings() }) - ); + ) new Setting(containerEl) .setName("Date Saved Format") .setDesc( @@ -739,10 +739,10 @@ class OmnivoreSettingTab extends PluginSettingTab { .setPlaceholder("yyyy-MM-dd'T'HH:mm:ss") .setValue(this.plugin.settings.dateSavedFormat) .onChange(async (value) => { - this.plugin.settings.dateSavedFormat = value; - await this.plugin.saveSettings(); + this.plugin.settings.dateSavedFormat = value + await this.plugin.saveSettings() }) - ); + ) new Setting(containerEl) .setName("Date Highlighted Format") .setDesc( @@ -753,19 +753,19 @@ class OmnivoreSettingTab extends PluginSettingTab { .setPlaceholder("Date Highlighted Format") .setValue(this.plugin.settings.dateHighlightedFormat) .onChange(async (value) => { - this.plugin.settings.dateHighlightedFormat = value; - await this.plugin.saveSettings(); + this.plugin.settings.dateHighlightedFormat = value + await this.plugin.saveSettings() }) - ); + ) containerEl.createEl("h5", { cls: "omnivore-collapsible", text: "Advanced Settings", - }); + }) const advancedSettings = containerEl.createEl("div", { cls: "omnivore-content", - }); + }) new Setting(advancedSettings) .setName("API Endpoint") @@ -775,10 +775,10 @@ class OmnivoreSettingTab extends PluginSettingTab { .setPlaceholder("API endpoint") .setValue(this.plugin.settings.endpoint) .onChange(async (value) => { - this.plugin.settings.endpoint = value; - await this.plugin.saveSettings(); + this.plugin.settings.endpoint = value + await this.plugin.saveSettings() }) - ); + ) new Setting(advancedSettings) .setName("Front Matter Template") @@ -796,7 +796,7 @@ class OmnivoreSettingTab extends PluginSettingTab { fragment.createEl("br"), fragment.createEl("br"), "If this template is set, it will override the Front Matter so please make sure your template is a valid YAML." - ); + ) }) ) .addTextArea((text) => { @@ -804,12 +804,12 @@ class OmnivoreSettingTab extends PluginSettingTab { .setPlaceholder("Enter the template") .setValue(this.plugin.settings.frontMatterTemplate) .onChange(async (value) => { - this.plugin.settings.frontMatterTemplate = value; - await this.plugin.saveSettings(); - }); + this.plugin.settings.frontMatterTemplate = value + await this.plugin.saveSettings() + }) - text.inputEl.setAttr("rows", 10); - text.inputEl.setAttr("cols", 30); + text.inputEl.setAttr("rows", 10) + text.inputEl.setAttr("cols", 30) }) .addExtraButton((button) => { // add a button to reset template @@ -818,30 +818,30 @@ class OmnivoreSettingTab extends PluginSettingTab { .setTooltip("Reset front matter template") .onClick(async () => { this.plugin.settings.frontMatterTemplate = - DEFAULT_SETTINGS.frontMatterTemplate; - await this.plugin.saveSettings(); - this.display(); - new Notice("Front matter template reset"); - }); - }); + DEFAULT_SETTINGS.frontMatterTemplate + await this.plugin.saveSettings() + this.display() + new Notice("Front matter template reset") + }) + }) - const help = containerEl.createEl("p"); - help.innerHTML = `For more information, please visit our GitHub page, email us at feedback@omnivore.app or join our Discord server.`; + const help = containerEl.createEl("p") + help.innerHTML = `For more information, please visit our GitHub page, email us at feedback@omnivore.app or join our Discord server.` // script to make collapsible sections - const coll = document.getElementsByClassName("omnivore-collapsible"); - let i; + const coll = document.getElementsByClassName("omnivore-collapsible") + let i for (i = 0; i < coll.length; i++) { coll[i].addEventListener("click", function () { - this.classList.toggle("omnivore-active"); - const content = this.nextElementSibling; + this.classList.toggle("omnivore-active") + const content = this.nextElementSibling if (content.style.maxHeight) { - content.style.maxHeight = null; + content.style.maxHeight = null } else { - content.style.maxHeight = "fit-content"; + content.style.maxHeight = "fit-content" } - }); + }) } } } diff --git a/src/settings/file-suggest.ts b/src/settings/file-suggest.ts index b5b5463..b697e8a 100644 --- a/src/settings/file-suggest.ts +++ b/src/settings/file-suggest.ts @@ -1,64 +1,64 @@ // Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes -import { TAbstractFile, TFile, TFolder } from "obsidian"; +import { TAbstractFile, TFile, TFolder } from 'obsidian' -import { TextInputSuggest } from "./suggest"; +import { TextInputSuggest } from './suggest' export class FileSuggest extends TextInputSuggest { getSuggestions(inputStr: string): TFile[] { - const abstractFiles = this.app.vault.getAllLoadedFiles(); - const files: TFile[] = []; - const lowerCaseInputStr = inputStr.toLowerCase(); + const abstractFiles = this.app.vault.getAllLoadedFiles() + const files: TFile[] = [] + const lowerCaseInputStr = inputStr.toLowerCase() abstractFiles.forEach((file: TAbstractFile) => { if ( file instanceof TFile && - file.extension === "md" && + file.extension === 'md' && file.path.toLowerCase().contains(lowerCaseInputStr) ) { - files.push(file); + files.push(file) } - }); + }) - return files; + return files } renderSuggestion(file: TFile, el: HTMLElement): void { - el.setText(file.path); + el.setText(file.path) } selectSuggestion(file: TFile): void { - this.inputEl.value = file.path; - this.inputEl.trigger("input"); - this.close(); + this.inputEl.value = file.path + this.inputEl.trigger('input') + this.close() } } export class FolderSuggest extends TextInputSuggest { getSuggestions(inputStr: string): TFolder[] { - const abstractFiles = this.app.vault.getAllLoadedFiles(); - const folders: TFolder[] = []; - const lowerCaseInputStr = inputStr.toLowerCase(); + const abstractFiles = this.app.vault.getAllLoadedFiles() + const folders: TFolder[] = [] + const lowerCaseInputStr = inputStr.toLowerCase() abstractFiles.forEach((folder: TAbstractFile) => { if ( folder instanceof TFolder && folder.path.toLowerCase().contains(lowerCaseInputStr) ) { - folders.push(folder); + folders.push(folder) } - }); + }) - return folders; + return folders } renderSuggestion(file: TFolder, el: HTMLElement): void { - el.setText(file.path); + el.setText(file.path) } selectSuggestion(file: TFolder): void { - this.inputEl.value = file.path; - this.inputEl.trigger("input"); - this.close(); + this.inputEl.value = file.path + this.inputEl.trigger('input') + this.close() } } diff --git a/src/settings/index.ts b/src/settings/index.ts index e7452c1..5e98d5e 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -1,79 +1,79 @@ -import { DEFAULT_TEMPLATE } from "./template"; +import { DEFAULT_TEMPLATE } from './template' export const FRONT_MATTER_VARIABLES = [ - "title", - "author", - "tags", - "date_saved", - "date_published", - "omnivore_url", - "site_name", - "original_url", - "description", - "note", - "type", - "date_read", - "words_count", - "read_length", - "state", - "date_archived", -]; + 'title', + 'author', + 'tags', + 'date_saved', + 'date_published', + 'omnivore_url', + 'site_name', + 'original_url', + 'description', + 'note', + 'type', + 'date_read', + 'words_count', + 'read_length', + 'state', + 'date_archived', +] export const DEFAULT_SETTINGS: OmnivoreSettings = { - dateHighlightedFormat: "yyyy-MM-dd HH:mm:ss", - dateSavedFormat: "yyyy-MM-dd HH:mm:ss", - apiKey: "", - filter: "HIGHLIGHTS", - syncAt: "", - customQuery: "", + dateHighlightedFormat: 'yyyy-MM-dd HH:mm:ss', + dateSavedFormat: 'yyyy-MM-dd HH:mm:ss', + apiKey: '', + filter: 'HIGHLIGHTS', + syncAt: '', + customQuery: '', template: DEFAULT_TEMPLATE, - highlightOrder: "LOCATION", + highlightOrder: 'LOCATION', syncing: false, - folder: "Omnivore/{{{date}}}", - folderDateFormat: "yyyy-MM-dd", - endpoint: "https://api-prod.omnivore.app/api/graphql", - filename: "{{{title}}}", - filenameDateFormat: "yyyy-MM-dd", - attachmentFolder: "Omnivore/attachments", - version: "0.0.0", + folder: 'Omnivore/{{{date}}}', + folderDateFormat: 'yyyy-MM-dd', + endpoint: 'https://api-prod.omnivore.app/api/graphql', + filename: '{{{title}}}', + filenameDateFormat: 'yyyy-MM-dd', + attachmentFolder: 'Omnivore/attachments', + version: '0.0.0', isSingleFile: false, frequency: 0, intervalId: 0, frontMatterVariables: [], - frontMatterTemplate: "", -}; + frontMatterTemplate: '', +} export enum Filter { - ALL = "import all my articles", - HIGHLIGHTS = "import just highlights", - ADVANCED = "advanced", + ALL = 'import all my articles', + HIGHLIGHTS = 'import just highlights', + ADVANCED = 'advanced', } export enum HighlightOrder { - LOCATION = "the location of highlights in the article", - TIME = "the time that highlights are updated", + LOCATION = 'the location of highlights in the article', + TIME = 'the time that highlights are updated', } export interface OmnivoreSettings { - apiKey: string; - filter: string; - syncAt: string; - customQuery: string; - highlightOrder: string; - template: string; - syncing: boolean; - folder: string; - folderDateFormat: string; - endpoint: string; - dateHighlightedFormat: string; - dateSavedFormat: string; - filename: string; - attachmentFolder: string; - version: string; - isSingleFile: boolean; - frequency: number; - intervalId: number; - frontMatterVariables: string[]; - frontMatterTemplate: string; - filenameDateFormat: string; + apiKey: string + filter: string + syncAt: string + customQuery: string + highlightOrder: string + template: string + syncing: boolean + folder: string + folderDateFormat: string + endpoint: string + dateHighlightedFormat: string + dateSavedFormat: string + filename: string + attachmentFolder: string + version: string + isSingleFile: boolean + frequency: number + intervalId: number + frontMatterVariables: string[] + frontMatterTemplate: string + filenameDateFormat: string } diff --git a/src/settings/suggest.ts b/src/settings/suggest.ts index fc0f29d..bceea2e 100644 --- a/src/settings/suggest.ts +++ b/src/settings/suggest.ts @@ -1,179 +1,187 @@ // Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes -import { createPopper, type Instance as PopperInstance } from "@popperjs/core"; -import { App, type ISuggestOwner, Scope } from "obsidian"; -import { wrapAround } from "../util"; +import { createPopper, type Instance as PopperInstance } from '@popperjs/core' +import { App, type ISuggestOwner, Scope } from 'obsidian' +import { wrapAround } from '../util' class Suggest { - private owner: ISuggestOwner; - private values: T[]; - private suggestions: HTMLDivElement[]; - private selectedItem: number; - private containerEl: HTMLElement; + private owner: ISuggestOwner + private values: T[] + private suggestions: HTMLDivElement[] + private selectedItem: number + private containerEl: HTMLElement constructor(owner: ISuggestOwner, containerEl: HTMLElement, scope: Scope) { - this.owner = owner; - this.containerEl = containerEl; + this.owner = owner + this.containerEl = containerEl - containerEl.on("click", ".suggestion-item", this.onSuggestionClick.bind(this)); containerEl.on( - "mousemove", - ".suggestion-item", + 'click', + '.suggestion-item', + this.onSuggestionClick.bind(this) + ) + containerEl.on( + 'mousemove', + '.suggestion-item', this.onSuggestionMouseover.bind(this) - ); + ) - scope.register([], "ArrowUp", (event) => { + scope.register([], 'ArrowUp', (event) => { if (!event.isComposing) { - this.setSelectedItem(this.selectedItem - 1, true); - return false; + this.setSelectedItem(this.selectedItem - 1, true) + return false } - }); + }) - scope.register([], "ArrowDown", (event) => { + scope.register([], 'ArrowDown', (event) => { if (!event.isComposing) { - this.setSelectedItem(this.selectedItem + 1, true); - return false; + this.setSelectedItem(this.selectedItem + 1, true) + return false } - }); + }) - scope.register([], "Enter", (event) => { + scope.register([], 'Enter', (event) => { if (!event.isComposing) { - this.useSelectedItem(event); - return false; + this.useSelectedItem(event) + return false } - }); + }) } onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void { - event.preventDefault(); + event.preventDefault() - const item = this.suggestions.indexOf(el); - this.setSelectedItem(item, false); - this.useSelectedItem(event); + const item = this.suggestions.indexOf(el) + this.setSelectedItem(item, false) + this.useSelectedItem(event) } onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void { - const item = this.suggestions.indexOf(el); - this.setSelectedItem(item, false); + const item = this.suggestions.indexOf(el) + this.setSelectedItem(item, false) } setSuggestions(values: T[]) { - this.containerEl.empty(); - const suggestionEls: HTMLDivElement[] = []; + this.containerEl.empty() + const suggestionEls: HTMLDivElement[] = [] values.forEach((value) => { - const suggestionEl = this.containerEl.createDiv("suggestion-item"); - this.owner.renderSuggestion(value, suggestionEl); - suggestionEls.push(suggestionEl); - }); - - this.values = values; - this.suggestions = suggestionEls; - this.setSelectedItem(0, false); + const suggestionEl = this.containerEl.createDiv('suggestion-item') + this.owner.renderSuggestion(value, suggestionEl) + suggestionEls.push(suggestionEl) + }) + + this.values = values + this.suggestions = suggestionEls + this.setSelectedItem(0, false) } useSelectedItem(event: MouseEvent | KeyboardEvent) { - const currentValue = this.values[this.selectedItem]; + const currentValue = this.values[this.selectedItem] if (currentValue) { - this.owner.selectSuggestion(currentValue, event); + this.owner.selectSuggestion(currentValue, event) } } setSelectedItem(selectedIndex: number, scrollIntoView: boolean) { - const normalizedIndex = wrapAround(selectedIndex, this.suggestions.length); - const prevSelectedSuggestion = this.suggestions[this.selectedItem]; - const selectedSuggestion = this.suggestions[normalizedIndex]; + const normalizedIndex = wrapAround(selectedIndex, this.suggestions.length) + const prevSelectedSuggestion = this.suggestions[this.selectedItem] + const selectedSuggestion = this.suggestions[normalizedIndex] - prevSelectedSuggestion?.removeClass("is-selected"); - selectedSuggestion?.addClass("is-selected"); + prevSelectedSuggestion?.removeClass('is-selected') + selectedSuggestion?.addClass('is-selected') - this.selectedItem = normalizedIndex; + this.selectedItem = normalizedIndex if (scrollIntoView) { - selectedSuggestion.scrollIntoView(false); + selectedSuggestion.scrollIntoView(false) } } } export abstract class TextInputSuggest implements ISuggestOwner { - protected app: App; - protected inputEl: HTMLInputElement; + protected app: App + protected inputEl: HTMLInputElement - private popper: PopperInstance; - private scope: Scope; - private suggestEl: HTMLElement; - private suggest: Suggest; + private popper: PopperInstance + private scope: Scope + private suggestEl: HTMLElement + private suggest: Suggest constructor(app: App, inputEl: HTMLInputElement) { - this.app = app; - this.inputEl = inputEl; - this.scope = new Scope(); - - this.suggestEl = createDiv("suggestion-container"); - const suggestion = this.suggestEl.createDiv("suggestion"); - this.suggest = new Suggest(this, suggestion, this.scope); - - this.scope.register([], "Escape", this.close.bind(this)); - - this.inputEl.addEventListener("input", this.onInputChanged.bind(this)); - this.inputEl.addEventListener("focus", this.onInputChanged.bind(this)); - this.inputEl.addEventListener("blur", this.close.bind(this)); - this.suggestEl.on("mousedown", ".suggestion-container", (event: MouseEvent) => { - event.preventDefault(); - }); + this.app = app + this.inputEl = inputEl + this.scope = new Scope() + + this.suggestEl = createDiv('suggestion-container') + const suggestion = this.suggestEl.createDiv('suggestion') + this.suggest = new Suggest(this, suggestion, this.scope) + + this.scope.register([], 'Escape', this.close.bind(this)) + + this.inputEl.addEventListener('input', this.onInputChanged.bind(this)) + this.inputEl.addEventListener('focus', this.onInputChanged.bind(this)) + this.inputEl.addEventListener('blur', this.close.bind(this)) + this.suggestEl.on( + 'mousedown', + '.suggestion-container', + (event: MouseEvent) => { + event.preventDefault() + } + ) } onInputChanged(): void { - const inputStr = this.inputEl.value; - const suggestions = this.getSuggestions(inputStr); + const inputStr = this.inputEl.value + const suggestions = this.getSuggestions(inputStr) if (suggestions.length > 0) { - this.suggest.setSuggestions(suggestions); + this.suggest.setSuggestions(suggestions) // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.open((this.app).dom.appContainerEl, this.inputEl); + this.open((this.app).dom.appContainerEl, this.inputEl) } } open(container: HTMLElement, inputEl: HTMLElement): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.app).keymap.pushScope(this.scope); + (this.app).keymap.pushScope(this.scope) - container.appendChild(this.suggestEl); + container.appendChild(this.suggestEl) this.popper = createPopper(inputEl, this.suggestEl, { - placement: "bottom-start", + placement: 'bottom-start', modifiers: [ { - name: "sameWidth", + name: 'sameWidth', enabled: true, fn: ({ state, instance }) => { // Note: positioning needs to be calculated twice - // first pass - positioning it according to the width of the popper // second pass - position it with the width bound to the reference element // we need to early exit to avoid an infinite loop - const targetWidth = `${state.rects.reference.width}px`; + const targetWidth = `${state.rects.reference.width}px` if (state.styles.popper.width === targetWidth) { - return; + return } - state.styles.popper.width = targetWidth; - instance.update(); + state.styles.popper.width = targetWidth + instance.update() }, - phase: "beforeWrite", - requires: ["computeStyles"], + phase: 'beforeWrite', + requires: ['computeStyles'], }, ], - }); + }) } close(): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.app).keymap.popScope(this.scope); + (this.app).keymap.popScope(this.scope) - this.suggest.setSuggestions([]); - this.popper.destroy(); - this.suggestEl.detach(); + this.suggest.setSuggestions([]) + this.popper.destroy() + this.suggestEl.detach() } - abstract getSuggestions(inputStr: string): T[]; - abstract renderSuggestion(item: T, el: HTMLElement): void; - abstract selectSuggestion(item: T): void; + abstract getSuggestions(inputStr: string): T[] + abstract renderSuggestion(item: T, el: HTMLElement): void + abstract selectSuggestion(item: T): void } diff --git a/src/settings/template.ts b/src/settings/template.ts index b3411d0..4b739ac 100644 --- a/src/settings/template.ts +++ b/src/settings/template.ts @@ -1,7 +1,7 @@ -import { truncate } from "lodash"; -import Mustache from "mustache"; -import { parseYaml, stringifyYaml } from "obsidian"; -import { Article, HighlightType, PageType } from "../api"; +import { truncate } from 'lodash' +import Mustache from 'mustache' +import { parseYaml, stringifyYaml } from 'obsidian' +import { Article, HighlightType, PageType } from '../api' import { compareHighlightsInFile, formatDate, @@ -10,14 +10,14 @@ import { removeFrontMatterFromContent, siteNameFromUrl, snakeToCamelCase, -} from "../util"; +} from '../util' type FunctionMap = { [key: string]: () => ( text: string, render: (text: string) => string - ) => string; -}; + ) => string +} export const DEFAULT_TEMPLATE = `# {{{title}}} #Omnivore @@ -36,99 +36,99 @@ export const DEFAULT_TEMPLATE = `# {{{title}}} {{/note}} {{/highlights}} -{{/highlights.length}}`; +{{/highlights.length}}` export interface LabelView { - name: string; + name: string } export interface HighlightView { - text: string; - highlightUrl: string; - highlightID: string; - dateHighlighted: string; - note?: string; - labels?: LabelView[]; - color: string; - positionPercent: number; - positionAnchorIndex: number; + text: string + highlightUrl: string + highlightID: string + dateHighlighted: string + note?: string + labels?: LabelView[] + color: string + positionPercent: number + positionAnchorIndex: number } export type ArticleView = | { - id: string; - title: string; - omnivoreUrl: string; - siteName: string; - originalUrl: string; - author?: string; - labels?: LabelView[]; - dateSaved: string; - highlights: HighlightView[]; - content?: string; - datePublished?: string; - fileAttachment?: string; - description?: string; - note?: string; - type: PageType; - dateRead?: string; - wordsCount?: number; - readLength?: number; - state: string; - dateArchived?: string; + id: string + title: string + omnivoreUrl: string + siteName: string + originalUrl: string + author?: string + labels?: LabelView[] + dateSaved: string + highlights: HighlightView[] + content?: string + datePublished?: string + fileAttachment?: string + description?: string + note?: string + type: PageType + dateRead?: string + wordsCount?: number + readLength?: number + state: string + dateArchived?: string } - | FunctionMap; + | FunctionMap enum ArticleState { - Inbox = "INBOX", - Reading = "READING", - Completed = "COMPLETED", - Archived = "ARCHIVED", + Inbox = 'INBOX', + Reading = 'READING', + Completed = 'COMPLETED', + Archived = 'ARCHIVED', } const getArticleState = (article: Article): string => { if (article.isArchived) { - return ArticleState.Archived; + return ArticleState.Archived } if (article.readingProgressPercent > 0) { return article.readingProgressPercent === 100 ? ArticleState.Completed - : ArticleState.Reading; + : ArticleState.Reading } - return ArticleState.Inbox; -}; + return ArticleState.Inbox +} function lowerCase() { return function (text: string, render: (text: string) => string) { - return render(text).toLowerCase(); - }; + return render(text).toLowerCase() + } } function upperCase() { return function (text: string, render: (text: string) => string) { - return render(text).toUpperCase(); - }; + return render(text).toUpperCase() + } } function upperCaseFirst() { return function (text: string, render: (text: string) => string) { - const str = render(text); - return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); - }; + const str = render(text) + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() + } } function formatDateFunc() { return function (text: string, render: (text: string) => string) { // get the date and format from the text - const [dateVariable, format] = text.split(",", 2); - const date = render(dateVariable); + const [dateVariable, format] = text.split(',', 2) + const date = render(dateVariable) if (!date) { - return ""; + return '' } // format the date - return formatDate(date, format); - }; + return formatDate(date, format) + } } const functionMap: FunctionMap = { @@ -136,38 +136,38 @@ const functionMap: FunctionMap = { upperCase, upperCaseFirst, formatDate: formatDateFunc, -}; +} export const renderFilename = ( article: Article, filename: string, dateFormat: string ) => { - const date = formatDate(article.savedAt, dateFormat); + const date = formatDate(article.savedAt, dateFormat) const datePublished = article.publishedAt ? formatDate(article.publishedAt, dateFormat).trim() - : undefined; + : undefined const renderedFilename = Mustache.render(filename, { title: article.title, - author: article.author ?? "unknown-author", + author: article.author ?? 'unknown-author', date, dateSaved: date, datePublished, id: article.id, - }); + }) // truncate the filename to 100 characters return truncate(renderedFilename, { length: 100, - }); -}; + }) +} export const renderLabels = (labels?: LabelView[]) => { return labels?.map((l) => ({ // replace spaces with underscores because Obsidian doesn't allow spaces in tags - name: l.name.replaceAll(" ", "_"), - })); -}; + name: l.name.replaceAll(' ', '_'), + })) +} export const renderArticleContnet = async ( article: Article, @@ -182,22 +182,22 @@ export const renderArticleContnet = async ( ) => { // filter out notes and redactions const articleHighlights = - article.highlights?.filter((h) => h.type === HighlightType.Highlight) || []; + article.highlights?.filter((h) => h.type === HighlightType.Highlight) || [] // sort highlights by location if selected in options - if (highlightOrder === "LOCATION") { + if (highlightOrder === 'LOCATION') { articleHighlights.sort((a, b) => { try { if (article.pageType === PageType.File) { // sort by location in file - return compareHighlightsInFile(a, b); + return compareHighlightsInFile(a, b) } // for web page, sort by location in the page - return getHighlightLocation(a.patch) - getHighlightLocation(b.patch); + return getHighlightLocation(a.patch) - getHighlightLocation(b.patch) } catch (e) { - console.error(e); - return compareHighlightsInFile(a, b); + console.error(e) + return compareHighlightsInFile(a, b) } - }); + }) } const highlights: HighlightView[] = articleHighlights.map((highlight) => { return { @@ -210,25 +210,25 @@ export const renderArticleContnet = async ( color: highlight.color ?? 'yellow', positionPercent: highlight.highlightPositionPercent, positionAnchorIndex: highlight.highlightPositionAnchorIndex + 1, // PDF page numbers start at 1 - }; - }); - const dateSaved = formatDate(article.savedAt, dateSavedFormat); + } + }) + const dateSaved = formatDate(article.savedAt, dateSavedFormat) const siteName = - article.siteName || siteNameFromUrl(article.originalArticleUrl); - const publishedAt = article.publishedAt; + article.siteName || siteNameFromUrl(article.originalArticleUrl) + const publishedAt = article.publishedAt const datePublished = publishedAt ? formatDate(publishedAt, dateSavedFormat).trim() - : undefined; + : undefined const articleNote = article.highlights?.find( (h) => h.type === HighlightType.Note - ); + ) const dateRead = article.readAt ? formatDate(article.readAt, dateSavedFormat).trim() - : undefined; - const wordsCount = article.wordsCount; + : undefined + const wordsCount = article.wordsCount const readLength = wordsCount ? Math.round(Math.max(1, wordsCount / 235)) - : undefined; + : undefined const articleView: ArticleView = { id: article.id, title: article.title, @@ -251,101 +251,101 @@ export const renderArticleContnet = async ( state: getArticleState(article), dateArchived: article.archivedAt, ...functionMap, - }; + } let frontMatter: { [id: string]: unknown } = { id: article.id, // id is required for deduplication - }; + } // if the front matter template is set, use it if (frontMatterTemplate) { const frontMatterTemplateRendered = Mustache.render( frontMatterTemplate, articleView - ); + ) try { // parse the front matter template as yaml - const frontMatterParsed = parseYaml(frontMatterTemplateRendered); + const frontMatterParsed = parseYaml(frontMatterTemplateRendered) frontMatter = { ...frontMatterParsed, ...frontMatter, - }; + } } catch (error) { // if there's an error parsing the front matter template, log it - console.error("Error parsing front matter template", error); + console.error('Error parsing front matter template', error) // and add the error to the front matter frontMatter = { ...frontMatter, omnivore_error: - "There was an error parsing the front matter template. See console for details.", - }; + 'There was an error parsing the front matter template. See console for details.', + } } } else { // otherwise, use the front matter variables for (const item of frontMatterVariables) { // split the item into variable and alias - const aliasedVariables = item.split("::"); - const variable = aliasedVariables[0]; + const aliasedVariables = item.split('::') + const variable = aliasedVariables[0] // we use snake case for variables in the front matter - const articleVariable = snakeToCamelCase(variable); + const articleVariable = snakeToCamelCase(variable) // use alias if available, otherwise use variable - const key = aliasedVariables[1] || variable; + const key = aliasedVariables[1] || variable if ( - variable === "tags" && + variable === 'tags' && articleView.labels && articleView.labels.length > 0 ) { // tags are handled separately // use label names as tags - frontMatter[key] = articleView.labels.map((l) => l.name); - continue; + frontMatter[key] = articleView.labels.map((l) => l.name) + continue } - const value = (articleView as any)[articleVariable]; + const value = (articleView as any)[articleVariable] if (value) { // if variable is in article, use it - frontMatter[key] = value; + frontMatter[key] = value } } } // Build content string based on template - const content = Mustache.render(template, articleView); - let contentWithoutFrontMatter = removeFrontMatterFromContent(content); - let frontMatterYaml = stringifyYaml(frontMatter); + const content = Mustache.render(template, articleView) + let contentWithoutFrontMatter = removeFrontMatterFromContent(content) + let frontMatterYaml = stringifyYaml(frontMatter) if (isSingleFile) { // wrap the content without front matter in comments - const sectionStart = `%%${article.id}_start%%`; - const sectionEnd = `%%${article.id}_end%%`; - contentWithoutFrontMatter = `${sectionStart}\n${contentWithoutFrontMatter}\n${sectionEnd}`; + const sectionStart = `%%${article.id}_start%%` + const sectionEnd = `%%${article.id}_end%%` + contentWithoutFrontMatter = `${sectionStart}\n${contentWithoutFrontMatter}\n${sectionEnd}` // if single file, wrap the front matter in an array - frontMatterYaml = stringifyYaml([frontMatter]); + frontMatterYaml = stringifyYaml([frontMatter]) } - const frontMatterStr = `---\n${frontMatterYaml}---`; + const frontMatterStr = `---\n${frontMatterYaml}---` - return `${frontMatterStr}\n\n${contentWithoutFrontMatter}`; -}; + return `${frontMatterStr}\n\n${contentWithoutFrontMatter}` +} export const renderFolderName = ( article: Article, template: string, dateFormat: string ) => { - const date = formatDate(article.savedAt, dateFormat); + const date = formatDate(article.savedAt, dateFormat) const datePublished = article.publishedAt ? formatDate(article.publishedAt, dateFormat).trim() - : undefined; + : undefined return Mustache.render(template, { date, dateSaved: date, datePublished, - author: article.author ?? "unknown-author", - }); -}; + author: article.author ?? 'unknown-author', + }) +} export const preParseTemplate = (template: string) => { - return Mustache.parse(template); -}; + return Mustache.parse(template) +} diff --git a/src/util.ts b/src/util.ts index 6ce8e2b..3e2e7b2 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,17 +1,17 @@ -import { diff_match_patch } from "diff-match-patch"; -import { DateTime } from "luxon"; -import escape from "markdown-escape"; -import { parseYaml } from "obsidian"; -import outOfCharacter from "out-of-character"; -import { Highlight } from "./api"; - -export const DATE_FORMAT_W_OUT_SECONDS = "yyyy-MM-dd'T'HH:mm"; -export const DATE_FORMAT = `${DATE_FORMAT_W_OUT_SECONDS}:ss`; -export const REPLACEMENT_CHAR = "-"; +import { diff_match_patch } from "diff-match-patch" +import { DateTime } from "luxon" +import escape from "markdown-escape" +import { parseYaml } from "obsidian" +import outOfCharacter from "out-of-character" +import { Highlight } from "./api" + +export const DATE_FORMAT_W_OUT_SECONDS = "yyyy-MM-dd'T'HH:mm" +export const DATE_FORMAT = `${DATE_FORMAT_W_OUT_SECONDS}:ss` +export const REPLACEMENT_CHAR = "-" // On Unix-like systems / is reserved and <>:"/\|?* as well as non-printable characters \u0000-\u001F on Windows // credit: https://github.com/sindresorhus/filename-reserved-regex // eslint-disable-next-line no-control-regex -export const ILLEGAL_CHAR_REGEX = /[<>:"/\\|?*\u0000-\u001F]/g; +export const ILLEGAL_CHAR_REGEX = /[<>:"/\\|?*\u0000-\u001F]/g export interface HighlightPoint { left: number; @@ -20,60 +20,60 @@ export interface HighlightPoint { export const getHighlightLocation = (patch: string | null): number => { if (!patch) { - return 0; + return 0 } - const dmp = new diff_match_patch(); - const patches = dmp.patch_fromText(patch); - return patches[0].start1 || 0; -}; + const dmp = new diff_match_patch() + const patches = dmp.patch_fromText(patch) + return patches[0].start1 || 0 +} export const getHighlightPoint = (patch: string | null): HighlightPoint => { if (!patch) { - return { left: 0, top: 0 }; + return { left: 0, top: 0 } } - const { bbox } = JSON.parse(patch) as { bbox: number[] }; + const { bbox } = JSON.parse(patch) as { bbox: number[] } if (!bbox || bbox.length !== 4) { - return { left: 0, top: 0 }; + return { left: 0, top: 0 } } - return { left: bbox[0], top: bbox[1] }; -}; + return { left: bbox[0], top: bbox[1] } +} export const compareHighlightsInFile = (a: Highlight, b: Highlight): number => { // get the position of the highlight in the file - const highlightPointA = getHighlightPoint(a.patch); - const highlightPointB = getHighlightPoint(b.patch); + const highlightPointA = getHighlightPoint(a.patch) + const highlightPointB = getHighlightPoint(b.patch) if (highlightPointA.top === highlightPointB.top) { // if top is same, sort by left - return highlightPointA.left - highlightPointB.left; + return highlightPointA.left - highlightPointB.left } // sort by top - return highlightPointA.top - highlightPointB.top; -}; + return highlightPointA.top - highlightPointB.top +} export const markdownEscape = (text: string): string => { try { - return escape(text); + return escape(text) } catch (e) { - console.error("markdownEscape error", e); - return text; + console.error("markdownEscape error", e) + return text } -}; +} export const escapeQuotationMarks = (text: string): string => { - return text.replace(/"/g, '\\"'); -}; + return text.replace(/"/g, '\\"') +} export const parseDateTime = (str: string): DateTime => { - const res = DateTime.fromFormat(str, DATE_FORMAT); + const res = DateTime.fromFormat(str, DATE_FORMAT) if (res.isValid) { - return res; + return res } - return DateTime.fromFormat(str, DATE_FORMAT_W_OUT_SECONDS); -}; + return DateTime.fromFormat(str, DATE_FORMAT_W_OUT_SECONDS) +} export const wrapAround = (value: number, size: number): number => { - return ((value % size) + size) % size; -}; + return ((value % size) + size) % size +} export const unicodeSlug = (str: string, savedAt: string) => { return ( @@ -94,20 +94,20 @@ export const unicodeSlug = (str: string, savedAt: string) => { .substring(0, 64) + "-" + new Date(savedAt).getTime().toString(16) - ); -}; + ) +} export const replaceIllegalChars = (str: string): string => { return removeInvisibleChars( str.replace(ILLEGAL_CHAR_REGEX, REPLACEMENT_CHAR) - ); -}; + ) +} export function formatDate(date: string, format: string): string { if (isNaN(Date.parse(date))) { - throw new Error(`Invalid date: ${date}`); + throw new Error(`Invalid date: ${date}`) } - return DateTime.fromJSDate(new Date(date)).toFormat(format); + return DateTime.fromJSDate(new Date(date)).toFormat(format) } export const getQueryFromFilter = ( @@ -116,68 +116,68 @@ export const getQueryFromFilter = ( ): string => { switch (filter) { case "ALL": - return ""; + return "" case "HIGHLIGHTS": - return `has:highlights`; + return `has:highlights` case "ADVANCED": - return customQuery; + return customQuery default: - return ""; + return "" } -}; +} export const siteNameFromUrl = (originalArticleUrl: string): string => { try { - return new URL(originalArticleUrl).hostname.replace(/^www\./, ""); + return new URL(originalArticleUrl).hostname.replace(/^www\./, "") } catch { - return ""; + return "" } -}; +} export const formatHighlightQuote = ( quote: string | null, template: string ): string => { if (!quote) { - return ""; + return "" } // if the template has highlights, we need to preserve paragraphs - const regex = /{{#highlights}}(\n)*>/gm; + const regex = /{{#highlights}}(\n)*>/gm if (regex.test(template)) { // replace all empty lines with blockquote '>' to preserve paragraphs - quote = quote.replaceAll(">", ">").replaceAll(/\n/gm, "\n> "); + quote = quote.replaceAll(">", ">").replaceAll(/\n/gm, "\n> ") } - return quote; -}; + return quote +} export const findFrontMatterIndex = ( frontMatter: any[], id: string ): number => { // find index of front matter with matching id - return frontMatter.findIndex((fm) => fm.id == id); -}; + return frontMatter.findIndex((fm) => fm.id == id) +} export const parseFrontMatterFromContent = (content: string) => { // get front matter yaml from content - const frontMatter = content.match(/^---\n(.*?)\n---/s); + const frontMatter = content.match(/^---\n(.*?)\n---/s) if (!frontMatter) { - return undefined; + return undefined } // parse yaml - return parseYaml(frontMatter[1]); -}; + return parseYaml(frontMatter[1]) +} export const removeFrontMatterFromContent = (content: string): string => { - const frontMatterRegex = /^---.*?---\n*/s; + const frontMatterRegex = /^---.*?---\n*/s - return content.replace(frontMatterRegex, ""); -}; + return content.replace(frontMatterRegex, "") +} export const snakeToCamelCase = (str: string) => - str.replace(/(_[a-z])/g, (group) => group.toUpperCase().replace("_", "")); + str.replace(/(_[a-z])/g, (group) => group.toUpperCase().replace("_", "")) const removeInvisibleChars = (str: string): string => { - return outOfCharacter.replace(str); -}; + return outOfCharacter.replace(str) +}