Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/SharpEquations/Internal/LaTeX/LaTeXReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) + "}";
Expand Down Expand Up @@ -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 _));
}

/// <summary>Whether an operator is large (∑, ∫, ⋃…) or a horizontal brace, whose limits go under and over it.</summary>
Expand Down Expand Up @@ -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);
}
Expand Down
23 changes: 23 additions & 0 deletions src/SharpEquations/Internal/LaTeX/LaTeXSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,29 @@ public static bool TryGetPlain(string ch, out string style, out string plain)
/// <summary>Whether a character is written as a command (α as \alpha, □ as \square).</summary>
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<string, string> NamesOfTwoWords = new Dictionary<string, string>
{
["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<string, string> VariantsOfTwoWords = new Dictionary<string, string>
{
["varliminf"] = "liminf", ["varlimsup"] = "limsup", ["varinjlim"] = "injlim", ["varprojlim"] = "projlim",
};

private static readonly Dictionary<string, string> CommandsOfTwoWords = NamesOfTwoWords.ToDictionary(entry => entry.Value, entry => entry.Key);

/// <summary>The operator name of two words of a command (liminf and varliminf: lim inf).</summary>
public static bool TryGetNameOfTwoWords(string command, out string name) =>
NamesOfTwoWords.TryGetValue(VariantsOfTwoWords.TryGetValue(command, out var plain) ? plain : command, out name);

/// <summary>The command of an operator name of two words (lim inf: liminf).</summary>
public static bool TryGetCommandOfTwoWords(string name, out string command) => CommandsOfTwoWords.TryGetValue(name, out command);

/// <summary>The LaTeX of a run of text in math: each character as a letter, a command or itself.</summary>
public static string Math(string text)
{
Expand Down
2 changes: 2 additions & 0 deletions src/SharpEquations/Internal/Text/Rune.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,15 +413,21 @@ private static Parser<object> Build()
L("∜").SkipAnd(operand).Then(o => (object)O("root", O(("degree", O("number", "4")), ("of", o)))),
opRoot.Then(o => (object)ColoredOperator(o)));

// the letters of a name marked as a function, its words parted by a six-per-em space (lim inf⁡, as MathML
// writes \liminf)
var markedName = OneOrMany(alphaN).And(ZeroOrMany(L("\u2006").And(OneOrMany(alphaN))))
.Then(t => (object)t.Item1.Concat(t.Item2.SelectMany(w => new[] { w.Item1 }.Concat(w.Item2))).ToList());
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));
// a function name that UnicodeMath knows, not the first word of a name marked as a function (lim inf⁡)
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)).AndSkip(Not(L("\u2006").And(alphaN)));
functionName.Parser = OneOf(
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()));
knownFunctionName,
markedName.AndSkip(L("\u2061")));
var function = OneOf(
functionName.AndSkip(Opt(__)).AndSkip(Opt(OneOf(L("\u2061"), opNaryand))).And(Opt(script)).AndSkip(Opt(__)).And(operand).AndSkip(Opt(_))
.Then(t =>
Expand All @@ -435,12 +441,22 @@ private static Parser<object> Build()
}
return (object)O("function", O(("f", O("atoms", c)), ("of", t.Item3)));
}),
// a name marked as a function with scripts but without an operand (argmax⁡_𝑥 alone) is the name with its
// scripts, and not its letters before a script of the function apply sign (but not a function name that
// UnicodeMath knows, lim⁡_(𝑎→∞), read as UnicodeMathML reads it)
Not(knownFunctionName).SkipAnd(markedName).AndSkip(L("\u2061")).And(script).AndSkip(Opt(_))
.Then(t =>
{
var s = (JsObject)t.Item2;
s["base"] = O("atoms", O(("chars", Join((List<object>)t.Item1)), ("funct", true)));
return (object)O("script", s);
}),
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)))));
markedName.AndSkip(Not(Not(L("⁡").And(Not(opScript)))))
.Then(t => (object)O("atoms", O(("chars", Join((List<object>)t)), ("funct", true)))));

