How to align the outline entries and fill. plus more

Something similar could likely be available in existing packages, especially those originating from a particular organisation. However considering that it’s your own, the chances of that are lower. Or so I understood.

About the code you have provided:

  • Seems like you’re using this as part of a template. Not that your use is wrong, but usually, you would have one template function which you would then show after importing, see Making a Template – Typst Documentation.

  • Something like set outline(...) can contain more than one argument. I suppose it’s reasonable that you separated them because you’re commenting what the purpose is.

  • Using em for spacing is more recommended I would say, but I decided not to change that.


From what I can tell, the outline.entry function is what I want, but I honestly don’t get how it operates from the examples in the Typst docs.

This is what you should follow: Outline Function – Typst Documentation.


I took the time to follow through your outline.

Notable features:

  • Outline entries from prelude pages using roman numerals and unnumbered headings are properly distinguished
  • Right-aligned dotted lines, which are simpler to achieve than aligned. Not to undermine @ensko’s suggestion, but I think it’s less relevant since you specified this. Mentioned also by @Eric in How to remake this outline in Typst? - #7 by Eric
  • Automated left and right tab-like spacing

These are the implemented horizontal spacings, each of which applies the same for all outline entries of the second level, mimicking Typst’s initial outline:

  • Helper spacings:
    • fill-gap controls the spacing between the repeat separators:
      • You can change the recommended 1em
    • fill-gap-pad ensures that the repeat separator on either end doesn’t interfere with the heading nor the page number:
      • Automatically based on fill-gap
  • Left indent: [1]
    • Automatically based on the widest-numbered level 1 outline entry
    • This could have simply been auto, see the code why it’s not
  • Left indented.gap: [1:1]
    • Automatically based on the widest-numbered level 2+ outline entry [2]
    • Comparable to what indented.gap represents would be widest-level-2 - curr-level-2 + 1em
    • You can change the recommended 1em
  • Right page number gap:
    • Automatically based on the last heading’s page number, even level 1’s, assuming that it’s the widest-numbered
    • The actual gap is last-page-number-width - curr-page-number-width + fill-gap-pad + 1em
    • You can change the recommended 1em

There’s also indented-gap which is indented.gap for the level 1 outline entries and can be changed. These also have a strong emphasis which can be changed through text-weight.

Code
// Outline rules from @hpcfzl on Typst forum
#{
  // 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 = 1em
  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 = 0.5em
    let text-weight = "bold"
    set text(weight: text-weight) if it.level == 1
    set block(above: 24pt) 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(
          ..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,
                str(counter(heading).final().first()) + h(indented-gap), // Wrong to use `counter(heading).display()`
              )
            )
            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 + 1em, align(right, it.page()))
        },
        
        gap: indented-gap,
      )
    )
  }

  outline()
}
Output

I believe my approach addresses the few conundrums, but feel free to allude to any further observations.


  1. Mind that these hyperlinked values aren’t the ones actually used. ↩︎ ↩︎

  2. This is something to be aware of, as there can be a level excl heading which is the widest but invisible to the outline. Filtering the query is all that’s needed, I just didn’t think this would be worth the additional hassle. ↩︎

3 Likes