diff --git a/Cargo.lock b/Cargo.lock index 5526da48c..1893f89fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,7 +413,7 @@ dependencies = [ [[package]] name = "codex" version = "0.1.1" -source = "git+https://github.com/typst/codex?rev=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928" +source = "git+https://github.com/typst/codex?rev=9ac86f9#9ac86f96af5b89fce555e6bba8b6d1ac7b44ef00" [[package]] name = "color-print" @@ -2861,7 +2861,7 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.13.1" -source = "git+https://github.com/typst/typst-assets?rev=c1089b4#c1089b46c461bdde579c55caa941a3cc7dec3e8a" +source = "git+https://github.com/typst/typst-assets?rev=edf0d64#edf0d648376e29738a05a933af9ea99bb81557b1" [[package]] name = "typst-cli" @@ -3032,6 +3032,7 @@ version = "0.13.1" dependencies = [ "az", "bumpalo", + "codex", "comemo", "ecow", "hypher", diff --git a/Cargo.toml b/Cargo.toml index 6cc59ee89..9657f207f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c1089b4" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "edf0d64" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" } arrayvec = "0.7.4" az = "1.2" @@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" codespan-reporting = "0.11" -codex = { git = "https://github.com/typst/codex", rev = "a5428cb" } +codex = { git = "https://github.com/typst/codex", rev = "9ac86f9" } color-print = "0.3.6" comemo = "0.4" csv = "1" diff --git a/crates/typst-layout/Cargo.toml b/crates/typst-layout/Cargo.toml index cc355a3db..2c314e5c5 100644 --- a/crates/typst-layout/Cargo.toml +++ b/crates/typst-layout/Cargo.toml @@ -21,6 +21,7 @@ typst-timing = { workspace = true } typst-utils = { workspace = true } az = { workspace = true } bumpalo = { workspace = true } +codex = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } hypher = { workspace = true } diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 53f88f2b6..634969cd4 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -1,10 +1,11 @@ use std::f64::consts::SQRT_2; +use codex::styling::{to_style, MathStyle}; use ecow::EcoString; use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Size}; -use typst_library::math::{EquationElem, MathSize, MathVariant}; +use typst_library::math::{EquationElem, MathSize}; use typst_library::text::{ BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric, }; @@ -64,12 +65,21 @@ fn layout_inline_text( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult { + let variant = styles.get(EquationElem::variant); + let bold = styles.get(EquationElem::bold); + // Disable auto-italic. + let italic = styles.get(EquationElem::italic).or(Some(false)); + if text.chars().all(|c| c.is_ascii_digit() || c == '.') { // Small optimization for numbers. Note that this lays out slightly // differently to normal text and is worth re-evaluating in the future. let mut fragments = vec![]; for unstyled_c in text.chars() { - let c = styled_char(styles, unstyled_c, false); + // This is fine as ascii digits and '.' can never end up as more + // than a single char after styling. + let style = MathStyle::select(unstyled_c, variant, bold, italic); + let c = to_style(unstyled_c, style).next().unwrap(); + let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?; fragments.push(glyph.into()); } @@ -83,8 +93,10 @@ fn layout_inline_text( .map(|p| p.wrap()); let styles = styles.chain(&local); - let styled_text: EcoString = - text.chars().map(|c| styled_char(styles, c, false)).collect(); + let styled_text: EcoString = text + .chars() + .flat_map(|c| to_style(c, MathStyle::select(c, variant, bold, italic))) + .collect(); let spaced = styled_text.graphemes(true).nth(1).is_some(); let elem = TextElem::packed(styled_text).spanned(span); @@ -124,9 +136,16 @@ pub fn layout_symbol( Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)), _ => (elem.text, styles), }; - let c = styled_char(styles, unstyled_c, true); + + let variant = styles.get(EquationElem::variant); + let bold = styles.get(EquationElem::bold); + let italic = styles.get(EquationElem::italic); + + let style = MathStyle::select(unstyled_c, variant, bold, italic); + let text: EcoString = to_style(unstyled_c, style).collect(); + let fragment: MathFragment = - match GlyphFragment::new_char(ctx.font, symbol_styles, c, elem.span()) { + match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) { Ok(mut glyph) => { adjust_glyph_layout(&mut glyph, ctx, styles); glyph.into() @@ -134,8 +153,7 @@ pub fn layout_symbol( Err(_) => { // Not in the math font, fallback to normal inline text layout. // TODO: Should replace this with proper fallback in [`GlyphFragment::new`]. - layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)? - .into() + layout_inline_text(&text, elem.span(), ctx, styles)?.into() } }; ctx.push(fragment); @@ -161,226 +179,6 @@ fn adjust_glyph_layout( } } -/// Style the character by selecting the unicode codepoint for italic, bold, -/// caligraphic, etc. -/// -/// -/// -fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char { - use MathVariant::*; - - let variant = styles.get(EquationElem::variant); - let bold = styles.get(EquationElem::bold); - let italic = styles.get(EquationElem::italic).unwrap_or( - auto_italic - && matches!( - c, - 'a'..='z' | 'ħ' | 'ı' | 'ȷ' | 'A'..='Z' | - 'α'..='ω' | '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ' - ) - && matches!(variant, Sans | Serif), - ); - - if let Some(c) = basic_exception(c) { - return c; - } - - if let Some(c) = latin_exception(c, variant, bold, italic) { - return c; - } - - if let Some(c) = greek_exception(c, variant, bold, italic) { - return c; - } - - let base = match c { - 'A'..='Z' => 'A', - 'a'..='z' => 'a', - 'Α'..='Ω' => 'Α', - 'α'..='ω' => 'α', - '0'..='9' => '0', - // Hebrew Alef -> Dalet. - '\u{05D0}'..='\u{05D3}' => '\u{05D0}', - _ => return c, - }; - - let tuple = (variant, bold, italic); - let start = match c { - // Latin upper. - 'A'..='Z' => match tuple { - (Serif, false, false) => 0x0041, - (Serif, true, false) => 0x1D400, - (Serif, false, true) => 0x1D434, - (Serif, true, true) => 0x1D468, - (Sans, false, false) => 0x1D5A0, - (Sans, true, false) => 0x1D5D4, - (Sans, false, true) => 0x1D608, - (Sans, true, true) => 0x1D63C, - (Cal, false, _) => 0x1D49C, - (Cal, true, _) => 0x1D4D0, - (Frak, false, _) => 0x1D504, - (Frak, true, _) => 0x1D56C, - (Mono, _, _) => 0x1D670, - (Bb, _, _) => 0x1D538, - }, - - // Latin lower. - 'a'..='z' => match tuple { - (Serif, false, false) => 0x0061, - (Serif, true, false) => 0x1D41A, - (Serif, false, true) => 0x1D44E, - (Serif, true, true) => 0x1D482, - (Sans, false, false) => 0x1D5BA, - (Sans, true, false) => 0x1D5EE, - (Sans, false, true) => 0x1D622, - (Sans, true, true) => 0x1D656, - (Cal, false, _) => 0x1D4B6, - (Cal, true, _) => 0x1D4EA, - (Frak, false, _) => 0x1D51E, - (Frak, true, _) => 0x1D586, - (Mono, _, _) => 0x1D68A, - (Bb, _, _) => 0x1D552, - }, - - // Greek upper. - 'Α'..='Ω' => match tuple { - (Serif, false, false) => 0x0391, - (Serif, true, false) => 0x1D6A8, - (Serif, false, true) => 0x1D6E2, - (Serif, true, true) => 0x1D71C, - (Sans, _, false) => 0x1D756, - (Sans, _, true) => 0x1D790, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Greek lower. - 'α'..='ω' => match tuple { - (Serif, false, false) => 0x03B1, - (Serif, true, false) => 0x1D6C2, - (Serif, false, true) => 0x1D6FC, - (Serif, true, true) => 0x1D736, - (Sans, _, false) => 0x1D770, - (Sans, _, true) => 0x1D7AA, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Hebrew Alef -> Dalet. - '\u{05D0}'..='\u{05D3}' => 0x2135, - - // Numbers. - '0'..='9' => match tuple { - (Serif, false, _) => 0x0030, - (Serif, true, _) => 0x1D7CE, - (Bb, _, _) => 0x1D7D8, - (Sans, false, _) => 0x1D7E2, - (Sans, true, _) => 0x1D7EC, - (Mono, _, _) => 0x1D7F6, - (Cal | Frak, _, _) => return c, - }, - - _ => unreachable!(), - }; - - std::char::from_u32(start + (c as u32 - base as u32)).unwrap() -} - -fn basic_exception(c: char) -> Option { - Some(match c { - '〈' => '⟨', - '〉' => '⟩', - '《' => '⟪', - '》' => '⟫', - _ => return None, - }) -} - -fn latin_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option { - use MathVariant::*; - Some(match (c, variant, bold, italic) { - ('B', Cal, false, _) => 'ℬ', - ('E', Cal, false, _) => 'ℰ', - ('F', Cal, false, _) => 'ℱ', - ('H', Cal, false, _) => 'ℋ', - ('I', Cal, false, _) => 'ℐ', - ('L', Cal, false, _) => 'ℒ', - ('M', Cal, false, _) => 'ℳ', - ('R', Cal, false, _) => 'ℛ', - ('C', Frak, false, _) => 'ℭ', - ('H', Frak, false, _) => 'ℌ', - ('I', Frak, false, _) => 'ℑ', - ('R', Frak, false, _) => 'ℜ', - ('Z', Frak, false, _) => 'ℨ', - ('C', Bb, ..) => 'ℂ', - ('H', Bb, ..) => 'ℍ', - ('N', Bb, ..) => 'ℕ', - ('P', Bb, ..) => 'ℙ', - ('Q', Bb, ..) => 'ℚ', - ('R', Bb, ..) => 'ℝ', - ('Z', Bb, ..) => 'ℤ', - ('D', Bb, _, true) => 'ⅅ', - ('d', Bb, _, true) => 'ⅆ', - ('e', Bb, _, true) => 'ⅇ', - ('i', Bb, _, true) => 'ⅈ', - ('j', Bb, _, true) => 'ⅉ', - ('h', Serif, false, true) => 'ℎ', - ('e', Cal, false, _) => 'ℯ', - ('g', Cal, false, _) => 'ℊ', - ('o', Cal, false, _) => 'ℴ', - ('ħ', Serif, .., true) => 'ℏ', - ('ı', Serif, .., true) => '𝚤', - ('ȷ', Serif, .., true) => '𝚥', - _ => return None, - }) -} - -fn greek_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option { - use MathVariant::*; - if c == 'Ϝ' && variant == Serif && bold { - return Some('𝟊'); - } - if c == 'ϝ' && variant == Serif && bold { - return Some('𝟋'); - } - - let list = match c { - 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡', 'ϴ'], - '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩', '∇'], - '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃', '∂'], - 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄', 'ϵ'], - 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅', 'ϑ'], - 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆', 'ϰ'], - 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇', 'ϕ'], - 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈', 'ϱ'], - 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉', 'ϖ'], - 'Γ' => ['𝚪', '𝛤', '𝜞', '𝝘', '𝞒', 'ℾ'], - 'γ' => ['𝛄', '𝛾', '𝜸', '𝝲', '𝞬', 'ℽ'], - 'Π' => ['𝚷', '𝛱', '𝜫', '𝝥', '𝞟', 'ℿ'], - 'π' => ['𝛑', '𝜋', '𝝅', '𝝿', '𝞹', 'ℼ'], - '∑' => ['∑', '∑', '∑', '∑', '∑', '⅀'], - _ => return None, - }; - - Some(match (variant, bold, italic) { - (Serif, true, false) => list[0], - (Serif, false, true) => list[1], - (Serif, true, true) => list[2], - (Sans, _, false) => list[3], - (Sans, _, true) => list[4], - (Bb, ..) => list[5], - _ => return None, - }) -} - /// The non-dotless version of a dotless character that can be used with the /// `dtls` OpenType feature. pub fn try_dotless(c: char) -> Option { diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs index 0c9ba11df..a2ae54471 100644 --- a/crates/typst-library/src/math/equation.rs +++ b/crates/typst-library/src/math/equation.rs @@ -1,5 +1,6 @@ use std::num::NonZeroUsize; +use codex::styling::MathVariant; use typst_utils::NonZeroExt; use unicode_math_class::MathClass; @@ -12,7 +13,7 @@ use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; use crate::layout::{ AlignElem, Alignment, BlockElem, OuterHAlignment, SpecificAlignment, VAlignment, }; -use crate::math::{MathSize, MathVariant}; +use crate::math::MathSize; use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement}; use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; @@ -111,7 +112,7 @@ pub struct EquationElem { /// The style variant to select. #[internal] #[ghost] - pub variant: MathVariant, + pub variant: Option, /// Affects the height of exponents. #[internal] @@ -128,7 +129,7 @@ pub struct EquationElem { /// Whether to use italic glyphs. #[internal] #[ghost] - pub italic: Smart, + pub italic: Option, /// A forced class to use for all fragment. #[internal] diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index 2e6d42b13..3d39e2fd2 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -80,6 +80,7 @@ pub fn module() -> Module { math.define_func::(); math.define_func::(); math.define_func::(); + math.define_func::(); math.define_func::(); math.define_func::(); math.define_func::(); diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs index 53242e6e0..6a85fd123 100644 --- a/crates/typst-library/src/math/style.rs +++ b/crates/typst-library/src/math/style.rs @@ -1,4 +1,6 @@ -use crate::foundations::{func, Cast, Content, Smart}; +use codex::styling::MathVariant; + +use crate::foundations::{func, Cast, Content}; use crate::math::EquationElem; /// Bold font style in math. @@ -24,7 +26,7 @@ pub fn upright( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::italic, Smart::Custom(false)) + body.set(EquationElem::italic, Some(false)) } /// Italic font style in math. @@ -35,7 +37,7 @@ pub fn italic( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::italic, Smart::Custom(true)) + body.set(EquationElem::italic, Some(true)) } /// Serif (roman) font style in math. @@ -46,7 +48,7 @@ pub fn serif( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Serif) + body.set(EquationElem::variant, Some(MathVariant::Plain)) } /// Sans-serif font style in math. @@ -59,23 +61,39 @@ pub fn sans( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Sans) + body.set(EquationElem::variant, Some(MathVariant::SansSerif)) } -/// Calligraphic font style in math. +/// Calligraphic (chancery) font style in math. /// /// ```example /// Let $cal(P)$ be the set of ... /// ``` /// -/// This corresponds both to LaTeX's `\mathcal` and `\mathscr` as both of these -/// styles share the same Unicode codepoints. Switching between the styles is -/// thus only possible if supported by the font via -/// [font features]($text.features). +/// This is the default calligraphic/script style for most math fonts. See +/// [`scr`]($math.scr) for more on how to get the other style (roundhand). +#[func(title = "Calligraphic", keywords = ["mathcal", "chancery"])] +pub fn cal( + /// The content to style. + body: Content, +) -> Content { + body.set(EquationElem::variant, Some(MathVariant::Chancery)) +} + +/// Script (roundhand) font style in math. /// -/// For the default math font, the roundhand style is available through the -/// `ss01` feature. Therefore, you could define your own version of `\mathscr` -/// like this: +/// ```example +/// $ scr(S) $ +/// ``` +/// +/// There are two ways that fonts can support differentiating `cal` and `scr`. +/// The first is using Unicode variation sequences. This works out of the box +/// in Typst, however only a few math fonts currently support this. +/// +/// The other way is using [font features]($text.features). For example, the +/// roundhand style might be available in a font through the `ss01` feature. +/// To use it in Typst, you could then define your own version of `scr` like +/// this: /// /// ```example /// #let scr(it) = text( @@ -88,12 +106,12 @@ pub fn sans( /// /// (The box is not conceptually necessary, but unfortunately currently needed /// due to limitations in Typst's text style handling in math.) -#[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])] -pub fn cal( +#[func(title = "Script Style", keywords = ["mathscr", "roundhand"])] +pub fn scr( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Cal) + body.set(EquationElem::variant, Some(MathVariant::Roundhand)) } /// Fraktur font style in math. @@ -106,7 +124,7 @@ pub fn frak( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Frak) + body.set(EquationElem::variant, Some(MathVariant::Fraktur)) } /// Monospace font style in math. @@ -119,7 +137,7 @@ pub fn mono( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Mono) + body.set(EquationElem::variant, Some(MathVariant::Monospace)) } /// Blackboard bold (double-struck) font style in math. @@ -137,7 +155,7 @@ pub fn bb( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Bb) + body.set(EquationElem::variant, Some(MathVariant::DoubleStruck)) } /// Forced display style in math. @@ -240,15 +258,3 @@ pub enum MathSize { /// Math on its own line. Display, } - -/// A mathematical style variant, as defined by Unicode. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Cast, Hash)] -pub enum MathVariant { - #[default] - Serif, - Sans, - Cal, - Frak, - Mono, - Bb, -} diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index 1aaa8f229..b187443e4 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -5,7 +5,7 @@ title: Variants category: math path: ["math"] - filter: ["serif", "sans", "frak", "mono", "bb", "cal"] + filter: ["serif", "sans", "frak", "mono", "bb", "cal", "scr"] details: | Alternate typefaces within formulas. diff --git a/tests/ref/math-style-fallback.png b/tests/ref/math-style-fallback.png new file mode 100644 index 000000000..de0283762 Binary files /dev/null and b/tests/ref/math-style-fallback.png differ diff --git a/tests/ref/math-style-hebrew-exceptions.png b/tests/ref/math-style-hebrew-exceptions.png index 723466e8a..a6f511e0e 100644 Binary files a/tests/ref/math-style-hebrew-exceptions.png and b/tests/ref/math-style-hebrew-exceptions.png differ diff --git a/tests/ref/math-style-script.png b/tests/ref/math-style-script.png new file mode 100644 index 000000000..379d270e7 Binary files /dev/null and b/tests/ref/math-style-script.png differ diff --git a/tests/suite/math/style.typ b/tests/suite/math/style.typ index 1fa2695e6..3ecf856b3 100644 --- a/tests/suite/math/style.typ +++ b/tests/suite/math/style.typ @@ -12,6 +12,15 @@ $A, italic(A), upright(A), bold(A), bold(upright(A)), \ bb("hello") + bold(cal("world")), \ mono("SQRT")(x) wreath mono(123 + 456)$ +--- math-style-fallback --- +// Test how math styles fallback. +$upright(frak(bold(alpha))) = upright(bold(alpha)) \ +bold(mono(ϝ)) = bold(ϝ) \ +sans(Theta) = bold(sans(Theta)) \ +bold(upright(planck)) != planck \ +bb(e) != italic(bb(e)) \ +serif(sans(A)) != serif(A)$ + --- math-style-dotless --- // Test styling dotless i and j. $ dotless.i dotless.j, @@ -21,7 +30,7 @@ $ dotless.i dotless.j, bb(dotless.i) bb(dotless.j), cal(dotless.i) cal(dotless.j), frak(dotless.i) frak(dotless.j), - mono(dotless.i) mono(dotless.j), + mono(dotless.i) mono(dotless.j), bold(frak(dotless.i)) upright(sans(dotless.j)), italic(bb(dotless.i)) frak(sans(dotless.j)) $ @@ -38,7 +47,15 @@ $bb(Gamma) , bb(gamma), bb(Pi), bb(pi), bb(sum)$ --- math-style-hebrew-exceptions --- // Test hebrew exceptions. -$aleph, beth, gimel, daleth$ +$aleph, beth, gimel, daleth$ \ +$upright(aleph), upright(beth), upright(gimel), upright(daleth)$ + +--- math-style-script --- +// Test variation selectors for scr and cal. +$cal(A) scr(A) bold(cal(O)) scr(bold(O))$ + +#show math.equation: set text(font: "Noto Sans Math") +$scr(E) cal(E) bold(scr(Y)) cal(bold(Y))$ --- issue-3650-italic-equation --- _abc $sin(x) "abc"$_ \