How to reference headings with custom counters?

I would like to use custom counters to number different “types” of headings (for example “Schedule” and “Lab”). But I cannot figure out how to reference these headings.

I have used a previous question to get it working with the following code:

#let schedule-counter = counter("schedule")
#let lab-counter = counter("lab")

#let lab-header(title) = {
  lab-counter.step()
  context {
    heading(supplement: "Lab", [#lab-counter.display(). #title])
  }
}

#let schedule-header(title) = {
  schedule-counter.step()
  context {
    heading(supplement: "Schedule", [Schedule #schedule-counter.display(): #title])
  }
}

#outline()

#schedule-header([My First Schedule]) <schedule:a>
#schedule-header([My Second Schedule]) <schedule:b>
#schedule-header([My Third Schedule]) <schedule:c>
#lab-header([My First Lab]) <lab:a>
#lab-header([My Second Lab]) <lab:b>
#lab-header([My Third Lab]) <lab:c>

It is working perfectly and everything is display exactly how I want it - the headings themselves as well as how they appear in the outline:

Contents
Schedule 1: My First Schedule ....... ⁠1
Schedule 2: My Second Schedule ...... ⁠1
Schedule 3: My Third Schedule ....... ⁠1
1. My First Lab ..................... ⁠1
2. My Second Lab .................... ⁠1
3. My Third Lab ..................... ⁠1


Schedule 1: My First Schedule
Schedule 2: My Second Schedule
Schedule 3: My Third Schedule
1. My First Lab
2. My Second Lab
3. My Third Lab

But how can I now reference them?

Testing @schedule:a and @schedule:b and @schedule:c and @lab:a and @lab:b and @lab:c.

I get “cannot reference sequence” errors. I have already added the supplement in the heading in anticipation.

If I try to create a label near where heading is called, I get “cannot join content with label”. If possible, I would like to keep #schedule-header([My First Schedule]) <schedule:a> syntax of defining headings and labels.

The reason your current implementation errors is because the labels get attached to context blocks, which aren’t referencable. One way to fix this is to pass a label as a function parameter, but you said you did not like this.

If you want to keep the syntax of adding the label after the function call, you can probably adapt one of the many theorem environment packages to your needs. For example:

#import "@preview/theorion:0.6.0": *
#show: show-theorion

#let (schedule-counter, schedule-box, schedule, show-schedule) = make-frame(
  "schedule",
  "Schedule",
  render: (prefix: none, title: "", full-title: auto, body) => {
    if title == "" and body != "" {
      // change empty title to empty body
      title = body
      body = ""
    }
    strong(text(size: 15.5pt)[
      #prefix:  #title
    ])
    if body != "" {
      body
    }
  },
)

#let (lab-counter, lab-box, lab, show-lab) = make-frame(
  "lab",
  "Lab",
  render: (prefix: none, title: "", full-title: auto, body) => {
    if title == "" and body != "" {
      // change empty title to empty body
      title = body
      body = ""
    }
    strong(text(size: 15.5pt)[
      #prefix.  #title
    ])
    if body != "" {
      body
    }
  },
)

#show: show-schedule
#show: show-lab

#outline(target: figure)

#let make-schedule(title) = schedule(title, "")
#let make-lab(title) = lab(
  prefix: context (lab-counter.display)(),
  title: title,
  ""
)


#make-schedule("My First Schedule")<schedule:a>
#make-lab("My First Lab")<lab:a>

@schedule:a
@lab:a

Hi, thank you for the detailed response. It works great. However, I may now reconsider passing the label as a function parameter. I am still a Typst beginner, so it would be easier to maintain.

1 Like