Templater: issues with formatting metadata

I wanted to create a templater template, that would update a timestamp in my metadata not automatically, but if I manually ask (with hotkey binding). While I have several useful simple templates with templater, I’m very new to scripting…

I found this thread: Templater - How to add information to YAML frontmatter

And basically tried imitating it. I copied the mutate_frontmatter.js to my templater scripts folder and I made a template in my templates folder that has:

 <% tp.user.mutate_frontmatter(tp, {updated: tp.date.now("YYYY-MM-DD HH:mm:ssZ")}) %>

I was expecting that if I trigger this template with the hotkey binding, it would update the frontmatter “updated” value with the current timestamp.

Instead I run into an error, that prints in the console:

plugin:templater-obsidian:61 Templater Error: Template parsing error, aborting. 
tp.user.formatted_frontmatter is not a function
log_error                       @plugin:templater-obsidian:61
errorWrapper                    @plugin:templater-obsidian:81
await in errorWrapper (async)		
append_template_to_active_file	@plugin:templater-obsidian:3700
callback	                    @plugin:templater-obsidian:3978
tj	                            @app.js:1
e.executeCommand	            @app.js:1
e.onTrigger	                    @app.js:1
e.handleKey	                    @app.js:1
e.handleKey	                    @app.js:1
t.handleKey	                    @app.js:1
e.onKeyEvent	                @app.js:1

So, if I understand correctly, it doesn’t recognize the function (although in my templater settings it shows as found and I have enabled user functions).

Any ideas what I’m doing wrong?

It could be helpful to see the actual javascript file defining your function.

Ok, I guess posting here just makes me realize things: it was using a different function as well. Copied that and now there is no error, but it doesn’t update the metadata, but creates a new one where my cursor happens to be…

Basically, because I’m noob, I was just copying it from here: Obsidian Snippets · GitHub

which was linked in the forum post I mentioned.

So, currently, these 2 scipts:

formatted_frontmatter.js

module.exports = async (tp, raw) => {
	const { position, ...frontmatter } =
		tp.frontmatter && Object.keys(tp.frontmatter).length > 0
			? tp.frontmatter
			: tp;

	let output = "";
	const yaml = await import("https://unpkg.com/js-yaml?module");
	try {
		output += yaml.dump(frontmatter, {
			// quotingType: '"',
			// forceQuotes: true,
		});
		if (raw) {
			return output;
		} else {
			return ["---", output, "---"].join("\n");
		}
	} catch (e) {
		console.log(e);
	}
};

and mutate_frontmatter.js

/*jshint esversion: 9 */
module.exports = (tp, attributes = {}) => {
	if (typeof attributes !== "object") {
		throw new Error("attributes must be an object");
	}
	let { position, ...frontmatter } =
		tp.frontmatter && typeof tp.frontmatter === "object" ? tp.frontmatter : {};
	for (let key in attributes) {
		if (Array.isArray(frontmatter[key]) || Array.isArray(attributes[key])) {
			if (Array.isArray(frontmatter[key]) && Array.isArray(attributes[key])) {
				frontmatter[key] = frontmatter[key].concat(attributes[key]);
			} else if (
				!Array.isArray(frontmatter[key]) &&
				Array.isArray(attributes[key])
			) {
				frontmatter[key] = attributes[key].concat([frontmatter[key]]);
			} else if (
				Array.isArray(frontmatter[key]) &&
				!Array.isArray(attributes[key])
			) {
				frontmatter[key] = frontmatter[key].concat([attributes[key]]);
			} else {
				frontmatter[key] = { ...frontmatter[key], ...attributes[key] };
			}
			frontmatter[key] = Array.from(new Set(frontmatter[key].filter(val => val)));
		} else if (
			typeof frontmatter[key] === "object" &&
			typeof attributes[key] === "object"
		) {
			frontmatter[key] = { ...frontmatter[key], ...attributes[key] };
		} else {
			frontmatter[key] = attributes[key];
		}
	}
	// remove duplicates from array values
	frontmatter = Object.fromEntries(
		Object.entries(frontmatter).map(([key, value]) => {
			if (Array.isArray(value)) {
				return [key, Array.from(new Set(value))];
			}
			return [key, value];
		})
	);


	return tp.user.formatted_frontmatter(frontmatter);
};

And that seems to be the intended purpose of those scripts. I can’t see any code suggesting it would actually change the existing frontmatter, but rather lift the existing frontmatter and update a value before producing a new frontmatter.

In other words, these functions seems to be constructed for a context where you replace the existing frontmatter, and not as an insertion which is how you call it now.

So, maybe you’ll get intended result of you work with source mode in properties, and select existing frontmatter before you trigger the template. (I’ve not looked extremely hard into the logic, so I might be wrong, but this is how I see the presented code)

I asked, on Discord, if there was a way to update an existing updated date/time key in Properties without having to go to Source mode (as I was using a one liner Templater template before Properties came to life :innocent: )

Zachatoo kindly gave me this Templater template :

