How to add an invisible Heading to an outline?

By using some of the same optimizations from How to mutate variables in a show rule? - #2 by Andrew, here is my slightly improved version:

#let make-outline-entry(label, body) = {
  let filler = box(width: 1fr, repeat[.])
  let content = body + " " + filler + counter(page).display()
  block(link(label, strong(content)))
}

#show outline.entry: it => {
  let label = <appendix>
  let is-set = state("is-set", false)
  context if not is-set.get() {
    let found = query(selector(label).before(it.element.location()))
    let is-after-appendix = found.len() > 0
    if is-after-appendix {
      make-outline-entry(<appendix>)[Appendix]
      is-set.update(true)
    }
  }
  it
}
The rest
#outline()

= H1
#lorem(25)
= H2
#lorem(25)

<appendix>
= A1 (Appendix starts here)
#lorem(25)
= A2

I made it more readable/maintainable. Or you can slap everything together to make the code smaller:

#show outline.entry: it => {
  let label = <appendix>
  let is-set = state("is-set", false)
  context if not is-set.get() {
    let found = query(selector(label).before(it.element.location()))
    if found.len() > 0 {
      block(link(label, strong({
        "Appendix "
        // box(width: 1fr, repeat[.])
        box(width: 1fr, it.fill)
        counter(page).display()
      })))
      is-set.update(true)
    }
  }
  it
}

Some things (e.g., using "" instead of [] for the name) are pure preference, but most things are the recommended way of writing Typst code (or code in general):

  • using descriptive variable/function names;
  • using state() over counter() if you don’t need to count natural numbers;
  • btw, counter() is initially set to 0, so you don’t have to explicitly set it initially unless you need to reset it at some point;
  • using query(selector(pivot).before(location)).len() > 0 over manually comparing pages/y position;
    • there is also .after();
    • sometimes you need to use things like <element>.where() which is already a selector;
  • the recommended variable/function naming convention is to use kebab case (which is typically easier to type).

A little bit less “you should do it” things:

  • adding aliases for things that need to be used multiple times (i.e., label) to reduce chance of a “desync value” bug (and similar stuff);
  • making context scope as small as possible.
2 Likes