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

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