How to conditionally disable heading numbering for some headings?

Because of user input, I have to search for a specific heading in its show rule, therefore it’s the only place where I can introduce a location mark, state update, etc.

To affect heading numbering in the PDF’s Document Outline, tope-level set heading(numbering) must be used.

The only way to check for the state to conditionally remove heading numbering is with a callback function, but it will trigger the h(0.3em) spacing in the heading, but also a space in the Document Outline. I can technically cancel h, but I need to remove the leading space in the Document Outline.

Moreover, a related issue that needs to be fixed is that the numbering callback doesn’t see the state update inside of same-heading’s show rule, so a numbering for the special heading is still present, even though the spacing can be removed.

Yeah, you can replace it with custom heading rendering logic to fix the last point, but it will bloat the complexity and amount of code big time, plus the issue in the Document Outline still exists.

#let started = state("started", false)
#set heading(numbering: (..n) => if not started.get() { numbering("I.", ..n) })
#show heading: it => {
  let s = started.get()
  if lower(it.body.text) == "very special" {
    s = true
    started.update(s)
  }
  show h.where(amount: 0.3em): if s { none } else { x => x }
  it
}

// #let started() = query(selector(<start>).before(here())) != ()
// #set heading(numbering: (..n) => if not started() { numbering("I.", ..n) })
// #show heading: it => {
//   let s = started()
//   if lower(it.body.text) == "very special" {
//     s = true
//     [#metadata(none)<start>]
//   }
//   show h.where(amount: 0.3em): if s { none } else { x => x }
//   it
// }

= Heading
= Another
= Very special
= Last
#bibliography(bytes(""))

If this is not possible, what’s the reasonable feature that can solve this problem?

If the hard-coded space can’t be changed

then something else must. But what? Maybe treating numbering: (..n) => none the same as numbering: none? That to me sounds like the most logical and easy fix. Though not sure how easy it would be to implement such a behavior. And will it spill into some other numbering related things/elements.


The only workaround that works is by explicitly adding top-level set rule before the special heading. But for a template/automatic approach this is bad.

This is one way that fixes it, it’s not foolproof though, but it shows we can find headings by name with a query and then use attribute matches to style them in the good way (show-set) by matching on level and body.

What it doesn’t do right is that duplicate headings with the same body elsewhere, will also lose their number, unfortunately. You may have to require labels or some other unique thing on each heading, to resolve this?

#set heading(numbering: "I.")
#show: doc => context {
  let special-heading = query(heading.where(level: 1)).filter(elt => lower(elt.body.text) == "very special").at(0, default: none)
  if special-heading == none {
    return doc
  } else {
    // all the rest of the headings:
    let rest-of-the-headings = query(selector(heading).after(special-heading.location(), inclusive: false))
    show selector.or(..rest-of-the-headings.map(h => heading.where(outlined: true, level: h.level, body: h.body))): set heading(numbering: none)
    doc
  }
}

= Heading
= Another
= Very special
= Last
#bibliography(bytes(""))
1 Like

Oh, noo… When I tested it, turns out that I do need context because of the heading/reference numbering suffix issue (with callback function for complex/custom numbering). GitHub · Where software is built

#let in-heading = state("in-heading", false)
#set heading(numbering: (..n) => context {
  if in-heading.get() { numbering("I.", ..n) } else { numbering("I", ..n) }
})
#show heading: it => in-heading.update(true) + it + in-heading.update(false)

= Heading <a>
@a

Is what I would say a few moments ago, but in reality, you don’t need this check/context IF you manually handle 1 of 2 use cases, i.e., in-heading numbering. Then reference can use built-for-reference heading.numbering and it’s all good. Also, this might fix this, though it’s only for body, but should also help with numbering. Contextual content doesn't appear in PDF document title and bookmarks/outline · Issue #3424 · typst/typst · GitHub

Is what I would say a few moments ago, but then you might use outline and it all crumbles as it uses built-for-reference numbering. You can’t use in-outline because it again will require context. You can probably also manually handle this 3rd use case. But you’ll end up with a big heading and outline entry show rules just so you can override the heading numbering. A better way would be to only override a very specific ref, plus you don’t have to write a destructive show rule (Option to hide last dot in refs to heading that had custom numbering. · Issue #6032 · typst/typst · GitHub):

#set heading(numbering: numbly("{1:I}.", "{2:1})"))
#show ref.where(form: "normal", supplement: auto): it => {
  if (
    it.element == none
      or it.element.func() != heading
      or it.element.numbering == none
      or it.element.supplement != [Section]
  ) { return it }
  show regex("[\\w.)]+[.)]"): it => it.text.slice(0, -1)
  it
}

Is what I would say a few moments ago, b… oh, wait.

This is kind of very closely related issue, but not exactly what I was asking about. Thankfully, they can work together, so I can use the above heading.numbering + ref combo if the numbering is complex, and it should be super robust.

The outlined: true doesn’t make much sense, especially since non-outlined outline is already unnumbered. Anyway, this is not a concern for me. And the duplicates. Even though with How to apply titlecase to headings in Document Outline? - #3 by Andrew, now there are actually duplicates, but both copies already have synced numbering, so this side effect is not a problem in this case.

#set heading(numbering: "I.1.")
#let special-heading-name = "very special"
#show: doc => context {
  let is-special = it => (
    lower(it.body.at("text", default: "")) == special-heading-name
  )
  let special-heading = query(heading.where(level: 1))
    .filter(is-special)
    .first(default: none)
  if special-heading == none { return doc }
  let rest-of-the-headings = query(
    selector(heading).after(special-heading.location()),
  )
  show selector.or(..rest-of-the-headings.map(it => heading.where(
    level: it.level,
    body: it.body,
  ))): set heading(numbering: none)
  doc
}

#outline()

= Heading
= Another
== 2nd level
= And another one
= Very special
= Last
#bibliography(bytes(""))

This is a very nice, proper-like solution, yet it’s hacky because you need to utilize not only the everything show rule, but also use context on top of it, where you query potentially a ton of elements. So the performance is a concern. But so far it seems not much of a problem. Moreover, it allows to remove whatever else code was there because of the numbering issue. And since it’s an isolated show rule that is not destructive, it’s pretty damn robust, considering the use case. It’s irreversible, but for a template function that’s usually the point anyway. But still, a native solution should exist.

1 Like