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)
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?
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.
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.
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.
That I also can’t reproduce. Isn’t kerning more like lacking whitespace?
I have added the conditionally-inserting period. You should be able to implement the conditionally-returning supplement similarly, otherwise feel free to ask.
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)
Original question 1) I like what I see in How can I prevent indent on paragraphs with run-in headings? - #2 by ensko to conditionally suppress the indentation of a par when it has been merged with a run-in heading, but have set it aside for now, leaving the approach of negative space. The screenshot shows the unwanted spaces after the dashes. These are selectable characters in the PDF output and this appear to be space characters. I do not understand why the trailing space is wider in the H3 than in the H2.
Original question 2) Sloppy word choice — yes, I meant a small amount excess space not kerning. Setting this complaint aside as the new code has large excess space in place of small excess space.
Original question 3) Yes your conditional period appears to work perfectly.
The trick in How can I prevent indent on paragraphs with run-in headings? - #2 by ensko breaks the other trick to suppress space between adjacent headings, so I am sticking with negative indentation. New code, which also adds negative space to address my complaint of the unwanted space after the heading’s supplement:
#set par(justify: true, first-line-indent: (amount: 1em, all: false), spacing: 0.6em)
#let body-size = 11pt
#set text(font: "New Computer Modern", size: body-size)
#set heading(numbering: "1.1." + sym.space.quad)
#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 (".", "?", "!") { "." }
"---"
h(-0.2em)
}
#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)
// The alternative to negative h() is the trick from https://forum.typst.app/t/how-can-i-prevent-indent-on-paragraphs-with-run-in-headings/3214/2 which breaks the other trick to reduce space between consecutive headings.
emph({
counter(heading).display()
it.body
if it.body.text.last() not in (".", "?", "!") { "." }
"---"
h(-0.2em)
})
}
// 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(10)
This has a strange result: different spacing after the level-3 heading’s supplement in the two instances of the H3. Screenshot with box highlighting to prove that there is misalignment:
I do not understand why “1.1.1. Subsubsection.—” and “1.2.1. Subsubsection.—” would not have the same spacing after each since their supplements are generated by the same show rule.
I take that there’s these couple of problems left to solve:
Indentation for the run-in headings.
Spacing following the heading supplement .---.
The trick […] breaks the other trick to suppress space between adjacent headings, so I am sticking with negative indentation.
It’s unfortunate that both tricks are incompatible. Now, I see you’re content with continuing on using negative h, but improvement is possible in the future.
This has a strange result: different spacing after the level-3 heading’s supplement in the two instances of the H3.
The different spacing you’re observing next to the em dash is due to #par(justify: true). Consequently, that spacing is dynamic. For some reason, I can’t determine what kind of a spacing that is. Seems to be simply h, not " ", though both are selectable " " in PDF as part of text I believe.
The reason why it’s appearing in the first place is because of line breaks that you have for the run-in headings. We can confirm this by using less appealing markup:
=== Subsubsection
#lorem(15) // Spacing
#[=== Subsubsection]#lorem(15) // No spacing
#heading(level: 3)[Subsubsection]#lorem(15) // No spacing
So to remove the spacing next to the heading supplement while still using the existing space-inducing markup, we would have to remove [ ] from the made paragraphs, and I’m not sure how we could achieve that reliably. Reminds me of this issue where a line break causes spacing, undesired for CJK: cjk-unbreak – Typst Universe
For the time being, removal of this spacing is in your case almost certainly most neatly done through negative h. Obviously you could have a separate function, combining a run-in heading and its paragraph altogether in something like #run-in[Heading][Lorem], but I assumed you have a preference for the default = Heading \ Lorem.
Thank you. That explains the mysterious (to me) appearance of the space exactly: it is coded by the line break after the heading. Not unexpected in retrospect. Since the unwanted space is stretchable with justification, not creating it at all by not having a line break after the heading is the solution:
#[=== Subsubsection]#lorem(15) // No spacing
#[=== Subsubsection 2]#{
}#lorem(15) // Also no spacing.
#[=== Or this way]/*
*/#lorem(15) // Also no spacing.
The markup is less appealing, it is true. Pity that this improvement would not work:
#[=== Subsubsection]// Comment does not include line break.
#lorem(15) // Spacing
The whitespace spacing is fixable with a weak spacing:
#let body-size = 11pt
#set text(font: "New Computer Modern", size: body-size)
#set par(justify: true, spacing: 0.6em, first-line-indent: 1em)
#set heading(numbering: "1.1.")
#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: 1): it => {
show h.where(amount: 0.3em): sym.space.quad
it
}
#let space-above-inline-headings = 1em
// Destructive rule before constructive.
#show selector.or(..(2, 3).map(n => heading.where(level: n))): it => {
v(space-above-inline-headings)
// The alternative to negative h() is the trick from https://forum.typst.app/t/how-can-i-prevent-indent-on-paragraphs-with-run-in-headings/3214/2 which breaks the other trick to reduce space between consecutive headings.
h(-par.first-line-indent.amount)
counter(heading).display()
sym.space.quad
it.body
let message = "Have to convert to string with t4t package after all"
assert("text" in it.body.fields(), message: message)
if it.body.text.last() not in ".?!" [.]
sym.dash.em
h(0pt, weak: true)
}
#show heading.where(level: 2): set text(size: body-size)
#show heading.where(level: 3): set text(size: body-size, weight: "regular")
#show heading.where(level: 3): emph
// Clever trick to reduce spacing between consecutive headings.
// Must be defined after the destructive show rules.
// 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 prev = previous-headings.last().location().position()
let cur = it.location().position() // Curent position.
let delta = 30pt // Threshold
if (cur.page == prev.page and cur.x == prev.x and cur.y - prev.y < delta) {
// Amount to reduce spacing, could make this dependent on it.level
v(-10pt)
}
}
it
}
= Introduction
#lorem(25)
#lorem(15)
== Motivation
#lorem(20)
=== Subsubsection
#lorem(15)
== Etc.
=== Subsubsection
#lorem(10)
// == Bad: indeed
// #lorem(20)
The space after numbering in block-level heading is different because the numbering spacing was defined incorrectly. There was also a big repeating show rule. Though it’s rather annoying that you can’t just use #show heading.where(level: 2): set par(first-line-indent: 0pt) for run-in headings and instead use a negative spacing hack.
#set par(
justify: true,
// first-line-indent: 1em,
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.")
#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: 1): it => {
show h.where(amount: 0.3em): sym.space.quad
it
}
#show heading.where(level: 2): set text(size: body-size)
#show heading.where(level: 3): set text(size: body-size, weight: "regular")
#show heading.where(level: 3): emph
#let space-above-inline-headings = 1em
#show selector.or(..(2, 3).map(n => heading.where(level: n))): it => {
v(space-above-inline-headings)
// The alternative to negative h() is the trick from https://forum.typst.app/t/how-can-i-prevent-indent-on-paragraphs-with-run-in-headings/3214/2 which breaks the other trick to reduce space between consecutive headings.
h(-par.first-line-indent.amount)
counter(heading).display()
sym.space.quad
it.body
/* To be move to a new thread: conditionally append a period to the inline heading when no end-of-sentence punctuation is already present.
let message = "Have to convert to string with t4t package after all"
assert("text" in it.body.fields(), message: message)
if it.body.text.last() not in ".?!" [.]
*/
sym.dash.em
h(0pt, weak: true)
}
// Clever trick to reduce spacing between consecutive headings.
// Must be defined after the destructive show rules.
// 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 prev = previous-headings.last().location().position()
let cur = it.location().position() // Curent position.
let delta = 30pt // Threshold
if (cur.page == prev.page and cur.x == prev.x and cur.y - prev.y < delta) {
// Amount to reduce spacing, could make this dependent on it.level
v(-10pt)
}
}
it
}
= Introduction
#lorem(25)
#lorem(15)
== Motivation
#lorem(20)
=== Subsubsection a
#lorem(15)
=== Subsubsection b
#lorem(20)
=== Subsubsection c
This single-line paragraph lacks adequate space after.
== Etc.
=== Subsubsection
#lorem(10)