New Properties and templater prompts and commands?

I’m glad you like it. As for the showcase, I was actually hoping to get some feedback before then. I will share the code below here.

In trying to optimize the performance, I actually made two versions, one using a switch, and one using an object of functions. Both versions seem fairly fast most of the time, but it varies. It’s a bit hard to judge these things from my end, because my PC is not very powerful.

I’m still a bit unsure about the need for async in this context. In the object-based script, I didn’t make the frontmatterCallback function async, and it still works, but I’m not sure if it affects the performance.

Code

merge_frontmatter_switch
/**
 * Merge frontmatter properties into a target file's frontmatter.
 * @param {Tfile} target - Target's TFile
 * @param {Array<Object>} properties - Array of property objects to merge.
 * @param {string|null} [override=null] - Optional parameter to specify override behavior.
 */
async function mergeFrontmatter(target, properties, override = null) {

    try {
        // Get the target file's frontmatter cache
        const fileCache = app.metadataCache.getFileCache(target)?.frontmatter;

        // Handle existing properties according to treatment
        const handleProperty = async (frontmatter, prop) => {
            const value = fileCache?.[prop.key];

            // Switch based on the treatment type of the property
            switch (prop.treatment) {
                case 'delete':
                    // Delete frontmatter property if override is not set to safe
                    if (override !== "safe") {
                        console.log(`Deleting ${prop.key} from ${target.basename}`);
                        delete frontmatter[prop.key];
                    }
                    break;

                case 'custom':
                    // Apply custom code, if override is not set to safe
                    if (override !== "safe") {
                        try {
                            // Execute custom function to get new value
                            const customFunction = new Function(`return this.${prop.code}`);
                            const newValue = customFunction.call(value);
                            console.log(`Changing ${prop.key} from "${value}" to "${newValue}" using custom code: ${prop.code}`);
                            frontmatter[prop.key] = newValue;
                        } catch (error) {
                            console.error(`Error applying custom treatment ${prop.code} for ${prop.key}: ${error}`);
                        }
                    }
                    break;

                case 'increment':
                    // Increment property value with new value, if override is not set to safe
                    if (override !== "safe") {
                        console.log(`Incrementing ${prop.key} by ${prop.value} in ${target.basename}`);
                        frontmatter[prop.key] += prop.value;
                    }
                    break;

                case 'add':
                    // Check if property value is not already set
                    if (!value || (value && override === "destructive" && value !== prop.value)) {
                        console.log(`Assigning ${prop.value} to ${prop.key} in ${target.basename}`);
                        // If value is null or override is destructive, set property value to new value
                        frontmatter[prop.key] = prop.value;
                    } else if (value && Array.isArray(value) && prop.value != null) {
                        // Push new values into existing array
                        arrayPush(target, frontmatter, value, prop);
                    }
                    break;

                case 'update':
                    // Replace property value with new value, if override is not set to safe
                    if ((override !== "safe") && (value !== prop.value)) {
                        console.log(`Updating ${prop.key} to ${prop.value} in ${target.basename}`);
                        frontmatter[prop.key] = prop.value;
                    }
                    break;

                default:
                    if (Array.isArray(value) && prop.value != null) {
                        arrayPush(target, frontmatter, value, prop);
                    }
                    break;
            }
        };

        // Callback function to handle frontmatter
        const frontmatterCallback = async (frontmatter) => {
            await Promise.all(properties.map(async (prop) => {
                if (!fileCache || !fileCache.hasOwnProperty(prop.key)) {
                    console.log(`${target.basename} doesn't contain ${prop.key}`);
                    console.log(`Adding property: ${prop.key} to ${target.basename} with value: ${prop.value}`);
                    frontmatter[prop.key] = prop.value;
                } else {
                    console.log(`${target.basename} contains property: ${prop.key}`);
                    // Handle existing property
                    await handleProperty(frontmatter, prop);
                }
            }));
        };

        // Process frontmatter of the target file
        await app.fileManager.processFrontMatter(target, frontmatterCallback);
    } catch (error) {
        console.error("Error in processing frontmatter: ", error);
    }
}

// Function to push new elements into existing array property
function arrayPush(target, frontmatter, value, prop) {
    console.log(`${prop.key} is an array`);
    const valueSet = new Set(value);
    for (const item of prop.value) {
        if (!valueSet.has(item)) {
            console.log(`Adding ${item} to ${prop.key} in ${target.basename}`);
            frontmatter[prop.key].push(item);
        }
    }
}

module.exports = mergeFrontmatter;
merge_frontmatter_function
/**
 * Merge frontmatter properties into a target file's frontmatter.
 * @param {Tfile} target - Target's TFile
 * @param {Array<Object>} properties - Array of property objects to merge.
 * @param {string|null} [override=null] - Optional parameter to specify override behavior.
 */
