From af54adf5b4682fc63bc2f7306b2fbba9e95d05fa Mon Sep 17 00:00:00 2001 From: Simone Bortolin Date: Mon, 9 Nov 2026 23:48:10 +0100 Subject: [PATCH] LaTeX reader: the operator names of amsmath (\lg, \liminf, \varinjlim, \operatorname*) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - \lg is a function name like \log - a name that UnicodeMath does not know, with the function apply sign and scripts but nothing to apply it to (argmax⁡_𝑥, argmax⁡┬𝑥), is the name with its scripts, not its letters; so is a lone lg⁡ or lim inf⁡, as main reads NaN⁡; the UnicodeMath writer writes the sign after such a name when nothing follows it, so that it reads back - \liminf, \limsup, \injlim, \projlim: one name with the thin space of TeX (lim inf), which UnicodeMath reads like lim and the LaTeX writer writes back as \liminf... - \varinjlim and \varprojlim: lim with an arrow under it, written back as such; \varliminf and \varlimsup: lim underlined and overlined - the limits of \operatorname*, \operatornamewithlimits, the names above and \lim go under them in a display equation, also with nothing after - seven latex2mathml snapshots corrected (tests/data/README.md), the ids out of NotReadYet, regression cases in ReadsOperatorNamesOfAmsmath; the ids b6eeafc reads out of the lists main kept them in Co-Authored-By: Claude Opus 5.5 --- .../Internal/LaTeX/LaTeXReader.cs | 50 ++++++++++++++++++- .../Internal/LaTeX/LaTeXSerializer.cs | 33 +++++++++++- .../UnicodeMath/UnicodeMathGrammar.parlot.cs | 40 ++++++++++++--- .../UnicodeMath/UnicodeMathSerializer.cs | 29 ++++++++++- .../LaTeX/LaTeXReaderTests.cs | 31 ++++++++++++ .../LaTeX/Latex2MathMLCorpusTests.cs | 24 ++++----- tests/data/README.md | 6 ++- tests/data/latex/latex2mathml-converter.json | 14 +++--- 8 files changed, 194 insertions(+), 33 deletions(-) diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs index a05cc75..1327c8e 100644 --- a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs +++ b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs @@ -291,6 +291,23 @@ private string Limits(string latex) i = afterScripts; continue; } + if (LimitName(tex, cw, next, display) is var (limitName, afterName) && limitName != null) + { + // the scripts of an operator name with limits go under and over it in a display equation + result.Append(limitName); + i = afterName; + if (display && NonBlank(tex, i) is "_" or "^") + { + var (under, over, afterScripts) = Scripts(tex, i); + result.Append(UnderOver(under, over)); + i = afterScripts; + // UnderOver ends the limits with a space + while (tex[i] == " ") + i++; + } + summation = -1; + continue; + } var token = tex.Sub(i, next); var isSummation = Summations.Contains(cw.Length > 0 ? cw : tex[i]); result.Append(token); @@ -308,6 +325,37 @@ private string Limits(string latex) return result.ToString(); } + // the operator names of amsmath with limits: two words with a thin space between them, as UnicodeMath reads them, + // or lim with an arrow or a bar under or over it + private static readonly Dictionary LimitNames = new Dictionary(StringComparer.Ordinal) + { + ["liminf"] = "lim\u2009inf\u2061", ["limsup"] = "lim\u2009sup\u2061", ["injlim"] = "inj\u2009lim\u2061", + ["projlim"] = "proj\u2009lim\u2061", ["varinjlim"] = "{\\underset{\\rightarrow}{\\lim}}", + ["varprojlim"] = "{\\underset{\\leftarrow}{\\lim}}", ["varliminf"] = "{\\underline{\\lim}}", + ["varlimsup"] = "{\\overline{\\lim}}", + }; + + /// + /// An operator name with limits at the index after the control word (\liminf, \operatorname*{argmax}, + /// \operatornamewithlimits{argmax}), or \lim in a display equation, as the reader reads it, and the index after it; + /// null if none. + /// + private static (string Name, int Next) LimitName(Text tex, string cw, int next, bool display) + { + // lim, whose limit goes under it in a display equation also with nothing after it (lim┬𝑥) + if (cw == "lim" && display) + return (" lim", next); + if (LimitNames.TryGetValue(cw, out var name)) + return (name, next); + if (!(cw == "operatornamewithlimits" || cw == "operatorname" && tex[next] == "*")) + return (null, next); + var (operatorName, afterName) = BracedArgument(tex, cw == "operatorname" ? next + 1 : next); + if (afterName < 0) + return (null, next); + // a name that UnicodeMath knows (lim) is read without the function apply sign, which would part it from its limits + return (UnicodeMathText.IsFunctionName(operatorName.Trim()) ? " " + operatorName.Trim() : "\\operatorname{" + operatorName + "}", afterName); + } + // the limits end at a space, which parts them from the operand: ∑┬𝑘 (−1)^𝑘, not ∑┬(𝑘(−1)) private static string UnderOver(string under, string over) => (under == null ? "" : "┬" + ScriptGroup(under)) + (over == null ? "" : "┴" + ScriptGroup(over)) + " "; @@ -370,7 +418,7 @@ private static (string Text, int Next) ScriptArgument(Text tex, int i) { ["dfrac"] = "\\frac", ["tfrac"] = "\\frac", ["cfrac"] = "\\frac", ["dbinom"] = "\\binom", ["tbinom"] = "\\binom", ["overrightarrow"] = "\\vec", ["Vert"] = "‖", ["lVert"] = "‖", ["rVert"] = "‖", ["bmod"] = "\\operatorname{mod}", - ["bm"] = "\\boldsymbol", ["square"] = "\\□", ["Box"] = "\\□", + ["bm"] = "\\boldsymbol", ["lg"] = "\\operatorname{lg}", ["square"] = "\\□", ["Box"] = "\\□", // the symbols of amsmath, amssymb and plain TeX that UnicodeMath has no control word for ["lt"] = "<", ["gt"] = ">", ["dotsb"] = "⋯", ["dotsi"] = "⋯", ["dotsm"] = "⋯", ["dotsc"] = "…", ["dotso"] = "…", ["ldotp"] = ".", ["gggtr"] = "⋙", ["llless"] = "⋘", ["gvertneqq"] = "≩", ["lvertneqq"] = "≨", ["ngeqq"] = "≱", diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs b/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs index 708780d..821df61 100644 --- a/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs +++ b/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs @@ -456,6 +456,11 @@ private string TeX(XElement value, bool noAddParens = false) break; case "munder": + if (VarLimit(value) is { } varLimit) + { + ret = varLimit; + break; + } if (Name(Last(value)) == "mo" && AccentsBelow.TryGetValue(Text(Last(value)), out var below)) { if (standard && below == "underbar") @@ -642,6 +647,11 @@ private string TeX(XElement value, bool noAddParens = false) break; } } + else if (LimitNames.TryGetValue(Text(value), out var limitName)) + { + ret = limitName; + break; + } else if (IsFunctionName(Text(value))) { ret = "\\" + Text(value) + " "; @@ -873,6 +883,23 @@ private static bool IsBinomial(XElement value) private static bool IsZeroThickness(string thickness) => thickness != null && ZeroThickness().IsMatch(thickness.Trim()) && thickness.Trim().Length > 0; + // the operator names of amsmath of two words, with the thin space of TeX + private static readonly Dictionary LimitNames = new Dictionary(StringComparer.Ordinal) + { + ["lim\u2009inf"] = "\\liminf ", ["lim\u2009sup"] = "\\limsup ", ["inj\u2009lim"] = "\\injlim ", + ["proj\u2009lim"] = "\\projlim ", + }; + + /// lim with an arrow under it, as amsmath writes it (\varinjlim, \varprojlim), or null. + private static string VarLimit(XElement element) => + Name(element) == "munder" && ChildCount(element) == 2 && Name(First(element)) == "mi" && Text(First(element)) == "lim" && + Name(Last(element)) == "mo" ? Text(Last(element)) switch + { + "→" => "\\varinjlim ", + "←" => "\\varprojlim ", + _ => null, + } : null; + /// Whether limits under or over the element are its subscript and superscript: an operator or a function name. private static bool TakesLimits(XElement element) { @@ -880,8 +907,10 @@ private static bool TakesLimits(XElement element) element = First(element); if (Name(element) == "mo") return IsLargeOperator(element) || IsFunctionName(Text(element)); + if (VarLimit(element) != null) + return true; // a name that is not a function's, \operatorname{argmax}, takes its scripts at its side: \underset puts them under - return Name(element) == "mi" && IsFunctionName(Text(element)); + return Name(element) == "mi" && (IsFunctionName(Text(element)) || LimitNames.ContainsKey(Text(element))); } /// Whether an operator is large (∑, ∫, ⋃…) or a horizontal brace, whose limits go under and over it. @@ -910,6 +939,8 @@ private static string StandardIdentifier(XElement value) // an empty identifier, which holds a place (the base of {}^3, the left side of &=) if (text.Length == 0) return "{}"; + if (LimitNames.TryGetValue(text, out var limitName)) + return limitName; if (IsFunctionName(text)) return "\\" + text + " "; var plain = string.Concat(Characters(text).Where(c => c.Length == 1 && (c[0] >= 'A' && c[0] <= 'Z' || c[0] >= 'a' && c[0] <= 'z'))); diff --git a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathGrammar.parlot.cs b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathGrammar.parlot.cs index 37c3bcb..86943eb 100644 --- a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathGrammar.parlot.cs +++ b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathGrammar.parlot.cs @@ -261,7 +261,8 @@ private static Parser Build() operand.Parser = OneOrMany(factor).Then(x => (object)x.ToList()); var trigName = Opt(L("a")).And(OneOf(L("sin"), L("cos"), L("tan"), L("sec"), L("csc"), L("cot"))).And(Opt(L("h"))) .Then(t => (object)A(t.Item1, t.Item2, t.Item3)); - var limName = OneOf(L("lim"), L("log"), L("lg")); + // the names of two words that TeX parts with a thin space (\liminf), before the name of their first word + var limName = OneOf(L("lim\u2009inf"), L("lim\u2009sup"), L("inj\u2009lim"), L("proj\u2009lim"), L("lim"), L("log"), L("lg")); // scripts var subU = unicodeSub.Then(b => (object)O("low", b)); @@ -415,13 +416,13 @@ private static Parser Build() var arctrigName = OneOf(L("arcsin"), L("arcsec"), L("arctan"), L("arccot"), L("arccos"), L("arccsc")).And(Opt(L("h"))) .Then(t => (object)A(t.Item1, t.Item2)); - functionName.Parser = OneOf( - OneOf(trigName, arctrigName, limName, + var knownFunctionName = OneOf(trigName, arctrigName, limName, L("arg"), L("def"), L("deg"), L("det"), L("dim"), L("erf"), L("exp"), L("gcd"), L("ker"), L("hom"), L("Im"), L("inf"), L("ln"), L("max"), L("min"), L("mod"), L("Pr"), L("Re"), L("sup"), L("tg"), L("lcm"), L("lub"), L("glb"), L("lim\u00A0inf"), L("lim\u00A0sup")) - .AndSkip(Not(alphaAscii)), - OneOrMany(alphaN).AndSkip(L("\u2061")).Then(t => (object)t.ToList())); + .AndSkip(Not(alphaAscii)); + var appliedFunctionName = OneOrMany(alphaN).AndSkip(L("\u2061")).Then(t => (object)t.ToList()); + functionName.Parser = OneOf(knownFunctionName, appliedFunctionName); var function = OneOf( functionName.AndSkip(Opt(__)).AndSkip(Opt(OneOf(L("\u2061"), opNaryand))).And(Opt(script)).AndSkip(Opt(__)).And(operand).AndSkip(Opt(_)) .Then(t => @@ -435,12 +436,35 @@ private static Parser Build() } return (object)O("function", O(("f", O("atoms", c)), ("of", t.Item3))); }), + // a function name that UnicodeMath does not know, with the function apply sign, scripts and nothing to apply + // it to (argmax⁡┬𝑥): the name with its scripts, not the letters of the name applied to the sign and its scripts + OneOf(knownFunctionName.AndSkip(Opt(__)).AndSkip(L("\u2061")), appliedFunctionName) + .When((context, name) => !UnicodeMathText.IsFunctionName(FunctionName(name, false))).And(OneOf(script, abscript)).AndSkip(Not(Opt(__).SkipAnd(operand))) + .Then(t => + { + var s = (JsObject)t.Item2; + s["base"] = O("atoms", O(("chars", FunctionName(t.Item1, false)), ("funct", true))); + return (object)O("script", s); + }), + // a name that UnicodeMath knows but would read as letters, with the function apply sign, a limit under or + // over it and its argument (lim inf⁡┬𝑛 𝑎) + knownFunctionName.When((context, name) => !UnicodeMathText.IsFunctionName(FunctionName(name, false))) + .AndSkip(Opt(__)).AndSkip(L("\u2061")).And(abscript).AndSkip(Opt(__)).And(operand).AndSkip(Opt(_)) + .Then(t => + { + var s = (JsObject)t.Item2; + s["base"] = O("atoms", O(("chars", FunctionName(t.Item1, false)), ("funct", true))); + return (object)O("function", O(("f", O("script", s)), ("of", t.Item3))); + }), functionName.AndSkip(Opt(__)).And(operand).AndSkip(Opt(_)) .Then(t => (object)O("function", O(("f", O("atoms", O("chars", FunctionName(t.Item1, false)))), ("of", t.Item2)))), // a name marked as a function without an operand (NaN⁡,) is one name, as a function name is (sin⁡,), and not - // its letters; the function apply sign is read after it (but not one with scripts after the sign, argmax⁡┬(𝑥∈𝑆)) - OneOrMany(alphaN).AndSkip(Not(Not(L("⁡").And(Not(opScript))))) - .Then(t => (object)O("atoms", O(("chars", Join(t.ToList())), ("funct", true))))); + // its letters; the function apply sign is read after it (but not one with scripts after the sign, argmax⁡┬(𝑥∈𝑆)); + // so is a name UnicodeMath knows but would read as letters (lg⁡, lim inf⁡) + OneOf(knownFunctionName.When((context, name) => !UnicodeMathText.IsFunctionName(FunctionName(name, false))).Then(n => (object)FunctionName(n, false)), + OneOrMany(alphaN).Then(t => (object)Join(t.ToList()))) + .AndSkip(Not(Not(L("⁡").And(Not(opScript))))) + .Then(t => (object)O("atoms", O(("chars", t), ("funct", true))))); // text, overrides, colors, comments, typewriter text.Parser = L("\"").SkipAnd(OneOrMany(OneOf(L("\\\"").Then(x => (object)"\""), Not(L("\"")).SkipAnd(anyChar)))).AndSkip(L("\"")) diff --git a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs index 4f1dfe7..42bcb67 100644 --- a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs +++ b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs @@ -709,6 +709,23 @@ private static XElement FunctionName(XElement head) return nested || !IsFunctionName(Text(name)) ? name : null; } + // the names of two words of TeX, with the thin space of \liminf, which UnicodeMath reads as it reads lim + private static readonly string[] TwoWordNames = { "lim\u2009inf", "lim\u2009sup", "inj\u2009lim", "proj\u2009lim" }; + + /// + /// The name of a function, alone or with scripts, that is written with the function apply sign after it when + /// nothing follows it, or null: a name of letters that UnicodeMath does not know, which it would read as letters + /// (lg, argmax, the lim inf of \liminf, with a thin space). + /// + private static XElement LoneFunctionName(XElement node) + { + var name = node; + while (Name(name) is "msub" or "msup" or "msubsup" or "munder" or "mover" or "munderover") + name = First(name); + return Name(name) == "mi" && !name.HasElements && Len(Text(name)) >= 2 && !IsFunctionName(Text(name)) && + Characters(Text(name)).All(c => c == "\u2009" || char.IsLetter(c, 0)) ? name : null; + } + /// The operand of an n-ary operator that does not grow it (∫64), an mpadded of no height. private static bool IsSmashed(XElement naryand) => Name(naryand) == "mpadded" && Attr(naryand, "height") == "0" && naryand.Attributes().All(a => a.Name == "height" || a.Name == "arg"); @@ -1345,11 +1362,21 @@ private string DumpCore(XElement value, bool noAddParens) { // a function name with scripts that is not one UnicodeMath knows (argmax), or whose scripts are not // where they go (lim²_(𝑥→0)): the function apply sign follows the name, as in argmax⁡┬(𝑥∈𝑆) 𝑓(𝑥) - functionName = name; + // (not the names of two words, which UnicodeMath reads as lim: lim inf┬𝑛 𝑎) + functionName = TwoWordNames.Contains(Text(name)) && name == node.Elements().FirstOrDefault() ? null : name; dumped = Dump(node); functionName = null; functionMarked = true; } + else if ((i + 1 == nodes.Count || Name(nodes[i + 1]) == "mo" && Text(nodes[i + 1]) != "\u2061" && !IsOpenDelimiter(Text(nodes[i + 1]))) && + LoneFunctionName(node) is { } lone) + { + // a function name with nothing to apply it to, which UnicodeMath would read as letters without the + // function apply sign after it: lg⁡, argmax⁡_𝑥 + functionName = lone; + dumped = Dump(node); + functionName = null; + } else if (functionMarked && Name(node) == "mo" && Text(node) == "\u2061") { dumped = " "; diff --git a/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs b/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs index b913c80..1389afa 100644 --- a/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs +++ b/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs @@ -320,6 +320,37 @@ public async Task ExpandsMacrosOfTheOptions() await Assert.That(Equation.Parse(@"\half x\in\R", EquationFormat.LaTeX, options).ToString(EquationFormat.LaTeX)).IsEqualTo(@"\frac{1}{2}x\in\mathbb{R}"); } + // the operator names of amsmath, alone or with their limits, which go under them in a display equation; the LaTeX + // and the UnicodeMath written for them read back as the same equation + [Test] + [Arguments(@"\lg", false, "lg\u2061")] + [Arguments(@"\lg x", false, "lg\u2061x")] + [Arguments(@"\operatorname{lg}", true, "lg\u2061")] + [Arguments(@"\liminf", true, "lim\u2009inf\u2061")] + [Arguments(@"\limsup_{n} a_n", true, "lim\u2009supn\u2061an")] + [Arguments(@"\limsup_{n} a_n", false, "lim\u2009supn\u2061an")] + [Arguments(@"\injlim", true, "inj\u2009lim\u2061")] + [Arguments(@"\projlim", true, "proj\u2009lim\u2061")] + [Arguments(@"\varinjlim", true, "lim→")] + [Arguments(@"\varprojlim", true, "lim←")] + [Arguments(@"\varliminf", true, "lim")] + [Arguments(@"\varlimsup", true, "lim¯")] + [Arguments(@"\operatorname*{argmax}_{x}", true, "argmaxx")] + [Arguments(@"\operatorname*{argmax}_{x}", false, "argmaxx")] + [Arguments(@"\operatorname*{argmax}_{x} f(x)", true, "argmaxx\u2061f(x)")] + [Arguments(@"\operatornamewithlimits{lim}_{x}", true, "limx")] + [Arguments(@"\lim_{x}", true, "limx")] + public async Task ReadsOperatorNamesOfAmsmath(string latex, bool display, string expected) + { + var options = new EquationOptions { Display = display }; + var equation = Equation.Parse(latex, EquationFormat.LaTeX, options); + var mathML = string.Concat(equation.ToMathML().Elements().Select(e => e.ToString(System.Xml.Linq.SaveOptions.DisableFormatting))) + .Replace(" xmlns=\"http://www.w3.org/1998/Math/MathML\"", ""); + await Assert.That(mathML).IsEqualTo(expected); + await Assert.That(Equation.Parse(equation.ToString(EquationFormat.LaTeX), EquationFormat.LaTeX, options)).IsEqualTo(equation); + await Assert.That(Equation.Parse(equation.ToString(EquationFormat.UnicodeMath), EquationFormat.UnicodeMath, options)).IsEqualTo(equation); + } + [Test] [Arguments(@"\foo x", @"Unknown control word \foox in the LaTeX.")] [Arguments(@"a+\undefinedcommand", @"Unknown control word \undefinedcommand in the LaTeX.")] diff --git a/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs b/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs index 012a521..c45a664 100644 --- a/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs +++ b/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs @@ -180,9 +180,9 @@ 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, and \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,21 +190,17 @@ 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, - // \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 - // 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 + // LaTeX and amsmath that the reader does not read yet: both scripts of \overbrace and \underbrace; \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", + "overbrace-c", "underbrace-c", "middle", "spaces", "mathrm", "mathsf", "boldsymbol", "newenvironment-multi-node", "newcommand-escaped-hash", }; diff --git a/tests/data/README.md b/tests/data/README.md index 525b927..913280c 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -69,7 +69,11 @@ corrected where latex2mathml writes MathML that is not what the LaTeX means: the `\textbf`, `\textit`, `\textrm`, `\textsf`, `\texttt` is a no-break space, not a tilde (`tilde-and-space-in-text`, `textbf`...); the superscript of `( - 25 ) ^ { 2 }` is that of the parenthesized group, not of `)` (`issue-42`); `\omicron` is a letter, not an operator (`omicron`); the limit of `\lim` goes under it in a display equation -(`limit-at-plus-infinity`). +(`limit-at-plus-infinity`). Seven snapshots of the operator names of amsmath were corrected too: `\liminf`, `\limsup`, +`\injlim` and `\projlim` are one name with the thin space of TeX between its words (`lim\,inf`), where latex2mathml +writes an operator with a six-per-em space (`liminf`, `limsup`, `injlim`, `projlim`); `\varinjlim` is lim with an arrow +under it, not inj lim (`varinjlim`); the limit of `\operatorname*` and of `\operatornamewithlimits` goes under the name +in a display equation (`operatorname-star`, `operatornamewithlimits`). `mathml/mml2tex.json`: the scenarios of `xspec/*.xspec` of [transpect/mml2tex](https://github.com/transpect/mml2tex) (commit 9458a7a2f5125609c92eb178fcd0f8b8419e2c40), MathML with the LaTeX mml2tex writes for it, and the MathML of diff --git a/tests/data/latex/latex2mathml-converter.json b/tests/data/latex/latex2mathml-converter.json index 9da7560..876844a 100644 --- a/tests/data/latex/latex2mathml-converter.json +++ b/tests/data/latex/latex2mathml-converter.json @@ -957,7 +957,7 @@ "id": "injlim", "latex": "\\injlim", "display": true, - "mathml": "inj\u2006lim" + "mathml": "inj\u2009lim" }, { "id": "ker", @@ -1077,13 +1077,13 @@ "id": "liminf", "latex": "\\liminf", "display": true, - "mathml": "lim\u2006inf" + "mathml": "lim\u2009inf" }, { "id": "limsup", "latex": "\\limsup", "display": true, - "mathml": "lim\u2006sup" + "mathml": "lim\u2009sup" }, { "id": "llless", @@ -1155,7 +1155,7 @@ "id": "projlim", "latex": "\\projlim", "display": true, - "mathml": "proj\u2006lim" + "mathml": "proj\u2009lim" }, { "id": "rvert", @@ -1605,7 +1605,7 @@ "id": "varinjlim", "latex": "\\varinjlim", "display": true, - "mathml": "inj\u2006lim" + "mathml": "lim→" }, { "id": "nolimits", @@ -1713,7 +1713,7 @@ "id": "operatornamewithlimits", "latex": "\\operatornamewithlimits{lim}_{x}", "display": true, - "mathml": "limx" + "mathml": "limx" }, { "id": "pod", @@ -1785,7 +1785,7 @@ "id": "operatorname-star", "latex": "\\operatorname*{argmax}_{x}", "display": true, - "mathml": "argmaxx" + "mathml": "argmaxx" }, { "id": "vcenter",