Obsidian Templater: creating multiple files based on a folder of reference files with string interpolation

I don’t know if this is too Obsidian-plugin-specific for this forum, but since a lot of the code here is based on examples from the fabulous website of @sylumer, and since Automators is the reason I got into Obsidian, I thought I’d cross-post to the Automators forum. I hope that’s not frowned upon.

Things I have tried

Apologies for a long-winded post. I hope the examples are understandable. I’m not a programmer, i just dabble in code I don’t 100% understand, which is probably the reason why I can’t do what I want. And maybe there is a much simpler/better way to do what I’m trying to accomplish?

Searching the forum

I have tried searching the forum, but I don’t really know what the right tool is to solve my problem, so I don’t know what to search for…

Scripting attempt 1: external JS-files loaded by Templater

+ this works on OSX
- does not work on iOS
- template literal string looks a bit messy inside the loop in the script
- annoying to maintain template text within the script and not within Obsidian

Scripting attempt 2: defining template with template literals within loop

+ this works on OSX and iOS
- looks very messy in Obsidian with the template literal strings nested inside loop
- hard to maintain and update the template text

Example script:

<%*
    const folder = "/referenceFolder"
    const destination = "/destinationFolder"
    let listOfFiles = await app.metadataCache.vault.adapter.list(folder);
	listOfFiles.files.forEach(file => {
		// get fileName from path - lazy, regex would be better...
		let fileName = file.slice(-9, -4)
		// this is the messy part:
		let text = `---
yaml-stuff: goes here
---
# ${fileName}
text, blah blah
`
		if (!tp.file.exists(fileName)) {
			app.vault.create(destination + fileName +".md", text);
		} else {
			// don't create a file...
		}
	});
%>

Scripting attempt 3: storing template literals in a separate Templater template

potential:
+ reusable, expandable,
+ clean, easy-to-read templates that are easy to maintain

I can’t seem to control the type of string Templater/obsidian API serves when reading a template file, so the string interpolation never happens, and I end up with the actual ${variable}-text in the output. In short: I don’t really know how to make this work…

TEMPLATE FILE

# ${title}
foo bar baz

I know this is not going to work, just wanted to show the idea:

SCRIPT TEMPLATE FILE

<%* 
	let template = "Template File"
	let title = "My amazing title"
	let text = await tp.file.find_tfile(template);
	// Next part would go inside the loop
	app.vault.create(destination + fileName +".md", text);
%>

Output:

CREATED FILE

# ${title} // no string interpolation here :(
foo bar baz
Scripting attempt 4: defining template outside of loop, referencing variables in loop

- only slightly better than attempt 2

similar to the problem with attempt 3: string interpolation does not work because the text template is evaluated first, and the ${variables} are evaluated to undefined

Example script:

<%*
const folder = "/referenceFolder";
const destination = "/destinationFolder";
	let fileName;
	let text = `---
yaml
---
# ${fileName}
foo bar baz
`
	
    let listOfFiles = await app.metadataCache.vault.adapter.list(folder);
	listOfFiles.files.forEach(file => {
		// get fileName from path - lazy, regex would be better...
		let fileName = file.slice(-9, -4)
		if (!tp.file.exists(fileName)) {
			app.vault.create(destination + fileName +".md", text);
		} else {
			// don't create a file...
		}
	});
%>

Output:

---
yaml
---
# undefined
foo bar baz

What I’m trying to do

I want to:

  • create multiple new notes from a template with some unique string replacement
  • the amount of files created is equal to amount of reference files in another folder
  • string replacement is based on filenames of reference files

Example:

referenceFolder/
---file1.pdf
---file2.pdf
---file3.pdf

I want to create:

destinationFolder/
---file1.md
---file2.md
---file3.md

So that file1.md would look like this:

# File1
Some text here
![[file1.pdf]]
Next file is here: [[file2.pdf]]

Hmmm…

Since I’m replacing multiple instances of a simple string, maybe string.replace() with some regex is the way to go?

Any other ideas? Are there any easier ways to accomplish what I want?

Glad you like my stuff. Let’s see if this helps.

I created an Obsidian vault to try putting something together and populated it with a few files:

2022-05-04-20.44.44

  • The PDF files in the referenceFolder are just some arbitrary PDFs for demo purposes.
  • The _Main.md is just supposed to be some arbitrary note, and is where I am when I use the Templater template.
  • Some Template.md contains the base content for the new files to be created in the destinationFolder.
  • T.TST1.md is the Templater template, and Templater has been configured to reference the Templates folder for templates.

The two template files are what is of note here:

Content: Some Template.md

This is the content read in to use for the output of the new files. The use of {{title}} was arbitrary as I just used it as a placeholder.

--- 

yaml

--- 

[[{{title}}]]

  

foo bar baz

Content: T.TST1.md

This is just you final template with some revisions to deal with variable scope and a text substitution. Forgive the variable renaming and reformatting, it just helps me track what’s what.

<%* 
// Define Constants
const REFERENCE = "/referenceFolder"; 
const DESTINATION = "/destinationFolder"; 
const NOTETEMPLATE = "Some Template"; 

// Get the template content
let fTemplate = await tp.file.find_tfile(NOTETEMPLATE);
let strTemplateContent = await app.vault.read(fTemplate);

// Get reference file list and make a new note for each if one does not already exist
let astrListOfFiles = await app.metadataCache.vault.adapter.list(REFERENCE); 
astrListOfFiles.files.forEach(file => 
{ 
	// Set file names and content
	let strReferenceName = file.substring(REFERENCE.length);
	let strName = file.substring(REFERENCE.length, file.lastIndexOf('.')) + ".md";
	let strContent = strTemplateContent.replaceAll("{{title}}", strReferenceName);

	// File does not exist, so create it
	if (!tp.file.exists(strName)) app.vault.create(DESTINATION + "/" + strName, strContent);
});
%>

When run the result looks like this:

Which I think gives you a few steps forward on what you were after and just leaves you the next file element to figure out.

Hope that helps.

1 Like

It’s amazing how much more elegant the solution is when you know what you’re doing! :smiley: This is very helpful and informative. Thank you so much! Looking forward to refactoring my hackish code…