Templater icon indicating copy to clipboard operation
Templater copied to clipboard

Deprecation of dynamic commands

Open AB1908 opened this issue 2 years ago • 51 comments

Let's use this issue to discuss and track this. We'll need to create a migration guide and make info accessible. Here's a small task list I think we can start with:

  • [ ] Modal to indicate upcoming deprecation
  • [ ] Bold description at top of readme
  • [ ] Shoutout on Eleanor's roundup
  • [ ] Pages in docs
  • [ ] Migration guide itself
  • [ ] Notice if dynamic commands found in note?

I think this should cover most of our bases and drive awareness. The second part is actually compiling the ways other plugins help with this and the common use cases. We'll have to deal with edge cases by hand I imagine.

AB1908 avatar Nov 11 '22 00:11 AB1908

I am using dynamic (javascript) commands quite extensively in combination with the Advanced URI plugin, to add notes from any browser environment > run an initial templater file in reading mode > trigger a dynamic command in the created file for data clean-up and moving the file to a folder based on metadata.

So for me, depreciating dynamic commands would be a workflow breaker.

dleeftink avatar Nov 11 '22 13:11 dleeftink

Can you show me your template? I have a feeling we're talking about different things.

AB1908 avatar Nov 11 '22 13:11 AB1908

Alright so this is a bit complex but here goes:

Bookmarklet

To my chrome bookmark bar, I've added a javascript:(function(){ ... })() bookmarklet that is synced across my devices. Upon triggering, it imports some npm packages (YAML, Citejs) and prepares an Advanced URI url command inside a plaintext payload.

Stepwise:

  1. The payload is prepared in a bookmarklet and send to Obsidian via Advanced URI (via the data={payload} and mode=append URL params)
  2. A new note is created in Obsidian from the payload using Advanced URI's
  3. The new note is opened in reading mode (via the viewmode=preview URL param)

In essence, the bookmarklet triggers and Advanced URI like so:

obsidian://advanced-uri?vault=lib&filepath=cite/${authorName}&data=${payload}&mode=append&viewmode=preview

with the ${payload} literal looking like this:

---
anno: ${add(inp.issued['date-parts'][0][0])}`
type: ${add(inp.type)}
cite: ${add(inp.id)}
full: ${add(apa.trim())}
item: ${add(inp.title)}
from: ${add(inp.author)}
 ---

### 

 <%+* window.location = "obsidian://advanced-uri?vault=lib&commandid=templater-obsidian%3Areplace-in-file-templater" %>
 <%_ tp.file.include("[[citation]]") %>`
 
~~~ bibx
${inp._graph[0].data}
~~~

Template file [[citation]]

As you can see, an Advanced URI is included in the payload as well! This is immediately triggered when the note opens in reading view, but only after another templater file (citation.md) is included on file creation.

  • This allows me to decouple the 'bookmarklet' stage from the 'templater formatting' stage, as the [[citation.md]] file can refer to all the metadata added in the first stage using tp.frontmatter, bypassing the caching problem as templater cannot refer to the frontmatter when inserted using Advanced URI
  • At least, templater could not access the frontmatter data in the past upon file creation, as a note needed to be 'saved' and indexed first before the frontmatter data was available, hence why the second Advanced URI is triggered a second time to replace the templater commands included from [[citation.md]]).

For completeness, the actual [[citation.md]] template looks like this:

 `i` 

__

<%_* let yml = tp.frontmatter; %>

#### view
[`v`] [[ 
<%- yml.cite -%>
 ]]  
[ apa :: <% yml.full %> ]

####
__

#### keys
- ...

###
__

### <% yml.item %>

__

...

### 
__

### `t`

__

#### refs
<%+* if(tp.file.title!="citation"){ window.location = "obsidian://advanced-uri?vault=lib&commandid=templater-obsidian%253Areplace-in-file-templater"} %>

<%_* window.location = "advanced-uri?vault=lib&searchregex=%2F%3C%25%5C%2B%5C*.*%25%3E%7Cfull%3A%20.*%2Fg&replace=" _%>

As you can see, the included [[citation.md]] template can even clean up after itself by running another dynamic command at the end that triggers an Advanced URI search and replace, leaving me with a static note with all data nicely formatted without leaving any trace of the dynamic templater commands.

dleeftink avatar Nov 11 '22 14:11 dleeftink

You're talking specifically about execution commands and not dynamic commands as defined in the docs. These are indicated by <%+. These are the ones that need to be deprecated since they're fairly confusing to use, don't gel well with templater's mental model, and frankly are better done by other plugins. Sorry for not clarifying that earlier.

