A Meta Template Picker iteration to manage multiple templates based on dictionaries

As a result of a recent question on how to centralize the maintenance of several templates from a master template, I came up with this iteration of the Meta Template Picker (MTP), whose idea makers are @tallguyjenks and Pamela Wang.

This Meta Template Picker uses a dictionary (object) for each of the templates. The idea of the dictionaries was recommended by @holroy, and the outcome debated with @gino_m. Thanks to both.

In this MTP, the templates do not live in files but are inline described in each dictionary. It works for me because I was having trouble opening each template file every time I wanted to add/modify a frontmatter property. With this approach I always have an immediate glance at all my frontmatter properties. Also, as you may have experimented, some properties repeat from one template to another. That is why you see a “common” dictionary there.

The use is identical to the video by Pamela Wang; have to place the meta template in templater options under the root folder. Maybe a very small deviation is a call to “Default Minimal Template” which will create a note with only four properties.

Here is the Meta Template Picker:

<%*
/* "Meta Template Picker"  idea makers: Bryan Jenksa and Pamela Wang
	This iteration by Alfonso R. Reyes. Houston, Texas.
 */
 // Common properties will be at the top of frontmatter
const common = {
	  name: "Common",
	  fields: `
	    aliases: 
		type: 
		archetype: 
		summary: 
		status: 
	    rating: 
	    up: 
	    related:
	  `
	}
/* List of dictionaries/objects are defined here
   A central place where to control the properties
   
   name:      is the name of the inline-template
   short:     will be used as a prefix for inline-template selection
   long:      will be used for the sub-folder
   datatable: a Dataview table added below the frontmatter
   fields:    properties of the inline-template
*/
const choices = [
  { name: "Collection",
	short: "collect",
	long: "Collections", 
	datatable: getTableUpRelated(),
    fields: `
		tags: map/collection
		archetype: "[[Collections]]"
    `
  },
    
  { name: "Company",
    short: "comp",
    long: "Companies",
    fields: `
	    country: 
	    industry: 
	    address: 
	    website: 
	    linkedin: 
		tags: map/company
		archetype: "[[Companies]]"
    `
  }, 
   
  { name: "Concept",
    short: "concept",
    long: "Concepts",
    fields: `
		tags: map/concept
		archetype: "[[Concepts]]"
    `
  }, 
  
  { name: "Discipline",
    short: "disc",
    long: "Disciplines",
    fields: `
		tags: map/discipline
		archetype: "[[Disciplines]]"
    `
  }, 
    
  { name: "Industry",
    short: "ind",
    long: "Industries",
    datatable: getTableUpRelated(), 
    fields: `
		tags: map/industry
		archetype: "[[Industries]]"
    `
  },     
  
  { name: "Location",
    short: "loc",  
    long: "locations", 
    datatable: getTableUpRelated(), 
    fields: `
	    country: 
	    state: 
	    region: 
		tags: map/location
		archetype: "[[Locations]]"
    `
  },
  
  { name: "Media",
    short: "med",  
    long: "Media",
    fields: `
		tags: map/media
		archetype: "[[Media]]"
    `
  },
  
  { name: "Object",
    short: "obj",  
    long: "Objects",
    fields: `
		tags: map/object
		archetype: "[[Objects]]"
    `
  },  
  
  { name: "People",
    short: "people",
    long: "People",
    datatable: getTableUpRelated(),
    fields: `
	    known_for: 
	    company: 
	    BU:
	    position: 
	    role: 
	    reports_to: 
	    email: 
	    address: 
	    phone: 
	    city: 
	    linkedin: 
	    github: 
		tags: map/person
		archetype: "[[People]]"
    `
  },    

  { name: "Rating",
    short: "rating",
    long: "Ratings",
    fields: `
		tags: map/rating
		archetype: "[[Ratings]]"	    
    `
  },

  { name: "Realm",
    short: "realm",  
    long: "Realms",
    fields: `
		tags: map/realm
		archetype: "[[Realms]]"
    `
  },
  
  { name: "Status",
    short: "status",
    long: "Status",  
    fields: `
		tags: map/status
		archetype: "[[Status]]"
    `
  }, 
   
  { name: "Type", 
    short: "type", 
    long: "Types", 
    fields: `
		tags: map/type
		archetype: "[[Types]]"
    `
  },
]