// text, overrides, colors, comments, typewriter
text.Parser = L("\"").SkipAnd(OneOrMany(OneOf(L("\\\"").Then(x => (object)"\""), Not(L("\"")).SkipAnd(anyChar)))).AndSkip(L("\""))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ private sealed class PreprocessStyle
["⒨"] = "()", ["⒩"] = "‖‖", ["ⓢ"] = "[]", ["Ⓢ"] = "{}", ["⒱"] = "||",
};

private static readonly string[] EulerFunctions = { "det", "gcd", "inf", "lim", "lim inf", "lim sup", "max", "min", "Pr", "sup" };
// the functions whose subscript goes under them in a display equation
internal static readonly string[] EulerFunctions = { "det", "gcd", "inf", "lim", "lim\u2006inf", "lim\u2006sup", "inj\u2006lim", "proj\u2006lim", "max", "min", "Pr", "sup" };

private object Preprocess(PreprocessStyle dsty, object uast, int? index = null, List<object> arr = null)
{
Expand Down
15 changes: 12 additions & 3 deletions src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,12 @@ private string Grouped(XElement node)
return "(" + text + ")";
}

/// <summary>A name of several letters, not a function's, as the base of scripts: argmax in argmax_𝑥.</summary>
private static bool IsScriptedName(XElement mi) =>
!mi.HasElements && mi.Parent != null && Name(mi.Parent) is "msub" or "msup" or "msubsup" or "munder" or "mover" or "munderover" &&
First(mi.Parent) == mi && Len(Text(mi)) > 1 && !IsFunctionName(Text(mi)) && !HasAttr(mi, "mathvariant") &&
CodePoints.Of(Text(mi)).All(c => c.Value == 0x2006 || System.Text.Rune.IsLetter(c));

