What is the "Typst-way" mental model around templates with shared logic and defaults?

I know the official “Making a Template” tutorial and I’ve looked at Why is the template function I wrote not working properly? as well as How can I load configuration and defined in another .typ file? who had somewhat similiar questions. I sense that more people have problems with the mental model around templates and Typst, and so do I. Therefore I wanted to “update” my understanding, to get it right.

Say you don’t want a “template” in the sense that the same document should be reproduced in version A and B e.g. a report that follows the same exact structure with slight differences.

But say a slide deck and a handout that share common logic (e.g. helper functions of some kind) and elements (e.g. headlines styled a certain way). What is the state of play or mental model to achieve this?

To showcase what I mean, say you have a…

main.typ

#import "template.typ": *
#show: template

// This doesn't work, but should show the "idea":
// ↓ how to "override" this in a way that actually works
#let highlight-color = blue

This should be #colorize("red") by default, but #colorize("blue") in after setting the `highlight-color` to blue. As should #colorbox("this").

and a…

template.typ

// Setting defaults
#let highlight-color = red

// Common logic/functions
#let colorize(body) = {
  set text(highlight-color)
  body
}

#let colorbox(body) = {
  box(fill: highlight-color, body)
}

#let template(doc) = {
  doc
}

What my example should showcase is what “my” understanding (and as it seems it is a quite common question or confusion) of what I would like to do with a template and the way Typst handles templates, includes and imports are still a bit at odds.

There is .with() but as I understand it this requires a function that produces “the same” document, just with different arguments.

What I would want to achieve is “just” shared logic and defaults for related documents. Is this possible too and just a matter of scope and wrapping everything in a function that does nothing further or not possible at all?

1 Like

This is very close to what was discussed in How can I have global configuration parameters for a module/package?.

It’s not a mental model, it’s the definition of the language, more specifically — Typst is based on pure functions because it can cache the function input & output and later reuse the output if the cache hit occurs. Hence why the fast incremental compilation exists. It’s a mix of procedural and functional programming, so it can be hard to grasp without any practice.

Basically, because all user functions are pure, you have to change their input to get a different output. Which is why you can’t change a default value simply by creating a same-name variable in a file where you import an already baked function and then expect that function to use the new local variable to change its behavior. You either:

  • change function’s named argument directly or through .with() (#let a-thing = a-thing.with(a: 5)),
  • change function’s positional arguments when used (can be also set + consumed via .with()),
  • change a state that the function uses to retrieve a value (need to use context outside or internally).

Same with show rule functions, but instead of using it in-place, you use it on the right side of a show rule.

Ah! Thank you, I didn’t find that question, maybe because it was framed a little different in terms focus on modules/packages. The mentioned What is the best way to retrieve template argument outside the template? is another one. Funny how users, myself included, chose very similar ways of showcasing independent from each other.

In your answer in the other thread, you gave a couple of examples, out of the variants not relying on state did or do you use one of the approaches yourself and would recommend it based on your experiences?

Can you list the approaches in question?

You mentioned using an init function for rubby and I guess the other approach I would find the most accessible (in terms of “looks”) would be the colorize.with(color: blue), but I wonder what I “should” use (I know of course it is depending on what should be achieved), but taking my example for a basic case for two somewhat related documents and wanting to switch things like font sizes, colors, page margins, what is most maintainable in your opinion and/or experience?

I think currently the most maintainable approach is to put all generic document styling into a single template function with some optional arguments for use with .with(). So things like title and author can be added as named arguments to the template() function. You can put the font size and page margins there as well, but you can also just set them separately after applying the template

#show: template
#set page(margin: 1.11cm)
#set text(14pt)

if template doesn’t set them or doesn’t use the needed values. This is the preferred or the only way when using someone’s template, since it can be not as customizable via its template function. But sometimes neither will work, in which case you would have to ask for a feature request or patch it yourself.

A standalone function should use named arguments that can be overridden at the call site or via .with(). But sometimes a let colorize(color, body) is better suited as you can shorten the call statement by not using a named argument, though the .with() will be the same with an exception that it can only be used once.

The init function I thing is pretty niche and in case with rubby, it can be avoided by just changing the arguments, so that you can just do #let r = ruby.with(dy: 1pt).

This is a general adivice, so the use of state might be better in a specific cituation. As with everything, the 6th sense comes with practice. Just try to make things as simple as possible without too much abstractions, if you can. With practice you will be able to know which approach is better suited for which use case. Sometimes (or maybe almost always) it’s just not worth it building a super complex logic just to not write a couple of characters, or something. Been there, done that.

1 Like

So do I understand you correctly that this would turn my example into something like this:

main.typ

#import "template.typ": *
#show: template.with(headline-size: 18pt)

#set page(margin: (top: 10%))
#set text(size: 12pt, font: "Libertinus Sans")

#let colorize = colorize.with(color: blue)
#let colorbox = colorbox.with(color: blue)

= Headline

This should be #colorize("red") by default, but #colorize("blue") in after setting the `highlight-color` to blue. As should #colorbox("this").

and

template.typ

#let colorize(body, color: red) = {
  set text(color)
  body
}

#let colorbox(body, color: red) = {
  box(fill: color, body)
}

#let template(doc, headline-size: 12pt) = {
 
  set page(margin: (top: 5%))
  set text(size: 12pt, font: "Comic Neue")

  show heading: set text(size: headline-size)
  
  doc
}

Would you say this means… in terms of rules (for myself!):

  • Just leave everything that can be set and is “procedural” so to speak as is in the template (meaning no further adaption for a template/variation setup needed)
  • Standalone functions get named arguments that can be directly modified using .with
  • Show-Rules are either left out of the template (and stay in each seperate file) altogether or get arguments on the template function if they really should be shared
  • … missing something?

(Mind that I’m controlling both ends here, the templates as well as the file for the specific case/output in my example slide deck vs. handout or whatever.)

Well, mostly yes, but if you want, you can add the font and margin parameters to the template() and just use one global show rule. But also this:

#let colorize(body, color: red) = text(color, body)

#let colorbox(body, color: red) = box(fill: color, body)

I think I remember that in some rare cases set text(color) would work better, but if you just apply this directly on a normal text, then you can use a shorter version.


Not sure what procedural means here, but the majority of set and show-set rules supposed to be in a template function, this is literally what it’s for, generally speaking.


Yes.


Well, the state thing, I guess, but that’s about it.

By “procedural,” I meant that, as I understand it (Disclaimer!), set rules get processed as they occur and can be reset in the following flow of the document at any time, but since the same is true for show… maybe it isn’t an accurate description.

I will try to experiment with state as well. Thank you for your answers!

1 Like

For the rules, you can check out Styling – Typst Documentation and Formatting – Typst Documentation.