How to cleanly declare arguments to document?

I’m experimenting with writing a document in a way that exposes arguments for query. This is per-se not that complicated, but I struggle with doing it cleanly. My ideal result would look something like this:

#import "some-package": declare_args
#let args = declare_args((sender: "Alice", receiver: "Bob"))

Hi #args.receiver, this is #args.sender

where the declare_args function:

  • inserts a metadata content block with the args, such that we can query for it from an external program
  • replaces the values of sender and receiver by values from sys.inputs (if provided)
  • colors all values of keys that were not provided via sys.inputs red to show they need to be provided (but not stopping the compilation)

This is as such not possible, since typst cannot return a value and insert a block at the same time.

I was able to get the following variants of this, but am not happy with them:

#let args =(sender: "Alice",receiver: "Bob")
#declare_args(args) // basically #metadata(args) <args>
#let args = mark_required(args) // Merge sys.inputs and color them
// needs two calls and reassigns the args

.

#let (args, meta) =declare_and_mark((sender: "Alice",receiver: "Bob")) // just the other two functions combined returning an array
#meta
// You need to insert that meta variable and don't really notice if your forget

.

// Using state and a regex rule, collect all occurrences of ARG_*, replace them with sys.inputs.at(*) or text(red, *) and insert a metadata tag of state.final()
#show: regex_spam
Hi ARGS_sender, this is ARGS_receiver
// Using a variable gives better hints and autocompletion. (and regex rules are scary)

.

#let arg_func(key) => [
  #metadata(key) <arg>
  #sys.inputs.at(key,default: text(red, args.at(key)))
]
Hi arg_func("receiver"), this is arg_func("sender")
// Again, the variable with fields just has better support

What I tried but didn’t get to work:

  • A show rule that somehow injects a variable into the document (not possible due to scoping)
  • Returning a variable and inserting a content block by the same function (not possible)

Any ideas how I could get closer to my ideal result?

Hello!
I think this should be achievable with the following code, you were nearly there!

#let declare_args(it) = {
  let inputs = sys.inputs
  for (k,v) in it {
    (str(k): [#inputs.at(k, default: text(red, v))#metadata(v)#label(str(k))]) 
  }
}
#let args = declare_args((sender: "Alice", receiver: "Bob"))

Hi #args.sender, this is #args.receiver.

If I compile with input sender=Steve, I get
image

and you can query either a label or metadata directly to get

❯ typst query test.typ "<sender>"
[{"func":"metadata","value":"Alice","label":"<sender>"}]
❯ typst query test.typ "<receiver>"
[{"func":"metadata","value":"Bob","label":"<receiver>"}]
❯ typst query test.typ "metadata"
[{"func":"metadata","value":"Alice","label":"<sender>"},{"func":"metadata","value":"Bob","label":"<receiver>"}]

I am not sure what you mean here, but I think you forgot that you can just return a dictionary with content values!

2 Likes

Thanks! That’s exactly what I wanted.

Just for your information, I tried to do something like this

#let declare_args(args) = {
  [#metadata(args) <args>]
  args.map(/* replace the values */)
}

Which will fail, because it wants to join the resulting content and dictionary (which is perfectly reasonable, I just wanted to have it insert the content and return the args^^)

1 Like

Keys in dictionary must be of type str, so you don’t need to convert them:

#let declare-args(dictionary) = for (k, v) in dictionary {
  ((k): [#sys.inputs.at(k, default: text(red, v))#metadata(v)#label(k)])
}

#let args = declare-args((sender: "Alice", receiver: "Bob"))

Hi #args.sender, this is #args.receiver.
1 Like

The for loop works the same way as this map-join call chain:

#let declare-args(dictionary) = {
  dictionary
    .pairs()
    .map(((k, v)) => (
      (k): [#sys.inputs.at(k, default: text(red, v))#metadata(v)#label(k)],
    ))
    .join()
}

But sometimes for loop is just cleaner, as in this case. But I saw a lot of cases where it’s the other way around.

2 Likes