/// <summary>The name of a function with scripts that is written with the function apply sign after it, or null.</summary>
private static XElement FunctionName(XElement head)
{
Expand Down Expand Up @@ -1030,8 +1036,9 @@ private string DumpCore(XElement value, bool noAddParens)
break;
}
op = HasAttr(value, "accentunder") || Attr(value, "accent") == "true" || IsCombining(Text(Last(value))) ? "" : "┬";
// lim_ is under lim in a function of a display equation only
if ((Text(First(value)) == "lim" && !First(value).HasElements || functionName != null && IsFunctionName(Text(functionName))) && display &&
// lim_ is under lim (and lim inf, lim sup) in a function of a display equation only
if ((Text(First(value)) == "lim" && !First(value).HasElements ||
functionName != null && (IsFunctionName(Text(functionName)) || UnicodeMathTranslator.EulerFunctions.Contains(Text(functionName)))) && display &&
Role(parent) == ":function")
op = "_";
ret = Binary(value, op);
Expand Down Expand Up @@ -1196,7 +1203,9 @@ private string DumpCore(XElement value, bool noAddParens)
break;

case "mi":
if (value == functionName)
// a name as the base of its scripts is marked as a function, without which it would be read as its
// letters (argmax⁡_𝑥)
if (value == functionName || IsScriptedName(value))
{
ret = Text(value) + "\u2061";
break;
Expand Down
2 changes: 1 addition & 1 deletion src/SharpEquations/Internal/UnicodeMath/UnicodeMathText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static readonly (string Fraction, int Numerator, int Denominator)[] Unico
private static readonly string[] FunctionNames =
{
"Im", "Pr", "Re", "arg", "cos", "cot", "csc", "ctg", "deg", "det", "dim", "erf", "exp", "gcd", "hom",
"inf", "ker", "lim", "log", "ln", "max", "min", "mod", "sec", "sin", "sup", "tan", "tg",
"inf", "ker", "lg", "lim", "log", "ln", "max", "min", "mod", "sec", "sin", "sup", "tan", "tg",
};

/// <summary>inRange(ch0, ch, ch1): ch a single code point between those of ch0 and ch1.</summary>
Expand Down
34 changes: 30 additions & 4 deletions tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,36 @@ public async Task ReadsSlashesAndBarsAsLaTeX(string latex, string expectedLaTeX,
await Assert.That(Equation.Parse(expectedLaTeX, EquationFormat.LaTeX)).IsEqualTo(Equation.Parse(latex, EquationFormat.LaTeX)).Because("the LaTeX written reads back");
}

// operator names with nothing after them, and the names of amsmath that are two words, one name whose words a
// six-per-em space parts (as MathML writes them); the forms with a bar or an arrow (\varinjlim) are read as the names
[Test]
[Arguments(@"\lg x", false, @"\lg x", "lg⁡𝑥")]
[Arguments(@"\lg", false, @"\lg", "lg")]
[Arguments(@"\liminf_{n\to\infty} a_n", false, @"\liminf_{n\to\infty}a_n", "lim\u2006inf⁡_(𝑛→∞) 𝑎_𝑛")]
[Arguments(@"\liminf_{n\to\infty} a_n", true, @"\liminf_{n\to\infty}a_n", "lim\u2006inf⁡_(𝑛→∞) 𝑎_𝑛")]
[Arguments(@"\limsup x_n", false, @"\limsup x_n", "lim\u2006sup⁡𝑥_𝑛")]
[Arguments(@"\injlim A_i", false, @"\injlim A_i", "inj\u2006lim⁡𝐴_𝑖")]
[Arguments(@"\projlim_i A_i", true, @"\projlim_iA_i", "proj\u2006lim⁡_𝑖 𝐴_𝑖")]
[Arguments(@"\liminf", false, @"\liminf", "lim\u2006inf⁡")]
[Arguments(@"\varinjlim", false, @"\injlim", "inj\u2006lim⁡")]
[Arguments(@"\varprojlim A_i", false, @"\projlim A_i", "proj\u2006lim⁡𝐴_𝑖")]
[Arguments(@"\varliminf + \varlimsup", false, @"\liminf+\limsup", "lim\u2006inf⁡+lim\u2006sup⁡")]
[Arguments(@"\operatorname*{argmax}_{x}", true, @"\operatorname{argmax}_x", "argmax⁡_𝑥")]
[Arguments(@"\operatorname{argmax}_x + 1", false, @"\operatorname{argmax}_x+1", "argmax⁡_𝑥+1")]
[Arguments(@"\operatornamewithlimits{lim}_{x}", true, @"\lim_x", "lim_𝑥")]
[Arguments(@"\operatorname{lim}_{x} f + \operatorname*{max}x", true, @"\lim_xf+\max x", "lim_𝑥 ⁡𝑓+max⁡𝑥")]
public async Task ReadsOperatorNames(string latex, bool display, string expectedLaTeX, string expectedUnicodeMath)
{
var options = new EquationOptions { Display = display };
var equation = Equation.Parse(latex, EquationFormat.LaTeX, options);
await Assert.That(equation.ToString(EquationFormat.LaTeX)).IsEqualTo(expectedLaTeX);
await Assert.That(equation.ToString(EquationFormat.UnicodeMath)).IsEqualTo(expectedUnicodeMath);
var word = new EquationOptions { Display = display, LaTeXFlavor = LaTeXFlavor.Word };
await Assert.That(Equation.Parse(expectedLaTeX, EquationFormat.LaTeX, options)).IsEqualTo(equation).Because("the standard LaTeX reads back");
await Assert.That(Equation.Parse(equation.ToString(EquationFormat.LaTeX, word), EquationFormat.LaTeX, word)).IsEqualTo(equation).Because("Word's LaTeX reads back");
await Assert.That(Equation.Parse(expectedUnicodeMath, EquationFormat.UnicodeMath, options)).IsEqualTo(equation).Because("the UnicodeMath reads back");
}

// the demos of math.webc.site, whose LaTeX and UnicodeMath read back to the same equation
[Test]
[Arguments(@"\left. \frac{df}{dx} \right| _0", @"{\left.\frac{df}{dx}\right|}_0", @"├𝑑𝑓/𝑑𝑥 ┤|₀")]
Expand Down Expand Up @@ -372,8 +402,6 @@ 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(@"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)
Expand All @@ -384,8 +412,6 @@ 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(@"a \backslash b", @"\backslash", "b")]
[Arguments(@"\sgn r", @"\sgn", "r")]
[Arguments(@"\sgn{r}", @"\sgn", "r")]
Expand Down
Loading
Loading