diff --git a/library/src/math/atom.rs b/library/src/math/atom.rs index 5b35d289a..6d7359bff 100644 --- a/library/src/math/atom.rs +++ b/library/src/math/atom.rs @@ -31,7 +31,7 @@ impl LayoutMath for AtomNode { { // A single letter that is available in the math font. if ctx.style.size == MathSize::Display - && glyph.class() == Some(MathClass::Large) + && glyph.class == Some(MathClass::Large) { let height = scaled!(ctx, display_operator_min_height); ctx.push(glyph.stretch_vertical(ctx, height, Abs::zero())); @@ -49,7 +49,8 @@ impl LayoutMath for AtomNode { ctx.push(frame); } else { // Anything else is handled by Typst's standard text layout. - TextNode(self.0.clone()).pack().layout_math(ctx)?; + let frame = ctx.layout_non_math(&TextNode(self.0.clone()).pack())?; + ctx.push(FrameFragment::new(frame).with_class(MathClass::Alphabetic)); } Ok(()) diff --git a/library/src/math/ctx.rs b/library/src/math/ctx.rs index 41299f985..551d6cd74 100644 --- a/library/src/math/ctx.rs +++ b/library/src/math/ctx.rs @@ -30,6 +30,7 @@ pub(super) struct MathContext<'a, 'b, 'v> { pub ttf: &'a ttf_parser::Face<'a>, pub table: ttf_parser::math::Table<'a>, pub constants: ttf_parser::math::Constants<'a>, + pub space_width: Em, pub fill: Paint, pub lang: Lang, pub row: MathRow, @@ -50,6 +51,14 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { let table = font.ttf().tables().math.unwrap(); let constants = table.constants.unwrap(); let size = styles.get(TextNode::SIZE); + + 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); + Self { vt, outer: styles, @@ -71,6 +80,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { ttf: font.ttf(), table, constants, + space_width, row: MathRow::new(), base_size: size, scaled_size: size, @@ -79,7 +89,16 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { } pub fn push(&mut self, fragment: impl Into) { - self.row.push(self.scaled_size, self.style, fragment); + self.row + .push(self.scaled_size, self.space_width, self.style, fragment); + } + + pub fn extend(&mut self, row: MathRow) { + let mut iter = row.0.into_iter(); + if let Some(first) = iter.next() { + self.push(first); + } + self.row.0.extend(iter); } pub fn layout_non_math(&mut self, content: &Content) -> SourceResult { diff --git a/library/src/math/fragment.rs b/library/src/math/fragment.rs index d6d55cc33..fef57a0ad 100644 --- a/library/src/math/fragment.rs +++ b/library/src/math/fragment.rs @@ -6,8 +6,9 @@ pub(super) enum MathFragment { Variant(VariantFragment), Frame(FrameFragment), Spacing(Abs), - Align, + Space, Linebreak, + Align, } impl MathFragment { @@ -54,13 +55,26 @@ impl MathFragment { pub fn class(&self) -> Option { match self { - Self::Glyph(glyph) => glyph.class(), - Self::Variant(variant) => variant.class(), + Self::Glyph(glyph) => glyph.class, + Self::Variant(variant) => variant.class, Self::Frame(fragment) => Some(fragment.class), _ => None, } } + pub fn set_class(&mut self, class: MathClass) { + match self { + Self::Glyph(glyph) => glyph.class = Some(class), + Self::Variant(variant) => variant.class = Some(class), + Self::Frame(fragment) => fragment.class = class, + _ => {} + } + } + + pub fn participating(&self) -> bool { + !matches!(self, Self::Space | Self::Spacing(_) | Self::Align) + } + pub fn italics_correction(&self) -> Abs { match self { Self::Glyph(glyph) => glyph.italics_correction, @@ -99,7 +113,7 @@ impl From for MathFragment { impl From for MathFragment { fn from(frame: Frame) -> Self { - Self::Frame(FrameFragment { frame, class: MathClass::Normal, limits: false }) + Self::Frame(FrameFragment::new(frame)) } } @@ -112,6 +126,7 @@ pub(super) struct GlyphFragment { pub ascent: Abs, pub descent: Abs, pub italics_correction: Abs, + pub class: Option, } impl GlyphFragment { @@ -144,6 +159,10 @@ impl GlyphFragment { ascent: bbox.y_max.scaled(ctx), descent: -bbox.y_min.scaled(ctx), italics_correction: italics, + class: match c { + ':' => Some(MathClass::Relation), + _ => unicode_math_class::class(c), + }, } } @@ -151,16 +170,13 @@ impl GlyphFragment { self.ascent + self.descent } - pub fn class(&self) -> Option { - unicode_math_class::class(self.c) - } - pub fn to_variant(&self, ctx: &MathContext) -> VariantFragment { VariantFragment { c: self.c, id: Some(self.id), frame: self.to_frame(ctx), italics_correction: self.italics_correction, + class: self.class, } } @@ -191,12 +207,7 @@ pub struct VariantFragment { pub id: Option, pub frame: Frame, pub italics_correction: Abs, -} - -impl VariantFragment { - pub fn class(&self) -> Option { - unicode_math_class::class(self.c) - } + pub class: Option, } #[derive(Debug, Clone)] @@ -204,6 +215,30 @@ pub struct FrameFragment { pub frame: Frame, pub class: MathClass, pub limits: bool, + pub spaced: bool, +} + +impl FrameFragment { + pub fn new(frame: Frame) -> Self { + Self { + frame, + class: MathClass::Normal, + limits: false, + spaced: false, + } + } + + pub fn with_class(self, class: MathClass) -> Self { + Self { class, ..self } + } + + pub fn with_limits(self, limits: bool) -> Self { + Self { limits, ..self } + } + + pub fn with_spaced(self, spaced: bool) -> Self { + Self { spaced, ..self } + } } /// Look up the italics correction for a glyph. diff --git a/library/src/math/lr.rs b/library/src/math/lr.rs index 89d123808..d5951050f 100644 --- a/library/src/math/lr.rs +++ b/library/src/math/lr.rs @@ -69,9 +69,7 @@ impl LayoutMath for LrNode { } } - for fragment in row.0 { - ctx.push(fragment); - } + ctx.extend(row); Ok(()) } diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 1e7a65807..2cc8fa9e8 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -43,6 +43,7 @@ use self::row::*; use self::spacing::*; use crate::layout::HNode; use crate::layout::ParNode; +use crate::layout::Spacing; use crate::prelude::*; use crate::text::LinebreakNode; use crate::text::TextNode; @@ -222,6 +223,7 @@ impl LayoutMath for FormulaNode { impl LayoutMath for Content { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { if self.is::() { + ctx.push(MathFragment::Space); return Ok(()); } @@ -230,6 +232,17 @@ impl LayoutMath for Content { return Ok(()); } + if let Some(node) = self.to::() { + if let Spacing::Relative(rel) = node.amount { + if rel.rel.is_zero() { + ctx.push(MathFragment::Spacing( + rel.abs.resolve(ctx.outer.chain(&ctx.map)), + )); + } + } + return Ok(()); + } + if let Some(node) = self.to::() { for child in &node.0 { child.layout_math(ctx)?; @@ -242,7 +255,7 @@ impl LayoutMath for Content { } let frame = ctx.layout_non_math(self)?; - ctx.push(frame); + ctx.push(FrameFragment::new(frame).with_spaced(true)); Ok(()) } diff --git a/library/src/math/op.rs b/library/src/math/op.rs index cf1f91058..22daee658 100644 --- a/library/src/math/op.rs +++ b/library/src/math/op.rs @@ -39,11 +39,11 @@ impl OpNode { impl LayoutMath for OpNode { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { let frame = ctx.layout_non_math(&TextNode(self.text.clone()).pack())?; - ctx.push(FrameFragment { - frame, - class: MathClass::Large, - limits: self.limits, - }); + ctx.push( + FrameFragment::new(frame) + .with_class(MathClass::Large) + .with_limits(self.limits), + ); Ok(()) } } diff --git a/library/src/math/row.rs b/library/src/math/row.rs index f7b2b3844..f75aed99e 100644 --- a/library/src/math/row.rs +++ b/library/src/math/row.rs @@ -15,36 +15,60 @@ impl MathRow { pub fn push( &mut self, font_size: Abs, + space_width: Em, style: MathStyle, fragment: impl Into, ) { - let fragment = fragment.into(); - if let Some(fragment_class) = fragment.class() { - for (i, prev) in self.0.iter().enumerate().rev() { - if matches!(prev, MathFragment::Align) { - continue; - } - - let mut amount = Abs::zero(); - if let MathFragment::Glyph(glyph) = *prev { - if !glyph.italics_correction.is_zero() - && fragment_class != MathClass::Alphabetic - { - amount += glyph.italics_correction; - } - } - - if let Some(prev_class) = prev.class() { - amount += spacing(prev_class, fragment_class, style).at(font_size); - } - - if !amount.is_zero() { - self.0.insert(i + 1, MathFragment::Spacing(amount)); - } - - break; - } + let mut fragment = fragment.into(); + if !fragment.participating() { + self.0.push(fragment); + return; } + + let mut space = false; + for (i, prev) in self.0.iter().enumerate().rev() { + if !prev.participating() { + space |= matches!(prev, MathFragment::Space); + if matches!(prev, MathFragment::Spacing(_)) { + break; + } + continue; + } + + if fragment.class() == Some(MathClass::Vary) { + if matches!( + prev.class(), + Some( + MathClass::Normal + | MathClass::Alphabetic + | MathClass::Binary + | MathClass::Closing + | MathClass::Fence + | MathClass::Relation + ) + ) { + fragment.set_class(MathClass::Binary); + } + } + + let mut amount = Abs::zero(); + if let MathFragment::Glyph(glyph) = *prev { + if !glyph.italics_correction.is_zero() + && fragment.class() != Some(MathClass::Alphabetic) + { + amount += glyph.italics_correction; + } + } + + amount += spacing(prev, &fragment, style, space, space_width).at(font_size); + + if !amount.is_zero() { + self.0.insert(i + 1, MathFragment::Spacing(amount)); + } + + break; + } + self.0.push(fragment); } diff --git a/library/src/math/script.rs b/library/src/math/script.rs index 671651d9c..2c765fbf1 100644 --- a/library/src/math/script.rs +++ b/library/src/math/script.rs @@ -139,7 +139,7 @@ fn scripts( frame.push_frame(base_pos, base.to_frame(ctx)); frame.push_frame(sub_pos, sub); frame.push_frame(sup_pos, sup); - ctx.push(FrameFragment { frame, class, limits: false }); + ctx.push(FrameFragment::new(frame).with_class(class)); Ok(()) } @@ -172,7 +172,7 @@ fn limits( frame.push_frame(base_pos, base.to_frame(ctx)); frame.push_frame(sub_pos, sub); frame.push_frame(sup_pos, sup); - ctx.push(FrameFragment { frame, class, limits: false }); + ctx.push(FrameFragment::new(frame).with_class(class)); Ok(()) } diff --git a/library/src/math/spacing.rs b/library/src/math/spacing.rs index cf86b6988..7083c5e10 100644 --- a/library/src/math/spacing.rs +++ b/library/src/math/spacing.rs @@ -1,10 +1,10 @@ use super::*; -const ZERO: Em = Em::zero(); -const THIN: Em = Em::new(1.0 / 6.0); -const MEDIUM: Em = Em::new(2.0 / 9.0); -const THICK: Em = Em::new(5.0 / 18.0); -const QUAD: Em = Em::new(1.0); +pub(super) const ZERO: Em = Em::zero(); +pub(super) const THIN: Em = Em::new(1.0 / 6.0); +pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0); +pub(super) const THICK: Em = Em::new(5.0 / 18.0); +pub(super) const QUAD: Em = Em::new(1.0); /// Hook up all spacings. pub(super) fn define(math: &mut Scope) { @@ -15,10 +15,20 @@ pub(super) fn define(math: &mut Scope) { } /// Determine the spacing between two fragments in a given style. -pub(super) fn spacing(left: MathClass, right: MathClass, style: MathStyle) -> Em { +pub(super) fn spacing( + left: &MathFragment, + right: &MathFragment, + style: MathStyle, + space: bool, + space_width: Em, +) -> Em { use MathClass::*; let script = style.size <= MathSize::Script; - match (left, right) { + let (Some(l), Some(r)) = (left.class(), right.class()) else { + return ZERO; + }; + + match (l, r) { // No spacing before punctuation; thin spacing after punctuation, unless // in script size. (_, Punctuation) => ZERO, @@ -33,12 +43,23 @@ pub(super) fn spacing(left: MathClass, right: MathClass, style: MathStyle) -> Em (Relation, _) | (_, Relation) if !script => THICK, // Medium spacing around binary operators, unless in script size. - (Vary | Binary, _) | (_, Vary | Binary) if !script => MEDIUM, + (Binary, _) | (_, Binary) if !script => MEDIUM, // Thin spacing around large operators, unless next to a delimiter. (Large, Opening | Fence) | (Closing | Fence, Large) => ZERO, (Large, _) | (_, Large) => THIN, + // Spacing around spaced frames. + _ if space && (is_spaced(left) || is_spaced(right)) => space_width, + _ => ZERO, } } + +/// Whether this fragment should react to adjacent spaces. +fn is_spaced(fragment: &MathFragment) -> bool { + match fragment { + MathFragment::Frame(frame) => frame.spaced, + _ => fragment.class() == Some(MathClass::Fence), + } +} diff --git a/library/src/math/stretch.rs b/library/src/math/stretch.rs index bd72a7693..aee226d11 100644 --- a/library/src/math/stretch.rs +++ b/library/src/math/stretch.rs @@ -178,6 +178,7 @@ fn assemble( id: None, frame, italics_correction: Abs::zero(), + class: base.class, } } diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs index b1df94d1f..9261d1570 100644 --- a/src/ide/highlight.rs +++ b/src/ide/highlight.rs @@ -209,7 +209,12 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::Unary => None, SyntaxKind::Binary => None, SyntaxKind::FieldAccess => match node.parent_kind() { - Some(SyntaxKind::Markup | SyntaxKind::Math) => Some(Category::Interpolated), + Some( + SyntaxKind::Markup + | SyntaxKind::Math + | SyntaxKind::MathFrac + | SyntaxKind::MathScript, + ) => Some(Category::Interpolated), Some(SyntaxKind::FieldAccess) => node.parent().and_then(highlight), _ => None, }, diff --git a/src/model/eval.rs b/src/model/eval.rs index 91a9aeeb0..d52b1272f 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -9,7 +9,7 @@ use unicode_segmentation::UnicodeSegmentation; use super::{ methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Func, Label, - LangItems, Module, Recipe, Scopes, Selector, StyleMap, Transform, Value, + LangItems, Module, Recipe, Scopes, Selector, StyleMap, Symbol, Transform, Value, }; use crate::diag::{ bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint, @@ -421,9 +421,7 @@ impl Eval for ast::Escape { type Output = Value; fn eval(&self, _: &mut Vm) -> SourceResult { - // This can be in markup and math, going through a string ensure - // that either text or atom is picked. - Ok(Value::Str(self.get().into())) + Ok(Value::Symbol(Symbol::new(self.get()))) } } @@ -431,9 +429,7 @@ impl Eval for ast::Shorthand { type Output = Value; fn eval(&self, _: &mut Vm) -> SourceResult { - // This can be in markup and math, going through a string ensure - // that either text or atom is picked. - Ok(Value::Str(self.get().into())) + Ok(Value::Symbol(Symbol::new(self.get()))) } } diff --git a/src/model/value.rs b/src/model/value.rs index ba3a550f1..d03911c61 100644 --- a/src/model/value.rs +++ b/src/model/value.rs @@ -151,7 +151,6 @@ impl Value { Self::Int(v) => item!(math_atom)(format_eco!("{}", v)), Self::Float(v) => item!(math_atom)(format_eco!("{}", v)), Self::Symbol(v) => item!(math_atom)(v.get().into()), - Self::Str(v) => item!(math_atom)(v.into()), _ => self.display(), } } diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index d8eeed241..f6ed2f5d7 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -308,7 +308,12 @@ fn math_delimited(p: &mut Parser, stop: MathClass) { p.eat(); let m2 = p.marker(); while !p.eof() && !p.at(SyntaxKind::Dollar) { - if math_class(p.current_text()) == Some(stop) { + let class = math_class(p.current_text()); + if stop == MathClass::Fence && class == Some(MathClass::Closing) { + break; + } + + if class == Some(stop) { p.wrap(m2, SyntaxKind::Math); p.eat(); p.wrap(m, SyntaxKind::MathDelimited); diff --git a/tests/ref/math/matrix.png b/tests/ref/math/matrix.png index 3bd177152..56a4db9c3 100644 Binary files a/tests/ref/math/matrix.png and b/tests/ref/math/matrix.png differ diff --git a/tests/ref/math/shorthand.png b/tests/ref/math/shorthand.png index 840feac26..e53e94652 100644 Binary files a/tests/ref/math/shorthand.png and b/tests/ref/math/shorthand.png differ diff --git a/tests/ref/math/simple.png b/tests/ref/math/simple.png index ebd55dcb4..4daa52e16 100644 Binary files a/tests/ref/math/simple.png and b/tests/ref/math/simple.png differ diff --git a/tests/ref/math/syntax.png b/tests/ref/math/syntax.png index 2497ef49e..0b7385117 100644 Binary files a/tests/ref/math/syntax.png and b/tests/ref/math/syntax.png differ diff --git a/tests/typ/math/syntax.typ b/tests/typ/math/syntax.typ index f18f7edf5..80facfb23 100644 --- a/tests/typ/math/syntax.typ +++ b/tests/typ/math/syntax.typ @@ -12,8 +12,8 @@ ``` Let $x in NN$ be ... $ (1 + x/2)^2 $ -$ x arrow:l y $ -$ sum_(n=1)^mu 1 + (2pi (5 + n)) / k $ +$ x arrow.l y $ +$ sum_(n=1)^mu 1 + (2pi(5 + n)) / k $ $ { x in RR | x "is natural" and x < 10 } $ $ sqrt(x^2) = frac(x, 1) $ $ "profit" = "income" - "expenses" $