let uuid; let yml; let title; let choice;
let folder = "Atlas";  // this is the parent of sub-folders
// find a prefix match to select dictionary member
 choices.forEach(item => {
	 if (tp.file.title.startsWith(item.short)) {
		 choice = item; }
})
// if the prefix was found
if (choice) {
	uuid = get_uuid()
	yml = common.fields + choice.fields + uuid
	yml = cleanFrontmatter(yml);   // remove blank lines, spaces

	title = extractTitle(tp.file.title); // get rid of the prefix
	await tp.file.rename(title);
	folder = "Atlas/" + choice.long + "/";
	await tp.file.move(folder + title);   // move to sub-folder
	// add frontmatter to the new note
	addFrontmatter(yml);
	// add table view to new note, if defined
	if (choice.datatable) {
		addTableBelowFrontmatter(choice.datatable)
	}
	new Notice("New " + choice.name + " just added ...")
} else {
	/*  Note name prefix was not found in dictionary 
		or note is "Untitled",
		or note is already named and is a link
	*/
	if (tp.file.title.startsWith("Untitled")) {
		// most likely you pressed Ctrl+N (new note)
		title = await tp.system.prompt("Note Name")
			if (title === null)  {
				return;
				// pressing <Enter> creates "Untitled-20240220104000"
				// pressing <Esc> creates empty "Untitled"
			} else {
				// because sometimes title is not changed; too fast
				setTimeout(() => {tp.file.rename(title)}, 350)
			}
	} else {
		// note being created from a link has a dash but prefix unknown
		new Notice("Note has a name but not known prefix")
	}
	// append/run the minimal template when note prefix not known
	tR += await tp.file.include("[[Default Minimal Template]]");
}


function cleanFrontmatter(yml) {
	// remove blank lines, spaces and tabs at the start of frontmatter
	const regex = /^(?=\n)$|^\s*|s*$|\n\n+/gm   
	yml = yml.split('\n')
		.map(function(line) {
			return line.replace(regex, "")})
		.filter(function(x) {return x}).join("\n");
	return yml;
}

function get_uuid() {
	// Make an uuid to time stamp each new note
	return "uuid: " 
	+ "'" 
	+ tp.file.creation_date("YYYYMMDDHHmmss") 
	+ "'"
}

function getTableUpRelated() {
	// Dataview table showing notes 
	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
}

function addTableBelowFrontmatter(dt) {
		tR += "\n\n"
		tR += "```dataview"
		tR += "\n"
		tR += cleanFrontmatter(dt)
		tR += "\n"
		tR += "```"
		tR += "\n"
}

function addFrontmatter(yml) {
	tR += "---\n"
	tR += yml
	tR += "\n---\n"
}

function extractTitle(title) {
	let result;
	let count = (title.match(/-/g) || []).length;
	let dateRegex = /(\d{4})([-])(\d{2})([-])(\d{2})/;
	
	switch (count) {
		case 0:
			result = title.trim()
			break
		case 1: 
			result = title.split("-").slice(1)[0].trim()
			break
		default:
			result = dateRegex.test(title) && count == 2
				? title.trim() 
				: title.split("-").slice(1).join("-").trim()
	}
	return result;
}
_%>

And this the Default Minimal Template:

<%-*
/* Create a new note but including uuid formed by the date en time
*/
var title = tp.file.title
const uuid = tp.file.creation_date("YYYYMMDDHHmmss");

if (tp.file.title.startsWith("Untitled")) {
	title = tp.file.title + "-" + uuid   // add a timestamp
	await tp.file.rename(title);
}
-%>
---
tags: map/minimal
up: 
related: []
uuid:  '<% uuid %>'
---
<% await tp.file.move("Notes/" + title) %>

Of course, this can be improved, such as adding a sub-folder in the dictionary for each of the inline-templates; use lists instead of a string with backticks for the properties; do not create an "“Untitled” note when one aborts the prompt for a new note with ; replace some of the functions with native Obsidian functions; remove specific common properties, etc.

Hope you find this useful.

2 Likes

I tried a few and it works well. Thank you very much.

For anyone wondering what this does and how to set it up, it is similar to what the Bryan Jenks inspired Pamela Wang did with her Meta Templater (see on YouTube for how she makes it from scratch).

What I find nice in this script is that it is one script (as opposed to two) and there are many different scenarios that people can easily overwrite to suit their own requirements.

So you save the main script as e.g. Meta_Template_Picker_by_A_Reyes, add it to your Templater folder and in the Templater settings you set this as your Folder Template for the folder that is set in your Obsidian settings for new file creation.
In my case, it is ‘pages’:
Screenshot from 2024-02-21 03-04-36

Then when you create a new file, you can (optional) enter prefixes (collect, comp, etc for short – but you will rename these anyway for yourself with your own metadata corresponding to your use case) plus space + - (hyphen) + space + yourownfilename. E.g. people - John Doe.

  • If there’s no prefix - added to your filename, the script will default to a normal note.

I recommend playing around with it to see what it creates and where – it can actually teach people some javascript too.

Okay, Alfonso, you owe a beer for this write-up :slight_smile:

1 Like

@gino_m Thank you so much for the explanation. Pretty cool!


I have updated the Meta Template Picker.

