How to detect local heading level in separate context blocks?

I would like to create templates that add headings along with some other content to the document. It would be nice if I would not have to specify the heading level repeatedly and just have the template detect it itself.
For this I have created a function that uses context to determine the heading level of the most recent heading level.

#let get_local_heading_level(increment: 0) = {
  let headings = query(selector(heading).before(here()))
  if headings.len() > 0 {
    let most_recent_heading = headings.last()
    return headings.last().level + increment
  }
  return 1 + increment
}

This works well, if all the calls are within one context block. Unfortunately, if the calls to the function are in separate context block it works for three headings and then suddenly starts applying wrong heading levels. E.g heading 2.5 and 2.6 are shown as heading 4.

#set heading(numbering: "1.")

// this works
#heading([Heading 1], level: 1)
#heading([Heading 1.1], level: 2)
#context [
  #heading([Heading 1.2], level: get_local_heading_level())
  #heading([Heading 1.3], level: get_local_heading_level())
  #heading([Heading 1.4], level: get_local_heading_level())
  #heading([Heading 1.5], level: get_local_heading_level())
  #heading([Heading 1.6], level: get_local_heading_level())
]

// this doesn't work
#heading([Heading 2], level: 1)
#heading([Heading 2.1], level: 2)
#context [ #heading([Heading 2.2], level: get_local_heading_level())]
#context [ #heading([Heading 2.3], level: get_local_heading_level())]
#context [ #heading([Heading 2.4], level: get_local_heading_level())]
#context [ #heading([Heading 2.5], level: get_local_heading_level())] // generates: 4. Heading 2.5
#context [ #heading([Heading 2.6], level: get_local_heading_level())] // generates: 4. Heading 2.6

Does anyone have an idea what the issue with the second example is?

Using the single context example is a fallback option, but since I am trying to create templates for my coworkers, I’d like to keep them as simple to use as possible.

Hi @Simon_Schneider,

your attempt is a bit problematic. The heading levels are wrong because the compiler is not able to compile your document and show the warning “Layout did not converge”.
image

This happens because your contextual get_local_heading_level calls depend on the previous ones. Essentially, your document compiles once, then a second time to resolve the big context block, then the second context call for Heading 2.2 depends on the first, and so on.
Your example works up to Heading 2.4 because Typst allows up to 5 iterations. I hope my explanation makes sense, for more information see the documentation about context, context iterations and feel free to ask follow up questions.

Here is my attempt to implement a working version using metadata.

#set heading(numbering: "1.a")

#let auto-heading(text) = [#metadata(text) <auto-heading>]
#show <auto-heading>: meta => {
  let prev-heading = query(selector(heading).before(meta.location())).last()
  heading(level: prev-heading.level, meta.value) 
}

= Heading 1

#lorem(10)

#auto-heading[Heading 2]

#lorem(10)

== Heading 2.1

#lorem(10)

#auto-heading[Heading 2.2]

#lorem(10)

= Heading 3
#auto-heading[Heading 4]
Output

test

Hi @flokl
Thank you for your explanation, it makes perfect sense.
Your example works perfectly, although I don’t really understand, how the show rule avoids the context iteration issue.
Would you mind expanding on that?

1 Like

One of the last steps of an iteration is the evaluation of the show rule. This means in this example first all auto-headings are replaced top to bottom with metadata and then the show rule gets evaluated top to bottom. At the point the show rule runs, Typst already knows the location/number of all the headings and can easily query the one before the current location.

That’s how I understand it, but I can’t guarantee that it’s 100% correct.

1 Like