<%*
const file = tp.file.find_tfile(tp.file.title);
await app.fileManager.processFrontMatter(file, (frontmatter) => {
  // Replace "updated" with the key you use
  frontmatter["updated"] = tp.file.last_modified_date("YYYY-MM-DD HH:mm:ss");
});
-%>

… which works perfectly to update my updated key.

Now if I completely misunderstood what was asked here: I am sorry and please, just ignore this post :innocent:

5 Likes

You’re spot on with a better solution, which indeed addresses the question in place, as this uses the Obsidian API to trigger a change to the frontmatter, instead of updating the file directly by yourself.

1 Like

Thank you for confirming I didn’t misread the question :smile: !
I was just deeply unsure :sweat_smile:.

Thank you! This is exactly what I wanted! I’m just not very good at these (yet). I tried something similar, but obviously was missing something, as that didn’t work, but this did.

Anyway, thanks for letting me know how to do it in a more simple way :slight_smile:

1 Like

This is really useful! I’m having to redo much of the frontmatter in my vault to accommodate properties; this will save me alot of time. Cheers!

This is cool. But what if you have the following organization in your vault?

Vault
├── Work
│   └── Assignments.md
└── School
    └── Fall Semester
        └── Assignments.md

Then which note would be returned by tp.file.find_tfile(tp.file.title) if you were editing Assignments.md ?

The first one? A random one? Both?

That’s indeed an interesting question :thinking: … to which I have no answer :sweat_smile: .
(I’m not advanced enough in Obsidian API/JS/Templater, etc…)

But I guess @Zachatoo would know :innocent:

I’m not sure if it’s the first one or a random one, it’s definitely arbitrary.

Fun fact: you can actually pass a file path instead of just a file name to tp.file.find_tfile to consistently get the file you want. You can pass as little or as much of the path as you want/need.

const workAssignmentTFile = tp.file.find_tfile("Work/Assignments");
const fallSemesterAssignmentTFile = tp.file.find_tfile("Fall Semester/Assignments");
const schoolAssignmentTFile = tp.file.find_tfile("School/Fall Semester/Assignments");

Edit: Now that I’ve actually read the post and the context of this thread, you can replace tp.file.title with tp.file.path(true) to consistently always edit the correct file.

<%*
const file = tp.file.find_tfile(tp.file.path(true));
await app.fileManager.processFrontMatter(file, (frontmatter) => {
  // Replace "updated" with the key you use
  frontmatter["updated"] = tp.file.last_modified_date("YYYY-MM-DD HH:mm:ss");
});
-%>
1 Like

Hi there, I am facing a similar issue.
I am using the code provided by @Zachatoo. It seems to work briliantly, but somehow it is overriding text which I also want to be shown after applying the template. Not everytime but sometimes, and I could note figure out the issue.

My template:

<%*
const file = tp.file.find_tfile(tp.file.path(true));
await app.fileManager.processFrontMatter(file, (frontmatter) => {
  // Replace "updated" with the key you use
  frontmatter["updated"] = tp.file.last_modified_date("YYYY-MM-DD");
});
%>

## Hello 
this is a test

After applying the template the note it should look like this:

---
updated: 2023-09-15
---

## Hello 
this is a test

But it only shows the frontmatter:

---
updated: 2023-09-15
---

Surprissingly, if i click Ctrl+z I see the text, but not the frontmatter, suggesting that somehow the frontmatter-part is overriding everything else.
I am not an expert in this, but I tried to find a solution in the web the whole day, but could not figure it out.

Using the processFrontMatter() on the current file, especially when the file is also being changed by the template itself, can very easily cause race conditions as to whichever process changes the file first/last.

As such I’ve often triggered this function through a slight delay, so try wrapping the function call within a setTimeout(), something like:

<%*
const file = tp.file.find_tfile(tp.file.path(true));
await setTimeout(
  async() => {
    await app.fileManager.processFrontMatter(
      file,
      frontmatter => {
        // Replace "updated" with the key you use
        frontmatter["updated"] = tp.file.last_modified_date("YYYY-MM-DD");
      })
  },
  2000) // 2000 is 2 seconds, adjust as needed
-%>

## Hello

This is a test

Be aware that this will trigger two saves in itself, as the template execution does one save operation, then there is the delay, followed by the frontmatter update, and another save.

( If you have the wrong delay, you’re also allowing yourself to potentially change the file in between, which can be fun, but also mildly confusing. Try changing the delay to 10000 (aka 10 seconds), change the file, and see what happens… Do this on a test file!!! )

Also to reduce the risk of other race conditions occuring, try keeping the code, and thusly execution time, of the inner loop changing the frontmatter to a minimum. Any lengthy calculations/preparations should be done on the outside of the setTimeout() function.

2 Likes

2 seconds is probably overkill for most templates. If your template is fairly simple, 100 ms is plenty, and not very noticeable. I’d start small and bump it up if you start seeing issues.

setTimeout(async () => {
  // ...
}, 100);

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.