Changes are:

  • Now you can press Enter or Escape at the prompt, and an untitled note with time stamp will be created.
  • Note classes can be entered from the prompt as well. If the MTP has been installed as @gino_m explained above, then Ctrl+N will automatically invoke the prompt. Enter the note with the class using a dash. Like so: loc-Dallas, where loc is short for the class location. It will automatically add the location Dallas as a note in the folder “Atlas/Locations”, or whatever the parent folder specified
  • Now, the common properties can be excluded at pleasure. For instance, the class Rating doesn’t need “rating”, “status”, “summary”, “up”, “related” properties. These values will have to be entered as an array named exclude. Like this:
  { name: "Rating",
    short: "rating",
    long: "Ratings",
    exclude: ["rating", "status", "summary", "up", "related"],
    fields: `
		tags: map/rating
		archetype: "[[Ratings]]"	    
    `
  },

Here is the updated Meta Template Picker:

<%*
/* "Meta Template Picker"  idea makers: Bryan Jenksa and Pamela Wang
	This iteration by Alfonso R. Reyes. Houston, Texas.
 */
 // Common properties will be at the top of frontmatter
const common = {
	  name: "Common",
	  fields: `
	    aliases: 
		type: 
		archetype: 
		summary: 
		status: 
	    rating: 
	    up: 
	    related:
	  `
	}
/* List of dictionaries/objects are defined here
   A central place where to control the properties
   
   name:      is the name of the inline-template
   short:     will be used as a prefix for inline-template selection
   long:      will be used for the sub-folder
   datatable: a Dataview table added below the frontmatter
   fields:    properties of the inline-template
*/
const choices = [
  { name: "Collection",
	short: "collect",
	long: "Collections", 
	datatable: getTableUpRelated(),
    fields: `
		tags: map/collection
		archetype: "[[Collections]]"
    `
  },
    
  { name: "Company",
    short: "comp",
    long: "Companies",
    fields: `
	    industry: 
	    country: 
	    address: 
	    website: 
	    linkedin: 
		tags: map/company
		archetype: "[[Companies]]"
    `
  }, 
   
  { name: "Concept",
    short: "concept",
    long: "Concepts",
    fields: `
		tags: map/concept
		archetype: "[[Concepts]]"
    `
  }, 
  
  { name: "Discipline",
    short: "disc",
    long: "Disciplines",
    fields: `
		tags: map/discipline
		archetype: "[[Disciplines]]"
    `
  }, 
    
  { name: "Industry",
    short: "ind",
    long: "Industries",
    datatable: getTableUpRelated(), 
    fields: `
		tags: map/industry
		archetype: "[[Industries]]"
    `
  },     
  
  { name: "Location",
    short: "loc",  
    long: "Locations", 
    datatable: getTableUpRelated(), 
    fields: `
	    country: 
	    state: 
	    region: 
		tags: map/location
		archetype: "[[Locations]]"
    `
  },
  
  { name: "Media",
    short: "med",  
    long: "Media",
    exclude: ["up", "related", "rating"],
    fields: `
		tags: map/media
		archetype: "[[Media]]"
    `
  },
  
  { name: "Object",
    short: "obj",  
    long: "Objects",
    fields: `
		tags: map/object
		archetype: "[[Objects]]"
    `
  },  
  
  { name: "People",
    short: "people",
    long: "People",
    datatable: getTableUpRelated(),
    fields: `
	    known_for: 
	    company: 
	    BU:
	    position: 
	    role: 
	    reports_to: 
	    email: 
	    address: 
	    phone: 
	    city: 
	    linkedin: 
	    github: 
		tags: map/person
		archetype: "[[People]]"
    `
  },    

  { name: "Rating",
    short: "rating",
    long: "Ratings",
    exclude: ["rating", "status", "summary", "up", "related"],
    fields: `
		tags: map/rating
		archetype: "[[Ratings]]"	    
    `
  },

  { name: "Realm",
    short: "realm",  
    long: "Realms",
    exclude: ["rating", "up", "related"],
    fields: `
		tags: map/realm
		archetype: "[[Realms]]"
    `
  },
  
  { name: "Status",
    short: "status",
    long: "Status",  
    exclude: ["rating", "status", "up", "related"],
    fields: `
		tags: map/status
		archetype: "[[Status]]"
    `
  }, 
   
  { name: "Type", 
    short: "type", 
    long: "Types", 
    exclude: ["rating", "status", "up", "related"],
    fields: `
		tags: map/type
		archetype: "[[Types]]"
    `
  },
]

let uuid; let yml; let choice; let commonFields;
let folder = "Atlas";  // this is the parent of sub-folders
// find a prefix match to select dictionary member

let title = tp.file.title;
uuid = get_uuid(true)

