Math tests

This commit is contained in:
Laurenz 2023-02-02 14:13:56 +01:00
parent 21dd99926a
commit 5f5c659279
49 changed files with 422 additions and 146 deletions

View File

@ -23,7 +23,7 @@ macro_rules! percent {
}
/// The context for math layout.
pub(super) struct MathContext<'a, 'b, 'v> {
pub struct MathContext<'a, 'b, 'v> {
pub vt: &'v mut Vt<'b>,
pub regions: Regions<'a>,
pub font: &'a Font,

View File

@ -1,5 +1,7 @@
use super::*;
const FRAC_AROUND: Em = Em::new(0.1);
/// # Fraction
/// A mathematical fraction.
///
@ -130,7 +132,7 @@ fn layout(
let denom = ctx.layout_frame(denom)?;
ctx.unstyle();
let around = Em::new(0.1).scaled(ctx);
let around = FRAC_AROUND.scaled(ctx);
let num_gap = (shift_up - axis - num.descent()).max(num_min + thickness / 2.0);
let denom_gap = (shift_down + axis - denom.ascent()).max(denom_min + thickness / 2.0);

View File

@ -1,7 +1,7 @@
use super::*;
#[derive(Debug, Clone)]
pub(super) enum MathFragment {
pub enum MathFragment {
Glyph(GlyphFragment),
Variant(VariantFragment),
Frame(FrameFragment),
@ -118,7 +118,7 @@ impl From<Frame> for MathFragment {
}
#[derive(Debug, Clone, Copy)]
pub(super) struct GlyphFragment {
pub struct GlyphFragment {
pub id: GlyphId,
pub c: char,
pub lang: Lang,

View File

@ -1,7 +1,7 @@
use super::*;
const ROW_GAP: Em = Em::new(0.5);
const COL_GAP: Em = Em::new(0.75);
const COL_GAP: Em = Em::new(0.5);
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
/// # Vector

View File

@ -5,29 +5,29 @@ mod ctx;
mod accent;
mod align;
mod attach;
mod delimited;
mod frac;
mod fragment;
mod lr;
mod matrix;
mod op;
mod root;
mod row;
mod spacing;
mod stack;
mod stretch;
mod style;
mod symbols;
mod underover;
pub use self::accent::*;
pub use self::align::*;
pub use self::attach::*;
pub use self::delimited::*;
pub use self::frac::*;
pub use self::lr::*;
pub use self::matrix::*;
pub use self::op::*;
pub use self::root::*;
pub use self::stack::*;
pub use self::style::*;
pub use self::underover::*;
use ttf_parser::{GlyphId, Rect};
use typst::font::Font;
@ -230,7 +230,7 @@ impl Layout for FormulaNode {
impl Inline for FormulaNode {}
#[capability]
trait LayoutMath {
pub trait LayoutMath {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>;
}

View File

@ -1,5 +1,7 @@
use super::*;
const MIN_INDEX_SHIFT: Em = Em::new(0.35);
/// # Square Root
/// A square root.
///
@ -126,7 +128,7 @@ fn layout(
if let Some(index) = &index {
sqrt_offset = kern_before + index.width() + kern_after;
shift_up.set_max(index.descent() + Em::new(0.35).scaled(ctx));
shift_up.set_max(index.descent() + MIN_INDEX_SHIFT.scaled(ctx));
ascent.set_max(shift_up + index.ascent());
}

View File

@ -2,8 +2,10 @@ use crate::layout::AlignNode;
use super::*;
pub const TIGHT_LEADING: Em = Em::new(0.25);
#[derive(Debug, Default, Clone)]
pub(super) struct MathRow(pub Vec<MathFragment>);
pub struct MathRow(pub Vec<MathFragment>);
impl MathRow {
pub fn new() -> Self {
@ -85,8 +87,12 @@ impl MathRow {
) -> Frame {
if self.0.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) {
let fragments = std::mem::take(&mut self.0);
let leading = if ctx.style.size >= MathSize::Text {
ctx.styles().get(ParNode::LEADING)
} else {
TIGHT_LEADING.scaled(ctx)
};
let leading = ctx.styles().get(ParNode::LEADING) * ctx.style.size.factor(ctx);
let rows: Vec<_> = fragments
.split(|frag| matches!(frag, MathFragment::Linebreak))
.map(|slice| Self(slice.to_vec()))

View File

@ -24,11 +24,8 @@ pub(super) fn spacing(
) -> Em {
use MathClass::*;
let script = style.size <= MathSize::Script;
let (Some(l), Some(r)) = (left.class(), right.class()) else {
return ZERO;
};
match (l, r) {
let class = |frag: &MathFragment| frag.class().unwrap_or(Special);
match (class(left), class(right)) {
// No spacing before punctuation; thin spacing after punctuation, unless
// in script size.
(_, Punctuation) => ZERO,

View File

@ -443,10 +443,6 @@ pub(super) fn styled_char(style: MathStyle, c: char) -> char {
'∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
));
if c == '-' {
return '';
}
if let Some(c) = latin_exception(c, variant, bold, italic) {
return c;
}

View File

@ -919,32 +919,26 @@ impl Eval for ast::FuncCall {
// field access and does not evaluate to a module.
let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee {
let target = access.target();
let method = access.field();
let method_span = method.span();
let method = method.take();
let point = || Tracepoint::Call(Some(method.clone()));
if methods::is_mutating(&method) {
let field = access.field();
let field_span = field.span();
let field = field.take();
let point = || Tracepoint::Call(Some(field.clone()));
if methods::is_mutating(&field) {
let args = args.eval(vm)?;
let value = target.access(vm)?;
let value = if let Value::Module(module) = &value {
module.get(&method).cloned().at(method_span)?
} else {
return methods::call_mut(value, &method, args, span)
let target = target.access(vm)?;
if !matches!(target, Value::Symbol(_) | Value::Module(_)) {
return methods::call_mut(target, &field, args, span)
.trace(vm.world, point, span);
};
(value, args)
}
(target.field(&field).at(field_span)?, args)
} else {
let target = target.eval(vm)?;
let args = args.eval(vm)?;
let value = if let Value::Module(module) = &target {
module.get(&method).cloned().at(method_span)?
} else {
return methods::call(vm, target, &method, args, span)
if !matches!(target, Value::Symbol(_) | Value::Module(_)) {
return methods::call(vm, target, &field, args, span)
.trace(vm.world, point, span);
};
(value, args)
}
(target.field(&field).at(field_span)?, args)
}
} else {
(callee.eval(vm)?, args.eval(vm)?)

View File

@ -426,11 +426,14 @@ node! {
impl Shorthand {
/// A list of all shorthands.
pub const LIST: &[(&'static str, char)] = &[
// Text only.
("~", '\u{00A0}'),
("--", '\u{2013}'),
("---", '\u{2014}'),
("-?", '\u{00AD}'),
("...", '…'),
// Math only.
("-", '\u{2212}'),
("'", ''),
("*", ''),
("!=", '≠'),
("<<", '≪'),
@ -450,6 +453,8 @@ impl Shorthand {
("[|", '⟦'),
("|]", '⟧'),
("||", '‖'),
// Both.
("...", '…'),
];
/// Get the shorthanded character.

View File

@ -380,17 +380,16 @@ impl Lexer<'_> {
'\\' => self.backslash(),
'"' => self.string(),
'*' => SyntaxKind::Shorthand,
'.' if self.s.eat_if("..") => SyntaxKind::Shorthand,
'|' if self.s.eat_if("->") => SyntaxKind::Shorthand,
'|' if self.s.eat_if("=>") => SyntaxKind::Shorthand,
'!' if self.s.eat_if('=') => SyntaxKind::Shorthand,
'<' if self.s.eat_if("<<") => SyntaxKind::Shorthand,
'<' if self.s.eat_if('<') => SyntaxKind::Shorthand,
'>' if self.s.eat_if(">>") => SyntaxKind::Shorthand,
'>' if self.s.eat_if('>') => SyntaxKind::Shorthand,
'<' if self.s.eat_if("=>") => SyntaxKind::Shorthand,
'<' if self.s.eat_if("->") => SyntaxKind::Shorthand,
'!' if self.s.eat_if('=') => SyntaxKind::Shorthand,
'<' if self.s.eat_if('<') => SyntaxKind::Shorthand,
'>' if self.s.eat_if('>') => SyntaxKind::Shorthand,
'<' if self.s.eat_if('=') => SyntaxKind::Shorthand,
'>' if self.s.eat_if('=') => SyntaxKind::Shorthand,
'<' if self.s.eat_if('-') => SyntaxKind::Shorthand,
@ -400,6 +399,7 @@ impl Lexer<'_> {
'[' if self.s.eat_if('|') => SyntaxKind::Shorthand,
'|' if self.s.eat_if(']') => SyntaxKind::Shorthand,
'|' if self.s.eat_if('|') => SyntaxKind::Shorthand,
'*' | '\'' | '-' => SyntaxKind::Shorthand,
'#' => SyntaxKind::Hashtag,
'_' => SyntaxKind::Underscore,

BIN
tests/ref/math/accent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

BIN
tests/ref/math/attach.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
tests/ref/math/cases.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
tests/ref/math/content.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
tests/ref/math/frac.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
tests/ref/math/op.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
tests/ref/math/root.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
tests/ref/math/spacing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
tests/ref/math/vec.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

22
tests/typ/math/accent.typ Normal file
View File

@ -0,0 +1,22 @@
// Test math accents.
---
// Test function call.
$grave(a), acute(b), hat(f), tilde(§), macron(ä), diaer(a), ä, \
breve(\&), dot(!), circle(a), caron(@), arrow(Z), arrow.l(Z)$
---
// Test `accent` function.
$accent(ö, .), accent(v, <-), accent(ZZ, \u{0303})$
---
// Test accent bounds.
$sqrt(tilde(T)) + hat(f)/hat(g)$
---
// Test wide base.
$arrow("ABC" + d), tilde(sum)$
---
// Test high base.
$ tilde(integral), tilde(integral)_a^b, tilde(integral_a^b) $

View File

@ -1,15 +0,0 @@
// Test math accents.
---
#set page(width: auto)
$ grave(a),
acute(a),
hat(a),
tilde(a),
macron(a),
breve(a),
dot(a),
diaer(a),
caron(a),
arrow(a) $

34
tests/typ/math/attach.typ Normal file
View File

@ -0,0 +1,34 @@
// Test top and bottom attachments.
---
// Test basics.
$f_x + t^b + V_1^2
+ attach(A, top: alpha, bottom: beta)$
---
// Test text vs ident parsing.
$pi_1(Y), a_f(x) != a_zeta(x)$
---
// Test associativity and scaling.
$ 1/(V^2^3^4^5) $
---
// Test high subscript and superscript.
$sqrt(a_(1/2)^zeta)$
$sqrt(a_alpha^(1/2))$
$sqrt(a_(1/2)^(3/4))$
---
// Test frame base.
$ (-1)^n + (1/2 + 3)^(-1/2) $
---
// Test limit.
$ lim_(n->infty \ n "grows") sum_(k=0 \ k in NN)^n k $
---
// Test forcing scripts and limits.
$ limits(A)_1^2 != A_1^2 $
$ scripts(sum)_1^2 != sum_1^2 $
$ limits(integral)_a^b != integral_a^b $

9
tests/typ/math/cases.typ Normal file
View File

@ -0,0 +1,9 @@
// Test case distinction.
---
$ f(x, y) := cases(
1 quad &"if" (x dot y)/2 <= 0,
2 &"if" x divides 2,
3 &"if" x in NN,
4 &"else",
) $

View File

@ -0,0 +1,14 @@
// Test arbitrary content in math.
---
// Test images and font fallback.
#let monkey = move(dy: 0.2em, image("/res/monkey.svg", height: 1em))
$ sum_(i=#emoji.apple)^#emoji.apple.red i + monkey/2 $
---
// Test table above fraction.
$ x := #table(columns: 2)[x][y]/mat(1, 2, 3) $
---
// Test non-formula math directly in content.
#math.attach($a$, top: [b])

View File

@ -0,0 +1,38 @@
// Test delimiter matching and scaling.
---
// Test automatic matching.
$ (a) + {b/2} + |a|/2 + (b) $
$f(x/2) < zeta(c^2 + |a + b/2|)$
---
// Test unmatched.
$[1,2[ = [1,2) != zeta\(x/2\) $
---
// Test manual matching.
$ [|a/b|] != lr(|]a/b|]) != [a/b) $
$ lr(| ]1,2\[ + 1/2|) $
---
// Test fence confusion.
$ |x + |y| + z/a| \
|x + lr(|y|) + z/a| $
---
// Test that symbols aren't matched automatically.
$ bracket.l a/b bracket.r
= lr(bracket.l a/b bracket.r) $
---
// Test half LRs.
$ lr(a/b\]) = a = lr(\{a/b) $
---
// Test manual scaling.
$ lr(]sum_(x=1)^n x], size: #70%)
< lr((1, 2), size: #200%) $
---
// Test predefined delimiter pairings.
$floor(x/2), ceil(x/2), abs(x), norm(x)$

25
tests/typ/math/frac.typ Normal file
View File

@ -0,0 +1,25 @@
// Test fractions.
---
// Test that denominator baseline matches in the common case.
$ x = 1/2 = a/(a h) = a/a = a/(1/2) $
---
// Test parenthesis removal.
$ (|x| + |y|)/2 < [1+2]/3 $
---
// Test associativity.
$ 1/2/3 = (1/2)/3 = 1/(2/3) $
---
// Test large fraction.
$ x = (-b plus.minus sqrt(b^2 - 4a c))/(2a) $
---
// Test binomial.
$ binom(circle, square) $
---
// Error: 8-13 missing argument: lower index
$ binom(x^2) $

View File

@ -1,27 +1,26 @@
// Test vectors, matrices, and cases.
// Test matrices.
---
$ v = vec(1, 2+3, 4) $
// Test semicolon syntax.
#set align(center)
$mat() dot
mat(;) dot
mat(1, 2) dot
mat(1, 2;) \
mat(1; 2) dot
mat(1, 2; 3, 4) dot
mat(1 + &2, 1/2; &3, 4)$
---
$ binom(n, 1) = 1/2 n (n-1) $
// Test sparse matrix.
$mat(
1, 2, ..., 10;
2, 2, ..., 10;
dots.v, dots.v, dots.down, dots.v;
10, 10, ..., 10;
)$
---
#set math.vec(delim: "|")
$ vec(1, 2) $
---
$ f(x, y) := cases(
1 "if" (x dot y)/2 <= 0,
2 "if" x in NN,
3 "if" x "is even",
4 "else",
) $
---
// Error: 22-25 expected "(", "[", "{", "|", or "||"
#set math.vec(delim: "%")
---
// Error: 8-13 missing argument: lower index
$ binom(x^2) $
// Test alternative delimiter.
#set math.mat(delim: "[")
$ mat(1, 2; 3, 4) $

View File

@ -0,0 +1,35 @@
// Test multiline math.
---
// Test basic alignment.
$ x &= x + y \
&= x + 2z \
&= sum x dot 2z $
---
// Test text before first alignment point.
$ x + 1 &= a^2 + b^2 \
y &= a + b^2 \
z &= alpha dot beta $
---
// Test space between inner alignment points.
$ a + b &= 2 + 3 &= 5 \
b &= c &= 3 $
---
// Test in case distinction.
$ f := cases(
1 + 2 &"iff" &x,
3 &"if" &y,
) $
---
// Test mixing lines with and some without alignment points.
$ "abc" &= c \
&= d + 1 \
= x $
---
// Test multiline subscript.
$ sum_(n in NN \ n <= 5) n = (5(5+1))/2 = 15 $

21
tests/typ/math/op.typ Normal file
View File

@ -0,0 +1,21 @@
// Test text operators.
---
// Test predefined.
$ max_(1<=n<=m) n $
---
// With or without parens.
$ &sin x + log_2 x \
= &sin(x) + log_2(x) $
---
// Test scripts vs limits.
#set text("Latin Modern Roman")
Discuss $lim_(n->infty) 1/n$ now.
$ lim_(n->infty) 1/n = 0 $
---
// Test custom operator.
$ op("myop", limits: #false)_(x:=1) x \
op("myop", limits: #true)_(x:=1) x $

38
tests/typ/math/root.typ Normal file
View File

@ -0,0 +1,38 @@
// Test roots.
---
// Test root with more than one character.
$A = sqrt(x + y) = c$
---
// Test root size with radicals containing attachments.
$ sqrt(a) quad
sqrt(f) quad
sqrt(q) quad
sqrt(a^2) \
sqrt(n_0) quad
sqrt(b^()) quad
sqrt(b^2) quad
sqrt(q_1^2) $
---
// Test precomposed vs constructed roots.
// 3 and 4 are precomposed.
$sqrt(x)$
$root(2, x)$
$root(3, x)$
$root(4, x)$
$root(5, x)$
---
// Test large bodies
$ sqrt([|x|]^2 + [|y|]^2) < [|z|] $
$ v = sqrt((1/2) / (4/5))
= root(3, (1/2/3) / (4/5/6))
= root(4, ((1/2) / (3/4)) / ((1/2) / (3/4))) $
---
// Test large index.
$ root(2, x) quad
root(3/(2/1), x) quad
root(1/11, x) $

View File

@ -1,4 +0,0 @@
// Test math shorthands.
---
$ f : NN <=> RR, n |-> sqrt(n) $

View File

@ -1,24 +0,0 @@
// Test math formulas.
---
The sum of $a$ and $b$ is $a + b$.
---
We will show that:
$ a^2 + b^2 = c^2 $
---
Prove by induction:
$ sum_(k=0)^n k = (n(n+1))/2 $
---
We know that:
$ floor(x/2) <= ceil(x/2) $
---
// Test that blackboard style looks nice.
$ f: NN -> RR $
---
// Error: 1:3 expected dollar sign
$a

View File

@ -0,0 +1,31 @@
// Test spacing in math formulas.
---
// Test spacing cases.
$ä, +, c, (, )$ \
$=), (+), {times}$
$<, |-|, [=$ \
$a=b, a==b$ \
$-a, +a$ \
$a not b$ \
$a+b, a*b$ \
$sum x, sum(x)$ \
$sum prod x$ \
$f(x), zeta(x), "frac"(x)$
---
// Test ignored vs non-ignored spaces.
$f (x), f(x)$ \
$[a|b], [a | b]$ \
$a"is"b, a "is" b$
---
// Test predefined spacings.
$a thin b, a med b, a thick b, a quad b$ \
$a = thin b$ \
$a - b ident c quad (mod 2)$
---
// Test spacing for set comprehension.
#set page(width: auto)
$ { x in RR | x "is natural" and x < 10 } $

View File

@ -1,15 +1,30 @@
#let part = $ a B pi Delta $
#let kinds = (math.serif, math.sans, math.cal, math.frak, math.mono, math.bb)
#let modifiers = (v => v, math.italic, math.bold, v => math.italic(math.bold(v)))
// Test text styling in math.
#let cells = (sym.triangle.nested, [--], [`italic`], [`bold`], [both])
#for kk in kinds {
cells.push(raw(repr(kk).trim("<function ").trim(">")))
for mm in modifiers {
cells.push($ mm(kk(part)) $)
}
}
---
// Test italic defaults.
$a, A, delta, ϵ, diff, Delta, ϴ$
#set page(width: auto)
#set align(center)
#table(columns: 1 + modifiers.len(), ..cells)
---
// Test forcing a specific style.
$A, italic(A), upright(A), bold(A), bold(upright(A)), \
serif(A), sans(A), cal(A), frak(A), mono(A), bb(A), \
italic(diff), upright(diff), \
bb("hello") + bold(cal("world")), \
mono("SQRT")(x) wreath mono(123 + 456)$
---
// Test a few style exceptions.
$h, bb(N), frak(R), Theta, italic(Theta), sans(Theta), sans(italic(Theta))$
---
// Test font fallback.
$ and 🏳🌈 $
---
// Test text properties.
$text(#red, "time"^2) + sqrt("place")$
---
// Test different font.
#show math.formula: set text(family: "Fira Math")
$ v := vec(1 + 2, 2 - 4, sqrt(3), arrow(x)) + 1 $

View File

@ -1,23 +1,18 @@
#set page(width: auto)
#show <table>: it => table(
columns: 2,
inset: 8pt,
..it.text
.split("\n")
.map(line => (raw(line, lang: "typ"), text("Latin Modern Roman", eval(line))))
.flatten()
)
// Test math syntax.
```
Let $x in NN$ be ...
$ (1 + x/2)^2 $
$ 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" $
$ x < #for i in range(5) [$ #i < $] y $
$ 1 + 2 = #{1 + 2} $
$ A subset.eq.not B $
```
<table>
---
// Test Unicode math.
$ _(i=0)^ a b = \u{2211}_(i=0)^NN a compose b $
---
// Test a few shorthands.
$ underline(f' : NN -> RR) \
n |-> cases(
[|1|] &"if" n >>> 10,
2 * 3 &"if" n != 5,
1 - 0 thick &...,
) $
---
// Error: 1:3 expected dollar sign
$a

View File

@ -0,0 +1,21 @@
// Test under/over things.
---
// Test braces.
$ x = underbrace(
1 + 2 + ... + 5,
underbrace("numbers", x + y)
) $
---
// Test lines and brackets.
$ x = overbracket(
overline(underline(x + y)),
1 + 2 + ... + 5,
) $
---
// Test brackets.
$ underbracket([1, 2/3], "relevant stuff")
arrow.l.r.double.long
overbracket([4/5,6], "irrelevant stuff") $

14
tests/typ/math/vec.typ Normal file
View File

@ -0,0 +1,14 @@
// Test vectors.
---
// Test wide cell.
$ v = vec(1, 2+3, 4) $
---
// Test alternative delimiter.
#set math.vec(delim: "[")
$ vec(1, 2) $
---
// Error: 22-25 expected "(", "[", "{", "|", or "||"
#set math.vec(delim: "%")

View File

@ -108,6 +108,12 @@ function getWebviewContent(pngSrc, refSrc, stdout, stderr) {
}
.flex {
display: flex;
flex-wrap: wrap;
}
.flex > * {
flex-grow: 1;
flex-shrink: 0;
max-width: 100%;
}
</style>
</head>