How to hierarchically label headings automatically?

The topic about auto labeling got me thinking, whether it would be possible to generate hierarchical labels.
For example: if my heading 1. has the label , then all sub headings labels should also start with the prefix heading-1.

The following code is the closest I’ve gotten so far.
Does anyone have an idea on how to improve this?
Preferably I’d like to avoid having to specify the label prefix and read it directly from the previous heading.

#let heading-numbering = "1."
#set heading(numbering: heading-numbering)

#let labeled_heading(body, prefix: none) = {
  show heading: this => {
    let key = lower(this.body.text.replace(" ", "-"))
    if prefix != none {
      key = prefix + "-" + key
    }
    return [
      #this
      #v(-1em)
      #figure(
        kind: "heading",
        numbering: (..numbers) => numbering(heading-numbering, ..(counter(heading).get())),
        supplement: "Section",
      )[]
      #label(key)
    ]
  }
  body
}

#labeled_heading(prefix: none)[
  #let heading_level = 1
  #heading([Heading 1], level: heading_level)
  Heading 1 body

  #labeled_heading(prefix: "heading-1")[
    #let heading_level = 2
    #heading([Heading 1.1], level: heading_level)
    Heading 1.1 body

    #labeled_heading(prefix: "heading-1-heading-1.1")[
      #let heading_level = 3
      #heading([Heading 1.1.1], level: heading_level)
      Heading 1.1.1 body

      #heading([Heading 1.1.2], level: heading_level)
      Heading 1.1.2 body
    ]
  ]

  #labeled_heading(prefix: "heading-1")[
    #let heading_level = 2
    #heading([Heading 1.2], level: heading_level)
    Heading 1.2 body
  ]

  #heading([Heading 2], level: heading_level)
  Heading 2 body
]

Reference to heading 1: @heading-1\
Reference to heading 1.1: @heading-1-heading-1.1\
Reference to heading 1.1.1: @heading-1-heading-1.1-heading-1.1.1\
Reference to heading 1.1.2: @heading-1-heading-1.1-heading-1.1.2\
Reference to heading 1.2: @heading-1-heading-1.2\
Reference to heading 2: @heading-2

You can query the previous heading to do that!

#let heading-numbering = "1."
#set heading(numbering: heading-numbering)
#let to-string(content) = {
  if content.has("text") and type(content.text) == "string" {
    content.text
  } else if content.has("children") {
    content.children.map(to-string).join("")
  } else if content.has("body") {
    to-string(content.body)
  } else if content == [ ] {
    " "
  }
}
#show heading: it => context {
  let f = it => lower(to-string(it).replace(" ", "-"))
  let prev = query(selector(heading).before(here()))
  if prev.len() > 1 {
    prev = prev.at(-2)
  } else {
    prev = [ ]
  }
  let prev-key = if prev != [ ] {f(prev) + "-"} else {""}
  let key = prev-key + f(it)
  return [
    #it
    #v(-1em)
    #figure(
      kind: "heading",
      numbering: (..numbers) => numbering(heading-numbering, ..(counter(heading).get())),
      supplement: "Section",
    )[]
    #label(key)
  ]
}

= Heading 1

@heading-1

== Heading 1.1

@heading-1-heading-1.1

=== Heading 1.1.1

@heading-1.1-heading-1.1.1

=== Heading 1.1.2

@heading-1.1.1-heading-1.1.2

== Heading 1.2

@heading-1.1.2-heading-1.2

= Heading 2

@heading-1.2-heading-2
1 Like

Hey @Simon_Schneider! I’ve changed your question post’s title to better fit our guidelines: How to post in the Questions category

For future posts, please make sure your title is a question you’d ask to a friend about Typst. :wink:

Expansion to the original solution that allows for maximum hierarchical depth and label key override using the heading supplement field:

#let heading-numbering = "1."
#set heading(numbering: heading-numbering)

#let to-string(content) = {
  if content.has("text") and type(content.text) == "string" {
    content.text
  } else if content.has("children") {
    content.children.map(to-string).join("")
  } else if content.has("body") {
    to-string(content.body)
  } else if content == [ ] {
    " "
  }
}

#show heading: it => context {
  // give users the option to disable auto labeling
  if it.supplement == [-] {
    return it
  }
  let get_label_key = it => {
    if it.supplement == [] {
      // if no supplement is provided, use the heading text to generate the label
      return lower(to-string(it).trim().replace(" ", "-"))
    } else {
      // if a supplement is provided, use the supplement to generate the label
      return lower(to-string(it.supplement).trim().replace(" ", "-"))
    }
  }

  // recursively construct hierarchical label key
  let prev = query(selector(heading).before(here())).rev()
  let key = ""
  let level = it.level
  for heading in prev {
    if level == 1 {
      break
    }
    if heading.level >= level {
      continue
    }
    level -= 1
    key = get_label_key(heading) + "-" + key
  }
  key = key + get_label_key(it)

  // return the heading with the auto-generated label
  return [
    #it
    // unfortunately there is an issue with attaching programmatically attaching labels to headings (see https://github.com/typst/typst/issues/2926)
    // therefore we have to use a fake figure to attach the label to the heading
    #v(-1em)
    #figure(
      kind: "heading",
      numbering: (..numbers) => numbering(heading-numbering, ..(counter(heading).get())),
      supplement: "Section",
    )[]
    #label(key)
    //#key // print the label key for debugging
  ]
}

#set heading(numbering: (..numbers) => numbering((..nums) => nums.pos().map(str).join("."), ..numbers), supplement: [])



#heading(supplement: [h1])[Section 1]
@h1

== Section 1.1
@h1-section-1.1

#heading(supplement: [-], level: 2)[Section 1.2]
Auto labeling has been disabled for this heading using the supplement field value `[-]`.

== Section 1.3
@h1-section-1.3

=== Section 1.3.1
@h1-section-1.3-section-1.3.1

=== Section 1.3.2
@h1-section-1.3-section-1.3.2

== Section 1.4
@h1-section-1.4

= Section 2
@section-2
1 Like