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
112 changes: 106 additions & 6 deletions src/SharpEquations/Internal/LaTeX/LaTeXReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ private static readonly (string Escape, char Held, char Character)[] TextEscapes
("\\textasciicircum{}", '\uFDD7', '^'), ("\\textasciitilde{}", '\uFDD8', '~'),
};

// the closing brackets that EscapeOpenBrackets escapes (x^{)}), held as noncharacters, since \) and \] would end the LaTeX
private static readonly (char Held, char Character)[] HeldBrackets = { ('\uFDDA', ')'), ('\uFDDB', ']') };

private static readonly HashSet<string> TextCommands = new HashSet<string>(StringComparer.Ordinal)
{
"text", "textrm", "textup", "textnormal", "textbf", "textit", "textsf", "texttt", "textmd", "mbox", "emph",
Expand All @@ -70,6 +73,8 @@ private static string RestoreTextEscapes(string unicodemath)
{
foreach (var (_, held, character) in TextEscapes)
unicodemath = unicodemath.Replace(held, character);
foreach (var (held, character) in HeldBrackets)
unicodemath = unicodemath.Replace(held, character);
if (!Characters(unicodemath).Any(c => CodePointAt(c, 0) >= HeldMarks))
return unicodemath;
return string.Concat(Characters(unicodemath).Select(c => CodePointAt(c, 0) >= HeldMarks ? char.ConvertFromUtf32(CodePointAt(c, 0) - HeldMarks) : c));
Expand Down Expand Up @@ -436,7 +441,7 @@ private static (string Text, int Next) ScriptArgument(Text tex, int i)
["smallfrown"] = "⌒", ["smallsmile"] = "⌣", ["thicksim"] = "∼", ["thickapprox"] = "β‰ˆ", ["varsubsetneqq"] = "β«‹",
["varsupsetneq"] = "βŠ‹", ["varsupsetneqq"] = "⫌", ["bigcirc"] = "\\β—―", ["centerdot"] = "⬝", ["degree"] = "Β°",
["intop"] = "\\int", ["smallint"] = "\\int", ["diagup"] = "\\β•±", ["diagdown"] = "\\β•²", ["thickspace"] = "\\;",
["And"] = "\\&", ["lvert"] = "|", ["rvert"] = "|", ["idotsint"] = "\\βˆ«β‹―\\∫",
["And"] = "\\&", ["lvert"] = "|", ["rvert"] = "|", ["vert"] = "|", ["idotsint"] = "\\βˆ«β‹―\\∫",
["mathscr"] = "\\mathcal", ["operatornamewithlimits"] = "\\operatorname", ["mathstrut"] = "\\vphantom{\\(}",
// \Re and \Im, the symbols β„œ and β„‘ of LaTeX, which UnicodeMath reads as the function names Re and Im
["Re"] = "β„œ", ["Im"] = "β„‘",
Expand Down Expand Up @@ -684,6 +689,20 @@ private static string RewriteTeXOnly(string latex)
i = fractionArguments.Next;
continue;
}
if (cw is "bigm" or "Bigm" or "biggm" or "Biggm" && brackets == 0)
{
// \bigm|, a relation outside brackets: a bar alone, {|}, which no other bar pairs with (|π‘Ž \bigm| 𝑏|)
int k = j;
while (tex[k] == " ")
k++;
var afterBar = tex[k] == "|" ? k + 1 : tex.Sub(k, k + 5) == "\\vert" && !IsAsciiAlphabetic(tex[k + 5]) ? k + 5 : -1;
if (afterBar >= 0)
{
result.Append("{|}");
i = afterBar;
continue;
}
}
if (Dropped.Contains(cw))
{
// sizes and styles that MathML leaves to the renderer: \bigl(, \limits, \displaystyle
Expand Down Expand Up @@ -1183,6 +1202,12 @@ void Close((int Open, List<int> Bars) frame, bool inBrackets, int close)
if (ch == "\\")
{
var cw = ControlWord(tex, i + 1);
// the text of \text and the like, whose bars are text (\text{a|b})
if (TextCommands.Contains(cw) && BracedArgument(tex, i + 1 + cw.Length) is var (_, afterText) && afterText >= 0)
{
i = afterText - 1;
continue;
}
// the brackets among the commands (\{, \}, \langle, \rangle) become the bracket they stand for; an
// escaped character (\|) is skipped
ch = cw switch
Expand Down Expand Up @@ -1256,13 +1281,15 @@ void Close((int Open, List<int> Bars) frame, bool inBrackets, int close)

/// <summary>
/// A bracket that a group opens and does not close, as in \phantom{(}, escaped: UnicodeMath would close it with an
/// empty fence at the end of the group. The text of \text and the like is left as it is.
/// empty fence at the end of the group; so are a closing bracket alone in a group (x^{)}) and a bracket
/// that is a script and does not close (x_(). The text of \text and the like is left as it is.
/// </summary>
private static string EscapeOpenBrackets(string latex)
{
var tex = new Text(latex);
var groups = new Stack<List<int>>();
var escaped = new List<int>();
var scripts = new HashSet<int>();
for (int i = 0; i < tex.Length; i++)
{
switch (tex[i])
Expand Down Expand Up @@ -1291,23 +1318,96 @@ private static string EscapeOpenBrackets(string latex)
case "(" or "[" when groups.Count > 0:
groups.Peek().Add(i);
break;
case ")" or "]" when groups.Count > 0 && groups.Peek().Count > 0:
groups.Peek().RemoveAt(groups.Peek().Count - 1);
case ")" or "]" when groups.Count > 0:
if (groups.Peek().Count > 0)
groups.Peek().RemoveAt(groups.Peek().Count - 1);
else if (Blank(tex, i, -1) == "{" && Blank(tex, i, 1) == "}")
{
// a group of a closing bracket alone (x^{)}) is escaped too; in others the bracket may close
// another (βŸ¨π‘Ž])
escaped.Add(i);
}
break;
case "_" or "^":
{
// a bracket that is a script and closes nothing, x^), or opens what does not close, x_(: Word's LaTeX
// groups a script in brackets (x^(a+b)), which UnicodeMath would read as the opening of what follows
int k = i + 1;
while (tex[k] == " ")
k++;
if (tex[k] is ")" or "]" || tex[k] is "(" or "[" && !BracketCloses(tex, k))
{
escaped.Add(k);
scripts.Add(k);
i = k;
}
break;
}
}
}
if (escaped.Count == 0)
return latex;
var result = new StringBuilder();
for (int i = 0; i < tex.Length; i++)
{
if (scripts.Contains(i))
result.Append('{');
if (escaped.Contains(i))
result.Append('\\');
result.Append(tex[i]);
{
// a closing bracket held, since \) and \] would end the LaTeX
result.Append('\\').Append(tex[i] switch
{
")" => "\uFDDA",
"]" => "\uFDDB",
var bracket => bracket,
});
}
else
result.Append(tex[i]);
if (scripts.Contains(i))
result.Append('}');
}
return result.ToString();
}

/// <summary>The character before (step -1) or after (step 1) the index, past the blanks.</summary>
private static string Blank(Text tex, int i, int step)
{
do
i += step;
while (i >= 0 && i < tex.Length && tex[i] == " ");
return i >= 0 && i < tex.Length ? tex[i] : null;
}

/// <summary>Whether the bracket at the index closes before the group around it ends.</summary>
private static bool BracketCloses(Text tex, int open)
{
int depth = 0, braces = 0;
for (int i = open; i < tex.Length; i++)
{
switch (tex[i])
{
case "\\":
i++;
break;
case "{":
braces++;
break;
case "}" when braces == 0:
return false;
case "}":
braces--;
break;
case "(" or "[":
depth++;
break;
case ")" or "]" when --depth == 0:
return true;
}
}
return false;
}

/// <summary>The count arguments (braced or single characters) at the index, and the index after them; null if missing.</summary>
private static (List<string> Arguments, int Next)? Arguments(Text tex, int i, int count)
{
Expand Down
16 changes: 11 additions & 5 deletions src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,13 @@ private string CheckBracing(string str, bool isBase = false)
{
if (Len(str) == 0)
return "{}";
if (Len(str) == 1 || SingleCommand().IsMatch(str) || IsGroup(str) || isBase && Ch(str, 0) == "\\" && IsUnit(str))
if (Len(str) == 1 && (isBase || !IsBracket(str)) || SingleCommand().IsMatch(str) || IsGroup(str) || isBase && Ch(str, 0) == "\\" && IsUnit(str))
return str;
return "{" + str + "}";
}
int length = Len(str);
if (length > 2 && !IsUnit(str) || length == 2)
// a script that is a bracket is braced, which the reader would otherwise read as the opening of what follows (x_{(})
if (length > 2 && !IsUnit(str) || length == 2 || length == 1 && !isBase && IsBracket(str))
str = "{" + str + "}";
return str;
}
Expand Down Expand Up @@ -597,9 +598,12 @@ private string TeX(XElement value, bool noAddParens = false)
value.ElementsAfterSelf().FirstOrDefault()?.DescendantsAndSelf().FirstOrDefault(d => !d.HasElements) is { } nextLeaf && IsBar(nextLeaf) &&
Text(nextLeaf) == "|" && Attr(nextLeaf, "intent") != ":text")
return "|\\!";
// a bar alone before other bars, which the reader would pair it with: {|}, the ordinary bar of TeX
if (val == "|" && Attr(value, "intent") == ":text" && value.ElementsAfterSelf().Concat(value.Ancestors().SelectMany(a => a.ElementsAfterSelf()))
.SelectMany(e => e.DescendantsAndSelf()).Any(d => IsBar(d) && Text(d) == "|"))
// a bar alone before other bars, which the reader would pair it with: {|}, the ordinary bar of TeX;
// or between brackets, where the reader would read it as their separator ({π‘₯β”‚π‘₯>0})
if (val == "|" && Attr(value, "intent") == ":text" && (value.ElementsAfterSelf().Concat(value.Ancestors().SelectMany(a => a.ElementsAfterSelf()))
.SelectMany(e => e.DescendantsAndSelf()).Any(d => IsBar(d) && Text(d) == "|") ||
value.Ancestors().Any(a => Name(a) == "mrow" && First(a) is { } open && open != value && Name(open) == "mo" &&
IsOpenDelimiter(Text(open)) && Attr(open, "intent") != ":text")))
return "{|}";
if (Attr(value, "intent") == ":text" && !standard)
{
Expand Down Expand Up @@ -877,6 +881,8 @@ private string EscapedStructureOperator(string val)
/// <summary>The operators that build a structure in UnicodeMath (β–­, √, ¦…), escaped when they are alone: \β–­.</summary>
internal static bool IsStructureOperator(string text) => text != null && Len(text) == 1 && "β–­β–β–’β—―βŸŒβ¬­β•±β•²β•³βˆšβˆ›βˆœβ’­Β¦β’žβ’œβ“’β–‘βŸ‘β¬„β‡³β¬β¬†β¬‡β¬Œβ– β’¨β’©β“’β“ˆβ’±β–ˆβ“˜β“β“‘β“œβ““β“‘β–’".Contains(text);

private static bool IsBracket(string str) => str is "(" or ")" or "[" or "]";

private static bool IsBar(XElement element) => element != null && Name(element) == "mo" && (Text(element) == "|" || Text(element) == "β€–");

private static bool IsCombiningMark(string text) =>
Expand Down
14 changes: 13 additions & 1 deletion tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ public async Task ReadsLessCommonCommands(string latex, string expectedLaTeX, st
}

// a slash and a bar are ordinary characters of LaTeX, which UnicodeMath reads as a fraction and an absolute value;
// so is a bracket that a group opens and does not close, which UnicodeMath would close
// so is a bracket that a group opens and does not close, which UnicodeMath would close, or a bracket alone as a
// script; a bar in text, after \bigm (a relation) or {|} is a bar alone, which no other bar pairs with
[Test]
[Arguments(@"a/b", @"a/b", @"π‘Ž\/𝑏")]
[Arguments(@"(a+b)/2", @"(a+b)/2", @"(π‘Ž+𝑏)\/2")]
Expand All @@ -194,6 +195,17 @@ public async Task ReadsLessCommonCommands(string latex, string expectedLaTeX, st
[Arguments(@"\mathstrut", @"\vphantom{(}", @"⇳\(")]
[Arguments(@"\sqrt{(}", @"\sqrt{(}", @"√\(")]
[Arguments(@"\text{(}", @"\text{(}", @"""(""")]
[Arguments(@"x_{[}", @"x_{[}", @"π‘₯_\[")]
[Arguments(@"x_(", @"x_{(}", @"π‘₯_\(")]
[Arguments(@"x^{)}", @"x^{)}", @"π‘₯^\)")]
[Arguments(@"x^{ ] }", @"x^{]}", @"π‘₯^\]")]
[Arguments(@"\text{a|b}", @"\text{a|b}", @"""a|b""")]
[Arguments(@"a\vert b", @"a|b", @"π‘Ž\|𝑏")]
[Arguments(@"\vert x\vert", @"|x|", @"|π‘₯|")]
[Arguments(@"a \Bigm\vert b", @"a|b", @"π‘Ž\|𝑏")]
[Arguments(@"|a \bigm| b|", @"\left|a{|}b\right|", @"|(π‘Ž\|𝑏)|")]
[Arguments(@"\{x \bigm| x>0\}", @"\{x|x>0\}", @"{π‘₯β”‚π‘₯>0}")]
[Arguments(@"\{x {|} x>0\}", @"\{x{|}x>0\}", @"{π‘₯\|π‘₯>0}")]
[Arguments(@"\langle a | b \rangle | c", @"\langle a|b\rangle|c", @"βŸ¨π‘Žβ”‚π‘βŸ©\|𝑐")]
[Arguments(@"a|b", @"a|b", @"π‘Ž\|𝑏")]
[Arguments(@"(a\|b)", @"(a\|b)", @"(π‘Ž\‖𝑏)")]
Expand Down
Loading