From 9299685eb41f2e25aa57f8a023ebadfd0d82a3de Mon Sep 17 00:00:00 2001 From: Simone Bortolin Date: Sun, 8 Nov 2026 15:18:19 +0100 Subject: [PATCH 1/2] LaTeX reader: limits of \mathop{x} and of both scripts of \overbrace, \underbrace \mathop of one symbol is a group, to which \limits attaches its scripts (munderover(x, 0, 1)); \overbrace and \underbrace with both scripts are, as for TeX, operators with limits: \underset{x}{\overset{a}{\overbrace{3}}}. The latex2mathml cases issue-125-4-limits, overbrace-c and underbrace-c pass. [no-new-version] Co-Authored-By: Claude Opus 5.5 --- src/SharpEquations/Internal/LaTeX/LaTeXReader.cs | 16 +++++++++++++++- .../LaTeX/LaTeXReaderTests.cs | 6 ++++++ .../LaTeX/Latex2MathMLCorpusTests.cs | 4 ++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs index 50174f4..741cf89 100644 --- a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs +++ b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs @@ -648,6 +648,14 @@ private static string RewriteTeXOnly(string latex) i = afterUnderBase; continue; } + if (cw is "overbrace" or "underbrace" && Argument(tex, j) is var (braced, afterBraced) && afterBraced >= 0 && + Scripts(tex, afterBraced) is var (braceUnder, braceOver, afterBraceScripts) && braceUnder != null && braceOver != null) + { + // \overbrace3^a_x: for TeX the brace is an operator with limits, both scripts under and over the whole construct + result.Append(RewriteTeXOnly("\\underset{" + braceUnder + "}{\\overset{" + braceOver + "}{\\" + cw + "{" + braced + "}}}")); + i = afterBraceScripts; + continue; + } if (cw == "underset" && Arguments(tex, j, 2) is var (underArguments, afterUnderset) && new Text(underArguments[1].Trim()) is var overText && overText.Find("\\overset", 0) == 0 && Arguments(overText, 8, 2) is var (overArguments, afterOverset) && afterOverset == overText.Length) @@ -718,8 +726,14 @@ private static string RewriteTeXOnly(string latex) } if (cw == "mathop" && BracedArgument(tex, j) is var (operatorText, afterOperator) && afterOperator >= 0) { - // \mathop{\mathrm{argmin}}: an operator name + // \mathop{\mathrm{argmin}}: an operator name; \mathop{x}, of one symbol, a group, which takes the limits var name = operatorText.Trim(); + if (Js.Len(name) == 1) + { + result.Append('{').Append(RewriteTeXOnly(name)).Append('}'); + i = afterOperator; + continue; + } if (name.StartsWith("\\mathrm{", StringComparison.Ordinal) && name[name.Length - 1] == '}') name = name.Substring(8, name.Length - 9); result.Append(name.All(char.IsLetter) ? "\\operatorname{" + name + "}" : RewriteTeXOnly(operatorText)); diff --git a/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs b/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs index cd4614c..56c34b7 100644 --- a/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs +++ b/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs @@ -137,6 +137,12 @@ public class LaTeXReaderTests [Arguments(@"\langle a \vert b \rangle", @"\langle a|b\rangle", @"โŸจ๐‘Žโ”‚๐‘โŸฉ")] [Arguments(@"\left. x \right. ", @"x", @"๐‘ฅ")] [Arguments(@"\mathop{\sum}_i", @"\sum_i", @"โˆ‘_๐‘–")] + [Arguments(@"\mathop{x}\limits_0^1", @"\underset{0}{\overset{1}{x}}", @"๐‘ฅโ”ฌ0โ”ด1")] + [Arguments(@"\underset{0}{\overset{1}{x}}", @"\underset{0}{\overset{1}{x}}", @"๐‘ฅโ”ฌ0โ”ด1")] + [Arguments(@"\overbrace3^a_x", @"\underset{x}{\overset{a}{\overbrace{3}}}", @"โž3โ”ฌ๐‘ฅโ”ด๐‘Ž")] + [Arguments(@"\underset{x}{\overset{a}{\overbrace{3}}}", @"\underset{x}{\overset{a}{\overbrace{3}}}", @"โž3โ”ฌ๐‘ฅโ”ด๐‘Ž")] + [Arguments(@"\underbrace{a+b}_n^x+c", @"\underset{n}{\overset{x}{\underbrace{a+b}}}+c", @"โŸ(๐‘Ž+๐‘)โ”ฌ๐‘›โ”ด๐‘ฅ+๐‘")] + [Arguments(@"\underset{n}{\overset{x}{\underbrace{a+b}}}+c", @"\underset{n}{\overset{x}{\underbrace{a+b}}}+c", @"โŸ(๐‘Ž+๐‘)โ”ฌ๐‘›โ”ด๐‘ฅ+๐‘")] [Arguments(@"\xleftarrow{g}", @"\overset{g}{\leftarrow}", @"โ†โ”ด๐‘”")] [Arguments(@"\pmod n", @"(\mod\,n)", @"(modโกโ€‰๐‘›)")] [Arguments(@"\hspace*{2cm}x", @"x", @"๐‘ฅ")] diff --git a/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs b/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs index 98efa05..ca9d9ab 100644 --- a/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs +++ b/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs @@ -198,13 +198,13 @@ private static string Canonical(Equation equation) // LaTeX and amsmath that the reader does not read yet: an operator name with nothing after it (\lg, \liminf, // \injlim, \operatorname*{argmax}_x), which UnicodeMath reads as letters, and the names of amsmath that are two - // words (lim inf); \limits after \mathop; both scripts of \overbrace and \underbrace; \middle between empty + // words (lim inf); \middle between empty // delimiters and bars with nothing between them (|\,|); a font over \text and escaped braces (\mathrm{\{b\}}, // \mathsf{\text{}}, \boldsymbol{1}); \newenvironment and ## in a macro private static readonly HashSet NotReadYet = new HashSet { "lg", "liminf", "limsup", "injlim", "projlim", "varinjlim", "operatornamewithlimits", "operatorname-star", - "issue-125-4-limits", "overbrace-c", "underbrace-c", "middle", "spaces", "mathrm", "mathsf", "boldsymbol", + "middle", "spaces", "mathrm", "mathsf", "boldsymbol", "newenvironment-multi-node", "newcommand-escaped-hash", }; From 463547492eaf32ec8f2ce8d617622e19d4dda93a Mon Sep 17 00:00:00 2001 From: Simone Bortolin Date: Sun, 8 Nov 2026 15:28:55 +0100 Subject: [PATCH 2/2] Merge fixes: one \Box in Renamed, the latex2mathml cases b6eeafc reads out of NotInWordLaTeX [no-new-version] Co-Authored-By: Claude Opus 5.5 --- src/SharpEquations/Internal/LaTeX/LaTeXReader.cs | 2 +- .../LaTeX/Latex2MathMLCorpusTests.cs | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs index 18065c0..3b22f55 100644 --- a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs +++ b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs @@ -378,7 +378,7 @@ private static (string Text, int Next) ScriptArgument(Text tex, int i) ["smallfrown"] = "โŒข", ["smallsmile"] = "โŒฃ", ["thicksim"] = "โˆผ", ["thickapprox"] = "โ‰ˆ", ["varsubsetneqq"] = "โซ‹", ["varsupsetneq"] = "โŠ‹", ["varsupsetneqq"] = "โซŒ", ["bigcirc"] = "\\โ—ฏ", ["centerdot"] = "โฌ", ["degree"] = "ยฐ", ["intop"] = "\\int", ["smallint"] = "\\int", ["diagup"] = "\\โ•ฑ", ["diagdown"] = "\\โ•ฒ", ["thickspace"] = "\\;", - ["And"] = "\\&", ["lvert"] = "|", ["rvert"] = "|", ["Box"] = "\\โ–ก", ["idotsint"] = "\\โˆซโ‹ฏ\\โˆซ", + ["And"] = "\\&", ["lvert"] = "|", ["rvert"] = "|", ["idotsint"] = "\\โˆซโ‹ฏ\\โˆซ", ["mathscr"] = "\\mathcal", ["operatornamewithlimits"] = "\\operatorname", ["mathstrut"] = "\\vphantom{\\(}", }; diff --git a/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs b/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs index aa55505..51c697e 100644 --- a/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs +++ b/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs @@ -180,9 +180,8 @@ private static string Canonical(Equation equation) // commands of MathJax, KaTeX, plain TeX or packages that Word's LaTeX, which the reader follows, does not have: // links, classes, colors, boxes and moves, sizes, \hbox and \verb, \LaTeX, \skew, the primitives of plain TeX - // (\matrix{}, \cr, \eqalign, \displaylines, \above, \atop, \brace) and its font switches (\Bbb, \frak, \scr, and \rm or \tt over text), - // the classes of \mathord and the like, the bra-kets of the physics package, \mathsfit, \argmax, \widecheck, - // the arrows of mathtools + // (\matrix{}, \cr, \eqalign, \displaylines, \above, \atop, \brace) and its font switches (\frak, \scr, \rm over text), + // the classes of \mathord, \mathbin and \mathrel, the bra-kets of the physics package, \mathsfit, \argmax, \widecheck private static readonly HashSet NotInWordLaTeX = new HashSet { "href", "class", "style", "textcolor", "colorbox", "fcolorbox", "raise", "lower", "moveleft", "moveright", @@ -190,10 +189,10 @@ private static string Canonical(Equation equation) "mathchoice-inline", "unicode", "verb", "huge", "large", "normalsize-scriptsize", "small-tiny", "footnotesize", "LaTeX", "LaTeX-with-style", "TeX", "TeX-with-style", "hbox", "hbox-with-math-mode", "hbox-with-backslash-in-text", "fbox", "leftroot-uproot", "skew", "skew-with-braces", "displaylines", "eqalign", "matrix-without-begin-and-end", - "hfil", "above", "abovewithdelims", "atop-and-atopwithdelims", "brace", "brace-1", "oldstyle", "scr", "tt", - "fraktur", "blackboard-bold", "global-fonts", "greatest-common-divisor", - "math-commands-that-currently-does-nothing", "mathbin", "mathord", "mathrel", "mathop", "issue-125-4-mathop", - "bra", "ket", "braket", "mathsfit", "argmax", "widecheck", "xLeftarrow-double", "xleftrightarrow", "xhookleftarrow", + "hfil", "above", "abovewithdelims", "atop-and-atopwithdelims", "brace", "brace-1", "oldstyle", "scr", + "fraktur", "global-fonts", "greatest-common-divisor", + "math-commands-that-currently-does-nothing", "mathbin", "mathord", "mathrel", "bra", "ket", "braket", "mathsfit", + "argmax", "widecheck", }; // LaTeX and amsmath that the reader does not read yet: an operator name with nothing after it (\lg, \liminf,