headertypst

Typst: Header that changes from page to page based on state


I would like to include the current section in my header in typst. The documentation on "page" show that a header needs to be of type none, or content. If I insert something like #counter("heading"), the value as I define the header is 0, so the header stays 0 throughout.

I though I could define use show heading to create a function that updated the header when a new heading is made.

  show heading: it => {
    set text(blue.darken(40%))
    set page(header: grid(columns: (1fr, 3fr, 1fr), 
      align(left)[LeftHead], 
      align(center, title), 
      align(right)[Right Head]
    ))
    it
  }

I however got the following error:

error: page configuration is not allowed inside of containers
   ┌─ my_definitions.typ:81:4
   │  
81 │ ╭     set page(header: grid(columns: (1fr, 3fr, 1fr), 
82 │ │       align(left)[LeftHead], 
83 │ │       align(center, title), 
84 │ │       align(right)[Right Head]
85 │ │     ))
   │ ╰──────^

Is it possible to get a heading that updates?

See details above.


Solution

  • I am not yet proficient enough in Typst to explain the reasons behind the error you are getting (maybe it has to do with Typst functions being "pure" functions?)...

    However, for what it's worth, I figured out that you can have a different header for the first page (or any page number) by defining the content in a #locate function, as follows:

    #set page(
        header: locate(
            loc => if [#loc.page()] == [1] {
                [header first page]
            } else {
                [header other pages]
            }
        )
    )
    

    Now, following the same principle, you should be able to retrieve the body of the sections from within a #locate call, and use it directly in your header definition. To achieve that, I used the #find function to get the first and last heading of each page, and two variables to "remember" these for pages that do not contain headings.

    #let ht-first = state("page-first-section", [])
    #let ht-last = state("page-last-section", [])
    
    #set page(
        header: locate(
            loc => [
                // find first heading of level 1 on current page
                #let first-heading = query(
                    heading.where(level: 1), loc)
                    .find(h => h.location().page() == loc.page())
                // find last heading of level 1 on current page
                #let last-heading = query(
                    heading.where(level: 1), loc)
                    .rev()
                    .find(h => h.location().page() == loc.page())
                // test if the find function returned none (i.e. no headings on this page)
                #{
                    if not first-heading == none {
                        ht-first.update([
                            // change style here if update needed section per section
                            (#counter(heading).at(first-heading.location()).at(0)) #first-heading.body
                        ])
                        ht-last.update([
                            // change style here if update needed section per section
                            (#counter(heading).at(last-heading.location()).at(0)) #last-heading.body
                        ])
                    // if one or more headings on the page, use first heading
                    // change style here if update needed page per page
                    [#ht-first.display(), p. #loc.page()]
                } else {
                    // no headings on the page, use last heading from variable
                    // change style here if update needed page per page
                    [#ht-last.display(), p. #loc.page()]
                }}
            ]
        )
    )
    

    Now each page should have as a header: (section-number) Section-title , p. page-number.