diff --git a/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs b/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs index bb6a4b32..5dde09b4 100644 --- a/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs +++ b/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs @@ -1156,6 +1156,71 @@ public void TestText() { Assert.Equal(@"\mathrm{\pounds x\ y}", LaTeXParser.MathListToLaTeX(list).ToString()); } + [Fact] + public void TestInlineMathInsideText() { + var list = ParseLaTeX(@"\text{before $x+1$ after}"); + Assert.Equal(FontStyle.Roman, list[0].FontStyle); + Assert.Equal(FontStyle.Default, list[7].FontStyle); + Assert.Equal(FontStyle.Default, list[8].FontStyle); + Assert.Equal(FontStyle.Default, list[9].FontStyle); + Assert.Equal(FontStyle.Roman, list[10].FontStyle); + Assert.Equal(@"\mathrm{before\ }x+1\mathrm{\ after}", LaTeXParser.MathListToLaTeX(list).ToString()); + } + + [Fact] + public void TestEscapedDollarInsideTextIsLiteralAndRoundTrips() { + var list = ParseLaTeX(@"\text{price \$5}"); + Assert.Contains(list, atom => atom is Ordinary { Nucleus: "$" }); + var serialized = LaTeXParser.MathListToLaTeX(list).ToString(); + Assert.Contains(@"\$", serialized); + Assert.Equal(list, ParseLaTeX(serialized)); + } + + [Fact] + public void TestTextInlineMathCommandsBracesScriptsAndEscapedDollarRoundTrip() { + var input = @"\text{cases: $\frac{n^{2}}{2}$, \mathbf{Roman} and \$ literal}"; + var list = ParseLaTeX(input); + var serialized = LaTeXParser.MathListToLaTeX(list).ToString(); + Assert.Equal(list, ParseLaTeX(serialized)); + } + + [Fact] + public void TestIssue205CasesRoundTrip() { + var input = @"f(n) = \begin{cases} n/2, & \text{if $n$ is even} \\ 3n+1, & \text{if $n$ is odd} \end{cases}"; + var list = ParseLaTeX(input); + Assert.Equal(list, ParseLaTeX(LaTeXParser.MathListToLaTeX(list).ToString())); + } + + [Fact] + public void TestUnbalancedInlineMathInsideTextReportsSourcePosition() { + var input = @"\text{before $x after}"; + var builder = new LaTeXParser(input); + var (list, error) = builder.Build(); + Assert.Null(list); + Assert.Contains("Expected character not found: $", error); + Assert.Equal(input.Length - 1, builder.NextChar); + Assert.EndsWith($"↑ (pos {input.Length - 1})", LaTeXParser.HelpfulErrorMessage(error!, input, builder.NextChar)); + } + + [Fact] + public void TestTextStateIsRestoredWhenTextArgumentFails() { + var builder = new LaTeXParser(@"\text{bad \notacommand}"); + var (_, error) = builder.Build(); + Assert.NotNull(error); + Assert.False(builder.TextMode); + Assert.Equal(FontStyle.Default, builder.CurrentFontStyle); + } + + [Theory] + [InlineData(@"\text{a $$ b}")] + [InlineData(@"\text{a $$$ b}")] + public void TestDisplayDollarDelimitersInsideTextAreRejected(string input) { + var builder = new LaTeXParser(input); + var (_, error) = builder.Build(); + Assert.Contains("Display math delimiters $$ are not allowed inside text", error); + Assert.Equal(input.IndexOf("$$", StringComparison.Ordinal) + 1, builder.NextChar); + } + [Fact] public void TestScriptOrdering() { var list = ParseLaTeX(@"\int_a^b"); @@ -1634,4 +1699,4 @@ public void TestErrorSurrogates() { } } } -} \ No newline at end of file +} diff --git a/CSharpMath.Rendering.Tests/MathDisplay/Issue205NestedTextMath.png b/CSharpMath.Rendering.Tests/MathDisplay/Issue205NestedTextMath.png new file mode 100644 index 00000000..a3ffcfca Binary files /dev/null and b/CSharpMath.Rendering.Tests/MathDisplay/Issue205NestedTextMath.png differ diff --git a/CSharpMath.Rendering.Tests/TestRenderingMathData.cs b/CSharpMath.Rendering.Tests/TestRenderingMathData.cs index 569e546e..82619a79 100644 --- a/CSharpMath.Rendering.Tests/TestRenderingMathData.cs +++ b/CSharpMath.Rendering.Tests/TestRenderingMathData.cs @@ -15,6 +15,7 @@ public sealed class TestRenderingMathData : TestRenderingSharedData BuildInternal(bool oneCharOnly, char stopChar = '\0', M r ??= new MathList(); MathAtom? prevAtom = null; while (HasCharacters) { + if (TextMode && Chars[NextChar] == '$') { + NextChar++; + if (HasCharacters && Chars[NextChar] == '$') + return "Display math delimiters $$ are not allowed inside text"; + var oldTextMode = TextMode; + var oldFontStyle = CurrentFontStyle; + TextMode = false; + CurrentFontStyle = FontStyle.Default; + Result inlineMath; + try { + inlineMath = BuildInternal(false, '$'); + } finally { + TextMode = oldTextMode; + CurrentFontStyle = oldFontStyle; + } + if (inlineMath.Error is string inlineError) return inlineError; + r.Append(inlineMath.Match(value => value, _ => throw new InvalidCodePathException("Inline math unexpectedly failed"))); + prevAtom = r.Atoms.LastOrDefault(); + continue; + } + if (stopChar == '$' && Chars[NextChar] == '}') + return "Expected character not found: $"; MathAtom? atom = null; if (Chars[NextChar] == stopChar && stopChar > '\0') { NextChar++; @@ -702,6 +724,9 @@ static bool MathAtomToLaTeX(MathAtom atom, StringBuilder builder, case { Nucleus: "\u2212" }: builder.Append('-'); break; + case { Nucleus: "$" }: + builder.Append(@"\$"); + break; case { Nucleus: var aNucleus }: builder.Append(aNucleus); break; @@ -731,4 +756,4 @@ public static StringBuilder MathListToLaTeX(MathList mathList, StringBuilder? sb return sb; } } -} \ No newline at end of file +} diff --git a/CSharpMath/Atom/LaTeXSettings.cs b/CSharpMath/Atom/LaTeXSettings.cs index 3ef5599a..a52c9439 100644 --- a/CSharpMath/Atom/LaTeXSettings.cs +++ b/CSharpMath/Atom/LaTeXSettings.cs @@ -364,9 +364,14 @@ public static class LaTeXSettings { var readsToEnd = !command.AsSpan().StartsWithInvariant("math") && !command.AsSpan().StartsWithInvariant("text"); - return (readsToEnd ? parser.ReadUntil(stopChar, accumulate) : parser.ReadArgument()).Bind(r => { + Result result; + try { + result = readsToEnd ? parser.ReadUntil(stopChar, accumulate) : parser.ReadArgument(); + } finally { parser.CurrentFontStyle = oldFontStyle; parser.TextMode = oldSpacesAllowed; + } + return result.Bind(r => { if (readsToEnd) return OkStop(accumulate); else return OkStyled(r); @@ -1189,4 +1194,4 @@ atom is Accent accent // \varsupsetneqq -> ⫌ + U+FE00 (Variation Selector 1) Not dealing with variation selectors, thank you very much }; } -} \ No newline at end of file +}