Template rendering question

I have a somewhat refined version of a template question I’d struggled with a few days ago. And, up front, I am in the process of learning both v2 and Lua, so … sorry in advance.

Question: In the current build of v2, is there a way to evaluate a string as a template in realtime such that the dates and cursor placement widget (for example) are properly interpreted upon page creation using an event.listener?

My situation:

  • In a space-lua that happens to be my CONFIG file, I’m defining a template:
local meetingTemplate = [[
---
date: ${os.date("%Y-%m-%d")}
time: ${os.date("%H:%M")}
tags: meeting-notes
---
      
# Agenda
- 

# Notes
- |^|
      
# Attendees
- 
      
# Tasks
- 
]]
  • I have a listener per Zef for page creation as follows (well, the relevant part of it anyway):
event.listen {
  name = "editor:pageCreating",
  run = function(e)
    if e.data.name:startsWith("Meeting/") then
      return {
        text = meetingTemplate
      }
    end
  end
}
  • As might be expected, this creates the page with the text of the template, but it does not render anything in the text string comprising the meetingTemplate. It just echoes the literal text.
  • This is a pretty simple use case, but I have some slightly more complex use cases, such as duplicating my v1 setup including queries of upcoming, due, and overdue tasks on my journal pages.

Templates are defined with template.new, and the opening delimiter is [==[ and the closing is ]==]. Using the template API example as a starting point, I put together something that almost does what you’re looking for:

(space-lua block)

examples = examples or {}

examples.meeting = template.new(
[==[
---
tags: meeting-notes
date: ${formattedDate}
time: ${formattedTime}
---
      
# Agenda
* 

# Notes
* |^|
      
# Attendees
* 
      
# Tasks
*
]==]
)

event.listen {
  name = "editor:pageCreating",
  run = function(e)
    if e.data.name:startsWith("Meeting/") then
      return {
        text = examples.meeting({formattedDate = os.date("%Y-%m-%d"), formattedTime=os.date("%H:%M")})
      }
    end
  end
}

I can’t get the date and time expression to inline, but that is likely my fault in some way.

The cursor placement still doesn’t work and just outputs |^|. This might be a bug, I’m not sure.

If you want to stray away from that pattern, you could write the template as a #meta page:

---
command: "Start a new meeting"
confirmName: true
forPrefix: "Meeting/"
suggestedName: "Meeting/${date.today()}"
tags: meta/template/page
---
---
date: ${os.date("%Y-%m-%d")}
time: ${os.date("%H:%M")}
tags: meeting-notes
---
      
# Agenda
- 

# Notes
- |^|
      
# Attendees
- 
      
# Tasks
- 

With that template, you can run the command “Start a new meeting” and everything works fine. The forPrefix frontmatter means the template will be applied for a new page in Meeting/ but it still doesn’t respect the cursor placement (it renders “|^|”).

Interesting that, if I’m reading you right, we’re getting essentially the same behavior from two distinct methods of attacking the issue, though I wouldn’t be surprised if event.listen and template.new were doing the same thing behind the scenes.

I’m guessing it’s something not quite implemented. I may see what I can see in the source this weekend.

Anyway, the workflow issue I’m trying to solve is that I find myself doing a lot of drive-by references to things that I go back and fill in later. For example, I might note in my day that I have ALT-SHIFT-M Meeting A and ALT-SHIFT-M Meeting B with ALT-SHIFT-P Person A who works at ALT-SHIFT-O Organization X. I enter references to things, and if they exist it’s just a link. If they don’t exist, I can click through and add some notes to, say, Person A when I have a second.

So the “type command to start a meeting” is a GREAT solution, but it doesn’t really fit with how I’m organically using v1. Which is interesting because I’ve been preaching to people for years to meet the tool where it is, but SB is just so good at being useful for my odd quirks.

I was wrong about getting the same behavior here. I must have forgotten to turn off one behavior while I was testing the other, so I was seeing the same thing.

Anyway, I figured out something that works I think. I corrected the event listener for editor:pageCreating to work properly with the formatted date and time, and I added a event listener for editor:pageLoaded to move the cursor to the |^| in the template:

examples = examples or {}

examples.meeting = template.new(
[==[
---
tags: meeting-notes
date: ${os.date("%Y-%m-%d")}
time: ${os.date("%H:%M")}
---
      
# Agenda
* 

# Notes
* |^|
      
# Attendees
* 
      
# Tasks
*
]==]
)

event.listen {
  name = "editor:pageCreating",
  run = function(e)
    if e.data.name:startsWith("Meeting/") then
      return {
        text = examples.meeting({})
      }
    end
  end
}

