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?
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?
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
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.
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.)
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!