diff --git a/.eslintrc b/.eslintrc index 734c3f3..bab550a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,11 +5,11 @@ "plugins": ["@typescript-eslint"], "extends": [ "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" ], "parserOptions": { - "sourceType": "module" + "project": "./tsconfig.json" }, "rules": { "no-unused-vars": "off", diff --git a/package.json b/package.json index 8497187..fea74bc 100644 --- a/package.json +++ b/package.json @@ -37,11 +37,13 @@ "builtin-modules": "3.3.0", "esbuild": "0.14.47", "eslint": "^8.53.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "jest": "^29.4.3", "jest-cli": "^29.4.3", "jest-junit-reporter": "^1.1.0", "obsidian": "^1.2.8", - "prettier": "^2.8.1", + "prettier": "^3.2.5", "semantic-release": "^19.0.5", "ts-jest": "^29.0.5", "tslib": "2.4.0", diff --git a/src/__tests__/formatDate.spec.ts b/src/__tests__/formatDate.spec.ts index 22525aa..10e6978 100644 --- a/src/__tests__/formatDate.spec.ts +++ b/src/__tests__/formatDate.spec.ts @@ -75,8 +75,8 @@ function generateRandomISODateStrings(quantity: number): string[] { Math.floor(Math.random() * 24), Math.floor(Math.random() * 60), Math.floor(Math.random() * 60), - Math.floor(Math.random() * 1000) - ) + Math.floor(Math.random() * 1000), + ), ) // Randomly select a timezone from the available time zones @@ -86,7 +86,7 @@ function generateRandomISODateStrings(quantity: number): string[] { // 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 }) + date.toLocaleString('en-US', { timeZone: randomTimeZone }), ) const luxonDate = DateTime.fromJSDate(jsDateTimeWithZone) randomISODateStrings.push(luxonDate.toISO() as string) @@ -102,13 +102,13 @@ describe('formatDate on random dates', () => { 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}$/) - } + }, ) }) function getCasesWithRandomDates( testFormats: string[], - quantity = 10 + quantity = 10, ): { date: string luxonFormat: string @@ -117,7 +117,7 @@ function getCasesWithRandomDates( generateRandomISODateStrings(quantity).map((date) => ({ date, luxonFormat, - })) + })), ) } @@ -135,7 +135,7 @@ describe('round trip on random dates', () => { const result = formatDate(testCase.date, testCase.luxonFormat) const result2 = formatDate(result, testCase.luxonFormat) expect(result2).toBe(result) - } + }, ) const atypicalFormats = [ @@ -148,9 +148,9 @@ describe('round trip on random dates', () => { const formattedDate = formatDate(testCase.date, testCase.luxonFormat) const parsedDate = DateTime.fromFormat( formattedDate, - testCase.luxonFormat + testCase.luxonFormat, ) expect(parsedDate.isValid).toBe(true) - } + }, ) }) diff --git a/src/__tests__/path_validation.spec.ts b/src/__tests__/path_validation.spec.ts index 1454553..d5a3067 100644 --- a/src/__tests__/path_validation.spec.ts +++ b/src/__tests__/path_validation.spec.ts @@ -29,7 +29,7 @@ describe('replaceIllegalChars() removes all expected characters', () => { const input = `this${character}string` const output = replaceIllegalChars(input) expect(output).not.toContain(character) - } + }, ) }) @@ -41,7 +41,7 @@ describe('replaceIllegalChars() function replaces illegal characters with replac const expectedOutput = `this${REPLACEMENT_CHAR}string` const output = replaceIllegalChars(input) expect(output).toEqual(expectedOutput) - } + }, ) }) @@ -51,7 +51,7 @@ describe('replaceIllegalChars() function does not modify string without illegal (input) => { const output = replaceIllegalChars(input) expect(output).toEqual(input) - } + }, ) }) @@ -72,14 +72,14 @@ describe('replaceIllegalChars() function replaces all occurrences of illegal cha 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', () => { const nonAlphanumericCharactersWithoutIllegal: string[] = Array.from( { length: 127 - 32 }, - (_, i) => String.fromCharCode(i + 32) + (_, i) => String.fromCharCode(i + 32), ) .filter((char) => !/^[a-zA-Z0-9]+$/.test(char)) .map(replaceIllegalChars) @@ -97,7 +97,7 @@ describe('file system behavior with non-alphanumeric characters not in the illeg fs.unlinkSync(input) // verify the file has been deleted expect(fs.existsSync(input)).toBe(false) - } + }, ) }) @@ -110,6 +110,6 @@ describe('replaceIllegalChars() function removes all occurrences of invisible ch 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 9669ed6..894b938 100644 --- a/src/api.ts +++ b/src/api.ts @@ -95,7 +95,7 @@ export const loadArticles = async ( updatedAt = '', query = '', includeContent = false, - format = 'html' + format = 'html', ): Promise<[Article[], boolean]> => { const res = await requestUrl({ url: endpoint, @@ -177,7 +177,7 @@ export const loadArticles = async ( export const deleteArticleById = async ( endpoint: string, apiKey: string, - articleId: string + articleId: string, ) => { const res = await requestUrl({ url: endpoint, diff --git a/src/main.ts b/src/main.ts index 4ca6b50..73f290e 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,16 +11,16 @@ 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, render, @@ -49,7 +49,7 @@ export default class OmnivorePlugin extends Plugin { const currentVersion = this.settings.version if (latestVersion !== currentVersion) { this.settings.version = latestVersion - this.saveSettings() + await this.saveSettings() // show release notes const releaseNotes = `Omnivore plugin is upgraded to ${latestVersion}. @@ -61,27 +61,27 @@ export default class OmnivorePlugin extends Plugin { this.addCommand({ id: 'sync', name: 'Sync new changes', - callback: () => { - this.fetchOmnivore() + callback: async () => { + await this.fetchOmnivore() }, }) this.addCommand({ id: 'deleteArticle', name: 'Delete Current Article from Omnivore', - callback: () => { - this.deleteCurrentArticle(this.app.workspace.getActiveFile()) + callback: async () => { + await this.deleteCurrentArticle(this.app.workspace.getActiveFile()) }, }) this.addCommand({ id: 'resync', name: 'Resync all articles', - callback: () => { + callback: async () => { this.settings.syncAt = '' - this.saveSettings() + await this.saveSettings() new Notice('Omnivore Last Sync reset') - this.fetchOmnivore() + await this.fetchOmnivore() }, }) @@ -89,7 +89,7 @@ export default class OmnivorePlugin extends Plugin { // add icon addIcon( iconId, - `` + ``, ) // This creates an icon in the left ribbon. @@ -119,7 +119,7 @@ export default class OmnivorePlugin extends Plugin { await this.saveData(this.settings) } - scheduleSync() { + async scheduleSync() { // clear previous interval if (this.settings.intervalId > 0) { window.clearInterval(this.settings.intervalId) @@ -127,12 +127,15 @@ export default class OmnivorePlugin extends Plugin { const frequency = this.settings.frequency if (frequency > 0) { // schedule new interval - const intervalId = window.setInterval(async () => { - await this.fetchOmnivore(false) - }, frequency * 60 * 1000) + const intervalId = window.setInterval( + async () => { + await this.fetchOmnivore(false) + }, + frequency * 60 * 1000, + ) // save new interval id this.settings.intervalId = intervalId - this.saveSettings() + await this.saveSettings() // clear interval when plugin is unloaded this.registerInterval(intervalId) } @@ -149,8 +152,8 @@ export default class OmnivorePlugin extends Plugin { render( article, this.settings.attachmentFolder, - this.settings.folderDateFormat - ) + this.settings.folderDateFormat, + ), ) const folder = app.vault.getAbstractFileByPath(folderName) if (!(folder instanceof TFolder)) { @@ -161,7 +164,7 @@ export default class OmnivorePlugin extends Plugin { if (!(file instanceof TFile)) { const newFile = await app.vault.createBinary( fileName, - response.arrayBuffer + response.arrayBuffer, ) return newFile.path } @@ -207,10 +210,10 @@ export default class OmnivorePlugin extends Plugin { const templateSpans = preParseTemplate(template) // check if we need to include content or file attachment const includeContent = templateSpans.some( - (templateSpan) => templateSpan[1] === 'content' + (templateSpan) => templateSpan[1] === 'content', ) const includeFileAttachment = templateSpans.some( - (templateSpan) => templateSpan[1] === 'fileAttachment' + (templateSpan) => templateSpan[1] === 'fileAttachment', ) const size = 50 @@ -227,12 +230,12 @@ export default class OmnivorePlugin extends Plugin { parseDateTime(syncAt).toISO() || undefined, getQueryFromFilter(filter, customQuery), includeContent, - 'highlightedMarkdown' + 'highlightedMarkdown', ) for (const article of articles) { const folderName = normalizePath( - render(article, folder, this.settings.folderDateFormat) + render(article, folder, this.settings.folderDateFormat), ) const omnivoreFolder = this.app.vault.getAbstractFileByPath(folderName) @@ -252,11 +255,11 @@ export default class OmnivorePlugin extends Plugin { isSingleFile, frontMatterVariables, frontMatterTemplate, - fileAttachment + fileAttachment, ) // use the custom filename const customFilename = replaceIllegalChars( - renderFilename(article, filename, this.settings.filenameDateFormat) + renderFilename(article, filename, this.settings.filenameDateFormat), ) const pageName = `${folderName}/${customFilename}.md` const normalizedPath = normalizePath(pageName) @@ -292,7 +295,7 @@ export default class OmnivorePlugin extends Plugin { // find the front matter with the same id const frontMatterIdx = findFrontMatterIndex( existingFrontMatter, - article.id + article.id, ) if (frontMatterIdx >= 0) { // this article already exists in the file @@ -302,12 +305,12 @@ export default class OmnivorePlugin extends Plugin { const sectionEnd = `%%${article.id}_end%%` const existingContentRegex = new RegExp( `${sectionStart}.*?${sectionEnd}`, - 's' + 's', ) newContentWithoutFrontMatter = existingContentWithoutFrontmatter.replace( existingContentRegex, - contentWithoutFrontmatter + contentWithoutFrontmatter, ) existingFrontMatter[frontMatterIdx] = newFrontMatter[0] @@ -320,12 +323,12 @@ export default class OmnivorePlugin extends Plugin { } const newFrontMatterStr = `---\n${stringifyYaml( - existingFrontMatter + existingFrontMatter, )}---` await this.app.vault.modify( omnivoreFile, - `${newFrontMatterStr}\n\n${newContentWithoutFrontMatter}` + `${newFrontMatterStr}\n\n${newContentWithoutFrontMatter}`, ) continue } @@ -342,9 +345,8 @@ export default class OmnivorePlugin extends Plugin { 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 - ) + const existingContent = + await this.app.vault.read(newOmnivoreFile) if (existingContent !== content) { await this.app.vault.modify(newOmnivoreFile, content) } @@ -359,7 +361,7 @@ export default class OmnivorePlugin extends Plugin { if (existingContent !== content) { await this.app.vault.modify(omnivoreFile, content) } - } + }, ) continue } @@ -369,7 +371,7 @@ export default class OmnivorePlugin extends Plugin { } 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.` + `Skipping file creation: ${normalizedPath}. Please check if you have duplicated article titles and delete the file if needed.`, ) } else { throw error @@ -403,7 +405,7 @@ export default class OmnivorePlugin extends Plugin { const isDeleted = deleteArticleById( this.settings.endpoint, this.settings.apiKey, - articleId + articleId, ) if (!isDeleted) { new Notice('Failed to delete article in Omnivore') @@ -436,90 +438,94 @@ class OmnivoreSettingTab extends PluginSettingTab { containerEl.empty() - containerEl.createEl("h2", { text: "Settings for Omnivore plugin" }) + containerEl.createEl('h2', { text: 'Settings for Omnivore plugin' }) new Setting(containerEl) - .setName("API Key") + .setName('API Key') .setDesc( createFragment((fragment) => { fragment.append( - "You can create an API key at ", - fragment.createEl("a", { - text: "https://omnivore.app/settings/api", - href: "https://omnivore.app/settings/api", - }) + 'You can create an API key at ', + fragment.createEl('a', { + text: 'https://omnivore.app/settings/api', + href: 'https://omnivore.app/settings/api', + }), ) - }) + }), ) .addText((text) => text - .setPlaceholder("Enter your Omnivore Api Key") + .setPlaceholder('Enter your Omnivore Api Key') .setValue(this.plugin.settings.apiKey) .onChange(async (value) => { this.plugin.settings.apiKey = value await this.plugin.saveSettings() - }) + }), ) new Setting(containerEl) - .setName("Filter") - .setDesc("Select an Omnivore search filter type. Changing this would reset the 'Last sync' timestamp") + .setName('Filter') + .setDesc( + "Select an Omnivore search filter type. Changing this would reset the 'Last sync' timestamp", + ) .addDropdown((dropdown) => { dropdown.addOptions(Filter) dropdown .setValue(this.plugin.settings.filter) .onChange(async (value) => { this.plugin.settings.filter = value - this.plugin.settings.syncAt = "" + this.plugin.settings.syncAt = '' await this.plugin.saveSettings() }) }) new Setting(containerEl) - .setName("Custom query") + .setName('Custom query') .setDesc( createFragment((fragment) => { fragment.append( - "See ", - fragment.createEl("a", { - text: "https://docs.omnivore.app/using/search", - href: "https://docs.omnivore.app/using/search", + 'See ', + fragment.createEl('a', { + text: 'https://docs.omnivore.app/using/search', + 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.", - " Changing this would reset the 'Last Sync' timestamp" + ' for more info on search query syntax. Make sure your Filter (in the section above) is set to advanced when using a custom query.', + " Changing this would reset the 'Last Sync' timestamp", ) - }) + }), ) .addText((text) => text .setPlaceholder( - "Enter an Omnivore custom search query if advanced filter is selected" + 'Enter an Omnivore custom search query if advanced filter is selected', ) .setValue(this.plugin.settings.customQuery) .onChange(async (value) => { this.plugin.settings.customQuery = value - this.plugin.settings.syncAt = "" + this.plugin.settings.syncAt = '' await this.plugin.saveSettings() - }) + }), ) new Setting(containerEl) - .setName("Last Sync") - .setDesc("Last time the plugin synced with Omnivore. The 'Sync' command fetches articles updated after this timestamp") + .setName('Last Sync') + .setDesc( + "Last time the plugin synced with Omnivore. The 'Sync' command fetches articles updated after this timestamp", + ) .addMomentFormat((momentFormat) => momentFormat - .setPlaceholder("Last Sync") + .setPlaceholder('Last Sync') .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() - }) + }), ) new Setting(containerEl) - .setName("Highlight Order") - .setDesc("Select the order in which highlights are applied") + .setName('Highlight Order') + .setDesc('Select the order in which highlights are applied') .addDropdown((dropdown) => { dropdown.addOptions(HighlightOrder) dropdown @@ -531,63 +537,63 @@ class OmnivoreSettingTab extends PluginSettingTab { }) new Setting(containerEl) - .setName("Front Matter") + .setName('Front Matter') .setDesc( createFragment((fragment) => { fragment.append( - "Enter the metadata to be used in your note separated by commas. You can also use custom aliases in the format of metatdata::alias, e.g. date_saved::date. ", - fragment.createEl("br"), - fragment.createEl("br"), - "Available metadata can be found at ", - fragment.createEl("a", { - text: "Reference", - href: "https://docs.omnivore.app/integrations/obsidian.html#front-matter", + 'Enter the metadata to be used in your note separated by commas. You can also use custom aliases in the format of metatdata::alias, e.g. date_saved::date. ', + fragment.createEl('br'), + fragment.createEl('br'), + 'Available metadata can be found at ', + fragment.createEl('a', { + text: 'Reference', + href: 'https://docs.omnivore.app/integrations/obsidian.html#front-matter', }), - 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" + 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) => { text - .setPlaceholder("Enter the metadata") - .setValue(this.plugin.settings.frontMatterVariables.join(",")) + .setPlaceholder('Enter the metadata') + .setValue(this.plugin.settings.frontMatterVariables.join(',')) .onChange(async (value) => { // validate front matter variables and deduplicate this.plugin.settings.frontMatterVariables = value - .split(",") + .split(',') .map((v) => v.trim()) .filter( (v, i, a) => - FRONT_MATTER_VARIABLES.includes(v.split("::")[0]) && - a.indexOf(v) === i + 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) + text.inputEl.setAttr('rows', 4) + text.inputEl.setAttr('cols', 30) }) new Setting(containerEl) - .setName("Article Template") + .setName('Article Template') .setDesc( createFragment((fragment) => { fragment.append( - "Enter template to render articles with ", - fragment.createEl("a", { - text: "Reference", - href: "https://docs.omnivore.app/integrations/obsidian.html#controlling-the-layout-of-the-data-imported-to-obsidian", + 'Enter template to render articles with ', + fragment.createEl('a', { + text: 'Reference', + href: 'https://docs.omnivore.app/integrations/obsidian.html#controlling-the-layout-of-the-data-imported-to-obsidian', }), - 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" + 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) => { text - .setPlaceholder("Enter the template") + .setPlaceholder('Enter the template') .setValue(this.plugin.settings.template) .onChange(async (value) => { // if template is empty, use default template @@ -596,36 +602,36 @@ class OmnivoreSettingTab extends PluginSettingTab { : DEFAULT_SETTINGS.template await this.plugin.saveSettings() }) - text.inputEl.setAttr("rows", 25) - text.inputEl.setAttr("cols", 50) + text.inputEl.setAttr('rows', 25) + text.inputEl.setAttr('cols', 50) }) .addExtraButton((button) => { // add a button to reset template button - .setIcon("reset") - .setTooltip("Reset template") + .setIcon('reset') + .setTooltip('Reset template') .onClick(async () => { this.plugin.settings.template = DEFAULT_SETTINGS.template await this.plugin.saveSettings() this.display() - new Notice("Template reset") + new Notice('Template reset') }) }) new Setting(containerEl) - .setName("Frequency") + .setName('Frequency') .setDesc( - "Enter the frequency in minutes to sync with Omnivore automatically. 0 means manual sync" + 'Enter the frequency in minutes to sync with Omnivore automatically. 0 means manual sync', ) .addText((text) => text - .setPlaceholder("Enter the frequency") + .setPlaceholder('Enter the frequency') .setValue(this.plugin.settings.frequency.toString()) .onChange(async (value) => { // validate frequency const frequency = parseInt(value) if (isNaN(frequency)) { - new Notice("Frequency must be a positive integer") + new Notice('Frequency must be a positive integer') return } // save frequency @@ -633,18 +639,18 @@ class OmnivoreSettingTab extends PluginSettingTab { await this.plugin.saveSettings() this.plugin.scheduleSync() - }) + }), ) new Setting(containerEl) - .setName("Folder") + .setName('Folder') .setDesc( - "Enter the folder where the data will be stored. {{{title}}}, {{{dateSaved}}} and {{{datePublished}}} could be used in the folder name" + '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) search - .setPlaceholder("Enter the folder") + .setPlaceholder('Enter the folder') .setValue(this.plugin.settings.folder) .onChange(async (value) => { this.plugin.settings.folder = value @@ -652,14 +658,14 @@ class OmnivoreSettingTab extends PluginSettingTab { }) }) new Setting(containerEl) - .setName("Attachment Folder") + .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" + '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) search - .setPlaceholder("Enter the attachment folder") + .setPlaceholder('Enter the attachment folder') .setValue(this.plugin.settings.attachmentFolder) .onChange(async (value) => { this.plugin.settings.attachmentFolder = value @@ -668,9 +674,9 @@ class OmnivoreSettingTab extends PluginSettingTab { }) new Setting(containerEl) - .setName("Is Single File") + .setName('Is Single File') .setDesc( - "Check this box if you want to save all articles in a single file" + 'Check this box if you want to save all articles in a single file', ) .addToggle((toggle) => toggle @@ -678,73 +684,73 @@ class OmnivoreSettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.isSingleFile = value await this.plugin.saveSettings() - }) + }), ) new Setting(containerEl) - .setName("Filename") + .setName('Filename') .setDesc( - "Enter the filename where the data will be stored. {{id}}, {{{title}}}, {{{dateSaved}}} and {{{datePublished}}} could be used in the filename" + 'Enter the filename where the data will be stored. {{id}}, {{{title}}}, {{{dateSaved}}} and {{{datePublished}}} could be used in the filename', ) .addText((text) => text - .setPlaceholder("Enter the filename") + .setPlaceholder('Enter the filename') .setValue(this.plugin.settings.filename) .onChange(async (value) => { this.plugin.settings.filename = value await this.plugin.saveSettings() - }) + }), ) new Setting(containerEl) - .setName("Filename Date Format") + .setName('Filename Date Format') .setDesc( createFragment((fragment) => { fragment.append( - "Enter the format date for use in rendered filename. Format ", - fragment.createEl("a", { - text: "reference", - href: "https://moment.github.io/luxon/#/formatting?id=table-of-tokens", - }) + 'Enter the format date for use in rendered filename. Format ', + fragment.createEl('a', { + text: 'reference', + href: 'https://moment.github.io/luxon/#/formatting?id=table-of-tokens', + }), ) - }) + }), ) .addText((text) => text - .setPlaceholder("yyyy-MM-dd") + .setPlaceholder('yyyy-MM-dd') .setValue(this.plugin.settings.filenameDateFormat) .onChange(async (value) => { this.plugin.settings.filenameDateFormat = value await this.plugin.saveSettings() - }) + }), ) new Setting(containerEl) - .setName("Folder Date Format") + .setName('Folder Date Format') .setDesc( createFragment((fragment) => { fragment.append( - "Enter the format date for use in rendered folder name. Format ", - fragment.createEl("a", { - text: "reference", - href: "https://moment.github.io/luxon/#/formatting?id=table-of-tokens", - }) + 'Enter the format date for use in rendered folder name. Format ', + fragment.createEl('a', { + text: 'reference', + href: 'https://moment.github.io/luxon/#/formatting?id=table-of-tokens', + }), ) - }) + }), ) .addText((text) => text - .setPlaceholder("yyyy-MM-dd") + .setPlaceholder('yyyy-MM-dd') .setValue(this.plugin.settings.folderDateFormat) .onChange(async (value) => { this.plugin.settings.folderDateFormat = value await this.plugin.saveSettings() - }) + }), ) new Setting(containerEl) - .setName("Date Saved Format") + .setName('Date Saved Format') .setDesc( - "Enter the date format for dateSaved variable in rendered template" + 'Enter the date format for dateSaved variable in rendered template', ) .addText((text) => text @@ -753,105 +759,105 @@ class OmnivoreSettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.dateSavedFormat = value await this.plugin.saveSettings() - }) + }), ) new Setting(containerEl) - .setName("Date Highlighted Format") + .setName('Date Highlighted Format') .setDesc( - "Enter the date format for dateHighlighted variable in rendered template" + 'Enter the date format for dateHighlighted variable in rendered template', ) .addText((text) => text - .setPlaceholder("Date Highlighted Format") + .setPlaceholder('Date Highlighted Format') .setValue(this.plugin.settings.dateHighlightedFormat) .onChange(async (value) => { this.plugin.settings.dateHighlightedFormat = value await this.plugin.saveSettings() - }) + }), ) - containerEl.createEl("h5", { - cls: "omnivore-collapsible", - text: "Advanced Settings", + containerEl.createEl('h5', { + cls: 'omnivore-collapsible', + text: 'Advanced Settings', }) - const advancedSettings = containerEl.createEl("div", { - cls: "omnivore-content", + const advancedSettings = containerEl.createEl('div', { + cls: 'omnivore-content', }) new Setting(advancedSettings) - .setName("API Endpoint") + .setName('API Endpoint') .setDesc("Enter the Omnivore server's API endpoint") .addText((text) => text - .setPlaceholder("API endpoint") + .setPlaceholder('API endpoint') .setValue(this.plugin.settings.endpoint) .onChange(async (value) => { this.plugin.settings.endpoint = value await this.plugin.saveSettings() - }) + }), ) new Setting(advancedSettings) - .setName("Front Matter Template") + .setName('Front Matter Template') .setDesc( createFragment((fragment) => { fragment.append( - "Enter YAML template to render the front matter with ", - fragment.createEl("a", { - text: "Reference", - href: "https://docs.omnivore.app/integrations/obsidian.html#front-matter-template", + 'Enter YAML template to render the front matter with ', + fragment.createEl('a', { + text: 'Reference', + href: 'https://docs.omnivore.app/integrations/obsidian.html#front-matter-template', }), - fragment.createEl("br"), - fragment.createEl("br"), - "We recommend you to use Front Matter section under the basic settings to define the metadata.", - 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." + fragment.createEl('br'), + fragment.createEl('br'), + 'We recommend you to use Front Matter section under the basic settings to define the metadata.', + 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) => { text - .setPlaceholder("Enter the template") + .setPlaceholder('Enter the template') .setValue(this.plugin.settings.frontMatterTemplate) .onChange(async (value) => { 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 button - .setIcon("reset") - .setTooltip("Reset front matter template") + .setIcon('reset') + .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") + new Notice('Front matter template reset') }) }) - const help = containerEl.createEl("p") + 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") + 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") + coll[i].addEventListener('click', function () { + this.classList.toggle('omnivore-active') const content = this.nextElementSibling if (content.style.maxHeight) { content.style.maxHeight = null } else { - content.style.maxHeight = "fit-content" + content.style.maxHeight = 'fit-content' } }) } diff --git a/src/settings/index.ts b/src/settings/index.ts index 773e7a5..f077628 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -17,7 +17,7 @@ export const FRONT_MATTER_VARIABLES = [ 'read_length', 'state', 'date_archived', - 'image' + 'image', ] export const DEFAULT_SETTINGS: OmnivoreSettings = { diff --git a/src/settings/suggest.ts b/src/settings/suggest.ts index d2def24..430bce6 100644 --- a/src/settings/suggest.ts +++ b/src/settings/suggest.ts @@ -18,12 +18,12 @@ class Suggest { containerEl.on( 'click', '.suggestion-item', - this.onSuggestionClick.bind(this) + this.onSuggestionClick.bind(this), ) containerEl.on( 'mousemove', '.suggestion-item', - this.onSuggestionMouseover.bind(this) + this.onSuggestionMouseover.bind(this), ) scope.register([], 'ArrowUp', (event) => { @@ -127,7 +127,7 @@ export abstract class TextInputSuggest implements ISuggestOwner { '.suggestion-container', (event: MouseEvent) => { event.preventDefault() - } + }, ) } diff --git a/src/settings/template.ts b/src/settings/template.ts index 04dd135..156a4b9 100644 --- a/src/settings/template.ts +++ b/src/settings/template.ts @@ -15,7 +15,7 @@ import { type FunctionMap = { [key: string]: () => ( text: string, - render: (text: string) => string + render: (text: string) => string, ) => string } @@ -81,301 +81,300 @@ export type ArticleView = } | FunctionMap - export type View = - | { - id: string - title: string - omnivoreUrl: string - siteName: string - originalUrl: string - author: string - date: string - dateSaved: string - datePublished?: string - type: PageType - dateRead?: string - state: string - dateArchived?: string - } - | FunctionMap - - enum ArticleState { - Inbox = 'INBOX', - Reading = 'READING', - Completed = 'COMPLETED', - Archived = 'ARCHIVED', - } - - const getArticleState = (article: Article): string => { - if (article.isArchived) { - return ArticleState.Archived - } - if (article.readingProgressPercent > 0) { - return article.readingProgressPercent === 100 - ? ArticleState.Completed - : ArticleState.Reading +export type View = + | { + id: string + title: string + omnivoreUrl: string + siteName: string + originalUrl: string + author: string + date: string + dateSaved: string + datePublished?: string + type: PageType + dateRead?: string + state: string + dateArchived?: string } + | FunctionMap - return ArticleState.Inbox - } +enum ArticleState { + Inbox = 'INBOX', + Reading = 'READING', + Completed = 'COMPLETED', + Archived = 'ARCHIVED', +} - function lowerCase() { - return function (text: string, render: (text: string) => string) { - return render(text).toLowerCase() - } +const getArticleState = (article: Article): string => { + if (article.isArchived) { + return ArticleState.Archived } - - function upperCase() { - return function (text: string, render: (text: string) => string) { - return render(text).toUpperCase() - } + if (article.readingProgressPercent > 0) { + return article.readingProgressPercent === 100 + ? ArticleState.Completed + : ArticleState.Reading } - function upperCaseFirst() { - return function (text: string, render: (text: string) => string) { - const str = render(text) - return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() - } + return ArticleState.Inbox +} + +function lowerCase() { + return function (text: string, render: (text: string) => string) { + return render(text).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) - if (!date) { - return '' - } - // format the date - return formatDate(date, format) - } +function upperCase() { + return function (text: string, render: (text: string) => string) { + return render(text).toUpperCase() } +} - const functionMap: FunctionMap = { - lowerCase, - upperCase, - upperCaseFirst, - formatDate: formatDateFunc, +function upperCaseFirst() { + return function (text: string, render: (text: string) => string) { + const str = render(text) + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() } +} - const getOmnivoreUrl = (article: Article) => { - return `https://omnivore.app/me/${article.slug}` +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) + if (!date) { + return '' + } + // format the date + return formatDate(date, format) } +} - export const renderFilename = ( - article: Article, - filename: string, - dateFormat: string - ) => { - const renderedFilename = render(article, filename, dateFormat) +const functionMap: FunctionMap = { + lowerCase, + upperCase, + upperCaseFirst, + formatDate: formatDateFunc, +} - // truncate the filename to 100 characters - return truncate(renderedFilename, { - length: 100, - }) - } +const getOmnivoreUrl = (article: Article) => { + return `https://omnivore.app/me/${article.slug}` +} - 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(' ', '_'), - })) - } +export const renderFilename = ( + article: Article, + filename: string, + dateFormat: string, +) => { + const renderedFilename = render(article, filename, dateFormat) + + // 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(' ', '_'), + })) +} - export const renderArticleContnet = async ( - article: Article, - template: string, - highlightOrder: string, - dateHighlightedFormat: string, - dateSavedFormat: string, - isSingleFile: boolean, - frontMatterVariables: string[], - frontMatterTemplate: string, - fileAttachment?: string - ) => { - // filter out notes and redactions - const articleHighlights = - article.highlights?.filter((h) => h.type === HighlightType.Highlight) || - [] - // sort highlights by location if selected in options - if (highlightOrder === 'LOCATION') { - articleHighlights.sort((a, b) => { - try { - if (article.pageType === PageType.File) { - // sort by location in file - return compareHighlightsInFile(a, b) - } - // for web page, sort by location in the page - return getHighlightLocation(a.patch) - getHighlightLocation(b.patch) - } catch (e) { - console.error(e) +export const renderArticleContnet = async ( + article: Article, + template: string, + highlightOrder: string, + dateHighlightedFormat: string, + dateSavedFormat: string, + isSingleFile: boolean, + frontMatterVariables: string[], + frontMatterTemplate: string, + fileAttachment?: string, +) => { + // filter out notes and redactions + const articleHighlights = + article.highlights?.filter((h) => h.type === HighlightType.Highlight) || [] + // sort highlights by location if selected in options + if (highlightOrder === 'LOCATION') { + articleHighlights.sort((a, b) => { + try { + if (article.pageType === PageType.File) { + // sort by location in file return compareHighlightsInFile(a, b) } - }) - } - const highlights: HighlightView[] = articleHighlights.map((highlight) => { - return { - text: formatHighlightQuote(highlight.quote, template), - highlightUrl: `https://omnivore.app/me/${article.slug}#${highlight.id}`, - highlightID: highlight.id.slice(0, 8), - dateHighlighted: formatDate(highlight.updatedAt, dateHighlightedFormat), - note: highlight.annotation ?? undefined, - labels: renderLabels(highlight.labels), - color: highlight.color ?? 'yellow', - positionPercent: highlight.highlightPositionPercent, - positionAnchorIndex: highlight.highlightPositionAnchorIndex + 1, // PDF page numbers start at 1 + // for web page, sort by location in the page + return getHighlightLocation(a.patch) - getHighlightLocation(b.patch) + } catch (e) { + console.error(e) + return compareHighlightsInFile(a, b) } }) - const dateSaved = formatDate(article.savedAt, dateSavedFormat) - const siteName = - article.siteName || siteNameFromUrl(article.originalArticleUrl) - const publishedAt = article.publishedAt - const datePublished = publishedAt - ? formatDate(publishedAt, dateSavedFormat).trim() - : 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 - const readLength = wordsCount - ? Math.round(Math.max(1, wordsCount / 235)) - : undefined - const articleView: ArticleView = { - id: article.id, - title: article.title, - omnivoreUrl: `https://omnivore.app/me/${article.slug}`, - siteName, - originalUrl: article.originalArticleUrl, - author: article.author, - labels: renderLabels(article.labels), - dateSaved, - highlights, - content: article.contentReader === 'WEB' ? article.content : undefined, - datePublished, - fileAttachment, - description: article.description, - note: articleNote?.annotation ?? undefined, - type: article.pageType, - dateRead, - wordsCount, - readLength, - state: getArticleState(article), - dateArchived: article.archivedAt, - image: article.image, - updatedAt: article.updatedAt, - ...functionMap, + } + const highlights: HighlightView[] = articleHighlights.map((highlight) => { + return { + text: formatHighlightQuote(highlight.quote, template), + highlightUrl: `https://omnivore.app/me/${article.slug}#${highlight.id}`, + highlightID: highlight.id.slice(0, 8), + dateHighlighted: formatDate(highlight.updatedAt, dateHighlightedFormat), + note: highlight.annotation ?? undefined, + labels: renderLabels(highlight.labels), + color: highlight.color ?? 'yellow', + positionPercent: highlight.highlightPositionPercent, + positionAnchorIndex: highlight.highlightPositionAnchorIndex + 1, // PDF page numbers start at 1 } + }) + const dateSaved = formatDate(article.savedAt, dateSavedFormat) + const siteName = + article.siteName || siteNameFromUrl(article.originalArticleUrl) + const publishedAt = article.publishedAt + const datePublished = publishedAt + ? formatDate(publishedAt, dateSavedFormat).trim() + : 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 + const readLength = wordsCount + ? Math.round(Math.max(1, wordsCount / 235)) + : undefined + const articleView: ArticleView = { + id: article.id, + title: article.title, + omnivoreUrl: `https://omnivore.app/me/${article.slug}`, + siteName, + originalUrl: article.originalArticleUrl, + author: article.author, + labels: renderLabels(article.labels), + dateSaved, + highlights, + content: article.contentReader === 'WEB' ? article.content : undefined, + datePublished, + fileAttachment, + description: article.description, + note: articleNote?.annotation ?? undefined, + type: article.pageType, + dateRead, + wordsCount, + readLength, + state: getArticleState(article), + dateArchived: article.archivedAt, + image: article.image, + updatedAt: article.updatedAt, + ...functionMap, + } - let frontMatter: { [id: string]: unknown } = { - id: article.id, // id is required for deduplication - } + 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) + // 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) - 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) - // 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.', - } + frontMatter = { + ...frontMatterParsed, + ...frontMatter, } - } 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] - // we use snake case for variables in the front matter - const articleVariable = snakeToCamelCase(variable) - // use alias if available, otherwise use variable - const key = aliasedVariables[1] || variable - if ( - 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 - } - - const value = (articleView as any)[articleVariable] - if (value) { - // if variable is in article, use it - frontMatter[key] = value - } + } catch (error) { + // if there's an error parsing the front matter template, log it + 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.', } } + } 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] + // we use snake case for variables in the front matter + const articleVariable = snakeToCamelCase(variable) + // use alias if available, otherwise use variable + const key = aliasedVariables[1] || variable + if ( + 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 + } - // Build content string based on template - 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}` - - // if single file, wrap the front matter in an array - frontMatterYaml = stringifyYaml([frontMatter]) + const value = (articleView as any)[articleVariable] + if (value) { + // if variable is in article, use it + frontMatter[key] = value + } } + } - const frontMatterStr = `---\n${frontMatterYaml}---` - - return `${frontMatterStr}\n\n${contentWithoutFrontMatter}` + // Build content string based on template + 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}` + + // if single file, wrap the front matter in an array + frontMatterYaml = stringifyYaml([frontMatter]) } - export const render = ( - article: Article, - template: string, - dateFormat: string - ) => { - const dateSaved = formatDate(article.savedAt, dateFormat) - const datePublished = article.publishedAt - ? formatDate(article.publishedAt, dateFormat).trim() - : undefined - const dateArchived = article.archivedAt - ? formatDate(article.archivedAt, dateFormat).trim() - : undefined - const dateRead = article.readAt - ? formatDate(article.readAt, dateFormat).trim() - : undefined - const view: View = { - ...article, - author: article.author || 'unknown-author', - omnivoreUrl: getOmnivoreUrl(article), - originalUrl: article.originalArticleUrl, - date: dateSaved, - dateSaved, - datePublished, - dateArchived, - dateRead, - type: article.pageType, - state: getArticleState(article), - ...functionMap, - } - return Mustache.render(template, view) + const frontMatterStr = `---\n${frontMatterYaml}---` + + return `${frontMatterStr}\n\n${contentWithoutFrontMatter}` +} + +export const render = ( + article: Article, + template: string, + dateFormat: string, +) => { + const dateSaved = formatDate(article.savedAt, dateFormat) + const datePublished = article.publishedAt + ? formatDate(article.publishedAt, dateFormat).trim() + : undefined + const dateArchived = article.archivedAt + ? formatDate(article.archivedAt, dateFormat).trim() + : undefined + const dateRead = article.readAt + ? formatDate(article.readAt, dateFormat).trim() + : undefined + const view: View = { + ...article, + author: article.author || 'unknown-author', + omnivoreUrl: getOmnivoreUrl(article), + originalUrl: article.originalArticleUrl, + date: dateSaved, + dateSaved, + datePublished, + dateArchived, + dateRead, + type: article.pageType, + state: getArticleState(article), + ...functionMap, } + return Mustache.render(template, view) +} export const preParseTemplate = (template: string) => { return Mustache.parse(template) diff --git a/src/util.ts b/src/util.ts index 510cf80..cb8f749 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,21 +1,21 @@ -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" +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 = "-" +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 interface HighlightPoint { - left: number; - top: number; + left: number + top: number } export const getHighlightLocation = (patch: string | null): number => { @@ -54,7 +54,7 @@ export const markdownEscape = (text: string): string => { try { return escape(text) } catch (e) { - console.error("markdownEscape error", e) + console.error('markdownEscape error', e) return text } } @@ -78,29 +78,27 @@ export const wrapAround = (value: number, size: number): number => { export const unicodeSlug = (str: string, savedAt: string) => { return ( str - .normalize("NFKD") // using NFKD method returns the Unicode Normalization Form of a given string. - .replace(/[\u0300-\u036f]/g, "") // remove all previously split accents + .normalize('NFKD') // using NFKD method returns the Unicode Normalization Form of a given string. + .replace(/[\u0300-\u036f]/g, '') // remove all previously split accents .trim() .toLowerCase() .replace( /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, - "" + '', ) // replace all the symbols with - - .replace(/\s+/g, "-") // collapse whitespace and replace by - - .replace(/_/g, "-") // replace _ with - - .replace(/-+/g, "-") // collapse dashes + .replace(/\s+/g, '-') // collapse whitespace and replace by - + .replace(/_/g, '-') // replace _ with - + .replace(/-+/g, '-') // collapse dashes // remove trailing - - .replace(/-$/g, "") + .replace(/-$/g, '') .substring(0, 64) + - "-" + + '-' + new Date(savedAt).getTime().toString(16) ) } export const replaceIllegalChars = (str: string): string => { - return removeInvisibleChars( - str.replace(ILLEGAL_CHAR_REGEX, REPLACEMENT_CHAR) - ) + return removeInvisibleChars(str.replace(ILLEGAL_CHAR_REGEX, REPLACEMENT_CHAR)) } export function formatDate(date: string, format: string): string { @@ -112,40 +110,40 @@ export function formatDate(date: string, format: string): string { export const getQueryFromFilter = ( filter: string, - customQuery: string + customQuery: string, ): string => { switch (filter) { - case "ALL": - return "in:all" - case "HIGHLIGHTS": + case 'ALL': + return 'in:all' + case 'HIGHLIGHTS': return `has:highlights in:all` - case "ADVANCED": + case 'ADVANCED': 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 + template: string, ): string => { if (!quote) { - return "" + return '' } // if the template has highlights, we need to preserve paragraphs 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 @@ -153,7 +151,7 @@ export const formatHighlightQuote = ( export const findFrontMatterIndex = ( frontMatter: any[], - id: string + id: string, ): number => { // find index of front matter with matching id return frontMatter.findIndex((fm) => fm.id == id) @@ -172,11 +170,11 @@ export const parseFrontMatterFromContent = (content: string) => { export const removeFrontMatterFromContent = (content: string): string => { 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) diff --git a/yarn.lock b/yarn.lock index f21bbc0..d2427dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -997,6 +997,11 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@pkgr/core@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== + "@pnpm/config.env-replace@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" @@ -2439,6 +2444,19 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +eslint-config-prettier@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== + +eslint-plugin-prettier@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz#17cfade9e732cef32b5f5be53bd4e07afd8e67e1" + integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw== + dependencies: + prettier-linter-helpers "^1.0.0" + synckit "^0.8.6" + eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -2605,6 +2623,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" @@ -4913,10 +4936,17 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^2.8.1: - version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== pretty-format@^29.0.0, pretty-format@^29.5.0: version "29.5.0" @@ -5563,6 +5593,14 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +synckit@^0.8.6: + version "0.8.8" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7" + integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + tar@^6.1.0, tar@^6.1.11, tar@^6.1.2: version "6.1.15" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.15.tgz#c9738b0b98845a3b344d334b8fa3041aaba53a69" @@ -5696,6 +5734,11 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"