AB1908 avatar Nov 11 '22 15:11 AB1908

So the <%+* ... %> formats remains in use? I.e. dynamic execution/javascript templates?

dleeftink avatar Nov 11 '22 15:11 dleeftink

Minor correction, <%* but essentially yes. You needn't worry for this one. I guess this brings up the point that we need to communicate who will be affected.

AB1908 avatar Nov 11 '22 16:11 AB1908

This removal of the 'dynamic' evaluation of js would break my workflow, e.g.

<%* return 1 %> does not evaluate on reading mode while <%+* return 1 %> does. The latter is needed in my use case.

dleeftink avatar Nov 11 '22 18:11 dleeftink

In fact, I love dynamic commands; elsewhere I use them to add per note eventListeners that detach themselves after unloading said notes. This allows custom hooks that are entirely encapsulated in the notes themselves, and forgo the need to write plugins for extending note interactivity .

dleeftink avatar Nov 12 '22 01:11 dleeftink

That sounds more like execution commands but I'll need to think about this some more meanwhile.

AB1908 avatar Nov 12 '22 07:11 AB1908

If the docs are anything to go by, yes, I do mean dynamic commands, whether of the 'templater' (<%+ %>) or 'javascript/execution' (<%+* %>) variety.

For instance, in case of attaching per note hooks, I add a dynamic YAML field to each note like so:

---
cssclass: taskline spacious
notehook: <%+ tp.user.details({app,open:false}) %>
---

Which runs a user script called details.js when opening a note in reading mode, and detaches itself after the note is closed. In this case, the script checks for an option parameter called open, and opens/closes any <details> element in the note based on the provided condition (true/false).

dleeftink avatar Nov 12 '22 13:11 dleeftink

This can be done via other means, Templater has an option for setting up hooks into obsidian events, though I haven't used it myself. Perhaps it would be sensible to move it to your own plugin given that you're one of the few folks doing this. As always, you're also free to maintain your own fork (like I do for a few other plugins).

AB1908 avatar Nov 12 '22 15:11 AB1908

The second part is actually compiling the ways other plugins help with this and the common use cases. ... and frankly are better done by other plugins.

I'm fairly new to Obsidian, so my workflow is easy enough to change (as I don't really have much of one at this stage). But I appreciate I'm in the minority here.

However, I do use modification date: <%+ tp.file.last_modified_date() %> in the YAML of my notes. This has stopped working entirely and results in it being replaced by NaN. This doesn't match my understanding of 'deprecated', but would suggest 'removed' instead. I could be wrong, and it's not working for other reasons, so I'm not making judgements. It did work recently. Unsure what update stopped it.

Anyway, I'm interested in suggestions of plugins that will give similar functionality to dynamic commands?

pauby avatar Nov 16 '22 12:11 pauby

Dataview is the main one. You can use the inline query `=this.file.mtime`

AB1908 avatar Nov 16 '22 13:11 AB1908

As for breakage, see #910

AB1908 avatar Nov 16 '22 13:11 AB1908

Dataview is the main one. You can use the inline query `=this.file.mtime`

Doesn't appear to work in the frontmatter. But this isn't the issue to discuss that in! Thanks for pointing me in that direction. I already have dataview, but haven't used it much. Will investigate further 😄

pauby avatar Nov 16 '22 13:11 pauby

Will dynamic commands be replaced by new commands like writing multiple times? (It feels more convenient to import and export data)

red-co avatar Nov 20 '22 16:11 red-co

Will dynamic commands be replaced by new commands like writing multiple times? (It feels more convenient to import and export data)

@red-co What do you mean by writing multiple times? Regardless, should probably be a separate issue if that's a feature you're looking for, as it wouldn't be related to deprecating dynamic commands.

Zachatoo avatar Dec 01 '22 02:12 Zachatoo

Dataview is the main one. You can use the inline query `=this.file.mtime`

Doesn't appear to work in the frontmatter. But this isn't the issue to discuss that in! Thanks for pointing me in that direction. I already have dataview, but haven't used it much. Will investigate further 😄

@pauby By any chance, have you found a way to make the DataView query work in frontmatter as a replacement to something like:

---
modified_on: '<%+ tp.file.last_modified_date("YYYY-MM-DD HH:mm:ss Z") %>'
---

I've searched to no avail.

muya avatar Feb 23 '23 04:02 muya

@AB1908 I am looking for the same thing. Dataview does not work in the frontmatter.

divinites avatar Feb 23 '23 15:02 divinites

@muya I didn't get a solution that workled using dataview. If that's what you need, the rest of this isn't for you.

