diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs index f24899c..e349097 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", + ["bm"] = "\\boldsymbol", ["lg"] = "\\operatorname{lg}", // the symbols of LaTeX, amsmath, amssymb and plain TeX that UnicodeMath has no control word for (the others of // LaTeXSymbols are read from it), and \square, \Box, \blacksquare, \surd and \oslash, which are symbols in LaTeX // and build a box, a root or a fraction in UnicodeMath diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs b/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs index dee8921..220041b 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") @@ -647,6 +652,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) + " "; @@ -878,6 +888,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) { @@ -885,8 +912,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. @@ -915,6 +944,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 01eecd0..4bbe121 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 83f19c0..e842d3c 100644 --- a/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs +++ b/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs @@ -347,6 +347,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(@"\newenvironment{wrapper}{a +}{+ c} \begin{wrapper} b \end{wrapper}", @"a+b+c")] [Arguments(@"\newenvironment{paren}[1]{\left( #1 +}{\right)} \begin{paren}{x} y \end{paren}", @"\left(x+y\right)")] @@ -372,8 +403,8 @@ public async Task RejectsEnvironmentsThatExpandForever() [Arguments(@"\foo x", @"Unknown control word \foo in the LaTeX.")] [Arguments(@"a+\undefinedcommand", @"Unknown control word \undefinedcommand in the LaTeX.")] [Arguments(@"\sgn r", @"Unknown control word \sgn in the LaTeX.")] - [Arguments(@"\lg d", @"Unknown control word \lg in the LaTeX.")] - [Arguments(@"\liminf v", @"Unknown control word \liminf in the LaTeX.")] + [Arguments(@"\tr d", @"Unknown control word \tr in the LaTeX.")] + [Arguments(@"\Var v", @"Unknown control word \Var in the LaTeX.")] [Arguments(@"a \backslash b", @"Unknown control word \backslash in the LaTeX.")] [Arguments(@"\sgn2", @"Unknown control word \sgn in the LaTeX.")] public async Task RejectsUnknownControlWordsWhenStrict(string latex, string message) @@ -384,8 +415,8 @@ public async Task RejectsUnknownControlWordsWhenStrict(string latex, string mess [Test] [Arguments(@"\sgn r", @"\sgn", "r")] - [Arguments(@"\lg d", @"\lg", "d")] - [Arguments(@"\liminf v", @"\liminf", "v")] + [Arguments(@"\tr d", @"\tr", "d")] + [Arguments(@"\Var v", @"\Var", "v")] [Arguments(@"a \backslash b", @"\backslash", "b")] [Arguments(@"\sgn r", @"\sgn", "r")] [Arguments(@"\sgn{r}", @"\sgn", "r")] diff --git a/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs b/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs index 820ad3e..f241f7f 100644 --- a/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs +++ b/tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs @@ -200,13 +200,8 @@ private static string Canonical(Equation equation) "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) - private static readonly HashSet NotReadYet = new HashSet - { - "lg", "liminf", "limsup", "injlim", "projlim", "varinjlim", "operatornamewithlimits", "operatorname-star", - }; + // LaTeX and amsmath that the reader does not read yet: none left + private static readonly HashSet NotReadYet = new HashSet(); // the same equation, which SharpEquations holds otherwise: an equation number as the label of its row (\tag), // and none for aligned equations, whose numbers latex2mathml writes as cells; the alignment of align, split and diff --git a/tests/data/README.md b/tests/data/README.md index 52df365..aa93395 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -77,7 +77,11 @@ corrected where latex2mathml writes MathML that is not what the LaTeX means: the (`limit-at-plus-infinity`). Ten more were corrected for the fonts of mathematics, which do not reach the text of `\text` (in the text font): `var` is plain text in `mathbf`, `mathit`, `mathcal`, `calligraphic-mode`, `mathscr`, `mathfrak`, `mathtt`, `mit` and `blackboard-bold`, and the `1` -of `\mathsf{1}` is sans-serif (`mathsf`). +of `\mathsf{1}` is sans-serif (`mathsf`). Seven more were corrected for the operator names of amsmath: `\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 c913cd8..4b50ac8 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",