diff --git a/docs/documentation.toml b/docs/documentation.toml index f1cd41bc..6d3a54d0 100644 --- a/docs/documentation.toml +++ b/docs/documentation.toml @@ -257,6 +257,25 @@ description = "Retrieves the file's title." name = "frontmatter" description = "This modules exposes all the frontmatter variables of a file as variables." + + +[tp.hooks] +name = "hooks" +description = "This module exposes hooks that allow you to execute code when a Templater event occurs." + +[tp.hooks.functions.on_all_templates_executed] +name = "on_all_templates_executed" +description = """Hooks into when all actively running templates have finished executing. Most of the time this will be a single template, unless you are using `tp.file.include` or `tp.file.create_new`. + +Multiple invokations of this method will have their callback functions run in parallel.""" +definition = "tp.hooks.on_all_templates_executed(callback_function: () => any)" + +[[tp.hooks.functions.on_all_templates_executed.args]] +name = "callback_function" +description = "Callback function that will be executed when all actively running templates have finished executing." + + + [tp.obsidian] name = "obsidian" description = "This module exposes all the functions and classes from the obsidian API." diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 6c0b68c1..d5f9dbf2 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -11,6 +11,7 @@ - [tp.date](./internal-functions/internal-modules/date-module.md) - [tp.file](./internal-functions/internal-modules/file-module.md) - [tp.frontmatter](./internal-functions/internal-modules/frontmatter-module.md) + - [tp.hooks](./internal-functions/internal-modules/hooks-module.md) - [tp.obsidian](./internal-functions/internal-modules/obsidian-module.md) - [tp.system](./internal-functions/internal-modules/system-module.md) - [tp.web](./internal-functions/internal-modules/web-module.md) diff --git a/docs/src/internal-functions/internal-modules/hooks-module.md b/docs/src/internal-functions/internal-modules/hooks-module.md new file mode 100644 index 00000000..51e3f148 --- /dev/null +++ b/docs/src/internal-functions/internal-modules/hooks-module.md @@ -0,0 +1,52 @@ +# Hooks Module + +{{ tp.hooks.description }} + + + +## Documentation + +Function documentation is using a specific syntax. More information [here](../../syntax.md#function-documentation-syntax) + + +{%- for key, fn in tp.hooks.functions %} +### `{{ fn.definition }}` + +{{ fn.description }} + +{% if fn.args %} +##### Arguments + +{% for arg in fn.args %} +- `{{ arg.name }}`: {{ arg.description }} +{% endfor %} +{% endif %} + +{% if fn.example %} +##### Example + +``` +{{ fn.example }} +``` +{% endif %} +{%- endfor %} + +## Examples + +```javascript +// Update frontmatter after template finishes executing +<%* +tp.hooks.on_all_templates_executed(async () => { + const file = tp.file.find_tfile(tp.file.path(true)); + await app.fileManager.processFrontMatter(file, (frontmatter) => { + frontmatter["key"] = "value"; + }); +}); +%> +// Run a command from another plugin that modifies the current file, after Templater has updated the file +<%* +tp.hooks.on_all_templates_executed(() => { + app.commands.executeCommandById("obsidian-linter:lint-file"); +}); +-%> +``` diff --git a/docs/src/internal-functions/overview.md b/docs/src/internal-functions/overview.md index 31b76a14..4a71e867 100644 --- a/docs/src/internal-functions/overview.md +++ b/docs/src/internal-functions/overview.md @@ -6,6 +6,7 @@ The different internal variables and functions offered by [Templater](https://gi - [Date module](./internal-modules/date-module.md): `tp.date` - [File module](./internal-modules/file-module.md): `tp.file` - [Frontmatter module](./internal-modules/frontmatter-module.md): `tp.frontmatter` +- [Hooks module](./internal-modules/hooks-module.md): `tp.hooks` - [Obsidian module](./internal-modules/obsidian-module.md): `tp.obsidian` - [System module](./internal-modules/system-module.md): `tp.system` - [Web module](./internal-modules/web-module.md): `tp.web` diff --git a/src/core/Templater.ts b/src/core/Templater.ts index a9c42a48..591d3f2c 100644 --- a/src/core/Templater.ts +++ b/src/core/Templater.ts @@ -2,7 +2,6 @@ import { MarkdownPostProcessorContext, MarkdownView, normalizePath, - requireApiVersion, TAbstractFile, TFile, TFolder, @@ -42,6 +41,7 @@ export class Templater { public parser: Parser; public functions_generator: FunctionsGenerator; public current_functions_object: Record; + private templater_task_counter: number; constructor(private plugin: TemplaterPlugin) { this.functions_generator = new FunctionsGenerator(this.plugin); @@ -49,6 +49,7 @@ export class Templater { } async setup(): Promise { + this.templater_task_counter = 0; await this.parser.init(); await this.functions_generator.init(); this.plugin.registerMarkdownPostProcessor((el, ctx) => @@ -94,12 +95,25 @@ export class Templater { return content; } + private start_templater_task() { + this.templater_task_counter++; + } + + private async end_templater_task() { + this.templater_task_counter--; + if (this.templater_task_counter === 0) { + app.workspace.trigger("templater:all-templates-executed"); + await this.functions_generator.teardown(); + } + } + async create_new_note_from_template( template: TFile | string, folder?: TFolder, filename?: string, open_new_note = true ): Promise { + this.start_templater_task(); // TODO: Maybe there is an obsidian API function for that if (!folder) { const new_file_location = app.vault.getConfig("newFileLocation"); @@ -123,11 +137,20 @@ export class Templater { } // TODO: Change that, not stable atm - const created_note = await app.fileManager.createNewMarkdownFile( - folder, - filename ?? "Untitled" + const created_note = await errorWrapper( + async () => + app.fileManager.createNewMarkdownFile( + folder, + filename ?? "Untitled" + ), + "Couldn't create markdown file." ); + if (created_note == null) { + await this.end_templater_task(); + return; + } + let running_config: RunningConfig; let output_content: string; if (template instanceof TFile) { @@ -154,6 +177,7 @@ export class Templater { if (output_content == null) { await app.vault.delete(created_note); + await this.end_templater_task(); return; } @@ -184,16 +208,19 @@ export class Templater { }); } + await this.end_templater_task(); return created_note; } async append_template_to_active_file(template_file: TFile): Promise { + this.start_templater_task(); const active_view = app.workspace.getActiveViewOfType(MarkdownView); const active_editor = app.workspace.activeEditor; if (!active_editor || !active_editor.file || !active_editor.editor) { log_error( new TemplaterError("No active editor, can't append templates.") ); + await this.end_templater_task(); return; } const running_config = this.create_running_config( @@ -207,6 +234,7 @@ export class Templater { ); // errorWrapper failed if (output_content == null) { + await this.end_templater_task(); return; } @@ -214,6 +242,7 @@ export class Templater { const doc = editor.getDoc(); const oldSelections = doc.listSelections(); doc.replaceSelection(output_content); + await app.vault.modify(active_editor.file, editor.getValue()); app.workspace.trigger("templater:template-appended", { view: active_view, @@ -227,12 +256,14 @@ export class Templater { active_editor.file, true ); + await this.end_templater_task(); } async write_template_to_file( template_file: TFile, file: TFile ): Promise { + this.start_templater_task(); const active_editor = app.workspace.activeEditor; const running_config = this.create_running_config( template_file, @@ -260,6 +291,7 @@ export class Templater { file, true ); + await this.end_templater_task(); } overwrite_active_file_commands(): void { @@ -279,6 +311,7 @@ export class Templater { file: TFile, active_file = false ): Promise { + this.start_templater_task(); const running_config = this.create_running_config( file, file, @@ -290,6 +323,7 @@ export class Templater { ); // errorWrapper failed if (output_content == null) { + await this.end_templater_task(); return; } await app.vault.modify(file, output_content); @@ -301,6 +335,7 @@ export class Templater { file, true ); + await this.end_templater_task(); } async process_dynamic_templates( @@ -451,6 +486,7 @@ export class Templater { if (!file) { continue; } + this.start_templater_task(); const running_config = this.create_running_config( file, file, @@ -460,6 +496,7 @@ export class Templater { async () => this.read_and_parse_template(running_config), `Startup Template parsing error, aborting.` ); + await this.end_templater_task(); } } } diff --git a/src/core/functions/FunctionsGenerator.ts b/src/core/functions/FunctionsGenerator.ts index 28d81d46..0f18b094 100644 --- a/src/core/functions/FunctionsGenerator.ts +++ b/src/core/functions/FunctionsGenerator.ts @@ -23,6 +23,10 @@ export class FunctionsGenerator implements IGenerateObject { await this.internal_functions.init(); } + async teardown(): Promise { + await this.internal_functions.teardown(); + } + additional_functions(): Record { return { obsidian: obsidian_module, diff --git a/src/core/functions/internal_functions/InternalFunctions.ts b/src/core/functions/internal_functions/InternalFunctions.ts index f94b8243..6b39a216 100644 --- a/src/core/functions/internal_functions/InternalFunctions.ts +++ b/src/core/functions/internal_functions/InternalFunctions.ts @@ -4,6 +4,7 @@ import { InternalModule } from "./InternalModule"; import { InternalModuleDate } from "./date/InternalModuleDate"; import { InternalModuleFile } from "./file/InternalModuleFile"; import { InternalModuleWeb } from "./web/InternalModuleWeb"; +import { InternalModuleHooks } from "./hooks/InternalModuleHooks"; import { InternalModuleFrontmatter } from "./frontmatter/InternalModuleFrontmatter"; import { InternalModuleSystem } from "./system/InternalModuleSystem"; import { RunningConfig } from "core/Templater"; @@ -17,6 +18,7 @@ export class InternalFunctions implements IGenerateObject { 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)); } @@ -27,6 +29,12 @@ export class InternalFunctions implements IGenerateObject { } } + async teardown(): Promise { + for (const mod of this.modules_array) { + await mod.teardown(); + } + } + async generate_object( config: RunningConfig ): Promise> { diff --git a/src/core/functions/internal_functions/InternalModule.ts b/src/core/functions/internal_functions/InternalModule.ts index 66122ba0..0592a03c 100644 --- a/src/core/functions/internal_functions/InternalModule.ts +++ b/src/core/functions/internal_functions/InternalModule.ts @@ -18,6 +18,7 @@ export abstract class InternalModule implements IGenerateObject { abstract create_static_templates(): Promise; abstract create_dynamic_templates(): Promise; + abstract teardown(): Promise; async init(): Promise { await this.create_static_templates(); diff --git a/src/core/functions/internal_functions/config/InternalModuleConfig.ts b/src/core/functions/internal_functions/config/InternalModuleConfig.ts index eda3b964..da307f5e 100644 --- a/src/core/functions/internal_functions/config/InternalModuleConfig.ts +++ b/src/core/functions/internal_functions/config/InternalModuleConfig.ts @@ -9,6 +9,8 @@ export class InternalModuleConfig extends InternalModule { async create_dynamic_templates(): Promise {} + async teardown(): Promise {} + async generate_object( config: RunningConfig ): Promise> { diff --git a/src/core/functions/internal_functions/date/InternalModuleDate.ts b/src/core/functions/internal_functions/date/InternalModuleDate.ts index fceeae95..324fccb0 100644 --- a/src/core/functions/internal_functions/date/InternalModuleDate.ts +++ b/src/core/functions/internal_functions/date/InternalModuleDate.ts @@ -14,6 +14,8 @@ export class InternalModuleDate extends InternalModule { async create_dynamic_templates(): Promise {} + async teardown(): Promise {} + generate_now(): ( format?: string, offset?: number | string, diff --git a/src/core/functions/internal_functions/file/InternalModuleFile.ts b/src/core/functions/internal_functions/file/InternalModuleFile.ts index 8680ff6c..3e5b0888 100644 --- a/src/core/functions/internal_functions/file/InternalModuleFile.ts +++ b/src/core/functions/internal_functions/file/InternalModuleFile.ts @@ -53,6 +53,8 @@ export class InternalModuleFile extends InternalModule { this.dynamic_functions.set("title", this.generate_title()); } + async teardown(): Promise {} + async generate_content(): Promise { return await app.vault.read(this.config.target_file); } diff --git a/src/core/functions/internal_functions/frontmatter/InternalModuleFrontmatter.ts b/src/core/functions/internal_functions/frontmatter/InternalModuleFrontmatter.ts index 21901029..6764d1cc 100644 --- a/src/core/functions/internal_functions/frontmatter/InternalModuleFrontmatter.ts +++ b/src/core/functions/internal_functions/frontmatter/InternalModuleFrontmatter.ts @@ -12,4 +12,6 @@ export class InternalModuleFrontmatter extends InternalModule { Object.entries(cache?.frontmatter || {}) ); } + + async teardown(): Promise {} } diff --git a/src/core/functions/internal_functions/hooks/InternalModuleHooks.ts b/src/core/functions/internal_functions/hooks/InternalModuleHooks.ts new file mode 100644 index 00000000..19addca9 --- /dev/null +++ b/src/core/functions/internal_functions/hooks/InternalModuleHooks.ts @@ -0,0 +1,38 @@ +import { EventRef } from "obsidian"; +import { ModuleName } from "editor/TpDocumentation"; +import { InternalModule } from "../InternalModule"; + +export class InternalModuleHooks extends InternalModule { + public name: ModuleName = "hooks"; + private event_refs: EventRef[] = []; + + async create_static_templates(): Promise { + this.static_functions.set( + "on_all_templates_executed", + this.generate_on_all_templates_executed() + ); + } + + async create_dynamic_templates(): Promise {} + + async teardown(): Promise { + this.event_refs.forEach((eventRef) => { + eventRef.e.offref(eventRef); + }); + this.event_refs = []; + } + + generate_on_all_templates_executed(): ( + callback_function: () => unknown + ) => void { + return (callback_function) => { + const event_ref = app.workspace.on( + "templater:all-templates-executed", + () => callback_function() + ); + if (event_ref) { + this.event_refs.push(event_ref); + } + }; + } +} diff --git a/src/core/functions/internal_functions/system/InternalModuleSystem.ts b/src/core/functions/internal_functions/system/InternalModuleSystem.ts index 0c3b73e5..297669a2 100644 --- a/src/core/functions/internal_functions/system/InternalModuleSystem.ts +++ b/src/core/functions/internal_functions/system/InternalModuleSystem.ts @@ -15,6 +15,8 @@ export class InternalModuleSystem extends InternalModule { async create_dynamic_templates(): Promise {} + async teardown(): Promise {} + generate_clipboard(): () => Promise { return async () => { return await navigator.clipboard.readText(); diff --git a/src/core/functions/internal_functions/web/InternalModuleWeb.ts b/src/core/functions/internal_functions/web/InternalModuleWeb.ts index 43ea7578..0aa50ab7 100644 --- a/src/core/functions/internal_functions/web/InternalModuleWeb.ts +++ b/src/core/functions/internal_functions/web/InternalModuleWeb.ts @@ -15,6 +15,8 @@ export class InternalModuleWeb extends InternalModule { async create_dynamic_templates(): Promise {} + async teardown(): Promise {} + async getRequest(url: string): Promise { try { const response = await fetch(url); diff --git a/src/editor/TpDocumentation.ts b/src/editor/TpDocumentation.ts index 31689a46..bcc2127c 100644 --- a/src/editor/TpDocumentation.ts +++ b/src/editor/TpDocumentation.ts @@ -8,12 +8,13 @@ const module_names = [ "date", "file", "frontmatter", + "hooks", "obsidian", "system", "user", "web", ] as const; -export type ModuleName = typeof module_names[number]; +export type ModuleName = (typeof module_names)[number]; const module_names_checker: Set = new Set(module_names); export function is_module_name(x: unknown): x is ModuleName { diff --git a/src/main.ts b/src/main.ts index f3253674..331203ea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -56,6 +56,11 @@ export default class TemplaterPlugin extends Plugin { }); } + async unload(): Promise { + // Failsafe in case teardown doesn't happen immediately after template execution + await this.templater.functions_generator.teardown(); + } + async save_settings(): Promise { await this.saveData(this.settings); } diff --git a/src/types.ts b/src/types.ts index f0571b47..974064b3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,12 +9,14 @@ declare module "obsidian" { getConfig: (key: string) => string; exists: (path: string) => Promise; } + interface FileManager { createNewMarkdownFile: ( folder: TFolder | undefined, filename: string ) => Promise; } + interface DataAdapter { basePath: string; fs: { @@ -22,6 +24,17 @@ declare module "obsidian" { }; } + interface Workspace { + on( + name: "templater:all-templates-executed", + callback: () => unknown + ): EventRef; + } + + interface EventRef { + e: Events; + } + interface MarkdownSubView { applyFoldInfo(foldInfo: FoldInfo): void; getFoldInfo(): FoldInfo | null;