I am applying show rules after another show rule. I thought all headings would be green according to the second rule.
However, headings after the show-function rule are not. Why?
#[
#show heading: it => { set text(purple); it }
#[
#show heading: set text(green)
= purple
]
#[
#show heading: it => { set text(green); it }
= purple
]
]
#[
#show heading: set text(purple)
#[
#show heading: set text(green)
= green
]
#[
#show heading: it => { set text(green); it }
= green
]
]
Which “order” do the show rules apply? We can try with this example:
#show heading: it => { set text(green); [A(] + box(it) + [)a] }
#show heading: it => { set text(red); [B(] + box(it) + [)b] }
#show heading: it => { set text(blue); [C(] + box(it) + [)c] }
= My Heading
The rule A is first in the source and is closest to the heading in the produced document.
But rule C is applied first, which we can confirm like this, if we use it.body there, it stops:
#show heading: it => { set text(green); [A(] + box(it) + [)a] }
#show heading: it => { set text(red); [B(] + box(it) + [)b] }
#show heading: it => { set text(blue); [C(] + box(it.body) + [)c] }
= My Heading
So the conclusion is that the rules are applied with C first, then B, then A. But they are applied with nesting
Apply C, which produces style and a heading
Apply B style to the heading inside what C produced
Apply A style to the heading inside what B produced
So this is an unfortunate or interesting thing with show rules with functions, they sort of apply inside out. The rule that runs last places its set directives closest to the actual heading.
It think it looks like it’s still applied to the heading elements, it is always the heading when the rules are applied. But the content that rule C produced around the heading is outside (wrapping) it when rule B is running.
Here is how rules are applied for a particular element according to my understanding (copy-pasted from another thread but it better belongs here I guess):
All applicable show-set rules are gathered, with later rules overwriting previous ones so
#show ref: set text(red, weight: "bold")
#show ref: set text(blue)
will show refs in blue and bold.
The element is materialized in accordance with these show-set rules. For example after
#show ref: set ref(supplement: [X])
all ref elements with unspecified supplement will be materialized with supplement [X].
Normal show rules (i.e. functions, not show-set) are applied starting with the most local one. The rule functions are called with the materialized element as argument. Each rule is applied recursively to its own output, until the output contains nothing new that matches the show rule. For example
#set math.equation(numbering: "(1)")
#set ref(supplement: [0])
#show ref: it => {
let i = int(it.supplement.text)
if i < 3 {
let new = ref(it.target, supplement: [#(i + 1)])
return [{it: #it, new: #new}]
}
[{final: #it}]
}
$ x = y $ <eq>
See @eq.
produces
Indeed the first time the show rule is applied, it returns the content {it: #it, new: #new}. The rule is then applied again on the new ref, but not on #it as that has already been processed. This is repeated until the new ref has supplement [3]: then the show rule simply returns {final: #it}. This output contains nothing new matching the show rule, so Typst moves on to the next (more outer) show rule.
Remarks:
Default values are used at the point where the element is materialized. If you change them afterwards it’s too late:
// Works for future heading elements (they are not yet materialized)
#show heading: set heading(outlined: false)
// Doesn't work: the `it` heading is already materialized
#show heading: it => { set heading(outlined: false); it }
// Works: the content of the heading is not yet materialized
#show heading: it => { set text(purple); it }
As explained by @bluss, a show rule applies to the element, but it can return something else than “just the element”. For example
#show heading: it => { set text(green); [A(] + box(it) + [)a] }
puts a style wrapper and box around every heading.
A show rule can replace the element with a new element of the same type:
// Replace every level-1 heading with a level-2 heading
#show heading.where(level: 1): it => heading(level: 2, it.body)
The show rule can also replace the element with something completely different:
// Replace headings with text `x`
#show heading: [x]
Note that a query will also find discarded elements. So in the first case above, query(heading) will return both the level-1 and level-2 headings. And in the second case it will return the heading that was replaced with [x].
As a side comment, this is the reason why probably packages/templates should return the original it instead of content when possible, so that template rules can compose with user rules. A (coincidentally?) good example is in the reference docs.
EDIT: below an example from glossarium README
#show: make-glossary
#show ref: it => {
let el = it.element
if el != none and el.func() == figure and el.kind == "glossarium_entry" {
// Make the glossarium entry references dark blue
text(fill: blue.darken(60%), it)
} else {
// Other references as usual.
it
}
}
The short answer is that show rules are wrappers, that can introduce styling issues later down the road (if you need to style the element further). Which is what you see. Therefore, you should always avoid using show rules and instead use set/show-set/set-if/show-set-if rules, i.e., try something else before making show rule. Usually, when you have a single template, it doesn’t matter, since you style everything once, and you know exactly how to style everything.
I finally have time and got to read through all these helpful explanations. Thank you all!
Conclusions
Use simple rules (set, show-set, set-if) if possible.
Show-function rules are applied from top to bottom. However, each function is applied to the element, which may or may not be the output of the last function.
Is there a question here? I think the question is why they are different. We notice bold face is applied differently.
The reason why is that inherent or existing set rules for heading apply first. When we enter the first rule (C) we already have the default rules for headings, that set bold text weight. These rules apply to everything that matches the show heading rule, so it includes our extra text “C(” and so on. (These are rules 1. and 2. in sijo’s overview above)