diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs index 30118c1..ee9d7d8 100644 --- a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs +++ b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs @@ -1432,6 +1432,10 @@ private string Convert(string latex) } if (symbol.Length > 0 && Js.Ch(symbol, 0) != "\"") uniTeX.Append(symbol); + // a control word not known ends where its letters do: a space ends it for UnicodeMath too, which + // would otherwise read the letters or digits after it as part of it (\sgn r, \sgn2) + else if (symbol.Length > 0) + uniTeX.Append('\\').Append(cw).Append(tex[i] != null && tex[i].Length == 1 && IsAsciiLetterOrDigit(tex[i][0]) ? " " : ""); else if (cw.Length > 0) uniTeX.Append('\\').Append(cw); break; diff --git a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathInput.cs b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathInput.cs index c024c4e..17090da 100644 --- a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathInput.cs +++ b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathInput.cs @@ -128,7 +128,9 @@ private static int ClosingQuote(List chars, int open) /// resolveCW: replaces control words (\alpha) with the characters they stand for. /// Called with each control word that is not one (as \foo), which is kept as text. - public static string ResolveControlWords(string unicodemath, Action unknown = null) + /// Whether the text was read from LaTeX, where the space after a control word only ends it and + /// is not kept in the text of one not known. + public static string ResolveControlWords(string unicodemath, Action unknown = null, bool tex = false) { string previous = ""; return ControlWord().Replace(unicodemath, match => @@ -215,7 +217,7 @@ public static string ResolveControlWords(string unicodemath, Action unkn return GetUnicodeFraction(Js.Ch(cw, 4), Js.Ch(cw, 5)); // not a control word: upright text unknown?.Invoke("\\" + cw); - return "\"" + match.Value + "\""; + return "\"" + (tex ? "\\" + cw : match.Value) + "\""; }); } diff --git a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTranslator.cs b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTranslator.cs index e60a170..5db738b 100644 --- a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTranslator.cs +++ b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTranslator.cs @@ -79,7 +79,7 @@ private static string ToMathML(string unicodemath, bool display, bool alignmentM unicodemath = unicodemath.Trim(); var resolved = UnicodeMathInput.ResolveControlWords(unicodemath, strict ? unknown => throw new FormatException("Unknown control word " + unknown + (tex ? " in the LaTeX." : ".")) - : null); + : null, tex); // macros (\def, \newcommand: ⓜ) apply in the equation that defines them if (!tex && LaTeXReader.HasMacros(resolved)) resolved = LaTeXReader.ExpandMacros(resolved); diff --git a/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs b/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs index bf94513..22ee7be 100644 --- a/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs +++ b/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs @@ -355,14 +355,35 @@ public async Task RejectsEnvironmentsThatExpandForever() } [Test] - [Arguments(@"\foo x", @"Unknown control word \foox in the LaTeX.")] + [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) { await Assert.That(() => Equation.Parse(latex, EquationFormat.LaTeX, Strict)).Throws().WithMessage(message); await Assert.That(Equation.Parse(latex, EquationFormat.LaTeX).ToMathML().Descendants(MathMLNamespaces.MathML + "mtext")).IsNotEmpty(); } + [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")] + [Arguments(@"\text{\sgn} r", @"\sgn", "r")] + [Arguments(@"\sgn2", @"\sgn", "2")] + public async Task EndsUnknownControlWordsAtTheirLetters(string latex, string text, string after) + { + var math = Equation.Parse(latex, EquationFormat.LaTeX).ToMathML(); + await Assert.That(math.Descendants(MathMLNamespaces.MathML + "mtext").Select(e => e.Value).ToList()).IsEquivalentTo(new[] { text }); + await Assert.That(math.Descendants().Where(e => e.Name.LocalName is "mi" or "mn").Select(e => e.Value).Last()).IsEqualTo(after); + } + [Test] public async Task KeepsKnownControlWordsWhenStrict() {