diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs index bb3e5f0..0a5e4e0 100644 --- a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs +++ b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs @@ -1281,15 +1281,17 @@ void Close((int Open, List Bars) frame, bool inBrackets, int close) /// /// 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; 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. + /// 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_(). A script in brackets that close is grouped whole (x^(a+b) as x^{(a+b)}). The + /// text of \text and the like is left as it is. /// private static string EscapeOpenBrackets(string latex) { var tex = new Text(latex); var groups = new Stack>(); var escaped = new List(); - var scripts = new HashSet(); + // the scripts grouped, from the index of their first character to that of their last + var scripts = new Dictionary(); for (int i = 0; i < tex.Length; i++) { switch (tex[i]) @@ -1330,27 +1332,32 @@ private static string EscapeOpenBrackets(string latex) 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 + // a bracket that is a script and closes nothing, x^), or opens what does not close, x_(, which + // UnicodeMath would read as the opening of what follows; a script in brackets that close, x^(a+b), + // is grouped whole, as Word's LaTeX writes it and Word reads it (x^{(a+b)}) int k = i + 1; while (tex[k] == " ") k++; - if (tex[k] is ")" or "]" || tex[k] is "(" or "[" && !BracketCloses(tex, k)) + var close = tex[k] is "(" or "[" ? ClosingBracket(tex, k) : -1; + if (close >= 0) + scripts[k] = close; + else if (tex[k] is "(" or "[" or ")" or "]") { escaped.Add(k); - scripts.Add(k); + scripts[k] = k; i = k; } break; } } } - if (escaped.Count == 0) + if (escaped.Count == 0 && scripts.Count == 0) return latex; + var ends = scripts.Values.ToList(); var result = new StringBuilder(); for (int i = 0; i < tex.Length; i++) { - if (scripts.Contains(i)) + if (scripts.ContainsKey(i)) result.Append('{'); if (escaped.Contains(i)) { @@ -1364,7 +1371,7 @@ private static string EscapeOpenBrackets(string latex) } else result.Append(tex[i]); - if (scripts.Contains(i)) + foreach (var _ in ends.Where(end => end == i)) result.Append('}'); } return result.ToString(); @@ -1379,8 +1386,8 @@ private static string Blank(Text tex, int i, int step) return i >= 0 && i < tex.Length ? tex[i] : null; } - /// Whether the bracket at the index closes before the group around it ends. - private static bool BracketCloses(Text tex, int open) + /// The index of the bracket that closes the one at the index before the group around it ends; -1 if none. + private static int ClosingBracket(Text tex, int open) { int depth = 0, braces = 0; for (int i = open; i < tex.Length; i++) @@ -1394,7 +1401,7 @@ private static bool BracketCloses(Text tex, int open) braces++; break; case "}" when braces == 0: - return false; + return -1; case "}": braces--; break; @@ -1402,10 +1409,10 @@ private static bool BracketCloses(Text tex, int open) depth++; break; case ")" or "]" when --depth == 0: - return true; + return i; } } - return false; + return -1; } /// The count arguments (braced or single characters) at the index, and the index after them; null if missing. diff --git a/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs b/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs index 743da6c..bae07c6 100644 --- a/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs +++ b/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs @@ -172,7 +172,7 @@ 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, 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 + // script (a script in brackets that close is grouped whole, x^(a+b), as Word reads it); 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")] @@ -199,6 +199,10 @@ public async Task ReadsLessCommonCommands(string latex, string expectedLaTeX, st [Arguments(@"x_(", @"x_{(}", @"𝑥_\(")] [Arguments(@"x^{)}", @"x^{)}", @"𝑥^\)")] [Arguments(@"x^{ ] }", @"x^{]}", @"𝑥^\]")] + [Arguments(@"x^(a+b)", @"x^{(a+b)}", @"𝑥^((𝑎+𝑏))")] + [Arguments(@"x_[i]y", @"x_{[i]}y", @"𝑥_[𝑖] 𝑦")] + [Arguments(@"x^ (a)(b)", @"x^{(a)}(b)", @"𝑥^((𝑎)) (𝑏)")] + [Arguments(@"e^(i\pi)_n", @"e_n^{(i\pi)}", @"𝑒_𝑛^((𝑖𝜋))")] [Arguments(@"\text{a|b}", @"\text{a|b}", @"""a|b""")] [Arguments(@"a\vert b", @"a|b", @"𝑎\|𝑏")] [Arguments(@"\vert x\vert", @"|x|", @"|𝑥|")]