Skip to content
Merged
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
50 changes: 49 additions & 1 deletion src/SharpEquations/Internal/LaTeX/LaTeXReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<string, string> LimitNames = new Dictionary<string, string>(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}}",
};

/// <summary>
/// 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.
/// </summary>
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)) + " ";
Expand Down Expand Up @@ -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
Expand Down
33 changes: 32 additions & 1 deletion src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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) + " ";
Expand Down Expand Up @@ -878,15 +888,34 @@ 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<string, string> LimitNames = new Dictionary<string, string>(StringComparer.Ordinal)
{
["lim\u2009inf"] = "\\liminf ", ["lim\u2009sup"] = "\\limsup ", ["inj\u2009lim"] = "\\injlim ",
["proj\u2009lim"] = "\\projlim ",
};

/// <summary>lim with an arrow under it, as amsmath writes it (\varinjlim, \varprojlim), or null.</summary>
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;

/// <summary>Whether limits under or over the element are its subscript and superscript: an operator or a function name.</summary>
private static bool TakesLimits(XElement element)
{
while (Name(element) == "mrow" && ChildCount(element) == 1)
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)));
}

/// <summary>Whether an operator is large (∑, ∫, ⋃…) or a horizontal brace, whose limits go under and over it.</summary>
Expand Down Expand Up @@ -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')));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ private static Parser<object> 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));
Expand Down Expand Up @@ -415,13 +416,13 @@ private static Parser<object> 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 =>
Expand All @@ -435,12 +436,35 @@ private static Parser<object> 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("\""))
Expand Down
29 changes: 28 additions & 1 deletion src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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" };

/// <summary>
/// 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).
/// </summary>
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;
}

/// <summary>The operand of an n-ary operator that does not grow it (∫64), an mpadded of no height.</summary>
private static bool IsSmashed(XElement naryand) =>
Name(naryand) == "mpadded" && Attr(naryand, "height") == "0" && naryand.Attributes().All(a => a.Name == "height" || a.Name == "arg");
Expand Down Expand Up @@ -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 = " ";
Expand Down
Loading
Loading