diff --git a/docs/documentation.toml b/docs/documentation.toml index c8d6181e..7dabe76b 100644 --- a/docs/documentation.toml +++ b/docs/documentation.toml @@ -342,7 +342,7 @@ example = "<% tp.file.folder(true) %>" [tp.file.functions.include] name = "include" -description = "Includes the file's link content. Templates in the included content will be resolved." +description = "Includes the file's link content. Templates in the included content will be resolved. Any frontmatter in the included file will be merged with the parent file." definition = "tp.file.include(include_link: string ⎮ TFile)" [[tp.file.functions.include.args]] diff --git a/src/core/Templater.ts b/src/core/Templater.ts index 4bcaed84..025b0ea2 100644 --- a/src/core/Templater.ts +++ b/src/core/Templater.ts @@ -41,13 +41,15 @@ export type RunningConfig = { target_file: TFile; run_mode: RunMode; active_file?: TFile | null; + frontmatter: Record; }; export class Templater { public parser: Parser; public functions_generator: FunctionsGenerator; - public current_functions_object: Record; public files_with_pending_templates: Set; + public current_functions_object: Record; + private functions_objects: Array>; constructor(private plugin: TemplaterPlugin) { this.functions_generator = new FunctionsGenerator(this.plugin); @@ -58,15 +60,16 @@ export class Templater { this.files_with_pending_templates = new Set(); await this.parser.init(); await this.functions_generator.init(); + this.functions_objects = []; this.plugin.registerMarkdownPostProcessor((el, ctx) => - this.process_dynamic_templates(el, ctx) + this.process_dynamic_templates(el, ctx), ); } create_running_config( template_file: TFile | undefined, target_file: TFile, - run_mode: RunMode + run_mode: RunMode, ): RunningConfig { const active_file = get_active_file(this.plugin.app); @@ -75,29 +78,39 @@ export class Templater { target_file: target_file, run_mode: run_mode, active_file: active_file, + frontmatter: {}, }; } async read_and_parse_template(config: RunningConfig): Promise { const template_content = await this.plugin.app.vault.read( - config.template_file as TFile + config.template_file as TFile, ); return this.parse_template(config, template_content); } async parse_template( config: RunningConfig, - template_content: string + template_content: string, ): Promise { const functions_object = await this.functions_generator.generate_object( config, - FunctionsMode.USER_INTERNAL + FunctionsMode.USER_INTERNAL, ); - this.current_functions_object = functions_object; + this.push_functions_object(functions_object); const content = await this.parser.parse_commands( template_content, - functions_object + functions_object, ); + this.pop_functions_object(); + + // Merge the frontmatter of any included templates into the root template frontmatter after parsing + // so that included templates frontmatter overrides root template frontmatter, + // and any functions in the root frontmatter have been executed + const frontmatter = get_frontmatter_and_content(content).frontmatter; + merge_objects(frontmatter, config.frontmatter); + config.frontmatter = frontmatter; + return content; } @@ -109,7 +122,7 @@ export class Templater { this.files_with_pending_templates.delete(path); if (this.files_with_pending_templates.size === 0) { this.plugin.app.workspace.trigger( - "templater:all-templates-executed" + "templater:all-templates-executed", ); await this.functions_generator.teardown(); } @@ -119,7 +132,7 @@ export class Templater { template: TFile | string, folder?: TFolder | string, filename?: string, - open_new_note = true + open_new_note = true, ): Promise { // TODO: Maybe there is an obsidian API function for that if (!folder) { @@ -150,13 +163,13 @@ export class Templater { const folderPath = folder instanceof TFolder ? folder.path : folder; const path = this.plugin.app.vault.getAvailablePath( normalizePath(`${folderPath ?? ""}/${filename || "Untitled"}`), - extension + extension, ); const folder_path = get_folder_path_from_file_path(path); if ( folder_path && !this.plugin.app.vault.getAbstractFileByPathInsensitive( - folder_path + folder_path, ) ) { await this.plugin.app.vault.createFolder(folder_path); @@ -176,21 +189,21 @@ export class Templater { running_config = this.create_running_config( template, created_note, - RunMode.CreateNewFromTemplate + RunMode.CreateNewFromTemplate, ); output_content = await errorWrapper( async () => this.read_and_parse_template(running_config), - "Template parsing error, aborting." + "Template parsing error, aborting.", ); } else { running_config = this.create_running_config( undefined, created_note, - RunMode.CreateNewFromTemplate + RunMode.CreateNewFromTemplate, ); output_content = await errorWrapper( async () => this.parse_template(running_config, template), - "Template parsing error, aborting." + "Template parsing error, aborting.", ); } @@ -200,6 +213,20 @@ export class Templater { return; } + // Update the frontmatter of output_content with the merged frontmatter + // from all included templates + const output_content_body = + get_frontmatter_and_content(output_content).content; + const frontmatter = running_config.frontmatter; + + if (Object.keys(frontmatter).length !== 0) { + output_content = + "---\n" + + stringifyYaml(frontmatter) + + "---\n" + + output_content_body; + } + await this.plugin.app.vault.modify(created_note, output_content); this.plugin.app.workspace.trigger("templater:new-note-from-template", { @@ -219,7 +246,7 @@ export class Templater { await this.plugin.editor_handler.jump_to_next_cursor_location( created_note, - true + true, ); active_leaf.setEphemeralState({ @@ -237,7 +264,7 @@ export class Templater { const active_editor = this.plugin.app.workspace.activeEditor; if (!active_editor || !active_editor.file || !active_editor.editor) { log_error( - new TemplaterError("No active editor, can't append templates.") + new TemplaterError("No active editor, can't append templates."), ); return; } @@ -246,11 +273,11 @@ export class Templater { const running_config = this.create_running_config( template_file, active_editor.file, - RunMode.AppendActiveFile + RunMode.AppendActiveFile, ); const output_content = await errorWrapper( async () => this.read_and_parse_template(running_config), - "Template parsing error, aborting." + "Template parsing error, aborting.", ); // errorWrapper failed if (output_content == null) { @@ -258,8 +285,8 @@ export class Templater { return; } - const { content, frontmatter } = - get_frontmatter_and_content(output_content); + const content = get_frontmatter_and_content(output_content).content; + const frontmatter = running_config.frontmatter; const editor = active_editor.editor; const doc = editor.getDoc(); @@ -291,14 +318,14 @@ export class Templater { await this.plugin.editor_handler.jump_to_next_cursor_location( active_editor.file, - true + true, ); await this.end_templater_task(path); } async write_template_to_file( template_file: TFile, - file: TFile + file: TFile, ): Promise { const { path } = file; this.start_templater_task(path); @@ -309,11 +336,11 @@ export class Templater { const running_config = this.create_running_config( template_file, file, - RunMode.OverwriteFile + RunMode.OverwriteFile, ); let output_content = await errorWrapper( async () => this.read_and_parse_template(running_config), - "Template parsing error, aborting." + "Template parsing error, aborting.", ); // errorWrapper failed if (output_content == null) { @@ -321,10 +348,9 @@ export class Templater { return; } - const { - content: output_content_body, - frontmatter: output_frontmatter, - } = get_frontmatter_and_content(output_content); + const output_content_body = + get_frontmatter_and_content(output_content).content; + const output_frontmatter = running_config.frontmatter; if ( active_file?.path === file.path && active_editor && @@ -333,7 +359,7 @@ export class Templater { ) { let result = ""; const { content, frontmatter } = get_frontmatter_and_content( - active_editor.editor.getValue() + active_editor.editor.getValue(), ); merge_objects(frontmatter, output_frontmatter); if (Object.keys(frontmatter).length > 0) { @@ -370,7 +396,7 @@ export class Templater { }); await this.plugin.editor_handler.jump_to_next_cursor_location( file, - true + true, ); await this.end_templater_task(path); } @@ -380,8 +406,8 @@ export class Templater { if (!active_editor || !active_editor.file) { log_error( new TemplaterError( - "Active editor is null, can't overwrite content" - ) + "Active editor is null, can't overwrite content", + ), ); return; } @@ -390,18 +416,18 @@ export class Templater { async overwrite_file_commands( file: TFile, - active_file = false + active_file = false, ): Promise { const { path } = file; this.start_templater_task(path); const running_config = this.create_running_config( file, file, - active_file ? RunMode.OverwriteActiveFile : RunMode.OverwriteFile + active_file ? RunMode.OverwriteActiveFile : RunMode.OverwriteFile, ); const output_content = await errorWrapper( async () => this.read_and_parse_template(running_config), - "Template parsing error, aborting." + "Template parsing error, aborting.", ); // errorWrapper failed if (output_content == null) { @@ -415,14 +441,14 @@ export class Templater { }); await this.plugin.editor_handler.jump_to_next_cursor_location( file, - true + true, ); await this.end_templater_task(path); } async process_dynamic_templates( el: HTMLElement, - ctx: MarkdownPostProcessorContext + ctx: MarkdownPostProcessorContext, ): Promise { const dynamic_command_regex = generate_dynamic_command_regex(); @@ -430,71 +456,97 @@ export class Templater { let node; let pass = false; let functions_object: Record; - while ((node = walker.nextNode())) { - let content = node.nodeValue; - if (content !== null) { - let match = dynamic_command_regex.exec(content); - if (match !== null) { - const file = - this.plugin.app.metadataCache.getFirstLinkpathDest( - "", - ctx.sourcePath - ); - if (!file || !(file instanceof TFile)) { - return; - } - if (!pass) { - pass = true; - const config = this.create_running_config( - file, - file, - RunMode.DynamicProcessor - ); - functions_object = - await this.functions_generator.generate_object( - config, - FunctionsMode.USER_INTERNAL + try { + while ((node = walker.nextNode())) { + let content = node.nodeValue; + if (content !== null) { + let match = dynamic_command_regex.exec(content); + if (match !== null) { + const file = + this.plugin.app.metadataCache.getFirstLinkpathDest( + "", + ctx.sourcePath, ); - this.current_functions_object = functions_object; + if (!file || !(file instanceof TFile)) { + return; + } + if (!pass) { + pass = true; + const config = this.create_running_config( + file, + file, + RunMode.DynamicProcessor, + ); + functions_object = + await this.functions_generator.generate_object( + config, + FunctionsMode.USER_INTERNAL, + ); + this.push_functions_object(functions_object); + } } - } - while (match != null) { - // Not the most efficient way to exclude the '+' from the command but I couldn't find something better - const complete_command = match[1] + match[2]; - const command_output: string = await errorWrapper( - async () => { - return await this.parser.parse_commands( - complete_command, - functions_object + while (match != null) { + // Not the most efficient way to exclude the '+' from the command but I couldn't find something better + const complete_command = match[1] + match[2]; + const command_output: string = await errorWrapper( + async () => { + return await this.parser.parse_commands( + complete_command, + functions_object, + ); + }, + `Command Parsing error in dynamic command '${complete_command}'`, + ); + if (command_output == null) { + throw new TemplaterError( + "Aborting dynamic command processing", ); - }, - `Command Parsing error in dynamic command '${complete_command}'` - ); - if (command_output == null) { - return; + } + const start = + dynamic_command_regex.lastIndex - match[0].length; + const end = dynamic_command_regex.lastIndex; + content = + content.substring(0, start) + + command_output + + content.substring(end); + + dynamic_command_regex.lastIndex += + command_output.length - match[0].length; + match = dynamic_command_regex.exec(content); } - const start = - dynamic_command_regex.lastIndex - match[0].length; - const end = dynamic_command_regex.lastIndex; - content = - content.substring(0, start) + - command_output + - content.substring(end); - - dynamic_command_regex.lastIndex += - command_output.length - match[0].length; - match = dynamic_command_regex.exec(content); + node.nodeValue = content; } - node.nodeValue = content; } + } finally { + if (pass) { + this.pop_functions_object(); + } + } + } + + private push_functions_object(obj: Record): void { + this.functions_objects.push(obj); + this.current_functions_object = obj; + } + + private pop_functions_object(): void { + this.functions_objects.pop(); + this.current_functions_object = this.get_current_functions_object(); + } + + get_current_functions_object(): Record { + const stack = this.functions_objects; + if (!stack || stack.length === 0) { + return {}; } + return stack[stack.length - 1]; } get_new_file_template_for_folder(folder: TFolder): string | undefined { do { const match = this.plugin.settings.folder_templates.find( - (e) => e.folder == folder.path + (e) => e.folder == folder.path, ); if (match && match.template) { @@ -519,7 +571,7 @@ export class Templater { static async on_file_creation( templater: Templater, app: App, - file: TAbstractFile + file: TAbstractFile, ): Promise { if (!(file instanceof TFile) || file.extension !== "md") { return; @@ -527,7 +579,7 @@ export class Templater { // Avoids template replacement when syncing template files const template_folder = normalizePath( - templater.plugin.settings.templates_folder + templater.plugin.settings.templates_folder, ); if (file.path.includes(template_folder) && template_folder !== "/") { return; @@ -560,7 +612,7 @@ export class Templater { async (): Promise => { return resolve_tfile(app, folder_template_match); }, - `Couldn't find template ${folder_template_match}` + `Couldn't find template ${folder_template_match}`, ); // errorWrapper failed if (template_file == null) { @@ -580,7 +632,7 @@ export class Templater { async (): Promise => { return resolve_tfile(app, file_template_match); }, - `Couldn't find template ${file_template_match}` + `Couldn't find template ${file_template_match}`, ); // errorWrapper failed if (template_file == null) { @@ -594,7 +646,7 @@ export class Templater { await templater.overwrite_file_commands(file); } else { console.log( - `Templater skipped parsing ${file.path} because file size exceeds ${SIZE_LIMIT}` + `Templater skipped parsing ${file.path} because file size exceeds ${SIZE_LIMIT}`, ); } } @@ -607,7 +659,7 @@ export class Templater { } const file = errorWrapperSync( () => resolve_tfile(this.plugin.app, template), - `Couldn't find startup template "${template}"` + `Couldn't find startup template "${template}"`, ); if (!file) { continue; @@ -617,11 +669,11 @@ export class Templater { const running_config = this.create_running_config( file, file, - RunMode.StartupTemplate + RunMode.StartupTemplate, ); await errorWrapper( async () => this.read_and_parse_template(running_config), - `Startup Template parsing error, aborting.` + `Startup Template parsing error, aborting.`, ); await this.end_templater_task(path); } diff --git a/src/core/functions/internal_functions/InternalFunctions.ts b/src/core/functions/internal_functions/InternalFunctions.ts index 6b39a216..2ba7b2ce 100644 --- a/src/core/functions/internal_functions/InternalFunctions.ts +++ b/src/core/functions/internal_functions/InternalFunctions.ts @@ -11,40 +11,45 @@ import { RunningConfig } from "core/Templater"; import { InternalModuleConfig } from "./config/InternalModuleConfig"; export class InternalFunctions implements IGenerateObject { - private modules_array: Array = []; + private modules: Array> = []; constructor(protected plugin: TemplaterPlugin) { - this.modules_array.push(new InternalModuleDate(this.plugin)); - this.modules_array.push(new InternalModuleFile(this.plugin)); - this.modules_array.push(new InternalModuleWeb(this.plugin)); - this.modules_array.push(new InternalModuleFrontmatter(this.plugin)); - this.modules_array.push(new InternalModuleHooks(this.plugin)); - this.modules_array.push(new InternalModuleSystem(this.plugin)); - this.modules_array.push(new InternalModuleConfig(this.plugin)); + this.modules = []; } - async init(): Promise { - for (const mod of this.modules_array) { - await mod.init(); - } - } + async init(): Promise {} async teardown(): Promise { - for (const mod of this.modules_array) { - await mod.teardown(); + for (const module_array of this.modules) { + for (const mod of module_array) { + await mod.teardown(); + } } + this.modules = []; } async generate_object( config: RunningConfig ): Promise> { + const modules_array = [ + new InternalModuleDate(this.plugin), + new InternalModuleFile(this.plugin), + new InternalModuleWeb(this.plugin), + new InternalModuleFrontmatter(this.plugin), + new InternalModuleHooks(this.plugin), + new InternalModuleSystem(this.plugin), + new InternalModuleConfig(this.plugin), + ]; const internal_functions_object: { [key: string]: unknown } = {}; - for (const mod of this.modules_array) { + for (const mod of modules_array) { + await mod.init(); internal_functions_object[mod.getName()] = await mod.generate_object(config); } + this.modules.push(modules_array); + return internal_functions_object; } } diff --git a/src/core/functions/internal_functions/file/InternalModuleFile.ts b/src/core/functions/internal_functions/file/InternalModuleFile.ts index 9e0b03db..1151d651 100644 --- a/src/core/functions/internal_functions/file/InternalModuleFile.ts +++ b/src/core/functions/internal_functions/file/InternalModuleFile.ts @@ -13,6 +13,7 @@ import { } from "obsidian"; import { TemplaterError } from "utils/Error"; import { ModuleName } from "editor/TpDocumentation"; +import { get_frontmatter_and_content, merge_objects } from "utils/Utils"; export const DEPTH_LIMIT = 10; @@ -214,10 +215,15 @@ export class InternalModuleFile extends InternalModule { const parsed_content = await this.plugin.templater.parser.parse_commands( inc_file_content, - this.plugin.templater.current_functions_object + this.plugin.templater.get_current_functions_object() ); + const { frontmatter, content } = + get_frontmatter_and_content(parsed_content); + + merge_objects(this.config.frontmatter, frontmatter); + this.include_depth -= 1; - return parsed_content; + return content; } catch (e) { this.include_depth -= 1; throw e; diff --git a/tests/main.test.ts b/tests/main.test.ts index 2e5b5a19..04dac6a0 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -194,6 +194,7 @@ export default class TestTemplaterPlugin extends Plugin { template_file: this.template_file, target_file: this.target_file, run_mode: RunMode.OverwriteFile, + frontmatter: {}, }; const content = await this.plugin.templater.read_and_parse_template( running_config