From 65d3b039129e26f975c25bf559cffb1acc8881b1 Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Mon, 31 Aug 2026 05:32:14 +0800 Subject: [PATCH] fix: parse inline math inside text commands --- CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs | 67 +++++++++++++++++- .../MathDisplay/Issue205NestedTextMath.png | Bin 0 -> 15520 bytes .../TestRenderingMathData.cs | 3 +- CSharpMath/Atom/LaTeXParser.cs | 27 ++++++- CSharpMath/Atom/LaTeXSettings.cs | 9 ++- 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 CSharpMath.Rendering.Tests/MathDisplay/Issue205NestedTextMath.png diff --git a/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs b/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs index bb6a4b32..5dde09b4 100644 --- a/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs +++ b/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs @@ -1156,6 +1156,71 @@ public void TestText() { Assert.Equal(@"\mathrm{\pounds x\ y}", LaTeXParser.MathListToLaTeX(list).ToString()); } + [Fact] + public void TestInlineMathInsideText() { + var list = ParseLaTeX(@"\text{before $x+1$ after}"); + Assert.Equal(FontStyle.Roman, list[0].FontStyle); + Assert.Equal(FontStyle.Default, list[7].FontStyle); + Assert.Equal(FontStyle.Default, list[8].FontStyle); + Assert.Equal(FontStyle.Default, list[9].FontStyle); + Assert.Equal(FontStyle.Roman, list[10].FontStyle); + Assert.Equal(@"\mathrm{before\ }x+1\mathrm{\ after}", LaTeXParser.MathListToLaTeX(list).ToString()); + } + + [Fact] + public void TestEscapedDollarInsideTextIsLiteralAndRoundTrips() { + var list = ParseLaTeX(@"\text{price \$5}"); + Assert.Contains(list, atom => atom is Ordinary { Nucleus: "$" }); + var serialized = LaTeXParser.MathListToLaTeX(list).ToString(); + Assert.Contains(@"\$", serialized); + Assert.Equal(list, ParseLaTeX(serialized)); + } + + [Fact] + public void TestTextInlineMathCommandsBracesScriptsAndEscapedDollarRoundTrip() { + var input = @"\text{cases: $\frac{n^{2}}{2}$, \mathbf{Roman} and \$ literal}"; + var list = ParseLaTeX(input); + var serialized = LaTeXParser.MathListToLaTeX(list).ToString(); + Assert.Equal(list, ParseLaTeX(serialized)); + } + + [Fact] + public void TestIssue205CasesRoundTrip() { + var input = @"f(n) = \begin{cases} n/2, & \text{if $n$ is even} \\ 3n+1, & \text{if $n$ is odd} \end{cases}"; + var list = ParseLaTeX(input); + Assert.Equal(list, ParseLaTeX(LaTeXParser.MathListToLaTeX(list).ToString())); + } + + [Fact] + public void TestUnbalancedInlineMathInsideTextReportsSourcePosition() { + var input = @"\text{before $x after}"; + var builder = new LaTeXParser(input); + var (list, error) = builder.Build(); + Assert.Null(list); + Assert.Contains("Expected character not found: $", error); + Assert.Equal(input.Length - 1, builder.NextChar); + Assert.EndsWith($"↑ (pos {input.Length - 1})", LaTeXParser.HelpfulErrorMessage(error!, input, builder.NextChar)); + } + + [Fact] + public void TestTextStateIsRestoredWhenTextArgumentFails() { + var builder = new LaTeXParser(@"\text{bad \notacommand}"); + var (_, error) = builder.Build(); + Assert.NotNull(error); + Assert.False(builder.TextMode); + Assert.Equal(FontStyle.Default, builder.CurrentFontStyle); + } + + [Theory] + [InlineData(@"\text{a $$ b}")] + [InlineData(@"\text{a $$$ b}")] + public void TestDisplayDollarDelimitersInsideTextAreRejected(string input) { + var builder = new LaTeXParser(input); + var (_, error) = builder.Build(); + Assert.Contains("Display math delimiters $$ are not allowed inside text", error); + Assert.Equal(input.IndexOf("$$", StringComparison.Ordinal) + 1, builder.NextChar); + } + [Fact] public void TestScriptOrdering() { var list = ParseLaTeX(@"\int_a^b"); @@ -1634,4 +1699,4 @@ public void TestErrorSurrogates() { } } } -} \ No newline at end of file +} diff --git a/CSharpMath.Rendering.Tests/MathDisplay/Issue205NestedTextMath.png b/CSharpMath.Rendering.Tests/MathDisplay/Issue205NestedTextMath.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ffcfca75bea5809582716c13a5a8e6444c66c7 GIT binary patch literal 15520 zcmX|o2{@GB`@TvVLP&O!eJ9ydeZ<%@YB2VZ!B|2_6S5Vuj4f-~Yh)XYwaG3^gd|G_ zV~Ht>?AfyY&-DFW|E{i(Gw*xe^PclO_jBL(6LZH{kBNbYfrf^L$v|Jngofsn3iv(_ zLI?gOY-PjPuobceG#GjRxwBk z%lXf(G}(t`?`As}J+auWBLB@v^FP*8?I!5AK|h0S$(#ON+hdLcpY{w&J(_kN=dSl{ zW*@6Bs4skMYimQW&{ici?asye@6JZ8|IAMmf}GS`dw*(oJnwLv7zJga)dC|PCA_^x z6DHV82@vogN3QPsb{mZS?>Di}D=$Se&86fWh{*J{lUlalj;uE~Uz>bH_EccZe_O>$ z{k&E!Jw0#O7|rLLt5Ja~^=IT;b{!UXnN%mATwQHCzZx(QXwAz@9mk@>DTvZ$;h*1I z$crj;^PkUfG3iEIbxrxr#J_9VDld=s%-S1$*YfOK=x#^d<1MM{+f|8guknQmLc#n)m&x zGjVwbJ$W91`v0aW^E84<>*4d;!HcH5@6TLU@C#DRxHB=%agX@2Sve2?XlbVJ)jlm2bBK5c;X=tEu1%bp%Scikvj#(Bs`w1GEzA!w#JRw6cx<7)t zRJ2y<>+;7N@+FSRA}Ea;+wb6_c>er6wU6ByGQ7t1IopUAc6$;TY%j`R1{9) zIhq@b>(mv{s)cezu4$fwvunZG^^5*N*ib)db|!t#9qlF_`>*UeCSHro`&*KC4{g-=F~@H@ zM(<)UJI%KM0pq;u$X}X;ZtcY7u~SGeF=qaVSKb02$S&WlUM@Hl_GtPMjT>Ikvn;o&2BJx+=Xm$rg0hPTl~qDKD2LvUPh z_Wl?iK5R~tt0U_}1^9 zw>HKHLR6+_%zjMNq-F_1UP&+%4ok}P%{|V!0Bv(pe7e-&OPB3QmXuf3Qi@;QuOF~M zpw82t2%;`(vdBu~`_c%pz6Y{y-kq8#uCA{?t3_5hwLGLmJRmr9GNLZ|&gad?2GdbB zE(7U*rzEg*{R4QsgJK5z#1-K_MT^Nb6DR~HnHinnRGRgp-vuYpj+ydrE@4i>9AmY%9c;CbjodRRfOgQ9()C7VxqgcuTrTt zUoSD8L&c+G*_y7*!15Zto;XPH@qQCWUkJIgpIj@XT5mb>`^Obb@`qe zpMwGyIQ>7&`K@(xld24Rkkmjpb^!z?+Nv7c`Uj`a-8fL7`L?@GDQJGtgh!*e9Mt&C zd2ZV+o&)CxjJ0H@etncKA2fTG8WU)~rQ6(ZQzqG(F79j!UFl(etN4Y^ojS7k|3)H) zzEtX7VvC%$z8(esTTq1?ycMnAA}{2SSkorsY_+GuMYmsE@5bFcfiX|T=`&N~x=a)v z$4U#2R$yd}yhN*2>{P1AP^|>wymM{6pRien5D>T z!=9~0OHT^o&C6Z6XtkPV?w{`ytEmr8tE%~9!+~TD6AXDIVvtF0{TUz-Civ51HT(Nc z3{E09914%XNnE`wenyLCW``9DU*rLy!z$gqnGTw#we4&9#quS1KP+eI!@!w*vmFP* zF>Lsi+eCX$|4y)4!ug#p{r28?JbnUudF_k#xeuW?0=GSWY#J4rswIRALgpUSBW_(; zSnhjmGN3b6*;U=+BIovs@9+_OqHvG>JcIfT^8mWwnZy}YyEXxHVm7iu_{^Cz7H5NP!UXRv^xZvDoZcqB>*sG> z@8YH0Lwd5&o}ddq;XU@mg#Cm6T=%TgLc>aSodNdJ{45*vaxTrafpAaq`aq&?{#!Y3 z`Q|@t(|-z8XBWSgs$mw{TXsLl6P9Z@FY__iU-%-^w-}eC<8h|Yt&7e=02%?}(mG!i z13G>YF^J($p!|nG6yvc9j3_SM-x-gPexL0fbuVT$vO{BB(z19^L%GcK16!xEVWG8VDUbhiZ%6 zU2ITR&4H}*ekncFG*@dtdIoEq0V&ZpHmHIW%Sla)8AfxVzIwBk5Ck9;dpr zlp+WWtuto~Ka+dhEw`^^Ma0;4DPDSS_*Z;bnUmgf+ema(OQ2e&yFc><)Z-y?LH8P$ z47Nu9;oMpVKXX)x^Kd#^Hct0)k;{hq-G@xo1NPH(;6q6T(=qCV0hNNLifo&v+bw(u03WSxN#gLuA}`Z z%(?7pCiJ(Mfnyq6XY6~@mPsRl9CIjc);o%g6gY;FW7fy3UCT+O- z$tlhk(uKF#E>8Mu%Kd3s z4{80`xENrEcd^OX3)`tz3lbQ+B8GK%?Vj13Yjz=C5F#hz`9!tHMf`5Mb(aoouz%A) zp0UsCe&#}MATy_&Ee-Q&Cr%wE=4Y_uOtx4Uaox0%_}>0OfY_X|aF^I(b@x*YKSuZzQS{ zS12)VP-hBDdZ<&u@F&O;G zcl$rHeoOnE*Pr|#9o};7wPn4MiB53AGKnwV5=1kEEPu1%ktQ_<&94QAqFgnlJ!rKg za@?7WzZbc+tUZ(eHuoWx=84Vy`qLy^-+t>Gd1nUtETZ&9AVN%6>Nfc3G`#``9}xRo z$|c8a1x(`or#?f;A1Z7F~qf@dA!EZqEWNc2X%95 zhGvnp6z*5FgTaQSwyb-M=U_YT>V7M>2Rk-i&2I+>q)raFcacYoSUMLKPF4P#1e>d% zwgmHLgs5eneF4@i^?-meDT3z(r*B9xZhS-Qq{dHFHkY8cKdwu0I}6LBKiY*OOymO& z{d;eM;v|~g!{l?;&&`N_sk{%aCS$e@59x1fuG7+d>x?Y2e4Nx5aaKI0j(#Huk%vqjNz0b4r^CfP!&fMM#mFxwezJT!&3+?2-DdzJ9TGah1 zse5CaCf&UZyxW`TQF}Wxw=|d3okqlmVE(l8)RHYJ%(F~Z%437^8T$KHzbm%;VkqAW z`MY$?oTj{nf>bh@4IMwk=u2!=E%j^!)Sq8e>#PJ3Jd9L|gj) z%A!S%Z_V7BPa$}Z5i!ONlRz`etszBL@#&hD-sX?K>}Ic-7v{|wvLvuXhx}=kpfe7e z0O6p}7ji30+8dt16q%NPkJrQp8?*9t8x&|iFTNB#pJ62Sj9(>L1YulJ?tZMxIjAH{ zf^2J^12xXfs-u_Jx$rhYxsu5XfsyZ4Msg(Yh*?(|YVf%Gmq-|vymd_LKZLq%hFo-C zPT>txK4gVf4LQ#f?5X)z&Q>)~a8xVDLF8(^O8su@pP^g+e}5I7peZuWY2PB9Ydf#5sL5y#xUXf1+H~2a|1b$Q$$aucv75Y9x2PW5 zt|*c`z$7hHY2=MC(C%~m>MjVm>C(AmP7mO@0v|J{%F#Hx5-fZnfQymtrIDD8>SeYp z17xg<^quE#dWO0dKIe3NEwy->9!uxchMpQ5;Oe%l%S zW^R(Wg={ZOU`q_3;^VQInKLw#{*?IgPv<-m97qGzGHlAUgRG0#>NP$U^2&2F#D$u1 zMu4>`>uM@PFWRyPJN+bVbY2+_K0%^n+auFxM=<;-uA9{I>nsUF@qUBi`hsa5svqwg zGOYzV*CelSIJIIeQD^!7p~eRf6^h+CGLdv| zcr60P$=6jD{U4jTj^M2+H`lT1!s__5yu(TqXolotx?8YYQrn8{U7MQzxo4no;`Avg z12)}!xU>Ls*njbM{lBkHf1jTi2tC^TANZfo^6CQMfB56V_t#4reBCiSpAf8JWlmCB zEL)})1$wz1g~lB?aM;!O(vSGM_BIzNtrb_9;-RlBSe47fEi8t`Ar8Z{%M~TbLv~##N3x1>uBO%B zD-)6BC(u{ilZ){hE;Z#>g!*H7Frq?oeH)WkQkqA9A6xj$ZxY&6##7@NvdZwc@j7<9 zoT71}=E!L7{l=47EXiNfAULSpcKC=q5uj-opF1hNcs56#&D_Vk_55EiSJ89?q!VDIsQlP?_; z0Rp6r&XWsF#WrIjk}DqM`v@5oF%SO>{OPhQVS?-oQyK~97s|YBiv0jJ@?SbS0##=s zsvahxjcga&|0IWh)uCyM*c`bSdf;Q*62z^hq?o{`Zt_bLO&|7(10LRqnb{sW5#}1+ zuuWPM;tJR_=Zb`c*;96A$|jnxtIHE?zOVo8(h%bCxcEnkIF1uKS?2m4ZzB(?g{yi< z$)_!=9SU)2d5H*Dqp>v|*v4!cS{;M3IzqD!h>#s|ZWFqJ*YNFjhGgQ^6kbCQRgAxK z0MmO8*_GUdax;X4)xSWV_8HD$-d@WLrlV;s=Fk!G)n4Jfx7sA0{Zv?SN}wrNnbDdY8aVMGt)JxxRDK6I0l~$XhQa)767!?iIIp`+Q zi@JSUwXTh96YH127C)yMRKsjl%yP2^#9qz%SlU9NRv)<`f8A)_mzWlLA8wH>G;VW) z%DR+L4ss#gV0DSBk!4k}ihBg9a3@Z%!SInW8?-7pDJSRRW?X{=)}Fj=$lKa@)9wQS zCldNDKKkLKyn*VL%YF7c#DrmzJbuLq%yQ(>(C(#ZvEsf`E3qGrUp42V1Rg^yD(p^=nkfldVRN>YC&=W| zgN5%sDLy`_ilhl8*!`8O2!wESz+$w{=JY$wj`TE@`!#*RIc16!mG1Ib2kCZQc&d6w zNLq7WBuWq~6xU5j^EA!%Me--Kw1N+~2#eGq%R~T4ThfdU}tb>(15F zWmHRwbY;u+CRDSa#rK{$z5#9k zY6!7S2XZ!~vssajc&7P2TTKweb%lqXLc2*RT5W43nN z6O{}vF+_pc@w!qDrf(=K^WN(&C7;j++o>{tgH#bTrK~8^7nf3>HUHN*(yyB6ROkD3 z?UI%Zj&P(5ZQm@rog&khmcu!4eP3pKXXgZs>a@QjB?+nWS{2HTs`=^C=xoV06=u;d z2?-ktQrUjqO5Cpco}^5JZwVdGp8l-~NK?9S zEnEFJQcJ&)j+xGsE^BV_&$wraCgw3Bktq9rL=DQzOJ4OfT8*#kSCYrEIh!+tneJ4! zW&oS`?cDDz=ReUH=9c!)*EF@*B0STaMxGYriyeom@d+_%*Ad8uzaBlRu)=!SnYAnR zKiknbOTP6nZXZe~dA{i|*bdx7$!?wvkDeyqKBA|bw+awjicQ$_z z0%~hsG6oqXfd%Y^7R!I>KSoZPrb}B8z9s1ioS@;m5yH0`vePJ?_!hDh9hQ1ILJ5|& z`LYymh>Ls~-W&x*{jjfr$4j9dNK>BIy%-J5tE8Q*BGFbp3 zU19ceX5j~-7ZQ4kqirSDEz_G%-0qgqun{sG)HrS8eTrR_^&yum(vlnZZw;5c4)nlg z65t$53$B;YfaNJUz8J47%{^t*HoK;k+3zf!=wrjh99894*25X+8hJbP@38;O{UPDKMT26YPw&;$Lt|YqY#qg2zkWXbs(e7s{*Wwo*r74w^fkXBkiy zYxx$AWX0Qg&FoMV_j~Sn(w_KS)A&>U7LOuMqL8u7<7?-e6K3BxV~>KSfNW7jcFi=8 zb$njnzt@r|ffY0`XCTp0EVS!e_QCd}>qyIk@^R?Q)OWtHY!~Uh@g8;2 z{hGdc!o)MCTjB=Q_%=`SBPsEkv+8){Q$oM?aamOz{Kbu2fY;s4A(a^ z256#m4+>>)Fl|r*0ebTEy&1=jK0Vv7yeUT7!rG#+jmHn&9L3;q1Hmi4zTJf8ZR<(z z4yN)rmz4U4zu0p9=jdtP17_X(@1I}9s1fd$F4DGv?%5e_G2@QeJ_vV?;fR zMJ0jqezQrf79sZV_kPHMc#KTn9rl8CmaAvaRx`!sF2FS=BG(Q!2Po~&&iZzr2|odD zHeudX+oK5-vbvx>MVfEhWUp=@6-@}3xIn_5%%29_r^8YSdlh{L3-y$$r1x!`#o$~~ z!VPHjR5CmosvdyoG2gT=fCM&Aw_^npD1ebgDR6yCD{~Zew}M6$-nuOQ!IaFa7&CY9 zEAL2hXV>d!-ir_{L;kOtXM*2va&XbIfi~;B!~_g5r5;z)B4EfYbSxz8HyuvnGUKTP zioK?Qqec#3+ttqGd=(!k^m~SiZnI~x@Xa(5Q&Urm4kclbpPXwfJwTNZ(mGRIXTT$4 zcIbMAis=OWzdfy|xO~oT%K{`F^--JtfAMqUh;k!gsiL)I`4Wb1U{er66;=K{u84<- zLHp6sb)ce|g!+H&mFdgeuZP~q6Ll*lWsLsVzYbVRoJ7gQw`7q5Mj9l^@lyA_tTS3q zfYd=6iEiT&xcpN89`?<*{OEq?wWP!#+L@#6cc$GffV;owHxumV?l|Ej@c8E?+vdMH z8WOM_!nlZKkBOl+sKEpuJ++QKqXbtIw9&1Wv+U)o&@v;Yib~)8n0dyyXt*Hq=BRdN z8O2U@T%RK*>poB;995-3u}(t~u2Zit2pA%!58zr{@;m$6fM1fTg1O*2U}fX7O%R<;ocP zF%wlG>=ZLCt?bbiX~-PzXWVG|{LTv1p)ruos;rrbedE+U^04DSKD!hcmeC;D?A9qF zVwb9H-u+u}w;gCGp+yPO*s%ST#)`K-o*F&46RDBgNXx(O$`L0Py3Cbp@z?!mIJ1wJA)T~S6Lp1ipW7%>wA>ZF%m0sN)>eaSeqcqu4%K`R|ixHA{SGn!H;GrlzFgk@Yp zhbtp=LFGy5gxLiBkiOfNam8gVts+SD`Zm_0=_Gi+s@K<^o~zX$!WLGKR#xX0j;M8o z^xeH)aC|ku5mjI;#v2B@gO?qhgCowJz^qodVbOy;xe~>T@BzCNu?juT3gr_)8!366 zjni9%)M8fJ6F>_Shnt>CjZ`^Dw$M)ckGfMy#j*|^=V_6XjvZYYoKI(ClB7!bnwGTY z9R2(kON!R6wr*ba9e9e9sN0ziT+bvm6~3+VJPV|X_LSS#>1ot_=4gRDIJNy;qwS%F zXCQ>C0uM68(`OOpG47dk2!!u&`y#$G{l$>3hy3x@-Q#CBjyG>~ho8G1&1vU%lz7dwIth8b=eP77mb0SA$mrZFtGVjo&o` zU7@YZM6xyi*?(gBvME_03&y4rFv5*?!=E`f2$Q?p5=Yg?C`V*~Q%0}nBJI`qZn8rU z7AuaVrgytkiT|gSTNiSmY9#mF)8Q7WA2yrnKi($)h>UR_CzCs_3}~L}P%v?OdAxq{ z`02TjwQv9iKpkA3;6wKBvzZPX_lUglH~+@HKndz{X+3<>716Jv{$GrIOW#LH>E%ygSpb&{=UfQSo|a)yvG$l)5Z}(XiL7C}Bu@*Rptt zZz#-s>`txo4v>g%U)y3|Y2LAGT0~S}6G3EYvwuR>{L2#(sUL$lkX?1gJhW@+E}S;7 zadTaA?KMbUXkHDLKRQ}H4!M8sBURS_#u+lp>HTY{V6;n@y0vMmU?V0MJdwOV!UbK8 zl+gJy7^u4r+%~q-09%5A-0JrEre;jZGv%bL*D1FhMyfWQxchhLg?Bf<-?!Mi2M$38 zx9y~J0?`;SN^>9_l+nt_baXME0njh}7qvW2^(letg)@A{;FT#AU_~ z8s_EqO|tZaB|`ZV!~>&`RTU@fm3UA}Jj#lZx)Cr_R^FK%-gOW=3 z#krwT!+(x=43Zr>noG=THU5$!P-@~HO0d4efNmc^*vamDRkueP-txn{mAk-%HQw`CuO zTfS-1b9r`6-DciJmVDx#c7A_?rN6hjB%sn$AcwYQHR`A6ELObg@Ex zq6@$fUZksksOUO>GW-G`;hh%Cd(2etYS}`7bCmUWaOfhvlXIzf7cMFXZgnZk- z4vYrKO(r2}q4xoaWPxEA+P_v}P=richMKMh-`2F*`Kl)cD{j-KeO5$R=A>`Yxx=H zZl`>Adb4RH-~x1(5P-*j9%sT35_Nd&u55g7WzB+ddcMcB_DL7u*2S6{o-vW14A|9a znAY7HKH~9iQC;OLW6KnTyoI_~XAv8VN9nI8i_A7G5jsA^a+-D@gHX+I@$jdf&!3-z zqh0BpN-$b2AWnl_b?Z0oU|L zJOrpVtoejs3h#_-Gae$0#i?PE(|lx>;0xDYpty*cXPRVn;Ou%=RYWn;SwuCSc;L?f z#X8E0gZr)Q`xQM7Efx-rHY=U+=29#UM1aA=yQcznY8RWuqUEZRD|>j3Sn3J|PBT}fMW?@=gv%L5CaZV}EXYr3&AX{4V@Mz^8g{N3dY|tstY&59 zg|kz9N%c=^LUCuz*r;sTHlO@86IQnX=-Vm~CPwxKdSp&K%!|U~ z*IhIcOf^V1&XF*)`$z50nP`y8YQ|-@af<6Bopxd0uf1Z~aDFh?Nx!nrc{j*s*>N^W zyt0=;-YUJ`x2y2YZPrdXt-zT_XBtSIM7X*`%2urkzpTE>u5=1rP2Bg$uNuHLK&1$O zbcrQ=#|V1$IY0Q#HS=z5*()@Vda)h5RM_oHC;sP88}^8+)Dn`aGT8eLOES{VC#>k0 zey5+WzNxVqEb4XXAB4cbvns9h1`h1VXT#z#* zXv$lgTKpCN_YqnS`YZ)TU+L#))k8UHj4fx+k-R}i2q&sNfixK>xCk~0uu1Uo@!6|n zrLDB3swRHva{8C-7oCj9DnEZ~wv#{ddX(=yddJE|nc$>1hXkTo!e@-iUyBUxZoA*LHRwuKPRPx~HpY_8u$$0?!t!!SZ*HjEEH~H?2RM=GXdDz?t8`$+gX9uy`_96aLP4%LsWdSmh zYS<}xyAVgZQNDcnvPpZ_PB6hwnYYS-N6OP5i9kG~sQGlfM{XQE&gFbjRM@PnLo(oD zVkF)!VgwS18sF!fSg7pg^fM|2sp7|H;AGY1uUS}Y?BD`SX$=JV18%GI7Ysm0V%U1C z$msZ*^=HG%knc#3lrNT|R_Ph&IG-xC!*k)c`oJc*K+w!>#7i`@ix@-DOt-gMgopuP z*S-0QMi+?~2jC)UwvTMUNmRXG*OW&zxhe5{z5i*y<>K8UC|M3@F?hUtPc}oCV9QKj zZZK@vda3*+K$U5+$@ObJ@*)N%?$dmI)n8V3fGy4#aoX{0G}`JaXNizXESP+z^CFk~ zNz3_h^sg#4r4T18klz6J1fe<;1sOZ)r_I6Q9v4b50NJ6pg9D_bXuwkdF^hz`ii&2@ z86Npc`;wc`_tQjl81D<*n?S7@$yUrTvYxp;rqkilp#UC_R_ndL<~&2&_Z{XA7T`tf zn62Ed8qwOAp1Y$Erz^bj_L=T26M?6|}Zb^9du ziPw}^K+l^(QHxJ>W0gd8?0Y}b;x^F!P+eBo{Q^8nrOa7yX~5MfX4I;rV^Eo5?Ftcv zZsoUAX{#8W`4n{EMhHu3EMX}c2LXE65po`Zw$lem-+&`(73-Gy9Id`{Wt^{_p+LdW zMI|d~_{rE~GerCh=X^}L1I?}E&Um82?E9aFe@1;(lW}7_I z>YYq@ErVj8VXqNHeSicBw6? zJg9=d8>neM5s?VdAL=WowW61O|M^^gXP2HI*qqxb0O?j2@BiP><}19ESc-vRQzZ*K zGZaQnVL#h;`B`0mC8u8U8+Ca$t%vl)m*O)9R_VU+Pob62mllWeSF5TAt}qHGeCM!w zm~xiu7E(CcGiv!X%1wCqS8cME802P9?)N;IWD%cGxz&X%+^6jJ`IF$CanS*BUnx;E zrwz`2!SJhl^6zFlQN{kI9koemt8LA6G)AgJm$YS;nY+~R%(NJ+EwT@&Wn7F>h~LVv zE24s9e-YoU-7+J4oY+D`kd>}?9<-WeT&^d?Dloe9o3fg;M?PauDsK9)J@v9M!!UfY zH}dBEX+_3-)7L8+s7Rq(luOZs_bLO`hxbA5CM|ssP@nHFU;N+@2~A?T;p;s{WI`a$ z>ZR74-6UQ$fP?{XQGQjLq?MumY}r@hYLOFDD*`7BgJX`h72S(^)h%oD&sHia>ZTPt zs5CcV>+A=aKnN%>t^3DIz8!Cqd7R2;IgdvK=N? zviSb4gQ6zA|g^NxB@FNu{iQ2QTUUK+bC#Mwe@zlZes0&cy+_q?nN# zVfK}_4~Qtmy{KzN|2VAIq$LD4tn!{hK#I`7S)GzhTCMMJw6TG6by>U0xM&;cT=aqaPjn{k`|Ki$!6v-LE+QyrLlPFIpk?{@oNPajwE6s2#fSWlA04-j5 z+A-aE1x5;9mUi&Mn3$FTaB~r4I!&wrVeFP!DO0*u^NHhGQ{I9s9u#7=7H&3-!05+O z<6%6@jYHry*5^|_CG?XI?F!w@r@7>?4(>@Zc6-N&crRk2Fw z72cVf1)FiY!4t2L)vuzlsKe4!ol*AjJ@9FL?Ef$?%YnFdf82NQsvZj&&@2TwWu>A| zDLE-ty*>{@(X3sR%OKzIa?Lt~34T+FC!CrN{w(d;e|4Jpn2MGA(P~!8K-M@Z+Yb9VnQ;;~;i>f>z7Rb>R%i33 zSvi@Fi0UpXIb*^0Q<<3FxQ-mmVl zvL9)y&W+boQ1g@X8ncQZMife@hQM>k`<>Ub|S3 zSAk+VNhy=^RCBK;6~!{=%c&%lGf+uS9+#x2fH9VCkL1*HYw*d&NxTiATZ&7#Af?9I zNvP@ZHOvBe#x;|UOq>m{LlrT(yTHXZYI9LI)Jhvztpe1Fr!FNrhM%IS!fvzVSe8 z9-a2BG)PJkAF9q9|KQ?~g**Irbxof3$+EjltBoAM(hra_q0Wl4X$#jAzloE0aKm?s zB5`sx;Ggr&ywWwSyg_i`(2o0@wQ2nt9zS1rhAZ-ENbB8SzF;$|!1^U)O>9&##wgtt zDY`mA2mqrcj!QDfpJH)exAxFFinn!Yt>}2w9sgZ#)Kob&Pt}c6S;XJ&vy?7GehUTX z$y^AqNeczuEx!`&vE7y%szow}+MAkQ`9n$7WfK%bP$KW#b@^kM{H9*Hj4-f}BfhHs zty~=$H&%PCPzt!&Ar1-9*!y3v$p)}bkxeXm^+#)@AU9XIfO5tTtvz;*9oI<|fuF3i~rEpM>EOgbz#Ai~? zSeNytZ|38r{NR?lxg?jQ6L-GzVIWyhI^AkL#wz3(F&@Y^I3Okf<5MK6HkaXM*$b6< z_U)Ri6`7KTfykt|%(6zqmw3Fd`y_s*+b6qz&vD$kXMYFLU3D+wnX1@y?ucTU4ObHN zNtn%vDTl%T6B4S+3>W$~BjdX%?F+@i=m>=Wtz;Mb2oz*slVhdzk_tc#^AT~LQ*Cy(V-djMcpv>jC<66ziCdP3zQjG0DY0p@@`wQ;WTx}3 z$L7G3JY82C3ws?qSlPgE#;ND(xhrc#wDDzF%}_9GTQ~(>e<4ZSU?}@U#nR(~RiLrn z3rWcKw5})$Lw<NnB*Qe{UogJ18{xCmtl1}J-EAFq0DJbEVRgIq~*RD0!IH_5Mmh1+wEh* z8dL4gOJgLKl1lmFlRdOlPFMmVVM(TJV+Ab!2%!Irf7QcDnBA7v?V-_bo~u{1Sa=5j zxnjyZ)~ODR+eOY*F|zboyM1r`zM><(d0%#n)y?&DSK}*;;D=uf$byFpMxRClni0fk z&O*P`00E9`fmvgB-!NEJeT+2+I?=56OT%t<=wwc7eIw%Gp2=12JILYunC+mG;v00pPM#D#|M4K2AVem0LtP4yc@0Dr4QM zuBwvI-OzA;W&#I>?4^d1&0h@(3XCtV%m3y_P}}Lw_S+^v*q^ju?m(V~^-?%|?^4X-J;|6E~A5B6nL1gsep zT!2kByvI(}5HwP;x-7E3iz6lKsLgnpa;)Cc%8ar!cFV(UY)?6w~&x` z)sHz*b3*TC3L3qMC)Cw@vvQEodB2)*|)zn1eF?0rD1mrG*QJ-9pJaG?>Ut^f&%m= z2?GL8kJC*Y*w_wd|Fw*MA1N^foab)U#r>+0b+T70e*=L6+BKcssh%;_ivb z$4ljPON7SCckf6OyZ=TCPK1HH=pmSRZJTSQarOEE<;IcY;%@o7Y3% BuildInternal(bool oneCharOnly, char stopChar = '\0', M r ??= new MathList(); MathAtom? prevAtom = null; while (HasCharacters) { + if (TextMode && Chars[NextChar] == '$') { + NextChar++; + if (HasCharacters && Chars[NextChar] == '$') + return "Display math delimiters $$ are not allowed inside text"; + var oldTextMode = TextMode; + var oldFontStyle = CurrentFontStyle; + TextMode = false; + CurrentFontStyle = FontStyle.Default; + Result inlineMath; + try { + inlineMath = BuildInternal(false, '$'); + } finally { + TextMode = oldTextMode; + CurrentFontStyle = oldFontStyle; + } + if (inlineMath.Error is string inlineError) return inlineError; + r.Append(inlineMath.Match(value => value, _ => throw new InvalidCodePathException("Inline math unexpectedly failed"))); + prevAtom = r.Atoms.LastOrDefault(); + continue; + } + if (stopChar == '$' && Chars[NextChar] == '}') + return "Expected character not found: $"; MathAtom? atom = null; if (Chars[NextChar] == stopChar && stopChar > '\0') { NextChar++; @@ -702,6 +724,9 @@ static bool MathAtomToLaTeX(MathAtom atom, StringBuilder builder, case { Nucleus: "\u2212" }: builder.Append('-'); break; + case { Nucleus: "$" }: + builder.Append(@"\$"); + break; case { Nucleus: var aNucleus }: builder.Append(aNucleus); break; @@ -731,4 +756,4 @@ public static StringBuilder MathListToLaTeX(MathList mathList, StringBuilder? sb return sb; } } -} \ No newline at end of file +} diff --git a/CSharpMath/Atom/LaTeXSettings.cs b/CSharpMath/Atom/LaTeXSettings.cs index 3ef5599a..a52c9439 100644 --- a/CSharpMath/Atom/LaTeXSettings.cs +++ b/CSharpMath/Atom/LaTeXSettings.cs @@ -364,9 +364,14 @@ public static class LaTeXSettings { var readsToEnd = !command.AsSpan().StartsWithInvariant("math") && !command.AsSpan().StartsWithInvariant("text"); - return (readsToEnd ? parser.ReadUntil(stopChar, accumulate) : parser.ReadArgument()).Bind(r => { + Result result; + try { + result = readsToEnd ? parser.ReadUntil(stopChar, accumulate) : parser.ReadArgument(); + } finally { parser.CurrentFontStyle = oldFontStyle; parser.TextMode = oldSpacesAllowed; + } + return result.Bind(r => { if (readsToEnd) return OkStop(accumulate); else return OkStyled(r); @@ -1189,4 +1194,4 @@ atom is Accent accent // \varsupsetneqq -> ⫌ + U+FE00 (Variation Selector 1) Not dealing with variation selectors, thank you very much }; } -} \ No newline at end of file +}