if (title.startsWith("Untitled")) {
	// most likely you pressed Ctrl+N (new note)
	title = await tp.system.prompt("Note Name")
	// pressing <Enter> or <Esc >creates "Untitled-20240220104000"
	if (title == null) {
		title = "0" 
	} else if (title.length <= 1) {
	  // if not null, ask for length; looking for <Enter>
		title = "1"
	}
}

 choices.forEach(item => {
	 if (title.startsWith(item.short)) {
		 choice = item; }
})
// if the prefix was found
if (choice) {
	if (choice.exclude) 
		commonFields = removeExcludedKeys(common.fields, choice.exclude)
	else
		commonFields = common.fields
	yml = commonFields + choice.fields + uuid
	yml = cleanFrontmatter(yml);   // remove blank lines, spaces

	title = extractTitle(title); // get rid of the prefix
	await tp.file.rename(title);
	folder = "Atlas/" + choice.long + "/";
	await tp.file.move(folder + title);   // move to sub-folder
	// add frontmatter to the new note
	addFrontmatter(yml);
	// add table view to new note, if defined
	if (choice.datatable) {
		addTableBelowFrontmatter(choice.datatable)
	}
	new Notice("New " + choice.name + " just added ...")
} else {
	/*  Note name prefix was not found in dictionary, 
		or note originally "Untitled" then renamed from prompt, 
		or note is already named but is a link,
		or note produced by <Enter> or <Escape>
	*/
	// append/run the minimal template when note prefix not known
	tR += await tp.file.include("[[Default Minimal Template]]");
	
	if (title == "0" || title == "1") {
		// Name the note "Untitled-" if <Enter>; "untitled-" if <Esc>
		uuid = get_uuid(false)
		title == "1"
	     ? setTimeout(() => {tp.file.rename("Untitled-"+uuid)}, 350)
	     : setTimeout(() => {tp.file.rename("untitled-"+uuid)}, 350)
	} else {
		setTimeout(() => {tp.file.rename(title)}, 350)
	}
}


function cleanFrontmatter(yml) {
	// remove blank lines, spaces and tabs at the start of frontmatter
	const regex = /^(?=\n)$|^\s*|s*$|\n\n+/gm   
	yml = yml.split('\n')
		.map(function(line) {
			return line.replace(regex, "")})
		.filter(function(x) {return x}).join("\n");
	return yml;
}

function get_uuid(wholeProperty) {
	// Make an uuid to time stamp each new note
	return wholeProperty 
				? "uuid: " + "'" 
				   +  tp.file.creation_date("YYYYMMDDHHmmss") + "'"
				: tp.file.creation_date("YYYYMMDDHHmmss")
}

function getTableUpRelated() {
	// Dataview table showing notes 
	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
}

function addTableBelowFrontmatter(dt) {
		tR += "\n\n"
		tR += "```dataview"
		tR += "\n"
		tR += cleanFrontmatter(dt)
		tR += "\n"
		tR += "```"
		tR += "\n"
}

function addFrontmatter(yml) {
	tR += "---\n"
	tR += yml
	tR += "\n---\n"
}

function extractTitle(title) {
	let result;
	let count = (title.match(/-/g) || []).length;
	let dateRegex = /(\d{4})([-])(\d{2})([-])(\d{2})/;
	
	switch (count) {
		case 0:
			result = title.trim()
			break
		case 1: 
			result = title.split("-").slice(1)[0].trim()
			break
		default:
			result = dateRegex.test(title) && count == 2
				? title.trim() 
				: title.split("-").slice(1).join("-").trim()
	}
	return result;
}

function removeExcludedKeys(txt, exclude) {
	removeList = exclude.map(p => p+":")
	//console.log("removeList:", removeList)
	var expStr = removeList.join("|");
	//console.log("expStr:", expStr)
	return txt
		.replace(new RegExp(expStr, 'gi'), ' ')
		.replace(/\s{2,}/g, '\n')
}
_%>

Actually, three, because there is a js file, a meta template picker and a third file called from the picker.
Although I like the flexibility of creating a template that is dissimilar to the rest, e.g.

---
obsidianUIMode: preview
obsidianEditingMode: live
tags:
  - wordsense
  - iframeforbrowsing
dg-publish: false
date created: <% tp.date.now("YYYY-MM-DD")%>
date modified: <% tp.date.now("YYYY-MM-DD")%>
---

# <%-* tR += tp.user.getTitleSnippet(tp.file.title) -%>

<iframe width="700" height="900" src="https://www.wordsense.eu/<%-* tR += tp.user.getTitleSnippet(tp.file.title) -%>/"></iframe>
  • WordSense.eu stopped dealing with foreign words, alas. You entered love and it used to give you the meaning in 50-100 languages. Not anymore. We are regressing, folks, not progressing.
  • Exclusions I find to be a deterrent for the average user.

