How to align the outline entries and fill. plus more

I made some light changes to the solution from hpcfzl. Mostly:

  • aligned the numbers of level 2+ to have the same alignment as level 1’s text/body. though in a hacky way by just adding an offset variable
  • added font size variable for level 1 heading
  • made it into a function
Code Put this into `functions.typ` for example:
#let docTableOfContents = {
  // Outline rules from @hpcfzl on Typst forum:
  // forum.typst.app/t/how-to-align-the-outline-entries-and-fill-plus-more/8753
  // Excludes headings level `excl`+
  let excl = 4
  set outline(depth: excl)
  
  /*
    This is unreliable for more than single-digit numbers, but
    would be in case `auto` was allowed in place of `1em`
  */
  // Indents only heading numbers level 2+, all the same
  // set outline(indent: n => if n == 0 { 0em } else { 1em })

  // Indentation is done in the `outline.entry` rule
  // set outline(indent: 0em)

  // Indentation is actually NOT needed for heading numbers level 2+
  // This would be equivalent to `set outline(indent: n => if n == 0 { auto } else { 0em })`
  show selector.or(..range(2, excl + 1).map(it => outline.entry.where(level: it))): set outline(indent: 0em)

  // Repeating separator
  let fill-gap = 0.5em
  let fill-gap-pad = fill-gap / 5
  set outline.entry(fill: repeat(gap: fill-gap, justify: false)[.])
  show repeat: set align(right)

  // Rule for outline entries
  show outline.entry: it => {
    let indented-gap = 1em
    let indented-gap-offset = 0.3em // to align numbers of level 2+ with body of level 1
    let text-weight = "bold"
    let text-size   = 14pt
    
    set text(weight: text-weight, size: text-size) if it.level == 1
    set block(above: 1.5em) if it.level == 1 // Could be made proportional to the `indented-gap`

    // Seizure warning
    let widest-heading = {
      let heading-locs = query(heading).map(h => h.location())
      (
        n: calc.max( 
          // could be simplified? : forum.typst.app/t/how-to-align-the-outline-entries-and-fill-plus-more/8753/11
          ..heading-locs.map(loc => counter(heading).at(loc)).map(it => it.map(str).join(".")).map(it => measure(it).width)
        ),
        p: measure(
          text(weight: text-weight, str(counter(page).at(heading-locs.last()).first()))
        ).width
      )
    }

    link(
      it.element.location(),
      it.indented(
        if it.level == 1 { it.prefix() },
        
        {
          if it.level > 1 {
            hide(
              text(
                weight: text-weight,
                // Wrong to use `counter(heading).display()`
                str(counter(heading).final().first()) + h(indented-gap + indented-gap-offset), 
              )
            )
            box(width: widest-heading.n + 1em, it.prefix()) // Pointless to add `h(indented-gap)`
          }
          it.body()
          h(fill-gap-pad)
          box(width: 1fr, if it.level > 1 { it.fill })
          h(fill-gap-pad)
          box(width: widest-heading.p + 2em, align(right, it.page()))
        },
        
        gap: indented-gap,
      )
    )
  }
  
  outline()
}

and call in main.typ like this:

#import "functions.typ": *
or: #import "functions.typ": docTableOfContents 
...
#docTableOfContents 
Output

vs my rewritten LaTeX template:

You can grab my rewritten LaTeX template here or look at the typst code here.
Same link as in the first reply, just adding it here again to have all in one place.


Is it ok to mark this as the solution? Technically it is correcter but @hpcfzl did 99% of the work. I do not want to overshadow that, even if their reply is marked in the code