How to avoid ghost numbering in headers?

The Level 1 headings in my document must be numbered like "Section : ". None of the other headers should be numbered. There are several ways to accomplish this, many discussed here. My problem: all solutions cause the un-numbered headings to be slightly indented!

Here’s an example:

#set heading(numbering: (..nums) => {
  nums = nums.pos()
  if nums.len() == 1 {
    return "Section " + numbering("1:", ..nums)
  } else {return ""}
})

= First

== Second
body text

Which produces

You can see that “Second” is left aligned but not to the same place where “Section” and “body” start. This seems to be caused by a ghost number… typst is laying out the level two heading like <number><tab><heading text>. There is no <number> but <tab> is still injected before “body”.

How can I avoid this and keep my headers aligned with the body text?

Another example:

#set heading(numbering: (first, ..other) =>
  if other.pos().len() == 0 { return first }
)

= First

== Second
body text

Produces

Here’s one way around it:

#set heading(numbering: "1")
#show heading: it => block(it.body)
#show heading.where(level: 1): it => block("Section " +it.numbering.at(0) + ". " + it.body)

= First

== Second
body text

This feels pretty hacky. Set a numbering scheme, override it for everything, override the override for a special case. This can’t possibly be the correct method, can it?

Yeah that method blows up if you use an outline() because that doesn’t have a numbering method:

#set heading(numbering: "1")
#show heading: it => block(it.body)
#show heading.where(level: 1): it => block("Section " +it.numbering.at(0) + ". " + it.body)

#outline()

= First

== Second
body text

The #outline() call creates a Level 1 heading with no numbering:

which causes the show rule to fail and the document won’t compile:

Hey there.

Why overcomplicate?

#show heading.where(level: 1, outlined: true): set heading(numbering: n => {
  let number = numbering("1", n)
  [Section #number:]
})

// Or
// #show heading.where(level: 1, outlined: true): set heading(
//   numbering: n => [section #numbering("1", n):],
// )

#outline()

= First
#lorem(5)

== Second
#lorem(5)

= First
#lorem(5)

== Second
#lorem(5)

The only issue is that outline also has a heading. You can do

#show heading.where(level: 1): set heading(numbering: n => {
  let number = numbering("1", n)
  [Section #number:]
})
#show outline: it => {
  show heading.where(level: 1): set heading(numbering: none)
  it
}

to prevent styling it, but it’s not as neat as the solution above.

Brilliant! That is miles better than what I was trying. The Outline indentation is way cleaner than I was getting as well. I’m fine with “Section N:” in the outline.

Looks like I need get better at using where. It’s not obvious to me what attributes are available there. It’s also confusing that you can check multiple attributes simultaneously, but not multiple values of a single attribute. (I think?)