I eventually transitioned to Linter which has some good options for working with YAML frontmatter including the option to add a 'modified' field that is automatically updated.

Before I found that I did find another plugin that isn't in the Obsidian Community Plugins list and all it did was add a created: and modified: frontmatter on save. It was simple and just worked but had no configuration (if you needed it). I can dig that plugin name out if you need it. Once I found Linter I moved to that.

pauby avatar Feb 24 '23 10:02 pauby

@muya I found the other plugin - Update time on edit.

pauby avatar Feb 27 '23 13:02 pauby

Hello, I'm looking for a system to link my notes within a single folder. My idea was to use dynamic commands to automate the next Link. Could someone tell me how to proceed?

Dercraker avatar Sep 03 '23 20:09 Dercraker

Look into Dataview, it's perfect for that use case.

Zachatoo avatar Sep 03 '23 20:09 Zachatoo

Look into Dataview, it's perfect for that use case.

How can dataview allow me to generate the next link? it is possible to execute script ?

Dercraker avatar Sep 03 '23 20:09 Dercraker

You'll have to do some research on your own, and I'm not 100% sure what you're looking for, but I have an example here of how I generate links to the previous and next daily notes in my daily notes files. I hope it helps.

https://zachyoung.dev/posts/dataview-snippets#get-links-to-previous-and-next-daily-notes

Zachatoo avatar Sep 03 '23 20:09 Zachatoo

I use dynamic commands for a "Read Time", such that when a file is in read mode, I get an accurate depiction of how long the note will take to read.

Due to suggestions in this thread, I looked into Dataview - though so far I have yet to come to a positive conclusion about dataview taking over for something like "Read Time".

Have any of you already implemented (or seen) an implementation of a "Read Time" feature in Dataview?

dashinja avatar Sep 08 '23 01:09 dashinja

Have any of you already implemented (or seen) an implementation of a "Read Time" feature in Dataview?

What's your current Templater code? I or someone else can help translate it to Dataview, they use similar APIs.

Zachatoo avatar Sep 08 '23 01:09 Zachatoo

<%+ await tp.user.helpers().readTime(tp, tR)%>

Implemented as such:

async function readTime(tp, tR) {
	const WPM = 255
	const words = await tp.file.content.trim().replace(/[^\w\s]/g, "").split(/\s+/).filter((x) => x !== "")
	const readTime = Math.ceil(words.length / WPM)
	const totalHours = (readTime / 60)
	const readHours = Math.floor(totalHours)
	const totalMinutes = (totalHours - readHours) * 60
	const readMinutes = Math.round(totalMinutes)

	if (readMinutes == 0) {
		const output = `Less than a minute`
		return output
	}

	if (readHours < 1) {
		const output = `${readMinutes} minute${readMinutes == 1 ? "" : "s"}`
		return output
	}

	if (readHours != 0) {
		const output = `${readHours} hour${readHours == 1 ? "" : "s"} and ${readMinutes} minute${readMinutes == 1 ? "" : "s"}`
		return output
	}
}

dashinja avatar Sep 08 '23 03:09 dashinja

Here's the "view" code. Paste this into a .js file in a folder where you'll put all your Dataview Views. I only had to change how you're getting the contents of the current file, how to render the result, and remove the function declaration wrapping the code.

const WPM = 255
const content = await dv.io.load(dv.current().file.path) // Dataview's way to get contents of current file
const words = content.trim().replace(/[^\w\s]/g, "").split(/\s+/).filter((x) => x !== "")
const readTime = Math.ceil(words.length / WPM)
const totalHours = (readTime / 60)
const readHours = Math.floor(totalHours)
const totalMinutes = (totalHours - readHours) * 60
const readMinutes = Math.round(totalMinutes)

if (readMinutes == 0) {
	dv.span(`Less than a minute`) // Dataview's way to show content in a span
	return
}

if (readHours < 1) {
	dv.span(`${readMinutes} minute${readMinutes == 1 ? "" : "s"}`)
	return
}

if (readHours != 0) {
	dv.span(`${readHours} hour${readHours == 1 ? "" : "s"} and ${readMinutes} minute${readMinutes == 1 ? "" : "s"}`)
	return
}

Then you can use it in a markdown file like this. Replace the path to the view with your path, excluding the extension.

```dataviewjs
dv.view("Dataview Views/read-time")
```

More information about Dataview views in the docs here.

Zachatoo avatar Sep 08 '23 05:09 Zachatoo

What a champ. Thanks for getting to these Zach.

AB1908 avatar Sep 08 '23 23:09 AB1908