I’ve got some suggestion to improvement, or comments on this solution. I’m sorry if this comes across as harsh, but for me they reduce the quality and reusability of this code to such an extent that I wouldn’t use this as it stands.

  • I believe short should rather be named prefix to clearer convey what it’s used for
  • Similar long seems to be used as the path
  • In the middle of the script there is a sudden reference to Atlas being a parent of all files?!
  • The data table function uses this.file.name, which I believe is the original name, not the name after any renaming like in the case of loc-Somewhere
  • There is also a hidden dependency on dates in extractTitle which I’m not sure what it does
  • Where did the usage of tp.system.suggester() go? It was the main idea of the other post to allow for a template to be chosen, which now seems to be abandoned (if I’ve not misread the code)
  • As stated before, I’m also not fond of the usage of strings to combine all the fields together. Especially not when seeing that you use repeated line setting ti incorporate the query into to output mix. I believe this leads to harder maintenance of the code in general
  • Finally, the tp.file.rename should most likely be awaited, not delayed using setTimeout

All in all, I commend the idea, but there are some bugs and flaws which might affect the usage and usefulness of this script. It surely works, kind of, in a specific use case like the OP’s, but it’ll fault in a more generic use case.

Yet again, I’m sorry if this post offends someone, but I’m not sure how to write it better, and I felt it had to be written.

1 Like

No offense taken. On the contrary, I appreciate the critique because it will make the script better. And second, I am a scientist, critique is fuel and chisel.

There are a couple of things that you mention that have already addressed, in the third iteration, such as the parent folder. I haven’t published yet; about to. I know having a static folder name was the uttermost ugly thing.

If something doesn’t make sense, I will add more comments, specially that date regex that doesn’t seem important but after tests, it is.

The tp.file.rename issue is unavoidable. Sometimes it runs too fast in my machine that doesn’t do the renaming, I had to delay it a bit. That’s why. Maybe you could test it your machine with and without the timeout.

The tp.system.suggester is not needed in this case, as this script is trying to replicate the original “Meta Template Picker” (MTP) by Jenks and Wang. The MTP is just a part that does something specific: automatic location of notes based on a prefix in the name. I am trying to make tp.system.suggester work to select between three more template pickers, when this MTP, is at the top of the choices, but I am struggling to execute the template pickers. In the use cases I’ve seen it is mostly used to select a template to run. In my case I want to select a template that contains other templates. I was about to ask for help in the forum.

The treatment of the properties entered as lines between backticks is probably a side-effect of me being influenced by my use of R’s Rmarkdown, knitr, and other R markdown packages. To me it seems neater that way.

One thing that comes obvious after using/working this template script so many times is that are clearly three parts identified: the properties declaration for many classes in a dictionary; the process of acting on the instructions in that dictionary; and the utility functions. I was thinking what if I were able to insert any dictionary, instead of being stuck at one, at the top, at will?

This is the third iteration of the Multi Template Picker.

Changes

  • Sub-folders can be entered per note class
  • key short renamed as prefix. Makes more sense
  • key long removed; instead using a folder for each class
  • Class “Event” uses a different folder: folder: "Calendar/Events"
  • Improved documentation of the utility functions
<%*
/* "Meta Template Picker"  idea makers: Bryan Jenks and Pamela Wang
	This iteration by Alfonso R. Reyes. Houston, Texas.
 */

// Common properties will be inserted at the top of frontmatter
const common = {
	  name: "Common",
	  fields: `
	    aliases: 
		type: 
		archetype: 
		summary: 
		status: 
	    rating: 
	    up: 
	    related:
	  `
	}
    