async function mergeFrontmatter(target, properties, override = null) {
    
    try {
        // Get the target file's frontmatter cache
        const fileCache = app.metadataCache.getFileCache(target)?.frontmatter;

        // Object containing treatment functions for different property treatments
        const treatments = {
            'delete': (frontmatter, prop) => {
                console.log(`Deleting ${prop.key} from ${target.basename}`);
                if (override !== 'safe') delete frontmatter[prop.key];
            },
            'custom': (frontmatter, prop, value) => {
                if (override !== 'safe') {
                    try {
                        const customFunction = new Function(`return this.${prop.code}`);
                        const newValue = customFunction.call(value);
                        console.log(`Changing ${prop.key} from "${value}" to "${newValue}" using custom code: ${prop.code}`);
                        frontmatter[prop.key] = newValue;
                    } catch (error) {
                        console.error(`Error applying custom treatment ${prop.code} for ${prop.key}: ${error}`);
                    }
                }
            },
            'increment': (frontmatter, prop) => {
                if (override !== 'safe') {
                    console.log(`Incrementing ${prop.key} by ${prop.value} in ${target.basename}`);
                    frontmatter[prop.key] += prop.value;
                }
            },
            'add': (frontmatter, prop, value) => {
                if (!value || (value && override === 'destructive' && value !== prop.value)) {
                    console.log(`Assigning ${prop.value} to ${prop.key} in ${target.basename}`);
                    frontmatter[prop.key] = prop.value;
                } else if (value && Array.isArray(value) && prop.value != null) {
                    arrayPush(target, frontmatter, value, prop);
                }
            },
            'update': (frontmatter, prop, value) => {
                if ((override !== 'safe') && (value !== prop.value)) {
                    console.log(`Updating ${prop.key} to ${prop.value} in ${target.basename}`);
                    frontmatter[prop.key] = prop.value;
                }
            }
        };

        // Callback function to handle frontmatter
        const frontmatterCallback = (frontmatter) => {
            properties.forEach((prop) => {
                const value = fileCache?.[prop.key];

                // Check if property exists, if not, add it

                if (!fileCache || !fileCache.hasOwnProperty(prop.key)) {
                    console.log(`${target.basename} doesn't contain ${prop.key}`);
                    console.log(`Adding property: ${prop.key} to ${target.basename} with value: ${prop.value}`);
                    frontmatter[prop.key] = prop.value;

                // If property exists, handle it according to treatment type   

                } else {
                    console.log(`${target.basename} contains property: ${prop.key}`);
                    if (treatments.hasOwnProperty(prop.treatment)) {
                        treatments[prop.treatment](frontmatter, prop, value);
                    } else {
                        // Default treatment
                        treatments['add'](frontmatter, prop, value);
                    }
                }
            });
        };
        // Process frontmatter of the target file
        await app.fileManager.processFrontMatter(target, frontmatterCallback);
    } catch (error) {
        console.error("Error in processing frontmatter: ", error);
    }
}

function arrayPush(target, frontmatter, value, prop) {
    if (Array.isArray(value) && prop.value != null) {
        console.log(`${prop.key} is an array`);
        const valueSet = new Set(value);
        for (const item of prop.value) {
            if (!valueSet.has(item)) {
                console.log(`Adding ${item} to ${prop.key} in ${target.basename}`);
                frontmatter[prop.key].push(item);
            }
        }
    }
}

module.exports = mergeFrontmatter;

Usage

In both scripts, properties can have the following treatments:

  • add: Only adds values to keys without values, and pushes additional values to arrays.
  • update: Replaces the existing value with a new one.
  • increment: Increases the existing value by the new value. (should probably be renamed)
  • delete: Deletes the key / property.
  • custom: Applies any custom code for string manipulation like toUpperCase(), slice(0,4), replace().

If no treatment is specified, the default condition equals add.

There are two override parameters: safe and destructive.

  • safe: Prevents all changes that overwrite existing properties, allowing only for add.
  • destructive: Makes add work like update.

Usage example

<%*
tp.hooks.on_all_templates_executed(async () => {
	const date = tp.date.now("YYYY-MM-DD HH:mm");
	const parentFile = tp.config.active_file;
	// Use tp.config.target_file to target the current file
	const targetFile = tp.config.target_file;
    const parentLink = app.fileManager.generateMarkdownLink(parentFile, tp.file.folder(true));
	const properties = [
        // Adding keys without values
    	{key:'connections', value: null},
        {key:'aliases', value: null},
        // Using add to add to existing values
        {key:'tags', value: ['tag3', 'tag4'], treatment: 'add'},
        // Increase existing value by new value
        {key:'number', value: 2, treatment: 'increment'},
        // Applying custom code
        {key:'status', treatment: 'custom', code: 'toUpperCase()'},
        {key:'too-long', treatment: 'custom', code: 'slice(0,7)'},
        // Add does nothing, if the keys exists with a single value
        // But update always replaces old value with new value
        {key:'date-created', value: date, treatment: 'add'},
        {key:'date-modified', value: date, treatment: 'update'},
        // Passing a variable as a value
        {key:'origin', value: parentLink},
        // Deleting a key
        {key:'toDelete', treatment: 'delete'}
    ];
	await tp.user.merge_frontmatter_function2(targetFile, properties, tp);
});
-%>

Edit: I refactored the scripts so that the target TFile is a parameter you pass to the user script. Initially, I was only using the script to target the current file, so the target was hard-coded as such. But obviously, it’s much more powerful if you can use it in a loop and pass the target file.

3 Likes