Styling of headings and outlines to match a LaTeX Rulebook Project

Good evening. I’m currently trying to convince our rules committee to switch from LaTeX to Typst (using Pandoc as the foundation), and therefore I need to reproduce parts of the existing rulebook in Typst.

Unfortunately, I’m already struggling with the interaction between the outline (table of contents) and the headings.

First, here are the relevant examples from the existing rulebook. In the ToC, only level-1 headings appear (primarily chapters and rules). Each chapter has a front page with the chapter number and title. The actual rules have a two-line layout (first line “Rule x”, second line the description of the rule). In the ToC, however, only the rule number and the rule description (the second line) should appear. Images below illustrate this.

ToC:

Chapter Page:

Rule Example:

Here is what I’ve tried so far:

#set text(
  font: "Roboto Serif",
  lang: "de"
)

// #set heading(numbering: "1.1")
#set outline(depth: 1)
#show outline: it => {
  show heading: set text(size: 1.5em)
  it
}

#show outline.entry: it => {
  link(
    it.location(),
    it.indented(it.prefix(), it.inner())
  )
}


#let rule-header(rule, subline) = {
  text(size: 1.5em)[ 
      #pagebreak()
      #heading(level: 1, outlined: false, rule)
      #v(-0.75em)
      #heading(level: 1, subline)
      #v(2em)
    ]
}


#outline()


#rule-header(
  "Regel 1",
  "Das Spiel, der Platz, der Ball, die Spieler, die Ausrüstung"
)

#heading(level: 2, numbering: "1.1")[Allgemeine Bedingungen]
#heading(level: 3, numbering: "1.1")[Das Spiel]

#lorem(400)


#rule-header(
  "Regel 2",
  "Definitionen"
)

#lorem(400)


#rule-header(
  "Regel 3",
  "Perioden, Zeitfaktoren, Ersatzspieler"
)
#lorem(400)

I’m very much looking forward to your input. :slight_smile:

I believe the prevailing aspect of this rulebook is numbering, since it’s fairly diverse. So the images you have provided especially helped.

The following changes only the styling, which is usually preferred, so no wrapper function is necessary for the use of headings or is hidden away from the user:

#set text(font: "Roboto", lang: "de")
#set par(justify: true)

// Numbering

#set enum(
  indent: 1em,
  full: true,
  numbering: (..n) => numbering(
    if n.pos().len() > 1 { "1." }
    else { "a)" },
    n.pos().last(),
  ),
)

// Headings

#set heading(numbering: (..n) => numbering(
  ..if n.pos().len() == 1 { ("I", n.pos().first()) }
  else { ("1.1", ..n.pos().slice(1)) }
))

#show heading.where(level: 1): set text(1.8em)
#show heading.where(level: 1): it => [
  #pagebreak(weak: true)
  #show block: set align(center + horizon)
  #block(below: 1.8em)[
    Teil #counter(heading).display()
    #v(0.25em)
    #linebreak()
    #it.body
  ]
  #pagebreak(weak: true)
]

#show heading.where(level: 2): set text(1.6em)
#show heading.where(level: 2): it => block(below: 1.6em)[
  Regel #counter(heading).display()
  #linebreak()
  #it.body
]

#show heading.where(level: 3): set text(1.4em)
#show heading.where(level: 3): it => block(below: 1.4em)[
  Abschnitt #counter(heading).display()
  #it.body
]

#show heading.where(level: 4): set text(1.2em)
#show heading.where(level: 4): it => block(below: 1.2em)[
  Artikel #counter(heading).display()
  #it.body
]

// Outline

#set outline(depth: 2)

#show outline: it => {
  set heading(level: 2)
  show heading: reset => block(reset.body)
  it
}

#show outline.entry: it => {
  let spacing = 2em - measure(it.prefix()).width
  if it.prefix().text.split("").any(it => it in ("I", "V", "X")) {
    link(it.location())[
      #v(1em)
      *#it.prefix() #h(spacing) #it.body() #h(1fr) #it.page() #linebreak()*
    ]
  } else {
    link(it.location())[
      #it.prefix() #h(spacing) #it.inner() #linebreak()
    ]
  }
}

// Rulebook

#outline()

= Die Regein
== Das Spiel, der Platz, der Ball, die Spieler, die Ausrüstung
=== Allgemeine Bedingungen
==== Das Spiel

+ #lorem(50)
+ #lorem(30)
  + #lorem(30)
  + #lorem(30)
+ #lorem(50)
+ #lorem(50)

#lorem(100)

== Definitionen

#lorem(100)

== Perioden, Zeitfaktoren, Ersatzspieler
#lorem(100)

= Chap
== Rule
=== Desc

#([= Chap] * 10)
Output

Output

Output

There’s quite a lot of refining possible:

  • Outline:
    • I omitted that first part Regeländerungen ..., since I was uncertain about its purpose.
    • Own page isn’t reserved for the outline, because that will probably done by the front one or the chapter. But you can add pagebreaks into outline’s show rule as you wish.
    • The basis for applying the strong style without dots to the chapter entries of the outline could be better. At the moment, it’s applied whenever one of the few Roman numerals is found in the numbering, assuming chapters are the only headings using them.
    • The dots aren’t aligned. I believe the only post on this so far comes from Reddit, but I leave trying out that answer up to you.
    • Because I can’t see how the rule numbering should continue throughout your chapters, I left them to reset in each:
      I    II    III         I    II    III
      1 -> 1  -> 1     not   1 -> 3  -> 5
      2    2     2           2    4     6
      
  • Unrelated to the posed question:
    • The enum numbering so far requires a hacky solution, as just "a)1." would become a. 1., not a) 1. that I initially assumed.
    • While there are ways of simplifying the headings’ show rules into a single one, I for example intentionally separated out their set text() so that it’s visible to the outline’s show rule.

Should you have further questions, feel free to ask.

1 Like

Thank you so much for the very fast response. As I am new to Typst, this is a lot of input. I will dig through it this evening and will be back with further questions for sure.

But it looks great for the moment. :slight_smile:

I didn’t check my old code from that post for the concrete differences, but in my own template I now use the following code, so I’d base new work off that: typst-diploma-thesis/src/outline.typ at main · TGM-HIT/typst-diploma-thesis · GitHub

Applying that then looks like this:

show outline.entry: align-fill()
set outline.entry(fill: repeat(gap: 6pt, justify: false)[.])
1 Like