diff --git a/docs/docs/templates.md b/docs/docs/templates.md index 14432f918ed9da99fa00380d5db9d5874a3e4df2..324eb7a06a6245cd8eace448c2d53728dfe7b451 100644 --- a/docs/docs/templates.md +++ b/docs/docs/templates.md @@ -16,6 +16,7 @@ This means the following characters will be removed: `\ , # % & / { } * < > $ ' This template will be used to create the note text. You can use the following syntax: - `{{title}}`: The title of the podcast episode. +- `{{safeTitle}}`: The title of the podcast episode, but with all special characters removed (like `{{title}}` in file path templates). - `{{description}}`: The description of the podcast episode. - You can use `{{description:> }}` to prepend each new line with a `>` (to put the entire description in a blockquote). - `{{podcast}}`: The name of the podcast. diff --git a/src/TemplateEngine.ts b/src/TemplateEngine.ts index 89bf5881c54e14722eb61f17b379b26ea3526700..7168f0da1e4c2d5042a447f4a32e477958177468 100644 --- a/src/TemplateEngine.ts +++ b/src/TemplateEngine.ts @@ -9,77 +9,94 @@ interface Tags { [tag: string]: string | ((...args: unknown[]) => string); } -function TemplateEngine(template: string, tags: Tags) { - return template.replace(/\{\{(.*?)(:\s*?.+?)?\}\}/g, (match: string, tagId: string, params: string) => { - const tagValue = tags[tagId.toLowerCase()]; - if (tagValue === null || tagValue === undefined) { - const fuse = new Fuse(Object.keys(tags), { - shouldSort: true, - findAllMatches: false, - threshold: 0.4, - isCaseSensitive: false, - }); - - const similarTag = fuse.search(tagId); - - new Notice(`Tag ${tagId} is invalid.${similarTag.length > 0 ? ` Did you mean ${similarTag[0].item}?` : ""}`); - return match; - } - - if (typeof tagValue === 'function') { - if (params) { - // Remove initial colon with splice. - const splitParams = params.slice(1).split(','); - const args = Array.isArray(splitParams) ? splitParams : [params]; - - return tagValue(...args); +type AddTagFn = (tag: Lowercase<string>, value: string | ((...args: unknown[]) => string)) => void; +type ReplacerFn = (template: string) => string; + +function useTemplateEngine(): Readonly<[ReplacerFn, AddTagFn]> { + const tags: Tags = {}; + + function addTag(tag: Lowercase<string>, value: string | ((...args: unknown[]) => string)): void { + tags[tag] = value; + } + + function replacer(template: string): string { + return template.replace(/\{\{(.*?)(:\s*?.+?)?\}\}/g, (match: string, tagId: string, params: string) => { + const tagValue = tags[tagId.toLowerCase()]; + if (tagValue === null || tagValue === undefined) { + const fuse = new Fuse(Object.keys(tags), { + shouldSort: true, + findAllMatches: false, + threshold: 0.4, + isCaseSensitive: false, + }); + + const similarTag = fuse.search(tagId); + + new Notice(`Tag ${tagId} is invalid.${similarTag.length > 0 ? ` Did you mean ${similarTag[0].item}?` : ""}`); + return match; } - return tagValue(); - } + if (typeof tagValue === 'function') { + if (params) { + // Remove initial colon with splice. + const splitParams = params.slice(1).split(','); + const args = Array.isArray(splitParams) ? splitParams : [params]; + + return tagValue(...args); + } + + return tagValue(); + } - return tagValue; - }); + return tagValue; + }); + } + + return [replacer, addTag] as const; } + export function NoteTemplateEngine(template: string, episode: Episode) { - return TemplateEngine(template, { - "title": episode.title, - "description": (prependToLines?: string) => { + const [replacer, addTag] = useTemplateEngine(); + + addTag('title', episode.title); + addTag('description', (prependToLines?: string) => { if (prependToLines) { return htmlToMarkdown(episode.description) .split("\n") - .map(prepend(prependToLines)) + .map((str) => `${prependToLines}${str}`) .join("\n") } return htmlToMarkdown(episode.description) - }, - "url": episode.url, - "date": (format?: string) => episode.episodeDate ? + }); + addTag('safetitle', replaceIllegalFileNameCharactersInString(episode.title)); + addTag('url', episode.url); + addTag('date', (format?: string) => episode.episodeDate ? window.moment(episode.episodeDate).format(format ?? "YYYY-MM-DD") - : "", - "podcast": episode.podcastName, - "artwork": episode.artworkUrl ?? "", - }); -} + : ""); + addTag('podcast', episode.podcastName); + addTag('artwork', episode.artworkUrl ?? ""); -function prepend(prepend: string) { - return (str: string) => `${prepend}${str}`; + return replacer(template); } export function TimestampTemplateEngine(template: string) { - return TemplateEngine(template, { - "time": (format?: string) => get(plugin).api.getPodcastTimeFormatted(format ?? "HH:mm:ss"), - "linktime": (format?: string) => get(plugin).api.getPodcastTimeFormatted(format ?? "HH:mm:ss", true), - }); + const [replacer, addTag] = useTemplateEngine(); + + addTag('time', (format?: string) => get(plugin).api.getPodcastTimeFormatted(format ?? "HH:mm:ss")) + addTag('linktime', (format?: string) => get(plugin).api.getPodcastTimeFormatted(format ?? "HH:mm:ss", true)) + + return replacer(template); } export function FilePathTemplateEngine(template: string, episode: Episode) { - return TemplateEngine(template, { - "title": replaceIllegalFileNameCharactersInString(episode.title), - "podcast": replaceIllegalFileNameCharactersInString(episode.podcastName), - }); + const [replacer, addTag] = useTemplateEngine(); + + addTag('title', replaceIllegalFileNameCharactersInString(episode.title)); + addTag('podcast', replaceIllegalFileNameCharactersInString(episode.podcastName)); + + return replacer(template); } export function DownloadPathTemplateEngine(template: string, episode: Episode) { @@ -89,10 +106,12 @@ export function DownloadPathTemplateEngine(template: string, episode: Episode) { template.replace(templateExtension, '') : template; - return TemplateEngine(templateWithoutExtension, { - "title": replaceIllegalFileNameCharactersInString(episode.title), - "podcast": replaceIllegalFileNameCharactersInString(episode.podcastName), - }); + const [replacer, addTag] = useTemplateEngine(); + + addTag("title", replaceIllegalFileNameCharactersInString(episode.title)); + addTag("podcast", replaceIllegalFileNameCharactersInString(episode.podcastName)); + + return replacer(templateWithoutExtension); } function replaceIllegalFileNameCharactersInString(string: string) { diff --git a/src/createPodcastNote.ts b/src/createPodcastNote.ts index b600cd85621520067fa3524f5b1f1bbab4667a80..0d417dfa19121afcf349a9013ea18941765843e1 100644 --- a/src/createPodcastNote.ts +++ b/src/createPodcastNote.ts @@ -3,6 +3,7 @@ import { FilePathTemplateEngine, NoteTemplateEngine } from "./TemplateEngine"; import { Episode } from "./types/Episode"; import { get } from "svelte/store"; import { plugin } from "./store"; +import addExtension from "./utility/addExtension"; export default async function createPodcastNote( episode: Episode @@ -14,43 +15,19 @@ export default async function createPodcastNote( episode ); - const filePathDotMd = filePath.endsWith(".md") - ? filePath - : `${filePath}.md`; + const filePathDotMd = addExtension(filePath, "md"); const content = NoteTemplateEngine( pluginInstance.settings.note.template, episode ); - const createOrGetFile: ( - path: string, - content: string - ) => Promise<TFile> = async (path: string, content: string) => { - const file = getPodcastNote(episode); - - if (file) { - new Notice( - `Note for "${pluginInstance.api.podcast.title}" already exists` - ); - return file; - } - - const foldersInPath = path.split("/").slice(0, -1); - for (let i = 0; i < foldersInPath.length; i++) { - const folderPath = foldersInPath.slice(0, i + 1).join("/"); - const folder = app.vault.getAbstractFileByPath(folderPath); - - if (!folder) { - await app.vault.createFolder(folderPath); - } - } - - return await app.vault.create(path, content); - }; - try { - const file = await createOrGetFile(filePathDotMd, content); + const file = await createFileIfNotExists( + filePathDotMd, + content, + episode + ); app.workspace.getLeaf().openFile(file); } catch (error) { @@ -67,9 +44,7 @@ export function getPodcastNote(episode: Episode): TFile | null { episode ); - const filePathDotMd = filePath.endsWith(".md") - ? filePath - : `${filePath}.md`; + const filePathDotMd = addExtension(filePath, "md"); const file = app.vault.getAbstractFileByPath(filePathDotMd); if (!file || !(file instanceof TFile)) { @@ -89,3 +64,30 @@ export function openPodcastNote(epiosode: Episode): void { app.workspace.getLeaf().openFile(file); } + +async function createFileIfNotExists( + path: string, + content: string, + episode: Episode, + createFolder = true +): Promise<TFile> { + const file = getPodcastNote(episode); + + if (file) { + new Notice(`Note for "${episode.title}" already exists`); + + return file; + } + + const foldersInPath = path.split("/").slice(0, -1); + for (let i = 0; i < foldersInPath.length; i++) { + const folderPath = foldersInPath.slice(0, i + 1).join("/"); + const folder = app.vault.getAbstractFileByPath(folderPath); + + if (!folder && createFolder) { + await app.vault.createFolder(folderPath); + } + } + + return await app.vault.create(path, content); +} diff --git a/src/utility/addExtension.test.ts b/src/utility/addExtension.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..1699b126d7848889e8fe83a364d21acf104a259e --- /dev/null +++ b/src/utility/addExtension.test.ts @@ -0,0 +1,17 @@ +import { expect, describe, test } from "vitest"; +import addExtension from "./addExtension"; + +describe("addExtension", () => { + test("adds an extension to a file path", () => { + expect(addExtension("path/to/file", "md")).toBe("path/to/file.md"); + }); + + test("does not add an extension if the file path already has one", () => { + expect(addExtension("path/to/file.md", "md")).toBe("path/to/file.md"); + }); + + test("works with and without dot in extension", () => { + expect(addExtension("path/to/file", ".md")).toBe("path/to/file.md"); + expect(addExtension("path/to/file.md", "md")).toBe("path/to/file.md"); + }); +}); \ No newline at end of file diff --git a/src/utility/addExtension.ts b/src/utility/addExtension.ts new file mode 100644 index 0000000000000000000000000000000000000000..67b2c2aa8f23d47b362daa23f64076550b462960 --- /dev/null +++ b/src/utility/addExtension.ts @@ -0,0 +1,5 @@ +export default function addExtension(path: string, extension: string): string { + const ext = extension.startsWith(".") ? extension : `.${extension}`; + + return path.endsWith(ext) ? path : `${path}${ext}`; +}