Hello,
this is the fourth iteration of Meta Template Picker, I am giving it a new name SMTP, where “S” is for super. We use super-templates based on dictionaries instead of the vanilla templates with only one class.
This templater template does:
- Creates a new note based on tens of templates that were generated from dictionaries
- Creates a note and automatically moves it to a designated folder in its own dictionary
- Creates a minimal note if the user presses Escape, or selects from the suggester.
Here is the code. This has to be copied to your templates folder.
<%*
/* Load all super templates
1. Read all super templates from folder set below
2. Load all variables from the super-templates into a super array
3. Each member of the new array will contain two arrays, "common" and "main" which are defined in each super template. "common" contains properties in common to one class; "main" is an array that contains properties that share a class.
4. The purpose of the super-templates is reducing the number of templater files and make it easier to maintain
*/
const templatesFolder = ['Extras/Templates/super templates'];
const test = false;
const templateFiles = [] // names of super template files
for (const folder of templatesFolder) {
const files = (await app.vault.adapter.list(folder))?.files || []
files.sort((a, b) => a.localeCompare(b)) // alphabetically
templateFiles.push(...files)
}
if (!templateFiles) return // no super template found
const templates = templateFiles.map(path => {
// read base names of super-templates
const file = app.vault.getAbstractFileByPath(path)
return file.basename
})
const superTemplates = []; // array will contain all super-templates
for (item in templates) { // iterate through each super-template
superTemplates.push(await tp.file.include(`[[${templates[item]}]]`))
} // super template variables loaded in super array
let title = tp.file.title; // grab current title
if (title.startsWith("Untitled")) {
/*
Create a note through a nested suggester of templates.
Two options here:
1. A new note prompt to type in the name of the note. This title may or may not contain a prefix. Call "newNoteFromPrefix"
2. More templates options where the classes and prefixes in the super-templates are exposed. Call "newNoteThroughSelectors"
*/
const options = ["New Note", "More Templates"];
const choice = await tp.system.suggester(options, options)
switch(choice) {
case "New Note":
title = await tp.system.prompt("Note Name")
if (!title) break // aborted
tR += await newNoteFromPrefix(superTemplates) // new note
break;
case "More Templates": // show a templater suggester
tR += await newNoteFromUntitled(superTemplates);
break;
default: // nothing selected
tR += await tp.file.include("[[Default Minimal Template]]");
}
} else { // Create a note if it has a title and a prefix
tR += await newNoteFromPrefix(superTemplates)
}
/**
CREATING A NEW NOTE THAT IS AN EXISTING LINK WITH A PREFIX
The user clicked on a link-title typed in a note. Process according to prefix of link-title in a note. This assumes we are starting with a note not named "Untitled" because the note is receiving its title from the link
* @param {array} sT - an array with all super-templates
* return {string} frontmatter
*/
async function newNoteFromPrefix(sT) {
let choice;
let chosen = findPrefixInSuperArray(sT);
let cleanTop;
let cleanBot;
let dataview;
let folder;
if (chosen) {
console.log("Found prefix in title: ", title)
const common = chosen.common;
const main = chosen.main;
let properties = cleanFrontmatter(main.fields)
if (main.exclude) { // we have properties in "exclude"
cleanTop = cleanRemovedExcluded(common[0].fields, main.exclude)
cleanBot = cleanRemovedExcluded(common[1].fields, main.exclude)
} else { // no exclusions defined in "exclude"
cleanTop = cleanFrontmatter(common[0].fields)
cleanBot = cleanFrontmatter(common[1].fields)
} // done with excluding properties
dataview = main.dataview // add dataview function if exists
? addTableBelowFrontmatter(main.dataview)
: ''
cleanBot = replace_uuid(cleanBot);
title = extractTitle(title); // get rid of the prefix
await tp.file.rename(title);
folder = main.folder ? main.folder + "/" : "/"
await tp.file.move(folder + title); // move to class sub-folder
if (test) tR += cleanTop + properties + cleanBot // frontmatter
return addFrontmatter(cleanTop + properties + cleanBot)
+ dataview
} else {
// no prefix provided, create a minimal note
console.log("No prefix in title. Add default properties to: ", title)
setTimeout(() => {tp.file.rename(title)}, 350)
return await tp.file.include("[[Default Minimal Template]]");
}
}
/*
* CREATING A NEW NOTE USING A NESTED SUGGESTER
* The user is creating a note from scratch by pressing Ctrl N. By default, the name of the new note is “Untitled”. We start by checking if the note being created is named “Untitled”. Otherwise, it means the note candidate is a link which contains the title plus a prefix. The prefix indicates the folder or tags as destinations.
*
* @param {array} superTemplate - an array of super templates
* return {string} frontmatter
*
*/
async function newNoteFromUntitled(sT) {
let result = await newNoteThroughSelectors(sT)
if (result == 0) { // Escape or Enter pressed
new Notice("Escape or Enter pressed")
title = "Untitled-" + get_uuid(false)
return await tp.file.include("[[Default Minimal Template]]");
}
return result
}
/**
* Nested suggester for all super-templates.
* @param {array} superTemplate - an array of super templates
* return {string} frontmatter
*/
async function newNoteThroughSelectors(superTemplate) {
const classes = getSuperClasses(superTemplate);
const chosen = await tp.system.suggester(classes, superTemplate)
if (!chosen) return 0
const common = chosen.common; // load common properties
const main = chosen.main; // load core propertiees
// wait a bit for the suggester to return the array
await new Promise((resolve, reject) => setTimeout(resolve, 300));
let mainNames = getValuesToListGivenKey(main, "name") // main
const selected = await tp.system.suggester(mainNames, main)
if (!selected) return 0;
let topProperties = getTopProperties(common) // read top properties
let botProperties = getBottomProperties(common) // bottom props
if (selected.exclude) { // there is a list of exclusions
topProperties = removeExcludedKeys(topProperties, selected.exclude)
botProperties = removeExcludedKeys(botProperties, selected.exclude)
botProperties = replace_uuid(botProperties);
} // properties in exclude removed
let dataview = selected.dataview // add dataview function if exists
? addTableBelowFrontmatter(selected.dataview)
: ''
let coreProperties = cleanFrontmatter(selected.fields)
return addFrontmatter(topProperties + coreProperties
+ botProperties) + dataview
}
/**
* Get the name of the classes for all super-templates.
* The class name in in the array "common"
* @param {array} sT - array of objects
* return {array} name of classes
*/
function getSuperClasses(sT) {
let result = [];
sT.forEach(i => {
result.push(i.common[2].class)
})
return result;
}
/**
* Make an uuid to time stamp each new note. If true, will return frontmatter as "uuid: '20240220103600'"; if false, the string 20240220103600
* @param {boolean} withProperty
* return {string} an uuid string or the uuid property
*/
function get_uuid(withProperty) {
return withProperty
? "uuid: " + "'"
+ tp.file.creation_date("YYYYMMDDHHmmss") + "'"
: tp.file.creation_date("YYYYMMDDHHmmss")
}
/**
* Replace the uuid in current properties
* @param {string} a YAML like string
* return {string} a string with an updated uuid
*/
function replace_uuid(txt) {
return txt + get_uuid(true)
}
/**
* Return an array of values for a given dictionary and key. Important for providing the prefixes that validate an entry
* @param {array} arr - array of objects
* @param {string} key - Title / filename of the new note
* return {array}
*/
function getValuesToListGivenKey(arr, key) {
let result = [];
arr.forEach(item => {
let dict = item;
let value = dict[key];
// console.log(`${key}: ${value}`);
result.push(value)
})
return result;
}
/**
* Remove blank lines, spaces and tabs at the start of frontmatter caused by the indentation in the dictionary
* @param {string} yml - a YAML like text
* return {string} without blank lines
**/
function cleanFrontmatter(yml) {
const regex = /^(?=\n)$|^\s*|s*$|\n\n+/gm // line breaks, spaces
yml = yml.split('\n')
.map(function(line) {
return line.replace(regex, "")})
.filter(function(x) {return x}).join("\n");
return yml + "\n"; // adding blank line so no mixed with next
}
/**
* Get the common properties, top or bottom depending of the index
* @param {array} arr - the common array from a super-template
* @param {integer} idx - a number 0 or 1. 0 for top; 1 for bottom
* return {array} common properties
*/
function getCommonProperties(arr, idx) {
return arr[idx].fields
}
/**
* Get the common top properties
* @param {array} arr - the common array from a super-template
* return {string}
*/
function getTopProperties(arr) {
return cleanFrontmatter(getCommonProperties(arr, 0))
}
/**
* Get the common bottom properties
* @param {array} arr - the common array from a super-template
* retrun {string}
*/
function getBottomProperties(arr) {
return cleanFrontmatter(getCommonProperties(arr, 1))
}
/**
* Print the frontmatter to the note
* @param {string} yml - text with properties
* return {void}
*/
function printFrontmatter(yml) {
tR += "---\n"
tR += yml
tR += "---\n"
}
/**
* Read the field prefix from each dictionary in the array
* @param {array} arr - array of objects
* return {array} list of all prefixes in super-templates
*/
function getPrefixValuesToList(arr) {
const key = "prefix";
return getValuesToListGivenKey(arr, key)
}
/**
* Find the prefix in the note title in the super-templates. It will iterate through each array in the super array and will stop when finding a match.
* @param {array} superArr - array of super-templates
* return {array} matched dictionary with super-template variables
*/
function findPrefixInSuperArray(superArr) {
console.log(superArr)
let choice;
let main;
let common;
superArr.forEach(item => {
item.main.map(d => {
if (test) console.log(d.prefix);
if (title.startsWith(d.prefix)) {
choice = item;
main = d
}
})
})
return !common && !main
? undefined
: { common: choice.common, main: main };
}
/**
* Clean the YAML string and remove the excluded properties
* @param {string} txt - a string of properties or fields
* @param {array} exclude - a list with the properties to exclude
* return {string} a clean text with some properties removed
*/
function cleanRemovedExcluded(txt, exclude) {
return cleanFrontmatter(removeExcludedKeys(txt, exclude));
}
/**
* Remove properties specified in the array exclude because some classes do not need all the common properties.
* @param {string} txt - original fields
* @param {array} exclude - list of exclusions given by "main"
* return {string} removed properties
*/
function removeExcludedKeys(txt, exclude) {
removeList = exclude.map(p => p+":")
var expStr = removeList.join("|");
return txt
.replace(new RegExp(expStr, 'gi'), ' ')
.replace(/\s{2,}/g, '\n') // remove void lines after exclusion
}
/**
* Add dashes to properties to form the YAML frontmatter
* @param {string} yml - a string of properties or fields
* return {string} frontmatter
*/
function addFrontmatter(yml) {
return "---\n" + yml + "\n---\n"
}
/**
* Print YAML frontmatter after adding dashes to properties
* @param {string} yml - a string of properties or fields
* return {void}
*/
function printFrontmatter(yml) {
tR += "---\n"
tR += yml
tR += "\n---\n"
}
/**
* Extract the title after the prefix. Date regex to detect a date in the title. We don't want to split it at yyyy- or mm-
* @param {string} title - a note title with a prefix
* return {string} note title
*/
function extractTitle(title) {
let result;
let count = (title.match(/-/g) || []).length;
let dateRegex = /(\d{4})([-])(\d{2})([-])(\d{2})/;
switch (count) {
case 0: // title has no dash
result = title.trim()
break
case 1: // title has one dash
result = title.split("-").slice(1)[0].trim()
break
default: // more than one dash
// title could be a date "2024-02-21", or
// of the form "prefix-Battery-powered-device"
result = dateRegex.test(title) && count == 2
? title.trim()
: title.split("-").slice(1).join("-").trim()
}
return result;
}
/**
* Insert a Yaml like string along the dataview query
* @param {string} fun - a function as a string
* return {string} text with a dataview block including the resolved function with DQL code
*/
function addTableBelowFrontmatter(fun) {
return "\n\n```dataview\n" + cleanFrontmatter(eval(fun))
+ "```"
}
/**
* Returns a dataview table showing notes where their properties up and related contain a link to the title of the current note. There could be as many as these DV functions as needed. But they have to be entered as strings in their respective dictionary.
* @param {void}
* return {string} the Dataview Query as text
*/
function getTableUpRelated() {
let yml = `
TABLE up, related
FROM ""
WHERE up = link(this.file.name)
OR contains(related, link(this.file.name))
SORT file.name ASC
`
return yml
}
-%>
The super-templates could be copied to a folder under templates as well.
This is how the super-templates would look like. I am sharing three.
The Atlas super-template takes care of creating the core organization of your Obsidian vault. In some way I have been inspired by @nickmilo on this part. No vaults can easily grow or maintained without core notes. This stresses the point that links are way more important than tags. At least in Obsidian.
Atlas Super Template
<%*
/**
* List of dictionaries/objects for each subclass are defined here. This the central place where to control all the properties.
* common {array} contains common properties to all sub-classes. Contains three unnamed dictionaries. [0] contains the common properties at the top; [1] common properties at the bottom; [2] name of the main class.
* main {array} contains attributes and frontmatter properties for each subclass. This array contains a flexible or dynamic number of dictionaries. As many as the user requires. Each dictionary correspond to a subclass. The mandatory keys are: name, prefix, folder, and fields. The other keys (exclude, dataview) are optional.
* name: name of the dictionary
* prefix: prefix in note title
* folder: where the note is going to be moved
* dataview: a function that will return a Dataview table below the frontmatter
* exclude: an array of properties to exclude
* fields: Frontmatter properties
*/
const common = [
{ name: "CommonTop",
prefix: "top",
description: "Common properties at the top of frontmatter",
fields: `
aliases:
type:
archetype:
`
},
{ name: "CommonBottom",
prefix: "bot",
description: "Common properties at the bottom of frontmatter",
fields: `
status:
rating:
summary:
up:
related:
uuid:
source:
`
},
{
name: "config",
class: "Atlas",
template: "Atlas Super Template"
}
]
const main = [
{ name: "Collection",
prefix: "collect",
exclude: ["source"],
dataview: "getTableUpRelated()",
folder: "Atlas/Collections",
fields: `
tags: map/collection
archetype: "[[Collections]]"
`
},
{ name: "Company",
prefix: "comp",
exclude: ["rating", "source"],
folder: "Atlas/Companies",
dataview: "getTableUpRelated()",
fields: `
industry:
country:
address:
website:
linkedin:
tags: map/company
archetype: "[[Companies]]"
`
},
{ name: "Concept",
prefix: "concept",
exclude: ["source"],
folder: "Atlas/Concepts",
dataview: "getTableUpRelated()",
fields: `
tags: map/concept
archetype: "[[Concepts]]"
`
},
{ name: "Discipline",
prefix: "disc",
exclude: ["rating", "source"],
folder: "Atlas/Disciplines",
fields: `
tags: map/discipline
archetype: "[[Disciplines]]"
`
},
{ name: "Event",
prefix: "eve",
folder: "Calendar/Events",
fields: `
type: "[[event]]"
when:
tags: map/events
archetype: "[[Calendar]]"
`
},
{ name: "Industry",
prefix: "ind",
exclude: ["rating", "source"],
dataview: "getTableUpRelated()",
folder: "Atlas/Industries",
fields: `
tags: map/industry
archetype: "[[Industries]]"
`
},
{ name: "Location",
prefix: "loc",
exclude: ["status", "rating", "source"],
dataview: "getTableUpRelated()",
folder: "Atlas/Locations",
fields: `
country:
state:
region:
tags: map/location
archetype: "[[Locations]]"
`
},
{ name: "Media",
prefix: "med",
exclude: ["up", "related", "rating", "source"],
folder: "Atlas/Media",
fields: `
tags: map/media
archetype: "[[Media]]"
`
},
{ name: "Object",
prefix: "obj",
exclude: ["status", "up", "related", "source"],
folder: "Atlas/Objects",
fields: `
tags: map/object
archetype: "[[Objects]]"
`
},
{ name: "People",
prefix: "people",
exclude: ["status", "rating", "source"],
dataview: "getTableUpRelated()",
folder: "Atlas/People",
fields: `
known_for:
company:
BU:
position:
role:
reports_to:
email:
address:
phone:
city:
linkedin:
github:
tags: map/person
archetype: "[[People]]"
`
},
{ name: "Private",
prefix: "priv",
exclude: ["rating", "summary", "status", "source"],
folder: "Spaces/Private",
fields: `
tags: private
archetype: "[[Private]]"
`
},
{ name: "Rating",
prefix: "rating",
exclude: ["rating", "status", "up", "related", "source"],
folder: "Atlas/Ratings",
fields: `
tags: map/rating
archetype: "[[Ratings]]"
`
},
{ name: "Realm",
prefix: "realm",
exclude: ["rating", "up", "related", "source"],
folder: "Atlas/Realms",
fields: `
tags: map/realm
archetype: "[[Realms]]"
`
},
{ name: "Status",
prefix: "status",
exclude: ["rating", "status", "up", "related", "source"],
folder: "Atlas/Status",
fields: `
tags: map/status
archetype: "[[Status]]"
`
},
{ name: "Type",
prefix: "type",
exclude: ["rating", "status", "up", "related", "source"],
folder: "Atlas/Types",
fields: `
tags: map/type
archetype: "[[Types]]"
`
},
]
let atlasArray = []
atlasArray.common = common;
atlasArray.main = main;
return atlasArray; // pass the named arrays to the master template
-%>
Stuff Super Template
This is another super-template with one common class and several dictionaries that carry the core properties for each sub-template.
<%*
// Define my document types
const common = [
{
name: "CommonTop",
prefix: "top",
fields: `
aliases:
type:
archetype: "[[My Stuff]]"
`
},
{
name: "CommonBottom",
prefix: "bot",
fields: `
summary:
purpose:
rating:
source:
`
},
{
name: "config",
class: "Stuff",
template: "Stuff Super Template"
}
]
const main = [
{ name: "My Articles",
prefix: "myart",
folder: "My Stuff",
fields: `
published:
journal:
repository:
partition:
folder:
up: "[[My Articles]]"
related:
status:
tags: content/article
`
},
{ name: "My Code",
prefix: "mycod",
folder: "My Stuff",
exclude: ["source"],
fields: `
gist_url:
partition:
folder:
language:
up: "[[My Code]]"
related:
status: active
tags: content/code
`
},
{ name: "My Comments",
prefix: "mycom",
folder: "My Stuff",
fields: `
prefix: "mycom",
published:
journal:
up: "[[My Comments]]"
related:
status:
tags: content/comment
`
},
{ name: "My Computers",
prefix: "mycomp",
folder: "My Stuff",
fields: `
purchased:
brand:
cost:
CPU:
RAM:
OS:
use:
assigned:
location: "[[At Home]]"
up: "[[Equipment]]"
related:
status: unknown
tags: digital/assets
`
},
{ name: "My Devices",
prefix: "mydev",
folder: "My Stuff",
fields: `
purchased:
brand:
cost:
use:
location: "[[At Home]]"
up: "[[My Devices]]"
related:
status: unknown
tags: digital/assets
`
},
{ name: "My Contributions",
prefix: "mycontrib",
folder: "My Stuff",
fields: `
published:
journal:
up: "[[My Contributions]]"
related:
status:
tags: content/contributions
`
},
{ name: "My Packages",
prefix: "mypkg",
folder: "My Stuff",
fields: `
published:
repository:
partition:
folder:
OS:
language:
up: "[[My Packages]]"
related:
status:
tags: code/packages
`
},
{ name: "My Partitions",
prefix: "mypart",
folder: "My Stuff",
fields: `
label:
name:
OS:
size:
up: "[[My Partitions]]"
related:
status:
tags: system/partitions
`
},
{ name: "My Portable Disks",
prefix: "myporta",
folder: "My Stuff",
fields: `
purchased:
brand:
cost:
use:
size:
location: "[[At Home]]"
up: "[[My Portable Disks]]"
related:
status: unknown
tags: digital/assets
`
},
{ name: "My Portfolio",
prefix: "myportf",
folder: "My Stuff",
fields: `
published:
language:
partition:
folder:
up: "[[My Portfolio]]"
related:
status: unknown
tags: content/portfolio
`
},
{ name: "My Repositories",
prefix: "myrepos",
folder: "My Stuff",
fields: `
created:
url:
branch:
partition: "[[2560x]]"
folder:
language:
up: "[[Programming]]"
related:
status: unknown
is_synced: false
tags: content/code
`
},
{ name: "My Transcripts",
prefix: "mytransc",
folder: "My Stuff",
fields: `
published:
journal:
url:
partition:
folder:
up: "[[My Transcripts]]"
related:
status: unknown
tags: content/transcript
`
},
{ name: "My Virtual Machines",
prefix: "myvm",
folder: "My Stuff",
fields: `
created:
last_used:
brand:
OS:
size:
RAM:
network:
partition:
folder:
up: "[[My Virtual Machines]]"
related:
status: unknown
tags: digital/virtual
`
},
]
let obj = [];
obj.common = common;
obj.main = main;
return obj;
-%>
The idea of Meta Template Picker comes from Bryan Jenks and Pamela Wang. Their ideas was instrumental in order to come to this super-templates. Special thanks to @holroy and @gino_m for their help, contributions and ideas. From @AlanG I got the inspiration for a better code. I am not sure if I achieve that goal though.
EDIT 1
I have modified the code above to allow the inclusion of dataview functions with each of the sub-classes. The function has to be entered as string in the dictionary, otherwise they will try to immediately execute requiring the function to be present in each super-template. I found that is better that the function is executed from a central location, the master template, where all the functions should live. The function is executed inside the master template with eval()
.
Before the function was entered as
datatable: getTableUpRelated(),
Now, should be like this:
dataview: "getTableUpRelated()",