diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs
index f24899c..8bfafd2 100644
--- a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs
+++ b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs
@@ -628,6 +628,16 @@ private static string RewriteTeXOnly(string latex)
i++;
continue;
}
+ if (cw is "operatorname" or "operatornamewithlimits" && BracedArgument(tex, cw == "operatorname" && tex[j] == "*" ? j + 1 : j) is var (namedFunction, afterNamedFunction) &&
+ afterNamedFunction >= 0 && IsFunctionName(namedFunction.Trim()))
+ {
+ // \operatorname{lim}: the function name, as \lim
+ result.Append('\\').Append(namedFunction.Trim());
+ i = afterNamedFunction;
+ if (i < tex.Length && IsAsciiAlphabetic(tex[i]))
+ result.Append(' ');
+ continue;
+ }
if (Renamed.TryGetValue(cw, out var renamed))
{
result.Append(renamed);
@@ -768,6 +778,13 @@ private static string RewriteTeXOnly(string latex)
i = afterNegated;
continue;
}
+ if (LaTeXSymbols.TryGetNameOfTwoWords(cw, out var nameOfTwoWords))
+ {
+ // \liminf: the name of two words, which UnicodeMath reads as one name when it is marked as a function
+ result.Append("\\operatorname{").Append(nameOfTwoWords).Append('}');
+ i = j;
+ continue;
+ }
if (cw == "operatorname" && tex[j] == "*")
{
// \operatorname*, a name with limits, read as \operatorname
diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs b/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs
index dee8921..ac6564d 100644
--- a/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs
+++ b/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs
@@ -652,6 +652,11 @@ private string TeX(XElement value, bool noAddParens = false)
ret = "\\" + Text(value) + " ";
break;
}
+ else if (LaTeXSymbols.TryGetCommandOfTwoWords(Text(value), out var command))
+ {
+ ret = "\\" + command + " ";
+ break;
+ }
else if (Len(Text(value)) > 1 && SharpEquations.Internal.Text.AsciiPatterns.Letters().IsMatch(Text(value)))
{
ret = "\\operatorname{" + Text(value) + "}";
@@ -886,7 +891,7 @@ private static bool TakesLimits(XElement element)
if (Name(element) == "mo")
return IsLargeOperator(element) || IsFunctionName(Text(element));
// 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)) || LaTeXSymbols.TryGetCommandOfTwoWords(Text(element), out _));
}
/// Whether an operator is large (∑, ∫, ⋃…) or a horizontal brace, whose limits go under and over it.
@@ -917,6 +922,8 @@ private static string StandardIdentifier(XElement value)
return "{}";
if (IsFunctionName(text))
return "\\" + text + " ";
+ if (LaTeXSymbols.TryGetCommandOfTwoWords(text, out var command))
+ return "\\" + command + " ";
var plain = string.Concat(Characters(text).Where(c => c.Length == 1 && (c[0] >= 'A' && c[0] <= 'Z' || c[0] >= 'a' && c[0] <= 'z')));
return plain.Length == text.Length ? "\\operatorname{" + text + "}" : LaTeXSymbols.Math(text);
}
diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXSymbols.cs b/src/SharpEquations/Internal/LaTeX/LaTeXSymbols.cs
index 99f4ecd..41d6814 100644
--- a/src/SharpEquations/Internal/LaTeX/LaTeXSymbols.cs
+++ b/src/SharpEquations/Internal/LaTeX/LaTeXSymbols.cs
@@ -140,6 +140,29 @@ public static bool TryGetPlain(string ch, out string style, out string plain)
/// Whether a character is written as a command (α as \alpha, □ as \square).
public static bool IsCommand(string ch) => Commands.ContainsKey(ch);
+ // the operator names of amsmath that are two words, with their words parted by a six-per-em space, as MathML
+ // writes them (MathJax, latex2mathml)
+ private static readonly Dictionary NamesOfTwoWords = new Dictionary
+ {
+ ["liminf"] = "lim\u2006inf", ["limsup"] = "lim\u2006sup", ["injlim"] = "inj\u2006lim", ["projlim"] = "proj\u2006lim",
+ };
+
+ // the forms of amsmath that write them as lim with a bar or an arrow under or over it, read as the names, as
+ // latex2mathml reads them
+ private static readonly Dictionary VariantsOfTwoWords = new Dictionary
+ {
+ ["varliminf"] = "liminf", ["varlimsup"] = "limsup", ["varinjlim"] = "injlim", ["varprojlim"] = "projlim",
+ };
+
+ private static readonly Dictionary CommandsOfTwoWords = NamesOfTwoWords.ToDictionary(entry => entry.Value, entry => entry.Key);
+
+ /// The operator name of two words of a command (liminf and varliminf: lim inf).
+ public static bool TryGetNameOfTwoWords(string command, out string name) =>
+ NamesOfTwoWords.TryGetValue(VariantsOfTwoWords.TryGetValue(command, out var plain) ? plain : command, out name);
+
+ /// The command of an operator name of two words (lim inf: liminf).
+ public static bool TryGetCommandOfTwoWords(string name, out string command) => CommandsOfTwoWords.TryGetValue(name, out command);
+
/// The LaTeX of a run of text in math: each character as a letter, a command or itself.
public static string Math(string text)
{
diff --git a/src/SharpEquations/Internal/Text/Rune.cs b/src/SharpEquations/Internal/Text/Rune.cs
index 1cf2a1e..1eb2d84 100644
--- a/src/SharpEquations/Internal/Text/Rune.cs
+++ b/src/SharpEquations/Internal/Text/Rune.cs
@@ -26,6 +26,8 @@ public Rune(int value)
public bool IsBmp => value <= 0xFFFF;
+ public static bool IsLetter(Rune value) => char.IsLetter(value.ToString(), 0);
+
public override string ToString() => char.ConvertFromUtf32(value);
public bool Equals(Rune other) => value == other.value;
diff --git a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathGrammar.parlot.cs b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathGrammar.parlot.cs
index 01eecd0..2698099 100644
--- a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathGrammar.parlot.cs
+++ b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathGrammar.parlot.cs
@@ -413,15 +413,21 @@ private static Parser