## Overview Templater is a template plugin for Obsidian.md that defines a templating language for inserting variables and function results into notes. It executes JavaScript code to manipulate variables and automate tasks. **Engine**: rusty_engine (custom templating engine) **License**: GNU AGPLv3 ## Core Syntax ### Command Structure All commands use opening `<%` and closing `%>` tags. **Basic Interpolation**: `<% expression %>` ```markdown <% tp.date.now() %> ``` **JavaScript Execution**: `<%* code %>` ```javascript <%* let value = await tp.system.prompt("Enter value"); tR += value; // Output to template result %> ``` **Dynamic Commands** (preview mode only): `<%+ expression %>` ```markdown Last modified: <%+ tp.file.last_modified_date() %> ``` _Note: Dynamic commands are deprecated; use Dataview plugin instead._ ### Whitespace Control - `<%_` - Trim all whitespace before command - `_%>` - Trim all whitespace after command - `<%-` - Trim one newline before command - `-%>` - Trim one newline after command ```markdown <%_ if (tp.file.title == "MyFile") { -%> Content here <%_ } -%> ``` ### Function Invocation Functions use dot notation under the `tp` object: ```javascript tp.<module>.<function>(arg1, arg2, ...) ``` **Argument Types**: - `string`: Quoted values (`"text"` or `'text'`) - `number`: Integers (`15`, `-5`) - `boolean`: `true` or `false` (lowercase) **Documentation Notation** (for reference only): ```typescript tp.function(arg?: type = default, arg2: type1|type2) ``` - `?` = optional argument - `= value` = default value - `|` = multiple type options ## Internal Modules ### tp.date Date manipulation functions using moment.js format strings. **tp.date.now(format?, offset?, reference?, reference_format?)** ```javascript <% tp.date.now() %> // 2024-01-15 <% tp.date.now("Do MMMM YYYY") %> // 15th January 2024 <% tp.date.now("YYYY-MM-DD", -7) %> // Last week <% tp.date.now("YYYY-MM-DD", "P-1M") %> // Last month (ISO 8601) <% tp.date.now("YYYY-MM-DD", 1, tp.file.title, "YYYY-MM-DD") %> // Tomorrow from file title date ``` **tp.date.tomorrow(format?)** ```javascript <% tp.date.tomorrow("YYYY-MM-DD") %> ``` **tp.date.yesterday(format?)** ```javascript <% tp.date.yesterday("YYYY-MM-DD") %> ``` **tp.date.weekday(format?, weekday, reference?, reference_format?)** ```javascript <% tp.date.weekday("YYYY-MM-DD", 0) %> // This week's Monday <% tp.date.weekday("YYYY-MM-DD", 7) %> // Next Monday <% tp.date.weekday("YYYY-MM-DD", -7, tp.file.title, "YYYY-MM-DD") %> // Previous Monday from reference ``` **Direct moment.js access**: ```javascript <% moment(tp.file.title, "YYYY-MM-DD").endOf("month").format("YYYY-MM-DD") %> ``` ### tp.file File operations and metadata. **Properties**: - `tp.file.content` - File contents (read-only string) - `tp.file.title` - File basename without extension - `tp.file.path(relative?)` - File path (absolute if `relative=false`) - `tp.file.tags` - Array of file tags **tp.file.creation_date(format?)** ```javascript <% tp.file.creation_date() %> // 2024-01-15 14:30 <% tp.file.creation_date("dddd Do MMMM YYYY HH:mm") %> ``` **tp.file.last_modified_date(format?)** ```javascript <% tp.file.last_modified_date() %> <% tp.file.last_modified_date("YYYY-MM-DD HH:mm:ss") %> ``` **tp.file.cursor(order?)** Set cursor position after template insertion. Same order = multi-cursor. ```javascript <% tp.file.cursor() %> <% tp.file.cursor(1) %>text<% tp.file.cursor(1) %> // Multi-cursor ``` **tp.file.cursor_append(content)** Append content after active cursor. ```javascript <% tp.file.cursor_append("Some text") %> ``` **tp.file.exists(filepath)** (async) ```javascript <%* if (await tp.file.exists("path/to/file.md")) { %> File exists <%* } %> ``` **tp.file.find_tfile(filename)** Returns TFile object for given filename. ```javascript <%* let tfile = tp.file.find_tfile("MyNote") %> <%* const currentFile = tp.file.find_tfile(tp.file.path(true)) %> // Get current file ``` **tp.file.folder(relative?)** ```javascript <% tp.file.folder() %> // Folder name <% tp.file.folder(true) %> // Path/To/Folder ``` **tp.file.include(include_link)** (async) Include and resolve another file's content. ```javascript <% await tp.file.include("[[Template1]]") %> <% await tp.file.include(tp.file.find_tfile("MyFile")) %> <% await tp.file.include("[[MyFile#Section1]]") %> // Section <% await tp.file.include("[[MyFile#^block1]]") %> // Block ``` **tp.file.create_new(template, filename?, open_new?, folder?)** (async) ```javascript <%* await tp.file.create_new("Content", "MyFile") %> <%* await tp.file.create_new(tp.file.find_tfile("Template"), "MyFile") %> <%* await tp.file.create_new("Content", "MyFile", true) %> // Open <%* await tp.file.create_new("Content", "MyFile", false, tp.file.folder(true)) %> <%* await tp.file.create_new("Content", "MyFile", false, "Path/To/Folder") %> ``` **tp.file.move(new_path, file_to_move?)** ```javascript <% await tp.file.move("/Notes/MyNote") %> // Move current file ``` **tp.file.rename(new_title)** ```javascript <% await tp.file.rename("New Title") %> ``` **tp.file.selection()** Returns selected text in editor. ```javascript <% tp.file.selection() %> ``` ### tp.frontmatter Access frontmatter variables. ```yaml --- alias: myfile note type: seedling tags: [tag1, tag2] --- ``` ```javascript <% tp.frontmatter.alias %> // myfile <% tp.frontmatter["note type"] %> // For keys with spaces <% tp.frontmatter.tags.join(", ") %> // Array methods work ``` **Note**: `tp.frontmatter` may return empty/undefined during file creation. Use `tp.hooks.on_all_templates_executed()` to modify frontmatter after template execution. ### tp.system User interaction functions. **tp.system.clipboard()** ```javascript <% tp.system.clipboard() %> ``` **tp.system.prompt(prompt_text?, default_value?, throw_on_cancel?, multiline?)** (async) ```javascript <% await tp.system.prompt("Enter value") %> <% await tp.system.prompt("Mood?", "happy") %> <% await tp.system.prompt("Notes", null, false, true) %> // Multiline <%* let value = await tp.system.prompt("Enter value"); %> Value: <% value %> ``` **tp.system.suggester(text_items, items, throw_on_cancel?, placeholder?, limit?)** (async) ```javascript <% await tp.system.suggester(["Happy", "Sad"], ["😊", "😢"]) %> <% await tp.system.suggester(item => item, ["Option1", "Option2"]) %> // Files suggester [[<% (await tp.system.suggester(item => item.basename, tp.app.vault.getMarkdownFiles())).basename %>]] // Tags suggester <% await tp.system.suggester(item => item, Object.keys(tp.app.metadataCache.getTags()).map(x => x.replace("#", ""))) %> <%* let selected = await tp.system.suggester(["A", "B"], [1, 2]); %> Selected: <% selected %> ``` **tp.system.multi_suggester(text_items, items, throw_on_cancel?, title?, limit?)** (async) Multiple selection variant. ```javascript <% await tp.system.multi_suggester(["A", "B", "C"], ["A", "B", "C"]) %> <% (await tp.system.multi_suggester(item => item.basename, tp.app.vault.getMarkdownFiles())).map(f => `[[${f.basename}]]`) %> ``` ### tp.web Web request functions (all async). **tp.web.daily_quote()** ```javascript <% await tp.web.daily_quote() %> ``` **tp.web.random_picture(size?, query?, include_size?)** ```javascript <% await tp.web.random_picture() %> <% await tp.web.random_picture("200x200") %> <% await tp.web.random_picture("200x200", "landscape,water") %> ``` **tp.web.request(url, path?)** ```javascript <% await tp.web.request("https://api.example.com/data") %> <% await tp.web.request("https://api.example.com/todos", "0.title") %> // Extract path ``` ### tp.config Templater running configuration (for scripts). **Properties**: - `tp.config.active_file` - Active file when Templater launched (if exists) - `tp.config.run_mode` - How Templater was launched (RunMode enum) - `tp.config.target_file` - TFile where template will be inserted - `tp.config.template_file` - TFile of the template ### tp.hooks Execute code on Templater events. **tp.hooks.on_all_templates_executed(callback_function)** Runs after all templates finish executing. Multiple calls run in parallel. ```javascript <%* tp.hooks.on_all_templates_executed(async () => { const file = tp.file.find_tfile(tp.file.path(true)); await tp.app.fileManager.processFrontMatter(file, (frontmatter) => { frontmatter["key"] = "value"; }); }); %> <%* tp.hooks.on_all_templates_executed(() => { tp.app.commands.executeCommandById("obsidian-linter:lint-file"); }); %> ``` ### tp.app Exposes Obsidian app instance. Use instead of global `app`. ```javascript // Get all folders <% tp.app.vault.getAllLoadedFiles() .filter(x => x instanceof tp.obsidian.TFolder) .map(x => x.name) %> // Update frontmatter <%* const file = tp.file.find_tfile("path/to/file"); await tp.app.fileManager.processFrontMatter(file, (fm) => { fm["key"] = "value"; }); %> // Read file content <%* const file = tp.file.find_tfile(tp.file.path(true)); const content = await app.vault.read(file); %> ``` ### tp.obsidian Exposes Obsidian API classes and functions. ```javascript // Get all folders using TFolder class <% tp.app.vault.getAllLoadedFiles() .filter(x => x instanceof tp.obsidian.TFolder) .map(x => x.name) %> // Normalize path <% tp.obsidian.normalizePath("Path/to/file.md") %> // HTML to markdown <% tp.obsidian.htmlToMarkdown("<h1>Title</h1><p>Text</p>") %> // HTTP request <%* const response = await tp.obsidian.requestUrl("https://api.example.com/data"); tR += response.json.title; %> ``` ## User Functions ### Script User Functions JavaScript files in configured script folder become functions under `tp.user`. **Setup**: Settings → Script folder location **Script file** (`Scripts/my_script.js`): ```javascript function myFunction(msg) { return `Message: ${msg}`; } module.exports = myFunction; // Or export object with multiple functions module.exports = { add: (a, b) => a + b, subtract: (a, b) => a - b, }; ``` **Usage**: ```javascript <% tp.user.my_script("Hello") %> <% tp.user.my_script.add(5, 3) %> // If exporting object // Pass tp object to access Templater API <% tp.user.my_script(tp) %> ``` **Script with tp access**: ```javascript function myFunction(tp, arg) { return `File: ${tp.file.title}, Arg: ${arg}`; } module.exports = myFunction; ``` **TSDoc support** for intellisense: ```javascript /** * Calculates sum of two numbers * @param {number} a - First number * @param {number} b - Second number * @returns {number} Sum of a and b */ function add(a, b) { return a + b; } module.exports = add; ``` **Type Definitions for Enhanced IntelliSense** For full TypeScript-like functionality and autocomplete in your JavaScript user scripts, use the `templater-scripts-types` package: **Setup**: 1. Clone the types repository: `gh repo clone TheRealWolfick/templater-scripts-types` 2. Copy `.dev` folder and `tsconfig.json` to your Templater scripts folder 3. (Optional) Add `**/.dev/**` and `**/tsconfig.json` to `.gitignore` if using Obsidian Git **Script template with types**: ```javascript //import moment from '.dev/types/moment'; /** * @param {import('templater-obsidian').TemplaterApi} tp */ function userFunction(tp) { // Full autocomplete and type checking available const title = tp.file.title; const date = tp.date.now("YYYY-MM-DD"); return `${title} - ${date}`; } module.exports = userFunction; ``` **With Dataview integration**: ```javascript //import moment from '.dev/types/moment'; /** * @param {import('templater-obsidian').TemplaterApi} tp * @param {import('api/inline-api').DataviewInlineApi} dv */ function userFunction(tp, dv) { const pages = dv.pages("#project"); return pages.length; } module.exports = userFunction; ``` **Usage in template**: ```javascript <% tp.user.userFunction(tp) %> <% tp.user.userFunction(tp, this.app.plugins.plugins["dataview"].api) %> ``` **Note on moment.js**: Uncomment the `import moment` line for types/autocomplete during development, then re-comment before execution (Templater will throw an error if the import is active). **Benefits**: - Full IntelliSense for all Templater API functions - JSDoc documentation on hover - Type checking for function parameters - Autocomplete for Obsidian API methods - Reduced errors through compile-time validation **Note**: Scripts can't access `tR` directly from their scope. To use `tR` in user scripts, pass it as a parameter: ```javascript /** * @param {import('templater-obsidian').TemplaterApi} tp * @param {string} tR - Template result variable for output control */ function userFunction(tp, tR) { // Now tR can be used within the function return " ---\ntype: note\n ---\n"; } module.exports = userFunction; ``` **Usage in template**: ```javascript <%* tR = ""; tR += await tp.user.userFunction(tp, tR) %> ``` This pattern is essential for scripts that need to: - Prepend content (like frontmatter) to the document - Clear existing output with `tR = ""` - Build complex output programmatically ### System Command User Functions Execute system commands as user functions. **Setup**: Settings → User System Command Functions Configure command name and shell command. Internal functions resolved before execution. ```javascript // If command "echo" is configured as: echo "{{VALUE}}" <% tp.user.echo({VALUE: "hello"}) %> // Command with internal function: cat <% tp.file.path() %> <% tp.user.cat_file() %> // Executes: cat /path/to/file.md ``` Arguments passed as environment variables to the system command. ## JavaScript Execution Commands ### Output Control Use `tR` variable to control output: ```javascript <%* tR += "appended text" %> // Append to output <%* tR = "" %> // Clear all previous output ``` **Example - Conditional frontmatter**: ```markdown --- type: template --- Template content here <%\* tR = "" -%> --- ## type: person # <% tp.file.cursor() %> ``` Output becomes: ```markdown --- type: person --- # ``` ### Async Functions Many functions are async - use `await`: ```javascript <%* const value = await tp.system.prompt("Enter value"); await tp.file.create_new("Content", "Filename"); %> ``` ### Control Flow Examples **Conditional**: ```javascript <%* if (tp.file.title.startsWith("Daily")) { -%> This is a daily note! <%* } else { -%> Regular note <%* } -%> <%* if (tp.frontmatter.type === "seedling") { -%> 🌱 Seedling content <%* } -%> <%* if (tp.file.tags.contains("#todo")) { -%> This is a todo! <%* } -%> ``` **Loops**: ```javascript <%* const files = tp.app.vault.getMarkdownFiles(); for (let file of files) { tR += `- [[${file.basename}]]\n`; } %> ``` **Functions**: ```javascript <%* function formatDate(date) { return moment(date).format("YYYY-MM-DD"); } let today = formatDate(new Date()); %> Date: <% today %> ``` **String manipulation**: ```javascript <%* tR += tp.file.content.replace(/old/g, "new") %> ``` ## Settings ### General Settings - **Template folder location**: Files in folder available as templates - **Syntax Highlighting**: Desktop/mobile edit mode highlighting - **Automatic jump to cursor**: Auto-trigger `tp.file.cursor` after insert - **Trigger on new file creation**: Auto-apply templates on file creation ### Template Hotkeys Bind templates to keyboard shortcuts. ### Folder Templates Auto-apply templates to folders (requires "Trigger on new file creation"). Deepest match used. Add `/` rule for catch-all. ### File Regex Templates Apply templates based on regex path matching (requires "Trigger on new file creation"). First match used. End with `.*` for catch-all. ### Startup Templates Templates executed once on Templater startup. No output. Useful for registering hooks. ### User Script Functions Folder for JavaScript files (CommonJS modules) as user functions. ### User System Command Functions Configure system commands as user functions. ⚠️ **Security Warning**: Only execute trusted code/commands from known sources. ## Advanced Patterns ### Modify Frontmatter After Execution ```javascript <%* tp.hooks.on_all_templates_executed(async () => { const file = tp.file.find_tfile(tp.file.path(true)); await tp.app.fileManager.processFrontMatter(file, (fm) => { fm.created = tp.date.now(); fm.modified = tp.date.now(); }); }); %> ``` ### Template File Creation with Link ```javascript [[<%* const newFile = await tp.file.create_new("Content", "NewNote"); tR += newFile.basename; %>]] ``` ### Nested User Functions (Mobile Workaround) Pass arguments via `tp` properties with `tp.file.include()`: **Caller**: ```javascript <%* tp.name = "Ryan"; await tp.file.include('[[SayHello]]'); %> ``` **SayHello template**: ```javascript Hello <% tp.name %>! ``` ### File Exists Check ```javascript <%* const path = "path/to/file.md"; if (await tp.file.exists(path)) { tR += "File exists"; } else { tR += "File not found"; } %> ``` ### Dynamic File List ```javascript <%* const files = tp.app.vault.getMarkdownFiles() .filter(f => f.path.startsWith("Notes/")) .sort((a, b) => a.basename.localeCompare(b.basename)); for (let file of files) { tR += `- [[${file.basename}]]\n`; } %> ``` ### Complex Suggester with Files ```javascript <%* const files = tp.app.vault.getMarkdownFiles() .filter(f => !f.path.includes("Archive")); const selected = await tp.system.suggester( f => `${f.basename} (${f.parent.name})`, files, false, "Select a file" ); if (selected) { tR += `[[${selected.basename}]]`; } %> ``` ## Common Patterns ### Daily Note Template ```markdown --- date: <% tp.date.now("YYYY-MM-DD") %> weekday: <% tp.date.now("dddd") %> created: <% tp.file.creation_date() %> --- # <% tp.date.now("dddd, MMMM DD, YYYY") %> << [[<% tp.date.now("YYYY-MM-DD", -1) %>]] | [[<% tp.date.now("YYYY-MM-DD", 1) %>]] >> ## Notes <% tp.file.cursor() %> ## Links ``` ### Meeting Note Template ```markdown --- date: <% tp.date.now() %> participants: <% await tp.system.prompt("Participants") %> tags: [meeting] --- # Meeting: <% await tp.system.prompt("Meeting Title") %> **Date**: <% tp.date.now("YYYY-MM-DD HH:mm") %> **Participants**: <% await tp.system.prompt("Participants") %> ## Agenda <% tp.file.cursor(1) %> ## Notes <% tp.file.cursor(2) %> ## Action Items <% tp.file.cursor(3) %> ``` ### Person Template ```markdown --- type: person name: <% tp.file.title %> created: <% tp.file.creation_date() %> --- # <% tp.file.title %> ## Contact Info <% tp.file.cursor(1) %> ## Notes <% tp.file.cursor(2) %> ## Related ``` ## Troubleshooting ### Frontmatter Returns Undefined - `tp.frontmatter` may be empty on file creation - Use `tp.hooks.on_all_templates_executed()` to access frontmatter after template execution - Ensure frontmatter format is correct (YAML between ` ---` markers) ### Dynamic Commands Not Working - Dynamic commands deprecated, use Dataview plugin - Only execute in preview mode, not live preview - Cached in preview, won't refresh automatically ### Template Not Executing - Check "Trigger on new file creation" is enabled - Verify Folder Templates or File Regex rules are configured - Check template folder location in settings - Verify file doesn't have syntax errors ### Async Function Issues - Always use `await` with async functions - Use `<%*` execution commands for async operations - Prompts/suggesters require `await` to capture values ### Performance with Large Lists - Use `limit` parameter in suggester functions - Filter data before passing to suggester - Consider pagination for very large datasets ## Best Practices 1. **Use execution commands** (`<%*`) for complex logic 2. **Always await** async functions (prompts, file operations, web requests) 3. **Use whitespace control** (`-%>`) to clean up output 4. **Store reusable code** in user script functions 5. **Use `tp.hooks`** for post-template operations (frontmatter updates) 6. **Prefer `tp.app`** over global `app` for consistency 7. **Test templates** on sample files before production use 8. **Use `tR` carefully** - understand when to append vs. overwrite 9. **Leverage `tp.file.include()`** for template composition 10. **Document custom scripts** with TSDoc for intellisense ## Security Considerations - ⚠️ Can execute arbitrary JavaScript code - ⚠️ Can run system commands - ⚠️ Only use trusted templates and scripts - ⚠️ Review user functions before enabling - ⚠️ Be cautious with "Trigger on new file creation" - ⚠️ System commands expose environment variables ## Resources - **Documentation**: https://silentvoid13.github.io/Templater/ - **GitHub**: https://github.com/SilentVoid13/Templater - **Obsidian API**: https://docs.obsidian.md/ - **Moment.js Format**: https://momentjs.com/docs/#/displaying/format/ - **Template Showcases**: GitHub Discussions / Community forums