Why is the value I receive from context always content?

When working with context, you need to put everything that depends on the contextual information into the context block/expression itself. The explicit context expression is a trade-off: You get to know where in the document you are (accessible via here()) and which set rules are active there (accessible via things like text.lang). In return, the context value itself becomes opaque. You cannot peek into it, so everything that depends on the contextual information must happen within it.

Let’s look at this example to understand why:

#let val = context text.lang + "!"

#set text(lang: "de")
#val // "de!"

#set text(lang: "fr")
#val // "fr!"

We have a context expression that accesses the text language and adds an exclamation point to it. We store the resulting value in val. Then, we use it in multiple places of our document. As the comments indicate, it outputs different things: once “de!” and once “fr!”.

Now, let’s try using that same val and performing a string operation on it:

#let val = context text.lang + "!"

// error: type content has no method `starts-with`
#if val.starts-with("d") {
  ..
}

Typst complains with type content has no method `starts-with` . But why is val of type content rather than string?

Because val is not well-defined unless you put it into the document, as content. Depending on where in the document you put val, it can produce different things. val is not a string, it can become a string once it knows all the relevant contextual information. As long as val is just stored in a variable, the code within the context block never actually runs! And if you put it into the document twice, then it runs twice.

Thus, to fix the error, we must move all logic that depends on the contextual information into the context itself. Since the starts-with logic depends on the text language, we must move it inside:

#let val() = text.lang + "!"

#context {
  if val().starts-with("d") {
    ..
  }
}

To encapsulate the logic of adding the exlamation point, we have turned val into a function. Context will pass through from where it is made available to every called function.

Note that since context wraps any expression and if .. happens to be one, we can actually also just write it like this, without extra braces:

#context if val().starts-with("d") { .. }

Equipped with this knowledge, let’s take another look at the original problem. The goal was to display something if a counter is even and then step it. The conditional that checks calc.even definitely depends on the counter value and must thus move into the context expression:

#let my-counter = counter("my-counter")

#let alternate(it) = context {
  let value = my-counter.get().first()
  if calc.even(value) {
    it
  }
  my-counter.step()
}

#alternate[I'm visible]
#alternate[I'm not]
#alternate[I'm again visible]

Where to put the context block?

You might have noticed that, technically, the counter step does not depend on the current counter value, so there’s no need to put it into the context. We could just as well write this:

#let alternate(it) = {
  context {
    let value = my-counter.get().first()
    if calc.even(value) {
      it
    }
  }
  my-counter.step()
}

In this case, there’s no harm in having the step within the context as well and it makes the code a little simpler, so I’ve opted for that. However, you might ask yourself where best to put the context block in general. To answer that question, consider what information you want to acquire.

The information provided by a context block is uniform across the whole block. If you have additional set rules or counter updates within the block, you’ll not see the effects of those.

Thus, if you put your whole document into one big context block, you’ll be able to get the default values of Typst’s standard properties, but not much more. Sometimes that’s enough and then there’s no harm in putting everything into a big context block. [1]

On the other hand, sometimes you need to work with local information, like the current font size or counters that step often. Then, you probably want to keep your context closely scoped to your logic.


  1. Well, at least once Typst 0.12 is released. As of Typst 0.11.1, there are still a few bugs with contexts that wrap around pagebreaks creating empty pages. ↩︎

5 Likes