Open calpa opened 3 years ago
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
.
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.
This would be great, of course. For anyone else looking, you can regex match and replace using Obsidian API or use MetaEdit's API.
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.
I wonder if the methods for parseYaml
and stringifyYaml
help with this.
+1 for this!
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.
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.
Obsidian now exposes an API to edit frontmatter directly. See https://github.com/obsidianmd/obsidian-api/blob/master/CHANGELOG.md#new-metadata-api
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;
}
});
%>
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;
});
}
%>
did you try assigning the array to the tags property, without converting it to a string?
...
app.fileManager.processFrontMatter(f, (fm) => {
fm.tags = tArr;
});
...
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
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.
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...
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});
}
}
%>
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)
}
%>
Updated version for completeness (currently broken in 2.3.2)
<%*
const f = app.workspace.getActiveFile();
const e = app.workspace.activeLeaf.view.editor;
var tStr = null;
var kStr = null;
var tArr = null;
const excludeTags = new Set([
'#foo',
'#bar',
'#baz'
]);
if (tp.file.tags.length) {
const tUnique = Array.from(new Set(tp.file.tags));
tArr = tUnique.filter(t => !excludeTags.has(t));
tArr.sort();
tStr = `tags: [ ${tArr.join(', ').replace(/#/g,'')} ]`;
}
const processFrontMatterResult = await app.fileManager.processFrontMatter(f, (fm) => {
var kArr = fm['keywords'];
if (kArr) {
kArr = Array.from(new Set(kArr));
kArr.sort();
kStr = `keywords: [ ${kArr.join(', ')} ]`;
}
delete fm['tags'];
delete fm['keywords'];
if (kArr || tArr) {
fm.placeholder = null;
} else {
return false;
}
});
if (processFrontMatterResult === false) {
return;
}
const newFmStr = [tStr, kStr].filter(k => k !== null).join('\n');
tp.hooks.on_all_templates_executed(async () => {
const nfm = await tp.obsidian.getFrontMatterInfo(e.getValue());
const bfm = await nfm.frontmatter.replace(/\bplaceholder: [^\n]*/, newFmStr);
await e.replaceRange(bfm, {line: 0, ch: nfm.from}, {line: 0, ch: nfm.to});
})
%>
I don't know exactly why but I monkeyed around with this for about 5 hours today. Here's what I ended up with, works with 2.3.2 even despite #1380
<%*
const e = app.workspace.activeLeaf?.view.editor;
if (!e) {
console.error('No editor leaf found');
return;
}
let tStr = null;
let kStr = null;
let tArr = null;
let kArr = null;
const excludeTags = new Set([
'#foo',
'#bar',
'#baz',
]);
if (tp.file.tags.length) {
const tUnique = Array.from(new Set(tp.file.tags));
tArr = tUnique.filter(t => !excludeTags.has(t));
tArr.sort();
tStr = `tags: [ ${tArr.join(', ').replace(/#/g,'')} ]`;
}
let k = tp.frontmatter.keywords;
if (typeof k === 'object' && k.length) {
kStr = `keywords: [ ${tp.frontmatter.keywords.sort().join(', ') ?? ''} ]`;
}
tp.hooks.on_all_templates_executed(async () => {
const f = tp.config.active_file;
const processFrontMatterResult = await app.fileManager.processFrontMatter(f, (fm) => {
delete fm['tags'];
delete fm['keywords'];
if (tStr || kStr) {
fm.processing = 'please wait';
} else {
return false;
}
});
if (processFrontMatterResult === false) {
console.error('Error processing frontmatter');
return;
}
setTimeout(async () => {
const filtered = [tStr, kStr].filter(s => s && s.length).join('\n');
const existingFm = await tp.obsidian.getFrontMatterInfo(e.getValue());
const replacementFm = await existingFm.frontmatter.replace(/\bprocessing: please wait\b/, filtered);
await e.replaceRange(replacementFm,
{line: 0, ch: existingFm.from},
{line: 0, ch: existingFm.to});
}, 200)
});
-%>
For now, there is only getter of frontmatter, setter is also needed.
=>