From 45245f069570cbbc63eb4120bf5dbb778b7175ef Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:31:45 -0400 Subject: [PATCH] Fix empty 2-d math args with whitespace/trivia (#3786) Co-authored-by: Laurenz --- crates/typst-syntax/src/parser.rs | 15 ++- .../issue-3774-math-call-empty-2d-args.png | Bin 0 -> 757 bytes tests/ref/math-call-2d-semicolon-priority.png | Bin 0 -> 796 bytes tests/ref/math-call-empty-args-non-func.png | Bin 0 -> 630 bytes tests/ref/math-call-pass-to-box.png | Bin 0 -> 2289 bytes ...th-shorthandes.png => math-shorthands.png} | Bin tests/suite/math/call.typ | 97 ++++++++++++++++++ tests/suite/math/syntax.typ | 8 +- 8 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 tests/ref/issue-3774-math-call-empty-2d-args.png create mode 100644 tests/ref/math-call-2d-semicolon-priority.png create mode 100644 tests/ref/math-call-empty-args-non-func.png create mode 100644 tests/ref/math-call-pass-to-box.png rename tests/ref/{math-shorthandes.png => math-shorthands.png} (100%) create mode 100644 tests/suite/math/call.typ 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 0000000000000000000000000000000000000000..ce4b4eb8c27f38a7b9a6b60ccf8af28a55f2f069 GIT binary patch literal 757 zcmVgj=1Kt?|hISZW+a|DSV}NfNg8{y03^q8Qj=IYa_`4Y3-2q@=mjTASQuFez7Fb|` z1r|7M)2pk~9PsuqFuctGcLsrAXFjbywZ_P&Ewj=X3~=`bu+hx`NBe>PC3!LR}GRXr^x!qG7aL55TLJV+w2ne;ymI->e#u)juW%hajZ!ZJ9-v{*VGr(9> zYEdo;fdv*=;4FvZaUOVLZEb=B4mbg4fC09Kfv}YUt}updnV^TOjKKg8dH~NL1ANc} z^c*n2a4F@IOG03Q1r|8V;aH3Z9$Q}@eXnvxCmmb12%Q7AOyL1ns(J`bgS-V9BSNnkFm#s*j zuYn5wVjT1vGZ>AZiQwzMGQ*y0H1P6eWQI|)LISrv*TFAZNZ^Y<0AQ}>>NrfAzd!-+ z9f?6x{8*F#UOEnKXTB4_9}1!E{Cfh}UJ7kD>;$m24%+Ih1aM;o{%^)&LrN8m1aNsV z{$=uyE-oj4Tk7?2eM>IEmP$QbX(52iijdNX6)6>!9mY}5H9Z|ajHB+j3vG8g2w>+G zXuI^90RD6q+D=asz}{2PcGO1z|JJfya_aL@MdPXBuoV3)hN;yZo3}vx008hIF!bbxr)n9G)amfX5r2rM{%mLZ(;WZFA zDh}>90-3RLT)KA~b&s8=7}z$V%1giBkpN@AFgQ~Jr0W>pu@P@+>3Sx2fxMgY96-tx z21mOA=`O}LqPY4{UDxZ|>FJR7Ql1UCqlLk%f#8`vom(!nFR`TMm2531wrDheW#{p@ zF<`YYcm+U6QNkfbRqzHCP`!l0ilR)7MZxMEC$CU-SRV+9~&PG0#s)SgI!4g>=}BTA1WZ-#{PPB6Q_Ia zQ*e??7|h8DfEO7h2P~u9w{h?%QMB)m9s?#45eGYR0DNm2lG2%ihuAV6Ok|vn5BpM_g05>Sci3Zfnk+-r)mjHGV@NBw1a!B zL{@8JMj=n z7B;ZGvSlMccYDT3z3eMeu;*`nb^`_?V9fBs! zV7UV9wO+<58^QCctJqHG%TWN_zi>iR{eWb4n9Y5ThDj=9&0yMwnRml&`XiykI;_M0 aDf|}+yU06|p*ZRQ0000 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5ca90df5c0f0aaec7d06d99df686978c543c8785 GIT binary patch literal 630 zcmV-+0*U>JP)CT2mpg#zs^W%>T zxF>+Aht1cJ_p1CehIbrNx1T=IRswtl6wi*Gy1?O4=H8-Qc3C^v+6_wK8OpY8*J0I{wEC$$jD7CJk6KIoK)d5S z)PR!9cJ9U?D5b4ext8H~?Q5FAM;@#ep7zi*L>1B*l%!znXf_2(fu#!=D9JUc3B2A> z`K<_hs6sZO_Nb3MYR$fsk&>KKn!vT#qGMfLoTakBaY)!8;5Pc)>7TcWi~y!>Yd?-C zo!@g^04-rnVArHi>a}iI@A@rn%ra>+d>VN@yo_95*lYtLV|!!FP0+WVuLo$-;Z-MG zuz*@|T7+=JvKcm#cwqtfIHm)PXKObNYAJTI!1$^%s~=8rs|74z0souuFROLm-ih|P Q5dZ)H07*qoM6N<$f|PJ5sQ>@~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0ce1b3d08f1cce80a54c550a11a419ea9d64c92b GIT binary patch literal 2289 zcmV{r~#?{`~y>`TYO;`}_F&{`&g*_WJ($`T6zw{`mO#_xJbl`2F_w z_V4%o_4W1i^z`%d^YZfY@$vEK^ZfAe@bB;M+9d{`s(WH z>FMd{=;-I?=h^D{=H}+*<>lAt_tfe6;NRch%j5Un z-rn8a-O%9g+}zyT+uO$A_RZex+S=O0-}Twq*~8xT*x1;?-SyYk*Vfk7)z#J1)YQ|{ z)6&w?xY+d3(b3S*(9h4$v)1&^&d$xv&9Kz-%*@Qo%ge3O^2*A}$;rvc$jGSB@x02{ zr_b`o$H&IT#-+~j#l^*>&GDkm@x;W$q0I2Z!^5}5(!#>Rp3CsT!NI`5z?;eNw8GE7 zzrUKu@Uy|rzP`S_y}i7=yp+c8l*R75yStOb@4C9Wxw*NxxVVqP?zgwMwzjs6!0w8` z?zOeGh`{Z%w6wFcvxdIyva+&;z3s8Fv9PePfxPXnudlAIu70`ft*xznxa@tn>#VG- ztE;Q3s;a4}si>%^r>Cc;rlzE%q@$ywqN1Xqp`oClpr4PVoSU1QnwpxK znVFcFn3tEAmX?;4m6eo~l#`Q_l9G~K!0~Ty zD_VeOkJ?Jc!^9C(bTBCpjeQ% zN<}5fEG@AMUF?20|ER-}F&@Wc9P)H(pFg(WIluRu?>Wyo@AKm4=jZo7l9+&tb{btU zs)FfV^0~MflZa>Y8&c_A4h!yT@yr&h!xvE{t7mVeqK?8OMCp!ol{GX45L0buCDD5q zI-wai&c)U1QpUd?rCdI$sv!(tWbj#NV)Wnxd|>pj`&luRM#!959@vVRG6U46wVs5W+O96gksCDEn$ z1Wi$UPCFSyF$E$1Th`C7djeu{(ME9Mlq%@pRN~o^=f~5#EYonKt;Dl1yM#}sW@LGRE z4PH(Y6i!&iAu&IMSd|fs0=8QU4@V z`)LltS1r$NaSq0|a6=U$^PVMZDpkDF4{CCLi3nS?`P~S3n+}T>@19ivo2L9h50%N@ zGk{1!0wRkTWb5AyZ9<)hPvnFZRMIoD`WfTs(oAjY(h3u_DVHHCb5==qIdO~jhnP|JS(ZU(7Q@waaV_DlLq$37jY#UD?gA`zD-FA zBAa9Dg7xf{uq=C)jl{pdWHvP8X3o-;o6{$6iB;E*s%$XeixgUQ7e~hI=c8Z`yB`T> zJEWErt3Jgbm1$tD5ts1Lf~BF#)FyNGOZY6{5K0`Se9f_vHlkz)@L}-1E09;H`@kE) zxOp=<5Wb*xB)+P>x6zQx{LBe&A~LR}y?FX^jynG-Py5Giz?-|zMey*M;^f(-*9w#1ygPd-tiwgEIDz3D zGcK|+yoc?{LwhS6Qi7Y9e6L}}s2bJr!;&#XX6y^74O)eXKrS{DewHi%x3r{9mHEtg<* zXy-7|t=RxXe<4-n0^JH{ePRJX{=EY+T=c04fL*}B!2AV-I0=JI8X>IRunGY4PCoA2 zy>MXh%i({f0I;e|2y0)Mm-a&}z-A3J_5CcdPu(lE>9C8=;o=Z`s-ZbI|sO2 zc4PZ=_5FBr(XBTNt~r^buDs=DFYY*pqt5jB+-(>7zx{rGe!b)`v~cmb_%!#z00000 LNkvXXu0mjfnmXWL literal 0 HcmV?d00001 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(