How to make functions compatible with parameters from a config/theme dictionary which the user selects at the top of a document

I want to be able to define themes that the user can select to use in a document. On the assumption that doing so will expand compilation time, I am trying to avoid using states/context as most functions will require some config data from a theme (although if I am wrong here please let me know).

I wonder if there is a more efficient approach to this problem then what I have come up with below, where functions, from a package, need to take some parameters from a theme and the user elects this the theme at the start of their document (after they include the package).

I have included an alternative with states below as a comparison.

To be clear, I should never need to change the theme throughout the document (or set it after a function is used). I am very happy for it to be set initially if this makes anything easier.

I am pretty new to Typst, so this might be very simple (e.g. maybe some show rule that remaps an arg in the function can solve all this really fast…)

Really look forward to feedback!

Here is a MWE:

Functions.typ:

// Theme configurations
#let themes = (
  first: (
    theme_colour: blue
  ),
  second: (
    theme_colour: red
  ),
)

// Function that creates themed functions
#let with-theme(theme-name) = {
  let colour = themes.at(theme-name).theme_colour
  
  // Return the themed functions
  (
    myrect: (width: 2cm, height: 1cm) => rect(fill: colour, width: width, height: height),
    mycircle: (width: 2cm, height: 1cm) => circle(fill: colour, width: width, height: height)
  )
}

// Helper to extract functions cleanly
#let use-theme(theme-name) = {
  let theme-funcs = with-theme(theme-name)
  (theme-funcs.myrect, theme-funcs.mycircle)
}


#let mytheme = state("mytheme", themes.at("first"))

#let myrect_state(width: 2cm, height: 1cm) = {
  context {
    rect(fill: mytheme.at(here()).theme_colour, width: width, height: height)
  }
}

doc.typ

#import "functions.typ": *

// Without states:
#let (myrect, mycircle) = use-theme("second")

#myrect()


// With states:

#myrect_state(width: 2cm, height: 1cm)

#mytheme.update(themes.at("second"))

#myrect_state(width: 2cm, height: 1cm)

I think there are three options for these kinds of configuration, all of which have their own tradeoffs:

  1. State-Based: Have a state object containing the configuration
  • Pro: This allows for better composability as it isn’t bound to scoping

    Example

    If someone makes a library using your package, they don’t need to add pass-through parameters for the theme options, but the end user can simply update the state (similarly to a set rule) and it will apply to the library-generated myrects.

    Otherwise, the myrects generated by the other library will either have the style the library author chose, or the library needs to have its own theme settings which also need to be set by the user.

  • Pro: This is the only option if it is required for every myrect to have the same config

    Example

    In my marginalia package, certain configuration options such as width of the margin notes must be consistent between notes, otherwise they cant correctly calculate their vertical positions correctly. Therefore these options must be state-based.

  • Con: Potentially less efficient compilation

  1. Generate themed functions: like your use-theme option.
  • Pro: If there is many such functions, this allows to easily apply a common theme to them
  • Con: This makes it harder to tweak the presets or have multiple variants
  1. Function Parameters & .with: Have all functions directly accept the configuration options, and expect the user to apply presets using .with
  • Pro: This allows all options to be specified (changed) on-the-fly if needed (maybe the user wants one (1) myrect to be red, but all others blue)
  • Pro: This allows using the “default” settings without a use-theme("default") step
  • Cons: This makes it more effort for the end user to have a consistent theme (need to set the themes for myrect and mycircle separately)
    This is less of an issue if you only have very few library functions.

I personally prefer the last option, or a combination of the latter two (i.e. every option can be specified on-the-fly, but there is a use-theme function which generates functions with uniform/preset defaults as a set)

However, opinions on this differ, and I think quite a few people prefer the state-based options for the composability benefits.

1 Like

I don’t really understand what you mean by theme (it’s a broad concept, so we may not all have the same picture in mind?), but for what it’s worth I’ve lately played with a theme + styles concept in package poster-syndrome.

The logic is that:

  • a "theme " provides a list of parameters for the text and par of each element (e.g. title, subtitle, section headers, etc.). It could also extend to other elements such as rect(), curve() etc.
  • this theme is converted into "styles " which are simply show: set <...> rules wrapping the corresponding theme elements

I found it useful to decouple the two steps because it’s much easier to work with a list of settings such as size: 24pt, fill: red than with a set of show: set ... rules.

show: set ... rules are a natural way to set parameters in typst, and so it’s probably a safe way to go as it doesn’t conflict with anything (e.g. even if the document set a specific style in a section, the user can still set it to whatever they want with their own show: set ... rule that overrides previous ones.

As I said, I may be missing the point entirely, as I’m thinking of a different kind of theming (?)

So I refer more to a style or deliberate variations of a theme that the user can select on a per document basis rather than needing to duplicate the package to make changes and then have the code be in two places