From 0922ffca02cd0a4dbd16730832ed51cd19b46c48 Mon Sep 17 00:00:00 2001 From: Andrew Chen Date: Tue, 16 Apr 2024 21:47:42 -0400 Subject: [PATCH] feature: render highlight color in article note (#215) improvement: settings refactor --- .../renderHighlightColorQuote.spec.ts | 109 ++++ src/api.ts | 7 + src/main.ts | 481 +------------- src/settings/index.ts | 43 +- src/settings/template.ts | 19 +- src/settingsTab.ts | 613 ++++++++++++++++++ src/util.ts | 42 ++ styles.css | 60 +- tsconfig.json | 2 +- 9 files changed, 888 insertions(+), 488 deletions(-) create mode 100644 src/__tests__/renderHighlightColorQuote.spec.ts create mode 100644 src/settingsTab.ts diff --git a/src/__tests__/renderHighlightColorQuote.spec.ts b/src/__tests__/renderHighlightColorQuote.spec.ts new file mode 100644 index 0000000..8138037 --- /dev/null +++ b/src/__tests__/renderHighlightColorQuote.spec.ts @@ -0,0 +1,109 @@ +import { HighlightRenderOption, formatHighlightQuote } from '../util' +import { HighlightManagerId } from '../settings' + +type testCase = { + quote: string + template: string + highlightRenderOption: HighlightRenderOption | null + expected: string +} + +const quote = 'some quote' +const color = 'red' +const templateWithoutBlockQuote = `{{#highlights}} +{{{text}}} +{{/highlights}}` +const templateWithBlockQuote = `{{#highlights}} +> {{{text}}} +{{/highlights}}` + +const blockQuoteNoHighlightRenderOption = { + quote: quote, + template: templateWithBlockQuote, + highlightRenderOption: null, + expected: quote, +} + +const noBlockQuoteNoHighlightRenderOption = { + quote: quote, + template: templateWithoutBlockQuote, + highlightRenderOption: null, + expected: quote, +} + +const blockQuoteOmnivoreRenderOption = { + quote: quote, + template: templateWithBlockQuote, + highlightRenderOption: { + highlightManagerId: HighlightManagerId.OMNIVORE, + highlightColor: color, + }, + expected: `${quote}`, +} + +const blockQuoteMultiLineOmnivoreRenderOption = { + quote: `${quote} +${quote}`, + template: templateWithBlockQuote, + highlightRenderOption: { + highlightManagerId: HighlightManagerId.OMNIVORE, + highlightColor: color, + }, + expected: `${quote} +> ${quote}`, +} + +const blockQuoteHighlightrRenderOption = { + quote: quote, + template: templateWithBlockQuote, + highlightRenderOption: { + highlightManagerId: HighlightManagerId.HIGHLIGHTR, + highlightColor: color, + }, + expected: `${quote}`, +} + +const noBlockQuoteMultiLineOmnivoreRenderOption = { + quote: `${quote} +${quote}`, + template: templateWithoutBlockQuote, + highlightRenderOption: { + highlightManagerId: HighlightManagerId.OMNIVORE, + highlightColor: color, + }, + expected: `${quote} +${quote}`, +} + +const blockQuoteEmptyLineOmnivoreRenderOption = { + quote: `${quote} + `, + template: templateWithBlockQuote, + highlightRenderOption: { + highlightManagerId: HighlightManagerId.OMNIVORE, + highlightColor: color, + }, + expected: `${quote} +>`, +} + +const testCases: testCase[] = [ + blockQuoteNoHighlightRenderOption, + noBlockQuoteNoHighlightRenderOption, + blockQuoteOmnivoreRenderOption, + blockQuoteMultiLineOmnivoreRenderOption, + blockQuoteHighlightrRenderOption, + noBlockQuoteMultiLineOmnivoreRenderOption, + blockQuoteEmptyLineOmnivoreRenderOption, +] + +describe('formatHighlightQuote', () => { + test.each(testCases)('should correctly for format %s', (testCase) => { + const result = formatHighlightQuote( + testCase.quote, + testCase.template, + testCase.highlightRenderOption, + ) + expect(result).toBe(testCase.expected) + }) +}) diff --git a/src/api.ts b/src/api.ts index 0e613f5..1300187 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,12 @@ import { Item, ItemFormat, Omnivore } from '@omnivore-app/api' +export enum HighlightColors { + Yellow = 'yellow', + Red = 'red', + Green = 'green', + Blue = 'blue', +} + export const getItems = async ( endpoint: string, apiKey: string, diff --git a/src/main.ts b/src/main.ts index 3b9ab88..4f1e028 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,31 +1,21 @@ import { Item } from '@omnivore-app/api' import { addIcon, - App, normalizePath, Notice, Plugin, - PluginSettingTab, requestUrl, - Setting, stringifyYaml, TFile, TFolder, } from 'obsidian' import { deleteItem, getItems } from './api' -import { - DEFAULT_SETTINGS, - Filter, - FRONT_MATTER_VARIABLES, - HighlightOrder, - OmnivoreSettings, -} from './settings' -import { FolderSuggest } from './settings/file-suggest' +import { DEFAULT_SETTINGS, OmnivoreSettings } from './settings' import { preParseTemplate, render, renderFilename, - renderItemContnet, + renderItemContent, } from './settings/template' import { findFrontMatterIndex, @@ -34,7 +24,9 @@ import { parseFrontMatterFromContent, removeFrontMatterFromContent, replaceIllegalChars, + setOrUpdateHighlightColors, } from './util' +import { OmnivoreSettingTab } from './settingsTab' export default class OmnivorePlugin extends Plugin { settings: OmnivoreSettings @@ -137,6 +129,8 @@ export default class OmnivorePlugin extends Plugin { ) this.saveSettings() } + // initialize css highlight color variables + setOrUpdateHighlightColors(this.settings.highlightColorMapping) } async saveSettings() { @@ -254,9 +248,7 @@ export default class OmnivorePlugin extends Plugin { for (const item of items) { const folderName = replaceIllegalChars( - normalizePath( - render(item, folder, this.settings.folderDateFormat), - ) + normalizePath(render(item, folder, this.settings.folderDateFormat)), ) const omnivoreFolder = this.app.vault.getAbstractFileByPath(folderName) @@ -267,10 +259,13 @@ export default class OmnivorePlugin extends Plugin { item.pageType === 'FILE' && includeFileAttachment ? await this.downloadFileAsAttachment(item) : undefined - const content = await renderItemContnet( + const content = await renderItemContent( item, template, highlightOrder, + this.settings.enableHighlightColorRender + ? this.settings.highlightManagerId + : undefined, this.settings.dateHighlightedFormat, this.settings.dateSavedFormat, isSingleFile, @@ -447,457 +442,3 @@ export default class OmnivorePlugin extends Plugin { await this.saveSettings() } } - -class OmnivoreSettingTab extends PluginSettingTab { - plugin: OmnivorePlugin - - constructor(app: App, plugin: OmnivorePlugin) { - super(app, plugin) - this.plugin = plugin - } - - display(): void { - const { containerEl } = this - - containerEl.empty() - - containerEl.createEl('h2', { text: 'Settings for Omnivore plugin' }) - - new Setting(containerEl) - .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', - }), - ) - }), - ) - .addText((text) => - text - .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 update the 'Custom Query' accordingly and 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.customQuery = getQueryFromFilter(value) - this.plugin.settings.syncAt = '' - await this.plugin.saveSettings() - this.display() - }) - }) - - new Setting(containerEl) - .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', - }), - " for more info on search query syntax. Changing this would reset the 'Last Sync' timestamp", - ) - }), - ) - .addText((text) => - text - .setPlaceholder( - '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 = '' - 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", - ) - .addMomentFormat((momentFormat) => - momentFormat - .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') - .addDropdown((dropdown) => { - dropdown.addOptions(HighlightOrder) - dropdown - .setValue(this.plugin.settings.highlightOrder) - .onChange(async (value) => { - this.plugin.settings.highlightOrder = value - await this.plugin.saveSettings() - }) - }) - - new Setting(containerEl) - .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', - }), - 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(',')) - .onChange(async (value) => { - // validate front matter variables and deduplicate - this.plugin.settings.frontMatterVariables = value - .split(',') - .map((v) => v.trim()) - .filter( - (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) - }) - - new Setting(containerEl) - .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', - }), - 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') - .setValue(this.plugin.settings.template) - .onChange(async (value) => { - // 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) - }) - .addExtraButton((button) => { - // add a button to reset template - button - .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 Setting(containerEl) - .setName('Sync on startup') - .setDesc( - 'Check this box if you want to sync with Omnivore when the app is loaded', - ) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.syncOnStart) - .onChange(async (value) => { - this.plugin.settings.syncOnStart = value - await this.plugin.saveSettings() - }), - ) - - new Setting(containerEl) - .setName('Frequency') - .setDesc( - 'Enter the frequency in minutes to sync with Omnivore automatically. 0 means manual sync', - ) - .addText((text) => - text - .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') - return - } - // save frequency - this.plugin.settings.frequency = frequency - await this.plugin.saveSettings() - - this.plugin.scheduleSync() - }), - ) - - new Setting(containerEl) - .setName('Folder') - .setDesc( - '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') - .setValue(this.plugin.settings.folder) - .onChange(async (value) => { - 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) - search - .setPlaceholder('Enter the attachment folder') - .setValue(this.plugin.settings.attachmentFolder) - .onChange(async (value) => { - this.plugin.settings.attachmentFolder = value - await this.plugin.saveSettings() - }) - }) - - new Setting(containerEl) - .setName('Is Single File') - .setDesc( - 'Check this box if you want to save all articles in a single file', - ) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.isSingleFile) - .onChange(async (value) => { - this.plugin.settings.isSingleFile = value - await this.plugin.saveSettings() - }), - ) - - new Setting(containerEl) - .setName('Filename') - .setDesc( - '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') - .setValue(this.plugin.settings.filename) - .onChange(async (value) => { - this.plugin.settings.filename = value - await this.plugin.saveSettings() - }), - ) - - new Setting(containerEl) - .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', - }), - ) - }), - ) - .addText((text) => - text - .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') - .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', - }), - ) - }), - ) - .addText((text) => - text - .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') - .setDesc( - 'Enter the date format for dateSaved variable in rendered template', - ) - .addText((text) => - text - .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() - }), - ) - new Setting(containerEl) - .setName('Date Highlighted Format') - .setDesc( - 'Enter the date format for dateHighlighted variable in rendered template', - ) - .addText((text) => - text - .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', - }) - - const advancedSettings = containerEl.createEl('div', { - cls: 'omnivore-content', - }) - - new Setting(advancedSettings) - .setName('API Endpoint') - .setDesc("Enter the Omnivore server's API endpoint") - .addText((text) => - text - .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') - .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', - }), - 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') - .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) - }) - .addExtraButton((button) => { - // add a button to reset template - button - .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') - }) - }) - - 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 - - for (i = 0; i < coll.length; i++) { - 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' - } - }) - } - } -} diff --git a/src/settings/index.ts b/src/settings/index.ts index 2c4f63a..e70d33f 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -1,3 +1,4 @@ +import { HighlightColors } from '../api' import { DEFAULT_TEMPLATE } from './template' export const FRONT_MATTER_VARIABLES = [ @@ -20,6 +21,25 @@ export const FRONT_MATTER_VARIABLES = [ 'image', ] +export enum Filter { + ALL = 'Sync all the items', + LIBRARY = 'Sync only the library items', + ARCHIVED = 'Sync only the archived items', + HIGHLIGHTS = 'Sync only the highlighted items', +} + +export enum HighlightOrder { + LOCATION = 'the location of highlights in the article', + TIME = 'the time that highlights are updated', +} + +export enum HighlightManagerId { + HIGHLIGHTR = 'hltr', + OMNIVORE = 'omni', +} + +export type HighlightColorMapping = { [key in HighlightColors]: string } + export const DEFAULT_SETTINGS: OmnivoreSettings = { dateHighlightedFormat: 'yyyy-MM-dd HH:mm:ss', dateSavedFormat: 'yyyy-MM-dd HH:mm:ss', @@ -43,18 +63,14 @@ export const DEFAULT_SETTINGS: OmnivoreSettings = { frontMatterVariables: [], frontMatterTemplate: '', syncOnStart: true, -} - -export enum Filter { - ALL = 'Sync all the items', - LIBRARY = 'Sync only the library items', - ARCHIVED = 'Sync only the archived items', - HIGHLIGHTS = 'Sync only the highlighted items', -} - -export enum HighlightOrder { - LOCATION = 'the location of highlights in the article', - TIME = 'the time that highlights are updated', + enableHighlightColorRender: false, + highlightManagerId: HighlightManagerId.OMNIVORE, + highlightColorMapping: { + [HighlightColors.Yellow]: '#fff3a3', + [HighlightColors.Red]: '#ff5582', + [HighlightColors.Blue]: '#adccff', + [HighlightColors.Green]: '#bbfabb', + }, } export interface OmnivoreSettings { @@ -80,4 +96,7 @@ export interface OmnivoreSettings { frontMatterTemplate: string filenameDateFormat: string syncOnStart: boolean + enableHighlightColorRender: boolean + highlightManagerId: HighlightManagerId + highlightColorMapping: HighlightColorMapping } diff --git a/src/settings/template.ts b/src/settings/template.ts index ee9a9dd..949327a 100644 --- a/src/settings/template.ts +++ b/src/settings/template.ts @@ -11,6 +11,7 @@ import { siteNameFromUrl, snakeToCamelCase, } from '../util' +import { HighlightManagerId } from '.' type FunctionMap = { [key: string]: () => ( @@ -182,10 +183,11 @@ export const renderLabels = (labels?: LabelView[]) => { })) } -export const renderItemContnet = async ( +export const renderItemContent = async ( item: Item, template: string, highlightOrder: string, + highlightManagerId: HighlightManagerId | undefined, dateHighlightedFormat: string, dateSavedFormat: string, isSingleFile: boolean, @@ -213,8 +215,19 @@ export const renderItemContnet = async ( }) } const highlights: HighlightView[] = itemHighlights.map((highlight) => { + const highlightColor = highlight.color ?? 'yellow' + const highlightRenderOption = highlightManagerId + ? { + highlightColor: highlightColor, + highlightManagerId: highlightManagerId, + } + : null return { - text: formatHighlightQuote(highlight.quote, template), + text: formatHighlightQuote( + highlight.quote, + template, + highlightRenderOption, + ), highlightUrl: `https://omnivore.app/me/${item.slug}#${highlight.id}`, highlightID: highlight.id.slice(0, 8), dateHighlighted: highlight.updatedAt @@ -222,7 +235,7 @@ export const renderItemContnet = async ( : undefined, note: highlight.annotation ?? undefined, labels: renderLabels(highlight.labels || undefined), - color: highlight.color ?? 'yellow', + color: highlightColor, positionPercent: highlight.highlightPositionPercent || 0, positionAnchorIndex: highlight.highlightPositionAnchorIndex ? highlight.highlightPositionAnchorIndex + 1 diff --git a/src/settingsTab.ts b/src/settingsTab.ts new file mode 100644 index 0000000..9ec1787 --- /dev/null +++ b/src/settingsTab.ts @@ -0,0 +1,613 @@ +import { + App, + ColorComponent, + Notice, + PluginSettingTab, + Setting, +} from 'obsidian' +import OmnivorePlugin from './main' +import { FolderSuggest } from './settings/file-suggest' +import { + DEFAULT_SETTINGS, + FRONT_MATTER_VARIABLES, + Filter, + HighlightManagerId, + HighlightOrder, +} from './settings' +import { getQueryFromFilter, setOrUpdateHighlightColors } from './util' +import { HighlightColors } from './api' + +export class OmnivoreSettingTab extends PluginSettingTab { + plugin: OmnivorePlugin + + constructor(app: App, plugin: OmnivorePlugin) { + super(app, plugin) + this.plugin = plugin + } + + display(): void { + const { containerEl } = this + + containerEl.empty() + + /** + * General Options + **/ + new Setting(containerEl) + .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', + }), + ) + }), + ) + .addText((text) => + text + .setPlaceholder('Enter your Omnivore Api Key') + .setValue(this.plugin.settings.apiKey) + .onChange(async (value) => { + this.plugin.settings.apiKey = value + await this.plugin.saveSettings() + }), + ) + + /** + * Query Options + **/ + containerEl.createEl('h3', { text: 'Query' }) + + new Setting(containerEl) + .setName('Filter') + .setDesc( + "Select an Omnivore search filter type. Changing this would update the 'Custom Query' accordingly and 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.customQuery = getQueryFromFilter(value) + this.plugin.settings.syncAt = '' + await this.plugin.saveSettings() + this.display() + }) + }) + + new Setting(containerEl) + .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', + }), + " for more info on search query syntax. Changing this would reset the 'Last Sync' timestamp", + ) + }), + ) + .addText((text) => + text + .setPlaceholder( + '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 = '' + await this.plugin.saveSettings() + }), + ) + + /** + * Sync Options, such as folder location, file format, etc. + **/ + containerEl.createEl('h3', { text: 'Sync' }) + + new Setting(containerEl) + .setName('Sync on startup') + .setDesc( + 'Check this box if you want to sync with Omnivore when the app is loaded', + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.syncOnStart) + .onChange(async (value) => { + this.plugin.settings.syncOnStart = value + await this.plugin.saveSettings() + }), + ) + new Setting(containerEl) + .setName('Frequency') + .setDesc( + 'Enter the frequency in minutes to sync with Omnivore automatically. 0 means manual sync', + ) + .addText((text) => + text + .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') + return + } + // save frequency + this.plugin.settings.frequency = frequency + await this.plugin.saveSettings() + + this.plugin.scheduleSync() + }), + ) + + new Setting(containerEl) + .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') + .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('Is Single File') + .setDesc( + 'Check this box if you want to save all articles in a single file', + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.isSingleFile) + .onChange(async (value) => { + this.plugin.settings.isSingleFile = value + await this.plugin.saveSettings() + }), + ) + + new Setting(containerEl) + .setName('Folder') + .setDesc( + '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') + .setValue(this.plugin.settings.folder) + .onChange(async (value) => { + this.plugin.settings.folder = value + await this.plugin.saveSettings() + }) + }) + new Setting(containerEl) + .setName('Folder Date Format') + .setDesc( + createFragment((fragment) => { + fragment.append( + 'If date is used as part of folder name, specify the format date for use. Format ', + fragment.createEl('a', { + text: 'reference', + href: 'https://moment.github.io/luxon/#/formatting?id=table-of-tokens', + }), + ) + }), + ) + .addText((text) => + text + .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('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) + search + .setPlaceholder('Enter the attachment folder') + .setValue(this.plugin.settings.attachmentFolder) + .onChange(async (value) => { + this.plugin.settings.attachmentFolder = value + await this.plugin.saveSettings() + }) + }) + + new Setting(containerEl) + .setName('Filename') + .setDesc( + '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') + .setValue(this.plugin.settings.filename) + .onChange(async (value) => { + this.plugin.settings.filename = value + await this.plugin.saveSettings() + }), + ) + + new Setting(containerEl) + .setName('Filename Date Format') + .setDesc( + createFragment((fragment) => { + fragment.append( + 'If date is used as part of file name, specify the format date for use. Format ', + fragment.createEl('a', { + text: 'reference', + href: 'https://moment.github.io/luxon/#/formatting?id=table-of-tokens', + }), + ) + }), + ) + .addText((text) => + text + .setPlaceholder('yyyy-MM-dd') + .setValue(this.plugin.settings.filenameDateFormat) + .onChange(async (value) => { + this.plugin.settings.filenameDateFormat = value + await this.plugin.saveSettings() + }), + ) + + /** + * Article Render Options + **/ + containerEl.createEl('h3', { text: 'Article' }) + + new Setting(containerEl) + .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', + }), + 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(',')) + .onChange(async (value) => { + // validate front matter variables and deduplicate + this.plugin.settings.frontMatterVariables = value + .split(',') + .map((v) => v.trim()) + .filter( + (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) + }) + + new Setting(containerEl) + .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', + }), + 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') + .setValue(this.plugin.settings.template) + .onChange(async (value) => { + // 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) + }) + .addExtraButton((button) => { + // add a button to reset template + button + .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 Setting(containerEl) + .setName('Date Saved Format') + .setDesc( + 'Enter the date format for dateSaved variable in rendered template', + ) + .addText((text) => + text + .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() + }), + ) + + new Setting(containerEl) + .setName('Date Highlighted Format') + .setDesc( + 'Enter the date format for dateHighlighted variable in rendered template', + ) + .addText((text) => + text + .setPlaceholder('Date Highlighted Format') + .setValue(this.plugin.settings.dateHighlightedFormat) + .onChange(async (value) => { + this.plugin.settings.dateHighlightedFormat = value + await this.plugin.saveSettings() + }), + ) + + /** + * Highlight Render Options in Article + **/ + containerEl.createEl('h4', { text: 'Highlights' }) + + new Setting(containerEl) + .setName('Highlight Order') + .setDesc('Select the order in which highlights are applied') + .addDropdown((dropdown) => { + dropdown.addOptions(HighlightOrder) + dropdown + .setValue(this.plugin.settings.highlightOrder) + .onChange(async (value) => { + this.plugin.settings.highlightOrder = value + await this.plugin.saveSettings() + }) + }) + + new Setting(containerEl) + .setName('Render Highlight Color') + .setDesc( + 'Check this box if you want to render highlights with color used in the Omnivore App', + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enableHighlightColorRender) + .onChange(async (value) => { + this.plugin.settings.enableHighlightColorRender = value + await this.plugin.saveSettings() + this.displayBlock(renderHighlightConfigContainer, value) + }), + ) + + const renderHighlightConfigContainer = containerEl.createEl('div') + this.displayBlock( + renderHighlightConfigContainer, + this.plugin.settings.enableHighlightColorRender, + ) + new Setting(renderHighlightConfigContainer) + .setName('Use Highlightr for Highlight styling') + .setDesc( + createFragment((fragment) => { + fragment.append( + fragment.createEl('a', { + text: 'Highlightr', + href: 'https://github.com/chetachiezikeuzor/Highlightr-Plugin', + }), + ' is a community plugin for managing highlight style and hotkeys', + fragment.createEl('br'), + "Check this if you'd like to delegate configuration of highlight color and styling to it", + fragment.createEl('br'), + 'Ensure to select "css-class" as the highlight-method in the highlightr plugin', + ) + }), + ) + .addToggle((toggle) => + toggle + .setValue( + this.plugin.settings.highlightManagerId == + HighlightManagerId.HIGHLIGHTR, + ) + .onChange(async (value) => { + this.plugin.settings.highlightManagerId = value + ? HighlightManagerId.HIGHLIGHTR + : HighlightManagerId.OMNIVORE + await this.plugin.saveSettings() + this.displayBlock(omnivoreHighlightConfigContainer, !value) + }), + ) + + const omnivoreHighlightConfigContainer = + renderHighlightConfigContainer.createEl('div', { + cls: 'omnivore-highlight-config-container', + }) + this.displayBlock( + omnivoreHighlightConfigContainer, + this.plugin.settings.highlightManagerId == HighlightManagerId.OMNIVORE, + ) + const highlighterSetting = new Setting(omnivoreHighlightConfigContainer) + const colorPickers: { [color in string]: ColorComponent } = {} + + highlighterSetting + .setName('Configure highlight colors') + .setDesc( + 'Configure how the highlight colors in Omnivore should render in notes', + ) + .addButton((button) => { + button.setButtonText('Save') + button.setTooltip('Save highlight color setting') + button.setClass('omnivore-btn') + button.setClass('omnivore-btn-primary') + button.onClick(async (e) => { + const highlightColorMapping = + this.plugin.settings.highlightColorMapping + Object.entries(colorPickers).forEach(([color, picker]) => { + highlightColorMapping[color as HighlightColors] = picker.getValue() + }) + setOrUpdateHighlightColors(highlightColorMapping) + await this.plugin.saveSettings() + new Notice('Saved highlight color settings') + }) + }) + + const getPenIcon = (hexCode: string) => + `` + + const colorMap = this.plugin.settings.highlightColorMapping + Object.entries(colorMap).forEach(([colorName, hexCode]) => { + let penIcon = getPenIcon(hexCode) + const settingItem = omnivoreHighlightConfigContainer.createEl('div') + settingItem.addClass('omnivore-highlight-setting-item') + const colorIcon = settingItem.createEl('span') + colorIcon.addClass('omnivore-highlight-setting-icon') + colorIcon.innerHTML = penIcon + + const colorSetting = new Setting(settingItem) + .setName(colorName) + .setDesc(hexCode) + + colorSetting.addColorPicker((colorPicker) => { + colorPicker.setValue(hexCode) + colorPickers[colorName] = colorPicker + colorPicker.onChange((v) => { + penIcon = getPenIcon(v) + colorIcon.innerHTML = penIcon + colorSetting.setDesc(v) + }) + }) + }) + + /** + * Advanced Settings + **/ + containerEl.createEl('h3', { + cls: 'omnivore-collapsible', + text: 'Advanced Settings', + }) + + const advancedSettings = containerEl.createEl('div', { + cls: 'omnivore-content', + }) + + new Setting(advancedSettings) + .setName('API Endpoint') + .setDesc("Enter the Omnivore server's API endpoint") + .addText((text) => + text + .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') + .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', + }), + 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') + .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) + }) + .addExtraButton((button) => { + // add a button to reset template + button + .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') + }) + }) + + 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 + + for (i = 0; i < coll.length; i++) { + 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' + } + }) + } + } + + displayBlock(block: HTMLElement, display: boolean) { + block.style.display = display ? 'block' : 'none' + } +} diff --git a/src/util.ts b/src/util.ts index 1cd2d58..adad48b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -4,6 +4,7 @@ import { DateTime } from 'luxon' import escape from 'markdown-escape' import { parseYaml } from 'obsidian' import outOfCharacter from 'out-of-character' +import { HighlightColorMapping, HighlightManagerId } from './settings' export const DATE_FORMAT_W_OUT_SECONDS = "yyyy-MM-dd'T'HH:mm" export const DATE_FORMAT = `${DATE_FORMAT_W_OUT_SECONDS}:ss` @@ -18,6 +19,11 @@ export interface HighlightPoint { top: number } +export interface HighlightRenderOption { + highlightManagerId: HighlightManagerId + highlightColor: string +} + export const getHighlightLocation = (patch: string | null): number => { if (!patch) { return 0 @@ -131,9 +137,32 @@ export const siteNameFromUrl = (originalArticleUrl: string): string => { } } +const wrapHighlightMarkup = ( + quote: string, + highlightRenderOption: HighlightRenderOption, +): string => { + const { highlightManagerId, highlightColor } = highlightRenderOption + + const markupRender = (content: string) => { + if (content.trim().length === 0) { + return '' + } + if (highlightManagerId == HighlightManagerId.HIGHLIGHTR) { + return `${content}` + } else { + return `${content}` + } + } + + return quote.replaceAll(/(>)?(.+)$/gm, (_, g1, g2) => { + return (g1 ?? '') + markupRender(g2) + }) +} + export const formatHighlightQuote = ( quote: string | null, template: string, + highlightRenderOption: HighlightRenderOption | null, ): string => { if (!quote) { return '' @@ -144,6 +173,9 @@ export const formatHighlightQuote = ( // replace all empty lines with blockquote '>' to preserve paragraphs quote = quote.replaceAll('>', '>').replaceAll(/\n/gm, '\n> ') } + if (highlightRenderOption != null) { + quote = wrapHighlightMarkup(quote, highlightRenderOption) + } return quote } @@ -178,3 +210,13 @@ export const snakeToCamelCase = (str: string) => const removeInvisibleChars = (str: string): string => { return outOfCharacter.replace(str) } + +export const setOrUpdateHighlightColors = ( + colorSetting: HighlightColorMapping, +) => { + const root = document.documentElement + + Object.entries(colorSetting).forEach(([k, v]) => { + root.style.setProperty(`--omni-${k}`, v) + }) +} diff --git a/styles.css b/styles.css index 7625532..07f3857 100644 --- a/styles.css +++ b/styles.css @@ -12,10 +12,66 @@ } .omnivore-collapsible:after { - content: "\02795"; /* Unicode character for "plus" sign (+) */ + content: '\02795'; /* Unicode character for "plus" sign (+) */ float: right; } .omnivore-active:after { - content: "\2796"; /* Unicode character for "minus" sign (-) */ + content: '\2796'; /* Unicode character for "minus" sign (-) */ +} + +.omnivore-highlight-config-container { + border-bottom: 1px solid var(--background-modifier-border); +} + +.modal.mod-settings + button:not(.mod-cta):not(.mod-warning).omnivore-btn.omnivore-btn-primary { + color: var(--text-accent); +} + +.modal.mod-settings + button:not(.mod-cta):not(.mod-warning).omnivore-btn.omnivore-btn-primary:hover { + color: var(--text-accent-hover); + background-color: var(--background-secondary); +} + +/*---------------------------------------------------------------- +Omnivore Highlight Color Config Setting +----------------------------------------------------------------*/ + +.omnivore-highlight-setting-item { + display: grid; + grid-gap: 8px; + grid-template-columns: 0.5fr 7fr; + align-items: center; + border-top: 1px solid var(--background-modifier-border); +} + +.omnivore-highlight-setting-icon { + display: flex; + height: 24px; + width: 24px; +} + +mark.omni { + background-color: var(--text-highlight-rgb); + font-weight: 500; + margin: 0 -0.05em; + padding: 0.125em 0.15em; + border-radius: 0.25em; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; +} + +mark.omni-yellow { + --text-highlight-rgb: var(--omni-yellow); +} +mark.omni-red { + --text-highlight-rgb: var(--omni-red); +} +mark.omni-green { + --text-highlight-rgb: var(--omni-green); +} +mark.omni-blue { + --text-highlight-rgb: var(--omni-blue); } diff --git a/tsconfig.json b/tsconfig.json index 179f2f1..f335682 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "isolatedModules": true, "strictNullChecks": true, "esModuleInterop": true, - "lib": ["ES2021", "DOM"] + "lib": ["ES2021","ES2021.String", "DOM"] }, "include": ["src/**/*.ts"] }