diff --git a/src/main.ts b/src/main.ts index 6b56950..4ca6b50 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,10 +23,10 @@ import { import { FolderSuggest } from "./settings/file-suggest" import { preParseTemplate, + render, renderArticleContnet, renderFilename, - renderFolderName, -} from "./settings/template" +} from './settings/template' import { DATE_FORMAT, findFrontMatterIndex, @@ -35,7 +35,7 @@ import { parseFrontMatterFromContent, removeFrontMatterFromContent, replaceIllegalChars, -} from "./util" +} from './util' export default class OmnivorePlugin extends Plugin { settings: OmnivoreSettings @@ -59,33 +59,33 @@ export default class OmnivorePlugin extends Plugin { } this.addCommand({ - id: "sync", - name: "Sync new changes", + id: 'sync', + name: 'Sync new changes', callback: () => { this.fetchOmnivore() }, }) this.addCommand({ - id: "deleteArticle", - name: "Delete Current Article from Omnivore", - callback: () => { - this.deleteCurrentArticle(this.app.workspace.getActiveFile()) - } + id: 'deleteArticle', + name: 'Delete Current Article from Omnivore', + callback: () => { + this.deleteCurrentArticle(this.app.workspace.getActiveFile()) + }, }) this.addCommand({ - id: "resync", - name: "Resync all articles", + id: 'resync', + name: 'Resync all articles', callback: () => { - this.settings.syncAt = "" + this.settings.syncAt = '' this.saveSettings() - new Notice("Omnivore Last Sync reset") + new Notice('Omnivore Last Sync reset') this.fetchOmnivore() }, }) - const iconId = "Omnivore" + const iconId = 'Omnivore' // add icon addIcon( iconId, @@ -143,10 +143,10 @@ export default class OmnivorePlugin extends Plugin { const url = article.url const response = await requestUrl({ url, - contentType: "application/pdf", + contentType: 'application/pdf', }) const folderName = normalizePath( - renderFolderName( + render( article, this.settings.attachmentFolder, this.settings.folderDateFormat @@ -185,12 +185,12 @@ export default class OmnivorePlugin extends Plugin { } = this.settings if (syncing) { - new Notice("🐢 Already syncing ...") + new Notice('🐢 Already syncing ...') return } if (!apiKey) { - new Notice("Missing Omnivore api key") + new Notice('Missing Omnivore api key') return } @@ -200,17 +200,17 @@ export default class OmnivorePlugin extends Plugin { try { console.log(`obsidian-omnivore starting sync since: '${syncAt}'`) - manualSync && new Notice("🚀 Fetching articles ...") + manualSync && new Notice('🚀 Fetching articles ...') // pre-parse template frontMatterTemplate && preParseTemplate(frontMatterTemplate) const templateSpans = preParseTemplate(template) // 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 @@ -219,7 +219,7 @@ export default class OmnivorePlugin extends Plugin { hasNextPage; after += size ) { - [articles, hasNextPage] = await loadArticles( + ;[articles, hasNextPage] = await loadArticles( this.settings.endpoint, apiKey, after, @@ -227,12 +227,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( - renderFolderName(article, folder, this.settings.folderDateFormat) + render(article, folder, this.settings.folderDateFormat) ) const omnivoreFolder = this.app.vault.getAbstractFileByPath(folderName) @@ -285,7 +285,7 @@ export default class OmnivorePlugin extends Plugin { !Array.isArray(newFrontMatter) || newFrontMatter.length === 0 ) { - throw new Error("Front matter does not exist in the template") + throw new Error('Front matter does not exist in the template') } let newContentWithoutFrontMatter: string @@ -302,7 +302,7 @@ export default class OmnivorePlugin extends Plugin { const sectionEnd = `%%${article.id}_end%%` const existingContentRegex = new RegExp( `${sectionStart}.*?${sectionEnd}`, - "s" + 's' ) newContentWithoutFrontMatter = existingContentWithoutFrontmatter.replace( @@ -367,7 +367,7 @@ export default class OmnivorePlugin extends Plugin { try { await this.app.vault.create(normalizedPath, content) } catch (error) { - if (error.toString().includes("File already exists")) { + 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.` ) @@ -378,10 +378,10 @@ export default class OmnivorePlugin extends Plugin { } } - manualSync && new Notice("🔖 Articles fetched") + manualSync && new Notice('🔖 Articles fetched') this.settings.syncAt = DateTime.local().toFormat(DATE_FORMAT) } catch (e) { - new Notice("Failed to fetch articles") + new Notice('Failed to fetch articles') console.error(e) } finally { this.settings.syncing = false @@ -390,23 +390,27 @@ export default class OmnivorePlugin extends Plugin { } private async deleteCurrentArticle(file: TFile | null) { - if(!file) { - return + if (!file) { + return } //use frontmatter id to find the file const articleId = this.app.metadataCache.getFileCache(file)?.frontmatter?.id if (!articleId) { - new Notice("Failed to delete article: article id not found") + new Notice('Failed to delete article: article id not found') } - try{ - const isDeleted = deleteArticleById(this.settings.endpoint, this.settings.apiKey, articleId) - if(!isDeleted) { - new Notice("Failed to delete article in Omnivore") - } + try { + const isDeleted = deleteArticleById( + this.settings.endpoint, + this.settings.apiKey, + articleId + ) + if (!isDeleted) { + new Notice('Failed to delete article in Omnivore') + } } catch (e) { - new Notice("Failed to delete article in Omnivore") - console.error(e) + new Notice('Failed to delete article in Omnivore') + console.error(e) } await this.app.vault.delete(file) diff --git a/src/settings/template.ts b/src/settings/template.ts index 855fbae..04dd135 100644 --- a/src/settings/template.ts +++ b/src/settings/template.ts @@ -81,274 +81,301 @@ export type ArticleView = } | FunctionMap -enum ArticleState { - Inbox = 'INBOX', - Reading = 'READING', - Completed = 'COMPLETED', - Archived = 'ARCHIVED', -} + 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 -const getArticleState = (article: Article): string => { - if (article.isArchived) { - return ArticleState.Archived - } - if (article.readingProgressPercent > 0) { - return article.readingProgressPercent === 100 - ? ArticleState.Completed - : ArticleState.Reading + enum ArticleState { + Inbox = 'INBOX', + Reading = 'READING', + Completed = 'COMPLETED', + Archived = 'ARCHIVED', } - return ArticleState.Inbox -} + const getArticleState = (article: Article): string => { + if (article.isArchived) { + return ArticleState.Archived + } + if (article.readingProgressPercent > 0) { + return article.readingProgressPercent === 100 + ? ArticleState.Completed + : ArticleState.Reading + } -function lowerCase() { - return function (text: string, render: (text: string) => string) { - return render(text).toLowerCase() + return ArticleState.Inbox } -} -function upperCase() { - return function (text: string, render: (text: string) => string) { - return render(text).toUpperCase() + function lowerCase() { + return function (text: string, render: (text: string) => string) { + return render(text).toLowerCase() + } } -} -function upperCaseFirst() { - return function (text: string, render: (text: string) => string) { - const str = render(text) - return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() + function upperCase() { + return function (text: string, render: (text: string) => string) { + return render(text).toUpperCase() + } } -} -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 '' + function upperCaseFirst() { + return function (text: string, render: (text: string) => string) { + const str = render(text) + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() } - // format the date - return formatDate(date, format) } -} -const functionMap: FunctionMap = { - lowerCase, - upperCase, - upperCaseFirst, - formatDate: formatDateFunc, -} + 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 date = formatDate(article.savedAt, dateFormat) - const datePublished = article.publishedAt - ? formatDate(article.publishedAt, dateFormat).trim() - : undefined - const renderedFilename = Mustache.render(filename, { - title: article.title, - author: article.author ?? 'unknown-author', - date, - dateSaved: date, - datePublished, - id: article.id, - }) - - // truncate the filename to 100 characters - return truncate(renderedFilename, { - length: 100, - }) -} + const functionMap: FunctionMap = { + lowerCase, + upperCase, + upperCaseFirst, + formatDate: formatDateFunc, + } -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(' ', '_'), - })) -} + const getOmnivoreUrl = (article: Article) => { + return `https://omnivore.app/me/${article.slug}` + } -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) - return compareHighlightsInFile(a, b) - } + 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, }) } - 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 + 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(' ', '_'), + })) } - // if the front matter template is set, use it - if (frontMatterTemplate) { - const frontMatterTemplateRendered = Mustache.render( - frontMatterTemplate, - articleView + 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) + 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 + } + }) + 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 ) - try { - // parse the front matter template as yaml - const frontMatterParsed = parseYaml(frontMatterTemplateRendered) + 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, + } - 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.', - } + let frontMatter: { [id: string]: unknown } = { + id: article.id, // id is required for deduplication } - } 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 + + // 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.', + } } + } 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 + const value = (articleView as any)[articleVariable] + if (value) { + // if variable is in article, use it + frontMatter[key] = value + } } } - } - // 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]) - } + // 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 frontMatterStr = `---\n${frontMatterYaml}---` + const frontMatterStr = `---\n${frontMatterYaml}---` - return `${frontMatterStr}\n\n${contentWithoutFrontMatter}` -} + return `${frontMatterStr}\n\n${contentWithoutFrontMatter}` + } -export const renderFolderName = ( - article: Article, - template: string, - dateFormat: string -) => { - const date = formatDate(article.savedAt, dateFormat) - const datePublished = article.publishedAt - ? formatDate(article.publishedAt, dateFormat).trim() - : undefined - return Mustache.render(template, { - date, - dateSaved: date, - datePublished, - author: article.author ?? 'unknown-author', - }) -} + 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)