/* List of dictionaries/objects are defined here.
   This the central place where to control all the properties.
   
   name:      is the name of the inline-template
   short:     will be used as a prefix for inline-template selection
   long:      will be used for the sub-folder
   datatable: a Dataview table added below the frontmatter
   exclude:   an array of classes to exclude from the common fields
   folder:    folder where to store notes for a given class
   fields:    properties of the inline-template
*/
const choices = [
  { name: "Collection",
	prefix: "collect",
	datatable: getTableUpRelated(),
	folder: "Atlas/Collections",
    fields: `
		tags: map/collection
		archetype: "[[Collections]]"
    `
  },

  { name: "Company",
    prefix: "comp",
    folder: "Atlas/Companies",
    fields: `
	    industry: 
	    country: 
	    address: 
	    website: 
	    linkedin: 
		tags: map/company
		archetype: "[[Companies]]"
    `
  },
     
  { name: "Concept",
    prefix: "concept",
    folder: "Atlas/Concepts",
    fields: `
		tags: map/concept
		archetype: "[[Concepts]]"
    `
  },
  
  { name: "Discipline",
    prefix: "disc",
    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",
    datatable: getTableUpRelated(),
    folder: "Atlas/Industries",
    fields: `
		tags: map/industry
		archetype: "[[Industries]]"
    `
  },

  { name: "Location",
    prefix: "loc",
    datatable: getTableUpRelated(), 
    folder: "Atlas/Location",
    fields: `
	    country: 
	    state: 
	    region: 
		tags: map/location
		archetype: "[[Locations]]"
    `
  },

  { name: "Media",
    prefix: "med",
    exclude: ["up", "related", "rating"],
    folder: "Atlas/Media",
    fields: `
		tags: map/media
		archetype: "[[Media]]"
    `
  },

  { name: "Object",
    prefix: "obj",
    folder: "Atlas/Objects",
    fields: `
		tags: map/object
		archetype: "[[Objects]]"
    `
  },

  { name: "People",
    prefix: "people",
    datatable: 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: "Rating",
    prefix: "rating",
    exclude: ["rating", "status", "summary", "up", "related"],
    folder: "Atlas/Ratings",
    fields: `
		tags: map/rating
		archetype: "[[Ratings]]"  
    `
  },

  { name: "Realm",
    prefix: "realm",
    exclude: ["rating", "up", "related"],
    folder: "Atlas/Realms",
    fields: `
		tags: map/realm
		archetype: "[[Realms]]"
    `
  },

  { name: "Status",
    prefix: "status",
    exclude: ["rating", "status", "up", "related"],
    folder: "Atlas/Status",
    fields: `
		tags: map/status
		archetype: "[[Status]]"
    `
  },

  { name: "Type",
    prefix: "type",
    exclude: ["rating", "status", "up", "related"],
    folder: "Atlas/Types",
    fields: `
		tags: map/type
		archetype: "[[Types]]"
    `
  },
]

let uuid; let yml; let choice; let commonFields; let folder; 

let title = tp.file.title;   // grab current title
uuid = get_uuid(true)        // grab uuid property

if (title.startsWith("Untitled")) {  // default title of a new note
	// most likely you pressed Ctrl+N (new note)
	title = await tp.system.prompt("Note Name")
	// pressing <Enter> or <Esc > will create an Untitled note
	if (title == null) {   // capturing null so we don't get error
		title = "0" 
	} else if (title.length <= 1) {
	  // if not null, ask for length of title; looking for <Enter>
		title = "1"
	}  
	// flags "0" and "1" will be used later on
}
// When note is not named "Untitled", run all this:

// find a prefix match to select note class
 choices.forEach(item => {
	 if (title.startsWith(item.prefix)) {
		 choice = item; }
})

if (choice) {
	// if the prefix was found in the dictionary
	if (choice.exclude)  // if "exclude" not empty
		// remove common properties specified in "exclude"
		commonFields = removeExcludedKeys(common.fields, choice.exclude)
	else
		commonFields = common.fields   // will exclude nothing
	yml = commonFields + choice.fields + uuid  // all fields together
	yml = cleanFrontmatter(yml);   // remove blank lines, spaces
	
	/*  extractTitle() was a tp.user function in getTitleSnippet.js
	    extractTitle has been simplified by using "switch"
	*/  
	title = extractTitle(title); // get rid of the prefix
	await tp.file.rename(title);
	folder = choice.folder ? choice.folder + "/" : "/"
	await tp.file.move(folder + title);   // move to class sub-folder
	addFrontmatter(yml);  // add frontmatter to the new note
	if (choice.datatable) {   // add table view to new note, if defined
		addTableBelowFrontmatter(choice.datatable)
	}
	new Notice("New " + choice.name + " just added ...")  // a message
} else {
	/*  Cases: 
	    * Note name prefix was not found in dictionary, or
		* note by default is "Untitled" then renamed from prompt, or
		* note is already named but is a link,
		* note produced by <Enter> or <Escape>
	*/
	
	// append/run the minimal template when note prefix not known
	tR += await tp.file.include("[[Default Minimal Template]]");
	
	if (title == "0" || title == "1") {
		// Name the note "Untitled-" if <Enter>; "untitled-" if <Esc>
		uuid = get_uuid(false)  // false to get timestamp, no Yaml key
		title = title == "1" 
		   ? "Untitled-" + uuid // Enter: "Untitled-20240220104000"
		   : "untitled-" + uuid // Escape: "untitled-20240220104000"
	}
	setTimeout(() => {tp.file.rename(title)}, 350)
}

function cleanFrontmatter(yml) {
	/* remove blank lines, spaces and tabs at the start of 
	   frontmatter caused by the indentation in the dictionary
	*/
	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;
}

function get_uuid(wholeProperty) {
	/* Make an uuid to time stamp each new note.
	   wholeProperty: boolean. 
	   If true, will return "uuid: '20240220103600'",
	   if false, 20240220103600
	*/
	return wholeProperty 
			? "uuid: " + "'" 
				   +  tp.file.creation_date("YYYYMMDDHHmmss")  + "'"
			: tp.file.creation_date("YYYYMMDDHHmmss")
}

