How can I create a outline with custom heading numbering?

Hey there :wave:

I’m a newcomer to Typst and wanted to ask how to create such a outline with “custom numbering”:

My layout is as following:

= Grundprinzipien

== Staatsaufbau und -form

== Judikativ-Recht
=== Gewaltenteilung
=== Gewaltentrennung

== Rechtsstaatlichkeit und Gleichheit vor dem Gesetz
...

So Level 1 is Roman, Level 2 is “Art. [Arabic]” and Level 3 is the letter.
I also tried packages like outrageous but it doesn’t seem to meet my criteria…

Hi there! This is relatively easy to achieve with the new outline styling capabilities of v0.13 so I used those already for you even if it is not officially released yet as of 2025-02-05T23:00:00Z.

I also used numbly to quickly insert the appropriate numbering but you can do that without it as well, their documentation shows that it is just a shorthand for a slightly longer numbering function. What’s a bit more tricky is the requirement to have the counter for the articles ("Art.") not reset after each level 1 heading; there I used a custom counter.

This is the code I came up with:

#import "@preview/numbly:0.1.0": numbly

#let article-counter = counter("article")
#set text(lang: "de", font: "DM Sans")
#set heading(numbering: numbly("{1:I.}", "Art. {2}", "{3:a}"))

#set outline(indent: 0em)
#set outline.entry(fill: none)

// Make it bold and have more space above it.
#show outline.entry.where(level: 1): set block(above: 2em)
#show outline.entry.where(level: 1): it => link(it.element.location(), strong(it.indented(it.prefix(), it.body())))

#show heading.where(level: 2): it => block({
  article-counter.step() 
  context "Art. " + article-counter.display() + h(0.3em, weak: true) + it.body
})

// Split entry into prefix "Art." or equivalent empty space, its number and its body.
#show outline.entry.where(level: 2).or(outline.entry.where(level: 3)): entry => {
  let (art-prefix, number) = if entry.level == 2 {
    numbering(
      entry.element.numbering, 1, 
      ..article-counter.at(entry.element.location()).map(x => x + 1)
    ).split(" ")
  } else {
    (hide("Art."), entry.prefix())  
  }

  link(
    entry.element.location(),
    box(grid(columns: (auto, 1em, auto), column-gutter: 1.25em, align: (left, center, left),
      art-prefix, number, entry.inner(),
    ))
  )
}

See result.

1 Like

I just have a small thing to add. For the show rule of the level 2 headings you should use block(sticky: true){} to correctly replicate the default heading behavior.
I was wrong about this one, see the reply by Eric for the explanation.

That should not be necessary, as Typst internally already applies the show-set rule

#show heading: set block(sticky: true)

to all headings, so adding the block in the show rule should be enough. There is an open issue about that, though I don’t think there is anything wrong.

1 Like