Templater icon indicating copy to clipboard operation
Templater copied to clipboard

Set Frontmatter

Open calpa opened this issue 3 years ago • 19 comments

For now, there is only getter of frontmatter, setter is also needed.

tp.frontmatter.set('a', 'b')

=>

---
a = b
---

calpa avatar Apr 14 '21 15:04 calpa

In the meantime, anyone know of a commandline tool that we could wire up to a user script to fiddle with the YAML frontmatter?

I found a handful of libraries in NPM and PyPI, but nothing stood out to me as the "right" choice. I am inclined to stick with good old sed and awk.

luckman212 avatar Apr 20 '21 03:04 luckman212

For now, there is only getter of frontmatter, setter is also needed.

tp.frontmatter.set('a', 'b')

=>

---
a = b
---

On top of just setting the values, since YAML supports lists and dictionaries, will also need to be able to work with them. For example, removing/appending a tag from existing list of tags. Or checking if a key exists in a dictionary, etc.

CovetingEpiphany2152 avatar May 07 '21 11:05 CovetingEpiphany2152

This would be great, of course. For anyone else looking, you can regex match and replace using Obsidian API or use MetaEdit's API.

AB1908 avatar Feb 17 '22 17:02 AB1908

I wanted to add my voice into this. Until now I have used the metaedit plugin, which has an API for modifying frontmatter. However the plugin hasn't been updated (generates console errors). Also the implementation is a bit wonky, seems not to be tested in all scenarios.

Anyhow, reading and writing frontmatter would be a natural fit for Templater.

Tnx for all the great work on this plugin.

TfTHacker avatar Sep 20 '22 10:09 TfTHacker

I wonder if the methods for parseYaml and stringifyYaml help with this.

AB1908 avatar Sep 20 '22 11:09 AB1908

+1 for this!

mayurankv avatar Oct 26 '22 22:10 mayurankv

Because Templater exposes the Obsidian-API, writing/replacing frontmatter can be done relatively easily already.

I'm using this script to completely replace the current frontmatter with frontmatterobject. This can of course be used in conjunction with tp.frontmatter to just update parts of it.

module.exports = async (tp, frontmatterObject = {}) => {
    if (typeof frontmatterObject !== "object") {
        throw new Error("frontmatterObject must be an object")
    }

    // get proper file object from vault path
    const currentNotePath = tp.file.path(true)
    let currentNoteFile = this.app.vault.getAbstractFileByPath(currentNotePath)

    try {
        // Obsidian-API method to work with frontmatter
        await this.app.fileManager.processFrontMatter(currentNoteFile, (frontmatter) => {
            // clear the object
            for (let member in frontmatter) delete frontmatter[member]
            // copy over the content I'm interested in
            Object.assign(frontmatter, frontmatterObject)
        })
    } catch (e) {
        console.log(e)
    }
}

The actual template I use to replace the current frontmatter with a computed default looks like this

<%*
let default_frontmatter = await tp.user.default_frontmatter(tp)
await tp.user.replace_frontmatter(tp, tp.user.default_frontmatter(tp, true))
%>

Hope this helps!

Credit where it's due:
I cobbled this together from code posted on the forum, and I also use (a modified version of) some of these very good snippets.

FynnFreyer avatar Jan 04 '23 14:01 FynnFreyer

Having an easy integrated way to set frontmatter through Templater would still be nice though ... but in the meantime you can use this I guess.

FynnFreyer avatar Jan 04 '23 14:01 FynnFreyer

Obsidian now exposes an API to edit frontmatter directly. See https://github.com/obsidianmd/obsidian-api/blob/master/CHANGELOG.md#new-metadata-api

AB1908 avatar Jan 04 '23 20:01 AB1908

Thanks for the info, @AB1908 !

I successfully create/update a frontmatter attribute using Templater like this:

<%*
// Generate a list of all notes in my "People" folder.
const fileList = app.vault.getMarkdownFiles().filter(file =>
	file.path.startsWith(baseDir) 
	&& 'md' === file.extension
	&& file.basename
);

// Get the `TFile` of a contact-note.
const contactFile = await tp.system.suggester(file => file.basename, fileList);

// For testing purposes, set the "contact date" to today.
const contactDate = moment().format('YYYY-MM-DD');

// Create or update the frontmatter property "last_contact".
app.fileManager.processFrontMatter(contactFile, (frontmatter) => {
	if (!frontmatter.last_contact || frontmatter.last_contact < contactDate) {
		frontmatter.last_contact = contactDate;
	}
});
%>

stracker-phil avatar Feb 22 '24 13:02 stracker-phil

