How to evaluate page header at the start of the page?

I’ve been trying to create a template for homework (math problems in particular, but could easily be expanded) which has an “exercise” function, and which shows the current (last as of the current page) exercise in the page’s header. The way I chose for doing this is to create an “exercises” state (which I’m not a fan of, as it exists outside of the main template function), update it with every call of the “exercise” function, and display the “last” one in the header.

The problem is, I’ve found out the header’s content seems to “evaluate” before any of the page itself does. In contrast, the footer seems to update after. So the template works as intended - always one page behind.

Consider the following example:

#let arr = state("arr", ())

#set page(
  header: [
    arr:
    #context arr.get()
    #line(length: 100%)
  ],
  footer: [
    arr:
    #context arr.get()
    #line(length: 100%)
  ]
)

#arr.update("Hey!")

Which renders as

while I want the header to look like the footer.

Anyone has any idea on how to solve this? Or alternatively, a better way to do what I want? Initially I tried using labels, but working with content is a nightmare. I do not know how the rendering in Typst actually works, so this problem may be deeper than it seems.

Edit: Just had a bit of time off and could restructure this post a bit.

Hello @ykremer , welcome to the forum,

What you are experiencing has been commented on before:

In your particular case, it is not zero, it is none.

Knowing that, there are a few solutions available for you, including but not limited to:

Using Hydra Package

With Typst Headings

Hydra is designed specifically for that purpose. You can use any heading level you want and it is highly configurable.

This is a working example showing the last defined exercise (here we are using level 1 headings) in the header:

#import "@preview/hydra:0.6.2": hydra
#set page(
  header: context {
    hydra(use-last: true, skip-starting: false)
    line(length: 100%)
  },
)
= Exercise 1
= Exercise 2
= Exercise 3

With Custom Element

If you don’t want to use the heading elements, you can define some custom ones for your needs. The key is to define a way for Hydra to display the custom element.

hydra is built with custom elements in mind. Some documents may use other elements for chapters or section-like content. hydra allows defining its own selectors for tight control over how elements are queried.

See Hydra’s documentation for more information.

#import "@preview/hydra:0.6.2": hydra

#let exercise(body, label: none) = {
  show figure: set align(left)
  set figure(numbering: "1")
 
  [#figure(kind: "exercise", supplement: [Exercise], caption: body, none)
    #label]
}

#set page(
  paper: "a5",
  header: context {
    hydra(
      figure.where(kind: "exercise"),
      display: (.., it) => it.caption,
      use-last: true,
      skip-starting: false,
    )
    line(length: 100%)
  },
)
#for n in range(1, 4) {
  exercise([#lorem(1)], label: <n>)
}

Without any Packages

Another Solution using Query

This is adapted from How do I count elements on a page so the result isn't always zero in the header? - #7 by janekfleper and uses query instead of state. No packages are needed.

#let exercise(body, label: none) = {
  show figure: set align(left)
  set figure(numbering: "1")

  [#figure(kind: "exercise", supplement: [Exercise], caption: body, none)
    #label]
}

// Find the last exercise on the current page
#let last-exercise() = {
  let ex = query(figure.where(kind: "exercise"))
  if ex.len() > 0 {
    ex.filter(it => it.location().page() == here().page()).last().caption
  }
}

#set page(
  header: context {
    last-exercise()
    line(length: 100%)
  },
)

#for n in range(1, 4) {
  exercise([#lorem(1)], label: <n>)
}

3 Likes

this is an incredible reply. thank you! I will implement one of your suggestions.

1 Like

Hmm. I forgot there’s more data I wanted to attach to each exercise. I settled on the exercises being content with a label, and metadata which is attached to it which is… a dictionary as a str, later evaluated to its “contents” using eval.

In addition to Hydra, it works. Pretty much. That’d be a hybrid of your suggestions and the cursed use of eval.

To that add a correct use of header setting (finally!) , so there won’t be a header in the title page, while the rest of the document won’t act up.

Typst is so powerful and I love it, but man, it’s just so overly complicated sometimes, and I haven’t tried studying the Rust API…

Feel free to open another question then, with your current layout and expectations and we will be happy to help.

1 Like