SilentVoid13 / Templater

A template plugin for obsidian
https://silentvoid13.github.io/Templater
GNU Affero General Public License v3.0
3.26k stars 198 forks source link

Deprecation of dynamic commands #913

Open AB1908 opened 1 year ago

AB1908 commented 1 year ago

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:

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.

dleeftink commented 1 year ago

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.

AB1908 commented 1 year ago

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

dleeftink commented 1 year ago

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.

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.

AB1908 commented 1 year ago

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.

dleeftink commented 1 year ago

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

AB1908 commented 1 year ago

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.

dleeftink commented 1 year ago

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 commented 1 year ago

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 .

AB1908 commented 1 year ago

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

dleeftink commented 1 year ago

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).

AB1908 commented 1 year ago

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).

pauby commented 1 year ago

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?

AB1908 commented 1 year ago

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

AB1908 commented 1 year ago

As for breakage, see #910

pauby commented 1 year ago

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 😄

red-co commented 1 year ago

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

Zachatoo commented 1 year ago

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.

muya commented 1 year ago

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.

divinites commented 1 year ago

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

pauby commented 1 year ago

@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 commented 1 year ago

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

Dercraker commented 1 year ago

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?

Zachatoo commented 1 year ago

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

Dercraker commented 1 year ago

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 ?

Zachatoo commented 1 year ago

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

dashinja commented 1 year ago

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?

Zachatoo commented 1 year ago

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.

dashinja commented 1 year ago

<%+ 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
    }
}
Zachatoo commented 1 year ago

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](https://blacksmithgu.github.io/obsidian-dataview/api/code-reference/#dvviewpath-input).
AB1908 commented 1 year ago

What a champ. Thanks for getting to these Zach.

dashinja commented 1 year ago

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](https://blacksmithgu.github.io/obsidian-dataview/api/code-reference/#dvviewpath-input).

Thank you @Zachatoo . It does work in non-frontmatter areas. I've tried to get rid of the newer 'properties' thing so it will always work where I want to put it but alas. Even so, at least there is a replacement in for the deprecation of dynamic commands.

I appreciate you.

Zachatoo commented 1 year ago

It does work in non-frontmatter areas. I've tried to get rid of the newer 'properties' thing so it will always work where I want to put it but alas.

I don't think Dataview would work in frontmatter before properties was introduced either, Obsidian would parse it as the Dataview string instead of the result of the Dataview execution. Though the same thing happens for Templater dynamic commands.

Either way, having dynamic values in frontmatter hasn't totally been solved, unless it's created/updated values, then the Linter plugin has solved that.

pauby commented 1 year ago

having dynamic values in frontmatter hasn't totally been solved

This is precisely why I'm still on 1.15.3 of Templater and can't move until it is resolved. I create a lot of tags based on information obtained dynamically when the page is created.

AB1908 commented 1 year ago

That's not creation though, is it? It's only something you see when viewing, it doesn't actually exist.

pauby commented 1 year ago

@AB1908 I'm unsure if that's aimed at me? What is created, exists, and is part of the frontmatter when the note is created.

AB1908 commented 1 year ago

It's not a dynamic command then, can you explain what trouble you're facing?

pauby commented 1 year ago

The issue I'm having is that dynamic commands have been deprecated and I use them. That's why I said I'm sticking with 1.15.3 as they are not deprecated in that version.

I always update plugins as soon as they become available. I did this with 1.16.0 and it broke my code. I investigated the issue and found that dynamic commands had been deprecated. I downgraded to 1.15.3 and everything started working again.

I didn't want to hijack this thread. I only posted my comment as a temporary alternative to rewriting. I'm just sitting and watching. I appreciate you're trying to help but I'll go back to lurking just now as the issue is clear. Once I have the time to look at alternatives to my code, I'll update it and upgrade to 1.16+

AB1908 commented 1 year ago

Just to point out, it technically hasn't been deprecated, I think there's just been some sort of regression but as a whole, we've only been discussing deprecating it since it's kinda hard to maintain. It has some CodeMirror stuff if I remember correctly and ain't nobody wanna touch that.

pauby commented 1 year ago

Then I've misunderstood. Apologies. I suppose the result is the same (it's not working).

If it's a regression, and 1.16.0 was released 10 months ago, is there a particular reason it's not been fixed by now? Or is it just lack of maintainer time?

Zachatoo commented 1 year ago

@pauby Since we're now talking about a feature regression in 1.16.0 and not deprecating dynamic commands, can we shift this conversation elsewhere? You can make an issue or discussion and post your template there and someone can help figure out why your template isn't working, and either make a bug fix or make a suggestion to your template to get it working again.

I think the biggest change in 1.16.0 was shifting to using a different templating engine (rusty_engine), which likely broke some templates. Would not be related to deprecating dynamic commands.

pauby commented 1 year ago

@Zachatoo I'm answering questions that were asked of me. I didn't really intend to contribute here.

But, point taken. I'll go back to lurking.

Zachatoo commented 1 year ago

