How can I create an "actions" function and table of actions?

I’m trying to create a function to be mark inline actions in a run of text which indicate the assignee in a margin note (via the drafting module) and then add them to a global dictionary to be able to render a table later at the end of the document.

This was my attempt:

// Create a dictionary to store actions for each assignee.  
// The key will be the assignee and the value will be array of actions
// Due to scoping constraints, use the "state" function
#let actions = state("actions", ("": ()))

// Define a helper function to add an action to the global dictionary
#let _add_action(assignee, action) = {
  let actions_for_assignee = actions.at(assignee, ())
  actions_for_assignee.append(action)
  actions.update(assignee, actions_for_assignee)
}

// Define an action for use in the body text
#let action(body, assignee: "", side: left) = {
    // Add action to the global state to be able to render a table later
    _add_action(assignee, body)
    // Display the action text inline as usual
    body
    // Mark the presence of an action in a sidenote with the assignee
    margin-note(side: side)[#strong("Action:") #assignee]
}

Here's an example of text with an #action(assignee: "Ben")[action for Ben to do].

This won’t compile, complaining at line

  let actions_for_assignee = actions.at(assignee, ())

“text is not locatable”

I suspect I’m misunderstanding how dictionaries and/or state variables should working. Any suggestions would be very welcome!

Hi there! Welcome to the Forum.
The actions.at() doesn’t work, because it references the at method of the state element itself, not the dictionary (see the typst doc). The at function of the state expects a location in the document or something like that (label etc.), so that’s what the error is about. Here is:

// Create a dictionary to store actions for each assignee.  
// The key will be the assignee and the value will be array of actions
// Due to scoping constraints, use the "state" function
#let actions = state("actions", (:))

// Define a helper function to add an action to the global dictionary
#let _add_action(actions, assignee, action) = {
  if not assignee in actions {
    actions.insert(assignee, (action,))
  }
  else {
  actions.at(assignee).push(action)
  }
  return actions
}

// Define an action for use in the body text
#let action(body, assignee: "", side: left) = {
    // Add action to the global state to be able to render a table later
    actions.update(x => _add_action(x, assignee, body))
    // Display the action text inline as usual
    body
    // Mark the presence of an action in a sidenote with the assignee
    // margin-note(side: side)[#strong("Action:") #assignee]
}

Here's an example of text with an #action(assignee: "Ben")[action for Ben to do]

#action(assignee: "Ben")[new action for Ben to do].

The current content of actions: 

#context actions.get()

#action(assignee: "Steven")[a new person with some action].

#action(assignee: "Steven")[steven action].

The current content of actions: 

#context actions.get()

Note that I commented the “margin-note” out, because you didn’t provide the function, so it wouldn’t compile otherwise.
I mainly changed the update function to “actions.update(x => _add_action(x, assignee, body))”. I also changed the helper function so it adds the actions to the array in the dictionary entry if it is present and add a dictionary entry otherwise. The x in the “actions.update” is the old value of the state and the content behind the “=>” is the new value of the state. The helper function _add_action now changes the x (which is the first parameter of the function) and returns the new value.

If there are any questions left or I misunderstood something, don’t hesitate to ask again :slight_smile:. If this answer solves your problem, please mark it as the solution.

PS: when initializing the state, you can use “state(“actions”, (:))” to initialze an empty dictionary.

2 Likes

@Adrian_Weitkemper - thank you so much - this is really helpful and works for me perfectly.

For anyone following along at home, here’s where I got to with my code (including a function to output the table of actions), thanks to help from @Adrian_Weitkemper.

#import "@preview/drafting:0.2.2": margin-note

// Create a dictionary to store actions for each assignee.  
// The key will be the assignee and the value will be array of actions
// Due to scoping constraints, use the "state" function
#let actions = state("actions", (:))

// Define a helper function to add an action to the global dictionary
#let _add_action(actions, assignee, action, timeframe, pos) = {

  let action_and_timeframe = (action, timeframe, pos)

  if not assignee in actions {
    actions.insert(assignee, ( action_and_timeframe,))
  }
  else {
    actions.at(assignee).push(action_and_timeframe)
  }
  return actions
}

// Define an action for use in the body text
#let action(body, assignee: "", timeframe: "ASAP", side: right) = {
    // Add action to the global state to be able to render a table later
    context {
      let pos = here().position()
      actions.update(x => _add_action(x, assignee, body, timeframe, pos))
    }
    // Display the action text inline as usual
    body
    // Mark the presence of an action in a sidenote with the assignee
    margin-note(side: side, stroke: red)[#strong("Action:") #assignee]
}

// Define a function to render a table of actions
#let table_of_actions() = {
  context {

    // Get the dictionary of actions
    let actions_dict = actions.get()

    // Custom styling for the table    
    show table.cell: it => {
      if it.y == 0 {
        strong(it)
      } else {
        it
      }
    }
    set table.hline(stroke: .6pt)

    // Render the table
    table(
      columns: (4fr, 1fr, 1fr),
      stroke: none,
      [Action], [Assignee], [Due],
      table.hline(),
      // Iterate over the dictionary and render each assignees' actions
      ..for pair in actions_dict.pairs() {

        let (assignee, actions) = pair
        // Iterate over the actions for each assignee
        for action_timeframe_and_pos in actions {
          // Unpack the action, timeframe, and position
          let (action, timeframe, pos) = action_timeframe_and_pos
          // Render the action in the table, linking back to the original text
          (link(pos, action), assignee, timeframe, table.hline())
        }
      }
    )
  }
}

Please do let me know if I missed a neater way of doing anything, or any other code improvements!

fwiw, since you want to have a list of actions, you may be interested in how packages add outlines for custom elements. Usually they do that by producing figures with custom kinds, e.g. theorion:

#import "@preview/theorion:0.3.3": *

...

#theorem(title: "Euclid's Theorem")[
  There are infinitely many prime numbers.
] <thm:euclid>

#outline(title: none, target: figure.where(kind: "theorem"))

The code to implement this is not trivial, but from a quick look it seems like the relevant parts are here, maybe you can use some of it: typst-theorion/core.typ at main · OrangeX4/typst-theorion · GitHub (the code is MIT licensed)