What is the best way to retrieve template argument outside the template?

Generally, in the template I create, I define a list of colors as:

#let colors = (
    primary: red,
    secondary: blue
)

My question is “What is the best way to give the user access to the template colors?”

Should I use a state variable ?

// in template.typ
#let user-colors = state("user-colors", colors)

// in main.typ
#let colors = context user-colors.get() // it gives a content

Do you have any recommendation ?

Thank you

If you want the user to be able to change (read/write access) the colors, then yes you will need a state-variable. They can use user-colors.update(...) without context and it will change for all content that relies on the color provided you used the state there.

However, for simple access, you will need context indeed and it will return content, see here: Why is the value I receive from context always content? - #2 by laurmaedje

1 Like

Hey maucejo, if your colors value is read-only and always the same regardless of the template parameters, you can expose it as a global variable in your template file, for example:

// template.typ
#let colors = (primary: red, secondary: blue)

// main.typ
#import "template.typ": colors, template

#show: template.with(...) // does not affect 'colors'

The colors are #colors.primary and #colors.secondary

However, if the value of colors depends on the parameters specified by the template, then you can’t define a variable inside the template function and expose it to the template user, as the variable would be local to the template function’s body (thus not accessible outside of it at all). Rather, you have two options (as of Typst 0.11.0):

  1. For read-only values (such as colors and text, but maybe even functions) which depend on template parameters, you can create a second function (let’s say, setup) which takes the template parameters, generates the read-only values and functions - including the template function itself - based on those parameters, and returns them so the user can access them. Here’s an example of how this could work (let’s say your values and functions depend on a title text parameter the user would specify to the template). Consider the template file below:

    // template.typ
    
    // Function depending on the 'title' parameter
    #let title-with-color(title, color) = text(color)[#title]
    
    // This will generate a read-only value depending on
    // the 'title' parameter
    #let generate-bold-title(title) = [*#title*]
    
    // This is your template which applies styles
    // to the document according to certain parameters.
    // We will expose this function exclusively through
    // 'setup' below, which will provide the 'title'
    // parameter.
    #let template(title, doc) = {
      set document(title: title)
      set heading(numbering: "1.")
      set text(font: "New Computer Modern")
    
      align(center, text(54pt)[#title])
      doc
    }
    
    // This is the main function you expose to your user.
    // It takes the parameters and, with them, prepares
    // constants and functions (such as the 'template' function)
    // which are returned to the user.
    #let setup(title: "Default title") = {
      // 1. Generate read-only constants
      // based on the setup parameters
      let bold-title = generate-bold-title(title)
      let emph-title = [_#title;_]
    
      // 2. Generate functions we want to expose
      // We will expose a function which allows the user
      // of the template to easily place the title they specified
      // with a certain color
      // Here, we pre-apply the 'title' parameter to the 'title-with-color'
      // function so that the template user only has to specify the 'color'
      // parameter later.
      let title-with-color = title-with-color.with(title)
      // Prepare the template function itself, pre-apply the parameters
      // (It has to remain a function to receive the 'doc' parameter,
      // apply styles and add content to it, and return the new,
      // modified document)
      let template = template.with(title)
    
      // 3. Return the values and functions generated based on
      // the given parameters
      (
        bold-title: bold-title,
        emph-title: emph-title,
        title-with-color: title-with-color,
        template: template
      )
    }
    

    You could then use that template as follows:

    // main.typ
    #import "template.typ": setup
    
    // 1. Customize the template's parameters,
    // obtain customized values and functions
    #let (bold-title, emph-title, title-with-color, template) = setup(title: "My Thesis")
    
    // 2. Apply the customized template function to the document
    // It will change the font, display the title at the top, and so on
    #show: template
    
    // 3. You can now use the template's generated values and functions!
    The bold title is #bold-title \
    The emphasized title is #emph-title \
    Here's my title but red: #title-with-color(red)
    

    This will produce the output below:

  2. For read-write values depending on template parameters, your only option will be to use Typst’s state system (see docs: State Type – Typst Documentation), as highlighted by @xkevio above (What is the best way to retrieve template argument outside the template? - #2 by xkevio). The way the state system works is that the value in a state variable with a certain key depends on your location in the document, so you can specify a initial value for your state anywhere in the document (for example, in your template function’s body) and later place special elements called “state updates” (created through, for example, my_state.update(5) to update a certain state to 5) in the document, which tell the Typst compiler “any queries for this state’s value AFTER this point in the document should return this new value.” This is why querying a state value requires a context { } block (see explanation: Context – Typst Documentation), which returns document content: the code inside it depends on where in the document it is evaluated, so the only way to know what the context block evaluates to is to place it somewhere in the document so it can use the correct values at that location. Consider the example below for how you would apply this to a template:

    // template.typ
    
    // Expose the state variable to template users so they can
    // query or change its value later if needed.
    #let title-state = state("template-title", "")
    
    // Similar to the previous example, but this time the 'title'
    // value is specified through the state and thus can be changed
    // through updates to it. Therefore, to access the state's value,
    // we wrap our code in `context` as the call to 'title-state.get()'
    // can return different values depending on where this function's
    // output is placed in the document (before or after state updates).
    #let title-with-color(color) = context {
       text(color)[#title-state.get()]
    }
    
    #let template(doc, title: none) = {
      // Apply styles as usual
      set heading(numbering: "1.")
      set text(font: "New Computer Modern")
    
      // Place a state update element at the top of the document
      // setting the initial value of the title state to the given
      // parameter. The user can change this later.
      title-state.update(title)
    
      // Place the title and the document.
      align(center, text(54pt)[#title])
      doc
    }
    

    You can then use the template above as follows:

    // main.typ
    #import "template.typ": template, title-state, title-with-color
    
    // Apply template styles and initialize 'title-state'
    #show: template.with(title: "My Article")
    
    // We can now use the template's functions normally
    // They will access the current value of 'title-state'
    Here's a red title: #title-with-color(red)
    
    // You can also access the title state's value itself, but note
    // that you need 'context' (which returns content that
    // needs to be placed to run your code) since the value of
    // the state depends on your document location, therefore
    // the same code can produce different outputs depending on
    // the current document context!
    #let title-indicator = context { "The title is " + title-state.get() }
    
    #title-indicator
    #title-state.update("New Title")
    
    // Notice how 'title-indicator' appears to have changed below,
    // since we updated the title state! Actually, it never changed:
    // 'title-indicator' is an element (contextual element) which
    // runs your code differently depending on the current context
    // each time it is placed! Hence why it is classified as 'content'
    // despite evaluating to a string (due to the "double quotes").
    #title-indicator
    
    // Even the 'title-with-color' function is producing different output now!
    // After the title state was updated, all calls to 'title-state.get()' will return
    // the new value - but for it to know which value to return, it has to be
    // aware of the current document location, which is only possible inside
    // a 'context' block! Hence why the function was defined using one.
    Here's a blue title: #title-with-color(blue)
    

    The document above produces:

Note that an initial setup function isn’t required when you use state for read-write values like in the second example above. It is, however, required if you want to expose read-only values, which is why the setup method is usually recommended if possible, given that, with it, you don’t need to use state and the introspection machinery (which requires placing content in the document) to access the exposed values. You can also mix both approaches to have a few easy-to-access but read-only values and functions (generated through setup) and a few read-write but harder to access (as it requires placing content in the document) values simultaneously. It’s all up to you!

Note that there are some ongoing proposals to make the setup example simpler, such as Syntax for calling a package entrypoint on import · Issue #2403 · typst/typst · GitHub and Explicit module syntax · Issue #4853 · typst/typst · GitHub, but they are still being discussed and no concrete changes are planned at the moment. Still, stay tuned for potential future improvements to the process of making templates, including, for example, custom types and elements (a planned feature).

Let us know if there are any further questions on the topic! Don’t forget to check out the Typst reference (Reference – Typst Documentation) as well as the template guide (Making a Template – Typst Documentation) for even more useful information.

4 Likes

Thank you @xkevio , as you suggest, my current try are to use state without success for the moment. I will read the doc again ;)

Wow @PgBiel ! Thank you for this detail and pedagogical answer. Currently, I use a global variable which works great for my personal use, but not from an user perspective.

I will try to work on the basis of your answer to find a solution that fits my need.

Thank you !

@PgBiel and @xkevio I have thought to my problem. In the original question, the problem is that a context gives a content. Despite the content of the content is correct (it is a dictionary), I can’t access to the content of the content itself.

It is like a picture, but you can see the object, but you don’t have it. In the case of a title, it works well, because text accepts a content.

To sum up, “How to access to the content of a content” ? If you think, it is another question, I open another thread.

Hey maucejo, recall this bit from my response:

This is why querying a state value requires a context { } block (see explanation: Context – Typst Documentation), which returns document content: the code inside it depends on where in the document it is evaluated, so the only way to know what the context block evaluates to is to place it somewhere in the document so it can use the correct values at that location.

It returns content regardless of what your code evaluates to, because code inside it is sensitive to where it is located in the document, so if you could access what is returned inside the context { } block outside of it (without placing some content somewhere), it would always be the same value as it would not have a fixed location in the document (which only content can have), but that’s not what you want (code outside of context { } is evaluated early, before document layout, so it would have no way to be aware of state updates, current location and other context-aware information). So, instead of:

#let value = context { value-state.get() + 5 }

// Error: trying to access context-aware code
// from non-context-aware code
#let sum = value + 10
The sum is #sum

Instead indicate that all of your relevant code is context-aware and needs to be placed in the document to work, as such:

#context {
   // Ok: All code depending on the context is
   // encapsulated in a context block
   let value = value-state.get() + 5
   let sum = value + 10
   [The sum is #sum]
}

Note that if you have a function which depends on the context to return a value that isn’t content, you can simply have it not return context to allow users to access what it returns, as long as they themselves use context:

// Don't:
#let get-height() = context here().pos().y
// Error: can't get contextual value from non-contextual code
#let sum = get-height() + 5pt 
#sum

// Do instead:
#let get-height() = here().pos().y
// Error: here() requires context, so this call fails
#let sum = get-height() + 5pt 

// Ok: wrapping in context, we can access what get-height()
// would return at this location
#context {
  let sum = get-height() + 5pt
  sum
}

For more information (and for further help on this matter), please check out this thread: Why is the value I receive from context always content? - #2 by laurmaedje

1 Like