Which show rule takes precedence?

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
  ]
]

My attempt

I can confirm that the selectors do match the heading, because replacing it with it.body will work. (except that the numbering and spaces are lost.)

In addition, if I inspect by repr, they will turn green.

Further information

Background: minor revision on page layout · Issue #11 · Ri-Nai/BIT-Typst-Template · GitHub (in Chinese)

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

bild

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

bild


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.

3 Likes

Thanks! So show-function rules are applied to the content of headings, rather than the whole heading elements?

#let a(it) = { set text(green); [A(] + box(it) + [)a] }
#let b(it) = { set text(red); [B(] + box(it) + [)b] }
#let c(it) = { set text(blue); [C(] + box(it) + [)c] }
#let c-body(it) = { set text(blue); [C(] + box(it.body) + [)c] }

#let example = [= example]

#[
  #show heading: a
  #show heading: b
  #show heading: c
  #example
]

#c(b(a(example)))

#[
  #show heading: a
  #show heading: b
  #show heading: c-body
  = heading
]

#c-body(b(a(example)))
// 💥 Styled does not have field "body"

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.

1 Like

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):

  1. 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.

  2. 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].

  3. 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
    image

    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].

5 Likes

Thanks for the excellent post.

1 Like

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
  }
}
2 Likes

the original it, or a new element of the same type (with a trick to avoid recursion) so the user rules still work…

1 Like

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.

And yes, the show rule closure is applied to the content of the heading and not to the element in the show rule selector. See Set rule inside show rule (closure) for the same element gets applied for table/grid · Issue #6219 · typst/typst · GitHub. If you know this, you can take advantage of it, like here.

2 Likes

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.
1 Like
#{
  set text(blue); [C(] + { set text(red); [B(] + { set text(green); [A(] + box(heading([My Heading])) + [)a] } + [)b] } + [)c]
}


#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

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)

No question.I want to express that the down code is actually equivalent to the up code

But visually the result is not equal, so they are different.