Fix empty 2-d math args with whitespace/trivia (#3786)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Ian Wrzesinski 2024-04-19 10:31:45 -04:00 committed by GitHub
parent d65d9d0fe6
commit 45245f0695
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 112 additions and 8 deletions

View File

@ -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<Marker>) {
let exprs = p.post_process(arg).filter(|node| node.is::<ast::Expr>()).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 {

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

97
tests/suite/math/call.typ Normal file
View File

@ -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;
) $

View File

@ -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(