Help Make Templater Work for Folder Structure?

I’m using the Templater plugin in Obsidian to automate the creation of a structured folder and file system for organizing medical-related notes. Specifically, I want a script that, upon creating a new note, prompts me once to enter a “Body System Name”. Based on this input, the script should generate a predefined folder hierarchy under Body Systems/, including various subfolders like Charting Tips, Disorders and Conditions, and others, each containing specific markdown files. This setup aims to streamline the organization of notes related to different body systems, treatments, and related medical information.
Things I Have Tried

Initial Script Implementation:
    Created a Templater script that prompts for the "Body System Name" and attempts to create the necessary folders and files.
    Encountered an issue where the script prompts for the "Body System Name" multiple times instead of just once.
Script Modification to Prevent Multiple Prompts:
    Updated the script to store the entered name in tp.variables to prevent repeated prompts.
    After modification, the script still prompted multiple times and did not create the intended folder structure.
Error Handling Enhancements:
    Added comprehensive try-catch blocks and input validation to ensure the script handles empty or invalid inputs gracefully.
    Observed console errors such as:
        Body System Name not provided or is empty.
        Cannot read properties of undefined (reading 'bodySystemName')
        Invalid file path: "Body Systems/Untitled.md" from the Omnisearch plugin.
Sanitization of User Input:
    Implemented a sanitization function to remove invalid characters from the "Body System Name".
    Despite sanitization, the script continued to prompt repeatedly and failed to create folders/files.
Running as a User Script:
    Converted the script into a standalone user script instead of embedding it in templates to avoid recursive prompts.
    Executed the script manually via the Command Palette, but the issue of multiple prompts persisted, and no folders/files were created.

Request for Assistance

I’m seeking guidance on:

  • Preventing Multiple Prompts: How to ensure the script prompts for the “Body System Name” only once during execution.
  • Successful Folder/File Creation: Identifying why the folders and files aren’t being created despite successful prompts.
  • Resolving Console Errors: Understanding and fixing the errors related to undefined variables and invalid file paths.
  • Best Practices for Templater Scripts: Any recommendations on structuring Templater scripts to handle user input and file operations reliably.

Attached is my current version of the Templater script for reference. Any insights or suggestions to help resolve these issues would be greatly appreciated! Current Templater Script:

javascript

