summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-01-27 15:09:05 +0100
committerLaurenz <laurmaedje@gmail.com>2023-01-27 15:32:05 +0100
commit2e039cb052fcb768027053cbf02ce396f6d7a6be (patch)
treeb1a1c1da0440805b296e3204fa30cd9666322a0e
parenta59b9fff93f708d5a35d2bf61c3b21efee71b7e9 (diff)
Fix math spacing bugs
-rw-r--r--library/src/math/atom.rs5
-rw-r--r--library/src/math/ctx.rs21
-rw-r--r--library/src/math/fragment.rs63
-rw-r--r--library/src/math/lr.rs4
-rw-r--r--library/src/math/mod.rs15
-rw-r--r--library/src/math/op.rs10
-rw-r--r--library/src/math/row.rs60
-rw-r--r--library/src/math/script.rs4
-rw-r--r--library/src/math/spacing.rs37
-rw-r--r--library/src/math/stretch.rs1
-rw-r--r--src/ide/highlight.rs7
-rw-r--r--src/model/eval.rs10
-rw-r--r--src/model/value.rs1
-rw-r--r--src/syntax/parser.rs7
-rw-r--r--tests/ref/math/matrix.pngbin6079 -> 5875 bytes
-rw-r--r--tests/ref/math/shorthand.pngbin1231 -> 1215 bytes
-rw-r--r--tests/ref/math/simple.pngbin8293 -> 8334 bytes
-rw-r--r--tests/ref/math/syntax.pngbin53626 -> 54038 bytes
-rw-r--r--tests/typ/math/syntax.typ4
19 files changed, 183 insertions, 66 deletions
diff --git a/library/src/math/atom.rs b/library/src/math/atom.rs
index 5b35d289..6d7359bf 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 41299f98..551d6cd7 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<MathFragment>) {
- 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<Frame> {
diff --git a/library/src/math/fragment.rs b/library/src/math/fragment.rs
index d6d55cc3..fef57a0a 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<MathClass> {
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<FrameFragment> for MathFragment {
impl From<Frame> 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<MathClass>,
}
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<MathClass> {
- 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<GlyphId>,
pub frame: Frame,
pub italics_correction: Abs,
-}
-
-impl VariantFragment {
- pub fn class(&self) -> Option<MathClass> {
- unicode_math_class::class(self.c)
- }
+ pub class: Option<MathClass>,
}
#[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 89d12380..d5951050 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 1e7a6580..2cc8fa9e 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::<SpaceNode>() {
+ ctx.push(MathFragment::Space);
return Ok(());
}
@@ -230,6 +232,17 @@ impl LayoutMath for Content {
return Ok(());
}
+ if let Some(node) = self.to::<HNode>() {
+ 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::<SequenceNode>() {
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 cf1f9105..22daee65 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 f7b2b384..f75aed99 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<MathFragment>,
) {
- 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 fragment = fragment.into();
+ if !fragment.participating() {
+ self.0.push(fragment);
+ return;
+ }
- 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;
- }
+ 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 let Some(prev_class) = prev.class() {
- amount += spacing(prev_class, fragment_class, style).at(font_size);
+ 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);
}
+ }
- if !amount.is_zero() {
- self.0.insert(i + 1, MathFragment::Spacing(amount));
+ 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);
- break;
+ 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 671651d9..2c765fbf 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 cf86b698..7083c5e1 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 bd72a769..aee226d1 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 b1df94d1..9261d157 100644
--- a/src/ide/highlight.rs
+++ b/src/ide/highlight.rs
@@ -209,7 +209,12 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
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 91a9aeeb..d52b1272 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<Self::Output> {
- // 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<Self::Output> {
- // 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 ba3a550f..d03911c6 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 d8eeed24..f6ed2f5d 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 3bd17715..56a4db9c 100644
--- a/tests/ref/math/matrix.png
+++ b/tests/ref/math/matrix.png
Binary files differ
diff --git a/tests/ref/math/shorthand.png b/tests/ref/math/shorthand.png
index 840feac2..e53e9465 100644
--- a/tests/ref/math/shorthand.png
+++ b/tests/ref/math/shorthand.png
Binary files differ
diff --git a/tests/ref/math/simple.png b/tests/ref/math/simple.png
index ebd55dcb..4daa52e1 100644
--- a/tests/ref/math/simple.png
+++ b/tests/ref/math/simple.png
Binary files differ
diff --git a/tests/ref/math/syntax.png b/tests/ref/math/syntax.png
index 2497ef49..0b738511 100644
--- a/tests/ref/math/syntax.png
+++ b/tests/ref/math/syntax.png
Binary files differ
diff --git a/tests/typ/math/syntax.typ b/tests/typ/math/syntax.typ
index f18f7edf..80facfb2 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" $