diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-10-27 19:04:55 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-27 18:04:55 +0000 |
| commit | be7cfc85d08c545abfac08098b7b33b4bd71f37e (patch) | |
| tree | f4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-layout/src/math/text.rs | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-layout/src/math/text.rs')
| -rw-r--r-- | crates/typst-layout/src/math/text.rs | 344 |
1 files changed, 344 insertions, 0 deletions
diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs new file mode 100644 index 00000000..df80b45a --- /dev/null +++ b/crates/typst-layout/src/math/text.rs @@ -0,0 +1,344 @@ +use std::f64::consts::SQRT_2; + +use ecow::{eco_vec, EcoString}; +use typst_library::diag::SourceResult; +use typst_library::foundations::{Packed, StyleChain, StyleVec}; +use typst_library::layout::{Abs, Size}; +use typst_library::math::{EquationElem, MathSize, MathVariant}; +use typst_library::text::{ + BottomEdge, BottomEdgeMetric, TextElem, TextSize, TopEdge, TopEdgeMetric, +}; +use typst_syntax::{is_newline, Span}; +use unicode_math_class::MathClass; +use unicode_segmentation::UnicodeSegmentation; + +use super::{ + scaled_font_size, FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun, +}; + +/// Lays out a [`TextElem`]. +pub fn layout_text( + elem: &Packed<TextElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let text = elem.text(); + let span = elem.span(); + let mut chars = text.chars(); + let math_size = EquationElem::size_in(styles); + + let fragment: MathFragment = if let Some(mut glyph) = chars + .next() + .filter(|_| chars.next().is_none()) + .map(|c| styled_char(styles, c, true)) + .and_then(|c| GlyphFragment::try_new(ctx, styles, c, span)) + { + // A single letter that is available in the math font. + match math_size { + MathSize::Script => { + glyph.make_scriptsize(ctx); + } + MathSize::ScriptScript => { + glyph.make_scriptscriptsize(ctx); + } + _ => (), + } + + if glyph.class == MathClass::Large { + let mut variant = if math_size == MathSize::Display { + let height = scaled!(ctx, styles, display_operator_min_height) + .max(SQRT_2 * glyph.height()); + glyph.stretch_vertical(ctx, height, Abs::zero()) + } else { + glyph.into_variant() + }; + // TeXbook p 155. Large operators are always vertically centered on the axis. + variant.center_on_axis(ctx); + variant.into() + } else { + glyph.into() + } + } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') { + // Numbers aren't that difficult. + let mut fragments = vec![]; + for c in text.chars() { + let c = styled_char(styles, c, false); + fragments.push(GlyphFragment::new(ctx, styles, c, span).into()); + } + let frame = MathRun::new(fragments).into_frame(ctx, styles); + FrameFragment::new(ctx, styles, frame).with_text_like(true).into() + } else { + let local = [ + TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)), + TextElem::set_bottom_edge(BottomEdge::Metric(BottomEdgeMetric::Bounds)), + TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())), + ] + .map(|p| p.wrap()); + + // Anything else is handled by Typst's standard text layout. + let styles = styles.chain(&local); + let text: EcoString = + text.chars().map(|c| styled_char(styles, c, false)).collect(); + if text.contains(is_newline) { + let mut fragments = vec![]; + for (i, piece) in text.split(is_newline).enumerate() { + if i != 0 { + fragments.push(MathFragment::Linebreak); + } + if !piece.is_empty() { + fragments.push(layout_complex_text(piece, ctx, span, styles)?.into()); + } + } + let mut frame = MathRun::new(fragments).into_frame(ctx, styles); + let axis = scaled!(ctx, styles, axis_height); + frame.set_baseline(frame.height() / 2.0 + axis); + FrameFragment::new(ctx, styles, frame).into() + } else { + layout_complex_text(&text, ctx, span, styles)?.into() + } + }; + + ctx.push(fragment); + Ok(()) +} + +/// Layout the given text string into a [`FrameFragment`]. +fn layout_complex_text( + text: &str, + ctx: &mut MathContext, + span: Span, + styles: StyleChain, +) -> SourceResult<FrameFragment> { + // There isn't a natural width for a paragraph in a math environment; + // because it will be placed somewhere probably not at the left margin + // it will overflow. So emulate an `hbox` instead and allow the paragraph + // to extend as far as needed. + let spaced = text.graphemes(true).nth(1).is_some(); + let elem = TextElem::packed(text).spanned(span); + let frame = (ctx.engine.routines.layout_inline)( + ctx.engine, + &StyleVec::wrap(eco_vec![elem]), + ctx.locator.next(&span), + styles, + false, + Size::splat(Abs::inf()), + false, + )? + .into_frame(); + + Ok(FrameFragment::new(ctx, styles, frame) + .with_class(MathClass::Alphabetic) + .with_text_like(true) + .with_spaced(spaced)) +} + +/// Select the correct styled math letter. +/// +/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings> +/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols> +fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char { + use MathVariant::*; + + let variant = EquationElem::variant_in(styles); + let bold = EquationElem::bold_in(styles); + let italic = EquationElem::italic_in(styles).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<char> { + Some(match c { + '〈' => '⟨', + '〉' => '⟩', + '《' => '⟪', + '》' => '⟫', + _ => return None, + }) +} + +fn latin_exception( + c: char, + variant: MathVariant, + bold: bool, + italic: bool, +) -> Option<char> { + 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) => '𝚥', + _ => return None, + }) +} + +fn greek_exception( + c: char, + variant: MathVariant, + bold: bool, + italic: bool, +) -> Option<char> { + use MathVariant::*; + 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, + }) +} |
