From aa2b04751b98ede306caddbf0a52e1a04985fa90 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 13 Apr 2023 16:26:36 +0800 Subject: [PATCH] fix test --- src/api.ts | 8 +- src/main.ts | 359 +++++++++++++++++++++++++++++---- src/settings/index.ts | 285 +------------------------- src/{ => settings}/template.ts | 91 ++++++--- 4 files changed, 391 insertions(+), 352 deletions(-) rename src/{ => settings}/template.ts (71%) diff --git a/src/api.ts b/src/api.ts index b2a79a7..1a9fde4 100644 --- a/src/api.ts +++ b/src/api.ts @@ -26,16 +26,16 @@ export interface Article { title: string; siteName: string; originalArticleUrl: string; - author: string; - description: string; + author?: string; + description?: string; slug: string; labels?: Label[]; highlights?: Highlight[]; updatedAt: string; savedAt: string; pageType: PageType; - content?: string; - publishedAt: string; + content: string; + publishedAt?: string; url: string; readAt?: string; } diff --git a/src/main.ts b/src/main.ts index 23f3bec..9bfaee9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,25 +1,30 @@ import { DateTime } from "luxon"; import { addIcon, + App, normalizePath, Notice, Plugin, + PluginSettingTab, requestUrl, + Setting, TFile, TFolder, } from "obsidian"; -import { Article, loadArticles } from "./api"; +import { Article, loadArticles, PageType } from "./api"; import { DEFAULT_SETTINGS, + Filter, + HighlightOrder, OmnivoreSettings, - OmnivoreSettingTab, } from "./settings"; +import { FolderSuggest } from "./settings/file-suggest"; import { renderArticleContnet, renderAttachmentFolder, renderFilename, renderFolderName, -} from "./template"; +} from "./settings/template"; import { DATE_FORMAT, formatDate, @@ -28,36 +33,6 @@ import { replaceIllegalChars, } from "./util"; -export const downloadFileAsAttachment = async ( - article: Article, - attachmentFolder: string, - folderDateFormat: string -): Promise => { - // download pdf from the URL to the attachment folder - const url = article.url; - const response = await requestUrl({ - url, - contentType: "application/pdf", - }); - const folderName = normalizePath( - renderAttachmentFolder(article, attachmentFolder, folderDateFormat) - ); - const folder = app.vault.getAbstractFileByPath(folderName); - if (!(folder instanceof TFolder)) { - await app.vault.createFolder(folderName); - } - const fileName = normalizePath(`${folderName}/${article.id}.pdf`); - const file = app.vault.getAbstractFileByPath(fileName); - if (!(file instanceof TFile)) { - const newFile = await app.vault.createBinary( - fileName, - response.arrayBuffer - ); - return newFile.path; - } - return file.path; -}; - export default class OmnivorePlugin extends Plugin { settings: OmnivoreSettings; @@ -114,6 +89,36 @@ export default class OmnivorePlugin extends Plugin { await this.saveData(this.settings); } + async downloadFileAsAttachment(article: Article): Promise { + // download pdf from the URL to the attachment folder + const url = article.url; + const response = await requestUrl({ + url, + contentType: "application/pdf", + }); + const folderName = normalizePath( + renderAttachmentFolder( + article, + this.settings.attachmentFolder, + this.settings.folderDateFormat + ) + ); + const folder = app.vault.getAbstractFileByPath(folderName); + if (!(folder instanceof TFolder)) { + await app.vault.createFolder(folderName); + } + const fileName = normalizePath(`${folderName}/${article.id}.pdf`); + const file = app.vault.getAbstractFileByPath(fileName); + if (!(file instanceof TFile)) { + const newFile = await app.vault.createBinary( + fileName, + response.arrayBuffer + ); + return newFile.path; + } + return file.path; + } + async fetchOmnivore() { const { syncAt, @@ -126,7 +131,6 @@ export default class OmnivorePlugin extends Plugin { folder, filename, folderDateFormat, - attachmentFolder, } = this.settings; if (syncing) { @@ -178,14 +182,17 @@ export default class OmnivorePlugin extends Plugin { if (!(omnivoreFolder instanceof TFolder)) { await this.app.vault.createFolder(folderName); } + const fileAttachment = + article.pageType === PageType.File + ? await this.downloadFileAsAttachment(article) + : undefined; const content = await renderArticleContnet( article, template, highlightOrder, this.settings.dateHighlightedFormat, this.settings.dateSavedFormat, - attachmentFolder, - folderDateFormat + fileAttachment ); // use the custom filename const customFilename = replaceIllegalChars( @@ -256,3 +263,283 @@ 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" }); + + // create a group of general settings + containerEl.createEl("h3", { + cls: "omnivore-collapsible", + text: "General Settings", + }); + + const generalSettings = containerEl.createEl("div", { + cls: "omnivore-content", + }); + + new Setting(generalSettings) + .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(generalSettings) + .setName("Filter") + .setDesc("Select an Omnivore search filter type") + .addDropdown((dropdown) => { + dropdown.addOptions(Filter); + dropdown + .setValue(this.plugin.settings.filter) + .onChange(async (value) => { + this.plugin.settings.filter = value; + await this.plugin.saveSettings(); + }); + }); + + new Setting(generalSettings) + .setName("Custom query") + .setDesc( + createFragment((fragment) => { + fragment.append( + "See ", + fragment.createEl("a", { + text: "https://omnivore.app/help/search", + href: "https://omnivore.app/help/search", + }), + " for more info on search query syntax" + ); + }) + ) + .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; + await this.plugin.saveSettings(); + }) + ); + + new Setting(generalSettings) + .setName("Last Sync") + .setDesc("Last time the plugin synced with Omnivore") + .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(generalSettings) + .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(generalSettings) + .setName("Template") + .setDesc( + createFragment((fragment) => { + fragment.append( + "Enter template to render articles with ", + fragment.createEl("a", { + text: "Reference", + href: "https://github.com/janl/mustache.js/#templates", + }), + fragment.createEl("p", { + text: "Available variables: id, title, omnivoreUrl, siteName, originalUrl, author, content, description, dateSaved, datePublished, pdfAttachment, note, labels.name, highlights.text, highlights.highlightUrl, highlights.note, highlights.dateHighlighted, highlights.labels.name", + }), + fragment.createEl("p", { + text: "Please note that id in the frontmatter is required for the plugin to work properly.", + }) + ); + }) + ) + .addTextArea((text) => { + text + .setPlaceholder("Enter the template") + .setValue(this.plugin.settings.template) + .onChange(async (value) => { + // TODO: validate template + // if template is empty, use default template + this.plugin.settings.template = value + ? value + : DEFAULT_SETTINGS.template; + await this.plugin.saveSettings(); + }); + text.inputEl.setAttr("rows", 10); + text.inputEl.setAttr("cols", 40); + }); + + new Setting(generalSettings) + .setName("Folder") + .setDesc( + "Enter the folder where the data will be stored. {{{date}}} 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(generalSettings) + .setName("Attachment Folder") + .setDesc( + "Enter the folder where the attachment will be downloaded to. {{{date}}} 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(generalSettings) + .setName("Filename") + .setDesc( + "Enter the filename where the data will be stored. {{{title}}} and {{{date}}} 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(generalSettings) + .setName("Folder Date Format") + .setDesc( + createFragment((fragment) => { + fragment.append( + "Enter the format date for use in rendered template. 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(generalSettings).setName("Date Saved Format").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(generalSettings) + .setName("Date Highlighted Format") + .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("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) => { + console.log("endpoint: " + value); + this.plugin.settings.endpoint = value; + await this.plugin.saveSettings(); + }) + ); + + const help = containerEl.createEl("p"); + help.innerHTML = `For more information, please visit the plugin's GitHub page or email us at feedback@omnivore.app.`; + + // 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 e8e4df6..4b1cdb3 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -1,7 +1,4 @@ -import { App, PluginSettingTab, Setting } from "obsidian"; -import OmnivorePlugin from "src/main"; -import { DEFAULT_TEMPLATE } from "src/template"; -import { FolderSuggest } from "./file-suggest"; +import { DEFAULT_TEMPLATE } from "./template"; export const DEFAULT_SETTINGS: OmnivoreSettings = { dateHighlightedFormat: "yyyy-MM-dd HH:mm:ss", @@ -47,283 +44,3 @@ export interface OmnivoreSettings { filename: string; attachmentFolder: string; } - -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(); - - containerEl.createEl("h2", { text: "Settings for Omnivore plugin" }); - - // create a group of general settings - containerEl.createEl("h3", { - cls: "omnivore-collapsible", - text: "General Settings", - }); - - const generalSettings = containerEl.createEl("div", { - cls: "omnivore-content", - }); - - new Setting(generalSettings) - .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(generalSettings) - .setName("Filter") - .setDesc("Select an Omnivore search filter type") - .addDropdown((dropdown) => { - dropdown.addOptions(Filter); - dropdown - .setValue(this.plugin.settings.filter) - .onChange(async (value) => { - this.plugin.settings.filter = value; - await this.plugin.saveSettings(); - }); - }); - - new Setting(generalSettings) - .setName("Custom query") - .setDesc( - createFragment((fragment) => { - fragment.append( - "See ", - fragment.createEl("a", { - text: "https://omnivore.app/help/search", - href: "https://omnivore.app/help/search", - }), - " for more info on search query syntax" - ); - }) - ) - .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; - await this.plugin.saveSettings(); - }) - ); - - new Setting(generalSettings) - .setName("Last Sync") - .setDesc("Last time the plugin synced with Omnivore") - .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(generalSettings) - .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(generalSettings) - .setName("Template") - .setDesc( - createFragment((fragment) => { - fragment.append( - "Enter template to render articles with ", - fragment.createEl("a", { - text: "Reference", - href: "https://github.com/janl/mustache.js/#templates", - }), - fragment.createEl("p", { - text: "Available variables: id, title, omnivoreUrl, siteName, originalUrl, author, content, description, dateSaved, datePublished, pdfAttachment, note, labels.name, highlights.text, highlights.highlightUrl, highlights.note, highlights.dateHighlighted, highlights.labels.name", - }), - fragment.createEl("p", { - text: "Please note that id in the frontmatter is required for the plugin to work properly.", - }) - ); - }) - ) - .addTextArea((text) => { - text - .setPlaceholder("Enter the template") - .setValue(this.plugin.settings.template) - .onChange(async (value) => { - // TODO: validate template - // if template is empty, use default template - this.plugin.settings.template = value - ? value - : DEFAULT_SETTINGS.template; - await this.plugin.saveSettings(); - }); - text.inputEl.setAttr("rows", 10); - text.inputEl.setAttr("cols", 40); - }); - - new Setting(generalSettings) - .setName("Folder") - .setDesc( - "Enter the folder where the data will be stored. {{{date}}} 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(generalSettings) - .setName("Attachment Folder") - .setDesc( - "Enter the folder where the attachment will be downloaded to. {{{date}}} 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(generalSettings) - .setName("Filename") - .setDesc( - "Enter the filename where the data will be stored. {{{title}}} and {{{date}}} 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(generalSettings) - .setName("Folder Date Format") - .setDesc( - createFragment((fragment) => { - fragment.append( - "Enter the format date for use in rendered template. 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(generalSettings).setName("Date Saved Format").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(generalSettings) - .setName("Date Highlighted Format") - .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("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) => { - console.log("endpoint: " + value); - this.plugin.settings.endpoint = value; - await this.plugin.saveSettings(); - }) - ); - - const help = containerEl.createEl("p"); - help.innerHTML = `For more information, please visit the plugin's GitHub page or email us at feedback@omnivore.app.`; - - // 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/template.ts b/src/settings/template.ts similarity index 71% rename from src/template.ts rename to src/settings/template.ts index 635fd8d..42f7f1f 100644 --- a/src/template.ts +++ b/src/settings/template.ts @@ -1,13 +1,13 @@ import Mustache from "mustache"; import { stringifyYaml } from "obsidian"; -import { Article, HighlightType, PageType } from "./api"; -import { downloadFileAsAttachment } from "./main"; +import { Article, HighlightType, PageType } from "../api"; +// import { downloadFileAsAttachment } from "../main"; import { compareHighlightsInFile, formatDate, getHighlightLocation, siteNameFromUrl, -} from "./util"; +} from "../util"; export const DEFAULT_TEMPLATE = `--- id: {{{id}}} @@ -45,6 +45,37 @@ date_published: {{{datePublished}}} {{/highlights}} {{/highlights.length}}`; +export interface LabelVariable { + name: string; +} + +export interface HighlightVariables { + text: string; + highlightUrl: string; + dateHighlighted: string; + note?: string; + labels?: LabelVariable[]; +} + +export interface ArticleVariables { + id: string; + title: string; + omnivoreUrl: string; + siteName: string; + originalUrl: string; + author?: string; + labels?: LabelVariable[]; + dateSaved: string; + highlights: HighlightVariables[]; + content: string; + datePublished?: string; + fileAttachment?: string; + description?: string; + note?: string; + type: PageType; + dateRead?: string; +} + export const renderFilename = ( article: Article, filename: string, @@ -69,14 +100,20 @@ export const renderAttachmentFolder = ( }); }; +export const renderLabels = (labels?: LabelVariable[]) => { + return labels?.map((l) => ({ + // replace spaces with underscores because Obsidian doesn't allow spaces in tags + name: l.name.replace(" ", "_"), + })); +}; + export const renderArticleContnet = async ( article: Article, template: string, highlightOrder: string, dateHighlightedFormat: string, dateSavedFormat: string, - attachmentFolder: string, - folderDateFormat: string + fileAttachment?: string ) => { // filter out notes and redactions const articleHighlights = @@ -104,29 +141,31 @@ export const renderArticleContnet = async ( } }); } - const highlights = articleHighlights.map((highlight) => { - return { - text: highlight.quote, - highlightUrl: `https://omnivore.app/me/${article.slug}#${highlight.id}`, - dateHighlighted: formatDate(highlight.updatedAt, dateHighlightedFormat), - note: highlight.annotation, - labels: highlight.labels?.map((l) => ({ - name: l.name, - })), - }; - }); + const highlights: HighlightVariables[] = articleHighlights.map( + (highlight) => { + return { + text: highlight.quote, + highlightUrl: `https://omnivore.app/me/${article.slug}#${highlight.id}`, + dateHighlighted: formatDate(highlight.updatedAt, dateHighlightedFormat), + note: highlight.annotation, + labels: renderLabels(highlight.labels), + }; + } + ); const dateSaved = formatDate(article.savedAt, dateSavedFormat); const siteName = article.siteName || siteNameFromUrl(article.originalArticleUrl); const publishedAt = article.publishedAt; const datePublished = publishedAt ? formatDate(publishedAt, dateSavedFormat) - : null; + : undefined; const articleNote = article.highlights?.find( (h) => h.type === HighlightType.Note ); - // Build content string based on template - let content = Mustache.render(template, { + const dateRead = article.readAt + ? formatDate(article.readAt, dateSavedFormat) + : undefined; + const articleVariables: ArticleVariables = { id: article.id, title: article.title, omnivoreUrl: `https://omnivore.app/me/${article.slug}`, @@ -142,18 +181,14 @@ export const renderArticleContnet = async ( highlights, content: article.content, datePublished, - pdfAttachment: - article.pageType === PageType.File - ? await downloadFileAsAttachment( - article, - attachmentFolder, - folderDateFormat - ) - : undefined, + fileAttachment, description: article.description, note: articleNote?.annotation, type: article.pageType, - }); + dateRead, + }; + // Build content string based on template + let content = Mustache.render(template, articleVariables); const frontmatterRegex = /^(---[\s\S]*?---)/gm; // get the frontmatter from the content