@pauby The point I was intending to make was that I'd love to help get your template working on 1.16.0, just not in this thread so that we're not sending notifications to people who don't care about your specific template. I apologize if any other messaging came across in my message. I appreciate your contributions, even if it wasn't your intention.

MarioRicalde commented 1 year ago

The issue with dynamic commands that makes it confusing is that they get cached and won't get updated until X or Y happens. Correct?

How about we provide with a "Replace All Tags in Documents", leaving the original command likew:

Score: %%<%+ tp.frontmatter["score"] %>%%**5**

Where the surrounding **,** can be replaced for other strings? That way the command can be run any time, inline and would work with Obsidian Publish without magic.

AB1908 commented 1 year ago

Okay now that we have an active maintainer, it's time to start planning for this. Zach, can we start by making a cosmetic version bump with a modal that asks users to recheck their templates to stop using dynamic commands if possible?

Let's also create some sort of discussion thread on both GitHub and Discord to see if we can get folks to migrate to other solutions that might be appropriate. I think giving a 1-3 month headstart should be enough and there will always be older versions to fall back on. Do you think there are any other important fixes that should make it in before we start working on the deprecation?

We also need to clarify a few other goals but let's start with these I guess. And sorry to roleplay as a PM lolol.

AB1908 commented 1 year ago

I'm also thinking if there's a way to cleanly decouple this and hide it behind a feature flag for people that can't find replacements. They can choose to opt in but we'll keep it disabled by default. Thoughts?

MarioRicalde commented 1 year ago

The issue with dynamic commands that makes it confusing is that they get cached and won't get updated until X or Y happens. Correct?

How about we provide with a "Replace All Tags in Documents", leaving the original command likew:

Score: %%<%+ tp.frontmatter["score"] %>%%**5**

Where the surrounding **,** can be replaced for other strings? That way the command can be run any time, inline and would work with Obsidian Publish without magic.

I implemented the functionality above with dataview, I have an open PR that allows to do it with some extra code on part of the user for maximum customization.

We also need to clarify a few other goals but let's start with these I guess. And sorry to roleplay as a PM lolol. You are funny.

I'm also thinking if there's a way to cleanly decouple this and hide it behind a feature flag for people that can't find replacements. They can choose to opt in but we'll keep it disabled by default. Thoughts?

How about disabling the feature for new installs only. That way you can discourage the use of the feature while teaching them as to why it's off (in case it's someone using an old guide, or active user installing on another device).

If the reinstall scenario for active users can be mitigated, that'd be much better.

AB1908 commented 1 year ago

We'll need to investigate how much work that would.

Zachatoo commented 1 year ago

Zach, can we start by making a cosmetic version bump with a modal that asks users to recheck their templates to stop using dynamic commands if possible?

I can get a modal together this weekend. I think it would be wise to have a migration guide prepared first. Do you have any interest in taking that on or would you like me to work on that AB? Not saying a migration guide has to be done that fast, we can take our time on this. I'm definitely interested in at least contributing to the guide.

The issue with dynamic commands that makes it confusing is that they get cached and won't get updated until X or Y happens. Correct?

In my mind there's two main issues:

  1. The bugs piling up for it that no one wants to take the time to fix. I don't expect I'll ever have time to address them, there are other more pressing bugs/features that I want to address for the foreseeable future.
  2. There are existing plugins like Dataview and Linter that can accomplish most of what dynamic commands currently provides.

It's high cost and low benefit to maintain this feature.

I'm also thinking if there's a way to cleanly decouple this and hide it behind a feature flag for people that can't find replacements. They can choose to opt in but we'll keep it disabled by default. Thoughts?

I think leaving the feature in, but having it under a flag would be a good compromise for those that absolutely need dynamic commands, knowing that it will be considered a legacy feature and could break at any time.

AB1908 commented 1 year ago

Thanks Zach, I will try to get started on a migration guide. Apologies in advance if progress is slow on it, drowning in office work at the moment.

MAUGUS2 commented 7 months ago

I've been trying to create a dynamic Obsidian note template that generates a link to the previous day's note using the Templater plugin. My goal is to have the note title reflect the date of "Yesterday" dynamically, but I'm encountering some inconsistencies with the date variables and their output. Here are the details of what I've tried:

[[🏡 HOME/DailyNotes/<%+ tp.date.now("YYYY") %>/<%+ tp.date.now("MM") %>/<%+ tp.date.now("YYYY-MM-DD", -1)%> | Yesterday]] 

This line should return a note titled with the date of "Yesterday", e.g., "YYYY-MM-DD", but it's not working as expected.

 [[🏡 HOME/DailyNotes/<% tp.date.now("YYYY") %>/<%+ tp.date.now("MM") %>/<%+ tp.date.now("YYYY-MM-DD", -1)%> | Yesterday]] 

In this case, I get a note titled "NaN", which indicates that the date is not being processed correctly.

I've explored various approaches, including attempting to utilize dynamic JAVA to trigger a non-dynamic Templater script that generates the link for yesterday's note. Unfortunately, I haven't been able to resolve this issue. If anyone has encountered a similar problem and found a solution, I would greatly appreciate your insight.

Peace.