How to mutate variables in a show rule?

Let’s take a look at the following example:
Every second level 1 heading is supposed to appear bold in the outline.

Easy, right? Just use an integer variable, calculate variable mod 2, and set the style accordingly.

#let i = 1
#show outline.entry.where(level: 1): it => context {
  if calc.rem(i, 2) == 1 {
    strong(it)
  } else {
    it
  }
  i += 1
}

#outline(depth: 3, indent: auto)

Well… Not so easy. I am not allowed to mutate integers within a show rule.

That’s why I came up with the following hack using a counter.

#let i = counter("i-counter")
#i.update(0)
#show outline.entry.where(level: 1): it => context {
  if calc.rem(i.get().at(0), 2) == 1 {
    strong(it)
  } else {
    it
  }
  i.step()
}
    
#outline(depth: 3, indent: auto)

But I have to admit that it just doesn’t feel right. I am pretty sure this isn’t how I am supposed to use a counter.


That’s why I wonder:
What is the Typst-way to approach a problem like this?

No, you are pretty much spot on. Here is a slightly modified version:

#set page(paper: "a5")

#show outline.entry.where(level: 1): it => {
  let i = counter("i-counter")
  i.step()
  context if calc.even(i.get().first()) {
    strong(it)
  } else {
    it
  }
}

#outline(depth: 3, indent: auto)

= First
= Second
= Third
= Fourth

image

If you don’t use the counter anywhere else, then it’s a good idea to put it where it is used. You can also use functions like even which will improve readability.

From https://typst.app/docs/reference/introspection/counter/:herb:

Counts through pages, elements, and more.

With the counter function, you can access and modify counters for pages, headings, figures, and more. Moreover, you can define custom counters for other things you want to count.

Since counters change throughout the course of the document, their current value is contextual. It is recommended to read the chapter on context before continuing here.

2 Likes

There are many Typst ways… Here is below an approach without a counter, but I think much more expensive, because you query headings in the document.

#show outline.entry.where(level: 1): it => context {
  let i = query(heading.where(level: 1).before(it.element.location())).len()
  if calc.rem(i, 2) == 1 {
    str(i) + "-" + strong(it)
  } else {
    str(i) + "-" + it
  }
}

= A
= B
= C
= D
= E
#outline(depth: 3, indent: auto)

2 Likes

Thank you! That makes it more readable :)

I never would have thought of this. Really good to know! Thank you :)

1 Like

Yeah, that’s great too (more verbose, but doesn’t use a custom counter). I was only thinking by looking at the initial example. But you can slightly improve it:

#set page(paper: "a5")

#show outline.entry.where(level: 1): it => context {
  let selector = heading.where(level: 1).before(it.element.location())
  let count = query(selector).len() + 1
  if calc.even(count) {
    strong(it)
  } else {
    it
  }
}

#outline(depth: 3, indent: auto)

= #lorem(6)
= #lorem(6)
= #lorem(6)
= #lorem(6)
= #lorem(6)
= #lorem(6)
Output

One small mistake I noticed is that your code makes odd entries bold, not even.

1 Like

Thanks for clearing it up! I did not notice the bug :smiley:. Although, I still think the counter solution is easier to think of, and might be more performant…

1 Like

Shouldn’t be much more expensive, if at all. This one seems most idiomatic.

2 Likes