summaryrefslogtreecommitdiff
path: root/crates/typst-layout/src/math/text.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-layout/src/math/text.rs')
-rw-r--r--crates/typst-layout/src/math/text.rs344
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,
+ })
+}