event.listen {
  name = "editor:pageLoaded",
  run = function(e)
    local pageName = e.data[1]
    if pageName:startsWith("Meeting/") then
      local text = editor.getText()
      local start, _ = string.find(text, "|^|", 0, true)
      if start then
        editor.replaceRange(0, #text, text, true)
      end
    end
  end
}

The editor:pageLoaded listener checks to see if the loaded page is in the Meeting directory. If it is, it loads in the text and checks for the plain string |^|. If it finds |^|, it uses the editor.replaceRange API to find |^| in the text, remove it, and move the cursor there. That does mean the string |^| will be removed on page load for any meeting notes.

1 Like

There is a third argument to editor.insertAtPos (as well as editor.insertAtCursor) that take a cursorPlaceholder argument. When set to true, it will find |^| and put the cursor there. See the implementation section of Page Templates here: Library/Std/Page Templates

Does that help?

1 Like

Here’s where I ended up, at least for now. I overrode a Library/Std function because I was having an issue the override seems to have fixed, but I’m not convinced this was necessary.

-- Function: Create a page from a template (override Library/Std to test what may be a race condition
local function createPageFromTemplate(templatePage, pageName)
  -- Won't override an existing page
  if space.pageExists(pageName) then
    editor.flashNotification("Page " .. pageName .. " already exists", "error")
    return
  end

  local tpl, fm = template.fromPage(templatePage)
  local initialText = ""
  if fm.frontmatter then
    initialText = "---\n"
      .. string.trim(template.new(fm.frontmatter)())
      .. "\n---\n"
  end

  -- Write an empty page to start
  space.writePage(pageName, initialText)
  editor.navigate({kind = "page", page = pageName})

  -- Insert content once page is fully loaded
  event.listen {
    name = "editor:pageLoaded",
    once = true,
    run = function(e)
      if e.data[1] == pageName then
        local tries = 0
        local function tryInsert()
          local doc = editor.getText()
          if doc and #doc >= #initialText then
            editor.insertAtPos(tpl(), #initialText, true)
          elseif tries < 10 then
            tries = tries + 1
            setTimeout(tryInsert, 100)
          else
            editor.flashNotification("Failed to insert template content", "error")
          end
        end
        tryInsert()
      end
    end
  }
end

-- Listener: When creating a page, use a template based on the name in the path (top-level, e.g., "Meeting"), or "Daily Note" for Journal, or a blank bullet for anything else
-- TODO: Change Daily Note to New Journal
event.listen {
  name = "editor:pageCreating",
  run = function(e)

    local templateFlag = e.data.name:match("([^/]*)")
    local templateName = ""
    
    if templateFlag == "Journal" then
      templateName = "Library/Templates/Daily Note"
    else
      templateName = "Library/Templates/New " .. templateFlag
    end

    if not space.pageExists(templateName) then
      templateName = "Library/Templates/Default"
    end
    
    createPageFromTemplate(templateName, e.data.name)

  end
}

-- Function: Generic function to create structured links for drive-by linking to certain types of content (meetings, people, organizations, etc.)
local function insertNewLink(opts)
  local name = editor.prompt("Please enter " .. opts.promptLabel)
  if not name then
    name = opts.defaultPrefix .. os.date(opts.fallbackFormat or "%Y-%m-%d-%H-%M")
  end

  local pageName = opts.contentType .. "/"
    .. (opts.subfolder and opts.subfolder() or "")
    .. name

  local link = "[[" .. pageName .. "]]"
  if opts.prefixTag then
    link = opts.prefixTag .. " " .. link
  end

  editor.insertAtCursor(link)
end

-- COMMANDS

-- Use case: I tend to do drive-by linking, creating links to content as I go 
-- and then revisiting the links to fill in new content (including for new pages)
-- I could create the page when creating the link, so I may revisit doing that
-- recycling the above event listener.

command.define {
  name = "New Meeting",
  key = "Alt-Shift-m",
  run = function()
    insertNewLink {
      promptLabel = "meeting name",
      defaultPrefix = "New Meeting - ",
      fallbackFormat = "%H%M",
      contentType = "Meeting",
      subfolder = function() return os.date("%Y-%m-%d/%H/") end,
      prefixTag = "#meeting-notes"
    }
  end
}

command.define {
  name = "New Person",
  key = "Alt-Shift-p",
  run = function()
    insertNewLink {
      promptLabel = "the person's name",
      defaultPrefix = "New Person - ",
      contentType = "Person",
      prefixTag = "#person"
    }
  end
}

command.define {
  name = "New Organization",
  key = "Alt-Shift-o",
  run = function()
    insertNewLink {
      promptLabel = "the organization's name",
      defaultPrefix = "New Organization - ",
      contentType = "Organization",
      prefixTag = "#organization"
    }
  end
}

Everything seems to be working.