How to dynamically label headers in outlines with a counter

I want to write down question and want a function which provides some nice formatting for easily adding new questions to a document. One of the this I want it for the questions to be auto-numbered, I have got that working with a counter but it seems the outline isn’t aware of the numbers using this method.

#let question-counter = counter("question")

#let question() = {
    question-counter.step()
    let question-number = context question-counter.display()
    [= Question #question-number]
}

#outline()

#question()

#question()

#question()

The outline just renders every question title as Question 0

Any suggestions for alternative approaches?

Hi. There is no perfect solution that is short, because headings only have generally one counter. So you either hack together a proper heading with custom counter, or use a figure with custom kind. First provides a Document Outline, but second gives an option to uniquely label questions.

A crucial info that you don’t say is: will there be other headings and which heading will have numbering enabled. Also if there will be nested structure in the outline.

heading + counter hack:

#let question-counter = counter("question")
#let question() = question-counter.step() + [= Question<question>]

#show <question>: it => {
  show: block
  it.body
  [~]
  question-counter.display()
}

#show outline.entry: it => {
  if it.element.func() != heading or it.element.level != 1 { return it }
  if not it.element.has("label") or it.element.label != <question> { return it }
  let h = it.element
  let inner = {
    h.body
    [~]
    numbering("1", ..question-counter.at(h.location()))
    [ ]
    box(width: 1fr, it.fill)
    [ ]
    it.page()
  }
  it.indented(it.prefix(), inner)
}

#outline()

= Normal heading

#question()

#question()

#question()

figure (+ heading for ease of styling):

#let question(label: none) = {
  show figure: none
  [#figure(kind: "question", supplement: "Question")[]#label]
  let number = context counter(figure.where(kind: "question")).display()
  heading(outlined: false)[Question #number]
}

#outline(target: selector.or(heading, figure.where(kind: "question")))

= Normal heading

#question()

#question(label: <this-question>)

This is @this-question.

#question()

Or with a hack that fixes the over-long space:

#let question(label: none) = {
  show figure: none
  [#figure(kind: "question", supplement: "Question")[]#label]
  let number = context counter(figure.where(kind: "question")).display()
  heading(outlined: false)[Question #number]
}

#show outline.entry: it => {
  show h.where(amount: 0.5em.to-absolute()): none
  it
}

#outline(target: selector.or(heading, figure.where(kind: "question")))

= Normal heading

#question()

#question(label: <this-question>)

This is @this-question.

#question()

For some reason, the fix for https://typst-doc-cn.github.io/clreq/#too-wide-spacing-between-heading-numbering-and-title doesn’t work. But converting em to absolute units works.

Context

1 Like

A cleaner way of doing this with figures is by abusing the fact that show rules provide implicit context for (some) built-in elements:

#show figure.where(kind: "question"): it => [
  #set align(left)
  = #it.caption.supplement #it.caption.counter.display()

  #it.body
]

#let question() = figure(
  kind: "question",
  supplement: "Question",
  caption: "",
  []
)



#outline()

#question()<q1>
What is $2 + 2$?

#question()
What is $(2+2)^2$? (Hint: use the results you got from @q1)

#question()

Why do you need it.body?

Yeah, this is what I meant by “figure (+ heading for ease of styling)”. You would have to separately apply whatever heading styling you have. So, potentially duplicating the styling, which doesn’t feel great, IMO. But in this case, it still uses heading, so it’s an alternative for what I did, but the split of the code is better and as a result less messing around is required.

Alternatively, I’d write

#let question-selector = figure.where(kind: "question")
#show question-selector: set align(left)
#show question-selector: it => [= Question #it.caption.counter.display()]

#let question() = figure(
  kind: "question",
  supplement: "Question",
  caption: "",
)[]

or

#let question-selector = figure.where(kind: "question")
#show question-selector: set align(left)
#show question-selector: it => {
  let number = counter(figure.where(kind: "question")).display()
  [= Question #number]
}

#let question() = figure(kind: "question", supplement: "Question")[]

Welcome to the forum, muoscar!
Others have provided comprehensive solutions that support mixing other headings and cross references.

However, if you don’t need advanced features, and only want to fix the Question 0, then the following is enough.

#let question() = {
  question-counter.step()
  context [= Question #question-counter.display()]
}

Full code
#set page(height: auto, width: 240pt, margin: 15pt)

#let question-counter = counter("question")

#let question() = {
  question-counter.step()
  context [= Question #question-counter.display()]
}

#outline()

#question()
#question()
#question()
1 Like