Label `<B>` occurs multiple times in the document when including #outline

I have numerous JSON files that I’m loading and processing with a function. Each JSON file gets its own heading and label, and some files will refer to others. My minimum reproducible example has two files.

a.json

{
    "title": "A",
    "refId": "B"
}

b.json

{
    "title": "B"
}

Then, my Typst file (pared down from the full project):

#set page(numbering: "1")

//#outline()

#let get-ref(ref-id) = [
  #context {
    let loc = locate(label(ref-id))
    let pn = loc.page-numbering()
    let nums = counter(page).at(loc).at(0)

    let a-text = numbering(pn, nums)
    link(loc, strong(ref-id) + [_(page _] + emph(a-text) + emph[)])
  }
]

#let show-item(..args) = [
  #let pos = args.pos();
  #let named = args.named();

  #let name = named.at("title")
  === #name #label(name)
  #let refId = named.at("refId", default: none)
  #if refId != none [
    See also  #get-ref(str(refId))
  ]
]

#show-item(..json("a.json")) // has "refId": "B"
#show-item(..json("b.json")) // has no refId

If I run it like this, it works. If I uncomment this line:

#outline()

Then I get this error:

 error: label `<B>` occurs multiple times in the document
  ┌─ \\path\to\main.typ:7:14
  │
7 │     let loc = locate(label(ref-id))
  │               ^^^^^^^^^^^^^^^^^^^^^

This certainly seems like a bug, but maybe I’m misunderstanding how labels should work?

This places the label inside the heading’s body (because you’re using the label function, not label syntax), and that should explain why you get duplicate labels: the outline also reproduces all heading titles (which now include labelled words).

It’s a bit awkward, but this is a way to attach the label to the heading while still using typst markup for the heading:

// in typst code mode, create heading with label
[#[=== #name]#label(name)]

I think that should hopefully explain what you see and give a way to resolve it.

Uuuuh, what? That doesn’t feel like an expected or wanted behavior. There is little to no difference for using either form for different elements, so this is an outlier. A change request probably, so I guess a feature request.

An oversight? If anything, I’d think that <label> is also “a part of heading’s body.” Though from “attachment” perspective it’s definitely a separate thing.

If I do #panic([#label("label")].func()), then it acts exactly like <label> and results in empty sequence.

It’s well described here Label Type – Typst Documentation so it’s at least well known.

Hmm, so it’s also kinda like the figure[] + label("a") issue in code mode. Rather an annoying edge case for scripting and automation.

Well, at least = #label("a") throws a warning. That’s something too. It’s also surprising to find such a niche example explicitly present in the docs.