From 37315f73202bd6f669c60d19bb656386bae1a58a Mon Sep 17 00:00:00 2001
From: Christian Bager Bach Houmann <christian@bagerbach.com>
Date: Mon, 14 Nov 2022 12:07:17 +0100
Subject: [PATCH] refactor: typesafe template engine

---
 src/TemplateEngine.ts | 122 +++++++++++++++++++++++++-----------------
 1 file changed, 72 insertions(+), 50 deletions(-)

diff --git a/src/TemplateEngine.ts b/src/TemplateEngine.ts
index cc9e0f2..fc10d39 100644
--- a/src/TemplateEngine.ts
+++ b/src/TemplateEngine.ts
@@ -9,43 +9,58 @@ 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")
@@ -54,28 +69,33 @@ export function NoteTemplateEngine(template: string, episode: Episode) {
 			}
 
 			return htmlToMarkdown(episode.description)
-		},
-		"url": episode.url,
-		"date": (format?: string) => episode.episodeDate ?
+		});
+	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 ?? "");
+
+	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) {
@@ -85,10 +105,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) {
-- 
GitLab