function getTableUpRelated() {
	/* Dataview table showing notes where their properties
	   "up" and "related" contain a link to the title
	   of the current note
	*/
	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
}

function addTableBelowFrontmatter(dt) {
	// Insert Yaml along the dataview chunk
		tR += "\n\n"
		tR += "```dataview"
		tR += "\n"
		tR += cleanFrontmatter(dt)
		tR += "\n"
		tR += "```"
		tR += "\n"
}

function addFrontmatter(yml) {
	tR += "---\n"
	tR += yml
	tR += "\n---\n"
}

function extractTitle(title) {
	/* 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-
	*/
	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;
}

function removeExcludedKeys(txt, exclude) {
	// remove properties specified in the array exclude
	// some classes do not need all the common properties
	removeList = exclude.map(p => p+":")
	var expStr = removeList.join("|");
	return txt
		.replace(new RegExp(expStr, 'gi'), ' ')
		.replace(/\s{2,}/g, '\n')
}
_%>

Comments and critique welcome!

2 Likes

Again I need to earn my beer which I can pour in the throat of the robot who made this (I’m a teetotaler):


This script is a versatile tool that allows you to quickly create new notes in Obsidian with predefined properties and structures based on different categories or classes of information.

It provides a central place to control all the properties associated with each class, making it easy to maintain consistency and organization across your notes.

The script works by defining a list of dictionaries or objects, each representing a specific class of information.

These dictionaries contain various properties, such as the name of the class, the prefix used to identify it, the folder where notes belonging to that class should be stored, and the specific fields or properties that should be included in the frontmatter of notes in that class.

Usage

To use the script, you simply need to start a new note and type the prefix of the class you want to create.

For example, if you want to create a new note for a company, you would type "comp - " followed by the name of the company. The script will then automatically generate a new note with the appropriate properties and structure based on the definition for the “Company” class.

The properties defined in the dictionaries/objects can be used in the file name in the following way:

Prefix:

The “short” property is used as a prefix for inline-template selection. This means that when you start a new note and type the prefix, the script will automatically select the corresponding inline-template and generate a new note with the appropriate properties and structure.

Folder:

The “folder” property specifies the folder where notes belonging to that class should be stored. When you create a new note using the prefix, the script will automatically move the note to the specified folder.

You can use the “folder” property to create a custom folder structure for your notes. This can be helpful for organizing your notes in a way that makes sense for your workflow.
For example, you could create a folder for each project that you are working on, and then use the “folder” property to specify that all notes related to that project should be stored in that folder.
Or, you could create a folder for each type of note that you take, such as “Ideas”, “Tasks”, and “Meetings”. Then, you could use the “folder” property to specify that all notes of a particular type should be stored in the corresponding folder.

Fields:

The “fields” property contains a list of properties that should be included in the frontmatter of notes in that class. These properties can be used to capture information specific to that class of notes. For example, for a “Company” class, you might have properties like “industry”, “country”, “address”, and “website”.

To use these properties in the file name, you simply need to include the prefix followed by the desired properties. For example, if you want to create a new note for a company called “Acme Corporation” in the “Technology” industry, you could use the following file name:

comp - Acme Corporation - Technology

The script will then automatically generate a new note with the appropriate properties and structure, and move it to the specified folder.

Here are some additional examples of how you can use the properties in the file name:

  • Concept: concept - The concept of time
  • Discipline: disc - Economics
  • Event: eve - Meeting with John Smith on Friday
  • Industry: ind - Manufacturing
  • Location: loc - New York City
  • Media: med - Image of a cat
  • Object: obj - My car
  • People: people - Jane Doe
  • Rating: rating - 5 stars
  • Realm: realm - Personal
  • Status: status - In progress
  • Type: type - Article

You can also use multiple properties in the file name, separated by dashes. For example:

comp - Acme Corporation - Technology - San Francisco

This would create a new note for a company called “Acme Corporation” in the “Technology” industry, located in “San Francisco”.

Exclusions

The script also allows you to exclude certain properties from the common fields for specific classes.

This is useful when you have classes that don’t require all the common properties. For example, you might not need the “rating” or “status” properties for a class of notes that represent concepts or ideas.

The “exclude” property in the dictionaries/objects allows you to exclude certain properties from the common fields for specific classes.

This is useful when you have classes that don’t require all the common properties. For example, you might not need the “rating” or “status” properties for a class of notes that represent concepts or ideas.

To use the “exclude” property, simply list the properties that you want to exclude in the array. For example, the following dictionary excludes the “rating”, “status”, and “summary” properties from the common fields for the “Concept” class:

