Templater
Templater copied to clipboard
Question: Is there a way to rename a file without triggering a new template file creation?
Is your feature request related to a problem? Please describe. A clear and
I'm currently developing a comprehensive template to automate my note creations. Everything works fine so far, but when I'm trying to rename the file with tp.file.rename
in the folder where templater listens on new file creations, that creates a new file with the old unwanted title. I solved this problem partially for another scope of my automation process where I manually trigger the creation of a file based on the template but the file will first be generated to a temporary location and after it's done it will be moved back to my zettelkasten folder.
This is the error I'm receiving, when I'm trying to rename or move a file (I checked templaters source code, move and rename are practically the same functions):
Templater Error: Template parsing error, aborting.
Destination file already exists!
Describe the solution you'd like It would be great if there was a way to disable the the template listening on new files temporarily with a boolean flag from the tp functions.
Describe alternatives you've considered My simple guess would be now to delegate the creation of my zettelkasten notes to a different template that manually launches my old template in a different location and then move it to my zettelkasten notes. That way no templates end up in my zettelkasten. But I haven't tried that yet and it would be very time consuming te refactor everything right now.
One possible solution would be to parse all the necessary data like title, book-search-values(These btw are only available in the first executed template) to functions which in return trigger the template below in a different location with tp.file.create_new()
. After that's done I would just have to return a string or a file object to the newly created file to move it into zettelkasten from within the entry template.
Here is my template if you are interested
<%*
let date_now = tp.date.now("YYYYMMDDHHmmss");
let new_title = "";
let old_file_name = await tp.file.name;
let new_file_name = "";
let title_prefix = tp.file.title.charAt(0);
if (title_prefix == "{") {
new_title = await tp.file.title.replace(/^\{\s*/, "");
new_file_name = new_title;
} else if (title_prefix == "@") {
new_title = await tp.file.title.replace(/^\@\s*/, "");
new_file_name = new_title;
} else if (title_prefix == "=") {
new_title = await tp.file.title.replace(/^\=\s*/, "");
new_file_name = date_now;
} else if (title_prefix == "[") {
new_title = await tp.file.title.replace(/^\[\s*/, "");
new_file_name = new_title;
} else {
new_file_name = date_now;
new_title = await tp.file.title;
}
-%>
<%*
let date_now = tp.date.now("YYYYMMDDHHmmss");
let new_title = "";
let old_file_name = await tp.file.name;
let new_file_name = "";
let title_prefix = tp.file.title.charAt(0);
if (title_prefix == "{") {
new_title = await tp.file.title.replace(/^\{\s*/, "");
new_file_name = new_title;
} else if (title_prefix == "@") {
new_title = await tp.file.title.replace(/^\@\s*/, "");
new_file_name = new_title;
} else if (title_prefix == "=") {
new_title = await tp.file.title.replace(/^\=\s*/, "");
new_file_name = date_now;
} else if (title_prefix == "[") {
new_title = await tp.file.title.replace(/^\[\s*/, "");
new_file_name = new_title;
} else {
new_file_name = date_now;
new_title = await tp.file.title;
}
-%>
---
note_id: <% date_now %>
<%* if (title_prefix == "{") { -%>
note_title: <% new_title %>
<% tp.file.include("[[Atomic Note Frontmatter]]") %>
# <% new_title %>
<%* } else if (title_prefix == "@") { -%>
note_title: <% new_title %>
<% tp.file.include("[[Research Note Frontmatter]]") %>
# <% new_title %>
<%* } else if (title_prefix == "=") { -%>
note_title: <% new_title %>
<% tp.file.include("[[Embedding Note Frontmatter]]") %>
# <% new_title %>
<%* } else if (title_prefix == "[") { -%>
note_title: "<% new_title %>"
tags:
- book
title: "<% tp.user.process_book_title(`{{title}}`) %>"
author_links: <% tp.user.process_author_on_book(tp, `{{authors}}`) %>
author_strings: <% tp.user.process_author_strings(`{{authors}}`) %>
publisher: "{{publisher}}"
category: "{{category}}"
publish: "{{publishDate}}"
total: {{totalPage}}
isbn10: {{isbn10}}
isbn13: {{isbn13}}
cover: "{{coverUrl}}"
rating:
status: unread
date created: "{{DATE:DD-MM-YYYY}}"
time created: "{{DATE:DD-MM-YYYY}}<% tp.date.now("HH:mm") %>"
dv-types: ["book-notes"]
owned:
---

# <% new_title %>
<%* } else { -%>
note_title: <% new_title %>
<% tp.file.include("[[Fleeting Note Frontmatter]]") %>
# <% new_title %>
<%* } %>
---
<%* if (title_prefix == "{") { -%>
<%- tp.file.include("[[Atomic Note Body]]") %>
<%* } else if (title_prefix == "@") { -%>
<%- tp.file.include("[[Research Note Body]]") %>
<%* } else if (title_prefix == "=") { -%>
<%- tp.file.include("[[Embedding Note Body]]") %>
<%* } else if (title_prefix == "[") { -%>
<%- tp.file.include("[[New Book Body]]") %>
<%* } else { -%>
<%- tp.file.include("[[Fleeting Note Body]]") %>
<%* } %>
---
<%* if (title_prefix == "{") { -%>
<%- tp.file.include("[[Atomic Note Footer]]") %>
<%* } else if (title_prefix == "@") { -%>
<%- tp.file.include("[[Research Note Footer]]") %>
<%* } else if (title_prefix == "=") { -%>
<%- tp.file.include("[[Embedding Note Footer]]") %>
<%* } else if (title_prefix == "[") { -%>
<%- tp.file.include("[[New Book Footer]]") %>
<%* } else { -%>
<%- tp.file.include("[[Fleeting Note Footer]]") %>
<%* } %>
<%* await tp.user.safely_rename_file(tp, old_file_name, new_file_name) %>
and here aer all the js functions I'm using:
/**
* This function is used to process the author(s) of a book.
*
* @param {Object} tp The templater object.
* @param {string} authors comma separated. Potentially includes title.
* @return {string} the constructed string used to declare a list frontmatter.
*/
async function process_author_on_book(tp, authors) {
console.log(`Processing author(s): ${authors}`);
const authors_and_titles = authors.split(",");
const possible_titles = [
// Academic Titles
"PhD", "EdD", "MD", "JD", "DVM", "DDS", "PsyD", "ScD", "DrPH", "MSc", "MA", "MBA", "MFA", "MPH", "MSW",
// Professional Titles
"PE", "RN", "CPA", "CFA", "PMP", "Esq", "APRN", "NP", "PA", "CCIE", "CISSP", "CISM", "CEH", "OCP", "SCJP",
// Honorary Titles
"Sir", "Dame", "Hon", "Rev", "Fr", "Rabbi", "Imam", "Swami", "Guru",
// Military Titles
"Gen", "Col", "Maj", "Capt", "Lt", "Sgt", "Adm", "Cdr", "Ens", "WO", "CWO",
// Medical Titles
"DO", "DC", "OD", "PharmD", "DPT", "DNP", "CRNA", "RD", "RPh",
// Other
"Sr", "Jr", "III", "IV", "V"
];
let constructed_string = "";
if (Array.isArray(authors_and_titles)) {
let i = 0;
while (i < authors_and_titles.length) {
let name = authors_and_titles[i];
let title = "";
// Check if the next string is a possible title
if (i < authors_and_titles.length - 1 && possible_titles.includes(authors_and_titles[i + 1])) {
title = `, ${authors_and_titles[i + 1]}`;
i += 2; // Skip the next item as it's a title
}
// Check if the previous string is a possible title
else if (i > 0 && possible_titles.includes(authors_and_titles[i - 1])) {
title = `, ${authors_and_titles[i - 1]}`;
i++; // Move to the next item
}
// If neither next nor previous is a title, just move to the next item
else {
i++;
}
const filePath = `Zettelkasten/${name}${title}`;
const fileExists = await tp.file.exists(filePath + ".md");
console.log(`File path: ${filePath}`);
if (!fileExists) {
// this avoids consecutive executions of templater
const templateFile = await tp.file.find_tfile("New Author");
await tp.file.create_new(templateFile, `${name}${title}`, false, app.vault.getAbstractFileByPath("__resources/__tmp"));
const new_file = await tp.file.find_tfile(`__resources/__tmp/${name}${title}`);
console.log(`Created new author file in temp directory: ${name}${title}`);
await tp.file.move(`Zettelkasten/${name}${title}`, new_file);
console.log(`Moved new author file to Zettelkasten: ${name}${title}`);
} else {
console.log(`File already exists: ${name}${title}`);
}
constructed_string += `\n - \"[[${filePath}|${name}${title}]]\"`;
}
console.log(`YAML frontmatter string: ` + constructed_string);
return constructed_string;
} else {
console.log(`Auther type: ` + typeof authors);
console.log(`Author: ` + authors);
return "Something went wrong. Check the console.";
}
}
module.exports = process_author_on_book;
/**
* Maps authors to a YAML frontmatter string.
*
* @param {string} authors comma separated. Potentially includes title.
* @return {string} the constructed string used to declare a list frontmatter.
*/
function process_author_strings(authors) {
const authors_and_titles = authors.split(",");
const possible_titles = [
// Academic Titles
"PhD", "EdD", "MD", "JD", "DVM", "DDS", "PsyD", "ScD", "DrPH", "MSc", "MA", "MBA", "MFA", "MPH", "MSW",
// Professional Titles
"PE", "RN", "CPA", "CFA", "PMP", "Esq", "APRN", "NP", "PA", "CCIE", "CISSP", "CISM", "CEH", "OCP", "SCJP",
// Honorary Titles
"Sir", "Dame", "Hon", "Rev", "Fr", "Rabbi", "Imam", "Swami", "Guru",
// Military Titles
"Gen", "Col", "Maj", "Capt", "Lt", "Sgt", "Adm", "Cdr", "Ens", "WO", "CWO",
// Medical Titles
"DO", "DC", "OD", "PharmD", "DPT", "DNP", "CRNA", "RD", "RPh",
// Other
"Sr", "Jr", "III", "IV", "V"
];
let constructed_string = "";
if (Array.isArray(authors_and_titles)) {
let i = 0;
while (i < authors_and_titles.length) {
let name = authors_and_titles[i];
let title = "";
// Check if the next string is a possible title
if (i < authors_and_titles.length - 1 && possible_titles.includes(authors_and_titles[i + 1])) {
title = `, ${authors_and_titles[i + 1]}`;
i += 2; // Skip the next item as it's a title
}
// Check if the previous string is a possible title
else if (i > 0 && possible_titles.includes(authors_and_titles[i - 1])) {
title = `, ${authors_and_titles[i - 1]}`;
i++; // Move to the next item
}
// If neither next nor previous is a title, just move to the next item
else {
i++;
}
constructed_string += `\n - \"${name}${title}\"`;
}
return constructed_string;
} else {
console.log(`Auther type: ` + typeof authors);
console.log(`Author: ` + authors);
return "Something went wrong. Check the console.";
}
}
module.exports = process_author_strings;
/**
* Idempotently process a book title.
*
* @param {string} title prepended with the book indicator symbol.
* @return {string} the modified title.
*/
function process_book_title(title) {
let processed_title = title.replace(/^\[\s*/, "");
console.log(`Processed title: ${processed_title}`);
return processed_title;
}
module.exports = process_book_title;
/**
* Renames a file without triggering a templater execution.
*
* @param {string} authors comma separated. Potentially includes title.
* @return {string} the constructed string used to declare a list frontmatter.
*/
async function safely_rename_file(tp, current_name, new_name) {
const target_path_string = `Zettelkasten`;
const tmp_path_string = `__resources/__tmp`;
let this_file_string = target_path_string + "/" + current_name;
let moved_file_string = tmp_path_string + "/" + new_name;
let renamed_file_string = target_path_string + "/" + new_name;
let this_t_file = await tp.file.find_tfile(this_file_string);
console.log(`File to be renamed: ${this_file_string}`);
await tp.file.move(moved_file_string, this_t_file);
console.log(`File renamed to: ${moved_file_string}`);
let moved_t_file = await tp.file.find_tfile(moved_file_string);
await tp.file.move(renamed_file_string, moved_t_file);
console.log(`File renamed to: ${renamed_file_string}`);
}
module.exports = safely_rename_file;
There's a lot to parse here. Can you narrow this down to the bare minimum you need to reproduce this issue?
I personally have not experienced any issues with renaming files triggering other templates, and I rename files and use folder templates frequently, so I suspect something else is at play.
There's a lot to parse here. Can you narrow this down to the bare minimum you need to reproduce this issue?
I personally have not experienced any issues with renaming files triggering other templates, and I rename files and use folder templates frequently, so I suspect something else is at play.
I mean I'm using the templater setting where a template is used once a new file hits a certain folder. That is essential for my workflow. And I assume that the rename function triggers the new file event.
Ok I figured it out while working on my solution.
@Zachatoo I create a new file with ctrl + o
, typing the title with my prefix to select a template and then hitting shift + return
to create a not yet existing file. That triggers my template as it should but it will also always in any case create an actual file without a template with the title that I entered.
I will post my solution here once it's done for future reference. I think that is a case that should be considered in templater as it's a commen workflow.
So I got what I wanted now: (The workaround with a temporary template is necessary because book search replaces strings in the original template with meta data. It is not possible to forward this metadata to included templates. This also forces me to generate a template with string concatenation to have my code somewhat maintainable.)
TL;DR: ctrl + o
-> type title -> hit enter -> Execute templater -> execute javascript code -> create the template file string -> create a new file based on the template string with td.file.create_new
in a temporary location with the old title -> move and rename the new file to the desired location -> delete the unwanted file that obsidian creates due to the invocation of the first template ->delete the temporary template
That's the templat that will be executed by templater because it listens on new file creations in a certain directory:
<%*
let old_file_name = tp.file.title;
let title_prefix = tp.file.title.charAt(0);
tp.user.append_and_execute_template(tp, title_prefix, old_file_name);
-%>
The rest of the template that will be appended with the frontmatter below:
tags: atomic
prefix: "{"
---
# <% tp.file.title %>
---
# References (MoC)
Here is the js code:
/**
* Appends and executes a template based on the file indicator.
*
* @param {string} file_indicator - The indicator to determine the type of note.
* @param {string} old_file_name - The original file name.
* @returns {Promise<TFile|null>} - Returns the tmpTemplate instance if deletion fails, otherwise null.
*
*/
async function append_and_execute_template(tp, file_indicator, old_file_name) {
console.log(`File indicator: ${file_indicator}`);
console.log(`Old file name: ${old_file_name}`);
let templateName;
switch (file_indicator) {
case "{": templateName = "Atomic Note.md"; break;
case "@": templateName = "Research Note.md"; break;
case "=": templateName = "Embedding Note.md"; break;
case "[": templateName = "New Book.md"; break;
default: templateName = "Fleeting Note.md"; break;
}
const templateDir = "__templates";
const tmpPathString = "__resources/__tmp";
const targetPathString = "Zettelkasten";
console.log(`Template name: ${templateName}`);
const date = await tp.date.now("YYYYMMDDHHmmss");
console.log(`Date: ${date}`);
const new_file_name = old_file_name.replace(new RegExp(`^\\${file_indicator}\\s*`), "");
console.log(`New File Name: ${new_file_name}`);
const targetFileName = `${targetPathString}/${new_file_name}`;
const potentially_unwanted_file_name = `${targetPathString}/${old_file_name}`;
const frontmatter = `---
note_id: ${date}
note_title: "${new_file_name}"
`;
const templateFile = await tp.file.find_tfile(`${templateDir}/${templateName}`);
const remainingTemplate = await app.vault.read(templateFile);
const finalTemplateString = frontmatter + remainingTemplate;
await tp.file.create_new(finalTemplateString, new_file_name, false, app.vault.getAbstractFileByPath(tmpPathString));
const createdFileString = `${tmpPathString}/${new_file_name}`;
const createdFile = await tp.file.find_tfile(createdFileString);
let unwanted_file_exists = await tp.file.exists(potentially_unwanted_file_name + ".md");
if (unwanted_file_exists) {
console.log(`Unwanted file exists: ${potentially_unwanted_file_name}`);
const unwanted_file = await tp.file.find_tfile(potentially_unwanted_file_name);
await app.vault.delete(unwanted_file, true);
}
await tp.file.move(targetFileName, createdFile);
}
module.exports = append_and_execute_template;
Edit: Creating a temporary template file was not necessary.
New Issue (Not for me tho) - I'm leaving this here just in case someone encounters this problem.
Invoking the following code to remove the temporary template causes a Template Error for some reason even though the template is created by an Obsidian api function.
await app.vault.delete(tmpTemplate, true);
When tmpTemplate
is deleted the error Templater Error: Template parsing error, aborting. ENOENT: no such file or directory, open '/home/leonch/Documents/obsidian/__resources/__tmp/Atomic Note.md'
is thrown. This error is not catchable inside my function. I assume that templater expects at some point the promise of my template because it uses that template to generate the actual file. This is not an error in my workflow and the error doesn't bother me much but a bug stays a bug.
I don't understand why you're creating another file as part of creating a file with the quick switcher. The quick switcher will create the file for you, so unless you want to create multiple files, you don't need to create a file explicitly in your template. Just put the contents that you want in your newly created file in your template. If you want to apply another template as part of your template, use tp.file.include()
instead of tp.file.create_new()
.
@Zachatoo I agree, I changed it now. It also works almost perfectly but sometimes, not always, I still get an error that the File already exists when I'm renaming the file.
Javascript function
/**
* Appends and executes a template based on the file indicator.
*
* @param {string} file_indicator - The indicator to determine the type of note.
* @param {string} old_file_name - The original file name.
* @returns {Promise<TFile|null>} - Returns the tmpTemplate instance if deletion fails, otherwise null.
*
*/
async function append_and_execute_template(tp, file_indicator, old_file_name, injected_frontmatter = "") {
console.log(`File indicator: ${file_indicator}`);
console.log(`Old file name: ${old_file_name}`);
const title = old_file_name.replace(new RegExp(`^\\${file_indicator}\\s*`), "");
const target_path_string = "Zettelkasten";
const { id, date, time } = await tp.user.prepare_date_timestamp(tp);
const { template_name, new_file_name, variable_frontmatter, remaining_template } = await tp.user.generate_template_scope(tp, file_indicator, id, title);
const frontmatter = `---
note_id: ${id}
note_title: "${title}"
date_created: ${date}
time_created: ${date} ${time}
`;
console.log(`Date: ${date}`);
console.log(`Template name: ${template_name}`);
console.log(`New File Name: ${new_file_name}`);
const targetFileName = `${target_path_string}/${new_file_name}`;
const final_template_string = frontmatter + variable_frontmatter + injected_frontmatter + `---\n` + `# ${title}\n` + remaining_template;
console.log(`Final template string: ${final_template_string}`);
return { final_template_string: final_template_string, new_file_name: new_file_name };
}
module.exports = append_and_execute_template;
This is my template now:
- the function
tp.user.append_and_execute_template
creates the final template string and internally usestp.file.include
for other templates
<%*
let old_file_name = tp.file.title;
let title_prefix = tp.file.title.charAt(0);
let final_template_string;
let new_file_name;
if (title_prefix == "[") {
let title = tp.user.process_book_title(`{{title}}`);
let author_links = tp.user.process_author_strings(`{{authors}}`, true);
let author_strings = tp.user.process_author_strings(`{{authors}}`);
let injected_frontmatter = `title: \"${title}\"
author_links: ${author_links}
author_strings: ${author_strings}
publisher: \"{{publisher}}\"
category: \"{{category}}\"
total: {{total}}
isbn10: {{isbn10}}
isbn13: {{isbn13}}
cover: \"{{coverUrl}}\"
rating:
status: unread
`;
({ final_template_string, new_file_name } = await tp.user.append_and_execute_template(tp, title_prefix, old_file_name, injected_frontmatter));
await new Promise(resolve => setTimeout(resolve, 1000)); // don't remove this because obsidian encounters a race condition here
await tp.file.rename(new_file_name);
await tp.user.process_author_on_book(tp, `{{author}}`)
} else {
({ final_template_string, new_file_name } = await tp.user.append_and_execute_template(tp, title_prefix, old_file_name));
await new Promise(resolve => setTimeout(resolve, 1000)); // don't remove this because obsidian encounters a race condition here
await tp.file.rename(new_file_name);
}
-%>
<% final_template_string %>
The renaming is very inconsistent and I'd say in 1 out of 5 times the renaming causes a Templater Error: Template parsing error, aborting. Destination file already exists!
. When the error occurs the file is renamed but the old file with the same content still exists. Let's say I'm renaming { test
to test
: If it goes smoothly I only have one file named test
but in case the error is thrown I have both files { test
and test
. This sounds like a race condition if you ask me.
EDIT: I added await new Promise(resolve => setTimeout(resolve, 1000));
before renaming the file. This gives more consistency.
EDIT 2: Also I noticed that renaming a file after a new file is created with tp.file.create_new
renames the newly created file instead of the file that it altered by the template. For me this only applies to one case where I need to create an author page.
Is this issue resolved for you? It at least appears that the original issue is resolved.
hey there, look. this probably isn't going to add much, but seeing as you're doing some of the same functions....
Basically I want to use templater to rename all of my files to the current H1 header. given what you've learned from this problem, is this possible?
@1Dbcj Give this script a shot. I recommend opening the dev console (CMD + Shift + I) to view the output of this command, it'll let you know which files it's renaming, where it fails, which files don't have an H1 header, etc. This script also accounts for updating links so none of your links to renamed files break.
<%*
// Loop over all markdown files in vault
await Promise.all(
app.vault.getMarkdownFiles().map(async file => {
try {
// Get top level heading from file
const { headings } = app.metadataCache.getFileCache(file);
const topLevelHeading = headings?.find(h => h.level === 1);
if (topLevelHeading) {
// If a top level heading is found, rename file
const oldPath = file.path;
const newPath = tp.obsidian.normalizePath(`${file.parent.path}/${topLevelHeading.heading}.md`);
await app.fileManager.renameFile(file, newPath);
console.log(`Renamed ${oldPath} to ${file.path}`);
} else {
// If top level heading is not found, let user know in console
console.warn(`Top level heading not found in ${file.path}`);
}
} catch (err) {
// If any files fail to be renamed for any reason, let user know in console
console.error(`Failed to process ${file.path}\n\n${err}`);
}
})
);
-%>
Cross-posted here.
Holy shit. you're a witch. this worked.
one reason its failing on many many pages is because I stupidly put a : in many H1 headers, is there a way to replace ": " with " - " when it encounters that?
thank you so much for your help. I was reaching a point of giving up and trying to do this manually over 3000 pages
I'll let you give it a shot first, I believe in you!
JavaScript has a built in replace function that you can use that should make this relatively easy to accomplish.
appreciate the reference. I wish i had learned javascript, I'm primary self taught in python (mostly thanks to chatGPT and a bunch of books), which is unfortunately useless for obsidian.
I'm guessing based on what you sent, that changing this line: const updatedHeading = topLevelHeading.heading.replace(": ", " - ");
so end script being:
<%*
// Loop over all markdown files in vault
await Promise.all(
app.vault.getMarkdownFiles().map(async file => {
try {
// Get top level heading from file
const { headings } = app.metadataCache.getFileCache(file);
const topLevelHeading = headings?.find(h => h.level === 1);
if (topLevelHeading) {
// If a top level heading is found, rename file
// Replace ": " with " - " in the heading before renaming
const updatedHeading = topLevelHeading.heading.replace(": ", " - ");
const oldPath = file.path;
const newPath = tp.obsidian.normalizePath(`${file.parent.path}/${updatedHeading}.md`);
await app.fileManager.renameFile(file, newPath);
console.log(`Renamed ${oldPath} to ${file.path}`);
} else {
// If top level heading is not found, let user know in console
console.warn(`Top level heading not found in ${file.path}`);
}
} catch (err) {
// If any files fail to be renamed for any reason, let user know in console
console.error(`Failed to process ${file.path}\n\n${err}`);
}
})
);
-%>
Marking as removed due to no response from issue author.