use comemo::Prehashed; use ttf_parser::gsub::SubstitutionSubtable; use ttf_parser::math::MathValue; use typst::font::{FontStyle, FontWeight}; use typst::model::realize; use typst::syntax::is_newline; use unicode_segmentation::UnicodeSegmentation; use super::*; use crate::text::{tags, BottomEdge, BottomEdgeMetric, TopEdge, TopEdgeMetric}; macro_rules! scaled { ($ctx:expr, text: $text:ident, display: $display:ident $(,)?) => { match $ctx.style.size { MathSize::Display => scaled!($ctx, $display), _ => scaled!($ctx, $text), } }; ($ctx:expr, $name:ident) => { $ctx.constants.$name().scaled($ctx) }; } macro_rules! percent { ($ctx:expr, $name:ident) => { $ctx.constants.$name() as f64 / 100.0 }; } /// The context for math layout. pub struct MathContext<'a, 'b, 'v> { pub vt: &'v mut Vt<'b>, pub regions: Regions<'static>, pub font: &'a Font, pub ttf: &'a ttf_parser::Face<'a>, pub table: ttf_parser::math::Table<'a>, pub constants: ttf_parser::math::Constants<'a>, pub ssty_table: Option>, pub glyphwise_tables: Option>>, pub space_width: Em, pub fragments: Vec, pub local: Styles, pub style: MathStyle, pub size: Abs, outer: StyleChain<'a>, style_stack: Vec<(MathStyle, Abs)>, } impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { pub fn new( vt: &'v mut Vt<'b>, styles: StyleChain<'a>, regions: Regions, font: &'a Font, block: bool, ) -> Self { let math_table = font.ttf().tables().math.unwrap(); let gsub_table = font.ttf().tables().gsub; let constants = math_table.constants.unwrap(); let ssty_table = gsub_table .and_then(|gsub| { gsub.features .find(ttf_parser::Tag::from_bytes(b"ssty")) .and_then(|feature| feature.lookup_indices.get(0)) .and_then(|index| gsub.lookups.get(index)) }) .and_then(|ssty| ssty.subtables.get::(0)) .and_then(|ssty| match ssty { SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs), _ => None, }); let features = tags(styles); let glyphwise_tables = gsub_table.map(|gsub| { features .into_iter() .filter_map(|feature| GlyphwiseSubsts::new(gsub, feature)) .collect() }); let size = TextElem::size_in(styles); let ttf = font.ttf(); let space_width = ttf .glyph_index(' ') .and_then(|id| ttf.glyph_hor_advance(id)) .map(|advance| font.to_em(advance)) .unwrap_or(THICK); let variant = variant(styles); Self { vt, regions: Regions::one(regions.base(), Axes::splat(false)), font, ttf: font.ttf(), table: math_table, constants, ssty_table, glyphwise_tables, space_width, fragments: vec![], local: Styles::new(), style: MathStyle { variant: MathVariant::Serif, size: if block { MathSize::Display } else { MathSize::Text }, class: Smart::Auto, cramped: false, bold: variant.weight >= FontWeight::BOLD, italic: match variant.style { FontStyle::Normal => Smart::Auto, FontStyle::Italic | FontStyle::Oblique => Smart::Custom(true), }, }, size, outer: styles, style_stack: vec![], } } pub fn push(&mut self, fragment: impl Into) { self.fragments.push(fragment.into()); } pub fn extend(&mut self, fragments: Vec) { self.fragments.extend(fragments); } pub fn layout_fragment( &mut self, elem: &dyn LayoutMath, ) -> SourceResult { let row = self.layout_fragments(elem)?; Ok(MathRow::new(row).into_fragment(self)) } pub fn layout_fragments( &mut self, elem: &dyn LayoutMath, ) -> SourceResult> { let prev = std::mem::take(&mut self.fragments); elem.layout_math(self)?; Ok(std::mem::replace(&mut self.fragments, prev)) } pub fn layout_row(&mut self, elem: &dyn LayoutMath) -> SourceResult { let fragments = self.layout_fragments(elem)?; Ok(MathRow::new(fragments)) } pub fn layout_frame(&mut self, elem: &dyn LayoutMath) -> SourceResult { Ok(self.layout_fragment(elem)?.into_frame()) } pub fn layout_box(&mut self, boxed: &BoxElem) -> SourceResult { Ok(boxed .layout(self.vt, self.outer.chain(&self.local), self.regions)? .into_frame()) } pub fn layout_content(&mut self, content: &Content) -> SourceResult { Ok(content .layout(self.vt, self.outer.chain(&self.local), self.regions)? .into_frame()) } pub fn layout_text(&mut self, elem: &TextElem) -> SourceResult { let text = elem.text(); let span = elem.span(); let mut chars = text.chars(); let fragment = if let Some(mut glyph) = chars .next() .filter(|_| chars.next().is_none()) .map(|c| self.style.styled_char(c)) .and_then(|c| GlyphFragment::try_new(self, c, span)) { // A single letter that is available in the math font. match self.style.size { MathSize::Script => { glyph.make_scriptsize(self); } MathSize::ScriptScript => { glyph.make_scriptscriptsize(self); } _ => (), } let class = self.style.class.as_custom().or(glyph.class); if class == Some(MathClass::Large) { let mut variant = if self.style.size == MathSize::Display { let height = scaled!(self, display_operator_min_height); glyph.stretch_vertical(self, height, Abs::zero()) } else { glyph.into_variant() }; // TeXbook p 155. Large operators are always vertically centered on the axis. variant.center_on_axis(self); 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 = self.style.styled_char(c); fragments.push(GlyphFragment::new(self, c, span).into()); } let frame = MathRow::new(fragments).into_frame(self); FrameFragment::new(self, frame).into() } else { // Anything else is handled by Typst's standard text layout. let mut style = self.style; if self.style.italic == Smart::Auto { style = style.with_italic(false); } let text: EcoString = text.chars().map(|c| style.styled_char(c)).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(self.layout_complex_text(piece, span)?.into()); } } let mut frame = MathRow::new(fragments).into_frame(self); let axis = scaled!(self, axis_height); frame.set_baseline(frame.height() / 2.0 + axis); FrameFragment::new(self, frame).into() } else { self.layout_complex_text(&text, span)?.into() } }; Ok(fragment) } pub fn layout_complex_text( &mut self, text: &str, span: Span, ) -> SourceResult { let spaced = text.graphemes(true).nth(1).is_some(); let elem = TextElem::packed(text) .styled(TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds))) .styled(TextElem::set_bottom_edge(BottomEdge::Metric( BottomEdgeMetric::Bounds, ))) .spanned(span); // 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 span = elem.span(); let frame = ParElem::new(vec![Prehashed::new(elem)]) .spanned(span) .layout( self.vt, self.outer.chain(&self.local), false, Size::splat(Abs::inf()), false, )? .into_frame(); Ok(FrameFragment::new(self, frame) .with_class(MathClass::Alphabetic) .with_spaced(spaced)) } pub fn styles(&self) -> StyleChain { self.outer.chain(&self.local) } pub fn realize(&mut self, content: &Content) -> SourceResult> { realize(self.vt, content, self.outer.chain(&self.local)) } pub fn style(&mut self, style: MathStyle) { self.style_stack.push((self.style, self.size)); let base_size = TextElem::size_in(self.styles()) / self.style.size.factor(self); self.size = base_size * style.size.factor(self); self.local.set(TextElem::set_size(TextSize(self.size.into()))); self.local .set(TextElem::set_style(if style.italic == Smart::Custom(true) { FontStyle::Italic } else { FontStyle::Normal })); self.local.set(TextElem::set_weight(if style.bold { FontWeight::BOLD } else { // The normal weight is what we started with. // It's 400 for CM Regular, 450 for CM Book. self.font.info().variant.weight })); self.style = style; } pub fn unstyle(&mut self) { (self.style, self.size) = self.style_stack.pop().unwrap(); self.local.unset(); self.local.unset(); self.local.unset(); } } pub(super) trait Scaled { fn scaled(self, ctx: &MathContext) -> Abs; } impl Scaled for i16 { fn scaled(self, ctx: &MathContext) -> Abs { ctx.font.to_em(self).scaled(ctx) } } impl Scaled for u16 { fn scaled(self, ctx: &MathContext) -> Abs { ctx.font.to_em(self).scaled(ctx) } } impl Scaled for Em { fn scaled(self, ctx: &MathContext) -> Abs { self.at(ctx.size) } } impl Scaled for MathValue<'_> { fn scaled(self, ctx: &MathContext) -> Abs { self.value.scaled(ctx) } }