What are the semantics of show rules wrapping headings in a block?

Earlier, I raised what I thought was a bug on GitHub, which I then got told was intentional behavior.

The question is regarding show rules targeting headings.

Consider the following snippet

#set page(height: auto, width: auto, margin: 1cm)
#show heading: block // this line does not affect anything ?!

= hello
// big space between these headings
= hello
// smaller space here
#block(heading[hello])
// big space again
= hello
// also big space
= hello
// small space again
#block(heading[hello])
// and big spacing for consecutive block-wrapped headings
#block(heading[hello])

Which produces the following output:
image

As to my understanding, the show element: function syntax (show heading: block in this case), is esentially a wrapper for function(element(content)) (i.e. block(heading(content))).

Another example is the code

#show "Project": smallcaps

taken directly from the docs. Naturally, this would be a wrap any Project to #smallcaps[Project].

I am not quite sure I understand the answer provided on GitHub, as I read it, it would assume the order heading(block(content)), or some hidden semantic rule?

Can someone explain to me why there is a difference between wrapping a heading manually and via a show rule – where do the semantics change?

There are default show-set rules for heading in typst/crates/typst-library/src/model/heading.rs at c21c1c391b48f843c8671993a28eaf1fe0d40b89 · typst/typst · GitHub that are almost like:

#show heading: it => {
  let level = it.level
  let scale = (1.4, 1.2).at(level - 1, default: 1.0)

  let size = 1em * scale
  // level 1: 1.285714286em
  let above = 1em * (if level == 1 { 1.8 } else { 1.44 }) / scale
  // level 1: 0.535714286em
  let below = 0.75em / scale

  set text(size, weight: "bold")
  set block(above: above, below: below, sticky: true)
  it
}

While the default block settings is just

#set block(spacing: 1.2em)

above and below will inherit the spacing unless overridden.

If you do show heading: block, then it will wrap heading block into block and you will get block(block(heading content)). Since heading show rule doesn’t set any block stuff other than inset, the show-set rules are doing all the work on block and text.

#set page(height: auto, width: auto, margin: 1cm)
= hello 1 // below: ~0.54em
= hello 2 // above: ~1.29em, below: ~0.54em
#block(heading[hello 3]) // spacing: 1.2em
#block(heading[hello 4]) // spacing: 1.2em

image

Here, between 1 and 2 you have 0.54 and 1.29, so the bigger margin wins.
Between 2 and 3 you have 0.54 and auto, so since 0.54 below is more specific than 1.2 spacing, the 0.54 wins.
Between 3 and 4 you have 1.2 spacing, so this is what used.

I guess this is the logic, though I didn’t look into it before.

There is also a separate problem that when you override em with em in some cases, it will stack up, see Smaller font when using a raw block in a show rule · Issue #1331 · typst/typst · GitHub.


After thinking for a bit, I guess the outer block absorbs the inner settings, because the inner one is the only element inside outer block, so its settings don’t affect anything around. So, in the end, I think the behavior makes sense.

2 Likes

Thanks, that’s the last puzzle piece, I was trying to dissect the problem without getting a solution. (I guess you linked a more complicated problem, but just the fact that em is contextual is important here.)


Here’s my tip for debugging: insert this code in a sandbox document on the typst webapp. Hover over the it in the show rule and you can observe all the settings of each block in the document.

#set page(height: auto, width: auto, margin: 1cm)
#show block: it => it
#set block(stroke: 0.20pt)


= hello1
// big space between these headings
= hello2
// smaller space here
#block(above: 1.4 * 1.29em, heading[hello3])

One would think that setting above spacing to 1.29em on your own block would make the spacing line up. But it has to be 1.4 * 1.29em for the spacing to line up - that’s the puzzle piece Andrew put down - the reason is that the text size inside the heading is larger, so 1.29em inside a heading is larger than 1.29em outside the heading - since 1em is the current contextual font size. Interesting all around.

You can’t observe spacing in any way, since it’s #[external].

Thank you so much for the detailed reply! It all makes sense to me now.

I guess my solution will be to use

#show heading: set block(spacing: 1.2em)

instead.

This has lead me to a follow-up question though.

When playing around with it, I noticed that the spacing in this example is still smaller between heading 3 and 4. Why is that? Does the explicit spacing set by the show rule not overrule any other spacing (and are they not the same, as auto resolves to 1.2em)?

#set page(height: auto, width: auto, margin: 1cm)
#show heading: set block(spacing: 1.2em)

= hello 1
= hello 2
#block(heading[hello 3])
#block(heading[hello 4])

drafting(2)

1 Like

I think it’s about the other point I brought up. em as a unit is relative to current font size.

The font size is larger inside heading than outside it. So for blocks inside heading, 1.2em is larger than 1.2em for blocks outside/wrapping the heading.

So can we test that?

#show heading: set text(size: 11pt)
#set text(size: 11pt)

Set the same text sizes in document body and the headings, and the spacing seems to line up with your setting, so it checks out.

1 Like

Hey @T1ng, welcome to the forum! I’ve changed your question post’s title to better fit our guidelines: How to post in the Questions category

Make sure your title is a question you’d ask to a friend about Typst. :wink:

It doesn’t, it’s just that default show-set rules always apply to whatever you replace an element with in a show rule. There is currently no way to “disable” them.

For example, show heading: [abc] will still show “abc” in a larger and bold font, even if you theoretically “discarded” the original heading, as its default show-set rules still apply.

The reason default show-set rules are used in the first place is so you can override those settings (like bold headings) without having to rewrite the heading from scratch in a show rule, through your own show-set rules.

Therefore, show heading: set block(spacing: ...) is indeed the correct answer if you want to change headings’ inter-block spacing back to the default.

What? I’m talking about block(heading[]), there is no show rule.

I still don’t get what you mean since the outer block does not inherit the inner block’s settings at all here, unless by absorb you mean that it causes the inner block’s settings to not apply at all, in which case that would be correct.

In addition, regarding

I should note (as a brief clarification for others) that show heading: block does create a block(heading[]), that is, it is not block(block(heading contents)) at first (but it does become that after applying the default heading show rule). However, the heading default show-set rules still apply to the whole new block(heading[]) created by that show rule, not only to the heading inside that block, for the reasons mentioned previously.

by absorb you mean that it causes the inner block’s settings to not apply at all

Yes.