Hi everyone (thanks for pinging me @laurmaedje, I was too busy to make a post in the last two weeks)!
I recently put a lot of thought into template design and think that the issue is three-fold:
a) How can the user of a template tweak styles that are set or shown by the template?
b) How can the user manipulate the content (like a title page) that the template produces?
c) How can the user of a template define things like authors, title, abstract might be used in more than one place of a document?
For now I want to address the first two parts.
1. How templates apply styles
In my opinion, one of the biggest issues is that templates do not separate styles from content.
Let us start by thinking about the default style. Every type (like heading, figure, etc.) defines a default show rule that determines how it looks. The way the initial style is set up, it is super easy to customize everything - well with two exceptions, one of which is heading which out-of-the-box comes with a bold and slightly bigger font which is not entirely trivial to undo.
But the main thing here is that the document starts with this style before any content is produced which allows us to change everything like the page size upfront.
I believe that packages need the same approach. First, set up some nice default styles (purely through set and show rules) and then immediately give it up to the user to tweak and change these styles, e.g.
// ----- template.typ
#let template(body) = {
set page(margin: 2em)
set text(11pt)
body
}
// ----- main.typ
#show: template
Currently that is not what all templates I know of do. They set up styling and also produce content right away, like this:
// ----- template.typ
#let template(body, title: []) = {
set page(margin: 2em)
set text(11pt, font: "DejaVu Sans")
// problem:
align(center, text(weight: "bold", title))
body
}
// ----- main.typ
#set page(margin: 1em) // is overwritten :(
#show: template.with(title: [Typst])
#set page(margin: 1em) // works only from second page on :(
The style of the content that this template above produces cannot be freely styled. In particular, we cannot change the page margins on the first page, nor can we change the font of the title (as demonstrated in the code above)!
It becomes evident that we need a cut!
The usage of a template would need to be structured like the following:
- The code
should only change the style defaults and not produce any content. It’s like we start with an empty document after that.#import "template.typ" #show: template.style - Now is the place to intervene and tweak the style that the template has set up − just as we do with the “default style” (note that for some things we need
revokerules as described by @sijo above).#set page(margin: 1em) // now we are safe. - Finally we can give the template the opportunity to produce content!
#show: template.content // (not the best name yet)
Yes, this does make applying template a two-step procedure but I think it is the only way to get out of this awkward situation. Otherwise, things like the title page generated by a template will never be nicely re-stylable.
There is actually another way but it is flawed
Consider the example of a generated title page and imagine user-defined types are already available. A template designer could define a type
#type titlepage(
field: title,
...
show: it => ..
)
(very simplified, titlepage may be complex) and would use it in the template
#let template(body, title: []) = {
set font(11pt)
show ...
titlepage(title)
body
// more content that the template injects here
}
Now the font size of the content produced by the template becomes accessible!
#show titlepage: set text(9pt)
#show: template
#set text(9pt)
Unfortunately, you need to use set text(9pt) twice which I find uncompelling, to say the least. Also, the end-user needs to know a lot about the template for more involved (and realistic) scenarios.
2. How templates generate content
The second problem from the list above is that templates generate complex content that cannot be properly customized, just like the title of the ‘standard’ template:
align(center)[
#block(text(weight: 700, 1.75em, title))
]
No way to customize that one properly.
Instead, templates should define different types for, e.g., the title.
#type title {
field body
show it => block(it.body)
}
and in the styling part of the template (see above) write default show rules for it. In this case:
// in template.typ
#let styles(body) = {
..
show title: set align(center)
show title: set text(weight: 700, 1.75em)
body
}
The user can simply and selectively override these rules. Even more power is granted by writing full show rules, e.g.,
// in main.typ
#show template.title: it = circle(it.body)
In particular, we need types to achieve this because they give us fine-grained and selective control over the styling.