How to control whitespace before and after run-in headings

I am customizing document style to achieve numbered inline (a.ka., run-in) headings and am, in essence, satisfied with the result of the following.

Code:

#let indent = 1.0em
#let space_above_inline_heads = 1em
#set par(justify: true, first-line-indent: indent, spacing: 0.6em)
#let bodysize = 11pt
#set text(font: "New Computer Modern", size: bodysize)

#let widespace = " "
#set heading(numbering: "1.1." + widespace)
#show heading: it => block[
	#counter(heading).display(it.numbering)#(it.body)
]
#show heading.where(level: 1): set align(center)
#show heading.where(level: 1): set text(size: 13pt, weight: "regular")
#show heading.where(level: 1): smallcaps
#show heading.where(level: 2): set text(
	size: bodysize,
	weight: "bold",
)
// https://typst.app/docs/reference/model/heading/ says to, When writing a rule that accesses the body field, wrap the content in a block to prevent headings from becoming orphans.  Advice seems inapplicable to an inline heading, though.
#show heading.where(level: 2): it => {
	v(space_above_inline_heads) + h(-indent) + counter(heading).display(it.numbering) + it.body + ".---" + context { h(-(measure([ ]).width)) }
}
#show heading.where(level: 3): set text(
	size: bodysize,
	weight: "regular",
)
#show heading.where(level: 3): it => {
	v(space_above_inline_heads) + h(-indent) + emph[#{counter(heading).display(it.numbering) + it.body + ".---"}] + context { h(-(measure([ ]).width)) }
}

#show heading: it => {
	// From https://github.com/typst/typst/issues/2953#issuecomment-3187505828 :
	// Clever trick to reduce spacing between consecutive headings
	let previous_headings = query(selector(heading).before(here(), inclusive: false))
	if previous_headings.len() > 0 {
		let ploc = previous_headings.last().location().position()
		let iloc = it.location().position()
		if (iloc.page == ploc.page and iloc.x == ploc.x and iloc.y - ploc.y < 30pt) { // threshold
			v(-10pt) // amount to reduce spacing, could make this dependent on it.level
		}
	}
	it
}

= Introduction
#lorem(25)

#lorem(15)

== Motivation
#lorem(20)

=== Subsubsection
#lorem(15)

== Etc.
=== Subsubsection
#lorem(20)
  1. I dislike the use of negative horizontal spacing to prevent the inline headings being indented and to avoid a gratuitous space between the inline heading’s supplement (a dash in this case) and its following paragraph body text. Is there a better way? Especially measuring the width of a space and backing up that distance seems very hackish and I would prefer to simply omit the trailing space that is being automatically inserted between the inline heading and the paragraph. Is there a settable parameter for that?

  2. Minor problem: in the output “1.1.1. Subsubsection.—Lorem […]” appears to have a bit of whitespace (kerning) between the period and the em dash.

  3. Ideally, I would like an evanescent period that looks back to the last printed character and prints itself if and only if that prior character is not a period, question mark, or exclamation mark. I would also use this when inserting a variable’s value into markup. That is a lot to ask of a markup processor, though. My next addition to these inline-heading rules will be some code to scan it.body to conditionally return the supplement.

Thanks for posing the questions, hopefully we can address all of them.

  1. I don’t know what spacing you’re seeing after the em dash, a screenshot would help. The negative spaces you added to counteract them didn’t have effect, so I removed them.

    As for counteracting the inline heading indentation caused by paragraphs, I swapped your indent variable for par.first-line-indent.amount. Another solution worth considering: How can I prevent indent on paragraphs with run-in headings?

  2. That I also can’t reproduce. Isn’t kerning more like lacking whitespace?

  3. I have added the conditionally-inserting period. You should be able to implement the conditionally-returning supplement similarly, otherwise feel free to ask.

Less relevant changes:

  • Simplified use of modes.

  • Removed it.numbering from counter(heading).display(it.numbering).

  • Avoided too many show rules for the same element.

  • Acknowledged your code comment on preventing headings from becoming orphans:

    • The docs there also say that heading by default is a block using sticky: true. So when changing heading via show-set rules, we must also use block to retain that behaviour.
    • But yes, I too don’t see how this could be applied to inline headings, because they’re not blocks and you’re not using them as inline headings everywhere. I assume you would want Etc.--- to stick to the following heading? Need need more help here.
  • Swapped widespace for sym.space.fig as your space was only a regular one.

  • Swapped snake_case for kebab-case as it’s the preferred styling.

New code:

Code
#set par(justify: true, first-line-indent: 1em, spacing: 0.6em)

#let body-size = 11pt
#set text(font: "New Computer Modern", size: body-size)

#set heading(numbering: "1.1." + sym.space.fig)

#show heading.where(level: 1): smallcaps
#show heading.where(level: 1): set align(center)
#show heading.where(level: 1): set text(size: 13pt, weight: "regular")

#let space-above-inline-heads = 1em

#show heading.where(level: 2): set text(size: body-size)
#show heading.where(level: 2): it => {
  v(space-above-inline-heads)
  h(-par.first-line-indent.amount)
  counter(heading).display()
  it.body
  if it.body.text.last() not in (".", "?", "!") { "." }
  "---"
}

#show heading.where(level: 3): set text(size: body-size, weight: "regular")
#show heading.where(level: 3): it => {
  v(space-above-inline-heads)
  h(-par.first-line-indent.amount)
  emph({
    counter(heading).display()
    it.body
    if it.body.text.last() not in (".", "?", "!") { "." }
    "---"
  })
}

// Clever trick to reduce spacing between consecutive headings
// See https://github.com/typst/typst/issues/2953#issuecomment-3187505828
#show heading: it => {
  let previous-headings = query(selector(heading).before(here(), inclusive: false))
  if previous-headings.len() > 0 {
    let ploc = previous-headings.last().location().position()
    let iloc = it.location().position()
    if (iloc.page == ploc.page and iloc.x == ploc.x and iloc.y - ploc.y < 30pt) { // Threshold
      v(-10pt) // Amount to reduce spacing, could make this dependent on it.level
    }
  }
  it
}

= Introduction
#lorem(25)

#lorem(15)

== Motivation
#lorem(20)

=== Subsubsection
#lorem(15)

== Etc.
=== Subsubsection
#lorem(20)