diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index ffbd72667..e61618e08 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -592,10 +592,23 @@ fn math_args(p: &mut Parser) { p.wrap(m, SyntaxKind::Args); } +/// Wrap math function arguments in a "Math" SyntaxKind to combine adjacent expressions +/// or create blank content. +/// +/// We don't wrap when `exprs == 1`, as there is only one expression, so the grouping +/// isn't needed, and this would change the type of the expression from potentially +/// non-content to content. +/// +/// Note that `exprs` might be 0 if we have whitespace or trivia before a comma i.e. +/// `mat(; ,)` or `sin(x, , , ,)`. This would create an empty Math element before that +/// trivia if we called `p.wrap()` -- breaking the expected AST for 2-d arguments -- so +/// we instead manually wrap to our current marker using `p.wrap_within()`. fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, named: Option) { let exprs = p.post_process(arg).filter(|node| node.is::()).count(); if exprs != 1 { - p.wrap(arg, SyntaxKind::Math); + // Convert 0 exprs into a blank math element (so empty arguments are allowed). + // Convert 2+ exprs into a math element (so they become a joined sequence). + p.wrap_within(arg, p.marker(), SyntaxKind::Math); } if let Some(m) = named { diff --git a/tests/ref/issue-3774-math-call-empty-2d-args.png b/tests/ref/issue-3774-math-call-empty-2d-args.png new file mode 100644 index 000000000..ce4b4eb8c Binary files /dev/null and b/tests/ref/issue-3774-math-call-empty-2d-args.png differ diff --git a/tests/ref/math-call-2d-semicolon-priority.png b/tests/ref/math-call-2d-semicolon-priority.png new file mode 100644 index 000000000..18807e0bf Binary files /dev/null and b/tests/ref/math-call-2d-semicolon-priority.png differ diff --git a/tests/ref/math-call-empty-args-non-func.png b/tests/ref/math-call-empty-args-non-func.png new file mode 100644 index 000000000..5ca90df5c Binary files /dev/null and b/tests/ref/math-call-empty-args-non-func.png differ diff --git a/tests/ref/math-call-pass-to-box.png b/tests/ref/math-call-pass-to-box.png new file mode 100644 index 000000000..0ce1b3d08 Binary files /dev/null and b/tests/ref/math-call-pass-to-box.png differ diff --git a/tests/ref/math-shorthandes.png b/tests/ref/math-shorthands.png similarity index 100% rename from tests/ref/math-shorthandes.png rename to tests/ref/math-shorthands.png diff --git a/tests/suite/math/call.typ b/tests/suite/math/call.typ new file mode 100644 index 000000000..9eef16136 --- /dev/null +++ b/tests/suite/math/call.typ @@ -0,0 +1,97 @@ +// Test math function call edge cases. + +// Note: 2d argument calls are tested for matrices in `mat.typ` + +--- math-call-non-func --- +$ pi(a) $ +$ pi(a,) $ +$ pi(a,b) $ +$ pi(a,b,) $ + +--- math-call-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(a)$, "([a])") +#check($args(a,)$, "([a])") +#check($args(a,b)$, "([a], [b])") +#check($args(a,b,)$, "([a], [b])") +#check($args(,a,b,,,)$, "([], [a], [b], [], [])") + +--- math-call-2d-non-func --- +// Error: 6-7 expected content, found array +// Error: 8-9 expected content, found array +$ pi(a;b) $ + +--- math-call-2d-semicolon-priority --- +// If the semicolon directlry follows a hash expression, it terminates that +// instead of indicating 2d arguments. +$ mat(#"math" ; "wins") $ +$ mat(#"code"; "wins") $ + +--- math-call-2d-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(a;b)$, "(([a],), ([b],))") +#check($args(a,b;c)$, "(([a], [b]), ([c],))") +#check($args(a,b;c,d;e,f)$, "(([a], [b]), ([c], [d]), ([e], [f]))") + +--- math-call-2d-repr-structure --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args( a; b; )$, "(([a],), ([b],))") +#check($args(a; ; c)$, "(([a],), ([],), ([c],))") +#check($args(a b,/**/; b)$, "((sequence([a], [ ], [b]), []), ([b],))") +#check($args(a/**/b, ; b)$, "((sequence([a], [b]), []), ([b],))") +#check($args( ;/**/a/**/b/**/; )$, "(([],), (sequence([a], [b]),))") +#check($args( ; , ; )$, "(([],), ([], []))") +#check($args(/**/; // funky whitespace/trivia + , /**/ ;/**/)$, "(([],), ([], []))") + +--- math-call-empty-args-non-func --- +// Trailing commas and empty args introduce blank content in math +$ sin(,x,y,,,) $ +// with whitespace/trivia: +$ sin( ,/**/x/**/, , /**/y, ,/**/, ) $ + +--- math-call-empty-args-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(,x,,y,,)$, "([], [x], [], [y], [])") +// with whitespace/trivia: +#check($args( ,/**/x/**/, , /**/y, ,/**/, )$, "([], [x], [], [y], [], [])") + +--- math-call-value-non-func --- +$ sin(1) $ +// Error: 8-9 expected content, found integer +$ sin(#1) $ + +--- math-call-pass-to-box --- +// When passing to a function, we lose the italic styling if we wrap the content +// in a non-math function unless it's already nested in some math element (lr, +// attach, etc.) +// +// This is not good, so this test should fail and be updated once it is fixed. +#let id(body) = body +#let bx(body) = box(body, stroke: blue+0.5pt, inset: (x:2pt, y:3pt)) +#let eq(body) = math.equation(body) +$ + x y &&quad x (y z) &quad x y^z \ + id(x y) &&quad id(x (y z)) &quad id(x y^z) \ + eq(x y) &&quad eq(x (y z)) &quad eq(x y^z) \ + bx(x y) &&quad bx(x (y z)) &quad bx(x y^z) \ +$ + +--- issue-3774-math-call-empty-2d-args --- +$ mat(;,) $ +// Add some whitespace/trivia: +$ mat(; ,) $ +$ mat(;/**/,) $ +$ mat(; +,) $ +$ mat(;// line comment +,) $ +$ mat( + 1, , ; + ,1, ; + , ,1; +) $ diff --git a/tests/suite/math/syntax.typ b/tests/suite/math/syntax.typ index fcb8b89ea..cd1124c37 100644 --- a/tests/suite/math/syntax.typ +++ b/tests/suite/math/syntax.typ @@ -1,16 +1,10 @@ // Test math syntax. ---- math-call-non-func --- -$ pi(a) $ -$ pi(a,) $ -$ pi(a,b) $ -$ pi(a,b,) $ - --- math-unicode --- // Test Unicode math. $ ∑_(i=0)^ℕ a ∘ b = \u{2211}_(i=0)^NN a compose b $ ---- math-shorthandes --- +--- math-shorthands --- // Test a few shorthands. $ underline(f' : NN -> RR) \ n |-> cases(