How to display outline using grid?

My university has a strict guide of style and layout for academic documents. I’m making a template for it and one of the styles I could not yet reproduce is the one for the outline.

The rules state that (in summary):

  • Each heading level has to reflect the corresponding styling from the text.
  • The numbering and page numbers have to be laid out in columns (so that they occupy the same width).
  • The filling has to start and end in the same line as the last letter of each title.
  • Front matter headings must not be present. Back matter headings must not display a numbering.

Here is the sample they provide from their style guide:

For the columnar layout I figured I have to use a custom show outline rule, but from there I didn’t find a way yet to get the data for the headings. I know I could query for them, but first I wanted to ask you if you know an easier way to get that info.

Using show outline.entry does not seem to cut it because I can’t guarantee the same-width rule for the numbering columns.

How could I solve this laying-out problem? Thank you in advance.

There are likely many different ways to achieve something like that.

One option would be to use a show rule on outline.entry like you suggested, and using a state to keep track of the widths for the number and page columns. The downside here is that you would need to use different states if you have multiple outlines (e.g. for a list of figures), or else they would all have the same column widths. This is of course possible by using counters to count the outlines and then using the corresponding counter value in the name of the state, but it would still introduce quite a bit of complexity.

Code Example
#let outline-state = state("outline", (number-width: 0pt, page-width: 0pt))
#show outline.entry: it => context {
  let body = if it.element.func() == heading { it.element.body }
             else { it.element.caption.body }
             
  let number = if it.element.numbering != none {
    let counter = if it.element.has("counter") { it.element.counter }
                  else { counter(heading) }
    numbering(
      it.element.numbering,
      ..counter.at(it.element.location())
    )
  }

  let page-numbering = it.element.location().page-numbering()
  if page-numbering == none { page-numbering = "1" }
  let page = numbering(
    page-numbering,
    ..counter(page).at(it.element.location())
  )

  // Update state with current number/page widths.
  let number-width = measure(number).width
  let page-width = measure(page).width
  outline-state.update(s => (
    number-width: calc.max(number-width, s.number-width),
    page-width: calc.max(page-width, s.page-width)
  ))

  // Get final width values and construct outline entry.
  let (number-width, page-width) = outline-state.final()
  box(grid(
    column-gutter: 0.5em,
    columns: (number-width, 1fr, page-width),
    align: (start, start, end + bottom),
    number, body + [ ] + box(width: 1fr, it.fill), page
  ))
}

Another shorter option would be to use a show rule on outline. There, you can then query the elements manually and directly lay them out in a grid with three columns. The downside here is that you can’t nicely reconstruct the automatically translated outline title.

Code Example
#show outline: it => {
  let elements = query(it.target)
  let has-number = elements.any(el => el.numbering != none)

  // Can't use the automatically translated title here.
  heading(if it.title == auto [ Contents ] else { it.title })
  
  grid(
    column-gutter: 0.5em,
    align: (start, start, end + bottom),
    row-gutter: par.leading,
    columns: (auto, 1fr, auto),
    ..elements.map(el => {
      let body = if el.func() == heading { el.body } else { el.caption.body }
      
      let num = if el.numbering != none {
        let counter = if el.has("counter") { el.counter } else { counter(heading) }
        numbering(el.numbering, ..counter.at(el.location()))
      }

      let page-numbering = el.location().page-numbering()
      if page-numbering == none { page-numbering = "1" }
      let page = numbering(page-numbering, ..counter(page).at(el.location()))

      (num, body + [ ] + box(width: 1fr, it.fill), page)
    }).flatten()
  )
}
2 Likes

Hey @ArthurAraruna, welcome to the forum! I’ve updated your post title to better fit our guidelines for question posts: How to post in the Questions category

Make sure your post title is a question you’d ask to a friend about Typst. :wink:

1 Like