Is there a way to have the property formatted as

tags: [ foo, bar, baz ]

I use the template below to collect stray tags throughout the file, sort and deduplicate them, and place them all in the frontmatter. But, it formats it as

tags: foo, bar, baz

I always thought the first style was "more correct"?

<%*
  function getUnique(value, index, self) {
    return self.indexOf(value) === index;
  }
  const f = app.workspace.getActiveFile();
  var tArr = tp.file.tags.filter(getUnique);
  if (tArr.length) {
    tArr.sort();
    var tStr = tArr.join(', ').replace(/[#\[\]]/g,'');
    app.fileManager.processFrontMatter(f, (fm) => {
      fm.tags = tStr;
    });
  }
%>

luckman212 avatar Feb 22 '24 17:02 luckman212

did you try assigning the array to the tags property, without converting it to a string?

...
    app.fileManager.processFrontMatter(f, (fm) => {
      fm.tags = tArr;
    });
...

stracker-phil avatar Feb 22 '24 18:02 stracker-phil

Yes I did. Unfortunately, that results in the multiline YAML format which I absolutely despise because it wastes so much vertical space:

tags:
  - foo
  - bar
  - baz

luckman212 avatar Feb 22 '24 19:02 luckman212

Using the app.fileManager.processFrontMatter API will always output arrays in the multiline format. You'd have to parse the frontmatter yourself if you don't like that format, though I'm sure many plugins use this API and will modify the frontmatter to that format for you if you have plugins that modify frontmatter.

I'd recommend either looking into integrating MetaEdit into your Templater script, or using the new getFrontmatterInfo function to find the frontmatter lines and using JS string manipulation to modify it.

Zachatoo avatar Feb 22 '24 19:02 Zachatoo

For now I settled on just allowing it to use the unbracketed version. Obsidian still recognizes the tags. Hopefully in the future the API might be updated to support a multiline=false parameter...

luckman212 avatar Feb 22 '24 21:02 luckman212

I hacked away a bit and got a weirdly working function. The first time you invoke it, it will produce the desired format

tags: [ foo, bar, baz ]

Then, if you run it a 2nd time, it seems to work like a toggle, the format becomes

tags: foo, bar, baz

Run it a 3rd time, and it again becomes

tags: [ foo, bar, baz ]

It's very strange, I banged my keyboard and my head for a few hours before giving up. Seems to be some kind of cache issue or bug in Editor.replaceRange() ... @SilentVoid13 do you have any idea?

<%*
  const f = app.workspace.getActiveFile();
  const e = app.workspace.activeLeaf.view.editor;
  var tArr = tp.file.tags;
  if (tArr.length) {
    tArr.sort();
    var tStr = tArr.join(', ').replace(/#/g,'');
    var tbStr = `tags: [ ${tStr} ]`;
    await app.fileManager.processFrontMatter(f, (fm) => {
      fm.tags = tStr;
    });
    const nfm = await tp.obsidian.getFrontMatterInfo(e.getValue());
    if (nfm.exists) {
      const bfm = nfm.frontmatter.replace(/\btags: [^\n]*/, tbStr);
      await e.replaceRange(bfm, {line: 0, ch: nfm.from}, {line:0, ch: nfm.to});
    }
  }
%>

luckman212 avatar Feb 25 '24 22:02 luckman212

I added a small 100ms delay and that "solves" the problem. Still would like to know if I'm going about this completely the wrong way...

<%*
  function getUnique(value, index, self) {
    return self.indexOf(value) === index;
  }
  const f = app.workspace.getActiveFile();
  const e = app.workspace.activeLeaf.view.editor;
  var exclude_tags = [
    '#any_tags',
    '#you_dont_want',
    '#added_to_frontmatter'
  ];
  var tUnique = tp.file.tags.filter(getUnique);
  var tArr = tUnique.filter(t => exclude_tags.includes(t) === false); 
  if (tArr.length) {
    tArr.sort();
    var tStr = tArr.join(', ').replace(/#/g,'');
    var tbStr = `tags: [ ${tStr} ]`;
    await app.fileManager.processFrontMatter(f, (fm) => {
      //fm.tags = tArr;
      fm.tags = tStr;
    });
    setTimeout(async () => {
      const nfm = await tp.obsidian.getFrontMatterInfo(e.getValue());
      const bfm = await nfm.frontmatter.replace(/\btags: [^\n]*/, tbStr);
      await e.replaceRange(bfm, {line: 0, ch: nfm.from}, {line:0, ch: nfm.to});
    }, 100)
  }
%>

luckman212 avatar Feb 27 '24 13:02 luckman212