Skip to content

Conversation

mkorje
Copy link
Collaborator

@mkorje mkorje commented Jun 2, 2025

Font handling

The default math font (new computer modern math) has been added to the text font fallback list. This means that math will always be able to render, as long as the user doesn't #set text(fallback: false).

The way in which font fallback in math works is nearly identical to normal text layout. The key difference is that we only use fonts that have a MATH table present, skipping ones that don't. Note that while strings in math don't use the text font, they do go through normal text layout. So $🍏$ will render as a tofu, whereas $"🍏"$ will not.

Math layout will fallback through all available fonts, and has its own set of fallback fonts distinct from normal text.

Test changes caused by font handling

math-font-fallback: Font fall back in math only goes through fonts with a MATH table.

Old New
math-font-fallback-old math-font-fallback-new
--- math-font-fallback ---
// Test font fallback.
$and "" and 🏳️‍🌈 $

math-lr-scripts: The zws is now just ignored, so it no longer removes the spacing after the lr. (i.e. the test now matches what it would look like if the zws weren't there.)

Old New
math-lr-scripts-old math-lr-scripts-new
--- math-lr-scripts ---
// Test interactions with script attachments.
$ lr(size: #3em, |)_a^b lr(size: #3em, zws|)_a^b
  lr(size: #3em, [x])_0^1 [x]_0^1
  lr(size: #1em, lr(size: #10em, [x]))_0^1 $

Repr in math

RawElem in math is kinda weird. At the moment it basically ends up going through layout_external since the font has changed:

// Hack because the font is fixed in math.
if styles != outer
&& styles.get_ref(TextElem::font) != outer.get_ref(TextElem::font)
{
let frame = layout_external(elem, self, styles)?;
self.push(FrameFragment::new(styles, frame).with_spaced(true));
continue;
}
To preserve this behaviour, I've opted to cut any processing of RawElem off during realisation in math mode and add a check to not set the baseline for a RawElem.

Multiple fonts in math

As the name of the PR would suggest, multiple fonts now just work in math. No need for box hacks anymore.

New tests as examples

math-font-features-switch: You can use glyphs from a font's stylistic sets now. Notice that the math kerning with the subscripts is still working!
math-font-features-switch

--- math-font-features-switch ---
#let scr(it) = text(features: ("ss01",), $cal(it)$)
$cal(P)_i != scr(P)_i$, $cal(bold(I))_l != bold(scr(I))_l$
$ product.co_(B in scr(B))^(B in scr(bold(B))) cal(B)(X) $

math-font-covers: You can use covers with fonts in math now, so that specific glyphs are from a given font. Again, notice the kerning with the subscripts.
math-font-covers

--- math-font-covers ---
#show math.equation: set text(
  font: (
    // Ignore that this regex actually misses some of the script glyphs...
    (name: "XITS Math", covers: regex("[\u{1D49C}-\u{1D503}]")),
  ),
  features: ("ss01",),
)
$ cal(P)_i (X) * cal(C)_1 $

math-op-font: You can use any font you'd like operators and limits will work as specified.
math-op-font

--- math-op-font ---
// Test with different font.
#let colim = math.op(
  text(font: "IBM Plex Sans", weight: "regular", size: 0.8em)[colim],
  limits: true,
)
$ colim_(x -> 0) inline(colim_(x -> 0)) $

math-op-set-font: Notice the improved kerning between the letters in each operator (less space between c and h in sech, more space between o and d in mod) as well as the ligature.
math-op-set-font

--- math-op-set-font ---
// Test setting font.
#show math.equation: set text(weight: "regular")
#let lig = math.op("fi")
#let test = $sech(x) mod_(x -> oo) lig_1(X)$
#test
#show math.op: set text(font: "New Computer Modern")
#test

More customisation and control

Whenever a glyph is laid out in math by Typst (e.g. the delims for matrices, the accent glyph for accents), it now goes through realisation. Essentially, this means you can now target them with show rules. Combine this with proper support for multiple fonts, and you can do all sorts of stuff (even very cursed things)!

New tests as examples

math-root-show-rule-1
math-root-show-rule-1

--- math-root-show-rule-1 ---
#show "": set text(red, font: "Noto Sans Math")
$ root(2, (a + b) / c) $

math-root-show-rule-2
math-root-show-rule-2

--- math-root-show-rule-2 ---
#show "": set text(2em)
$ sqrt(2) $

math-root-show-rule-3
math-root-show-rule-3

--- math-root-show-rule-3 ---
// Test cursed show rule.
#show "": "!"
$ sqrt(2) root(2, 2) $

math-root-show-rule-4
math-root-show-rule-4

--- math-root-show-rule-4 ---
#show math.root: set text(red)
$ sqrt(x + y) root(4, 2) $

math-root-show-rule-5
math-root-show-rule-5

--- math-root-show-rule-5 ---
#show math.root: it => {
  show "": set text(purple) if it.index == none
  it
}
$ sqrt(1/2) root(3, 1/2) $

math-delim-show-rule-1
math-delim-show-rule-1

--- math-delim-show-rule-1 ---
#show regex("\[|\]"): set text(green, font: "Noto Sans Math")
$ mat(delim: \[, a, b, c; d, e, f; g, h, i) quad [x + y] $

math-delim-show-rule-2
math-delim-show-rule-2

--- math-delim-show-rule-2 ---
#show math.vec: it => {
  show regex("\(|\)"): set text(blue)
  it
}
$ vec(1, 0, 0), mat(1; 0; 0), (1), binom(n, k) $

math-delim-show-rule-3
math-delim-show-rule-3

--- math-delim-show-rule-3 ---
#show "": set text(fuchsia)
$ underbrace(1 + 1 = 2, "obviously") $

math-delim-show-rule-4
math-delim-show-rule-4

--- math-delim-show-rule-4 ---
#show "{": set text(navy)
$ cases(x + y + z = 0, 2x - y = 0, -5y + 2z = 0) $

math-delim-show-rule-5
math-delim-show-rule-5

--- math-delim-show-rule-5 ---
#show regex("\(|\)"): set text(1.5em)
$ 10 dot (9 - 5) dot (1/2 - 1) $

math-primes-show-rule
math-primes-show-rule

--- math-primes-show-rule ---
#show math.primes: set text(maroon)
$f'(x), f''''''(x)$

math-glyph-show-rule
math-glyph-show-rule

--- math-glyph-show-rule ---
#show "+": set text(orange, font: "Noto Sans Math")
$ 1 + 1 = +2 $
#show "+": text(2em)[#sym.plus.circle]
$ 1 + 1 = +2 $

math-accent-show-rule-1
math-accent-show-rule-1

--- math-accent-show-rule-1 ---
#show "\u{0302}": set text(blue, font: "XITS Math")
$hat(x)$, $hat(hat(x))$, x\u{0302}

math-accent-show-rule-2
math-accent-show-rule-2

--- math-accent-show-rule-2 ---
#let rhat(x) = {
  show "\u{0302}": set text(red)
  math.hat(x)
}
$hat(x)$, $rhat(x)$, $hat(rhat(x))$, $rhat(hat(x))$, x\u{0302}

math-accent-show-rule-3
math-accent-show-rule-3

--- math-accent-show-rule-3 ---
#show math.accent: it => {
  show "\u{0300}": set text(green)
  it
}
$grave(x)$, x\u{0300}

math-accent-show-rule-4
math-accent-show-rule-4

--- math-accent-show-rule-4 ---
#show "\u{0302}": box(inset: (bottom: 5pt), text(0.5em, sym.diamond.small))
$hat(X)$, $hat(x)$

Issues

@mkorje mkorje force-pushed the math-shaper-fonts branch from 030ed95 to 0d0481c Compare June 2, 2025 09:41
@istudyatuni
Copy link
Contributor

istudyatuni commented Jun 2, 2025

Why is raw emoji showing but #emoji is not?

@Enivex
Copy link
Collaborator

Enivex commented Jun 2, 2025

Why is raw emoji showing but #emoji is not?

I don't know exactly what you're referring to, but it probably has to do with math fonts not having emojis in them.

@istudyatuni
Copy link
Contributor

When written as $🍏$, emoji is rendered, but when as $#emoji.apple$, is not

@laurmaedje laurmaedje added math Related to math syntax, layout, etc. text Text layout, shaping, internationalization, etc. labels Jun 4, 2025
@mkorje mkorje mentioned this pull request Jun 10, 2025
@laurmaedje laurmaedje added the waiting-on-author Pull request waits on author label Jun 23, 2025
@mkorje mkorje force-pushed the math-shaper-fonts branch from 0d0481c to 640cca9 Compare July 8, 2025 12:58
@mkorje
Copy link
Collaborator Author

mkorje commented Jul 8, 2025

I don't know exactly what you're referring to, but it probably has to do with math fonts not having emojis in them.

Yes, that's why.

When written as $🍏$, emoji is rendered, but when as $#emoji.apple$, is not

I can't reproduce this, they both render as tofus on this branch.

@mkorje mkorje force-pushed the math-shaper-fonts branch 6 times, most recently from abe5a38 to c516a65 Compare July 9, 2025 05:48
@mkorje
Copy link
Collaborator Author

mkorje commented Jul 9, 2025

I've opened typst/typst-dev-assets#7 to add another math font to the test suite.

@mkorje mkorje force-pushed the math-shaper-fonts branch 6 times, most recently from 2587d3b to fe4a4cc Compare July 9, 2025 09:36
@mkorje mkorje force-pushed the math-shaper-fonts branch 4 times, most recently from da12225 to e84434a Compare July 9, 2025 11:41
@mkorje mkorje force-pushed the math-shaper-fonts branch from 27ebd1d to 2baaf22 Compare August 4, 2025 10:04
@mkorje mkorje force-pushed the math-shaper-fonts branch from 69cca4f to e06ebac Compare August 4, 2025 14:30
.and_then(|id| world.font(id))
.filter(|_| family.covers().is_none())
})
.ok_or(::ecow::eco_vec![error!(span, "no font could be found")])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error is new, right? I guess relying purely on fallback does not work with the font stack thing, but also generally fails because we need all those metrics?

This seem a bit inconsistent with how font fallback otherwise works in Typst, but I guess it is not more restrictive than before with "current font is not suitable for math".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's basically replaced the old "current font doesn't support math" error thing. Yeah, we just need to grab whatever the current font is for the metrics.

Surely though this error will basically never occur, right? (Unless the font specified doesn't exist and fallback is disabled, I guess)

@mkorje mkorje force-pushed the math-shaper-fonts branch from 366add8 to d7a83b7 Compare August 5, 2025 03:35
@mkorje mkorje force-pushed the math-shaper-fonts branch from 5715c49 to b58935a Compare August 5, 2025 04:29
@mkorje mkorje force-pushed the math-shaper-fonts branch from 172274f to 8e671b4 Compare August 5, 2025 04:41
@laurmaedje
Copy link
Member

Thank you. This is excellent work!

Merged via the queue into typst:main with commit 89dce86 Aug 7, 2025
8 checks passed
@mkorje mkorje deleted the math-shaper-fonts branch August 7, 2025 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
math Related to math syntax, layout, etc. text Text layout, shaping, internationalization, etc.
Projects
None yet
4 participants