How do I automatically label my headings, figures, equations?

With a large number of referenceable content, it may become bothersome to label and references elements manually. Automatically generating labels for content based on their body (headings), caption (figures) is an interesting approach but can also pollute your autocompletion field quickly.

Depending on your preferences, you may prefer manually labelling, having all content addressable, or some in-between solution.

The methods below are implemented for headings, but remain valid for figures. The only difference are the fields accessed. In the case of a figure, one may want to use the caption to generate the label text.

Approach 1: naive show rule (error)

#show heading: it => {
  let key = lower(it.body.text.replace(" ", "-"))
  [#it #label(key)]
}

= Long Heading with long title
See @long-heading-with-long-title.,

cf. Programmatically attaching labels to locatable elements #2926

As mentionned in the issue above, this simple approach results in an error: label <...> does not exist in the document.

As of 2024-09-17T00:00:00Z, it is still the case.

Approach 2: invisible figure workaround

Starting from the final result, you may obtain the following, where content is addressable even though no label is explictly defined.

= Test
@test

== Test 2 ah $x y z$ #rect(fill: black) test

@test-2-ah-x-y-z--test
Output

image

Complete code

You may simply copy the entire code below for your purposes.

Code
#let heading-numbering = "1.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 => {
  let key = lower(to-string(it).replace(" ", "-"))
  return [
    #it
    #v(-1em)
    #figure(
      kind: "heading",
      numbering: (..numbers) => numbering(heading-numbering, ..(counter(heading).get())),
      supplement: "Section",
    )[]
    #label(key)
  ]
}

Technical details

This approach relies on:

  1. show heading rule adding an invisible, referenceable figure
  2. a to-string conversion function for the heading.body

numbering

In order to reference heading, you need to define the numbering[1] scheme first. As an example, "1.1" is enough.

#let heading-numbering = "1.1"
#set heading(numbering: heading-numbering)

to-string

The content to string function comes from GitHub issue #3876[2].

#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 rule

In order:

  1. We generate the label key, replacing spaces by -
  2. Return the heading content by juxtaposing
    a. the original heading
    b. negative vertical space to remove the linebreak
    c. the figure used for labelling

Notice that the figure must folllow the heading-numbering scheme and the counter(heading).[3]

From this point, it is fairly simple to customize the generation rule for your own purposes, e.g., only generate level 1 heading labels, determine the heading supplement according to the level, etc.

#show heading: it => {
  let key = lower(to-string(it).replace(" ", "-"))
  return [
    #it
    #v(-1em)
    #figure(
      kind: "heading",
      numbering: (..numbers) => numbering(heading-numbering, ..(counter(heading).get())),
      supplement: "Section",
    )[]
    #label(key)
  ]
}

  1. Heading numbering ↩︎

  2. Turning content into string #3876
    ↩︎

  3. Introspection: counter ↩︎

3 Likes

Oh, neat feature. Here is a link about how date/time can be inserted: Insert Date / Time Question - #8 by david - Support - Discourse Meta. Now I can make my posts even better.

I can’t imagine wanting this over manually picking labels, but it’s interesting that it’s possible.

To be fair, I may or may not have too many headings … Also, most of the time, I end up writing the heading title, or the figure caption exactly as I did above, but manually. I just too the obvious next step, which was to automate it!

@quachpas Thank you so much!
I ran into the issue described in your first approach and your post was a god send.

To avoid duplicate labels (since I often have multiple headings with the same name in different chapters) I’ve expanded your approach 2 to generate hierarchical labels. As this tends to generate very long label keys, I’ve added the option to override the default key generation using the headings supplement field.

#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
Full Code
#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