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 likeupdate
.
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.