<%*
try {
    // Prompt the user for the Body System Name only once
    let bodySystemName = await tp.system.prompt("Enter Body System Name");
    console.log(`User entered Body System Name: "${bodySystemName}"`);

    // Validate the input
    if (!bodySystemName || !bodySystemName.trim()) {
        tR += "**Error:** Body System Name is required.";
        console.log(`Body System Name not provided or is empty.`);
        return;
    }

    // Function to sanitize folder and file names
    function sanitizeName(name) {
        return name.replace(/[\\/:"*?<>|]+/g, '').trim();
    }

    // Sanitize and trim the name
    const sanitizedBodySystemName = sanitizeName(bodySystemName);
    if (!sanitizedBodySystemName) {
        tR += "**Error:** Body System Name contains only invalid characters.";
        console.log(`Body System Name contains only invalid characters.`);
        return;
    }

    let basePath = `Body Systems/${sanitizedBodySystemName}`;

    // Function to check if a path exists
    function pathExists(path) {
        return !!app.vault.getAbstractFileByPath(path);
    }

    // Function to create a folder if it doesn't exist
    async function createFolder(path) {
        if (!pathExists(path)) {
            await app.vault.createFolder(path);
            console.log(`Folder created: ${path}`);
        } else {
            console.log(`Folder already exists: ${path}`);
        }
    }

    // Function to create a file if it doesn't exist
    async function createFile(path, content = "") {
        if (!pathExists(path)) {
            await app.vault.create(path, content);
            console.log(`File created: ${path}`);
        } else {
            console.log(`File already exists: ${path}`);
        }
    }

    // Create main folder structure
    await createFolder(basePath);
    await createFolder(`${basePath}/Charting Tips`);
    await createFolder(`${basePath}/Disorders and Conditions`);
    await createFolder(`${basePath}/Disorders and Conditions/Treatments and Modalities`);
    await createFolder(`${basePath}/Disorders and Conditions/Treatments and Modalities/Physical Medicine`);
    await createFolder(`${basePath}/Examination`);

    // List of files to create
    const files = [
        "Charting Tips/Billing and Coding.md",
        "Charting Tips/Clinical Pearls.md",
        "Charting Tips/Common Clinic Questions.md",
        "Charting Tips/Epic Troubleshooting.md",
        "Disorders and Conditions/Treatments and Modalities/Physical Medicine/Electrostimulation.md",
        "Disorders and Conditions/Treatments and Modalities/Physical Medicine/HVLA.md",
        "Disorders and Conditions/Treatments and Modalities/Physical Medicine/Hydrotherapy.md",
        "Disorders and Conditions/Treatments and Modalities/Physical Medicine/Muscle Energy Stretches.md",
        "Disorders and Conditions/Treatments and Modalities/Behavioral Med.md",
        "Disorders and Conditions/Treatments and Modalities/Botanical Med.md",
        "Disorders and Conditions/Treatments and Modalities/Environmental Med.md",
        "Disorders and Conditions/Treatments and Modalities/Formulations.md",
        "Disorders and Conditions/Treatments and Modalities/Homeopathy.md",
        "Disorders and Conditions/Treatments and Modalities/Nutrition.md",
        "Disorders and Conditions/Treatments and Modalities/Pharmaceuticals.md",
        "Disorders and Conditions/Treatments and Modalities/Supplementation.md",
        "Disorders and Conditions/By Functional Disorders.md",
        "Disorders and Conditions/By Health History.md",
        "Disorders and Conditions/By Organ.md",
        "Disorders and Conditions/By Patterns of Pain.md",
        "Disorders and Conditions/By Symptoms.md",
        "Healthy Anatomy.md",
        "Wellness Support.md"
    ];

    // Create each file
    for (let file of files) {
        const filePath = `${basePath}/${file}`;
        await createFile(filePath);
    }

    // Inform the user of successful creation
    tR += `✅ **Folder structure created successfully for:** ${sanitizedBodySystemName}`;
    console.log(`Folder structure created successfully for: "${sanitizedBodySystemName}"`);

} catch (error) {
    console.error(`Error creating folder structure: ${error.message}`);
    tR += `**Error:** ${error.message}`;
}
%>

Console Logs Observed:

text

User entered Body System Name: ""
Body System Name not provided or is empty.
User entered Body System Name: ""
Body System Name not provided or is empty.
...
Error creating folder structure: Cannot read properties of undefined (reading 'bodySystemName')

Thank you in advance for your assistance!

I think I’ve got something :blush:

It not necessarily complete and doesn’t necessarily do everything you initially implemented but triggering it using Templater’s command Templater: Open insert template modal in the command palette (on an already opened note) seems to create the files where and when needed :blush: .

Errors I ran into when playing around with the template you shared were mostly coming from the RegEx you used: You didn’t escape everything that needed to be esacped (such as the character * for example, as in “RegEx world”, it already means something and is used to indicate zero or more of the character before it). When it comes to the JS string.replace() I was never able to make it work without giving the RegEx it’s own constant/variable (iirc :sweat_smile: )

When it comes to the folders and files structure, I chose another road and used Templater’s tp.file.create_new() :innocent: (as from my tests, it seemed to handle the creation of a potentially missing folder while creating the files :blush: )

<%*
/* 
 * Helper function to sanitize user input (used later on)
 * regex match characters: \ or / or : or " or * or ? or ! or < or > or | 
 * regex2 replace any superfluous white space characters between words by a simple space 
*/
function sanitizeName(name) {
	const regex = /\\|\/|:|"|\*|\?|!|<|>|\|/g;
	const regex2 = /\s+/g;
	const sanitized = name.replace(regex, '').replace(regex2, ' ').trim();
	return sanitized;
}

/* 
 * Array of objects storing the info about the files to create in their respective folders (under the new "body system" provided by the user)
 * Note: the key "fTemplate" could be used to pre-fill the new note with a dedicated template if the "TEMPLATENAME" is provided (untested at this moment)
 * Note 2: this only contains the info of the first 17 files that were shared
*/
const files = [
	{fName: "Billing and Coding", fPath: "Charting Tips", fExt: ".md", fTemplate: ""},
	{fName: "Clinical Pearls", fPath: "Charting Tips", fExt: ".md", fTemplate: ""},
	{fName: "Common Clinic Questions", fPath: "Charting Tips", fExt: ".md", fTemplate: ""},
	{fName: "Epic Troubleshooting", fPath: "Charting Tips", fExt: ".md", fTemplate: ""},
	{fName: "Electrostimulation", fPath: "Disorders and Conditions/Treatments and Modalities/Physical Medicine", fExt: ".md", fTemplate: ""},
	{fName: "HVLA", fPath: "Disorders and Conditions/Treatments and Modalities/Physical Medicine", fExt: ".md", fTemplate: ""},
	{fName: "Hydrotherapy", fPath: "Disorders and Conditions/Treatments and Modalities/Physical Medicine", fExt: ".md", fTemplate: ""},
	{fName: "Muscle Energy Stretches", fPath: "Disorders and Conditions/Treatments and Modalities/Physical Medicine", fExt: ".md", fTemplate: ""},
	{fName: "Behavioral Med", fPath: "Disorders and Conditions/Treatments and Modalities", fExt: ".md", fTemplate: ""},
	{fName: "Botanical Med", fPath: "Disorders and Conditions/Treatments and Modalities", fExt: ".md", fTemplate: ""},
	{fName: "Environmental Med", fPath: "Disorders and Conditions/Treatments and Modalities", fExt: ".md", fTemplate: ""},
	{fName: "Formulations", fPath: "Disorders and Conditions/Treatments and Modalities", fExt: ".md", fTemplate: ""},
	{fName: "Homeopathy", fPath: "Disorders and Conditions/Treatments and Modalities", fExt: ".md", fTemplate: ""},
	{fName: "Nutrition", fPath: "Disorders and Conditions/Treatments and Modalities", fExt: ".md", fTemplate: ""},
	{fName: "Pharmaceuticals", fPath: "Disorders and Conditions/Treatments and Modalities", fExt: ".md", fTemplate: ""},
	{fName: "Supplementation", fPath: "Disorders and Conditions/Treatments and Modalities", fExt: ".md", fTemplate: ""},
	{fName: "By Functional Disorders", fPath: "Disorders and Conditions", fExt: ".md", fTemplate: ""},
	{fName: "By Health History", fPath: "Disorders and Conditions", fExt: ".md", fTemplate: ""},
	{fName: "By Organ", fPath: "Disorders and Conditions", fExt: ".md", fTemplate: ""},
	{fName: "By Patterns of Pain", fPath: "Disorders and Conditions", fExt: ".md", fTemplate: ""},
	{fName: "By Symptoms", fPath: "Disorders and Conditions", fExt: ".md", fTemplate: ""}
]

// Parent folder (at root)
const topFolder = "Body Systems";

// Prompt user for the Body System Name (folder under the topFolder and the files)
const systemName = await tp.system.prompt("Enter Body System Name");
console.log(`User entered Body System Name: "${systemName}"`);

// Sanitize the system name provided by the user
const sanitizedName = sanitizeName(systemName);
console.log(`"${systemName}" was sanitized in: "${sanitizedName}"`);

// If the sanitization fails, template stops here
if (!sanitizedName) {
	console.log("Error: Body System Name not provided, is empty or contained only invalid characters");
	return; 
}

// If the sanitization is successful ...
// For each object ("file") within Files ...
for (let f of files) {
	// The file's full path including the file's name + extension as required by tp.file.exists()
	const fileFullPath = `${topFolder}/${sanitizedName}/${f.fPath}/${f.fName}${f.fExt}`;
	
	// Just the file's path required by tp.file.create_new()
	const filePath = `${topFolder}/${sanitizedName}/${f.fPath}`;
	
	// Checks if the file exists under the full path 
    // (so only missing/non-existant files are created)
	const fileExist = await tp.file.exists(fileFullPath);

	// if the file doesn't exist, create it where needed
	if (!fileExist) {
		await tp.file.create_new(
			f.fTemplate, 
			f.fName, 
			false, 
			filePath
		);
		console.log(`File ${f.fName}${f.fExt} created under ${filePath.split("/").join(" > ")}`)
	}
}
-%>

This might not be sufficient, but I thought it could possibly give you a lead, maybe … or ideas :innocent:, as I didn’t really had the opportunity to dig further into this and keep all the checks/error logging you first wrote :blush:

Hey that did seem to help a bit! Its MUCH closer now. Still getting prompted to submit the “Body System Name” twice now. But its better than 23-26 times.

Here is what happens. I make a new note under “Body Systems” folder. It prompts me to enter the “Body System Name” it then makes a Folder under that name with all the folders inside of it.

But prompts me a second time. If I type the same name its all good. If I enter a different name it makes the folders again. I will include the logs of what happens when I make a folder.

:information_source: No new files were created. All files for “Test Forum Logs” already exist.

VM1465:125 Skipped (already exists): Body Systems/Test Forum Logs/Examination/Physical Exam.md

VM1465:136 :information_source: No new files were created. All files for “Test Forum Logs” already exist.

VM1482:125 Skipped (already exists): Body Systems/Test Forum Logs/Examination/Physical Exam.md

VM1482:136 :information_source: No new files were created. All files for “Test Forum Logs” already exist.

I would like to figure it out. So If someone sees something I am missing let me know!

Updated Script:
<%*
// Function to sanitize user input
function sanitizeName(name) {
const regex = /[\/:*?"<>|]+/g;
return name.replace(regex, ‘’).trim();
}

// Function to create a folder if it doesn’t exist
async function createFolder(path) {
try {
await app.vault.createFolder(path);
console.log(Created folder: ${path});
} catch (error) {
if (!error.message.includes(‘already exists’)) {
console.error(Error creating folder ${path}: ${error.message});
}
}
}

// Use a global variable to track if we’re already prompting
if (typeof window.isPromptingForBodySystem === ‘undefined’) {
window.isPromptingForBodySystem = false;
}

// Function to get the body system name
async function getBodySystemName() {
if (window.isPromptingForBodySystem) {
// Wait for the existing prompt to finish
while (window.isPromptingForBodySystem) {
await new Promise(resolve => setTimeout(resolve, 100));
}
return window.lastBodySystemName;
}

window.isPromptingForBodySystem = true;
try {
    const systemName = await tp.system.prompt("Enter Body System Name");
    if (!systemName) {
        throw new Error("Body System Name not provided");
    }
    window.lastBodySystemName = systemName;
    return systemName;
} finally {
    window.isPromptingForBodySystem = false;
}

}

// Get the body system name
const systemName = await getBodySystemName();
const sanitizedName = sanitizeName(systemName);

if (!sanitizedName) {
throw new Error(“Body System Name is empty or contains only invalid characters”);
}

console.log(Creating structure for body system: "${sanitizedName}");

const topFolder = “Body Systems”;

// Create the main folder structure
const folderStructure = [
${topFolder}/${sanitizedName},
${topFolder}/${sanitizedName}/Charting Tips,
${topFolder}/${sanitizedName}/Disorders and Conditions,
${topFolder}/${sanitizedName}/Disorders and Conditions/Treatments and Modalities,
${topFolder}/${sanitizedName}/Disorders and Conditions/Treatments and Modalities/Physical Medicine,
${topFolder}/${sanitizedName}/Examination
];

// Create all folders
for (const folder of folderStructure) {
await createFolder(folder);
}

// Array of objects storing the info about the files to create
const files = [
{fName: “Billing and Coding”, fPath: “Charting Tips”},
{fName: “Clinical Pearls”, fPath: “Charting Tips”},
{fName: “Common Clinic Questions”, fPath: “Charting Tips”},
{fName: “Epic Troubleshooting”, fPath: “Charting Tips”},
{fName: “Electrostimulation”, fPath: “Disorders and Conditions/Treatments and Modalities/Physical Medicine”},
{fName: “HVLA”, fPath: “Disorders and Conditions/Treatments and Modalities/Physical Medicine”},
{fName: “Hydrotherapy”, fPath: “Disorders and Conditions/Treatments and Modalities/Physical Medicine”},
{fName: “Muscle Energy Stretches”, fPath: “Disorders and Conditions/Treatments and Modalities/Physical Medicine”},
{fName: “Behavioral Med”, fPath: “Disorders and Conditions/Treatments and Modalities”},
{fName: “Botanical Med”, fPath: “Disorders and Conditions/Treatments and Modalities”},
{fName: “Environmental Med”, fPath: “Disorders and Conditions/Treatments and Modalities”},
{fName: “Formulations”, fPath: “Disorders and Conditions/Treatments and Modalities”},
{fName: “Homeopathy”, fPath: “Disorders and Conditions/Treatments and Modalities”},
{fName: “Nutrition”, fPath: “Disorders and Conditions/Treatments and Modalities”},
{fName: “Pharmaceuticals”, fPath: “Disorders and Conditions/Treatments and Modalities”},
{fName: “Supplementation”, fPath: “Disorders and Conditions/Treatments and Modalities”},
{fName: “By Functional Disorders”, fPath: “Disorders and Conditions”},
{fName: “By Health History”, fPath: “Disorders and Conditions”},
{fName: “By Organ”, fPath: “Disorders and Conditions”},
{fName: “By Patterns of Pain”, fPath: “Disorders and Conditions”},
{fName: “By Symptoms”, fPath: “Disorders and Conditions”},
{fName: “Healthy Anatomy”, fPath: “”},
{fName: “Wellness Support”, fPath: “”},
// New files under Examination
{fName: “Labs and Imaging”, fPath: “Examination”},
{fName: “Orthopedic Testing”, fPath: “Examination”},
{fName: “Physical Exam”, fPath: “Examination”}
];

// Counter for created files
let createdFiles = 0;

// Create files
for (let f of files) {
const filePath = ${topFolder}/${sanitizedName}/${f.fPath}.replace(//+$/, ‘’);
const fileName = ${f.fName}.md;
const fullPath = ${filePath}/${fileName};

try {
    await app.vault.create(fullPath, '');
    console.log(`Created: ${fullPath}`);
    createdFiles++;
} catch (error) {
    if (error.message.includes('already exists')) {
        console.log(`Skipped (already exists): ${fullPath}`);
    } else {
        console.error(`Error creating ${fullPath}: ${error.message}`);
    }
}

}

// Provide feedback
if (createdFiles > 0) {
console.log(✅ Created ${createdFiles} files for the "${sanitizedName}" body system.);
} else {
console.log(ℹ️ No new files were created. All files for "${sanitizedName}" already exist.);
}

// Return a message to be inserted into the current note
tR = Body system structure created for: ${sanitizedName};
-%>

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