{
  name: "Concept",
  short: "concept",
  folder: "Atlas/Concepts",
  fields: `
    tags: map/concept
    archetype: "[[Concepts]]"
  `,
  exclude: ["rating", "status", "summary"]
}

When you create a new note using the “concept” prefix, the script will automatically exclude the specified properties from the frontmatter.

Here is an example of how you could use the “exclude” property in the file name:

concept - The concept of time - exclude:rating,status,summary

This would create a new note for a concept called “The concept of time”, and exclude the “rating”, “status”, and “summary” properties from the frontmatter.

DataView Table

Additionally, the script can generate a Dataview table below the frontmatter of a note, showing related notes that have links to the current note in their “up” or “related” properties.

This table provides a quick and easy way to navigate and explore connections between different notes.

The Dataview table generated by the script is a powerful tool for exploring connections between different notes in Obsidian.

It works by querying the “up” and “related” properties of the current note, and displaying all the notes that have links to the current note in those properties.

This can be incredibly useful for navigating complex networks of information and ideas. For example, if you have a note about a particular company, you can use the Dataview table to quickly see all the other notes that mention that company, or that are related to it in some way.

To use the Dataview table, simply create a new note using one of the prefixes defined in the script. The script will automatically generate a Dataview table below the frontmatter of the note, showing all the related notes.

Here is an example of a Dataview table generated for a note about the company “Acme Corporation”:

TABLE up, related
FROM ""
WHERE up = link("Acme Corporation")
   OR contains(related, link("Acme Corporation"))
SORT file.name ASC

This table shows all the notes that have a link to “Acme Corporation” in their “up” or “related” properties. In this example, we can see that there are notes about “Acme Corporation’s financial performance”, “Acme Corporation’s new product launch”, and “Acme Corporation’s acquisition of XYZ Company”.

You can click on any of the links in the table to quickly navigate to the corresponding note. This makes it very easy to explore the connections between different notes and to gather information from multiple sources.

The Dataview table is also customizable. You can modify the query to show different types of relationships between notes. For example, you could modify the query to show only the notes that have a link to the current note in their “up” property, or only the notes that have a specific tag in common with the current note.

Overall, this script is a powerful tool that can greatly enhance your note-taking workflow in Obsidian by providing a structured and efficient way to create and organize notes across different categories or classes of information.

You sir @gino_m, are amazing!

Your write up adds an enormous value to the MTP script.
Many beers accrued!

PS. Let me know when you will be visiting Houston.

– joke(?) (reference to current world events?) deleted –

1 Like

Joking aside, how would you feel about adding an empty line below the second index of the YAML separator?

Just point the part to be modified so people can make adjustments if they want to (3 am where I’m at so I’ll leave it to you, comrade).

1 Like

@gino_m,
to give you an idea what I have in the classes (or archetypes), of my personal vault:

Status

Realms

image

Media

Industries

Disciplines

Concepts

Collections

Types

image

Sure. No problem.
Could you send me screenshot pointing at the place you want the empty line inserted?

Fixed:

function addTableBelowFrontmatter(dt) {
	// Insert Yaml along the dataview chunk
		tR += "```dataview"
		tR += "\n"
		tR += cleanFrontmatter(dt)
		tR += "\n"
		tR += "```"
		tR += "\n"
}

function addFrontmatter(yml) {
	tR += "---\n"
	tR += yml
	tR += "\n---\n\n"
}
1 Like

Hi, @msfz751

This is great! I am also using Pamela Wang’s MTP for a while, and I found your MTP is more flexible.

Just wonder, is it possible to use a template for each classes? I had been set up a bunch of templates for each prefix, so I just wonder if it is possible to use a template for each class.

Anyway, this is a great js, thank you very much for sharing it.

1 Like

I am working on it. I call them “super-templates”.

The idea is having a subfolder under Templates with various super-templates -in my case I only need three: Atlas, MyStuff, and Career, which basically are arrays of dictionaries. Each super-template contains several independent set of properties, which reduces drastically the number of template files , improves the coherence, and cuts the time of maintenance. Then, a master template reads them in memory and voila: you have a super-selector of many classes, each with its collection of properties. This is a screenshot of the the top of one of the super-templates.

And these screenshots are from my three other super templates:

I am close to complete it but I am now struggling with await, async, and promises.

Waiting for your good news.

See here

Hi all. Non technical user here asking what is probably obvious to most of you.

Does Metatemplater automatically keep notes of a defined type up to date with the defined Type template as the template updates?

If so, lets say I start using metatemplater today and I have a bunch of existing notes and I create a metatemplate that I want to apply to all those notes - is it sufficient to simply apply the given Type to all those notes and then they can bulk update to the defined template. Or is it still a one-by-one case?

If Metatemplater does NOT automatically update all notes of a given type as the template changes, then I dont understand what it does differently to Templater.