diff --git a/.github/workflows/Test all projects.yml b/.github/workflows/Test all projects.yml index 67cc14c3..909d43c4 100644 --- a/.github/workflows/Test all projects.yml +++ b/.github/workflows/Test all projects.yml @@ -14,7 +14,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: '3.1.201' + dotnet-version: '3.1.301' - name: Setup JDK # Needed to run ANTLR for AngouriMath uses: actions/setup-java@v1 with: diff --git a/CSharpMath.CoreTests/BiDictionaryTests.cs b/CSharpMath.CoreTests/BiDictionaryTests.cs deleted file mode 100644 index 50549229..00000000 --- a/CSharpMath.CoreTests/BiDictionaryTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Xunit; - -namespace CSharpMath.CoreTests { - public class BiDictionaryTests { - [Fact] - public void TestRemove() { - var testBiDictionary = new CSharpMath.Structures.BiDictionary { - { 0, "0" }, - { 1, "1" }, - { 2, "8" }, - { 3, "10" } - }; - Assert.Equal(4, testBiDictionary.Firsts.Count); - Assert.Equal(4, testBiDictionary.Seconds.Count); - - Assert.True(testBiDictionary.Remove(2, "8")); - Assert.False(testBiDictionary.ContainsByFirst(2)); - Assert.False(testBiDictionary.ContainsBySecond("8")); - Assert.Equal(3, testBiDictionary.Firsts.Count); - Assert.Equal(3, testBiDictionary.Seconds.Count); - - // Remove with wrong first key - Assert.False(testBiDictionary.Remove(4, "10")); - Assert.False(testBiDictionary.ContainsByFirst(4)); - Assert.True(testBiDictionary.ContainsBySecond("10")); - Assert.Equal(3, testBiDictionary.Firsts.Count); - Assert.Equal(3, testBiDictionary.Seconds.Count); - - // Remove with wrong second key - Assert.False(testBiDictionary.Remove(3, "15")); - Assert.True(testBiDictionary.ContainsByFirst(3)); - Assert.False(testBiDictionary.ContainsBySecond("15")); - Assert.Equal(3, testBiDictionary.Firsts.Count); - Assert.Equal(3, testBiDictionary.Seconds.Count); - - // Remove when both exists but not corresponding to each other - Assert.True(testBiDictionary.Remove(0, "1")); - Assert.False(testBiDictionary.ContainsByFirst(0)); - Assert.False(testBiDictionary.ContainsBySecond("1")); - Assert.Single(testBiDictionary.Firsts); - Assert.Single(testBiDictionary.Seconds); - } - - [Fact] - public void TestAddOrReplace() { - var testBiDictionary = new CSharpMath.Structures.BiDictionary(); - - testBiDictionary.AddOrReplace(0, "Value1"); - Assert.Equal("Value1", testBiDictionary[0]); - Assert.Equal(0, testBiDictionary["Value1"]); - - testBiDictionary.AddOrReplace(2, "Value10"); - Assert.Equal("Value10", testBiDictionary[2]); - Assert.Equal(2, testBiDictionary["Value10"]); - - testBiDictionary.AddOrReplace(2, "Value2"); - Assert.Equal("Value2", testBiDictionary[2]); - Assert.Equal(2, testBiDictionary["Value2"]); - Assert.Equal(2, testBiDictionary.Firsts.Count); - Assert.Equal(2, testBiDictionary.Seconds.Count); - - testBiDictionary.AddOrReplace(3, "Value3"); - Assert.Equal("Value3", testBiDictionary[3]); - Assert.Equal(3, testBiDictionary["Value3"]); - - testBiDictionary.AddOrReplace(10, "Value3"); - Assert.Equal("Value3", testBiDictionary[10]); - Assert.Equal(10, testBiDictionary["Value3"]); - Assert.Equal(3, testBiDictionary.Firsts.Count); - Assert.Equal(3, testBiDictionary.Seconds.Count); - } - } -} diff --git a/CSharpMath.CoreTests/DictionaryTests.cs b/CSharpMath.CoreTests/DictionaryTests.cs new file mode 100644 index 00000000..b35afe48 --- /dev/null +++ b/CSharpMath.CoreTests/DictionaryTests.cs @@ -0,0 +1,37 @@ +using Xunit; +using CSharpMath.Structures; + +namespace CSharpMath.CoreTests { + public class DictionaryTests { + private AliasBiDictionary InitTestDict() { + return new AliasBiDictionary{ + { "0", 0 }, + { "zero", 0 }, + { "1", 1 } + }; + } + [Theory] + [InlineData("0", 2, 2, true)] + [InlineData("zero", 2, 2, true)] + [InlineData("1", 2, 1, true)] + [InlineData("2", 3, 2, false)] + public void TestRemoveByFirst(string remove, int expectedFTS, int expectedSTF, bool expectedRemoved) { + var bd = InitTestDict(); + var removed = bd.RemoveByFirst(remove); + Assert.Equal(expectedFTS, bd.FirstToSecond.Count); + Assert.Equal(expectedSTF, bd.SecondToFirst.Count); + Assert.Equal(expectedRemoved, removed); + } + [Theory] + [InlineData(0, 1, 1, true)] + [InlineData(1, 2, 1, true)] + [InlineData(2, 3, 2, false)] + public void TestRemoveBySecond(int remove, int expectedFTS, int expectedSTF, bool expectedRemoved) { + var bd = InitTestDict(); + var removed = bd.RemoveBySecond(remove); + Assert.Equal(expectedFTS, bd.FirstToSecond.Count); + Assert.Equal(expectedSTF, bd.SecondToFirst.Count); + Assert.Equal(expectedRemoved, removed); + } + } +} diff --git a/CSharpMath.CoreTests/LaTeXParserTest.cs b/CSharpMath.CoreTests/LaTeXParserTest.cs index a5c481da..95102782 100644 --- a/CSharpMath.CoreTests/LaTeXParserTest.cs +++ b/CSharpMath.CoreTests/LaTeXParserTest.cs @@ -10,10 +10,10 @@ namespace CSharpMath.CoreTests { public class LaTeXParserTest { public static MathList ParseLaTeX(string latex) { var builder = new LaTeXParser(latex); - if (builder.Build() is { } mathList) { - Assert.Null(builder.Error); - return mathList; - } else throw new Xunit.Sdk.NotNullException(); + var (mathList, error) = builder.Build(); + Assert.Null(error); + Assert.NotNull(mathList); + return mathList; } [Theory] @@ -46,14 +46,18 @@ public void TestBuilder(string input, Type[] atomTypes, string output) { Assert.Equal(output, LaTeXParser.MathListToLaTeX(list).ToString()); } + /// new[] { Base list }, new[] { Script of first atom }, new[] { Script of first atom inside script of first atom } [Theory] [InlineData("x^2", "x^2", new[] { typeof(Variable) }, new[] { typeof(Number) })] [InlineData("x^23", "x^23", new[] { typeof(Variable), typeof(Number) }, new[] { typeof(Number) })] [InlineData("x^{23}", "x^{23}", new[] { typeof(Variable) }, new[] { typeof(Number), typeof(Number) })] [InlineData("x^2^3", "x^2{}^3", new[] { typeof(Variable), typeof(Ordinary) }, new[] { typeof(Number) })] + [InlineData("x^^3", "x^{{}^3}", new[] { typeof(Variable), }, new[] { typeof(Ordinary) }, new[] { typeof(Number) })] [InlineData("x^{2^3}", "x^{2^3}", new[] { typeof(Variable) }, new[] { typeof(Number) }, new[] { typeof(Number) })] [InlineData("x^{^2*}", "x^{{}^2*}", new[] { typeof(Variable) }, new[] { typeof(Ordinary), typeof(BinaryOperator) }, new[] { typeof(Number) })] [InlineData("^2", "{}^2", new[] { typeof(Ordinary) }, new[] { typeof(Number) })] + [InlineData("^{^3}", "{}^{{}^3}", new[] { typeof(Ordinary), }, new[] { typeof(Ordinary) }, new[] { typeof(Number) })] + [InlineData("^^3", "{}^{{}^3}", new[] { typeof(Ordinary), }, new[] { typeof(Ordinary) }, new[] { typeof(Number) })] [InlineData("{}^2", "{}^2", new[] { typeof(Ordinary) }, new[] { typeof(Number) })] [InlineData("5{x}^2", "5x^2", new[] { typeof(Number), typeof(Variable) }, new Type[] { })] public void TestScript(string input, string output, params Type[][] atomTypes) { @@ -122,14 +126,72 @@ public void TestSymbols() { Assert.Equal(@"5\times 3^{2\div 2}", LaTeXParser.MathListToLaTeX(list).ToString()); } + [Theory] + [InlineData("%", false, "", false, "%\n")] + [InlineData("1%", true, "", false, "1%\n")] + [InlineData("%\n", false, "", false, "%\n")] + [InlineData("%\f", false, "", false, "%\n")] + [InlineData("%\r", false, "", false, "%\n")] + [InlineData("%\r\n", false, "", false, "%\n")] + [InlineData("%\v", false, "", false, "%\n")] + [InlineData("%\u0085", false, "", false, "%\n")] + [InlineData("%\u2028", false, "", false, "%\n")] + [InlineData("%\u2029", false, "", false, "%\n")] + [InlineData("1%1234\n", true, "1234", false, "1%1234\n")] + [InlineData("1%1234\f", true, "1234", false, "1%1234\n")] + [InlineData("1%1234\r", true, "1234", false, "1%1234\n")] + [InlineData("1%1234\r\n", true, "1234", false, "1%1234\n")] + [InlineData("1%1234\v", true, "1234", false, "1%1234\n")] + [InlineData("1%1234\u0085", true, "1234", false, "1%1234\n")] + [InlineData("1%1234\u2028", true, "1234", false, "1%1234\n")] + [InlineData("1%1234\u2029", true, "1234", false, "1%1234\n")] + [InlineData("% \na", false, " ", true, "% \na")] + [InlineData("% \fa", false, " ", true, "% \na")] + [InlineData("% \ra", false, " ", true, "% \na")] + [InlineData("% \r\na", false, " ", true, "% \na")] + [InlineData("% \va", false, " ", true, "% \na")] + [InlineData("% \u0085a", false, " ", true, "% \na")] + [InlineData("% \u2028a", false, " ", true, "% \na")] + [InlineData("% \u2029a", false, " ", true, "% \na")] + [InlineData("1% comment!! \\notacommand \na", true, " comment!! \\notacommand ", true, "1% comment!! \\notacommand \na")] + [InlineData("1% comment!! \\notacommand \fa", true, " comment!! \\notacommand ", true, "1% comment!! \\notacommand \na")] + [InlineData("1% comment!! \\notacommand \ra", true, " comment!! \\notacommand ", true, "1% comment!! \\notacommand \na")] + [InlineData("1% comment!! \\notacommand \r\na", true, " comment!! \\notacommand ", true, "1% comment!! \\notacommand \na")] + [InlineData("1% comment!! \\notacommand \va", true, " comment!! \\notacommand ", true, "1% comment!! \\notacommand \na")] + [InlineData("1% comment!! \\notacommand \u0085a", true, " comment!! \\notacommand ", true, "1% comment!! \\notacommand \na")] + [InlineData("1% comment!! \\notacommand \u2028a", true, " comment!! \\notacommand ", true, "1% comment!! \\notacommand \na")] + [InlineData("1% comment!! \\notacommand \u2029a", true, " comment!! \\notacommand ", true, "1% comment!! \\notacommand \na")] + public void TestComment(string input, bool hasBefore, string comment, bool hasAfter, string output) { + var list = ParseLaTeX(input); + IEnumerable> GetInspectors() { + if (hasBefore) yield return CheckAtom("1"); + yield return CheckAtom(comment); + if (hasAfter) yield return CheckAtom("a"); + } + Assert.Collection(list, GetInspectors().ToArray()); + Assert.Equal(output, LaTeXParser.MathListToLaTeX(list).ToString()); + } + [Theory] + [InlineData("\\sum%\\limits\n\\limits", true, "\\sum \\limits %\\limits\n")] + [InlineData("\\sum%\\limits\n\\nolimits", false, "\\sum \\nolimits %\\limits\n")] + [InlineData("\\sum \\limits %\\limits\n \\nolimits", false, "\\sum \\nolimits %\\limits\n")] + [InlineData("\\sum \\nolimits %\\limits\n \\limits", true, "\\sum \\limits %\\limits\n")] + public void TestCommentWithLimits(string input, bool limits, string output) { + var list = ParseLaTeX(input); + Assert.Collection(list, + CheckAtom("∑", op => Assert.Equal(limits, op.Limits)), + CheckAtom("\\limits")); + Assert.Equal(output, LaTeXParser.MathListToLaTeX(list).ToString()); + } + [Fact] public void TestFraction() { var list = ParseLaTeX(@"\frac1c"); Assert.Collection(list, CheckAtom("", fraction => { Assert.True(fraction.HasRule); - Assert.Null(fraction.LeftDelimiter); - Assert.Null(fraction.RightDelimiter); + Assert.Equal(Boundary.Empty, fraction.LeftDelimiter); + Assert.Equal(Boundary.Empty, fraction.RightDelimiter); Assert.Collection(fraction.Numerator, CheckAtom("1")); Assert.Collection(fraction.Denominator, CheckAtom("c")); }) @@ -241,11 +303,13 @@ public void TestKet() { // Scripts on left InlineData(@"\left(^2 \right )", new[] { typeof(Inner) }, new[] { typeof(Ordinary) }, @"(", @")", @"\left( {}^2\right) "), // Dot - InlineData(@"\left( 2 \right.", new[] { typeof(Inner) }, new[] { typeof(Number) }, @"(", @"", @"\left( 2\right. ") + InlineData(@"\left( 2 \right.", new[] { typeof(Inner) }, new[] { typeof(Number) }, @"(", null, @"\left( 2\right. "), + // Dot both sides + InlineData(@"\left.2\right.", new[] { typeof(Inner) }, new[] { typeof(Number) }, null, null, @"2"), ] public void TestLeftRight( string input, Type[] expectedOutputTypes, Type[] expectedInnerTypes, - string leftBoundary, string rightBoundary, string expectedLatex) { + string? leftBoundary, string? rightBoundary, string expectedLatex) { var list = ParseLaTeX(input); CheckAtomTypes(list, expectedOutputTypes); @@ -266,8 +330,8 @@ public void TestOverAndAtop(string input, string output, bool hasRule) { Assert.Collection(list, CheckAtom("", fraction => { Assert.Equal(hasRule, fraction.HasRule); - Assert.Null(fraction.LeftDelimiter); - Assert.Null(fraction.RightDelimiter); + Assert.Equal(Boundary.Empty, fraction.LeftDelimiter); + Assert.Equal(Boundary.Empty, fraction.RightDelimiter); Assert.Collection(fraction.Numerator, CheckAtom("1")); Assert.Collection(fraction.Denominator, CheckAtom("c")); }) @@ -285,8 +349,8 @@ public void TestOverAndAtopInParens(string input, string output, bool hasRule) { CheckAtom("+"), CheckAtom("", fraction => { Assert.Equal(hasRule, fraction.HasRule); - Assert.Null(fraction.LeftDelimiter); - Assert.Null(fraction.RightDelimiter); + Assert.Equal(Boundary.Empty, fraction.LeftDelimiter); + Assert.Equal(Boundary.Empty, fraction.RightDelimiter); Assert.Collection(fraction.Numerator, CheckAtom("1")); Assert.Collection(fraction.Denominator, CheckAtom("c")); }), @@ -301,13 +365,21 @@ public void TestOverAndAtopInParens(string input, string output, bool hasRule) { [InlineData(@"n \brack k", @"{n \brack k}", "[", "]")] [InlineData(@"n \brace k", @"{n \brace k}", "{", "}")] [InlineData(@"\binom{n}{k}", @"{n \choose k}", "(", ")")] - public void TestChooseBrackBraceBinomial(string input, string output, string left, string right) { + [InlineData(@"n \atopwithdelims() k", @"{n \choose k}", "(", ")")] + [InlineData(@"n \atopwithdelims [ ] k", @"{n \brack k}", "[", "]")] + [InlineData(@"n \atopwithdelims\{ \} k", @"{n \brace k}", "{", "}")] + [InlineData(@"n \atopwithdelims <> k", @"{n \atopwithdelims<> k}", "〈", "〉")] + [InlineData(@"n \atopwithdelims\Uparrow\downarrow k", @"{n \atopwithdelims\Uparrow\downarrow k}", "⇑", "↓")] + [InlineData(@"n \atopwithdelims.. k", @"{n \atop k}", null, null)] + [InlineData(@"n \atopwithdelims| . k", @"{n \atopwithdelims|. k}", "|", null)] + [InlineData(@"n \atopwithdelims .( k", @"{n \atopwithdelims.( k}", null, "(")] + public void TestChooseBrackBraceBinomial(string input, string output, string? left, string? right) { var list = ParseLaTeX(input); Assert.Collection(list, CheckAtom("", fraction => { Assert.False(fraction.HasRule); - Assert.Equal(left, fraction.LeftDelimiter); - Assert.Equal(right, fraction.RightDelimiter); + Assert.Equal(left, fraction.LeftDelimiter.Nucleus); + Assert.Equal(right, fraction.RightDelimiter.Nucleus); Assert.Collection(fraction.Numerator, CheckAtom("n")); Assert.Collection(fraction.Denominator, CheckAtom("k")); }) @@ -345,7 +417,7 @@ public void TestAccent() { Assert.Collection(accent.InnerList, CheckAtom("x")) ) ); - Assert.Equal(@"\bar{x}", LaTeXParser.MathListToLaTeX(list).ToString()); + Assert.Equal(@"\bar {x}", LaTeXParser.MathListToLaTeX(list).ToString()); } [Fact] @@ -415,7 +487,7 @@ public void TestMathStyle() { [InlineData("Bmatrix", "{", "}", @"\left\{ ", @"\right\} ")] [InlineData("vmatrix", "|", "|", @"\left| ", @"\right| ")] [InlineData("Vmatrix", "‖", "‖", @"\left\| ", @"\right\| ")] - public void TestMatrix(string env, string left, string right, string leftOutput, string rightOutput) { + public void TestMatrix(string env, string? left, string? right, string? leftOutput, string? rightOutput) { var list = ParseLaTeX($@"\begin{{{env}}} x & y \\ z & w \end{{{env}}}"); Table table; if (left is null && right is null) @@ -951,13 +1023,43 @@ public void TestCases2() { public void TestCustom() { var input = @"\lcm(a,b)"; var builder = new LaTeXParser(input); - var list = builder.Build(); + var (list, error) = builder.Build(); Assert.Null(list); - Assert.NotNull(builder.Error); + Assert.Equal(@"Invalid command \lcm", error); + + LaTeXSettings.CommandSymbols.Add(@"\lcm", new LargeOperator("lcm", false)); + list = ParseLaTeX(input); + Assert.Collection(list, + CheckAtom("lcm"), + CheckAtom("("), + CheckAtom("a"), + CheckAtom(","), + CheckAtom("b"), + CheckAtom(")") + ); + Assert.Equal(@"\lcm (a,b)", LaTeXParser.MathListToLaTeX(list).ToString()); + + LaTeXSettings.CommandSymbols.Add(@"lcm", new LargeOperator("lcm", false)); + LaTeXSettings.CommandSymbols.Add(@"lcm12", new LargeOperator("lcm12", false)); + LaTeXSettings.CommandSymbols.Add(@"lcm1234", new LargeOperator("lcm1234", false)); + LaTeXSettings.CommandSymbols.Add(@"lcm1235", new LargeOperator("lcm1235", false)); + + // Does not match custom atoms added above + list = ParseLaTeX("lc(a,b)"); + Assert.Collection(list, + CheckAtom("l"), + CheckAtom("c"), + CheckAtom("("), + CheckAtom("a"), + CheckAtom(","), + CheckAtom("b"), + CheckAtom(")") + ); + Assert.Equal(@"lc(a,b)", LaTeXParser.MathListToLaTeX(list).ToString()); - LaTeXSettings.Commands.Add("lcm", new LargeOperator("lcm", false)); - var list2 = ParseLaTeX(input); - Assert.Collection(list2, + // Baseline for lookup as a non-command (not starting with \) + list = ParseLaTeX("lcm(a,b)"); + Assert.Collection(list, CheckAtom("lcm"), CheckAtom("("), CheckAtom("a"), @@ -965,25 +1067,124 @@ public void TestCustom() { CheckAtom("b"), CheckAtom(")") ); - Assert.Equal(@"\lcm (a,b)", LaTeXParser.MathListToLaTeX(list2).ToString()); + Assert.Equal(@"\lcm (a,b)", LaTeXParser.MathListToLaTeX(list).ToString()); + + // Originally in https://github.com/verybadcat/CSharpMath/pull/143, + // the non-command dictionary of LaTeXCommandDictionary were implemented with a trie. + // With the above LaTeXSettings.CommandSymbols.Add calls, it would have looked like: + // [l] -> l[cm] -> lcm[12] -> @lcm12[3] -> lcm123[4] + // ^--> lcm123[5] + // where [square brackets] denote added characters compared to previous node + // and the @at sign denotes the node without an atom to provide + // Here we ensure that all behaviours of the trie carry over to the new SortedSet implementation + + // Test lookup fallbacks when trie node key (lcm12) does not fully match input (lcm1) + list = ParseLaTeX("lcm1(a,b)"); + Assert.Collection(list, + CheckAtom("lcm"), + CheckAtom("1"), + CheckAtom("("), + CheckAtom("a"), + CheckAtom(","), + CheckAtom("b"), + CheckAtom(")") + ); + Assert.Equal(@"\lcm 1(a,b)", LaTeXParser.MathListToLaTeX(list).ToString()); + + // Test lookup success for trie node between above case and below case + list = ParseLaTeX("lcm12(a,b)"); + Assert.Collection(list, + CheckAtom("lcm12"), + CheckAtom("("), + CheckAtom("a"), + CheckAtom(","), + CheckAtom("b"), + CheckAtom(")") + ); + Assert.Equal(@"lcm12(a,b)", LaTeXParser.MathListToLaTeX(list).ToString()); + + // Test lookup fallbacks when trie node key (lcm123) fully matches input (lcm123) but has no atoms to provide + list = ParseLaTeX("lcm123(a,b)"); + Assert.Collection(list, + CheckAtom("lcm12"), + CheckAtom("3"), + CheckAtom("("), + CheckAtom("a"), + CheckAtom(","), + CheckAtom("b"), + CheckAtom(")") + ); + Assert.Equal(@"lcm123(a,b)", LaTeXParser.MathListToLaTeX(list).ToString()); + + // Add a new shorter entry to ensure that the longest key matches instead of the last one + LaTeXSettings.CommandSymbols.Add(@"lcm123", new LargeOperator("lcm123", false)); + list = ParseLaTeX("lcm1234(a,b)"); + Assert.Collection(list, + CheckAtom("lcm1234"), + CheckAtom("("), + CheckAtom("a"), + CheckAtom(","), + CheckAtom("b"), + CheckAtom(")") + ); + Assert.Equal(@"lcm1234(a,b)", LaTeXParser.MathListToLaTeX(list).ToString()); } - [Fact] - public void TestFontSingle() { - var list = ParseLaTeX(@"\mathbf x"); - Assert.Collection(list, CheckAtom("x", - variable => Assert.Equal(FontStyle.Bold, variable.FontStyle))); - Assert.Equal(@"\mathbf{x}", LaTeXParser.MathListToLaTeX(list).ToString()); + [Theory] + [InlineData("mathnormal", false, FontStyle.Default, null)] + [InlineData("mathrm", false, FontStyle.Roman, "mathrm")] + [InlineData("rm", true, FontStyle.Roman, "mathrm")] + [InlineData("text", false, FontStyle.Roman, "mathrm")] + [InlineData("mathbf", false, FontStyle.Bold, "mathbf")] + [InlineData("bf", true, FontStyle.Bold, "mathbf")] + [InlineData("mathcal", false, FontStyle.Caligraphic, "mathcal")] + [InlineData("cal", true, FontStyle.Caligraphic, "mathcal")] + [InlineData("mathtt", false, FontStyle.Typewriter, "mathtt")] + [InlineData("tt", true, FontStyle.Typewriter, "mathtt")] + [InlineData("mathit", false, FontStyle.Italic, "mathit")] + [InlineData("mit", true, FontStyle.Italic, "mathit")] + [InlineData("it", true, FontStyle.Italic, "mathit")] + [InlineData("mathsf", false, FontStyle.SansSerif, "mathsf")] + [InlineData("sf", true, FontStyle.SansSerif, "mathsf")] + [InlineData("mathfrak", false, FontStyle.Fraktur, "mathfrak")] + [InlineData("frak", true, FontStyle.Fraktur, "mathfrak")] + [InlineData("mathbb", false, FontStyle.Blackboard, "mathbb")] + [InlineData("bb", true, FontStyle.Blackboard, "mathbb")] + [InlineData("mathbfit", false, FontStyle.BoldItalic, "mathbfit")] + [InlineData("bm", true, FontStyle.BoldItalic, "mathbfit")] + public void TestFont(string inputCommand, bool readsToEnd, FontStyle style, string? outputCommand) { + // Without braces + var list = ParseLaTeX($@"w\{inputCommand} xyz"); + Assert.Collection(list, + CheckAtom("w", w => Assert.Equal(FontStyle.Default, w.FontStyle)), + CheckAtom("x", x => Assert.Equal(style, x.FontStyle)), + CheckAtom("y", y => Assert.Equal(readsToEnd ? style : FontStyle.Default, y.FontStyle)), + CheckAtom("z", z => Assert.Equal(readsToEnd ? style : FontStyle.Default, z.FontStyle))); + Assert.Equal(outputCommand is null ? "wxyz" : + readsToEnd ? $@"w\{outputCommand}{{xyz}}" : $@"w\{outputCommand}{{x}}yz", LaTeXParser.MathListToLaTeX(list).ToString()); + + // With braces + list = ParseLaTeX(readsToEnd ? $@"w{{\{inputCommand} xy}}z" : $@"w\{inputCommand}{{xy}}z"); + Assert.Collection(list, + CheckAtom("w", w => Assert.Equal(FontStyle.Default, w.FontStyle)), + CheckAtom("x", x => Assert.Equal(style, x.FontStyle)), + CheckAtom("y", y => Assert.Equal(style, y.FontStyle)), + CheckAtom("z", z => Assert.Equal(FontStyle.Default, z.FontStyle))); + Assert.Equal(outputCommand is null ? "wxyz" : $@"w\{outputCommand}{{xy}}z", LaTeXParser.MathListToLaTeX(list).ToString()); } - [Fact] - public void TestFontMultipleCharacters() { - var list = ParseLaTeX(@"\frak{xy}"); + [Theory] + [InlineData(@"\mathit\mathrm xy")] + [InlineData(@"\mathit\mathrm{x}y")] + [InlineData(@"\mathit{\mathrm x}y")] + [InlineData(@"\mathit{\mathrm{x}}y")] + public void TestFontRecursive(string input) { + var list = ParseLaTeX(input); Assert.Collection(list, - CheckAtom("x", variable => Assert.Equal(FontStyle.Fraktur, variable.FontStyle)), - CheckAtom("y", variable => Assert.Equal(FontStyle.Fraktur, variable.FontStyle)) + CheckAtom("x", variable => Assert.Equal(FontStyle.Roman, variable.FontStyle)), + CheckAtom("y", variable => Assert.Equal(FontStyle.Default, variable.FontStyle)) ); - Assert.Equal(@"\mathfrak{xy}", LaTeXParser.MathListToLaTeX(list).ToString()); + Assert.Equal(@"\mathrm{x}y", LaTeXParser.MathListToLaTeX(list).ToString()); } [Fact] @@ -1018,13 +1219,14 @@ public void TestFontInsideScript() { [Fact] public void TestText() { - var list = ParseLaTeX(@"\text{x y}"); + var list = ParseLaTeX(@"\text {\pounds x y}"); Assert.Collection(list, - CheckAtom(@"x", variable => Assert.Equal(FontStyle.Roman, variable.FontStyle)), - CheckAtom(" "), - CheckAtom(@"y", variable => Assert.Equal(FontStyle.Roman, variable.FontStyle)) + CheckAtom("£", pounds => Assert.Equal(FontStyle.Roman, pounds.FontStyle)), + CheckAtom("x", x => Assert.Equal(FontStyle.Roman, x.FontStyle)), + CheckAtom(" ", space => Assert.Equal(FontStyle.Roman, space.FontStyle)), + CheckAtom("y", y => Assert.Equal(FontStyle.Roman, y.FontStyle)) ); - Assert.Equal(@"\mathrm{x\ y}", LaTeXParser.MathListToLaTeX(list).ToString()); + Assert.Equal(@"\mathrm{\pounds x\ y}", LaTeXParser.MathListToLaTeX(list).ToString()); } [Fact] @@ -1171,6 +1373,44 @@ public void TestOperatorName(string operatorname, string output) { Assert.Equal(output, LaTeXParser.MathListToLaTeX(list).ToString()); } + [Theory] + [InlineData(@"\TeX")] + [InlineData(@"\left.\mathrm{T\! \raisebox{-4.5mu}{E}\mkern-2.25muX}\right.")] + public void TestTeX(string input) { + var list = ParseLaTeX(input); + Assert.Collection(list, + CheckAtom("", inner => { + Assert.Equal(Boundary.Empty, inner.LeftBoundary); + Assert.Equal(Boundary.Empty, inner.RightBoundary); + Assert.Equal(FontStyle.Default, inner.FontStyle); + Assert.Collection(inner.InnerList, + CheckAtom("T", t => Assert.Equal(FontStyle.Roman, t.FontStyle)), + CheckAtom("", space => { + Assert.Equal(FontStyle.Roman, space.FontStyle); + var expected = -1 / 6f * Structures.Space.EmWidth; + Assert.Equal(expected.Length, space.Length); + Assert.Equal(expected.IsMu, space.IsMu); + }), + CheckAtom("", raise => { + Assert.Equal(FontStyle.Roman, raise.FontStyle); + Assert.Equal(-1 / 2f * Structures.Space.ExHeight, raise.Raise); + Assert.Collection(raise.InnerList, + CheckAtom("E", e => Assert.Equal(FontStyle.Roman, e.FontStyle))); + }), + CheckAtom("", space => { + Assert.Equal(FontStyle.Roman, space.FontStyle); + var expected = -1 / 8f * Structures.Space.EmWidth; + Assert.Equal(expected.Length, space.Length); + Assert.Equal(expected.IsMu, space.IsMu); + }), + CheckAtom("X", x => Assert.Equal(FontStyle.Roman, x.FontStyle)) + ); + }) + ); + Assert.Equal(@"\mathrm{T\! \raisebox{-4.5mu}{E}\mkern-2.25muX}", + LaTeXParser.MathListToLaTeX(list).ToString()); + } + [Theory, InlineData("0", 1, @"Error: Error Message 0 @@ -1216,49 +1456,41 @@ public void TestHelpfulErrorMessage(string input, int index, string expected) { Assert.Equal(expected.Replace("\r", null), actual); } + const string EnquiryControlChar = "\x5"; // https://en.wikipedia.org/wiki/Enquiry_character [Theory, - InlineData(@"x^^2", @"Error: ^ cannot appear as an argument to a command -x^^2 - ↑ (pos 3)"), - InlineData(@"x^_2", @"Error: _ cannot appear as an argument to a command -x^_2 - ↑ (pos 3)"), - InlineData(@"x_^2", @"Error: ^ cannot appear as an argument to a command -x_^2 - ↑ (pos 3)"), - InlineData(@"x__2", @"Error: _ cannot appear as an argument to a command -x__2 - ↑ (pos 3)"), - InlineData(@"x^&2", @"Error: & cannot appear as an argument to a command -x^&2 - ↑ (pos 3)"), - InlineData(@"x^}2", @"Error: } cannot appear as an argument to a command + InlineData(@"\", @"Error: Invalid command \ +\ +↑ (pos 1)"), + InlineData(@"\" + EnquiryControlChar, @"Error: Invalid command \" + EnquiryControlChar + @" +\" + EnquiryControlChar + @" +↑ (pos 1)"), + InlineData(@"\" + EnquiryControlChar + "a", @"Error: Invalid command \" + EnquiryControlChar + @" +\" + EnquiryControlChar + @"a +↑ (pos 1)"), + InlineData(@"x^}2", @"Error: Missing opening brace x^}2 ↑ (pos 3)"), - InlineData(@"x_&2", @"Error: & cannot appear as an argument to a command -x_&2 - ↑ (pos 3)"), - InlineData(@"x_}2", @"Error: } cannot appear as an argument to a command -x_}2 - ↑ (pos 3)"), - InlineData(@"\sqrt^2", @"Error: ^ cannot appear as an argument to a command -\sqrt^2 - ↑ (pos 6)"), - InlineData(@"\sqrt_2", @"Error: _ cannot appear as an argument to a command -\sqrt_2 - ↑ (pos 6)"), - InlineData(@"\sqrt&2", @"Error: & cannot appear as an argument to a command -\sqrt&2 - ↑ (pos 6)"), - InlineData(@"\sqrt}2", @"Error: } cannot appear as an argument to a command + InlineData(@"x_ }2", @"Error: Missing opening brace +x_ }2 + ↑ (pos 4)"), + InlineData(@"{x_}", @"Error: Missing opening brace +{x_} + ↑ (pos 4)"), + InlineData(@"\sqrt}2", @"Error: Missing opening brace \sqrt}2 ↑ (pos 6)"), InlineData(@"\notacommand", @"Error: Invalid command \notacommand \notacommand - ↑ (pos 12)"), +↑ (pos 1)"), + InlineData(@"\notacommand x", @"Error: Invalid command \notacommand +\notacommand x +↑ (pos 1)"), InlineData(@"\sqrt[5+3", @"Error: Expected character not found: ] \sqrt[5+3 ↑ (pos 9)"), + InlineData(@"\sqrt[5+3}", @"Error: Missing opening brace +\sqrt[5+3} + ↑ (pos 10)"), InlineData(@"{5+3", @"Error: Missing closing brace {5+3 ↑ (pos 4)"), @@ -1271,27 +1503,27 @@ public void TestHelpfulErrorMessage(string input, int index, string expected) { InlineData(@"{1+\frac{3+2", @"Error: Missing closing brace {1+\frac{3+2 ↑ (pos 12)"), - InlineData(@"1+\left", @"Error: Missing delimiter for left + InlineData(@"1+\left", @"Error: Missing delimiter for \left 1+\left ↑ (pos 7)"), - InlineData(@"\left{", @"Error: Missing \right for \left with delimiter { -\left{ - ↑ (pos 6)"), - InlineData(@"\left(\frac12\right", @"Error: Missing delimiter for right + InlineData(@"\left\{", @"Error: Missing \right for \left with delimiter { +\left\{ + ↑ (pos 7)"), + InlineData(@"\left(\frac12\right", @"Error: Missing delimiter for \right \left(\frac12\right ↑ (pos 19)"), - InlineData(@"\left 5 + 3 \right)", @"Error: Invalid delimiter for \left: 5 + InlineData(@"\left 5 + 3 \right)", @"Error: Invalid delimiter 5 \left 5 + 3 \right) ↑ (pos 7)"), - InlineData(@"\left(\frac12\right + 3", @"Error: Invalid delimiter for \right: + + InlineData(@"\left(\frac12\right + 3", @"Error: Invalid delimiter + \left(\frac12\right + 3 ↑ (pos 21)"), - InlineData(@"\left\notadelimiter 5 + 3 \right)", @"Error: Invalid delimiter for \left: notadelimiter + InlineData(@"\left\notadelimiter 5 + 3 \right)", @"Error: Invalid delimiter \notadelimiter \left\notadelimiter 5 + 3 \right) - ↑ (pos 19)"), - InlineData(@"\left(\frac12\right\notadelimiter + 3", @"Error: Invalid delimiter for \right: notadelimiter -···2\right\notadelimiter + 3 - ↑ (pos 33)"), + ↑ (pos 6)"), + InlineData(@"\left(\frac12\right\notadelimiter + 3", @"Error: Invalid delimiter \notadelimiter +\left(\frac12\right\notadelimiter + 3 + ↑ (pos 20)"), InlineData(@"5 + 3 \right)", @"Error: Missing \left 5 + 3 \right) ↑ (pos 12)"), @@ -1304,6 +1536,12 @@ public void TestHelpfulErrorMessage(string input, int index, string expected) { InlineData(@"5+ \left|\frac12\right| \right)", @"Error: Missing \left ···\frac12\right| \right) ↑ (pos 30)"), + InlineData(@"{\it", @"Error: Missing closing brace +{\it + ↑ (pos 4)"), + InlineData(@"\it}", @"Error: Missing opening brace +\it} + ↑ (pos 4)"), InlineData(@"\begin matrix \end matrix", @"Error: Missing { \begin matrix \end matrix ↑ (pos 7)"), @@ -1344,8 +1582,8 @@ public void TestHelpfulErrorMessage(string input, int index, string expected) { ···env} x \end{notanenv} ↑ (pos 33)"), InlineData(@"\begin{matrix} \notacommand \end{matrix}", @"Error: Invalid command \notacommand -···{matrix} \notacommand \end{matrix} - ↑ (pos 27)"), +\begin{matrix} \notacommand \end{matrix} + ↑ (pos 16)"), InlineData(@"\begin{displaylines} x & y \end{displaylines}", @"Error: displaylines environment can only have 1 column ··· y \end{displaylines} ↑ (pos 45)"), @@ -1370,30 +1608,30 @@ public void TestHelpfulErrorMessage(string input, int index, string expected) { InlineData(@"\left(\begin{matrix}\right)", @"Error: Missing \end{matrix} ···(\begin{matrix}\right) ↑ (pos 26)"), - InlineData(@"\Bra^2", @"Error: ^ cannot appear as an argument to a command -\Bra^2 - ↑ (pos 5)"), - InlineData(@"\Bra_2", @"Error: _ cannot appear as an argument to a command -\Bra_2 - ↑ (pos 5)"), - InlineData(@"\Bra&2", @"Error: & cannot appear as an argument to a command -\Bra&2 - ↑ (pos 5)"), - InlineData(@"\Bra}2", @"Error: } cannot appear as an argument to a command + InlineData(@"\Bra}2", @"Error: Missing opening brace \Bra}2 ↑ (pos 5)"), - InlineData(@"\Ket^2", @"Error: ^ cannot appear as an argument to a command -\Ket^2 - ↑ (pos 5)"), - InlineData(@"\Ket_2", @"Error: _ cannot appear as an argument to a command -\Ket_2 - ↑ (pos 5)"), - InlineData(@"\Ket&2", @"Error: & cannot appear as an argument to a command -\Ket&2 - ↑ (pos 5)"), - InlineData(@"\Ket}2", @"Error: } cannot appear as an argument to a command + InlineData(@"\Ket}2", @"Error: Missing opening brace \Ket}2 ↑ (pos 5)"), + InlineData(@"\Bra{\notacommand}", @"Error: Invalid command \notacommand +\Bra{\notacommand} + ↑ (pos 6)"), + InlineData(@"\Ket{\notacommand}", @"Error: Invalid command \notacommand +\Ket{\notacommand} + ↑ (pos 6)"), + InlineData(@"\operatorname", @"Error: Expected { +\operatorname + ↑ (pos 13)"), + InlineData(@"\operatorname {", @"Error: Expected } +\operatorname { + ↑ (pos 15)"), + InlineData(@"\operatorname{a", @"Error: Expected } +\operatorname{a + ↑ (pos 15)"), + InlineData(@"\operatorname {a|}", @"Error: Expected } +\operatorname {a|} + ↑ (pos 16)"), ] public void TestErrors(string badInput, string expected) { var (list, actual) = LaTeXParser.MathListFromLaTeX(badInput); diff --git a/CSharpMath.CoreTests/LaTeXSettingsTests.cs b/CSharpMath.CoreTests/LaTeXSettingsTests.cs index 267e0ffb..39202d11 100644 --- a/CSharpMath.CoreTests/LaTeXSettingsTests.cs +++ b/CSharpMath.CoreTests/LaTeXSettingsTests.cs @@ -6,30 +6,16 @@ namespace CSharpMath.CoreTests { public class LaTeXSettingsTests { [Fact] public void ForAsciiHandlesAllInputs() { - for (sbyte i = -36 + 1; i != -36; i++) // Break loop at arbitrary negative value (-36) + for (char i = '\0'; i <= sbyte.MaxValue; i++) switch (i) { - case var _ when i < 0: - Assert.Throws( - () => LaTeXSettings.ForAscii(i) - ); - break; - case var _ when i <= ' ': - case (sbyte)'\u007F': - case (sbyte)'$': - case (sbyte)'%': - case (sbyte)'#': - case (sbyte)'&': - case (sbyte)'~': - case (sbyte)'\'': - case (sbyte)'^': - case (sbyte)'_': - case (sbyte)'{': - case (sbyte)'}': - case (sbyte)'\\': - Assert.Null(LaTeXSettings.ForAscii(i)); + case '\\': // The command character is handled specially + case '$': // Unimplemented + case '#': // Unimplemented + case '~': // Unimplemented + Assert.DoesNotContain(LaTeXSettings.Commands, kvp => kvp.Key == i.ToString()); break; default: - Assert.NotNull(LaTeXSettings.ForAscii(i)); + Assert.Contains(LaTeXSettings.Commands, kvp => kvp.Key == i.ToString()); break; } } @@ -38,14 +24,14 @@ public void CommandForAtomIgnoresInnerLists() { var atom = new Atoms.Accent("\u0308", new MathList(new Atoms.Number("1"))); atom.Superscript.Add(new Atoms.Number("4")); atom.Subscript.Add(new Atoms.Variable("x")); - Assert.Equal("ddot", LaTeXSettings.CommandForAtom(atom)); + Assert.Equal(@"\ddot", LaTeXSettings.CommandForAtom(atom)); } [Fact] public void AtomForCommandGeneratesACopy() { - var atom = LaTeXSettings.AtomForCommand("int"); + var atom = LaTeXSettings.AtomForCommand(@"\int"); if (atom == null) throw new Xunit.Sdk.NotNullException(); atom.IndexRange = Range.NotFound; - var atom2 = LaTeXSettings.AtomForCommand("int"); + var atom2 = LaTeXSettings.AtomForCommand(@"\int"); if (atom2 == null) throw new Xunit.Sdk.NotNullException(); Assert.Equal(Range.Zero, atom2.IndexRange); } diff --git a/CSharpMath.CoreTests/MathAtomTest.cs b/CSharpMath.CoreTests/MathAtomTest.cs index 4d6dc8f2..66e59f1c 100644 --- a/CSharpMath.CoreTests/MathAtomTest.cs +++ b/CSharpMath.CoreTests/MathAtomTest.cs @@ -56,8 +56,8 @@ public void TestCopyFraction() { var list = new MathList { atom, atom2, atom3 }; var list2 = new MathList { atom3, atom2 }; var frac = new Fraction(list, list2, false) { - LeftDelimiter = "a", - RightDelimiter = "b" + LeftDelimiter = new Boundary("a"), + RightDelimiter = new Boundary("b") }; Assert.IsType(frac); @@ -66,8 +66,8 @@ public void TestCopyFraction() { CheckClone(copy, frac); CheckClone(copy.Numerator, frac.Numerator); Assert.False(copy.HasRule); - Assert.Equal("a", copy.LeftDelimiter); - Assert.Equal("b", copy.RightDelimiter); + Assert.Equal(new Boundary("a"), copy.LeftDelimiter); + Assert.Equal(new Boundary("b"), copy.RightDelimiter); } [Fact] public void TestCopyRadical() { diff --git a/CSharpMath.CoreTests/MathListTest.cs b/CSharpMath.CoreTests/MathListTest.cs index 9be93fca..bfbb81df 100644 --- a/CSharpMath.CoreTests/MathListTest.cs +++ b/CSharpMath.CoreTests/MathListTest.cs @@ -142,7 +142,7 @@ public void TestListCopyWithFusedItems() { [Fact] public void TestListFinalizedCopy() { - var input = @"-52x^{13+y}_{15-} + (-12.3 *)\frac{-12}{15.2}\int^\sqrt[!\ ]{=(}_0 \theta"; + var input = @"-52x^{13+y}_{15-} + (-12.3 *)\frac{-12}{15.2}\int^\sqrt[!\ ]{=(}_0 \theta%:)" + "\n" + @","; var list = LaTeXParserTest.ParseLaTeX(input); Assert.ThrowsAny(() => CheckListContents(list)); Assert.ThrowsAny(() => CheckListContents(list.Clone(false))); @@ -172,7 +172,10 @@ static void CheckListContents(MathList? list) { CheckAtomNucleusAndRange(")", 12, 1), CheckAtomNucleusAndRange("", 13, 1), CheckAtomNucleusAndRange("∫", 14, 1), - CheckAtomNucleusAndRange("θ", 15, 1) + CheckAtomNucleusAndRange("θ", 15, 1), + // Comments are not given ranges as they won't affect typesetting + CheckAtomNucleusAndRange(":)", Range.UndefinedInt, Range.UndefinedInt), + CheckAtomNucleusAndRange(",", 16, 1) ); Assert.Collection(list.Atoms[2].Superscript, CheckAtomNucleusAndRange("13", 0, 2), diff --git a/CSharpMath.CoreTests/TypesetterTests.cs b/CSharpMath.CoreTests/TypesetterTests.cs index 68fefb40..228406ca 100644 --- a/CSharpMath.CoreTests/TypesetterTests.cs +++ b/CSharpMath.CoreTests/TypesetterTests.cs @@ -51,8 +51,8 @@ public void TestSimpleVariable(string latex) => Assert.Equal(10, line.Width); }); - [Theory, InlineData("xyzw"), InlineData("xy2w"), InlineData("1234")] - public void TestVariablesAndNumbers(string latex) => + [Theory, InlineData("xyzw"), InlineData("xy2w"), InlineData("12.3"), InlineData("|`@/"), InlineData("1`y.")] + public void TestVariablesNumbersAndOrdinaries(string latex) => TestOuter(latex, 4, 14, 4, 40, d => { var line = Assert.IsType>(d); @@ -62,6 +62,25 @@ public void TestVariablesAndNumbers(string latex) => Assert.Equal(new Range(0, 4), line.Range); Assert.False(line.HasScript); + Assert.Equal(14, line.Ascent); + Assert.Equal(4, line.Descent); + Assert.Equal(40, line.Width); + }); + [Theory] + [InlineData("%\n1234", "1234")] + [InlineData("12.b% comment ", "12.b")] + [InlineData("|`% \\notacommand \u2028@/", "|`@/")] + public void TestComments(string latex, string resultText) => + TestOuter(latex, 4, 14, 4, 40, + d => { + var line = Assert.IsType>(d); + Assert.Equal(4, line.Atoms.Count); + Assert.All(line.Atoms, Assert.IsNotType); + AssertText(resultText, line); + Assert.Equal(new PointF(), line.Position); + Assert.Equal(new Range(0, 4), line.Range); + Assert.False(line.HasScript); + Assert.Equal(14, line.Ascent); Assert.Equal(4, line.Descent); Assert.Equal(40, line.Width); @@ -242,8 +261,8 @@ public void TestEquationWithOperatorsAndRelations(string latex) => Assert.Equal(80, line.Width); }); - [Theory, InlineData('[', ']'), InlineData('(', '}'), InlineData('{', ']')] // Using ) confuses the test explorer... - public void TestInner(char left, char right) => + [Theory, InlineData("[", "]"), InlineData("(", @"\}"), InlineData(@"\{", "]")] // Using ) confuses the test explorer... + public void TestInner(string left, string right) => TestOuter($@"a\left{left}x\right{right}", 2, 14, 4, 43.333, d => Assert.IsType>(d), d => { @@ -258,7 +277,7 @@ public void TestInner(char left, char right) => Approximately.At(13.333, 0, glyph.Position); Assert.Equal(Range.NotFound, glyph.Range); Assert.False(glyph.HasScript); - Assert.Equal(left, glyph.Glyph); + Assert.Equal(left[^1], glyph.Glyph); TestList(1, 14, 4, 10, 23.333, 0, LinePosition.Regular, Range.UndefinedInt, d => { @@ -274,7 +293,7 @@ public void TestInner(char left, char right) => Approximately.At(33.333, 0, glyph2.Position); Assert.Equal(Range.NotFound, glyph2.Range); Assert.False(glyph2.HasScript); - Assert.Equal(right, glyph2.Glyph); + Assert.Equal(right[^1], glyph2.Glyph); }); [Theory, InlineData("\\sqrt2", "", "2"), InlineData("\\sqrt[3]2", "3", "2")] public void TestRadical(string latex, string degree, string radicand) => @@ -462,11 +481,11 @@ public void TestColor() => TestOuter(@"\color{red}\color{blue}x\colorbox{yellow}\colorbox{green}yz", 3, 14, 4, 30, l1 => { Assert.Null(l1.BackColor); - Assert.Equal(LaTeXSettings.PredefinedColors["red"], l1.TextColor); + Assert.Equal(LaTeXSettings.PredefinedColors.FirstToSecond["red"], l1.TextColor); TestList(1, 14, 4, 10, 0, 0, LinePosition.Regular, Range.UndefinedInt, l2 => { Assert.Null(l2.BackColor); - Assert.Equal(LaTeXSettings.PredefinedColors["blue"], l2.TextColor); + Assert.Equal(LaTeXSettings.PredefinedColors.FirstToSecond["blue"], l2.TextColor); TestList(1, 14, 4, 10, 0, 0, LinePosition.Regular, Range.UndefinedInt, d => { var line = Assert.IsType>(d); Assert.Single(line.Atoms); @@ -474,16 +493,16 @@ public void TestColor() => Assert.Equal(new PointF(), line.Position); Assert.False(line.HasScript); Assert.Null(line.BackColor); - Assert.Equal(LaTeXSettings.PredefinedColors["blue"], line.TextColor); + Assert.Equal(LaTeXSettings.PredefinedColors.FirstToSecond["blue"], line.TextColor); })(l2); })(l1); }, l1 => { - Assert.Equal(LaTeXSettings.PredefinedColors["yellow"], l1.BackColor); + Assert.Equal(LaTeXSettings.PredefinedColors.FirstToSecond["yellow"], l1.BackColor); Assert.Null(l1.TextColor); TestList(1, 14, 4, 10, 10, 0, LinePosition.Regular, Range.UndefinedInt, l2 => { - Assert.Equal(LaTeXSettings.PredefinedColors["green"], l2.BackColor); + Assert.Equal(LaTeXSettings.PredefinedColors.FirstToSecond["green"], l2.BackColor); Assert.Null(l2.TextColor); TestList(1, 14, 4, 10, 0, 0, LinePosition.Regular, Range.UndefinedInt, d => { var line = Assert.IsType>(d); diff --git a/CSharpMath.Editor.Tests/KeyPressTests.cs b/CSharpMath.Editor.Tests/KeyPressTests.cs index e5d84b13..c2908744 100644 --- a/CSharpMath.Editor.Tests/KeyPressTests.cs +++ b/CSharpMath.Editor.Tests/KeyPressTests.cs @@ -54,7 +54,7 @@ public void NoDuplicateValues() { //Decimals T(@"0123456789.", K.D0, K.D1, K.D2, K.D3, K.D4, K.D5, K.D6, K.D7, K.D8, K.D9, K.Decimal), //Basic operators - T(@"+-\times \div :\% ,!\infty \angle \degree \vert \log \ln ", + T(@"+-\times \div :\% ,!\infty \angle \degree |\log \ln ", K.Plus, K.Minus, K.Multiply, K.Divide, K.Ratio, K.Percentage, K.Comma, K.Factorial, K.Infinity, K.Angle, K.Degree, K.VerticalBar, K.Logarithm, K.NaturalLogarithm), T(@"''\partial \leftarrow \uparrow \rightarrow \downarrow \ ", @@ -159,7 +159,7 @@ public void NoDuplicateValues() { K.Sine, K.Cosine, K.Right, K.Right, K.ArcTangent, K.Left, K.Left, K.Left, K.Tangent), T(@"e^{\square }", K.Power, K.Left, K.SmallE, K.Right), T(@"e^■", K.Power, K.Left, K.SmallE, K.Left), - T(@"\left| x\right| \vert y\vert ", K.Absolute, K.SmallX, K.Right, K.VerticalBar, K.SmallY, K.VerticalBar), + T(@"\left| x\right| |y|", K.Absolute, K.SmallX, K.Right, K.VerticalBar, K.SmallY, K.VerticalBar), T(@"\left( 1\right) (2)", K.BothRoundBrackets, K.D1, K.Right, K.LeftRoundBracket, K.D2, K.RightRoundBracket), T(@"1\left( 2\left[ 3\left\{ ■\right\} \right] \right) ", K.BothRoundBrackets, K.BothSquareBrackets, K.BothCurlyBrackets, K.Left, K.D3, K.Left, K.Left, K.D2, K.Left, K.Left, K.D1, K.Left, K.Left, K.Right, K.Right, K.Right, K.Right, K.Right, K.Right), @@ -373,7 +373,7 @@ public void Return(params K[] inputs) => T(@"\frac{(1+2)}{■}", K.LeftRoundBracket, K.D1, K.Plus, K.D2, K.RightRoundBracket, K.Slash), T(@"\frac{\left( 1+2\right) }{■}", K.BothRoundBrackets, K.D1, K.Plus, K.D2, K.Right, K.Slash), - T(@"\vert 1+\frac{2\vert }{■}", K.VerticalBar, K.D1, K.Plus, K.D2, K.VerticalBar, K.Slash), + T(@"|1+\frac{2|}{■}", K.VerticalBar, K.D1, K.Plus, K.D2, K.VerticalBar, K.Slash), T(@"\frac{\left| 1+2\right| }{■}", K.Absolute, K.D1, K.Plus, K.D2, K.Right, K.Slash), T(@"1+\frac{2}{■}", K.D1, K.Plus, K.D2, K.Slash), T(@"1-\frac{2}{■}", K.D1, K.Minus, K.D2, K.Slash), diff --git a/CSharpMath.Editor/MathKeyboard.cs b/CSharpMath.Editor/MathKeyboard.cs index 5dfb427f..104aaec8 100644 --- a/CSharpMath.Editor/MathKeyboard.cs +++ b/CSharpMath.Editor/MathKeyboard.cs @@ -490,207 +490,207 @@ void InsertSymbolName(string name, bool subscript = false, bool superscript = fa InsertInner("|", "|"); break; case MathKeyboardInput.BaseEPower: - InsertAtom(LaTeXSettings.ForAscii((sbyte)'e') - ?? throw new InvalidCodePathException("LaTeXDefaults.ForAscii((byte)'e') is null")); + InsertAtom(LaTeXSettings.AtomForCommand("e") + ?? throw new InvalidCodePathException($"{nameof(LaTeXSettings.AtomForCommand)} returned null for e")); HandleScriptButton(true); break; case MathKeyboardInput.Logarithm: - InsertSymbolName("log"); + InsertSymbolName(@"\log"); break; case MathKeyboardInput.NaturalLogarithm: - InsertSymbolName("ln"); + InsertSymbolName(@"\ln"); break; case MathKeyboardInput.LogarithmWithBase: - InsertSymbolName("log", subscript: true); + InsertSymbolName(@"\log", subscript: true); break; case MathKeyboardInput.Sine: - InsertSymbolName("sin"); + InsertSymbolName(@"\sin"); break; case MathKeyboardInput.Cosine: - InsertSymbolName("cos"); + InsertSymbolName(@"\cos"); break; case MathKeyboardInput.Tangent: - InsertSymbolName("tan"); + InsertSymbolName(@"\tan"); break; case MathKeyboardInput.Cotangent: - InsertSymbolName("cot"); + InsertSymbolName(@"\cot"); break; case MathKeyboardInput.Secant: - InsertSymbolName("sec"); + InsertSymbolName(@"\sec"); break; case MathKeyboardInput.Cosecant: - InsertSymbolName("csc"); + InsertSymbolName(@"\csc"); break; case MathKeyboardInput.ArcSine: - InsertSymbolName("arcsin"); + InsertSymbolName(@"\arcsin"); break; case MathKeyboardInput.ArcCosine: - InsertSymbolName("arccos"); + InsertSymbolName(@"\arccos"); break; case MathKeyboardInput.ArcTangent: - InsertSymbolName("arctan"); + InsertSymbolName(@"\arctan"); break; case MathKeyboardInput.ArcCotangent: - InsertSymbolName("arccot"); + InsertSymbolName(@"\arccot"); break; case MathKeyboardInput.ArcSecant: - InsertSymbolName("arcsec"); + InsertSymbolName(@"\arcsec"); break; case MathKeyboardInput.ArcCosecant: - InsertSymbolName("arccsc"); + InsertSymbolName(@"\arccsc"); break; case MathKeyboardInput.HyperbolicSine: - InsertSymbolName("sinh"); + InsertSymbolName(@"\sinh"); break; case MathKeyboardInput.HyperbolicCosine: - InsertSymbolName("cosh"); + InsertSymbolName(@"\cosh"); break; case MathKeyboardInput.HyperbolicTangent: - InsertSymbolName("tanh"); + InsertSymbolName(@"\tanh"); break; case MathKeyboardInput.HyperbolicCotangent: - InsertSymbolName("coth"); + InsertSymbolName(@"\coth"); break; case MathKeyboardInput.HyperbolicSecant: - InsertSymbolName("sech"); + InsertSymbolName(@"\sech"); break; case MathKeyboardInput.HyperbolicCosecant: - InsertSymbolName("csch"); + InsertSymbolName(@"\csch"); break; case MathKeyboardInput.AreaHyperbolicSine: - InsertSymbolName("arsinh"); + InsertSymbolName(@"\arsinh"); break; case MathKeyboardInput.AreaHyperbolicCosine: - InsertSymbolName("arcosh"); + InsertSymbolName(@"\arcosh"); break; case MathKeyboardInput.AreaHyperbolicTangent: - InsertSymbolName("artanh"); + InsertSymbolName(@"\artanh"); break; case MathKeyboardInput.AreaHyperbolicCotangent: - InsertSymbolName("arcoth"); + InsertSymbolName(@"\arcoth"); break; case MathKeyboardInput.AreaHyperbolicSecant: - InsertSymbolName("arsech"); + InsertSymbolName(@"\arsech"); break; case MathKeyboardInput.AreaHyperbolicCosecant: - InsertSymbolName("arcsch"); + InsertSymbolName(@"\arcsch"); break; case MathKeyboardInput.LimitWithBase: - InsertSymbolName("lim", subscript: true); + InsertSymbolName(@"\lim", subscript: true); break; case MathKeyboardInput.Integral: - InsertSymbolName("int"); + InsertSymbolName(@"\int"); break; case MathKeyboardInput.IntegralLowerLimit: - InsertSymbolName("int", subscript: true); + InsertSymbolName(@"\int", subscript: true); break; case MathKeyboardInput.IntegralUpperLimit: - InsertSymbolName("int", superscript: true); + InsertSymbolName(@"\int", superscript: true); break; case MathKeyboardInput.IntegralBothLimits: - InsertSymbolName("int", subscript: true, superscript: true); + InsertSymbolName(@"\int", subscript: true, superscript: true); break; case MathKeyboardInput.Summation: - InsertSymbolName("sum"); + InsertSymbolName(@"\sum"); break; case MathKeyboardInput.SummationLowerLimit: - InsertSymbolName("sum", subscript: true); + InsertSymbolName(@"\sum", subscript: true); break; case MathKeyboardInput.SummationUpperLimit: - InsertSymbolName("sum", superscript: true); + InsertSymbolName(@"\sum", superscript: true); break; case MathKeyboardInput.SummationBothLimits: - InsertSymbolName("sum", subscript: true, superscript: true); + InsertSymbolName(@"\sum", subscript: true, superscript: true); break; case MathKeyboardInput.Product: - InsertSymbolName("prod"); + InsertSymbolName(@"\prod"); break; case MathKeyboardInput.ProductLowerLimit: - InsertSymbolName("prod", subscript: true); + InsertSymbolName(@"\prod", subscript: true); break; case MathKeyboardInput.ProductUpperLimit: - InsertSymbolName("prod", superscript: true); + InsertSymbolName(@"\prod", superscript: true); break; case MathKeyboardInput.ProductBothLimits: - InsertSymbolName("prod", subscript: true, superscript: true); + InsertSymbolName(@"\prod", subscript: true, superscript: true); break; case MathKeyboardInput.DoubleIntegral: - InsertSymbolName("iint"); + InsertSymbolName(@"\iint"); break; case MathKeyboardInput.TripleIntegral: - InsertSymbolName("iiint"); + InsertSymbolName(@"\iiint"); break; case MathKeyboardInput.QuadrupleIntegral: - InsertSymbolName("iiiint"); + InsertSymbolName(@"\iiiint"); break; case MathKeyboardInput.ContourIntegral: - InsertSymbolName("oint"); + InsertSymbolName(@"\oint"); break; case MathKeyboardInput.DoubleContourIntegral: - InsertSymbolName("oiint"); + InsertSymbolName(@"\oiint"); break; case MathKeyboardInput.TripleContourIntegral: - InsertSymbolName("oiiint"); + InsertSymbolName(@"\oiiint"); break; case MathKeyboardInput.ClockwiseIntegral: - InsertSymbolName("intclockwise"); + InsertSymbolName(@"\intclockwise"); break; case MathKeyboardInput.ClockwiseContourIntegral: - InsertSymbolName("varointclockwise"); + InsertSymbolName(@"\varointclockwise"); break; case MathKeyboardInput.CounterClockwiseContourIntegral: - InsertSymbolName("ointctrclockwise"); + InsertSymbolName(@"\ointctrclockwise"); break; case MathKeyboardInput.LeftArrow: - InsertSymbolName("leftarrow"); + InsertSymbolName(@"\leftarrow"); break; case MathKeyboardInput.UpArrow: - InsertSymbolName("uparrow"); + InsertSymbolName(@"\uparrow"); break; case MathKeyboardInput.RightArrow: - InsertSymbolName("rightarrow"); + InsertSymbolName(@"\rightarrow"); break; case MathKeyboardInput.DownArrow: - InsertSymbolName("downarrow"); + InsertSymbolName(@"\downarrow"); break; case MathKeyboardInput.PartialDifferential: - InsertSymbolName("partial"); + InsertSymbolName(@"\partial"); break; case MathKeyboardInput.NotEquals: - InsertSymbolName("neq"); + InsertSymbolName(@"\neq"); break; case MathKeyboardInput.LessOrEquals: - InsertSymbolName("leq"); + InsertSymbolName(@"\leq"); break; case MathKeyboardInput.GreaterOrEquals: - InsertSymbolName("geq"); + InsertSymbolName(@"\geq"); break; case MathKeyboardInput.Multiply: - InsertSymbolName("times"); + InsertSymbolName(@"\times"); break; case MathKeyboardInput.Divide: - InsertSymbolName("div"); + InsertSymbolName(@"\div"); break; case MathKeyboardInput.Infinity: - InsertSymbolName("infty"); + InsertSymbolName(@"\infty"); break; case MathKeyboardInput.Degree: - InsertSymbolName("degree"); + InsertSymbolName(@"\degree"); break; case MathKeyboardInput.Angle: - InsertSymbolName("angle"); + InsertSymbolName(@"\angle"); break; case MathKeyboardInput.LeftCurlyBracket: - InsertSymbolName("{"); + InsertSymbolName(@"\{"); break; case MathKeyboardInput.RightCurlyBracket: - InsertSymbolName("}"); + InsertSymbolName(@"\}"); break; case MathKeyboardInput.Percentage: - InsertSymbolName("%"); + InsertSymbolName(@"\%"); break; case MathKeyboardInput.Space: - InsertSymbolName(" "); + InsertSymbolName(@"\ "); break; case MathKeyboardInput.Prime: InsertAtom(new Atoms.Prime(1)); @@ -772,9 +772,8 @@ void InsertSymbolName(string name, bool subscript = false, bool superscript = fa case MathKeyboardInput.SmallX: case MathKeyboardInput.SmallY: case MathKeyboardInput.SmallZ: - InsertAtom(LaTeXSettings.ForAscii(checked((sbyte)input)) - ?? throw new InvalidCodePathException - ($"Invalid LaTeX character {input} was handled by ascii case")); + InsertAtom(LaTeXSettings.AtomForCommand(new string((char)input, 1)) + ?? throw new InvalidCodePathException($"{nameof(LaTeXSettings.AtomForCommand)} returned null for {input}")); break; case MathKeyboardInput.Alpha: case MathKeyboardInput.Beta: diff --git a/CSharpMath.Evaluation.Tests/EvaluationTests.cs b/CSharpMath.Evaluation.Tests/EvaluationTests.cs index 48ac2eb4..12a2632b 100644 --- a/CSharpMath.Evaluation.Tests/EvaluationTests.cs +++ b/CSharpMath.Evaluation.Tests/EvaluationTests.cs @@ -64,10 +64,10 @@ public void Numbers(string number, string output) => @"\times \Pi \times \Sigma \times \Upsilon \times \Phi \times \Psi \times \Omega ", @"\alpha \times \beta \times \chi \times \delta \times \Delta \times \epsilon \times \eta " + @"\times \gamma \times \Gamma \times \iota \times \kappa \times \lambda \times \Lambda \times \mu " + - @"\times \nu \times \omega \times \Omega \times \omicron \times \phi \times \Phi \times \pi " + - @"\times \Pi \times \psi \times \Psi \times \rho \times \sigma \times \Sigma \times \tau " + - @"\times \theta \times \Theta \times \upsilon \times \Upsilon \times \varepsilon \times \varkappa " + - @"\times \varphi \times \varpi \times \varrho \times \varsigma \times \xi \times \Xi \times \zeta ")] + @"\times \nu \times \omega \times \Omega \times \omicron \times \phi \times \Phi \times \Pi " + + @"\times \psi \times \Psi \times \rho \times \sigma \times \Sigma \times \tau \times \theta " + + @"\times \Theta \times \upsilon \times \Upsilon \times \varepsilon \times \varkappa \times \varphi " + + @"\times \varpi \times \varrho \times \varsigma \times \xi \times \Xi \times \zeta \times \pi ")] [InlineData(@"a_2", @"a_2", @"a_2")] [InlineData(@"a_2+a_2", @"a_2+a_2", @"2\times a_2")] [InlineData(@"a_{23}", @"a_{23}", @"a_{23}")] diff --git a/CSharpMath.Evaluation/Evaluation.cs b/CSharpMath.Evaluation/Evaluation.cs index de724429..5e6867fa 100644 --- a/CSharpMath.Evaluation/Evaluation.cs +++ b/CSharpMath.Evaluation/Evaluation.cs @@ -142,8 +142,8 @@ static Result TryMakeSet(MathItem.Comma c, bool leftClosed, bool right { "[", ("]", Precedence.BracketContext) }, { "{", ("}", Precedence.BraceContext) }, }; - static readonly Dictionary<(string left, string right), Func>> BracketHandlers = - new Dictionary<(string left, string right), Func>> { + static readonly Dictionary<(string? left, string? right), Func>> BracketHandlers = + new Dictionary<(string? left, string? right), Func>> { { ("(", ")"), item => item switch { null => "Missing math inside ( )", MathItem.Comma c => TryMakeSet(c, false, false), @@ -308,7 +308,7 @@ _ when LaTeXSettings.CommandForAtom(atom) is string s => MathS.Var(s + subscript (@this, error) = BracketHandlers.TryGetValue((left, right), out handler) ? handler(@this) - : $"Unrecognized bracket pair {left} {right}"; + : $"Unrecognized bracket pair {left ?? "(empty)"} {right ?? "(empty)"}"; if (error != null) return error; goto handleThis; case Atoms.UnaryOperator { Nucleus: "+" }: diff --git a/CSharpMath.Ios.Example/AppDelegate.cs b/CSharpMath.Ios.Example/AppDelegate.cs index 6ddab56e..9b29b8b8 100644 --- a/CSharpMath.Ios.Example/AppDelegate.cs +++ b/CSharpMath.Ios.Example/AppDelegate.cs @@ -7,7 +7,6 @@ namespace CSharpMath.Ios.Example { // as well as listening (and optionally responding) to application events from iOS. [Register("AppDelegate")] public class AppDelegate : UIApplicationDelegate { - public override UIWindow Window { get; set; } public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions) { Window = new UIWindow { RootViewController = new IosMathViewController() }; diff --git a/CSharpMath.Ios.Tests/CSharpMath.Ios.Tests.csproj b/CSharpMath.Ios.Tests/CSharpMath.Ios.Tests.csproj index 67a38908..6ea6090e 100644 --- a/CSharpMath.Ios.Tests/CSharpMath.Ios.Tests.csproj +++ b/CSharpMath.Ios.Tests/CSharpMath.Ios.Tests.csproj @@ -148,4 +148,4 @@ - + \ No newline at end of file diff --git a/CSharpMath.Ios.Tests/Tests.cs b/CSharpMath.Ios.Tests/Tests.cs index bddd2fb6..0016ef0a 100644 --- a/CSharpMath.Ios.Tests/Tests.cs +++ b/CSharpMath.Ios.Tests/Tests.cs @@ -44,7 +44,7 @@ async Task Test(string directory, Action init, string file, // The following are produced by inherently different implementations, so they are not comparable case nameof(TestData.Cyrillic): case nameof(TestData.ErrorInvalidColor): - case nameof(TestData.ErrorInvalidCommand): + case nameof(TestData.ErrorMissingArgument): case nameof(TestData.ErrorMissingBrace): break; default: diff --git a/CSharpMath.Rendering.Tests/MathDisplay/ErrorInvalidColor.png b/CSharpMath.Rendering.Tests/MathDisplay/ErrorInvalidColor.png index b301b28e..f0975954 100644 Binary files a/CSharpMath.Rendering.Tests/MathDisplay/ErrorInvalidColor.png and b/CSharpMath.Rendering.Tests/MathDisplay/ErrorInvalidColor.png differ diff --git a/CSharpMath.Rendering.Tests/MathDisplay/ErrorInvalidCommand.png b/CSharpMath.Rendering.Tests/MathDisplay/ErrorInvalidCommand.png deleted file mode 100644 index 8d4c7897..00000000 Binary files a/CSharpMath.Rendering.Tests/MathDisplay/ErrorInvalidCommand.png and /dev/null differ diff --git a/CSharpMath.Rendering.Tests/MathDisplay/ErrorMissingArgument.png b/CSharpMath.Rendering.Tests/MathDisplay/ErrorMissingArgument.png new file mode 100644 index 00000000..b24201a9 Binary files /dev/null and b/CSharpMath.Rendering.Tests/MathDisplay/ErrorMissingArgument.png differ diff --git a/CSharpMath.Rendering.Tests/MathInline/ErrorInvalidColor.png b/CSharpMath.Rendering.Tests/MathInline/ErrorInvalidColor.png index b301b28e..f0975954 100644 Binary files a/CSharpMath.Rendering.Tests/MathInline/ErrorInvalidColor.png and b/CSharpMath.Rendering.Tests/MathInline/ErrorInvalidColor.png differ diff --git a/CSharpMath.Rendering.Tests/MathInline/ErrorInvalidCommand.png b/CSharpMath.Rendering.Tests/MathInline/ErrorInvalidCommand.png deleted file mode 100644 index 8d4c7897..00000000 Binary files a/CSharpMath.Rendering.Tests/MathInline/ErrorInvalidCommand.png and /dev/null differ diff --git a/CSharpMath.Rendering.Tests/MathInline/ErrorMissingArgument.png b/CSharpMath.Rendering.Tests/MathInline/ErrorMissingArgument.png new file mode 100644 index 00000000..b24201a9 Binary files /dev/null and b/CSharpMath.Rendering.Tests/MathInline/ErrorMissingArgument.png differ diff --git a/CSharpMath.Rendering.Tests/TestCommandDisplay.cs b/CSharpMath.Rendering.Tests/TestCommandDisplay.cs index bc74a65a..01e7962e 100644 --- a/CSharpMath.Rendering.Tests/TestCommandDisplay.cs +++ b/CSharpMath.Rendering.Tests/TestCommandDisplay.cs @@ -11,7 +11,7 @@ public TestCommandDisplay() => typefaces = Fonts.GlobalTypefaces.ToArray(); readonly Typography.OpenFont.Typeface[] typefaces; public static IEnumerable AllCommandValues => - Atom.LaTeXSettings.Commands.Values + Atom.LaTeXSettings.CommandSymbols.SecondToFirst.Keys .SelectMany(v => v.Nucleus.EnumerateRunes()) .Distinct() .OrderBy(r => r.Value) diff --git a/CSharpMath.Rendering.Tests/TestRendering.cs b/CSharpMath.Rendering.Tests/TestRendering.cs index 7fbfcd4b..3b0fec0f 100644 --- a/CSharpMath.Rendering.Tests/TestRendering.cs +++ b/CSharpMath.Rendering.Tests/TestRendering.cs @@ -180,11 +180,11 @@ protected void Run( { "ScriptLineStyle", new TPainter { LineStyle = Atom.LineStyle.Script } }, { "ScriptScriptLineStyle", new TPainter { LineStyle = Atom.LineStyle.ScriptScript } }, { "GlyphBoxColor", new TPainter { GlyphBoxColor = ( - new TPainter().UnwrapColor(Atom.LaTeXSettings.PredefinedColors["green"]), - new TPainter().UnwrapColor(Atom.LaTeXSettings.PredefinedColors["blue"]) + new TPainter().UnwrapColor(Atom.LaTeXSettings.PredefinedColors.FirstToSecond["green"]), + new TPainter().UnwrapColor(Atom.LaTeXSettings.PredefinedColors.FirstToSecond["blue"]) ) } }, { "TextColor", new TPainter { TextColor = - new TPainter().UnwrapColor(Atom.LaTeXSettings.PredefinedColors["orange"]) } }, + new TPainter().UnwrapColor(Atom.LaTeXSettings.PredefinedColors.FirstToSecond["orange"]) } }, }; public static TheoryData MathPainterSettingsData => PainterSettingsData(); public static TheoryData TextPainterSettingsData => PainterSettingsData(); diff --git a/CSharpMath.Rendering.Tests/TestRenderingMathData.cs b/CSharpMath.Rendering.Tests/TestRenderingMathData.cs index bfc3d365..9a55c02e 100644 --- a/CSharpMath.Rendering.Tests/TestRenderingMathData.cs +++ b/CSharpMath.Rendering.Tests/TestRenderingMathData.cs @@ -86,7 +86,7 @@ public sealed class TestRenderingMathData : TestRenderingSharedData GetEnumerator() => public const string Cyrillic = @"А а\ Б б\ В в\ Г г\ Д д\ Е е\ Ё ё\ Ж ж\\ З з\ И и\ Й й\ К к\ Л л\ М м\ Н н\ О о\ П п" + @"\\ Р р\ С с\ Т т\ У у\ Ф ф\ Х х\ Ц ц\ Ч ч\\ Ш ш\ Щ щ\ Ъ ъ\ Ы ы\ Ь ь\ Э э\ Ю ю\ Я я"; public const string Color = @"\color{#000088}a\color{#0000FF}b\color{#008800}c\color{#008888}d\color{#0088FF}e\color{#00FF00}f\color{#00FF88}g\color{#00FFFF}h\color{#880000}i\color{#880088}j\color{#8800FF}k\color{#888800}l\color{#888888}m\color{#8888FF}n\color{#88FF00}o\color{#88FF88}p\color{#88FFFF}q\color{#FF0000}r\color{#FF0088}s\color{#FF00FF}t\color{#FF8800}u\color{#FF8888}v\color{#FF88FF}w\color{#FFFF00}x\color{#FFFF88}y\color{#FFFFFF}z"; - public const string ErrorInvalidCommand = @"\color{#000088}a\color{#0000FF}b\color{#008800}c\color{#008888}d\color{#0088FF}e\color{#00FF00}f\color{#00FF88}g\color{#00FFFF}h\color{#880000}i\color{#880088}j\color{#8800FF}k\color{#888800}l\color{#888888}m\color{#8888FF}n\color{#88FF00}o\color{#88FF88}p\color{#88FFFF}q\color{#FF0000}r\color{#FF0088}s\color{#FF00FF}t\color{#FF8800}u\color{#FF8888}v\color{#FF88FF}w\color{#FFFF00}x\color{#FFFF88}y\color{#FFFFFF}\notacommand"; - public const string ErrorInvalidColor = @"\color{#000088}a\color{#0000FF}b\color{#008800}c\color{#008888}d\color{#0088FF}e\color{#00FF00}f\color{0F8}g\color{#00FFFF}h\color{#880000}i\color{#880088}j\color{#8800FF}k\color{#888800}l\color{#888888}m\color{#8888FF}n\color{#88FF00}o\color{#88FF88}p\color{#88FFFF}q\color{#FF0000}r\color{#FF0088}s\color{#FF00FF}t\color{#FF8800}u\color{#FF8888}v\color{#FF88FF}w\color{#FFFF00}x\color{#FFFF88}y\color{#FFFFFF}z"; + public const string ErrorInvalidColor = @"\color{#000088}a\color{#0000FF}b\color{#008800}c\color{#008888}d\color{#0088FF}e\color{#00FF00}f\color{00FF88}g\color{#00FFFF}h\color{#880000}i\color{#880088}j\color{#8800FF}k\color{#888800}l\color{#888888}m\color{#8888FF}n\color{#88FF00}o\color{#88FF88}p\color{#88FFFF}q\color{#FF0000}r\color{#FF0088}s\color{#FF00FF}t\color{#FF8800}u\color{#FF8888}v\color{#FF88FF}w\color{#FFFF00}x\color{#FFFF88}y\color{#FFFFFF}z"; + public const string ErrorMissingArgument = @"\color{#000088}a\color{#0000FF}b\color{#008800}c\color{#008888}d\color{#0088FF}e\color{#00FF00}f\color{#00FF88}g\color{#00FFFF}h\color{#880000}i\color{#880088}j\color{#8800FF}k\color{#888800}l\color{#888888}m\color{#8888FF}n\color{#88FF00}o\color{#88FF88}p\color{#88FFFF}q\color{#FF0000}r\color{#FF0088}s\color{#FF00FF}t\color{#FF8800}u\color{#FF8888}v\color{#FF88FF}w\color{#FFFF00}x\color{#FFFF88}y\color{#FFFFFF}z\color"; public const string ErrorMissingBrace = @"}z"; } } diff --git a/CSharpMath.Rendering.Tests/TextCenter/ErrorInvalidColor.png b/CSharpMath.Rendering.Tests/TextCenter/ErrorInvalidColor.png index 55b4a6c4..51e071a0 100644 Binary files a/CSharpMath.Rendering.Tests/TextCenter/ErrorInvalidColor.png and b/CSharpMath.Rendering.Tests/TextCenter/ErrorInvalidColor.png differ diff --git a/CSharpMath.Rendering.Tests/TextCenter/ErrorInvalidCommand.png b/CSharpMath.Rendering.Tests/TextCenter/ErrorInvalidCommand.png deleted file mode 100644 index 8d4c7897..00000000 Binary files a/CSharpMath.Rendering.Tests/TextCenter/ErrorInvalidCommand.png and /dev/null differ diff --git a/CSharpMath.Rendering.Tests/TextCenter/ErrorMissingArgument.png b/CSharpMath.Rendering.Tests/TextCenter/ErrorMissingArgument.png new file mode 100644 index 00000000..b24201a9 Binary files /dev/null and b/CSharpMath.Rendering.Tests/TextCenter/ErrorMissingArgument.png differ diff --git a/CSharpMath.Rendering.Tests/TextCenterInfiniteWidth/ErrorInvalidColor.png b/CSharpMath.Rendering.Tests/TextCenterInfiniteWidth/ErrorInvalidColor.png index 55b4a6c4..51e071a0 100644 Binary files a/CSharpMath.Rendering.Tests/TextCenterInfiniteWidth/ErrorInvalidColor.png and b/CSharpMath.Rendering.Tests/TextCenterInfiniteWidth/ErrorInvalidColor.png differ diff --git a/CSharpMath.Rendering.Tests/TextCenterInfiniteWidth/ErrorInvalidCommand.png b/CSharpMath.Rendering.Tests/TextCenterInfiniteWidth/ErrorInvalidCommand.png deleted file mode 100644 index 8d4c7897..00000000 Binary files a/CSharpMath.Rendering.Tests/TextCenterInfiniteWidth/ErrorInvalidCommand.png and /dev/null differ diff --git a/CSharpMath.Rendering.Tests/TextCenterInfiniteWidth/ErrorMissingArgument.png b/CSharpMath.Rendering.Tests/TextCenterInfiniteWidth/ErrorMissingArgument.png new file mode 100644 index 00000000..b24201a9 Binary files /dev/null and b/CSharpMath.Rendering.Tests/TextCenterInfiniteWidth/ErrorMissingArgument.png differ diff --git a/CSharpMath.Rendering.Tests/TextLeft/ErrorInvalidColor.png b/CSharpMath.Rendering.Tests/TextLeft/ErrorInvalidColor.png index 55b4a6c4..51e071a0 100644 Binary files a/CSharpMath.Rendering.Tests/TextLeft/ErrorInvalidColor.png and b/CSharpMath.Rendering.Tests/TextLeft/ErrorInvalidColor.png differ diff --git a/CSharpMath.Rendering.Tests/TextLeft/ErrorInvalidCommand.png b/CSharpMath.Rendering.Tests/TextLeft/ErrorInvalidCommand.png deleted file mode 100644 index 8d4c7897..00000000 Binary files a/CSharpMath.Rendering.Tests/TextLeft/ErrorInvalidCommand.png and /dev/null differ diff --git a/CSharpMath.Rendering.Tests/TextLeft/ErrorMissingArgument.png b/CSharpMath.Rendering.Tests/TextLeft/ErrorMissingArgument.png new file mode 100644 index 00000000..b24201a9 Binary files /dev/null and b/CSharpMath.Rendering.Tests/TextLeft/ErrorMissingArgument.png differ diff --git a/CSharpMath.Rendering.Tests/TextLeftInfiniteWidth/ErrorInvalidColor.png b/CSharpMath.Rendering.Tests/TextLeftInfiniteWidth/ErrorInvalidColor.png index 55b4a6c4..51e071a0 100644 Binary files a/CSharpMath.Rendering.Tests/TextLeftInfiniteWidth/ErrorInvalidColor.png and b/CSharpMath.Rendering.Tests/TextLeftInfiniteWidth/ErrorInvalidColor.png differ diff --git a/CSharpMath.Rendering.Tests/TextLeftInfiniteWidth/ErrorInvalidCommand.png b/CSharpMath.Rendering.Tests/TextLeftInfiniteWidth/ErrorInvalidCommand.png deleted file mode 100644 index 8d4c7897..00000000 Binary files a/CSharpMath.Rendering.Tests/TextLeftInfiniteWidth/ErrorInvalidCommand.png and /dev/null differ diff --git a/CSharpMath.Rendering.Tests/TextLeftInfiniteWidth/ErrorMissingArgument.png b/CSharpMath.Rendering.Tests/TextLeftInfiniteWidth/ErrorMissingArgument.png new file mode 100644 index 00000000..b24201a9 Binary files /dev/null and b/CSharpMath.Rendering.Tests/TextLeftInfiniteWidth/ErrorMissingArgument.png differ diff --git a/CSharpMath.Rendering.Tests/TextRight/ErrorInvalidColor.png b/CSharpMath.Rendering.Tests/TextRight/ErrorInvalidColor.png index 55b4a6c4..51e071a0 100644 Binary files a/CSharpMath.Rendering.Tests/TextRight/ErrorInvalidColor.png and b/CSharpMath.Rendering.Tests/TextRight/ErrorInvalidColor.png differ diff --git a/CSharpMath.Rendering.Tests/TextRight/ErrorInvalidCommand.png b/CSharpMath.Rendering.Tests/TextRight/ErrorInvalidCommand.png deleted file mode 100644 index 8d4c7897..00000000 Binary files a/CSharpMath.Rendering.Tests/TextRight/ErrorInvalidCommand.png and /dev/null differ diff --git a/CSharpMath.Rendering.Tests/TextRight/ErrorMissingArgument.png b/CSharpMath.Rendering.Tests/TextRight/ErrorMissingArgument.png new file mode 100644 index 00000000..b24201a9 Binary files /dev/null and b/CSharpMath.Rendering.Tests/TextRight/ErrorMissingArgument.png differ diff --git a/CSharpMath.Rendering.Tests/TextRightInfiniteWidth/ErrorInvalidColor.png b/CSharpMath.Rendering.Tests/TextRightInfiniteWidth/ErrorInvalidColor.png index 55b4a6c4..51e071a0 100644 Binary files a/CSharpMath.Rendering.Tests/TextRightInfiniteWidth/ErrorInvalidColor.png and b/CSharpMath.Rendering.Tests/TextRightInfiniteWidth/ErrorInvalidColor.png differ diff --git a/CSharpMath.Rendering.Tests/TextRightInfiniteWidth/ErrorInvalidCommand.png b/CSharpMath.Rendering.Tests/TextRightInfiniteWidth/ErrorInvalidCommand.png deleted file mode 100644 index 8d4c7897..00000000 Binary files a/CSharpMath.Rendering.Tests/TextRightInfiniteWidth/ErrorInvalidCommand.png and /dev/null differ diff --git a/CSharpMath.Rendering.Tests/TextRightInfiniteWidth/ErrorMissingArgument.png b/CSharpMath.Rendering.Tests/TextRightInfiniteWidth/ErrorMissingArgument.png new file mode 100644 index 00000000..b24201a9 Binary files /dev/null and b/CSharpMath.Rendering.Tests/TextRightInfiniteWidth/ErrorMissingArgument.png differ diff --git a/CSharpMath.Rendering.Text.Tests/TextLaTeXParserTests.cs b/CSharpMath.Rendering.Text.Tests/TextLaTeXParserTests.cs index 4d264d10..511389f7 100644 --- a/CSharpMath.Rendering.Text.Tests/TextLaTeXParserTests.cs +++ b/CSharpMath.Rendering.Text.Tests/TextLaTeXParserTests.cs @@ -87,7 +87,7 @@ public void CommandArguments(string input, string colored, string? after, string void Test(string input) { var atom = Parse(input); var list = new List { - new TextAtom.Colored(new TextAtom.Text(colored), Atom.LaTeXSettings.PredefinedColors["red"]) + new TextAtom.Colored(new TextAtom.Text(colored), Atom.LaTeXSettings.PredefinedColors.FirstToSecond["red"]) }; if (after != null) list.Add(new TextAtom.Text(after)); Assert.Equal(list.Count == 1 ? list[0] : new TextAtom.List(list), atom); @@ -431,7 +431,7 @@ public void Braces(string input, string output) { ↑ (pos 24)"), InlineData(@"\(\notacommand \frac12\)", @"Error: [Math] Invalid command \notacommand \(\notacommand \frac12\) - ↑ (pos 14)"), + ↑ (pos 3)"), InlineData(@"\(\notacommand \frac12\[", @"Error: Cannot open display math mode in inline math mode ···notacommand \frac12\[ ↑ (pos 24)"), @@ -440,7 +440,7 @@ public void Braces(string input, string output) { ↑ (pos 24)"), InlineData(@"\(\notacommand \frac12$", @"Error: [Math] Invalid command \notacommand \(\notacommand \frac12$ - ↑ (pos 14)"), + ↑ (pos 3)"), InlineData(@"\(\notacommand \frac12$$", @"Error: Cannot close inline math mode with $$ ···notacommand \frac12$$ ↑ (pos 24)"), @@ -455,14 +455,14 @@ public void Braces(string input, string output) { ↑ (pos 24)"), InlineData(@"\[\notacommand \frac12\]", @"Error: [Math] Invalid command \notacommand \[\notacommand \frac12\] - ↑ (pos 14)"), + ↑ (pos 3)"), InlineData(@"\[\notacommand \frac12$", @"Error: Cannot close display math mode with $ ···\notacommand \frac12$ ↑ (pos 23)"), InlineData(@"\[\notacommand \frac12$$", @"Error: [Math] Invalid command \notacommand \[\notacommand \frac12$$ - ↑ (pos 14)"), - InlineData(@"\color", @"Error: Missing argument + ↑ (pos 3)"), + InlineData(@"\color", @"Error: Missing { \color ↑ (pos 6)"), InlineData(@"\color{", @"Error: Missing } @@ -480,7 +480,7 @@ public void Braces(string input, string output) { InlineData(@"\color{#12345}a", @"Error: Invalid color: #12345 \color{#12345}a ↑ (pos 14)"), - InlineData(@"\fontsize", @"Error: Missing argument + InlineData(@"\fontsize", @"Error: Missing { \fontsize ↑ (pos 9)"), InlineData(@"\fontsize{", @"Error: Missing } diff --git a/CSharpMath.Rendering/Settings.cs b/CSharpMath.Rendering/Settings.cs index 265da505..311116c5 100644 --- a/CSharpMath.Rendering/Settings.cs +++ b/CSharpMath.Rendering/Settings.cs @@ -1,20 +1,24 @@ namespace CSharpMath { using System.Collections.Generic; - using CSharpMath.Structures; + using Structures; public static class Settings { public static Rendering.BackEnd.Typefaces GlobalTypefaces => Rendering.BackEnd.Fonts.GlobalTypefaces; - public static BiDictionary PredefinedColors => + public static AliasBiDictionary PredefinedColors => Atom.LaTeXSettings.PredefinedColors; - public static AliasDictionary PredefinedLaTeXBoundaryDelimiters => + public static LaTeXCommandDictionary PredefinedLaTeXBoundaryDelimiters => Atom.LaTeXSettings.BoundaryDelimiters; - public static AliasDictionary PredefinedLaTeXFontStyles => + public static AliasBiDictionary PredefinedLaTeXFontStyles => Atom.LaTeXSettings.FontStyles; - public static AliasDictionary PredefinedLaTeXCommands => + public static LaTeXCommandDictionary> + > PredefinedLaTeXCommands => Atom.LaTeXSettings.Commands; - public static BiDictionary PredefinedLaTeXTextAccents => + public static AliasBiDictionary PredefinedLaTeXCommandSymbols => + Atom.LaTeXSettings.CommandSymbols; + public static AliasBiDictionary PredefinedLaTeXTextAccents => Rendering.Text.TextLaTeXSettings.PredefinedAccents; - public static AliasDictionary PredefinedLaTeXTextSymbols => + public static AliasBiDictionary PredefinedLaTeXTextSymbols => Rendering.Text.TextLaTeXSettings.PredefinedTextSymbols; public static Dictionary PredefinedLengthUnits => Space.PredefinedLengthUnits; diff --git a/CSharpMath.Rendering/Text/TextAtomListBuilder.cs b/CSharpMath.Rendering/Text/TextAtomListBuilder.cs index 84b5d49a..25032677 100644 --- a/CSharpMath.Rendering/Text/TextAtomListBuilder.cs +++ b/CSharpMath.Rendering/Text/TextAtomListBuilder.cs @@ -27,15 +27,14 @@ public void Text(string text) { public void Color(TextAtom atom, Color color) => Add(new TextAtom.Colored(atom, color)); public Result Math(string mathLaTeX, bool displayStyle, int startAt, ref int endAt) { var builder = new Atom.LaTeXParser(mathLaTeX); - var mathList = builder.Build(); - if (builder.Error is { } error) { - endAt = startAt - mathLaTeX.Length + builder.CurrentChar - 1; + var (mathList, error) = builder.Build(); + if (error != null) { + endAt = startAt - mathLaTeX.Length + builder.NextChar - 1; return Result.Err("[Math] " + error); - } else if (mathList != null) { + } else { Add(new TextAtom.Math(mathList, displayStyle)); return Result.Ok(); } - throw new InvalidCodePathException("Both error and list are null?"); } public void List(IReadOnlyList textAtoms) => Add(new TextAtom.List(textAtoms)); public void Break() => Add(new TextAtom.Newline()); diff --git a/CSharpMath.Rendering/Text/TextLaTeXParser.cs b/CSharpMath.Rendering/Text/TextLaTeXParser.cs index d805de7b..ded25668 100644 --- a/CSharpMath.Rendering/Text/TextLaTeXParser.cs +++ b/CSharpMath.Rendering/Text/TextLaTeXParser.cs @@ -124,8 +124,7 @@ Result ReadArgumentAtom(ReadOnlySpan latexInput) { } SpanResult ReadArgumentString(ReadOnlySpan latexInput, ref ReadOnlySpan section) { afterCommand = false; - if (!NextSection(latexInput, ref section)) return Err("Missing argument"); - if (section.IsNot('{')) return Err("Missing {"); + if (!NextSection(latexInput, ref section) || section.IsNot('{')) return Err("Missing {"); int endingIndex = -1; //startAt + 1 to not start at the { we started at bool isEscape = false; @@ -381,7 +380,7 @@ Result ReadColor(ReadOnlySpan latexInput, ref ReadOnlySpan se } //case "red", "yellow", ... case var shortColor when - LaTeXSettings.PredefinedColors.TryGetByFirst(shortColor, out var color): { + LaTeXSettings.PredefinedColors.FirstToSecond.TryGetValue(shortColor, out var color): { int tmp_commandLength = shortColor.Length; if (ReadArgumentAtom(latex).Bind( coloredContent => atoms.Color(coloredContent, color) @@ -391,7 +390,7 @@ Result ReadColor(ReadOnlySpan latexInput, ref ReadOnlySpan se } //case "textbf", "textit", ... case var textStyle when !textStyle.StartsWith("math") - && LaTeXSettings.FontStyles.TryGetValue( + && LaTeXSettings.FontStyles.FirstToSecond.TryGetValue( textStyle.StartsWith("text") ? textStyle.Replace("text", "math") : textStyle, out var fontStyle): { int tmp_commandLength = textStyle.Length; @@ -403,7 +402,7 @@ Result ReadColor(ReadOnlySpan latexInput, ref ReadOnlySpan se } //case "^", "\"", ... case var textAccent when - TextLaTeXSettings.PredefinedAccents.TryGetByFirst(textAccent, out var accent): { + TextLaTeXSettings.PredefinedAccents.FirstToSecond.TryGetValue(textAccent, out var accent): { if (ReadArgumentAtom(latex) .Bind(builtContent => atoms.Accent(builtContent, accent)) .Error is string error) @@ -411,7 +410,7 @@ Result ReadColor(ReadOnlySpan latexInput, ref ReadOnlySpan se break; } //case "textasciicircum", "textless", ... - case var textSymbol when TextLaTeXSettings.PredefinedTextSymbols.TryGetValue(textSymbol, out var replaceResult): + case var textSymbol when TextLaTeXSettings.PredefinedTextSymbols.FirstToSecond.TryGetValue(textSymbol, out var replaceResult): atoms.Text(replaceResult); break; case var command: @@ -443,7 +442,7 @@ public static StringBuilder TextAtomToLaTeX(TextAtom atom, StringBuilder? b = nu case TextAtom.Text t: foreach (var ch in t.Content) { var c = ch.ToStringInvariant(); - if (TextLaTeXSettings.PredefinedTextSymbols.TryGetKey(c, out var v)) + if (TextLaTeXSettings.PredefinedTextSymbols.SecondToFirst.TryGetValue(c, out var v)) if ('a' <= v[0] && v[0] <= 'z' || 'A' <= v[0] && v[0] <= 'Z') b.Append('\\').Append(v).Append(' '); else b.Append('\\').Append(v); @@ -475,11 +474,11 @@ public static StringBuilder TextAtomToLaTeX(TextAtom atom, StringBuilder? b = nu case TextAtom.ControlSpace _: return b.Append(@"\ "); case TextAtom.Accent a: - b.Append('\\').Append(TextLaTeXSettings.PredefinedAccents[second: a.AccentChar]).Append('{'); + b.Append('\\').Append(TextLaTeXSettings.PredefinedAccents.SecondToFirst[a.AccentChar]).Append('{'); return TextAtomToLaTeX(a.Content, b).Append('}'); case TextAtom.Style t: b.Append('\\') - .Append(LaTeXSettings.FontStyles[t.FontStyle] is var style && style.StartsWith("math") + .Append(LaTeXSettings.FontStyles.SecondToFirst[t.FontStyle] is var style && style.StartsWith("math") ? style.Replace("math", "text") : style) .Append('{'); return TextAtomToLaTeX(t.Content, b).Append('}'); diff --git a/CSharpMath.Rendering/Text/TextLaTeXSettings.cs b/CSharpMath.Rendering/Text/TextLaTeXSettings.cs index 8eee4688..7837d0b9 100644 --- a/CSharpMath.Rendering/Text/TextLaTeXSettings.cs +++ b/CSharpMath.Rendering/Text/TextLaTeXSettings.cs @@ -1,8 +1,8 @@ namespace CSharpMath.Rendering.Text { using CSharpMath.Structures; public static class TextLaTeXSettings { - public static AliasDictionary PredefinedTextSymbols { get; } = - new AliasDictionary { + public static AliasBiDictionary PredefinedTextSymbols { get; } = + new AliasBiDictionary { /*Ten special characters and their commands: & \& % \% @@ -131,8 +131,8 @@ public static class TextLaTeXSettings { { "textvisiblespace", "␣" }, { "textgreater", ">" }, }; - public static BiDictionary PredefinedAccents { get; } = - new BiDictionary { + public static AliasBiDictionary PredefinedAccents { get; } = + new AliasBiDictionary { //textsuperscript, textsubscript //textcircled { "`", "\u0300" }, //grave diff --git a/CSharpMath.Xaml.Tests/Test.cs b/CSharpMath.Xaml.Tests/Test.cs index 0797171d..f169823f 100644 --- a/CSharpMath.Xaml.Tests/Test.cs +++ b/CSharpMath.Xaml.Tests/Test.cs @@ -148,10 +148,10 @@ void Test(TView view, TContent oneTwoThree) SetBindingContext(view, viewModel); using (var binding = SetBinding(view, nameof(viewModel.LaTeX))) { - viewModel.LaTeX = @"\alpha\beta\gamme"; - Assert.Equal(@"\alpha\beta\gamme", view.LaTeX); + viewModel.LaTeX = @"\alpha\beta\color"; + Assert.Equal(@"\alpha\beta\color", view.LaTeX); Assert.Null(view.Content); - Assert.Equal("Error: Invalid command \\gamme\n\\alpha\\beta\\gamme\n ↑ (pos 17)", view.ErrorMessage); + Assert.Equal("Error: Missing {\n\\alpha\\beta\\color\n ↑ (pos 17)", view.ErrorMessage); } using (var binding = SetBinding(view, nameof(viewModel.LaTeX), OneWayToSource)) { view.LaTeX = @"123"; diff --git a/CSharpMath/Atom/Atoms/Comment.cs b/CSharpMath/Atom/Atoms/Comment.cs new file mode 100644 index 00000000..926ce320 --- /dev/null +++ b/CSharpMath/Atom/Atoms/Comment.cs @@ -0,0 +1,8 @@ +namespace CSharpMath.Atom.Atoms { + public sealed class Comment : MathAtom { + public Comment(string nucleus) : base(nucleus) { } + public override bool ScriptsAllowed => false; + public new Comment Clone(bool finalize) => (Comment)base.Clone(finalize); + protected override MathAtom CloneInside(bool finalize) => new Comment(Nucleus); + } +} \ No newline at end of file diff --git a/CSharpMath/Atom/Atoms/Fraction.cs b/CSharpMath/Atom/Atoms/Fraction.cs index 629966d7..2a28d515 100644 --- a/CSharpMath/Atom/Atoms/Fraction.cs +++ b/CSharpMath/Atom/Atoms/Fraction.cs @@ -6,8 +6,8 @@ public sealed class Fraction : MathAtom, IMathListContainer { public MathList Denominator { get; } System.Collections.Generic.IEnumerable IMathListContainer.InnerLists => new[] { Numerator, Denominator }; - public string? LeftDelimiter { get; set; } - public string? RightDelimiter { get; set; } + public Boundary LeftDelimiter { get; set; } + public Boundary RightDelimiter { get; set; } /// In this context, a "rule" is a fraction line. public bool HasRule { get; } public Fraction(MathList numerator, MathList denominator, bool hasRule = true) => @@ -21,8 +21,8 @@ protected override MathAtom CloneInside(bool finalize) => }; public override string DebugString => new StringBuilder(HasRule ? @"\frac" : @"\atop") - .AppendInBracketsOrNothing(LeftDelimiter) - .AppendInBracketsOrNothing(RightDelimiter) + .AppendInBracketsOrNothing(LeftDelimiter.Nucleus) + .AppendInBracketsOrNothing(RightDelimiter.Nucleus) .AppendInBracesOrEmptyBraces(Numerator?.DebugString) .AppendInBracesOrEmptyBraces(Denominator?.DebugString) .AppendDebugStringOfScripts(this).ToString(); diff --git a/CSharpMath/Atom/Boundary.cs b/CSharpMath/Atom/Boundary.cs index 47dc27e8..06743097 100644 --- a/CSharpMath/Atom/Boundary.cs +++ b/CSharpMath/Atom/Boundary.cs @@ -4,16 +4,16 @@ namespace CSharpMath.Atom { /// We don't need two since we track boundaries separately. /// public readonly struct Boundary : IMathObject, System.IEquatable { - public static readonly Boundary Empty = new Boundary(""); - public string Nucleus { get; } - public string DebugString => Nucleus; + public static readonly Boundary Empty = default; + public string? Nucleus { get; } + public string DebugString => Nucleus ?? "(null)"; public Boundary(string nucleus) => Nucleus = nucleus; public bool EqualsBoundary(Boundary boundary) => Nucleus == boundary.Nucleus; bool System.IEquatable.Equals(Boundary other) => EqualsBoundary(other); - public override bool Equals(object obj) => obj is Boundary b ? EqualsBoundary(b) : false; - public override int GetHashCode() => Nucleus.GetHashCode(); + public override bool Equals(object obj) => obj is Boundary b && EqualsBoundary(b); + public override int GetHashCode() => Nucleus?.GetHashCode() ?? 0; public static bool operator ==(Boundary left, Boundary right) => left.EqualsBoundary(right); public static bool operator !=(Boundary left, Boundary right) => !left.EqualsBoundary(right); - public override string ToString() => Nucleus; + public override string ToString() => Nucleus ?? "(null)"; } } \ No newline at end of file diff --git a/CSharpMath/Atom/LaTeXParser.cs b/CSharpMath/Atom/LaTeXParser.cs index e97aebc6..bb42412a 100644 --- a/CSharpMath/Atom/LaTeXParser.cs +++ b/CSharpMath/Atom/LaTeXParser.cs @@ -1,243 +1,162 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Text; -using System.Drawing; namespace CSharpMath.Atom { using Atoms; - using CSharpMath.Structures; + using Space = Atoms.Space; + using Structures; + using static Structures.Result; using InvalidCodePathException = Structures.InvalidCodePathException; public class LaTeXParser { - interface IEnvironment { } - class TableEnvironment : IEnvironment { + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1040:Avoid empty interfaces", + Justification = "This is a marker interface to enable compile-time type checking")] +#pragma warning disable CA1034 // Nested types should not be visible + // Justification: Implementation details exposed for extensibility + public interface IEnvironment { } + public class TableEnvironment : IEnvironment { public TableEnvironment(string? name) => Name = name; public string? Name { get; set; } public bool Ended { get; set; } public int NRows { get; set; } public string? ArrayAlignments { get; set; } } - class InnerEnvironment : IEnvironment { + public class InnerEnvironment : IEnvironment { public Boundary? RightBoundary { get; set; } } +#pragma warning restore CA1034 // Nested types should not be visible public string Chars { get; } - public int CurrentChar { get; private set; } - private bool _textMode; //_spacesAllowed in iosMath - private FontStyle _currentFontStyle; - private readonly Stack _environments = new Stack(); - public string? Error { get; private set; } + public int NextChar { get; private set; } + public bool TextMode { get; set; } //_spacesAllowed in iosMath + public FontStyle CurrentFontStyle { get; set; } + public Stack Environments { get; } = new Stack(); public LaTeXParser(string str) { Chars = str; - _currentFontStyle = FontStyle.Default; - } - public MathList? Build() { - var r = BuildInternal(false); - if (HasCharacters && Error == null) { - SetError("Error; most likely mismatched braces."); - } - return Error != null ? null : r; + CurrentFontStyle = FontStyle.Default; } - private char GetNextCharacter() => Chars[CurrentChar++]; - private void UnlookCharacter() => - _ = CurrentChar == 0 + public Result Build() => BuildInternal(false); + public char ReadChar() => Chars[NextChar++]; + public void UndoReadChar() => + _ = NextChar == 0 ? throw new InvalidCodePathException("Can't unlook below character 0") - : CurrentChar--; - private bool HasCharacters => CurrentChar < Chars.Length; - private MathList? BuildInternal(bool oneCharOnly, char stopChar = '\0', MathList? r = null) { + : NextChar--; + public bool HasCharacters => NextChar < Chars.Length; + public Result ReadArgument(MathList? appendTo = null) => BuildInternal(true, r: appendTo); + public Result ReadArgumentOptional(MathList? appendTo = null) => + ReadCharIfAvailable('[') + ? BuildInternal(false, ']', r: appendTo).Bind(mathList => (MathList?)mathList) + : (MathList?)null; + public Result ReadUntil(char stopChar, MathList? appendTo = null) => + BuildInternal(false, stopChar, r: appendTo); + // TODO: Example + //https://phabricator.wikimedia.org/T99369 + //https://phab.wmfusercontent.org/file/data/xsimlcnvo42siudvwuzk/PHID-FILE-bdcqexocj5b57tj2oezn/math_rendering.png + //dt, \text{d}t, \partial t, \nabla\psi \\ \underline\overline{dy/dx, \text{d}y/\text{d}x, \frac{dy}{dx}, \frac{\text{d}y}{\text{d}x}, \frac{\partial^2}{\partial x_1\partial x_2}y} \\ \prime, + private Result BuildInternal(bool oneCharOnly, char stopChar = '\0', MathList? r = null) { if (oneCharOnly && stopChar > '\0') { throw new InvalidCodePathException("Cannot set both oneCharOnly and stopChar"); } r ??= new MathList(); MathAtom? prevAtom = null; while (HasCharacters) { - if (Error != null) { - return null; + MathAtom? atom = null; + if (Chars[NextChar] == stopChar && stopChar > '\0') { + NextChar++; + return r; } - MathAtom atom; - switch (GetNextCharacter()) { - case var ch when oneCharOnly && (ch == '^' || ch == '}' || ch == '_' || ch == '&'): - SetError($"{ch} cannot appear as an argument to a command"); - return r; - case var ch when stopChar > '\0' && ch == stopChar: - return r; - case '^': - if (prevAtom == null || prevAtom.Superscript.IsNonEmpty() || !prevAtom.ScriptsAllowed) { - prevAtom = new Ordinary(string.Empty); - r.Add(prevAtom); - } - // this is a superscript for the previous atom. - // note, if the next char is StopChar, it will be consumed and doesn't count as stop. - this.BuildInternal(true, r: prevAtom.Superscript); - if (Error != null) return null; - continue; - case '_': - if (prevAtom == null || prevAtom.Subscript.IsNonEmpty() || !prevAtom.ScriptsAllowed) { - prevAtom = new Ordinary(string.Empty); - r.Add(prevAtom); - } - // this is a subscript for the previous atom. - // note, if the next char is StopChar, it will be consumed and doesn't count as stop. - this.BuildInternal(true, r: prevAtom.Subscript); - if (Error != null) return null; - continue; - case '{': - MathList? sublist; - if (_environments.PeekOrDefault() is TableEnvironment { Name: null }) { - // \\ or \cr which do not have a corrosponding \end - var oldEnv = _environments.Pop(); - sublist = BuildInternal(false, '}'); - _environments.Push(oldEnv); - } else { - sublist = BuildInternal(false, '}'); - } - if (sublist == null) return null; - prevAtom = sublist.Atoms.LastOrDefault(); - r.Append(sublist); - if (oneCharOnly) { + var ((handler, splitIndex), error) = LaTeXSettings.Commands.TryLookup(Chars.AsSpan(NextChar)); + if (error != null) { + NextChar++; // Point to the start of the erroneous command + return error; + } + NextChar += splitIndex; + + (MathAtom?, MathList?) handlerResult; + (handlerResult, error) = handler(this, r, stopChar); + if (error != null) return error; + + switch (handlerResult) { + case ({ } /* dummy */, { } atoms): // Atoms producer (pre-styled) + r.Append(atoms); + prevAtom = r.Atoms.LastOrDefault(); + if (oneCharOnly) return r; - } + else continue; + case (null, { } @return): // Environment ender + return @return; + case (null, null): // Atom modifier continue; - // TODO: Example - //https://phabricator.wikimedia.org/T99369 - //https://phab.wmfusercontent.org/file/data/xsimlcnvo42siudvwuzk/PHID-FILE-bdcqexocj5b57tj2oezn/math_rendering.png - //dt, \text{d}t, \partial t, \nabla\psi \\ \underline\overline{dy/dx, \text{d}y/\text{d}x, \frac{dy}{dx}, \frac{\text{d}y}{\text{d}x}, \frac{\partial^2}{\partial x_1\partial x_2}y} \\ \prime, - case '}' when oneCharOnly || stopChar != '\0': - throw new InvalidCodePathException("This should have been handled before."); - case '}': - SetError("Missing opening brace"); - return null; - case '\\': - var command = ReadCommand(); - var done = StopCommand(command, r, stopChar); - if (done != null) { - return done; - } - if (Error != null) { - return null; - } - if (ApplyModifier(command, prevAtom)) { - continue; - } - if (LaTeXSettings.FontStyles.TryGetValue(command, out var fontStyle)) { - var oldSpacesAllowed = _textMode; - var oldFontStyle = _currentFontStyle; - _textMode = (command == "text"); - _currentFontStyle = fontStyle; - var childList = BuildInternal(true); - if (childList == null) return null; - _currentFontStyle = oldFontStyle; - _textMode = oldSpacesAllowed; - prevAtom = childList.Atoms.LastOrDefault(); - r.Append(childList); - if (oneCharOnly) { - return r; - } - continue; - } - switch (AtomForCommand(command, stopChar)) { - case null: - SetError(Error ?? "Internal error"); - return null; - case var a: - atom = a; - break; - } - break; - case '&': // column separation in tables - if (_environments.PeekOrDefault() is TableEnvironment) { - return r; - } - var table = BuildTable(null, r, false, stopChar); - if (table == null) return null; - return new MathList(table); - case '\'': // this case is NOT in iosMath - int i = 1; - while (ExpectCharacter('\'')) i++; - atom = new Prime(i); - break; - case ' ' when _textMode: - atom = new Ordinary(" "); - break; - case var ch when ch <= sbyte.MaxValue: - if (LaTeXSettings.ForAscii((sbyte)ch) is MathAtom asciiAtom) - atom = asciiAtom; - else continue; // Ignore ASCII spaces and control characters - break; - case var ch: - // not a recognized character, display it directly - atom = new Ordinary(ch.ToStringInvariant()); + case ({ } resultAtom, null): // Atom producer + atom = resultAtom; break; } - atom.FontStyle = _currentFontStyle; + atom.FontStyle = CurrentFontStyle; r.Add(atom); prevAtom = atom; if (oneCharOnly) { return r; // we consumed our character. } } - if (stopChar > 0) { - if (stopChar == '}') { - SetError("Missing closing brace"); - } else { - // we never found our stop character. - SetError("Expected character not found: " + stopChar.ToStringInvariant()); - } - } - return r; + return stopChar switch + { + '\0' => r, + '}' => "Missing closing brace", + _ => "Expected character not found: " + stopChar.ToStringInvariant(), + }; } - private string ReadString() { + public string ReadString() { var builder = new StringBuilder(); while (HasCharacters) { - var ch = GetNextCharacter(); + var ch = ReadChar(); if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { builder.Append(ch.ToStringInvariant()); } else { - UnlookCharacter(); + UndoReadChar(); break; } } return builder.ToString(); } - private Color? ReadColor() { - if (!ExpectCharacter('{')) { - SetError("Missing {"); - return null; + public Result ReadColor() { + if (!ReadCharIfAvailable('{')) { + return "Missing {"; } SkipSpaces(); - var builder = new StringBuilder(); + var index = NextChar; + var length = 0; while (HasCharacters) { - var ch = GetNextCharacter(); + var ch = ReadChar(); if (char.IsLetterOrDigit(ch) || ch == '#') { - builder.Append(ch); + length++; } else { // we went too far - UnlookCharacter(); + UndoReadChar(); break; } } - var str = builder.ToString(); + var str = Chars.Substring(index, length); if (LaTeXSettings.ParseColor(str) is Color color) { SkipSpaces(); - if (!ExpectCharacter('}')) { - SetError("Missing }"); - return null; - } + if (!ReadCharIfAvailable('}')) + return "Missing }"; return color; } else { - SetError("Invalid color: " + str); - return null; + return "Invalid color: " + str; } } - private void SkipSpaces() { + public void SkipSpaces() { while (HasCharacters) { - var ch = GetNextCharacter(); + var ch = ReadChar(); if (char.IsWhiteSpace(ch) || char.IsControl(ch)) { continue; } else { - UnlookCharacter(); + UndoReadChar(); return; } } @@ -250,78 +169,45 @@ private static void AssertNotSpace(char ch) { } } - private bool ExpectCharacter(char ch) { + /// Advances if is available. + /// Whether the char was read. + public bool ReadCharIfAvailable(char ch) { AssertNotSpace(ch); SkipSpaces(); if (HasCharacters) { - var c = GetNextCharacter(); + var c = ReadChar(); AssertNotSpace(c); if (c == ch) { return true; } else { - UnlookCharacter(); + UndoReadChar(); return false; } } return false; } - - //static readonly char[] _singleCharCommands = @"{}$#%_| ,:>;!\".ToCharArray(); - private string ReadCommand() { - if (HasCharacters) { - var ch = GetNextCharacter(); - if ((ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z')) { - return ch.ToStringInvariant(); - } else { - UnlookCharacter(); - } - } - return ReadString(); - } - - private string? ReadDelimiter() { - SkipSpaces(); - while (HasCharacters) { - var ch = GetNextCharacter(); - AssertNotSpace(ch); - if (ch == '\\') { - // a command - var command = ReadCommand(); - if (command == "|") { - return @"||"; - } - return command; - } - return ch.ToStringInvariant(); - } - return null; - } - - private string? ReadEnvironment() { - if (!ExpectCharacter('{')) { - SetError("Missing {"); - return null; + public Result ReadEnvironment() { + if (!ReadCharIfAvailable('{')) { + return Err("Missing {"); } SkipSpaces(); var env = ReadString(); SkipSpaces(); - if (!ExpectCharacter('}')) { - SetError("Missing }"); - return null; + if (!ReadCharIfAvailable('}')) { + return Err("Missing }"); } - return env; + return Ok(env); } - - private Structures.Result ReadSpace() { + public Result ReadSpace() { SkipSpaces(); var sb = new StringBuilder(); while (HasCharacters) { - var ch = GetNextCharacter(); + var ch = ReadChar(); if (char.IsDigit(ch) || ch == '.' || ch == '-' || ch == '+') { sb.Append(ch); } else { - UnlookCharacter(); + UndoReadChar(); break; } } @@ -332,258 +218,37 @@ private string ReadCommand() { SkipSpaces(); var unit = new char[2]; for (int i = 0; i < 2 && HasCharacters; i++) { - unit[i] = GetNextCharacter(); - } - return Structures.Space.Create(length, new string(unit), _textMode); - } - private Boundary? BoundaryAtomForDelimiterType(string delimiterType) { - var delim = ReadDelimiter(); - if (delim == null) { - SetError("Missing delimiter for " + delimiterType); - return null; + unit[i] = ReadChar(); } - if (!LaTeXSettings.BoundaryDelimiters.TryGetValue(delim, out var boundary)) { - SetError(@"Invalid delimiter for \" + delimiterType + ": " + delim); - } - return boundary; + return Structures.Space.Create(length, new string(unit), TextMode); } - - private MathAtom? AtomForCommand(string command, char stopChar) { - switch (LaTeXSettings.AtomForCommand(command)) { - case Accent accent: - var innerList = BuildInternal(true); - if (innerList is null) return null; - return new Accent(accent.Nucleus, innerList); - case MathAtom atom: - return atom; + public Result ReadDelimiter(string commandName) { + if (!HasCharacters) { + return @"Missing delimiter for \" + commandName; } - switch (command) { - case "frac": - var numerator = BuildInternal(true); - if (numerator is null) return null; - var denominator = BuildInternal(true); - if (denominator is null) return null; - return new Fraction(numerator, denominator); - case "binom": - numerator = BuildInternal(true); - if (numerator is null) return null; - denominator = BuildInternal(true); - if (denominator is null) return null; - return new Fraction(numerator, denominator, false) { - LeftDelimiter = "(", - RightDelimiter = ")" - }; - case "sqrt": - var degree = ExpectCharacter('[') ? BuildInternal(false, ']') : new MathList(); - if (degree is null) return null; - var radicand = BuildInternal(true); - return radicand != null ? new Radical(degree, radicand) : null; - case "left": - var leftBoundary = BoundaryAtomForDelimiterType("left"); - if (!(leftBoundary is Boundary left)) return null; - _environments.Push(new InnerEnvironment()); - var innerList = BuildInternal(false, stopChar); - if (innerList is null) return null; - if (!(_environments.PeekOrDefault() is InnerEnvironment { RightBoundary: { } right })) { - SetError($@"Missing \right for \left with delimiter {leftBoundary}"); - return null; - } - _environments.Pop(); - return new Inner(left, innerList, right); - case "overline": - innerList = BuildInternal(true); - if (innerList is null) return null; - return new Overline(innerList); - case "underline": - innerList = BuildInternal(true); - if (innerList is null) return null; - return new Underline(innerList); - case "begin": - var env = ReadEnvironment(); - if (env == null) { - return null; - } - return BuildTable(env, null, false, stopChar); - case "color": - return (ReadColor()) switch - { - Color color when BuildInternal(true) is MathList ml => new Colored(color, ml), - _ => null, - }; - case "colorbox": - return (ReadColor()) switch - { - { } color when BuildInternal(true) is { } ml => new ColorBox(color, ml), - _ => null, - }; - case "prime": - SetError(@"\prime won't be supported as Unicode has no matching character. Use ' instead."); - return null; - case "kern": - case "hskip": - if (_textMode) { - var (space, error) = ReadSpace(); - if (error != null) { - SetError(error); - return null; - } else return new Atoms.Space(space); - } - SetError($@"\{command} is not allowed in math mode"); - return null; - case "mkern": - case "mskip": - if (!_textMode) { - var (space, error) = ReadSpace(); - if (error != null) { - SetError(error); - return null; - } else return new Atoms.Space(space); - } - SetError($@"\{command} is not allowed in text mode"); - return null; - case "raisebox": - if (!ExpectCharacter('{')) { SetError("Expected {"); return null; } - var (raise, err) = ReadSpace(); - if (err != null) { - SetError(err); - return null; - } - if (!ExpectCharacter('}')) { SetError("Expected }"); return null; } - innerList = BuildInternal(true); - if (innerList is null) return null; - return new RaiseBox(raise, innerList); - case "TeX": - return TeX; - case "operatorname": - if (!ExpectCharacter('{')) { SetError("Expected {"); return null; } - var operatorname = ReadString(); - if (!ExpectCharacter('}')) { SetError("Expected }"); return null; } - return new LargeOperator(operatorname, null); - // Bra and Ket implementations are derived from Donald Arseneau's braket LaTeX package. - // See: https://www.ctan.org/pkg/braket - case "Bra": - var braContents = BuildInternal(true); - if (braContents is null) return null; - return new Inner(new Boundary("〈"), braContents, new Boundary("|")); - case "Ket": - var ketContents = BuildInternal(true); - if (ketContents is null) return null; - return new Inner(new Boundary("|"), ketContents, new Boundary("〉")); - default: - SetError("Invalid command \\" + command); - return null; - } - } - - private static readonly Dictionary fractionCommands = - new Dictionary { - { "over", null }, - { "atop", null }, - { "choose", ("(", ")") }, - { "brack", ("[", "]") }, - { "brace", ("{", "}") } - }; - - //should be \textrm instead of \text - private static readonly MathAtom TeX = new Inner(Boundary.Empty, - MathListFromLaTeX(@"\text{T\kern-.1667em\raisebox{-.5ex}{E}\kern-.125emX}") - .Match(mathList => mathList, e => - throw new FormatException(@"A syntax error is present in the definition of \TeX.")), - Boundary.Empty); - - private MathList? StopCommand(string command, MathList list, char stopChar) { - switch (command) { - case "right": - while (_environments.PeekOrDefault() is TableEnvironment table) - if (table.Name is null) { - table.Ended = true; - _environments.Pop(); // Get out of \\ or \cr before looking for \right - } else { - SetError($"Missing \\end{{{table.Name}}}"); - return null; - } - if (!(_environments.PeekOrDefault() is InnerEnvironment inner)) { - SetError("Missing \\left"); - return null; - } - inner.RightBoundary = BoundaryAtomForDelimiterType("right"); - if (inner.RightBoundary == null) { - return null; - } - return list; - case var _ when fractionCommands.ContainsKey(command): - var denominator = BuildInternal(false, stopChar); - if (denominator is null) return null; - var fraction = new Fraction(list, denominator, command == "over"); - if (fractionCommands[command] is (var left, var right)) { - fraction.LeftDelimiter = left; - fraction.RightDelimiter = right; - }; - return new MathList(fraction); - case "\\": - case "cr": - if (!(_environments.PeekOrDefault() is TableEnvironment environment)) { - var table = BuildTable(null, list, true, stopChar); - if (table == null) return null; - return new MathList(table); - } else { - // stop the current list and increment the row count - environment.NRows++; - return list; - } - case "end": - if (!(_environments.PeekOrDefault() is TableEnvironment endEnvironment)) { - SetError(@"Missing \begin"); - return null; - } - var env = ReadEnvironment(); - if (env == null) { - return null; - } - if (env != endEnvironment.Name) { - SetError($"Begin environment name {endEnvironment.Name} does not match end environment name {env}"); - return null; - } - endEnvironment.Ended = true; - return list; - } - return null; - } - private bool ApplyModifier(string modifier, MathAtom? atom) { - switch (modifier) { - case "limits": - if (atom is LargeOperator limitsOp) { - limitsOp.Limits = true; - } else { - SetError(@"\limits can only be applied to an operator"); - } - return true; - case "nolimits": - if (atom is LargeOperator noLimitsOp) { - noLimitsOp.Limits = false; - } else { - SetError(@"\nolimits can only be applied to an operator"); - } - return true; + SkipSpaces(); + var ((result, splitIndex), error) = LaTeXSettings.BoundaryDelimiters.TryLookup(Chars.AsSpan(NextChar)); + if (error != null) { + NextChar++; // Point to the start of the erroneous command + return error; } - return false; + NextChar += splitIndex; + return result; } - private void SetError(string error) => Error ??= error; - private static readonly Dictionary _matrixEnvironments = new Dictionary { { "matrix", null } , { "pmatrix", ("(", ")") } , { "bmatrix", ("[", "]") }, { "Bmatrix", ("{", "}") }, - { "vmatrix", ("vert", "vert") }, - { "Vmatrix", ("Vert", "Vert") } + { "vmatrix", ("|", "|") }, + { "Vmatrix", ("‖", "‖") } }; - private MathAtom? BuildTable + public Result ReadTable (string? name, MathList? firstList, bool isRow, char stopChar) { var environment = new TableEnvironment(name); - _environments.Push(environment); + Environments.Push(environment); int currentRow = 0; int currentColumn = 0; var rows = new List> { new List() }; @@ -598,14 +263,13 @@ private MathAtom? BuildTable } } if (environment.Name == "array") { - if (!ExpectCharacter('{')) { - SetError("Missing array alignment"); - return null; + if (!ReadCharIfAvailable('{')) { + return "Missing array alignment"; } var builder = new StringBuilder(); var done = false; while (HasCharacters && !done) { - var ch = GetNextCharacter(); + var ch = ReadChar(); switch (ch) { case 'l': case 'c': @@ -618,20 +282,16 @@ private MathAtom? BuildTable done = true; break; default: - SetError($"Invalid character '{ch}' encountered while parsing array alignments"); - return null; + return $"Invalid character '{ch}' encountered while parsing array alignments"; } } if (!done) { - SetError("Missing }"); - return null; + return "Missing }"; } } while (HasCharacters && !environment.Ended) { - var list = BuildInternal(false, stopChar); - if (list == null) { - return null; - } + var (list, error) = BuildInternal(false, stopChar); + if (error != null) return error; rows[currentRow].Add(list); currentColumn++; if (environment.NRows > currentRow) { @@ -640,19 +300,18 @@ private MathAtom? BuildTable currentColumn = 0; } // The } in \begin{matrix} is not stopChar so this line is not written in the while-condition - if (stopChar != '\0' && Chars[CurrentChar - 1] == stopChar) break; + if (stopChar != '\0' && Chars[NextChar - 1] == stopChar) break; } if (environment.Name != null && !environment.Ended) { - SetError($@"Missing \end for \begin{{{environment.Name}}}"); - return null; + return $@"Missing \end for \begin{{{environment.Name}}}"; } // We have finished parsing the table, now interpret the environment name = environment.Name; var arrayAlignments = environment.ArrayAlignments; // Table environments with { Name: null } may have been popped by \right - if (_environments.PeekOrDefault() == environment) - _environments.Pop(); + if (Environments.PeekOrDefault() == environment) + Environments.Pop(); var table = new Table(name, rows); switch (name) { @@ -675,9 +334,9 @@ private MathAtom? BuildTable return delimiters switch { (var left, var right) => new Inner( - LaTeXSettings.BoundaryDelimiters[left], + new Boundary(left), new MathList(table), - LaTeXSettings.BoundaryDelimiters[right] + new Boundary(right) ), null => table }; @@ -702,8 +361,7 @@ private MathAtom? BuildTable case "split": case "aligned": if (table.NColumns != 2) { - SetError(name + " environment can only have 2 columns"); - return null; + return name + " environment can only have 2 columns"; } else { // add a spacer before each of the second column elements, in order to create the correct spacing for "=" and other relations. var spacer = new Ordinary(string.Empty); @@ -720,8 +378,7 @@ private MathAtom? BuildTable case "displaylines": case "gather": if (table.NColumns != 1) { - SetError(name + " environment can only have 1 column"); - return null; + return name + " environment can only have 1 column"; } table.InterRowAdditionalSpacing = 1; table.InterColumnSpacing = 0; @@ -729,8 +386,7 @@ private MathAtom? BuildTable return table; case "eqnarray": if (table.NColumns != 3) { - SetError(name + " must have exactly 3 columns"); - return null; + return name + " must have exactly 3 columns"; } else { table.InterRowAdditionalSpacing = 1; table.InterColumnSpacing = 18; @@ -741,8 +397,7 @@ private MathAtom? BuildTable } case "cases": if (table.NColumns < 1 || table.NColumns > 2) { - SetError("cases environment must have 1 to 2 columns"); - return null; + return "cases environment must have 1 to 2 columns"; } else { table.Environment = "array"; table.InterColumnSpacing = 18; @@ -756,28 +411,24 @@ private MathAtom? BuildTable } // add delimiters return new Inner( - LaTeXSettings.BoundaryDelimiters["{"], - new MathList(new Atoms.Space(Structures.Space.ShortSpace), table), + new Boundary("{"), + new MathList(new Space(Structures.Space.ShortSpace), table), Boundary.Empty ); } default: - SetError("Unknown environment " + name); - return null; + return "Unknown environment " + name; } } - public static Structures.Result MathListFromLaTeX(string str) { + public static Result MathListFromLaTeX(string str) { var builder = new LaTeXParser(str); - var list = builder.Build(); - return builder.Error is { } error - ? Structures.Result.Err(HelpfulErrorMessage(error, builder.Chars, builder.CurrentChar)) - : list != null - ? Structures.Result.Ok(list) - : throw new InvalidCodePathException("Both error and list are null?"); + return builder.Build().Match(Ok, + error => Err(HelpfulErrorMessage(error, builder.Chars, builder.NextChar))); } public static string HelpfulErrorMessage(string error, string source, int right) { + if (right <= 0) right = 1; // Just like Xunit's helpful error message in Assert.Equal(string, string) const string dots = "···"; const int lookbehind = 20; @@ -818,8 +469,26 @@ public static string EscapeAsLaTeX(string literal) => .Replace("~", @"\textasciitilde ") .ToString(); + static string BoundaryToLaTeX(Boundary delimiter) => + LaTeXSettings.BoundaryDelimitersReverse.TryGetValue(delimiter, out var command) + ? command + : delimiter.Nucleus ?? ""; + private static void MathListToLaTeX (MathList mathList, StringBuilder builder, FontStyle outerFontStyle) { + static bool MathAtomToLaTeX(MathAtom atom, StringBuilder builder, + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? command) { + if (LaTeXSettings.CommandForAtom(atom) is string name) { + command = name; + builder.Append(name); + if (name.AsSpan().StartsWithInvariant(@"\")) + builder.Append(" "); + return true; + } + command = null; + return false; + } + if (mathList is null) throw new ArgumentNullException(nameof(mathList)); if (mathList.IsEmpty()) return; var currentFontStyle = outerFontStyle; @@ -831,11 +500,14 @@ private static void MathListToLaTeX } if (atom.FontStyle != outerFontStyle) { // open a new font style - builder.Append(@"\").Append(LaTeXSettings.FontStyles[atom.FontStyle]).Append("{"); + builder.Append(@"\").Append(LaTeXSettings.FontStyles.SecondToFirst[atom.FontStyle]).Append("{"); } } currentFontStyle = atom.FontStyle; switch (atom) { + case Comment { Nucleus: var comment }: + builder.Append('%').Append(comment).Append('\n'); + break; case Fraction fraction: if (fraction.HasRule) { builder.Append(@"\frac{"); @@ -849,11 +521,11 @@ private static void MathListToLaTeX builder.Append(@" \").Append( (fraction.LeftDelimiter, fraction.RightDelimiter) switch { - (null, null) => "atop", - ("(", ")") => "choose", - ("{", "}") => "brace", - ("[", "]") => "brack", - (var left, var right) => $"atopwithdelims{left}{right}", + ({ Nucleus: null }, { Nucleus: null }) => "atop", + ({ Nucleus: "(" }, { Nucleus: ")" }) => "choose", + ({ Nucleus: "{" }, { Nucleus: "}" }) => "brace", + ({ Nucleus: "[" }, { Nucleus: "]" }) => "brack", + (var left, var right) => $"atopwithdelims{BoundaryToLaTeX(left)}{BoundaryToLaTeX(right)}", }).Append(" "); MathListToLaTeX(fraction.Denominator, builder, currentFontStyle); builder.Append("}"); @@ -870,37 +542,23 @@ private static void MathListToLaTeX MathListToLaTeX(radical.Radicand, builder, currentFontStyle); builder.Append('}'); break; - case Inner inner: - if (inner.LeftBoundary == Boundary.Empty && inner.RightBoundary == Boundary.Empty) { - builder.Append('{'); - MathListToLaTeX(inner.InnerList, builder, currentFontStyle); - builder.Append('}'); - } else if (inner.LeftBoundary.Nucleus == "〈" && inner.RightBoundary.Nucleus == "|") { - builder.Append(@"\Bra{"); - MathListToLaTeX(inner.InnerList, builder, currentFontStyle); - builder.Append("}"); - } else if (inner.LeftBoundary.Nucleus == "|" && inner.RightBoundary.Nucleus == "〉") { - builder.Append(@"\Ket{"); - MathListToLaTeX(inner.InnerList, builder, currentFontStyle); - builder.Append("}"); - } else { - static string BoundaryToLaTeX(Boundary delimiter) { - var command = LaTeXSettings.BoundaryDelimiters[delimiter]; - if (command == null) { - return string.Empty; - } - if ("()[]<>|./".Contains(command) && command.Length == 1) - return command; - if (command == "||") { - return @"\|"; - } else { - return @"\" + command; - } - } - builder.Append(@"\left").Append(BoundaryToLaTeX(inner.LeftBoundary)).Append(' '); - MathListToLaTeX(inner.InnerList, builder, currentFontStyle); - builder.Append(@"\right").Append(BoundaryToLaTeX(inner.RightBoundary)).Append(' '); - } + case Inner { LeftBoundary: { Nucleus: null }, InnerList: var list, RightBoundary: { Nucleus: null } }: + MathListToLaTeX(list, builder, currentFontStyle); + break; + case Inner { LeftBoundary: { Nucleus: "〈" }, InnerList: var list, RightBoundary: { Nucleus: "|" } }: + builder.Append(@"\Bra{"); + MathListToLaTeX(list, builder, currentFontStyle); + builder.Append("}"); + break; + case Inner { LeftBoundary: { Nucleus: "|" }, InnerList: var list, RightBoundary: { Nucleus: "〉" } }: + builder.Append(@"\Ket{"); + MathListToLaTeX(list, builder, currentFontStyle); + builder.Append("}"); + break; + case Inner { LeftBoundary: var left, InnerList: var list, RightBoundary: var right }: + builder.Append(@"\left").Append(BoundaryToLaTeX(left)).Append(' '); + MathListToLaTeX(list, builder, currentFontStyle); + builder.Append(@"\right").Append(BoundaryToLaTeX(right)).Append(' '); break; case Table table: if (table.Environment != null) { @@ -967,22 +625,19 @@ static string BoundaryToLaTeX(Boundary delimiter) { builder.Append("}"); break; case Accent accent: - builder.Append(@"\") - .Append(LaTeXSettings.CommandForAtom(accent)) - .Append("{"); + MathAtomToLaTeX(accent, builder, out _); + builder.Append("{"); MathListToLaTeX(accent.InnerList, builder, currentFontStyle); builder.Append("}"); break; case LargeOperator op: - var command = LaTeXSettings.CommandForAtom(op); - if (command == null) { - builder.Append($@"\operatorname{{{op.Nucleus}}} "); - } else { - builder.Append($@"\{command} "); + if (MathAtomToLaTeX(op, builder, out var command)) { if (!(LaTeXSettings.AtomForCommand(command) is LargeOperator originalOperator)) throw new InvalidCodePathException("original operator not found!"); if (originalOperator.Limits == op.Limits) break; + } else { + builder.Append($@"\operatorname{{{op.Nucleus}}} "); } switch (op.Limits) { case true: @@ -1020,8 +675,7 @@ static string BoundaryToLaTeX(Boundary delimiter) { MathListToLaTeX(r.InnerList, builder, currentFontStyle); builder.Append("}"); break; - case var _ when LaTeXSettings.CommandForAtom(atom) is string name: - builder.Append(@"\").Append(name).Append(" "); + case var _ when MathAtomToLaTeX(atom, builder, out _): break; case Atoms.Space space: var intSpace = (int)space.Length; diff --git a/CSharpMath/Atom/LaTeXSettings.cs b/CSharpMath/Atom/LaTeXSettings.cs index 6f8425bd..60969c39 100644 --- a/CSharpMath/Atom/LaTeXSettings.cs +++ b/CSharpMath/Atom/LaTeXSettings.cs @@ -2,131 +2,362 @@ using System.Collections.Generic; using System.Drawing; using System.Globalization; +using System.Linq; +using System.Text; namespace CSharpMath.Atom { - using System.Text; using Atoms; using Structures; using Space = Atoms.Space; //https://mirror.hmc.edu/ctan/macros/latex/contrib/unicode-math/unimath-symbols.pdf public static class LaTeXSettings { - public static MathAtom Times => new BinaryOperator("×"); - public static MathAtom Divide => new BinaryOperator("÷"); - public static MathAtom Placeholder => new Placeholder("\u25A1"); - public static MathList PlaceholderList => new MathList { Placeholder }; - - public static MathAtom? ForAscii(sbyte c) { - if (c < 0) throw new ArgumentOutOfRangeException(nameof(c), c, "The character cannot be negative"); - var s = ((char)c).ToStringInvariant(); - if (char.IsControl((char)c) || char.IsWhiteSpace((char)c)) { - return null; // skip spaces - } - if (c >= '0' && c <= '9') { - return new Number(s); - } - if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { - return new Variable(s); - } - switch (c) { - case (sbyte)'$': - case (sbyte)'%': - case (sbyte)'#': - case (sbyte)'&': - case (sbyte)'~': - case (sbyte)'\'': - case (sbyte)'^': - case (sbyte)'_': - case (sbyte)'{': - case (sbyte)'}': - case (sbyte)'\\': // All these are special characters we don't support. - return null; - case (sbyte)'(': - case (sbyte)'[': - return new Open(s); - case (sbyte)')': - case (sbyte)']': - return new Close(s); - case (sbyte)',': - case (sbyte)';': - case (sbyte)'!': - case (sbyte)'?': - return new Punctuation(s); - case (sbyte)'=': - case (sbyte)'<': - case (sbyte)'>': - return new Relation(s); - case (sbyte)':': // Colon is a ratio. Regular colon is \colon - return new Relation("\u2236"); - case (sbyte)'-': // Use the math minus sign - return new BinaryOperator("\u2212"); - case (sbyte)'+': - case (sbyte)'*': // Star operator, not multiplication - return new BinaryOperator(s); - case (sbyte)'.': - return new Number(s); - case (sbyte)'"': - // AMSMath: Although / is (semantically speaking) of class 2: Binary Operator, - // we write k/2 with no space around the slash rather than k / 2. - // And compare p|q -> p|q (no space) with p\mid q -> p | q (class-3 spacing). - case (sbyte)'/': - case (sbyte)'@': - case (sbyte)'`': - case (sbyte)'|': - return new Ordinary(s); - default: - throw new Structures.InvalidCodePathException - ($"Ascii character {c} should have been accounted for."); - } - } - - public static Structures.AliasDictionary BoundaryDelimiters { get; } = - new Structures.AliasDictionary { - { ".", Boundary.Empty }, // . means no delimiter + static readonly Dictionary boundaryDelimitersReverse = new Dictionary(); + public static IReadOnlyDictionary BoundaryDelimitersReverse => boundaryDelimitersReverse; + public static LaTeXCommandDictionary BoundaryDelimiters { get; } = + new LaTeXCommandDictionary( + consume => { + if (consume.IsEmpty) throw new InvalidCodePathException("Unexpected empty " + nameof(consume)); + if (char.IsHighSurrogate(consume[0])) { + if (consume.Length == 1) + return "Unexpected single high surrogate without its counterpart"; + if (!char.IsLowSurrogate(consume[1])) + return "Low surrogate not found after high surrogate"; + return "Invalid delimiter " + consume.Slice(0, 2).ToString(); + } else { + if (char.IsLowSurrogate(consume[0])) + return "Unexpected low surrogate without its counterpart"; + return "Invalid delimiter " + consume[0]; + } + }, + command => "Invalid delimiter " + command.ToString(), + (key, value) => { + if (!boundaryDelimitersReverse.ContainsKey(value)) + boundaryDelimitersReverse.Add(value, key); + }) + { + { @".", Boundary.Empty }, // . means no delimiter // Table 14: Delimiters - { "(", new Boundary("(") }, - { ")", new Boundary(")") }, - { "uparrow", new Boundary("↑") }, - { "Uparrow", new Boundary("⇑") }, - { "[", new Boundary("[") }, - { "]", new Boundary("]") }, - { "downarrow", new Boundary("↓") }, - { "Downarrow", new Boundary("⇓") }, - { "{", "lbrace", new Boundary("{") }, - { "}", "rbrace", new Boundary("}") }, - { "updownarrow", new Boundary("↕") }, - { "Updownarrow", new Boundary("⇕") }, - { "lfloor", new Boundary("⌊") }, - { "rfloor", new Boundary("⌋") }, - { "lceil", new Boundary("⌈") }, - { "rceil", new Boundary("⌉") }, - { "<", "langle", new Boundary("〈") }, - { ">", "rangle", new Boundary("〉") }, - { "/", new Boundary("/") }, - { "\\", "backslash", new Boundary("\\") }, - { "|", "vert", new Boundary("|") }, - { "||", "Vert", new Boundary("‖") }, + { @"(", new Boundary("(") }, + { @")", new Boundary(")") }, + { @"\uparrow", new Boundary("↑") }, + { @"\Uparrow", new Boundary("⇑") }, + { @"[", new Boundary("[") }, + { @"]", new Boundary("]") }, + { @"\downarrow", new Boundary("↓") }, + { @"\Downarrow", new Boundary("⇓") }, + { @"\{", @"\lbrace", new Boundary("{") }, + { @"\}", @"\rbrace", new Boundary("}") }, + { @"\updownarrow", new Boundary("↕") }, + { @"\Updownarrow", new Boundary("⇕") }, + { @"\lfloor", new Boundary("⌊") }, + { @"\rfloor", new Boundary("⌋") }, + { @"\lceil", new Boundary("⌈") }, + { @"\rceil", new Boundary("⌉") }, + { @"<", @"\langle", new Boundary("〈") }, + { @">", @"\rangle", new Boundary("〉") }, + { @"/", new Boundary("/") }, + { @"\\", @"backslash", new Boundary("\\") }, + { @"|", @"\vert", new Boundary("|") }, + { @"\|", @"\Vert", new Boundary("‖") }, // Table 15: Large Delimiters - // { "lmoustache", new Boundary("⎰") }, // Glyph not in Latin Modern Math - // { "rmoustache", new Boundary("⎱") }, // Glyph not in Latin Modern Math - { "rgroup", new Boundary("⟯") }, - { "lgroup", new Boundary("⟮") }, - { "arrowvert", new Boundary("|") }, // unsure, copied from \vert - { "Arrowvert", new Boundary("‖") }, // unsure, copied from \Vert - { "bracevert", new Boundary("|") }, // unsure, copied from \vert + // { @"\lmoustache", new Boundary("⎰") }, // Glyph not in Latin Modern Math + // { @"\rmoustache", new Boundary("⎱") }, // Glyph not in Latin Modern Math + { @"\rgroup", new Boundary("⟯") }, + { @"\lgroup", new Boundary("⟮") }, + { @"\arrowvert", new Boundary("|") }, // unsure, copied from \vert + { @"\Arrowvert", new Boundary("‖") }, // unsure, copied from \Vert + { @"\bracevert", new Boundary("|") }, // unsure, copied from \vert // Table 19: AMS Delimiters - { "ulcorner", new Boundary("⌜") }, - { "urcorner", new Boundary("⌝") }, - { "llcorner", new Boundary("⌞") }, - { "lrcorner", new Boundary("⌟") }, + { @"\ulcorner", new Boundary("⌜") }, + { @"\urcorner", new Boundary("⌝") }, + { @"\llcorner", new Boundary("⌞") }, + { @"\lrcorner", new Boundary("⌟") }, + }; + + static readonly MathAtom? Dummy = Placeholder; + public static Result<(MathAtom? Atom, MathList? Return)> Ok(MathAtom? atom) => Result.Ok((atom, (MathList?)null)); + public static Result<(MathAtom? Atom, MathList? Return)> OkStyled(MathList styled) => Result.Ok((Dummy, (MathList?)styled)); + public static Result<(MathAtom? Atom, MathList? Return)> OkStop(MathList @return) => Result.Ok(((MathAtom?)null, (MathList?)@return)); + public static ResultImplicitError Err(string error) => Result.Err(error); + public static LaTeXCommandDictionary>> Commands { get; } = + new LaTeXCommandDictionary>>(consume => { + if (consume.IsEmpty) throw new InvalidCodePathException("Unexpected empty " + nameof(consume)); + if (char.IsHighSurrogate(consume[0])) { + if (consume.Length == 1) + return "Unexpected single high surrogate without its counterpart"; + if (!char.IsLowSurrogate(consume[1])) + return "Low surrogate not found after high surrogate"; + var atom = new Ordinary(consume.Slice(0, 2).ToString()); + return ((parser, accumulate, stopChar) => Ok(atom), 2); + } else { + if (char.IsLowSurrogate(consume[0])) + return "Unexpected low surrogate without its counterpart"; + var atom = new Ordinary(consume[0].ToStringInvariant()); + return ((parser, accumulate, stopChar) => Ok(atom), 1); + } + }, command => "Invalid command " + command.ToString()) { + #region Atom producers + { Enumerable.Range(0, 33).Concat(new[] { 127 }).Select(c => ((char)c).ToStringInvariant()), + _ => (parser, accumulate, stopChar) => { + if (parser.TextMode) { + parser.SkipSpaces(); // Multiple spaces are collapsed into one in text mode + return Ok(new Ordinary(" ")); + } else return Ok(null); + } }, + { "%", (parser, accumulate, stopChar) => { + var index = parser.NextChar; + var length = 0; + while (parser.HasCharacters) { + switch (parser.ReadChar()) { + // https://en.wikipedia.org/wiki/Newline#Unicode + case '\u000A': + case '\u000B': + case '\u000C': + case '\u0085': + case '\u2028': + case '\u2029': + goto exitWhile; + case '\u000D': + if (parser.HasCharacters && parser.ReadChar() != '\u000A') + parser.UndoReadChar(); + goto exitWhile; + default: + length++; + break; + } + } + exitWhile: + return Ok(new Comment(parser.Chars.Substring(index, length))); + } }, + { @"\frac", (parser, accumulate, stopChar) => + parser.ReadArgument().Bind(numerator => + parser.ReadArgument().Bind(denominator => + Ok(new Fraction(numerator, denominator)))) }, + { @"\binom", (parser, accumulate, stopChar) => + parser.ReadArgument().Bind(numerator => + parser.ReadArgument().Bind(denominator => + Ok(new Fraction(numerator, denominator, false) { + LeftDelimiter = new Boundary("("), + RightDelimiter = new Boundary(")") + }))) }, + { @"\sqrt", (parser, accumulate, stopChar) => + parser.ReadArgumentOptional().Bind(degree => + parser.ReadArgument().Bind(radicand => + Ok(new Radical(degree ?? new MathList(), radicand)))) }, + { @"\left", (parser, accumulate, stopChar) => + parser.ReadDelimiter("left").Bind(left => { + parser.Environments.Push(new LaTeXParser.InnerEnvironment()); + return parser.ReadUntil(stopChar).Bind(innerList => { + if (!(parser.Environments.PeekOrDefault() is + LaTeXParser.InnerEnvironment { RightBoundary: { } right })) { + return Err($@"Missing \right for \left with delimiter {left}"); + } + parser.Environments.Pop(); + return Ok(new Inner(left, innerList, right)); + }); + }) }, + { @"\overline", (parser, accumulate, stopChar) => + parser.ReadArgument().Bind(mathList => Ok(new Overline(mathList))) }, + { @"\underline", (parser, accumulate, stopChar) => + parser.ReadArgument().Bind(mathList => Ok(new Underline(mathList))) }, + { @"\begin", (parser, accumulate, stopChar) => + parser.ReadEnvironment().Bind(env => + parser.ReadTable(env, null, false, stopChar)).Bind(Ok) }, + { @"\color", (parser, accumulate, stopChar) => + parser.ReadColor().Bind( + color => parser.ReadArgument().Bind( + colored => Ok(new Colored(color, colored)))) }, + { @"\colorbox", (parser, accumulate, stopChar) => + parser.ReadColor().Bind( + color => parser.ReadArgument().Bind( + colored => Ok(new ColorBox(color, colored)))) }, + { @"\prime", (parser, accumulate, stopChar) => + Err(@"\prime won't be supported as Unicode has no matching character. Use ' instead.") }, + { @"\kern", (parser, accumulate, stopChar) => + parser.TextMode ? parser.ReadSpace().Bind(kern => Ok(new Space(kern))) : @"\kern is not allowed in math mode" }, + { @"\hskip", (parser, accumulate, stopChar) => +//TODO \hskip and \mskip: Implement plus and minus for expansion + parser.TextMode ? parser.ReadSpace().Bind(skip => Ok(new Space(skip))) : @"\hskip is not allowed in math mode" }, + { @"\mkern", (parser, accumulate, stopChar) => + !parser.TextMode ? parser.ReadSpace().Bind(kern => Ok(new Space(kern))) : @"\mkern is not allowed in text mode" }, + { @"\mskip", (parser, accumulate, stopChar) => + !parser.TextMode ? parser.ReadSpace().Bind(skip => Ok(new Space(skip))) : @"\mskip is not allowed in text mode" }, + { @"\raisebox", (parser, accumulate, stopChar) => { + if (!parser.ReadCharIfAvailable('{')) return "Expected {"; + return parser.ReadSpace().Bind(raise => { + if (!parser.ReadCharIfAvailable('}')) return "Expected }"; + return parser.ReadArgument().Bind(innerList => + Ok(new RaiseBox(raise, innerList))); + }); + } }, + { @"\operatorname", (parser, accumulate, stopChar) => { + if (!parser.ReadCharIfAvailable('{')) return "Expected {"; + var operatorname = parser.ReadString(); + if (!parser.ReadCharIfAvailable('}')) return "Expected }"; + return Ok(new LargeOperator(operatorname, null)); + } }, + // Bra and Ket implementations are derived from Donald Arseneau's braket LaTeX package. + // See: https://www.ctan.org/pkg/braket + { @"\Bra", (parser, accumulate, stopChar) => + parser.ReadArgument().Bind(innerList => + Ok(new Inner(new Boundary("〈"), innerList, new Boundary("|")))) }, + { @"\Ket", (parser, accumulate, stopChar) => + parser.ReadArgument().Bind(innerList => + Ok(new Inner(new Boundary("|"), innerList, new Boundary("〉")))) }, + #endregion Atom producers + #region Atom modifiers + { @"^", (parser, accumulate, stopChar) => { + var prevAtom = accumulate.Last; + if (prevAtom == null || prevAtom.Superscript.IsNonEmpty() || !prevAtom.ScriptsAllowed) { + prevAtom = new Ordinary(string.Empty); + accumulate.Add(prevAtom); + } + // this is a superscript for the previous atom. + // note, if the next char is StopChar, it will be consumed and doesn't count as stop. + return parser.ReadArgument(prevAtom.Superscript).Bind(_ => Ok(null)); + } }, + { @"_", (parser, accumulate, stopChar) => { + var prevAtom = accumulate.Last; + if (prevAtom == null || prevAtom.Subscript.IsNonEmpty() || !prevAtom.ScriptsAllowed) { + prevAtom = new Ordinary(string.Empty); + accumulate.Add(prevAtom); + } + // this is a subscript for the previous atom. + // note, if the next char is StopChar, it will be consumed and doesn't count as stop. + return parser.ReadArgument(prevAtom.Subscript).Bind(_ => Ok(null)); + } }, + { @"{", (parser, accumulate, stopChar) => { + if (parser.Environments.PeekOrDefault() is LaTeXParser.TableEnvironment { Name: null }) { + // \\ or \cr which do not have a corresponding \end + var oldEnv = parser.Environments.Pop(); + return parser.ReadUntil('}').Bind(sublist => { + parser.Environments.Push(oldEnv); + return OkStyled(sublist); + }); + } else { + return parser.ReadUntil('}').Bind(OkStyled); + } + } }, + { @"}", (parser, accumulate, stopChar) => "Missing opening brace" }, + { @"\limits", (parser, accumulate, stopChar) => { + if (accumulate.Last is LargeOperator largeOp) { + largeOp.Limits = true; + return Ok(null); + } else return @"\limits can only be applied to an operator"; + } }, + { @"\nolimits", (parser, accumulate, stopChar) => { + if (accumulate.Last is LargeOperator largeOp) { + largeOp.Limits = false; + return Ok(null); + } else return @"\nolimits can only be applied to an operator"; + } }, + #endregion Atom modifiers + #region Environment enders + { @"&", (parser, accumulate, stopChar) => // column separation in tables + parser.Environments.PeekOrDefault() is LaTeXParser.TableEnvironment + ? OkStop(accumulate) + : parser.ReadTable(null, accumulate, false, stopChar).Bind(table => OkStop(new MathList(table))) }, + { @"\over", (parser, accumulate, stopChar) => + parser.ReadUntil(stopChar).Bind(denominator => + OkStop(new MathList(new Fraction(accumulate, denominator)))) }, + { @"\atop", (parser, accumulate, stopChar) => + parser.ReadUntil(stopChar).Bind(denominator => + OkStop(new MathList(new Fraction(accumulate, denominator, false)))) }, + { @"\choose", (parser, accumulate, stopChar) => + parser.ReadUntil(stopChar).Bind(denominator => + OkStop(new MathList(new Fraction(accumulate, denominator, false) { LeftDelimiter = new Boundary("("), RightDelimiter = new Boundary(")") }))) }, + { @"\brack", (parser, accumulate, stopChar) => + parser.ReadUntil(stopChar).Bind(denominator => + OkStop(new MathList(new Fraction(accumulate, denominator, false) { LeftDelimiter = new Boundary("["), RightDelimiter = new Boundary("]") }))) }, + { @"\brace", (parser, accumulate, stopChar) => + parser.ReadUntil(stopChar).Bind(denominator => + OkStop(new MathList(new Fraction(accumulate, denominator, false) { LeftDelimiter = new Boundary("{"), RightDelimiter = new Boundary("}") }))) }, + { @"\atopwithdelims", (parser, accumulate, stopChar) => + parser.ReadDelimiter(@"atopwithdelims").Bind(left => + parser.ReadDelimiter(@"atopwithdelims").Bind(right => + parser.ReadUntil(stopChar).Bind(denominator => + OkStop(new MathList(new Fraction(accumulate, denominator, false) { LeftDelimiter = left, RightDelimiter = right }))))) }, + { @"\right", (parser, accumulate, stopChar) => { + while (parser.Environments.PeekOrDefault() is LaTeXParser.TableEnvironment table) + if (table.Name is null) { + table.Ended = true; + parser.Environments.Pop(); // Get out of \\ or \cr before looking for \right + } else { + return $"Missing \\end{{{table.Name}}}"; + } + if (!(parser.Environments.PeekOrDefault() is LaTeXParser.InnerEnvironment inner)) { + return "Missing \\left"; + } + var (boundary, error) = parser.ReadDelimiter("right"); + if (error != null) return error; + inner.RightBoundary = boundary; + return OkStop(accumulate); + } }, + { @"\\", @"\cr", (parser, accumulate, stopChar) => { + if (!(parser.Environments.PeekOrDefault() is LaTeXParser.TableEnvironment environment)) { + return parser.ReadTable(null, accumulate, true, stopChar).Bind(table => OkStop(new MathList(table))); + } else { + // stop the current list and increment the row count + environment.NRows++; + return OkStop(accumulate); + } + } }, + { @"\end", (parser, accumulate, stopChar) => { + if (!(parser.Environments.PeekOrDefault() is LaTeXParser.TableEnvironment endEnvironment)) { + return @"Missing \begin"; + } + return parser.ReadEnvironment().Bind(env => { + if (env != endEnvironment.Name) { + return $"Begin environment name {endEnvironment.Name} does not match end environment name {env}"; + } + endEnvironment.Ended = true; + return OkStop(accumulate); + }); + } }, + #endregion Environment enders }; + public static MathAtom Times => new BinaryOperator("×"); + public static MathAtom Divide => new BinaryOperator("÷"); + public static MathAtom Placeholder => new Placeholder("\u25A1"); + public static MathList PlaceholderList => new MathList { Placeholder }; + public static AliasBiDictionary FontStyles { get; } = + new AliasBiDictionary((command, fontStyle) => { + Commands.Add(@"\" + command, (parser, accumulate, stopChar) => { + var oldSpacesAllowed = parser.TextMode; + var oldFontStyle = parser.CurrentFontStyle; + parser.TextMode = command == "text"; + parser.CurrentFontStyle = fontStyle; + var readsToEnd = + !command.AsSpan().StartsWithInvariant("math") + && !command.AsSpan().StartsWithInvariant("text"); + return (readsToEnd ? parser.ReadUntil(stopChar, accumulate) : parser.ReadArgument()).Bind(r => { + parser.CurrentFontStyle = oldFontStyle; + parser.TextMode = oldSpacesAllowed; + if (readsToEnd) + return OkStop(accumulate); + else return OkStyled(r); + }); + }); + }) { + { "mathnormal", FontStyle.Default }, + { "mathrm", "rm", "text", FontStyle.Roman }, + { "mathbf", "bf", FontStyle.Bold }, + { "mathcal", "cal", FontStyle.Caligraphic }, + { "mathtt", "tt", FontStyle.Typewriter }, + { "mathit", "it", "mit", FontStyle.Italic }, + { "mathsf", "sf", FontStyle.SansSerif }, + { "mathfrak", "frak", FontStyle.Fraktur }, + { "mathbb", "bb", FontStyle.Blackboard }, + { "mathbfit", "bm", FontStyle.BoldItalic }, + }; public static Color? ParseColor(string? hexOrName) { if (hexOrName == null) return null; - if (hexOrName.StartsWith("#", StringComparison.InvariantCulture)) { + if (hexOrName.StartsWith("#", StringComparison.Ordinal)) { var hex = hexOrName.Substring(1); return (hex.Length, int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var i)) switch @@ -137,13 +368,13 @@ public static class LaTeXSettings { }; } #pragma warning disable CA1308 // Normalize strings to uppercase - if (PredefinedColors.TryGetByFirst(hexOrName.ToLowerInvariant(), out var predefined)) + if (PredefinedColors.FirstToSecond.TryGetValue(hexOrName.ToLowerInvariant(), out var predefined)) return predefined; #pragma warning restore CA1308 // Normalize strings to uppercase return null; } public static StringBuilder ColorToString(Color color, StringBuilder sb) { - if (PredefinedColors.TryGetBySecond(color, out var outString)) { + if (PredefinedColors.SecondToFirst.TryGetValue(color, out var outString)) { return sb.Append(outString); } else { sb.Append('#'); @@ -155,8 +386,8 @@ public static StringBuilder ColorToString(Color color, StringBuilder sb) { } } //https://en.wikibooks.org/wiki/LaTeX/Colors#Predefined_colors - public static BiDictionary PredefinedColors { get; } = - new BiDictionary { + public static AliasBiDictionary PredefinedColors { get; } = + new AliasBiDictionary { { "black", Color.FromArgb(0, 0, 0) }, { "blue", Color.FromArgb(0, 0, 255) }, { "brown", Color.FromArgb(150, 75, 0) }, @@ -178,22 +409,8 @@ public static StringBuilder ColorToString(Color color, StringBuilder sb) { { "yellow", Color.FromArgb(255, 255, 0) } }; - public static Structures.AliasDictionary FontStyles { get; } = - new Structures.AliasDictionary { - { "mathnormal", FontStyle.Default }, - { "mathrm", "rm", "text", FontStyle.Roman }, - { "mathbf", "bf", FontStyle.Bold }, - { "mathcal", "cal", FontStyle.Caligraphic }, - { "mathtt", FontStyle.Typewriter }, - { "mathit", "mit", FontStyle.Italic }, - { "mathsf", FontStyle.SansSerif }, - { "mathfrak", "frak", FontStyle.Fraktur }, - { "mathbb", FontStyle.Blackboard }, - { "mathbfit", "bm", FontStyle.BoldItalic }, - }; - public static MathAtom? AtomForCommand(string symbolName) => - Commands.TryGetValue( + CommandSymbols.FirstToSecond.TryGetValue( symbolName ?? throw new ArgumentNullException(nameof(symbolName)), out var symbol) ? symbol.Clone(false) : null; @@ -204,87 +421,117 @@ public static StringBuilder ColorToString(Color color, StringBuilder sb) { if (atomWithoutScripts is IMathListContainer container) foreach (var list in container.InnerLists) list.Clear(); - return Commands.TryGetKey(atomWithoutScripts, out var name) ? name : null; + return CommandSymbols.SecondToFirst.TryGetValue(atomWithoutScripts, out var name) ? name : null; } - public static Structures.AliasDictionary Commands { get; } = - new Structures.AliasDictionary { + public static AliasBiDictionary CommandSymbols { get; } = + new AliasBiDictionary((command, atom) => + Commands.Add(command, (parser, accumulate, stopChar) => + atom is Accent accent + ? parser.ReadArgument().Bind(accentee => Ok(new Accent(accent.Nucleus, accentee))) + : Ok(atom.Clone(false)))) { // Custom additions - { "diameter", new Ordinary("\u2300") }, - { "npreccurlyeq", new Relation("⋠") }, - { "nsucccurlyeq", new Relation("⋡") }, - { "iint", new LargeOperator("∬", false) }, - { "iiint", new LargeOperator("∭", false) }, - { "iiiint", new LargeOperator("⨌", false) }, - { "oiint", new LargeOperator("∯", false) }, - { "oiiint", new LargeOperator("∰", false) }, - { "intclockwise", new LargeOperator("∱", false) }, - { "awint", new LargeOperator("⨑", false) }, - { "varointclockwise", new LargeOperator("∲", false) }, - { "ointctrclockwise", new LargeOperator("∳", false) }, - { "bigbot", new LargeOperator("⟘", null) }, - { "bigtop", new LargeOperator("⟙", null) }, - { "bigcupdot", new LargeOperator("⨃", null) }, - { "bigsqcap", new LargeOperator("⨅", null) }, - { "bigtimes", new LargeOperator("⨉", null) }, - { "arsinh", new LargeOperator("arsinh", false, true) }, - { "arcosh", new LargeOperator("arcosh", false, true) }, - { "artanh", new LargeOperator("artanh", false, true) }, - { "arccot", new LargeOperator("arccot", false, true) }, - { "arcoth", new LargeOperator("arcoth", false, true) }, - { "arcsec", new LargeOperator("arcsec", false, true) }, - { "sech", new LargeOperator("sech", false, true) }, - { "arsech", new LargeOperator("arsech", false, true) }, - { "arccsc", new LargeOperator("arccsc", false, true) }, - { "csch", new LargeOperator("csch", false, true) }, - { "arcsch", new LargeOperator("arcsch", false, true) }, + { @"\diameter", new Ordinary("\u2300") }, + { @"\npreccurlyeq", new Relation("⋠") }, + { @"\nsucccurlyeq", new Relation("⋡") }, + { @"\iint", new LargeOperator("∬", false) }, + { @"\iiint", new LargeOperator("∭", false) }, + { @"\iiiint", new LargeOperator("⨌", false) }, + { @"\oiint", new LargeOperator("∯", false) }, + { @"\oiiint", new LargeOperator("∰", false) }, + { @"\intclockwise", new LargeOperator("∱", false) }, + { @"\awint", new LargeOperator("⨑", false) }, + { @"\varointclockwise", new LargeOperator("∲", false) }, + { @"\ointctrclockwise", new LargeOperator("∳", false) }, + { @"\bigbot", new LargeOperator("⟘", null) }, + { @"\bigtop", new LargeOperator("⟙", null) }, + { @"\bigcupdot", new LargeOperator("⨃", null) }, + { @"\bigsqcap", new LargeOperator("⨅", null) }, + { @"\bigtimes", new LargeOperator("⨉", null) }, + { @"\arsinh", new LargeOperator("arsinh", false, true) }, + { @"\arcosh", new LargeOperator("arcosh", false, true) }, + { @"\artanh", new LargeOperator("artanh", false, true) }, + { @"\arccot", new LargeOperator("arccot", false, true) }, + { @"\arcoth", new LargeOperator("arcoth", false, true) }, + { @"\arcsec", new LargeOperator("arcsec", false, true) }, + { @"\sech", new LargeOperator("sech", false, true) }, + { @"\arsech", new LargeOperator("arsech", false, true) }, + { @"\arccsc", new LargeOperator("arccsc", false, true) }, + { @"\csch", new LargeOperator("csch", false, true) }, + { @"\arcsch", new LargeOperator("arcsch", false, true) }, // Use escape sequence for combining characters - { "overbar", new Accent("\u0305") }, - { "ovhook", new Accent("\u0309") }, - { "ocirc", new Accent("\u030A") }, - { "leftharpoonaccent", new Accent("\u20D0") }, - { "rightharpoonaccent", new Accent("\u20D1") }, - { "vertoverlay", new Accent("\u20D2") }, - { "dddot", new Accent("\u20DB") }, - { "ddddot", new Accent("\u20DC") }, - { "widebridgeabove", new Accent("\u20E9") }, - { "asteraccent", new Accent("\u20F0") }, - { "threeunderdot", new Accent("\u20E8") }, + { @"\overbar", new Accent("\u0305") }, + { @"\ovhook", new Accent("\u0309") }, + { @"\ocirc", new Accent("\u030A") }, + { @"\leftharpoonaccent", new Accent("\u20D0") }, + { @"\rightharpoonaccent", new Accent("\u20D1") }, + { @"\vertoverlay", new Accent("\u20D2") }, + { @"\dddot", new Accent("\u20DB") }, + { @"\ddddot", new Accent("\u20DC") }, + { @"\widebridgeabove", new Accent("\u20E9") }, + { @"\asteraccent", new Accent("\u20F0") }, + { @"\threeunderdot", new Accent("\u20E8") }, + { @"\TeX", new Inner(Boundary.Empty, new MathList( + new Variable("T") { FontStyle = FontStyle.Roman }, + new Space(-1/6f * Structures.Space.EmWidth) { FontStyle = FontStyle.Roman }, + new RaiseBox(-1/2f * Structures.Space.ExHeight, + new MathList(new Variable("E") { FontStyle = FontStyle.Roman }) + ) { FontStyle = FontStyle.Roman }, + new Space(-1/8f * Structures.Space.EmWidth) { FontStyle = FontStyle.Roman }, + new Variable("X") { FontStyle = FontStyle.Roman } + ), Boundary.Empty) }, // Delimiters outside \left or \right - { "lceil", new Open("⌈") }, - { "rceil", new Close("⌉") }, - { "lfloor", new Open("⌊") }, - { "rfloor", new Close("⌋") }, - { "langle", new Open("〈") }, - { "rangle", new Close("〉") }, - { "lgroup", new Open("⟮") }, - { "rgroup", new Close("⟯") }, - { "ulcorner", new Open("⌜") }, - { "urcorner", new Close("⌝") }, - { "llcorner", new Open("⌞") }, - { "lrcorner", new Close("⌟") }, + { @"(", new Open("(") }, + { @")", new Close(")") }, + { @"[", new Open("[") }, + { @"]", new Close("]") }, + { @"\lceil", new Open("⌈") }, + { @"\rceil", new Close("⌉") }, + { @"\lfloor", new Open("⌊") }, + { @"\rfloor", new Close("⌋") }, + { @"\langle", new Open("〈") }, + { @"\rangle", new Close("〉") }, + { @"\lgroup", new Open("⟮") }, + { @"\rgroup", new Close("⟯") }, + { @"\ulcorner", new Open("⌜") }, + { @"\urcorner", new Close("⌝") }, + { @"\llcorner", new Open("⌞") }, + { @"\lrcorner", new Close("⌟") }, // Standard TeX - { " ", new Ordinary(" ") }, - { ",", new Space(Structures.Space.ShortSpace) }, - { ":", ">", new Space(Structures.Space.MediumSpace) }, - { ";", new Space(Structures.Space.LongSpace) }, - { "!", new Space(-Structures.Space.ShortSpace) }, - { "enspace", new Space(Structures.Space.EmWidth / 2) }, - { "quad", new Space(Structures.Space.EmWidth) }, - { "qquad", new Space(Structures.Space.EmWidth * 2) }, - { "displaystyle", new Style(LineStyle.Display) }, - { "textstyle", new Style(LineStyle.Text) }, - { "scriptstyle", new Style(LineStyle.Script) }, - { "scriptscriptstyle", new Style(LineStyle.ScriptScript) }, + { Enumerable.Range('0', 10).Select(c => ((char)c).ToStringInvariant()), + n => new Number(n) }, + { Enumerable.Range('A', 26).Concat(Enumerable.Range('a', 26)).Select(c => ((char)c).ToStringInvariant()), + v => new Variable(v) }, + { @"\ ", new Ordinary(" ") }, + { @"\,", new Space(Structures.Space.ShortSpace) }, + { @"\:", @"\>", new Space(Structures.Space.MediumSpace) }, + { @"\;", new Space(Structures.Space.LongSpace) }, + { @"\!", new Space(-Structures.Space.ShortSpace) }, + { @"\enspace", new Space(Structures.Space.EmWidth / 2) }, + { @"\quad", new Space(Structures.Space.EmWidth) }, + { @"\qquad", new Space(Structures.Space.EmWidth * 2) }, + { @"\displaystyle", new Style(LineStyle.Display) }, + { @"\textstyle", new Style(LineStyle.Text) }, + { @"\scriptstyle", new Style(LineStyle.Script) }, + { @"\scriptscriptstyle", new Style(LineStyle.ScriptScript) }, // The gensymb package for LaTeX2ε: http://mirrors.ctan.org/macros/latex/contrib/was/gensymb.pdf - { "degree", new Ordinary("°") }, - { "celsius" , new Ordinary("℃") }, - { "perthousand" , new Ordinary("‰") }, - { "ohm" , new Ordinary("Ω") }, - { "micro" , new Ordinary("µ") }, + { @"\degree", new Ordinary("°") }, + { @"\celsius", new Ordinary("℃") }, + { @"\perthousand", new Ordinary("‰") }, + { @"\ohm", new Ordinary("Ω") }, + { @"\micro", new Ordinary("µ") }, + + // ASCII characters without special properties (Not a special Command or CommandSymbol) + // AMSMath: Although / is (semantically speaking) of class 2: Binary Operator, + // we write k/2 with no space around the slash rather than k / 2. + // And compare p|q -> p|q (no space) with p\mid q -> p | q (class-3 spacing). + { @"/", new Ordinary("/") }, + { @"@", new Ordinary("@") }, + { @"`", new Ordinary("`") }, + { @"|", new Ordinary("|") }, // LaTeX Symbol List: https://rpi.edu/dept/arc/training/latex/LaTeX_symbols.pdf // (Included in the same folder as this file) @@ -297,323 +544,326 @@ public static StringBuilder ColorToString(Color color, StringBuilder sb) { // Following tables are from the LaTeX Symbol List // Table 1: Escapable “Special” Characters - { "$", new Ordinary("$") }, - { "%", new Ordinary("%") }, - { "_", new Ordinary("_") }, - { "}", "rbrace", new Close("}") }, - { "&", new Ordinary("&") }, - { "#", new Ordinary("#") }, - { "{", "lbrace", new Open("{") }, + { @"\$", new Ordinary("$") }, + { @"\%", new Ordinary("%") }, + { @"\_", new Ordinary("_") }, + { @"\}", @"\rbrace", new Close("}") }, + { @"\&", new Ordinary("&") }, + { @"\#", new Ordinary("#") }, + { @"\{", @"\lbrace", new Open("{") }, // Table 2: LaTeX2ε Commands Defined to Work in Both Math and Text Mode - // $ is defined in Table 1 - { "P", new Ordinary("¶") }, - { "S", new Ordinary("§") }, - // _ is defined in Table 1 - { "copyright", new Ordinary("©") }, - { "dag", new Ordinary("†") }, - { "ddag", new Ordinary("‡") }, - { "dots", new Ordinary("…") }, - { "pounds", new Ordinary("£") }, - // { is defined in Table 1 - // } is defined in Table 1 + // \$ is defined in Table 1 + { @"\P", new Ordinary("¶") }, + { @"\S", new Ordinary("§") }, + // \_ is defined in Table 1 + { @"\copyright", new Ordinary("©") }, + { @"\dag", new Ordinary("†") }, + { @"\ddag", new Ordinary("‡") }, + { @"\dots", new Ordinary("…") }, + { @"\pounds", new Ordinary("£") }, + // \{ is defined in Table 1 + // \} is defined in Table 1 // Table 3: Non-ASCII Letters (Excluding Accented Letters) - { "aa", new Ordinary("å") }, - { "AA", "angstrom", new Ordinary("Å") }, - { "AE", new Ordinary("Æ") }, - { "ae", new Ordinary("æ") }, - { "DH", new Ordinary("Ð") }, - { "dh", new Ordinary("ð") }, - { "DJ", new Ordinary("Đ") }, - //{ "dj", new Ordinary("đ") }, // Glyph not in Latin Modern Math - { "L", new Ordinary("Ł") }, - { "l", new Ordinary("ł") }, - { "NG", new Ordinary("Ŋ") }, - { "ng", new Ordinary("ŋ") }, - { "o", new Ordinary("ø") }, - { "O", new Ordinary("Ø") }, - { "OE", new Ordinary("Œ") }, - { "oe", new Ordinary("œ") }, - { "ss", new Ordinary("ß") }, - { "SS", new Ordinary("SS") }, - { "TH", new Ordinary("Þ") }, - { "th", new Ordinary("þ") }, + { @"\aa", new Ordinary("å") }, + { @"\AA", @"\angstrom", new Ordinary("Å") }, + { @"\AE", new Ordinary("Æ") }, + { @"\ae", new Ordinary("æ") }, + { @"\DH", new Ordinary("Ð") }, + { @"\dh", new Ordinary("ð") }, + { @"\DJ", new Ordinary("Đ") }, + //{ @"\dj", new Ordinary("đ") }, // Glyph not in Latin Modern Math + { @"\L", new Ordinary("Ł") }, + { @"\l", new Ordinary("ł") }, + { @"\NG", new Ordinary("Ŋ") }, + { @"\ng", new Ordinary("ŋ") }, + { @"\o", new Ordinary("ø") }, + { @"\O", new Ordinary("Ø") }, + { @"\OE", new Ordinary("Œ") }, + { @"\oe", new Ordinary("œ") }, + { @"\ss", new Ordinary("ß") }, + { @"\SS", new Ordinary("SS") }, + { @"\TH", new Ordinary("Þ") }, + { @"\th", new Ordinary("þ") }, // Table 4: Greek Letters - { "alpha", new Variable("α") }, - { "beta", new Variable("β") }, - { "gamma", new Variable("γ") }, - { "delta", new Variable("δ") }, - { "epsilon", new Variable("ϵ") }, - { "varepsilon", new Variable("ε") }, - { "zeta", new Variable("ζ") }, - { "eta", new Variable("η") }, - { "theta", new Variable("θ") }, - { "vartheta", new Variable("ϑ") }, - { "iota", new Variable("ι") }, - { "kappa", new Variable("κ") }, - { "lambda", new Variable("λ") }, - { "mu", new Variable("μ") }, - { "nu", new Variable("ν") }, - { "xi", new Variable("ξ") }, - { "omicron", new Variable("ο") }, - { "pi", new Variable("π") }, - { "varpi", new Variable("ϖ") }, - { "rho", new Variable("ρ") }, - { "varrho", new Variable("ϱ") }, - { "sigma", new Variable("σ") }, - { "varsigma", new Variable("ς") }, - { "tau", new Variable("τ") }, - { "upsilon", new Variable("υ") }, - { "phi", new Variable("ϕ") }, // Don't be fooled by Visual Studio! - { "varphi", new Variable("φ") }, // The Visual Studio font is wrong! - { "chi", new Variable("χ") }, - { "psi", new Variable("ψ") }, - { "omega", new Variable("ω") }, - - { "Gamma", new Variable("Γ") }, - { "Delta", new Variable("Δ") }, - { "Theta", new Variable("Θ") }, - { "Lambda", new Variable("Λ") }, - { "Xi", new Variable("Ξ") }, - { "Pi", new Variable("Π") }, - { "Sigma", new Variable("Σ") }, - { "Upsilon", new Variable("Υ") }, - { "Phi", new Variable("Φ") }, - { "Psi", new Variable("Ψ") }, - { "Omega", new Variable("Ω") }, + { @"\alpha", new Variable("α") }, + { @"\beta", new Variable("β") }, + { @"\gamma", new Variable("γ") }, + { @"\delta", new Variable("δ") }, + { @"\epsilon", new Variable("ϵ") }, + { @"\varepsilon", new Variable("ε") }, + { @"\zeta", new Variable("ζ") }, + { @"\eta", new Variable("η") }, + { @"\theta", new Variable("θ") }, + { @"\vartheta", new Variable("ϑ") }, + { @"\iota", new Variable("ι") }, + { @"\kappa", new Variable("κ") }, + { @"\lambda", new Variable("λ") }, + { @"\mu", new Variable("μ") }, + { @"\nu", new Variable("ν") }, + { @"\xi", new Variable("ξ") }, + { @"\omicron", new Variable("ο") }, + { @"\pi", new Variable("π") }, + { @"\varpi", new Variable("ϖ") }, + { @"\rho", new Variable("ρ") }, + { @"\varrho", new Variable("ϱ") }, + { @"\sigma", new Variable("σ") }, + { @"\varsigma", new Variable("ς") }, + { @"\tau", new Variable("τ") }, + { @"\upsilon", new Variable("υ") }, + { @"\phi", new Variable("ϕ") }, // Don't be fooled by Visual Studio! + { @"\varphi", new Variable("φ") }, // The Visual Studio font is wrong! + { @"\chi", new Variable("χ") }, + { @"\psi", new Variable("ψ") }, + { @"\omega", new Variable("ω") }, + + { @"\Gamma", new Variable("Γ") }, + { @"\Delta", new Variable("Δ") }, + { @"\Theta", new Variable("Θ") }, + { @"\Lambda", new Variable("Λ") }, + { @"\Xi", new Variable("Ξ") }, + { @"\Pi", new Variable("Π") }, + { @"\Sigma", new Variable("Σ") }, + { @"\Upsilon", new Variable("Υ") }, + { @"\Phi", new Variable("Φ") }, + { @"\Psi", new Variable("Ψ") }, + { @"\Omega", new Variable("Ω") }, // (The remaining Greek majuscules can be produced with ordinary Latin letters. // The symbol “M”, for instance, is used for both an uppercase “m” and an uppercase “µ”. // Table 5: Punctuation Marks Not Found in OT - { "guillemotleft", new Punctuation("«") }, - { "guillemotright", new Punctuation("»") }, - { "guilsinglleft", new Punctuation("‹") }, - { "guilsinglright", new Punctuation("›") }, - { "quotedblbase", new Punctuation("„") }, - { "quotesinglbase", new Punctuation("‚") }, // This is not the comma - { "textquotedbl", new Punctuation("\"") }, + { @"\guillemotleft", new Punctuation("«") }, + { @"\guillemotright", new Punctuation("»") }, + { @"\guilsinglleft", new Punctuation("‹") }, + { @"\guilsinglright", new Punctuation("›") }, + { @"\quotedblbase", new Punctuation("„") }, + { @"\quotesinglbase", new Punctuation("‚") }, // This is not the comma + { "\"", @"\textquotedbl", new Punctuation("\"") }, // Table 6: Predefined LaTeX2ε Text-Mode Commands // [Skip text mode commands] // Table 7: Binary Operation Symbols - { "pm", new BinaryOperator("±") }, - { "mp", new BinaryOperator("∓") }, - { "times", Times }, - { "div", Divide }, - { "ast", new BinaryOperator("∗") }, - { "star" , new BinaryOperator("⋆") }, - { "circ" , new BinaryOperator("◦") }, - { "bullet", new BinaryOperator("•") }, - { "cdot" , new BinaryOperator("·") }, - // + - { "cap", new BinaryOperator("∩") }, - { "cup", new BinaryOperator("∪") }, - { "uplus", new BinaryOperator("⊎") }, - { "sqcap", new BinaryOperator("⊓") }, - { "sqcup", new BinaryOperator("⊔") }, - { "vee", "lor", new BinaryOperator("∨") }, - { "wedge", "land", new BinaryOperator("∧") }, - { "setminus", new BinaryOperator("∖") }, - { "wr", new BinaryOperator("≀") }, - // - - { "diamond", new BinaryOperator("⋄") }, - { "bigtriangleup", new BinaryOperator("△") }, - { "bigtriangledown", new BinaryOperator("▽") }, - { "triangleleft", new BinaryOperator("◁") }, // Latin Modern Math doesn't have ◃ - { "triangleright", new BinaryOperator("▷") }, // Latin Modern Math doesn't have ▹ - { "lhd", new BinaryOperator("⊲") }, - { "rhd", new BinaryOperator("⊳") }, - { "unlhd", new BinaryOperator("⊴") }, - { "unrhd", new BinaryOperator("⊵") }, - { "oplus", new BinaryOperator("⊕") }, - { "ominus", new BinaryOperator("⊖") }, - { "otimes", new BinaryOperator("⊗") }, - { "oslash", new BinaryOperator("⊘") }, - { "odot", new BinaryOperator("⊙") }, - { "bigcirc", new BinaryOperator("◯") }, - { "dagger", new BinaryOperator("†") }, - { "ddagger", new BinaryOperator("‡") }, - { "amalg", new BinaryOperator("⨿") }, + { @"\pm", new BinaryOperator("±") }, + { @"\mp", new BinaryOperator("∓") }, + { @"\times", Times }, + { @"\div", Divide }, + { @"\ast", new BinaryOperator("∗") }, + { @"*", new BinaryOperator("*") }, // ADDED: For consistency with \ast + { @"\star", new BinaryOperator("⋆") }, + { @"\circ", new BinaryOperator("◦") }, + { @"\bullet", new BinaryOperator("•") }, + { @"\cdot", new BinaryOperator("·") }, + { @"+", new BinaryOperator("+") }, + { @"\cap", new BinaryOperator("∩") }, + { @"\cup", new BinaryOperator("∪") }, + { @"\uplus", new BinaryOperator("⊎") }, + { @"\sqcap", new BinaryOperator("⊓") }, + { @"\sqcup", new BinaryOperator("⊔") }, + { @"\vee", @"\lor", new BinaryOperator("∨") }, + { @"\wedge", @"\land", new BinaryOperator("∧") }, + { @"\setminus", new BinaryOperator("∖") }, + { @"\wr", new BinaryOperator("≀") }, + { @"-", new BinaryOperator("−") }, // Use the math minus sign, not hyphen + { @"\diamond", new BinaryOperator("⋄") }, + { @"\bigtriangleup", new BinaryOperator("△") }, + { @"\bigtriangledown", new BinaryOperator("▽") }, + { @"\triangleleft", new BinaryOperator("◁") }, // Latin Modern Math doesn't have ◃ + { @"\triangleright", new BinaryOperator("▷") }, // Latin Modern Math doesn't have ▹ + { @"\lhd", new BinaryOperator("⊲") }, + { @"\rhd", new BinaryOperator("⊳") }, + { @"\unlhd", new BinaryOperator("⊴") }, + { @"\unrhd", new BinaryOperator("⊵") }, + { @"\oplus", new BinaryOperator("⊕") }, + { @"\ominus", new BinaryOperator("⊖") }, + { @"\otimes", new BinaryOperator("⊗") }, + { @"\oslash", new BinaryOperator("⊘") }, + { @"\odot", new BinaryOperator("⊙") }, + { @"\bigcirc", new BinaryOperator("◯") }, + { @"\dagger", new BinaryOperator("†") }, + { @"\ddagger", new BinaryOperator("‡") }, + { @"\amalg", new BinaryOperator("⨿") }, // Table 8: Relation Symbols - { "leq", "le", new Relation("≤") }, - { "geq", "ge", new Relation("≥") }, - { "equiv", new Relation("≡") }, - { "models", new Relation("⊧") }, - { "prec", new Relation("≺") }, - { "succ", new Relation("≻") }, - { "sim", new Relation("∼") }, - { "perp", new Relation("⟂") }, - { "preceq", new Relation("⪯") }, - { "succeq", new Relation("⪰") }, - { "simeq", new Relation("≃") }, - { "mid", new Relation("∣") }, - { "ll", new Relation("≪") }, - { "gg", new Relation("≫") }, - { "asymp", new Relation("≍") }, - { "parallel", new Relation("∥") }, - { "subset", new Relation("⊂") }, - { "supset", new Relation("⊃") }, - { "approx", new Relation("≈") }, - { "bowtie", new Relation("⋈") }, - { "subseteq", new Relation("⊆") }, - { "supseteq", new Relation("⊇") }, - { "cong", new Relation("≅") }, + { @"\leq", @"\le", new Relation("≤") }, + { @"\geq", @"\ge", new Relation("≥") }, + { @"\equiv", new Relation("≡") }, + { @"\models", new Relation("⊧") }, + { @"\prec", new Relation("≺") }, + { @"\succ", new Relation("≻") }, + { @"\sim", new Relation("∼") }, + { @"\perp", new Relation("⟂") }, + { @"\preceq", new Relation("⪯") }, + { @"\succeq", new Relation("⪰") }, + { @"\simeq", new Relation("≃") }, + { @"\mid", new Relation("∣") }, + { @"\ll", new Relation("≪") }, + { @"\gg", new Relation("≫") }, + { @"\asymp", new Relation("≍") }, + { @"\parallel", new Relation("∥") }, + { @"\subset", new Relation("⊂") }, + { @"\supset", new Relation("⊃") }, + { @"\approx", new Relation("≈") }, + { @"\bowtie", new Relation("⋈") }, + { @"\subseteq", new Relation("⊆") }, + { @"\supseteq", new Relation("⊇") }, + { @"\cong", new Relation("≅") }, // Latin Modern Math doesn't have ⨝ so we copy the one from \bowtie - { "Join", new Relation("⋈") }, // Capital J is intentional - { "sqsubset", new Relation("⊏") }, - { "sqsupset", new Relation("⊐") }, - { "neq", "ne", new Relation("≠") }, - { "smile", new Relation("⌣") }, - { "sqsubseteq", new Relation("⊑") }, - { "sqsupseteq", new Relation("⊒") }, - { "doteq", new Relation("≐") }, - { "frown", new Relation("⌢") }, - { "in", new Relation("∈") }, - { "ni", new Relation("∋") }, - { "notin", new Relation("∉") }, - { "propto", new Relation("∝") }, - // = - { "vdash", new Relation("⊢") }, - { "dashv", new Relation("⊣") }, - // < - // > - // : + { @"\Join", new Relation("⋈") }, // Capital J is intentional + { @"\sqsubset", new Relation("⊏") }, + { @"\sqsupset", new Relation("⊐") }, + { @"\neq", @"\ne", new Relation("≠") }, + { @"\smile", new Relation("⌣") }, + { @"\sqsubseteq", new Relation("⊑") }, + { @"\sqsupseteq", new Relation("⊒") }, + { @"\doteq", new Relation("≐") }, + { @"\frown", new Relation("⌢") }, + { @"\in", new Relation("∈") }, + { @"\ni", new Relation("∋") }, + { @"\notin", new Relation("∉") }, + { @"\propto", new Relation("∝") }, + { @"=", new Relation("=") }, + { @"\vdash", new Relation("⊢") }, + { @"\dashv", new Relation("⊣") }, + { @"<", new Relation("<") }, + { @">", new Relation(">") }, + { @":", new Relation("∶") }, // Colon is a ratio. Regular colon is \colon // Table 9: Punctuation Symbols - // , - // ; - { "colon", new Punctuation(":") }, // \colon is different from : which is a relation - { "ldotp", new Punctuation(".") }, // Aka the full stop or decimal dot - { "cdotp", new Punctuation("·") }, - + { @",", new Punctuation(",") }, + { @";", new Punctuation(";") }, + { @"\colon", new Punctuation(":") }, // \colon is different from : which is a relation + { @"\ldotp", new Punctuation(".") }, // Aka the full stop or decimal dot + { @"\cdotp", new Punctuation("·") }, + { @"!", new Punctuation("!") }, // ADDED: According to https://latex.wikia.org/wiki/List_of_LaTeX_symbols#Class_6_.28Pun.29_symbols:_postfix_.2F_punctuation + { @"?", new Punctuation("?") }, // ADDED: According to https://latex.wikia.org/wiki/List_of_LaTeX_symbols#Class_6_.28Pun.29_symbols:_postfix_.2F_punctuation + // Table 10: Arrow Symbols - { "leftarrow", "gets", new Relation("←") }, - { "longleftarrow", new Relation("⟵") }, - { "uparrow", new Relation("↑") }, - { "Leftarrow", new Relation("⇐") }, - { "Longleftarrow", new Relation("⟸") }, - { "Uparrow", new Relation("⇑") }, - { "rightarrow", "to", new Relation("→") }, - { "longrightarrow", new Relation("⟶") }, - { "downarrow", new Relation("↓") }, - { "Rightarrow", new Relation("⇒") }, - { "Longrightarrow", new Relation("⟹") }, - { "Downarrow", new Relation("⇓") }, - { "leftrightarrow", new Relation("↔") }, - { "Leftrightarrow", new Relation("⇔") }, - { "updownarrow", new Relation("↕") }, - { "longleftrightarrow", new Relation("⟷") }, - { "Longleftrightarrow", "iff", new Relation("⟺") }, - { "Updownarrow", new Relation("⇕") }, - { "mapsto", new Relation("↦") }, - { "longmapsto", new Relation("⟼") }, - { "nearrow", new Relation("↗") }, - { "hookleftarrow", new Relation("↩") }, - { "hookrightarrow", new Relation("↪") }, - { "searrow", new Relation("↘") }, - { "leftharpoonup", new Relation("↼") }, - { "rightharpoonup", new Relation("⇀") }, - { "swarrow", new Relation("↙") }, - { "leftharpoondown", new Relation("↽") }, - { "rightharpoondown", new Relation("⇁") }, - { "nwarrow", new Relation("↖") }, - { "rightleftharpoons", new Relation("⇌") }, - { "leadsto", new Relation("⇝") }, // same as \rightsquigarrow + { @"\leftarrow", @"\gets", new Relation("←") }, + { @"\longleftarrow", new Relation("⟵") }, + { @"\uparrow", new Relation("↑") }, + { @"\Leftarrow", new Relation("⇐") }, + { @"\Longleftarrow", new Relation("⟸") }, + { @"\Uparrow", new Relation("⇑") }, + { @"\rightarrow", @"\to", new Relation("→") }, + { @"\longrightarrow", new Relation("⟶") }, + { @"\downarrow", new Relation("↓") }, + { @"\Rightarrow", new Relation("⇒") }, + { @"\Longrightarrow", new Relation("⟹") }, + { @"\Downarrow", new Relation("⇓") }, + { @"\leftrightarrow", new Relation("↔") }, + { @"\Leftrightarrow", new Relation("⇔") }, + { @"\updownarrow", new Relation("↕") }, + { @"\longleftrightarrow", new Relation("⟷") }, + { @"\Longleftrightarrow", @"\iff", new Relation("⟺") }, + { @"\Updownarrow", new Relation("⇕") }, + { @"\mapsto", new Relation("↦") }, + { @"\longmapsto", new Relation("⟼") }, + { @"\nearrow", new Relation("↗") }, + { @"\hookleftarrow", new Relation("↩") }, + { @"\hookrightarrow", new Relation("↪") }, + { @"\searrow", new Relation("↘") }, + { @"\leftharpoonup", new Relation("↼") }, + { @"\rightharpoonup", new Relation("⇀") }, + { @"\swarrow", new Relation("↙") }, + { @"\leftharpoondown", new Relation("↽") }, + { @"\rightharpoondown", new Relation("⇁") }, + { @"\nwarrow", new Relation("↖") }, + { @"\rightleftharpoons", new Relation("⇌") }, + { @"\leadsto", new Relation("⇝") }, // same as \rightsquigarrow // Table 11: Miscellaneous Symbols - { "ldots", new Ordinary("…") }, - { "aleph", new Ordinary("ℵ") }, - { "hbar", new Ordinary("ℏ") }, - { "imath", new Ordinary("𝚤") }, - { "jmath", new Ordinary("𝚥") }, - { "ell", new Ordinary("ℓ") }, - { "wp", new Ordinary("℘") }, - { "Re", new Ordinary("ℜ") }, - { "Im", new Ordinary("ℑ") }, - { "mho", new Ordinary("℧") }, - { "cdots", new Ordinary("⋯") }, + { @"\ldots", new Punctuation("…") }, // CHANGED: Not Ordinary for consistency with \cdots, \vdots and \ddots + { @"\aleph", new Ordinary("ℵ") }, + { @"\hbar", new Ordinary("ℏ") }, + { @"\imath", new Ordinary("𝚤") }, + { @"\jmath", new Ordinary("𝚥") }, + { @"\ell", new Ordinary("ℓ") }, + { @"\wp", new Ordinary("℘") }, + { @"\Re", new Ordinary("ℜ") }, + { @"\Im", new Ordinary("ℑ") }, + { @"\mho", new Ordinary("℧") }, + { @"\cdots", @"\dotsb", new Ordinary("⋯") }, // CHANGED: Not Ordinary according to https://latex.wikia.org/wiki/List_of_LaTeX_symbols#Class_6_.28Pun.29_symbols:_postfix_.2F_punctuation // \prime is removed because Unicode has no matching character - { "emptyset", new Ordinary("∅") }, - { "nabla", new Ordinary("∇") }, - { "surd", new Ordinary("√") }, - { "top", new Ordinary("⊤") }, - { "bot", new Ordinary("⊥") }, - { "|", "Vert", new Ordinary("‖") }, - { "angle", new Ordinary("∠") }, - // . - { "vdots", new Ordinary("⋮") }, - { "forall", new Ordinary("∀") }, - { "exists", new Ordinary("∃") }, - { "neg", "lnot", new Ordinary("¬") }, - { "flat", new Ordinary("♭") }, - { "natural", new Ordinary("♮") }, - { "sharp", new Ordinary("♯") }, - { "backslash", new Ordinary("\\") }, - { "partial", new Ordinary("𝜕") }, - { "vert", new Ordinary("|") }, - { "ddots", new Ordinary("⋱") }, - { "infty", new Ordinary("∞") }, - { "Box", new Ordinary("□") }, // same as \square - { "Diamond", new Ordinary("◊") }, // same as \lozenge - { "triangle", new Ordinary("△") }, - { "clubsuit", new Ordinary("♣") }, - { "diamondsuit", new Ordinary("♢") }, - { "heartsuit", new Ordinary("♡") }, - { "spadesuit", new Ordinary("♠") }, + { @"\emptyset", new Ordinary("∅") }, + { @"\nabla", new Ordinary("∇") }, + { @"\surd", new Ordinary("√") }, + { @"\top", new Ordinary("⊤") }, + { @"\bot", new Ordinary("⊥") }, + { @"\|", @"\Vert", new Ordinary("‖") }, + { @"\angle", new Ordinary("∠") }, + { @".", new Number(".") }, // CHANGED: Not punctuation for easy parsing of numbers + { @"\vdots", new Punctuation("⋮") }, // CHANGED: Not Ordinary according to https://latex.wikia.org/wiki/List_of_LaTeX_symbols#Class_6_.28Pun.29_symbols:_postfix_.2F_punctuation + { @"\forall", new Ordinary("∀") }, + { @"\exists", new Ordinary("∃") }, + { @"\neg", "lnot", new Ordinary("¬") }, + { @"\flat", new Ordinary("♭") }, + { @"\natural", new Ordinary("♮") }, + { @"\sharp", new Ordinary("♯") }, + { @"\backslash", new Ordinary("\\") }, + { @"\partial", new Ordinary("𝜕") }, + { @"\vert", new Ordinary("|") }, + { @"\ddots", new Punctuation("⋱") }, // CHANGED: Not Ordinary according to https://latex.wikia.org/wiki/List_of_LaTeX_symbols#Class_6_.28Pun.29_symbols:_postfix_.2F_punctuation + { @"\infty", new Ordinary("∞") }, + { @"\Box", new Ordinary("□") }, // same as \square + { @"\Diamond", new Ordinary("◊") }, // same as \lozenge + { @"\triangle", new Ordinary("△") }, + { @"\clubsuit", new Ordinary("♣") }, + { @"\diamondsuit", new Ordinary("♢") }, + { @"\heartsuit", new Ordinary("♡") }, + { @"\spadesuit", new Ordinary("♠") }, // Table 12: Variable-sized Symbols - { "sum", new LargeOperator("∑", null) }, - { "prod", new LargeOperator("∏", null) }, - { "coprod", new LargeOperator("∐", null) }, - { "int", new LargeOperator("∫", false) }, - { "oint", new LargeOperator("∮", false) }, - { "bigcap", new LargeOperator("⋂", null) }, - { "bigcup", new LargeOperator("⋃", null) }, - { "bigsqcup", new LargeOperator("⨆", null) }, - { "bigvee", new LargeOperator("⋁", null) }, - { "bigwedge", new LargeOperator("⋀", null) }, - { "bigodot", new LargeOperator("⨀", null) }, - { "bigoplus", new LargeOperator("⨁", null) }, - { "bigotimes", new LargeOperator("⨂", null) }, - { "biguplus", new LargeOperator("⨄", null) }, + { @"\sum", new LargeOperator("∑", null) }, + { @"\prod", new LargeOperator("∏", null) }, + { @"\coprod", new LargeOperator("∐", null) }, + { @"\int", new LargeOperator("∫", false) }, + { @"\oint", new LargeOperator("∮", false) }, + { @"\bigcap", new LargeOperator("⋂", null) }, + { @"\bigcup", new LargeOperator("⋃", null) }, + { @"\bigsqcup", new LargeOperator("⨆", null) }, + { @"\bigvee", new LargeOperator("⋁", null) }, + { @"\bigwedge", new LargeOperator("⋀", null) }, + { @"\bigodot", new LargeOperator("⨀", null) }, + { @"\bigoplus", new LargeOperator("⨁", null) }, + { @"\bigotimes", new LargeOperator("⨂", null) }, + { @"\biguplus", new LargeOperator("⨄", null) }, // Table 13: Log-like Symbols - { "arccos", new LargeOperator("arccos", false, true) }, - { "arcsin", new LargeOperator("arcsin", false, true) }, - { "arctan", new LargeOperator("arctan", false, true) }, - { "arg", new LargeOperator("arg", false, true) }, - { "cos", new LargeOperator("cos", false, true) }, - { "cosh", new LargeOperator("cosh", false, true) }, - { "cot", new LargeOperator("cot", false, true) }, - { "coth", new LargeOperator("coth", false, true) }, - { "csc", new LargeOperator("csc", false, true) }, - { "deg", new LargeOperator("deg", false, true) }, - { "det", new LargeOperator("det", null) }, - { "dim", new LargeOperator("dim", false, true) }, - { "exp", new LargeOperator("exp", false, true) }, - { "gcd", new LargeOperator("gcd", null) }, - { "hom", new LargeOperator("hom", false, true) }, - { "inf", new LargeOperator("inf", null) }, - { "ker", new LargeOperator("ker", false, true) }, - { "lg", new LargeOperator("lg", false, true) }, - { "lim", new LargeOperator("lim", null) }, - { "liminf", new LargeOperator("lim inf", null) }, - { "limsup", new LargeOperator("lim sup", null) }, - { "ln", new LargeOperator("ln", false, true) }, - { "log", new LargeOperator("log", false, true) }, - { "max", new LargeOperator("max", null) }, - { "min", new LargeOperator("min", null) }, - { "Pr", new LargeOperator("Pr", null) }, - { "sec", new LargeOperator("sec", false, true) }, - { "sin", new LargeOperator("sin", false, true) }, - { "sinh", new LargeOperator("sinh", false, true) }, - { "sup", new LargeOperator("sup", null) }, - { "tan", new LargeOperator("tan", false, true) }, - { "tanh", new LargeOperator("tanh", false, true) }, + { @"\arccos", new LargeOperator("arccos", false, true) }, + { @"\arcsin", new LargeOperator("arcsin", false, true) }, + { @"\arctan", new LargeOperator("arctan", false, true) }, + { @"\arg", new LargeOperator("arg", false, true) }, + { @"\cos", new LargeOperator("cos", false, true) }, + { @"\cosh", new LargeOperator("cosh", false, true) }, + { @"\cot", new LargeOperator("cot", false, true) }, + { @"\coth", new LargeOperator("coth", false, true) }, + { @"\csc", new LargeOperator("csc", false, true) }, + { @"\deg", new LargeOperator("deg", false, true) }, + { @"\det", new LargeOperator("det", null) }, + { @"\dim", new LargeOperator("dim", false, true) }, + { @"\exp", new LargeOperator("exp", false, true) }, + { @"\gcd", new LargeOperator("gcd", null) }, + { @"\hom", new LargeOperator("hom", false, true) }, + { @"\inf", new LargeOperator("inf", null) }, + { @"\ker", new LargeOperator("ker", false, true) }, + { @"\lg", new LargeOperator("lg", false, true) }, + { @"\lim", new LargeOperator("lim", null) }, + { @"\liminf", new LargeOperator("lim inf", null) }, + { @"\limsup", new LargeOperator("lim sup", null) }, + { @"\ln", new LargeOperator("ln", false, true) }, + { @"\log", new LargeOperator("log", false, true) }, + { @"\max", new LargeOperator("max", null) }, + { @"\min", new LargeOperator("min", null) }, + { @"\Pr", new LargeOperator("Pr", null) }, + { @"\sec", new LargeOperator("sec", false, true) }, + { @"\sin", new LargeOperator("sin", false, true) }, + { @"\sinh", new LargeOperator("sinh", false, true) }, + { @"\sup", new LargeOperator("sup", null) }, + { @"\tan", new LargeOperator("tan", false, true) }, + { @"\tanh", new LargeOperator("tanh", false, true) }, // Table 14: Delimiters // Table 15: Large Delimiters @@ -621,20 +871,20 @@ public static StringBuilder ColorToString(Color color, StringBuilder sb) { // Table 16: Math-Mode Accents // Use escape sequence for combining characters - { "hat", new Accent("\u0302") }, // In our implementation hat and widehat behave the same. - { "acute", new Accent("\u0301") }, - { "bar", new Accent("\u0304") }, - { "dot", new Accent("\u0307") }, - { "breve", new Accent("\u0306") }, - { "check", new Accent("\u030C") }, - { "grave", new Accent("\u0300") }, - { "vec", new Accent("\u20D7") }, - { "ddot", new Accent("\u0308") }, - { "tilde", new Accent("\u0303") }, // In our implementation tilde and widetilde behave the same. + { @"\hat", new Accent("\u0302") }, // In our implementation hat and widehat behave the same. + { @"\acute", new Accent("\u0301") }, + { @"\bar", new Accent("\u0304") }, + { @"\dot", new Accent("\u0307") }, + { @"\breve", new Accent("\u0306") }, + { @"\check", new Accent("\u030C") }, + { @"\grave", new Accent("\u0300") }, + { @"\vec", new Accent("\u20D7") }, + { @"\ddot", new Accent("\u0308") }, + { @"\tilde", new Accent("\u0303") }, // In our implementation tilde and widetilde behave the same. // Table 17: Some Other Constructions - { "widehat", new Accent("\u0302") }, - { "widetilde", new Accent("\u0303") }, + { @"\widehat", new Accent("\u0302") }, + { @"\widetilde", new Accent("\u0303") }, // TODO: implement \overleftarrow, \overrightarrow, \overbrace, \underbrace // \overleftarrow{} // \overrightarrow{} @@ -644,7 +894,10 @@ public static StringBuilder ColorToString(Color color, StringBuilder sb) { // \underbrace{} // \sqrt{} // \sqrt[]{} - // ' + { @"'", new Ordinary("′") }, + { @"''", new Ordinary("″") }, // ADDED: Custom addition + { @"'''", new Ordinary("‴") }, // ADDED: Custom addition + { @"''''", new Ordinary("⁗") }, // ADDED: Custom addition // \frac{}{} // Table 18: textcomp Symbols @@ -654,235 +907,235 @@ public static StringBuilder ColorToString(Color color, StringBuilder sb) { // [See BoundaryDelimiters dictionary above] // Table 20: AMS Arrows - //{ "dashrightarrow", new Relation("⇢") }, // Glyph not in Latin Modern Math - //{ "dashleftarrow", new Relation("⇠") }, // Glyph not in Latin Modern Math - { "leftleftarrows", new Relation("⇇") }, - { "leftrightarrows", new Relation("⇆") }, - { "Lleftarrow", new Relation("⇚") }, - { "twoheadleftarrow", new Relation("↞") }, - { "leftarrowtail", new Relation("↢") }, - { "looparrowleft", new Relation("↫") }, - { "leftrightharpoons", new Relation("⇋") }, - { "curvearrowleft", new Relation("↶") }, - { "circlearrowleft", new Relation("↺") }, - { "Lsh", new Relation("↰") }, - { "upuparrows", new Relation("⇈") }, - { "upharpoonleft", new Relation("↿") }, - { "downharpoonleft", new Relation("⇃") }, - { "multimap", new Relation("⊸") }, - { "leftrightsquigarrow", new Relation("↭") }, - { "rightrightarrows", new Relation("⇉") }, - { "rightleftarrows", new Relation("⇄") }, + //{ @"\dashrightarrow", new Relation("⇢") }, // Glyph not in Latin Modern Math + //{ @"\dashleftarrow", new Relation("⇠") }, // Glyph not in Latin Modern Math + { @"\leftleftarrows", new Relation("⇇") }, + { @"\leftrightarrows", new Relation("⇆") }, + { @"\Lleftarrow", new Relation("⇚") }, + { @"\twoheadleftarrow", new Relation("↞") }, + { @"\leftarrowtail", new Relation("↢") }, + { @"\looparrowleft", new Relation("↫") }, + { @"\leftrightharpoons", new Relation("⇋") }, + { @"\curvearrowleft", new Relation("↶") }, + { @"\circlearrowleft", new Relation("↺") }, + { @"\Lsh", new Relation("↰") }, + { @"\upuparrows", new Relation("⇈") }, + { @"\upharpoonleft", new Relation("↿") }, + { @"\downharpoonleft", new Relation("⇃") }, + { @"\multimap", new Relation("⊸") }, + { @"\leftrightsquigarrow", new Relation("↭") }, + { @"\rightrightarrows", new Relation("⇉") }, + { @"\rightleftarrows", new Relation("⇄") }, // Duplicate entry in LaTeX Symbol list: \rightrightarrows // Duplicate entry in LaTeX Symbol list: \rightleftarrows - { "twoheadrightarrow", new Relation("↠") }, - { "rightarrowtail", new Relation("↣") }, - { "looparrowright", new Relation("↬") }, + { @"\twoheadrightarrow", new Relation("↠") }, + { @"\rightarrowtail", new Relation("↣") }, + { @"\looparrowright", new Relation("↬") }, // \rightleftharpoons defined in Table 10 - { "curvearrowright", new Relation("↷") }, - { "circlearrowright", new Relation("↻") }, - { "Rsh", new Relation("↱") }, - { "downdownarrows", new Relation("⇊") }, - { "upharpoonright", new Relation("↾") }, - { "downharpoonright", new Relation("⇂") }, - { "rightsquigarrow", new Relation("⇝") }, + { @"\curvearrowright", new Relation("↷") }, + { @"\circlearrowright", new Relation("↻") }, + { @"\Rsh", new Relation("↱") }, + { @"\downdownarrows", new Relation("⇊") }, + { @"\upharpoonright", new Relation("↾") }, + { @"\downharpoonright", new Relation("⇂") }, + { @"\rightsquigarrow", new Relation("⇝") }, // Table 21: AMS Negated Arrows - { "nleftarrow", new Relation("↚") }, - { "nrightarrow", new Relation("↛") }, - { "nLeftarrow", new Relation("⇍") }, - { "nRightarrow", new Relation("⇏") }, - { "nleftrightarrow", new Relation("↮") }, - { "nLeftrightarrow", new Relation("⇎") }, + { @"\nleftarrow", new Relation("↚") }, + { @"\nrightarrow", new Relation("↛") }, + { @"\nLeftarrow", new Relation("⇍") }, + { @"\nRightarrow", new Relation("⇏") }, + { @"\nleftrightarrow", new Relation("↮") }, + { @"\nLeftrightarrow", new Relation("⇎") }, // Table 22: AMS Greek - // { "digamma", new Variable("ϝ") }, // Glyph not in Latin Modern Math - { "varkappa", new Variable("ϰ") }, + // { @"\digamma", new Variable("ϝ") }, // Glyph not in Latin Modern Math + { @"\varkappa", new Variable("ϰ") }, // Table 23: AMS Hebrew - { "beth", new Ordinary("ℶ") }, - { "daleth", new Ordinary("ℸ") }, - { "gimel", new Ordinary("ℷ") }, + { @"\beth", new Ordinary("ℶ") }, + { @"\daleth", new Ordinary("ℸ") }, + { @"\gimel", new Ordinary("ℷ") }, // Table 24: AMS Miscellaneous // \hbar defined in Table 11 - { "hslash", new Ordinary("ℏ") }, // Same as \hbar - { "vartriangle", new Ordinary("△") }, // ▵ not in Latin Modern Math // ▵ is actually a triangle, not an inverted v as displayed in Visual Studio - { "triangledown", new Ordinary("▽") }, // ▿ not in Latin Modern Math - { "square", Placeholder }, - { "lozenge", new Ordinary("◊") }, - // { "circledS", new Ordinary("Ⓢ") }, // Glyph not in Latin Modern Math + { @"\hslash", new Ordinary("ℏ") }, // Same as \hbar + { @"\vartriangle", new Ordinary("△") }, // ▵ not in Latin Modern Math // ▵ is actually a triangle, not an inverted v as displayed in Visual Studio + { @"\triangledown", new Ordinary("▽") }, // ▿ not in Latin Modern Math + { @"\square", Placeholder }, + { @"\lozenge", new Ordinary("◊") }, + // { @"\circledS", new Ordinary("Ⓢ") }, // Glyph not in Latin Modern Math // \angle defined in Table 11 - { "measuredangle", new Ordinary("∡") }, - { "nexists", new Ordinary("∄") }, + { @"\measuredangle", new Ordinary("∡") }, + { @"\nexists", new Ordinary("∄") }, // \mho defined in Table 11 - // { "Finv", new Ordinary("Ⅎ") }, // Glyph not in Latin Modern Math - // { "Game", new Ordinary("⅁") }, // Glyph not in Latin Modern Math - { "Bbbk", new Ordinary("𝐤") }, - { "backprime", new Ordinary("‵") }, - { "varnothing", new Ordinary("∅") }, // Same as \emptyset - { "blacktriangle", new Ordinary("▲") }, // ▴ not in Latin Modern Math - { "blacktriangledown", new Ordinary("▼") }, // ▾ not in Latin Modern Math - { "blacksquare", new Ordinary("▪") }, - { "blacklozenge", new Ordinary("♦") }, // ⧫ not in Latin Modern Math - { "bigstar", new Ordinary("⋆") }, // ★ not in Latin Modern Math - { "sphericalangle", new Ordinary("∢") }, - { "complement", new Ordinary("∁") }, - { "eth", new Ordinary("ð") }, // Same as \dh - { "diagup", new Ordinary("/") }, // ╱ not in Latin Modern Math - { "diagdown", new Ordinary("\\") }, // ╲ not in Latin Modern Math + // { @"\Finv", new Ordinary("Ⅎ") }, // Glyph not in Latin Modern Math + // { @"\Game", new Ordinary("⅁") }, // Glyph not in Latin Modern Math + { @"\Bbbk", new Ordinary("𝐤") }, + { @"\backprime", new Ordinary("‵") }, + { @"\varnothing", new Ordinary("∅") }, // Same as \emptyset + { @"\blacktriangle", new Ordinary("▲") }, // ▴ not in Latin Modern Math + { @"\blacktriangledown", new Ordinary("▼") }, // ▾ not in Latin Modern Math + { @"\blacksquare", new Ordinary("▪") }, + { @"\blacklozenge", new Ordinary("♦") }, // ⧫ not in Latin Modern Math + { @"\bigstar", new Ordinary("⋆") }, // ★ not in Latin Modern Math + { @"\sphericalangle", new Ordinary("∢") }, + { @"\complement", new Ordinary("∁") }, + { @"\eth", new Ordinary("ð") }, // Same as \dh + { @"\diagup", new Ordinary("/") }, // ╱ not in Latin Modern Math + { @"\diagdown", new Ordinary("\\") }, // ╲ not in Latin Modern Math // Table 25: AMS Commands Defined to Work in Both Math and Text Mode - { "checkmark", new Ordinary("✓") }, - { "circledR", new Ordinary("®") }, - { "maltese", new Ordinary("✠") }, + { @"\checkmark", new Ordinary("✓") }, + { @"\circledR", new Ordinary("®") }, + { @"\maltese", new Ordinary("✠") }, // Table 26: AMS Binary Operators - { "dotplus", new BinaryOperator("∔") }, - { "smallsetminus", new BinaryOperator("∖") }, - { "Cap", new BinaryOperator("⋒") }, - { "Cup", new BinaryOperator("⋓") }, - { "barwedge", new BinaryOperator("⌅") }, - { "veebar", new BinaryOperator("⊻") }, - // { "doublebarwedge", new BinaryOperator("⩞") }, //Glyph not in Latin Modern Math - { "boxminus", new BinaryOperator("⊟") }, - { "boxtimes", new BinaryOperator("⊠") }, - { "boxdot", new BinaryOperator("⊡") }, - { "boxplus", new BinaryOperator("⊞") }, - { "divideontimes", new BinaryOperator("⋇") }, - { "ltimes", new BinaryOperator("⋉") }, - { "rtimes", new BinaryOperator("⋊") }, - { "leftthreetimes", new BinaryOperator("⋋") }, - { "rightthreetimes", new BinaryOperator("⋌") }, - { "curlywedge", new BinaryOperator("⋏") }, - { "curlyvee", new BinaryOperator("⋎") }, - { "circleddash", new BinaryOperator("⊝") }, - { "circledast", new BinaryOperator("⊛") }, - { "circledcirc", new BinaryOperator("⊚") }, - { "centerdot", new BinaryOperator("·") }, // Same as \cdot - { "intercal", new BinaryOperator("⊺") }, + { @"\dotplus", new BinaryOperator("∔") }, + { @"\smallsetminus", new BinaryOperator("∖") }, + { @"\Cap", new BinaryOperator("⋒") }, + { @"\Cup", new BinaryOperator("⋓") }, + { @"\barwedge", new BinaryOperator("⌅") }, + { @"\veebar", new BinaryOperator("⊻") }, + // { @"\doublebarwedge", new BinaryOperator("⩞") }, //Glyph not in Latin Modern Math + { @"\boxminus", new BinaryOperator("⊟") }, + { @"\boxtimes", new BinaryOperator("⊠") }, + { @"\boxdot", new BinaryOperator("⊡") }, + { @"\boxplus", new BinaryOperator("⊞") }, + { @"\divideontimes", new BinaryOperator("⋇") }, + { @"\ltimes", new BinaryOperator("⋉") }, + { @"\rtimes", new BinaryOperator("⋊") }, + { @"\leftthreetimes", new BinaryOperator("⋋") }, + { @"\rightthreetimes", new BinaryOperator("⋌") }, + { @"\curlywedge", new BinaryOperator("⋏") }, + { @"\curlyvee", new BinaryOperator("⋎") }, + { @"\circleddash", new BinaryOperator("⊝") }, + { @"\circledast", new BinaryOperator("⊛") }, + { @"\circledcirc", new BinaryOperator("⊚") }, + { @"\centerdot", new BinaryOperator("·") }, // Same as \cdot + { @"\intercal", new BinaryOperator("⊺") }, // Table 27: AMS Binary Relations - { "leqq", new Relation("≦") }, - { "leqslant", new Relation("⩽") }, - { "eqslantless", new Relation("⪕") }, - { "lesssim", new Relation("≲") }, - { "lessapprox", new Relation("⪅") }, - { "approxeq", new Relation("≊") }, - { "lessdot", new Relation("⋖") }, - { "lll", new Relation("⋘") }, - { "lessgtr", new Relation("≶") }, - { "lesseqgtr", new Relation("⋚") }, - { "lesseqqgtr", new Relation("⪋") }, - { "doteqdot", new Relation("≑") }, - { "risingdotseq", new Relation("≓") }, - { "fallingdotseq", new Relation("≒") }, - { "backsim", new Relation("∽") }, - { "backsimeq", new Relation("⋍") }, - // { "subseteqq", new Relation("⫅") }, // Glyph not in Latin Modern Math - { "Subset", new Relation("⋐") }, + { @"\leqq", new Relation("≦") }, + { @"\leqslant", new Relation("⩽") }, + { @"\eqslantless", new Relation("⪕") }, + { @"\lesssim", new Relation("≲") }, + { @"\lessapprox", new Relation("⪅") }, + { @"\approxeq", new Relation("≊") }, + { @"\lessdot", new Relation("⋖") }, + { @"\lll", new Relation("⋘") }, + { @"\lessgtr", new Relation("≶") }, + { @"\lesseqgtr", new Relation("⋚") }, + { @"\lesseqqgtr", new Relation("⪋") }, + { @"\doteqdot", new Relation("≑") }, + { @"\risingdotseq", new Relation("≓") }, + { @"\fallingdotseq", new Relation("≒") }, + { @"\backsim", new Relation("∽") }, + { @"\backsimeq", new Relation("⋍") }, + // { @"\subseteqq", new Relation("⫅") }, // Glyph not in Latin Modern Math + { @"\Subset", new Relation("⋐") }, // \sqsubset is defined in Table 8 - { "preccurlyeq", new Relation("≼") }, - { "curlyeqprec", new Relation("⋞") }, - { "precsim", new Relation("≾") }, - // { "precapprox", new Relation("⪷") }, // Glyph not in Latin Modern Math - { "vartriangleleft", new Relation("⊲") }, - { "trianglelefteq", new Relation("⊴") }, - { "vDash", new Relation("⊨") }, - { "Vvdash", new Relation("⊪") }, - { "smallsmile", new Relation("⌣") }, //Same as \smile - { "smallfrown", new Relation("⌢") }, //Same as \frown - { "bumpeq", new Relation("≏") }, - { "Bumpeq", new Relation("≎") }, - { "geqq", new Relation("≧") }, - { "geqslant", new Relation("⩾") }, - { "eqslantgtr", new Relation("⪖") }, - { "gtrsim", new Relation("≳") }, - { "gtrapprox", new Relation("⪆") }, - { "gtrdot", new Relation("⋗") }, - { "ggg", new Relation("⋙") }, - { "gtrless", new Relation("≷") }, - { "gtreqless", new Relation("⋛") }, - { "gtreqqless", new Relation("⪌") }, - { "eqcirc", new Relation("≖") }, - { "circeq", new Relation("≗") }, - { "triangleq", new Relation("≜") }, - { "thicksim", new Relation("∼") }, - { "thickapprox", new Relation("≈") }, - // { "supseteqq", new Relation("⫆") }, // Glyph not in Latin Modern Math - { "Supset", new Relation("⋑") }, + { @"\preccurlyeq", new Relation("≼") }, + { @"\curlyeqprec", new Relation("⋞") }, + { @"\precsim", new Relation("≾") }, + // { @"\precapprox", new Relation("⪷") }, // Glyph not in Latin Modern Math + { @"\vartriangleleft", new Relation("⊲") }, + { @"\trianglelefteq", new Relation("⊴") }, + { @"\vDash", new Relation("⊨") }, + { @"\Vvdash", new Relation("⊪") }, + { @"\smallsmile", new Relation("⌣") }, //Same as \smile + { @"\smallfrown", new Relation("⌢") }, //Same as \frown + { @"\bumpeq", new Relation("≏") }, + { @"\Bumpeq", new Relation("≎") }, + { @"\geqq", new Relation("≧") }, + { @"\geqslant", new Relation("⩾") }, + { @"\eqslantgtr", new Relation("⪖") }, + { @"\gtrsim", new Relation("≳") }, + { @"\gtrapprox", new Relation("⪆") }, + { @"\gtrdot", new Relation("⋗") }, + { @"\ggg", new Relation("⋙") }, + { @"\gtrless", new Relation("≷") }, + { @"\gtreqless", new Relation("⋛") }, + { @"\gtreqqless", new Relation("⪌") }, + { @"\eqcirc", new Relation("≖") }, + { @"\circeq", new Relation("≗") }, + { @"\triangleq", new Relation("≜") }, + { @"\thicksim", new Relation("∼") }, + { @"\thickapprox", new Relation("≈") }, + // { @"\supseteqq", new Relation("⫆") }, // Glyph not in Latin Modern Math + { @"\Supset", new Relation("⋑") }, // \sqsupset is defined in Table 8 - { "succcurlyeq", new Relation("≽") }, - { "curlyeqsucc", new Relation("⋟") }, - { "succsim", new Relation("≿") }, - // { "succapprox", new Relation("⪸") }, // Glyph not in Latin Modern Math - { "vartriangleright", new Relation("⊳") }, - { "trianglerighteq", new Relation("⊵") }, - { "Vdash", new Relation("⊩") }, - { "shortmid", new Relation("∣") }, - { "shortparallel", new Relation("∥") }, - { "between", new Relation("≬") }, - // { "pitchfork", new Relation("⋔") }, // Glyph not in Latin Modern Math - { "varpropto", new Relation("∝") }, - { "blacktriangleleft", new Relation("◀") }, // ◂ not in Latin Modern Math - { "therefore", new Relation("∴") }, - // { "backepsilon", new Relation("϶") }, // Glyph not in Latin Modern Math - { "blacktriangleright", new Relation("▶") }, // ▸ not in Latin Modern Math - { "because", new Relation("∵") }, + { @"\succcurlyeq", new Relation("≽") }, + { @"\curlyeqsucc", new Relation("⋟") }, + { @"\succsim", new Relation("≿") }, + // { @"\succapprox", new Relation("⪸") }, // Glyph not in Latin Modern Math + { @"\vartriangleright", new Relation("⊳") }, + { @"\trianglerighteq", new Relation("⊵") }, + { @"\Vdash", new Relation("⊩") }, + { @"\shortmid", new Relation("∣") }, + { @"\shortparallel", new Relation("∥") }, + { @"\between", new Relation("≬") }, + // { @"\pitchfork", new Relation("⋔") }, // Glyph not in Latin Modern Math + { @"\varpropto", new Relation("∝") }, + { @"\blacktriangleleft", new Relation("◀") }, // ◂ not in Latin Modern Math + { @"\therefore", new Relation("∴") }, + // { @"\backepsilon", new Relation("϶") }, // Glyph not in Latin Modern Math + { @"\blacktriangleright", new Relation("▶") }, // ▸ not in Latin Modern Math + { @"\because", new Relation("∵") }, // Table 28: AMS Negated Binary Relations // U+0338, an overlapping slant, is used as a workaround when Unicode has no matching character - { "nless", new Relation("≮") }, - { "nleq", new Relation("≰") }, - { "nleqslant", new Relation("⩽\u0338") }, - { "nleqq", new Relation("≦\u0338") }, - { "lneq", new Relation("⪇") }, - { "lneqq", new Relation("≨") }, + { @"\nless", new Relation("≮") }, + { @"\nleq", new Relation("≰") }, + { @"\nleqslant", new Relation("⩽\u0338") }, + { @"\nleqq", new Relation("≦\u0338") }, + { @"\lneq", new Relation("⪇") }, + { @"\lneqq", new Relation("≨") }, // \lvertneqq -> ≨ + U+FE00 (Variation Selector 1) Not dealing with variation selectors, thank you very much - { "lnsim", new Relation("⋦") }, - { "lnapprox", new Relation("⪉") }, - { "nprec", new Relation("⊀") }, - { "npreceq", new Relation("⪯\u0338") }, - { "precnsim", new Relation("⋨") }, - // { "precnapprox", new Relation("⪹") }, // Glyph not in Latin Modern Math - { "nsim", new Relation("≁") }, - { "nshortmid", new Relation("∤") }, - { "nmid", new Relation("∤") }, - { "nvdash", new Relation("⊬") }, - { "nvDash", new Relation("⊭") }, - { "ntriangleleft", new Relation("⋪") }, - { "ntrianglelefteq", new Relation("⋬") }, - { "nsubseteq", new Relation("⊈") }, - { "subsetneq", new Relation("⊊") }, + { @"\lnsim", new Relation("⋦") }, + { @"\lnapprox", new Relation("⪉") }, + { @"\nprec", new Relation("⊀") }, + { @"\npreceq", new Relation("⪯\u0338") }, + { @"\precnsim", new Relation("⋨") }, + // { @"\precnapprox", new Relation("⪹") }, // Glyph not in Latin Modern Math + { @"\nsim", new Relation("≁") }, + { @"\nshortmid", new Relation("∤") }, + { @"\nmid", new Relation("∤") }, + { @"\nvdash", new Relation("⊬") }, + { @"\nvDash", new Relation("⊭") }, + { @"\ntriangleleft", new Relation("⋪") }, + { @"\ntrianglelefteq", new Relation("⋬") }, + { @"\nsubseteq", new Relation("⊈") }, + { @"\subsetneq", new Relation("⊊") }, // \varsubsetneq -> ⊊ + U+FE00 (Variation Selector 1) Not dealing with variation selectors, thank you very much - // { "subsetneqq", new Relation("⫋") }, // Glyph not in Latin Modern Math + // { @"\subsetneqq", new Relation("⫋") }, // Glyph not in Latin Modern Math // \varsubsetneqq -> ⫋ + U+FE00 (Variation Selector 1) Not dealing with variation selectors, thank you very much - { "ngtr", new Relation("≯") }, - { "ngeq", new Relation("≱") }, - { "ngeqslant", new Relation("⩾\u0338") }, - { "ngeqq", new Relation("≧\u0338") }, - { "gneq", new Relation("⪈") }, - { "gneqq", new Relation("≩") }, + { @"\ngtr", new Relation("≯") }, + { @"\ngeq", new Relation("≱") }, + { @"\ngeqslant", new Relation("⩾\u0338") }, + { @"\ngeqq", new Relation("≧\u0338") }, + { @"\gneq", new Relation("⪈") }, + { @"\gneqq", new Relation("≩") }, // \gvertneqq -> ≩ + U+FE00 (Variation Selector 1) Not dealing with variation selectors, thank you very much - { "gnsim", new Relation("⋧") }, - { "gnapprox", new Relation("⪊") }, - { "nsucc", new Relation("⊁") }, - { "nsucceq", new Relation("⪰\u0338") }, + { @"\gnsim", new Relation("⋧") }, + { @"\gnapprox", new Relation("⪊") }, + { @"\nsucc", new Relation("⊁") }, + { @"\nsucceq", new Relation("⪰\u0338") }, // Duplicate entry in LaTeX Symbol list: \nsucceq - { "succnsim", new Relation("⋩") }, - // { "succnapprox", new Relation("⪺") }, // Glyph not in Latin Modern Math - { "ncong", new Relation("≇") }, - { "nshortparallel", new Relation("∦") }, - { "nparallel", new Relation("∦") }, - { "nVdash", new Relation("⊮") }, // Error in LaTeX Symbol list: defined as \nvDash which duplicates above - { "nVDash", new Relation("⊯") }, - { "ntriangleright", new Relation("⋫") }, - { "ntrianglerighteq", new Relation("⋭") }, - { "nsupseteq", new Relation("⊉") }, - // { "nsupseteqq", new Relation("⫆\u0338") }, // Glyph not in Latin Modern Math - { "supsetneq", new Relation("⊋") }, + { @"\succnsim", new Relation("⋩") }, + // { @"\succnapprox", new Relation("⪺") }, // Glyph not in Latin Modern Math + { @"\ncong", new Relation("≇") }, + { @"\nshortparallel", new Relation("∦") }, + { @"\nparallel", new Relation("∦") }, + { @"\nVdash", new Relation("⊮") }, // Error in LaTeX Symbol list: defined as \nvDash which duplicates above + { @"\nVDash", new Relation("⊯") }, + { @"\ntriangleright", new Relation("⋫") }, + { @"\ntrianglerighteq", new Relation("⋭") }, + { @"\nsupseteq", new Relation("⊉") }, + // { @"\nsupseteqq", new Relation("⫆\u0338") }, // Glyph not in Latin Modern Math + { @"\supsetneq", new Relation("⊋") }, // \varsupsetneq -> ⊋ + U+FE00 (Variation Selector 1) Not dealing with variation selectors, thank you very much - // { "supsetneqq", new Relation("⫌") }, // Glyph not in Latin Modern Math + // { @"\supsetneqq", new Relation("⫌") }, // Glyph not in Latin Modern Math // \varsupsetneqq -> ⫌ + U+FE00 (Variation Selector 1) Not dealing with variation selectors, thank you very much }; } diff --git a/CSharpMath/Atom/MathList.cs b/CSharpMath/Atom/MathList.cs index 53af9617..a1f0e2f0 100644 --- a/CSharpMath/Atom/MathList.cs +++ b/CSharpMath/Atom/MathList.cs @@ -3,6 +3,7 @@ using System.Collections; namespace CSharpMath.Atom { + using Atoms; #pragma warning disable CA1710 // Identifiers should have correct suffix // WTF CA1710, you want types inheriting IList to have the Collection suffix? class DisabledMathList : MathList { @@ -16,6 +17,33 @@ public class MathList : IMathObject, IList, IReadOnlyList, I public MathList() => Atoms = new List(); public MathList(IEnumerable atoms) => Atoms = new List(atoms); public MathList(params MathAtom[] atoms) => Atoms = new List(atoms); + + /// The last that is not a , + /// or when is empty. + [System.Diagnostics.CodeAnalysis.DisallowNull] + public MathAtom? Last { + get { + for (int i = Atoms.Count - 1; i >= 0; i--) + switch (Atoms[i]) { + case Comment _: + continue; + case var atom: + return atom; + } + return null; + } + set { + for (int i = Atoms.Count - 1; i >= 0; i--) + switch (Atoms[i]) { + case Comment _: + continue; + default: + Atoms[i] = value; + return; + } + Atoms.Add(value); + } + } /// Just a deep copy if finalize is false; A finalized list if finalize is true public MathList Clone(bool finalize) { var newList = new MathList(); @@ -24,7 +52,13 @@ public MathList Clone(bool finalize) { newList.Add(atom.Clone(finalize)); } else { foreach (var atom in Atoms) { - var prevNode = newList.Count > 0 ? newList[newList.Count - 1] : null; + if (atom is Comment) { + var newComment = atom.Clone(finalize); + newComment.IndexRange = Range.NotFound; + newList.Add(newComment); + continue; + } + var prevNode = newList.Last; var newNode = atom.Clone(finalize); if (atom.IndexRange == Range.Zero) { int prevIndex = @@ -33,34 +67,34 @@ public MathList Clone(bool finalize) { } //TODO: One day when C# receives "or patterns", simplify this abomination switch (prevNode, newNode) { - case (null, Atoms.BinaryOperator b): + case (null, BinaryOperator b): newNode = b.ToUnaryOperator(); break; - case (Atoms.BinaryOperator _, Atoms.BinaryOperator b): + case (BinaryOperator _, BinaryOperator b): newNode = b.ToUnaryOperator(); break; - case (Atoms.Relation _, Atoms.BinaryOperator b): + case (Relation _, BinaryOperator b): newNode = b.ToUnaryOperator(); break; - case (Atoms.Open _, Atoms.BinaryOperator b): + case (Open _, BinaryOperator b): newNode = b.ToUnaryOperator(); break; - case (Atoms.Punctuation _, Atoms.BinaryOperator b): + case (Punctuation _, BinaryOperator b): newNode = b.ToUnaryOperator(); break; - case (Atoms.LargeOperator _, Atoms.BinaryOperator b): + case (LargeOperator _, BinaryOperator b): newNode = b.ToUnaryOperator(); break; - case (Atoms.BinaryOperator b, Atoms.Relation _): - newList[newList.Count - 1] = b.ToUnaryOperator(); + case (BinaryOperator b, Relation _): + newList.Last = b.ToUnaryOperator(); break; - case (Atoms.BinaryOperator b, Atoms.Punctuation _): - newList[newList.Count - 1] = b.ToUnaryOperator(); + case (BinaryOperator b, Punctuation _): + newList.Last = b.ToUnaryOperator(); break; - case (Atoms.BinaryOperator b, Atoms.Close _): - newList[newList.Count - 1] = b.ToUnaryOperator(); + case (BinaryOperator b, Close _): + newList.Last = b.ToUnaryOperator(); break; - case (Atoms.Number n, Atoms.Number _) when n.Superscript.IsEmpty() && n.Subscript.IsEmpty(): + case (Number n, Number _) when n.Superscript.IsEmpty() && n.Subscript.IsEmpty(): n.Fuse(newNode); continue; // do not add the new node; we fused it instead. } diff --git a/CSharpMath/Display/Typesetter.cs b/CSharpMath/Display/Typesetter.cs index d21b387a..a222d170 100644 --- a/CSharpMath/Display/Typesetter.cs +++ b/CSharpMath/Display/Typesetter.cs @@ -133,6 +133,7 @@ List _PreprocessMathList() { MathAtom? prevAtom = null; var r = new List(); foreach (var atom in list.Atoms) { + if (atom is Comment) continue; // These are not a TeX type nodes. TeX does this during parsing the input. // switch to using the font specified in the atom and convert it to ordinary var newAtom = atom switch @@ -167,6 +168,7 @@ private void CreateDisplayAtoms(List preprocessedAtoms) { case Number _: case Variable _: case UnaryOperator _: + case Comment _: throw new InvalidCodePathException ($"Type {atom.TypeName} should have been removed by preprocessing"); case Space space: @@ -658,13 +660,13 @@ float _FractionDelimiterHeight() => // Add delimiters to fraction display - if (fraction.LeftDelimiter is null && fraction.RightDelimiter is null) + if (fraction.LeftDelimiter == Boundary.Empty && fraction.RightDelimiter == Boundary.Empty) return display; var glyphHeight = _FractionDelimiterHeight(); var position = new PointF(); var innerGlyphs = new List>(); - if (fraction.LeftDelimiter?.Length > 0) { - var leftGlyph = FindGlyphForBoundary(fraction.LeftDelimiter, glyphHeight); + if (fraction.LeftDelimiter.Nucleus?.Length > 0) { + var leftGlyph = FindGlyphForBoundary(fraction.LeftDelimiter.Nucleus, glyphHeight); leftGlyph.Position = position; innerGlyphs.Add(leftGlyph); position.X += leftGlyph.Width; @@ -672,8 +674,8 @@ float _FractionDelimiterHeight() => display.Position = position; position.X += display.Width; innerGlyphs.Add(display); - if (fraction.RightDelimiter?.Length > 0) { - var rightGlyph = FindGlyphForBoundary(fraction.RightDelimiter, glyphHeight); + if (fraction.RightDelimiter.Nucleus?.Length > 0) { + var rightGlyph = FindGlyphForBoundary(fraction.RightDelimiter.Nucleus, glyphHeight); rightGlyph.Position = position; innerGlyphs.Add(rightGlyph); position.X += rightGlyph.Width; @@ -699,12 +701,12 @@ private InnerDisplay MakeInner(Inner inner, Range range) { float glyphHeight = Math.Max(d1, d2); var leftGlyph = - inner.LeftBoundary is Boundary { Nucleus: var left } && left.Length > 0 + inner.LeftBoundary is Boundary { Nucleus: var left } && left?.Length > 0 ? FindGlyphForBoundary(left, glyphHeight) : null; var rightGlyph = - inner.RightBoundary is Boundary { Nucleus: var right } && right.Length > 0 + inner.RightBoundary is Boundary { Nucleus: var right } && right?.Length > 0 ? FindGlyphForBoundary(right, glyphHeight) : null; return new InnerDisplay(innerListDisplay, leftGlyph, rightGlyph, range); diff --git a/CSharpMath/Structures/BiDictionary.cs b/CSharpMath/Structures/BiDictionary.cs deleted file mode 100644 index d7156cbf..00000000 --- a/CSharpMath/Structures/BiDictionary.cs +++ /dev/null @@ -1,304 +0,0 @@ -using System; -using System.Buffers; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; - -namespace CSharpMath.Structures { - public class AliasDictionary : IDictionary, IReadOnlyDictionary { - readonly Dictionary k2v = new Dictionary(); - readonly Dictionary v2k = new Dictionary(); - public TValue this[TKey key] { get => k2v[key]; set { k2v[key] = value; v2k[value] = key; } } - public TKey this[TValue val] { get => v2k[val]; set { v2k[val] = value; k2v[value] = val; } } - public int Count => k2v.Count; - public bool IsReadOnly => false; - public Dictionary.KeyCollection Keys => k2v.Keys; - public Dictionary.KeyCollection Values => v2k.Keys; - #region AliasDictionary.Add - public void Add(ReadOnlySpan keys, TValue value) { - if (!v2k.ContainsKey(value) && !keys.IsEmpty) v2k.Add(value, keys[0]); - foreach (var key in keys) - k2v.Add(key, value); - } - //Array renting may result in larger arrays than normal -> the unused slots are nulls. - //Therefore, slicing prevents nulls from propagating through. - public void Add(TKey mainKey, TValue value) { - var array = ArrayPool.Shared.Rent(1); - array[0] = mainKey; - Add(new ReadOnlySpan(array, 0, 1), value); - ArrayPool.Shared.Return(array); - } - public void Add(TKey mainKey, TKey aliasKey, TValue value) { - var array = ArrayPool.Shared.Rent(2); - array[0] = mainKey; - array[1] = aliasKey; - Add(new ReadOnlySpan(array, 0, 2), value); - ArrayPool.Shared.Return(array); - } - public void Add(TKey mainKey, TKey aliasKey1, TKey aliasKey2, TValue value) { - var array = ArrayPool.Shared.Rent(3); - array[0] = mainKey; - array[1] = aliasKey1; - array[2] = aliasKey2; - Add(new ReadOnlySpan(array, 0, 3), value); - ArrayPool.Shared.Return(array); - } - public void Add(TKey mainKey, TKey aliasKey1, TKey aliasKey2, TKey aliasKey3, TValue value) { - var array = ArrayPool.Shared.Rent(4); - array[0] = mainKey; - array[1] = aliasKey1; - array[2] = aliasKey2; - array[3] = aliasKey3; - Add(new ReadOnlySpan(array, 0, 4), value); - ArrayPool.Shared.Return(array); - } - public void Add(TKey mainKey, TKey aliasKey1, TKey aliasKey2, TKey aliasKey3, TKey aliasKey4, TValue value) { - var array = ArrayPool.Shared.Rent(5); - array[0] = mainKey; - array[1] = aliasKey1; - array[2] = aliasKey2; - array[3] = aliasKey3; - array[4] = aliasKey4; - Add(new ReadOnlySpan(array, 0, 5), value); - ArrayPool.Shared.Return(array); - } - public void Add(TKey mainKey, TKey aliasKey1, TKey aliasKey2, TKey aliasKey3, TKey aliasKey4, - TKey aliasKey5, TValue value) { - var array = ArrayPool.Shared.Rent(6); - array[0] = mainKey; - array[1] = aliasKey1; - array[2] = aliasKey2; - array[3] = aliasKey3; - array[4] = aliasKey4; - array[5] = aliasKey5; - Add(new ReadOnlySpan(array, 0, 6), value); - ArrayPool.Shared.Return(array); - } - public void Add(TKey mainKey, TKey aliasKey1, TKey aliasKey2, TKey aliasKey3, TKey aliasKey4, - TKey aliasKey5, TKey aliasKey6, TValue value) { - var array = ArrayPool.Shared.Rent(7); - array[0] = mainKey; - array[1] = aliasKey1; - array[2] = aliasKey2; - array[3] = aliasKey3; - array[4] = aliasKey4; - array[5] = aliasKey5; - array[6] = aliasKey6; - Add(new ReadOnlySpan(array, 0, 7), value); - ArrayPool.Shared.Return(array); - } - public void Add(TKey mainKey, TKey aliasKey1, TKey aliasKey2, TKey aliasKey3, TKey aliasKey4, - TKey aliasKey5, TKey aliasKey6, TKey aliasKey7, TValue value) { - var array = ArrayPool.Shared.Rent(8); - array[0] = mainKey; - array[1] = aliasKey1; - array[2] = aliasKey2; - array[3] = aliasKey3; - array[4] = aliasKey4; - array[5] = aliasKey5; - array[6] = aliasKey6; - array[7] = aliasKey7; - Add(new ReadOnlySpan(array, 0, 8), value); - ArrayPool.Shared.Return(array); - } - public void Add(TKey mainKey, TKey aliasKey1, TKey aliasKey2, TKey aliasKey3, TKey aliasKey4, - TKey aliasKey5, TKey aliasKey6, TKey aliasKey7, TKey aliasKey8, TValue value) { - var array = ArrayPool.Shared.Rent(9); - array[0] = mainKey; - array[1] = aliasKey1; - array[2] = aliasKey2; - array[3] = aliasKey3; - array[4] = aliasKey4; - array[5] = aliasKey5; - array[6] = aliasKey6; - array[7] = aliasKey7; - array[8] = aliasKey8; - Add(new ReadOnlySpan(array, 0, 9), value); - ArrayPool.Shared.Return(array); - } - #endregion AliasDictionary.Add - public void Clear() { - k2v.Clear(); - v2k.Clear(); - } - public bool Contains(KeyValuePair pair) => - k2v.TryGetValue(pair.Key, out var value) && EqualityComparer.Default.Equals(value, pair.Value); - public bool ContainsKey(TKey key) => k2v.ContainsKey(key); - public bool ContainsValue(TValue value) => v2k.ContainsKey(value); - public void CopyTo(KeyValuePair[] array, int arrayIndex) => - ((ICollection>)k2v).CopyTo(array, arrayIndex); - public Dictionary.Enumerator GetEnumerator() => k2v.GetEnumerator(); - public bool Remove(TValue value) { - if (!v2k.Remove(value)) return false; - foreach (var pair in k2v.Where(p => EqualityComparer.Default.Equals(p.Value, value))) - k2v.Remove(pair.Key); - return true; - } - public bool Remove(TKey key) => Remove(key, k2v[key]); - public bool Remove(KeyValuePair pair) => Remove(pair.Key, pair.Value); - public bool Remove(TKey key, TValue value) { - var valueMatches = k2v.Where(p => EqualityComparer.Default.Equals(p.Value, value)).ToList(); - switch (valueMatches.Count) { - case 0: - return false; - case 1: - if (!EqualityComparer.Default.Equals(valueMatches[0].Key, key)) return false; - k2v.Remove(key); - v2k.Remove(value); - return true; - case var _: - if (!valueMatches.Any(p => EqualityComparer.Default.Equals(p.Key, key))) return false; - k2v.Remove(key); - if (EqualityComparer.Default.Equals(v2k[value], key)) - v2k[value] = valueMatches.First(p => !EqualityComparer.Default.Equals(p.Key, key)).Key; - return true; - } - } - public bool TryGetKey(TValue value, out TKey key) => v2k.TryGetValue(value, out key); - public bool TryGetValue(TKey key, out TValue value) => k2v.TryGetValue(key, out value); - void ICollection>.Add(KeyValuePair item) => - Add(item.Key, item.Value); - IEnumerator IEnumerable.GetEnumerator() => k2v.GetEnumerator(); - IEnumerator> IEnumerable>.GetEnumerator() => - k2v.GetEnumerator(); - ICollection IDictionary.Keys => k2v.Keys; - IEnumerable IReadOnlyDictionary.Keys => k2v.Keys; - ICollection IDictionary.Values => v2k.Keys; - IEnumerable IReadOnlyDictionary.Values => v2k.Keys; - } - - //https://stackoverflow.com/questions/255341/getting-key-of-value-of-a-generic-dictionary/255638#255638 - public class BiDictionary - : IDictionary, IReadOnlyDictionary { - readonly Dictionary firstToSecond = new Dictionary(); - readonly Dictionary secondToFirst = new Dictionary(); - public TSecond this[TFirst first] { - get => firstToSecond[first]; - set => AddOrReplace(first, value); - } - public TFirst this[TSecond second] { - get => secondToFirst[second]; - set => AddOrReplace(value, second); - } - public int Count => firstToSecond.Count; - public Dictionary.KeyCollection Firsts => firstToSecond.Keys; - public bool IsReadOnly => false; - public Dictionary.KeyCollection Seconds => secondToFirst.Keys; - public void Add(TFirst first, TSecond second) { - // Call the Add() that will throw first - if (firstToSecond.ContainsKey(first)) - firstToSecond.Add(first, second); - else if (secondToFirst.ContainsKey(second)) - secondToFirst.Add(second, first); - - firstToSecond.Add(first, second); - secondToFirst.Add(second, first); - } - public void Add(KeyValuePair item) => Add(item.Key, item.Value); - public void AddOrReplace(TFirst first, TSecond second) { - if (firstToSecond.ContainsKey(first)) - RemoveByFirst(first); - if (secondToFirst.ContainsKey(second)) - RemoveBySecond(second); - firstToSecond.Add(first, second); - secondToFirst.Add(second, first); - } - public void AddOrReplace(KeyValuePair item) => AddOrReplace(item.Key, item.Value); - public void Clear() { - firstToSecond.Clear(); - secondToFirst.Clear(); - } - public bool ContainsByFirst(TFirst first) => firstToSecond.ContainsKey(first); - public bool ContainsBySecond(TSecond second) => secondToFirst.ContainsKey(second); - public bool Contains(KeyValuePair pair) => - firstToSecond.TryGetValue(pair.Key, out var second) - && EqualityComparer.Default.Equals(second, pair.Value); - public void CopyTo(KeyValuePair[] array, int arrayIndex) { - if (array is null) throw new ArgumentNullException(nameof(array)); - foreach (var pair in firstToSecond) - array[arrayIndex++] = pair; - } - public Dictionary.Enumerator GetEnumerator() => - firstToSecond.GetEnumerator(); - public bool Remove(TFirst first, TSecond second) { - if (TryGetByFirst(first, out var svalue) && TryGetBySecond(second, out var fvalue)) { - firstToSecond.Remove(first); - firstToSecond.Remove(fvalue); - secondToFirst.Remove(second); - secondToFirst.Remove(svalue); - return true; - } - return false; - } - public bool Remove(KeyValuePair pair) => Remove(pair.Key, pair.Value); - public bool RemoveByFirst(TFirst first) => Remove(first, firstToSecond[first]); - public bool RemoveBySecond(TSecond second) => Remove(secondToFirst[second], second); - public bool TryGetByFirst(TFirst first, out TSecond second) => - firstToSecond.TryGetValue(first, out second); - public bool TryGetBySecond(TSecond second, out TFirst first) => - secondToFirst.TryGetValue(second, out first); -#pragma warning disable CA1033 // Interface methods should be callable by child types - bool IDictionary.ContainsKey(TFirst first) => firstToSecond.ContainsKey(first); - bool IReadOnlyDictionary.ContainsKey(TFirst first) => firstToSecond.ContainsKey(first); - IEnumerator IEnumerable.GetEnumerator() => firstToSecond.GetEnumerator(); - IEnumerator> IEnumerable>.GetEnumerator() => - firstToSecond.GetEnumerator(); - ICollection IDictionary.Keys => Firsts; - IEnumerable IReadOnlyDictionary.Keys => Firsts; - bool IDictionary.Remove(TFirst first) => Remove(first, firstToSecond[first]); - bool IDictionary.TryGetValue(TFirst first, out TSecond second) => - firstToSecond.TryGetValue(first, out second); - bool IReadOnlyDictionary.TryGetValue(TFirst first, out TSecond second) => - firstToSecond.TryGetValue(first, out second); - ICollection IDictionary.Values => Seconds; - IEnumerable IReadOnlyDictionary.Values => Seconds; -#pragma warning restore CA1033 // Interface methods should be callable by child types - } -#pragma warning disable CA1710 // Identifiers should have correct suffix - public class MultiDictionary : IEnumerable> { -#pragma warning restore CA1710 // Identifiers should have correct suffix - readonly Dictionary> firstToSecond = new Dictionary>(); - readonly Dictionary> secondToFirst = new Dictionary>(); - private static readonly ReadOnlyCollection EmptyFirstList = - new ReadOnlyCollection(Array.Empty()); - private static readonly ReadOnlyCollection EmptySecondList = - new ReadOnlyCollection(Array.Empty()); - public ReadOnlyCollection this[TFirst first] => - firstToSecond.TryGetValue(first, out var list) ? new ReadOnlyCollection(list) : EmptySecondList; - public ReadOnlyCollection this[TSecond second] => - secondToFirst.TryGetValue(second, out var list) ? new ReadOnlyCollection(list) : EmptyFirstList; - public void Add(TFirst first, TSecond second) { - if (!firstToSecond.TryGetValue(first, out var seconds)) { - seconds = new List(); - firstToSecond[first] = seconds; - } - if (!secondToFirst.TryGetValue(second, out var firsts)) { - firsts = new List(); - secondToFirst[second] = firsts; - } - seconds.Add(second); - firsts.Add(first); - } - public bool TryGetByFirst(TFirst first, out TSecond second) { - if (firstToSecond.TryGetValue(first, out var list) && list.Count > 0) { - second = list[0]; - return true; - } - second = default!; - return false; - } - public bool TryGetBySecond(TSecond second, out TFirst first) { - if (secondToFirst.TryGetValue(second, out var list) && list.Count > 0) { - first = list[0]; - return true; - } - first = default!; - return false; - } - public IEnumerator> GetEnumerator() => - firstToSecond.SelectMany(p => p.Value.Select(v => new KeyValuePair(p.Key, v))) - .GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} \ No newline at end of file diff --git a/CSharpMath/Structures/Dictionary.cs b/CSharpMath/Structures/Dictionary.cs new file mode 100644 index 00000000..b545640f --- /dev/null +++ b/CSharpMath/Structures/Dictionary.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace CSharpMath.Structures { + /// + /// Funnels collection initializers to . + /// Implements but throws upon enumeration because it is there only to enable collection initializers. + /// + /// + /// new ProxyAdder<int, string>((key, value) => Console.WriteLine(value)) + /// { { 1, 2, 3, "1 to 3" }, { Enumerable.Range(7, 10), i => i.ToString() } } + /// + [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = NotACollection)] + [SuppressMessage("Design", "CA1010:Collections should implement generic interface", Justification = NotACollection)] + public class ProxyAdder : IEnumerable { + const string NotACollection = "This is not a collection. It implements IEnumerable just to support collection initializers."; + [Obsolete(NotACollection, true)] + [SuppressMessage("Design", "CA1033:Interface methods should be callable by child types", Justification = NotACollection)] + IEnumerator IEnumerable.GetEnumerator() => throw new NotSupportedException(NotACollection); + public ProxyAdder(Action? extraCommandToPerformWhenAdding = null) => Added += extraCommandToPerformWhenAdding; + public event Action? Added; + public void Add(TKey key1, TValue value) => Added?.Invoke(key1, value); + public void Add(TKey key1, TKey key2, TValue value) { + Add(key1, value); Add(key2, value); + } + public void Add(TKey key1, TKey key2, TKey key3, TValue value) { + Add(key1, value); Add(key2, value); Add(key3, value); + } + public void Add(TKey key1, TKey key2, TKey key3, TKey key4, TValue value) { + Add(key1, value); Add(key2, value); Add(key3, value); Add(key4, value); + } + public void Add(TCollection keys, TValue value) where TCollection : IEnumerable { + foreach (var key in keys) Add(key, value); + } + public void Add(TCollection keys, Func valueFunc) where TCollection : IEnumerable { + foreach (var key in keys) Add(key, valueFunc(key)); + } + } + /// Ensures that longer s with same beginnings are listed first, to be matched first. + class DescendingStringComparer : IComparer<(string NonCommand, TValue Value)> { + public int Compare((string NonCommand, TValue Value) x, (string NonCommand, TValue Value) y) => + string.CompareOrdinal(y.NonCommand, x.NonCommand); + } + + /// + /// A dictionary-based helper where the keys are classes of LaTeX s, with special treatment + /// for commands (starting with "\"). The start of an inputted with type argument + /// is parsed, and an arbitrary object is returned, + /// along with the number of matching characters. Processing is based on dictionary lookup with fallback + /// to specified default functions for command and non-commands when lookup fails. + /// For non-commands, dictionary lookup finds the longest matching non-command. + /// + [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", + Justification = "This is conceptually a dictionary but has different lookup behavior")] + public class LaTeXCommandDictionary : ProxyAdder, IEnumerable> { + public delegate Result<(TValue Result, int SplitIndex)> DefaultDelegate(ReadOnlySpan consume); + + public LaTeXCommandDictionary(DefaultDelegate defaultParser, + DefaultDelegate defaultParserForCommands, Action? extraCommandToPerformWhenAdding = null) : base(extraCommandToPerformWhenAdding) { + this.defaultParser = defaultParser; + this.defaultParserForCommands = defaultParserForCommands; + Added += (key, value) => { + if (key.AsSpan().StartsWithInvariant(@"\")) + if (SplitCommand(key.AsSpan()) != key.Length - 1) + commands.Add(key, value); + else throw new ArgumentException("Key is unreachable: " + key, nameof(key)); + else nonCommands.Add((key, value)); + }; + } + readonly DefaultDelegate defaultParser; + readonly DefaultDelegate defaultParserForCommands; + + readonly SortedSet<(string NonCommand, TValue Value)> nonCommands = + new SortedSet<(string NonCommand, TValue Value)>(new DescendingStringComparer()); + readonly Dictionary commands = new Dictionary(); + + public IEnumerator> GetEnumerator() => + nonCommands.Select(t => new KeyValuePair(t.NonCommand, t.Value)) + .Concat(commands).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// Finds the number of characters corresponding to a LaTeX command at the beginning of s. + static int SplitCommand(ReadOnlySpan chars) { + // Note on '@': https://stackoverflow.com/questions/29217603/extracting-all-latex-commands-from-a-latex-code-file#comment47075515_29218404 + static bool IsEnglishAlphabetOrAt(char c) => 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || c == '@'; + + System.Diagnostics.Debug.Assert(chars[0] == '\\'); + var splitIndex = 1; + if (splitIndex < chars.Length) + if (IsEnglishAlphabetOrAt(chars[splitIndex])) { + do splitIndex++; while (splitIndex < chars.Length && IsEnglishAlphabetOrAt(chars[splitIndex])); + if (splitIndex < chars.Length) + switch (chars[splitIndex]) { + case '*': + case '=': + case '\'': + splitIndex++; + break; + } + } else splitIndex++; + return splitIndex; + } + /// Tries to find a command at the beginning of s, returning the + /// corresponding to the command Key, and the length of the command. + public Result<(TValue Result, int SplitIndex)> TryLookup(ReadOnlySpan chars) { + Result<(TValue Result, int SplitIndex)> TryLookupCommand(ReadOnlySpan chars) { + var splitIndex = SplitCommand(chars); + var lookup = chars.Slice(0, splitIndex); + while (splitIndex < chars.Length && char.IsWhiteSpace(chars[splitIndex])) + splitIndex++; + return commands.TryGetValue(lookup.ToString(), out var result) + ? Result.Ok((result, splitIndex)) + : defaultParserForCommands(lookup); + } + Result<(TValue Result, int SplitIndex)> TryLookupNonCommand(ReadOnlySpan chars) { + foreach (var (nonCommand, value) in nonCommands) { + if (chars.StartsWith(nonCommand.AsSpan(), StringComparison.Ordinal)) { + return Result.Ok((value, nonCommand.Length)); + } + } + return defaultParser(chars); + } + + if (chars.IsEmpty) throw new ArgumentException("There are no characters to read.", nameof(chars)); + return chars.StartsWithInvariant(@"\") ? TryLookupCommand(chars) : TryLookupNonCommand(chars); + } + } + + // Taken from https://stackoverflow.com/questions/255341/getting-key-of-value-of-a-generic-dictionary/255638#255638 + /// + /// Represents a many to one relationship between s and s, + /// allowing fast lookup of the first corresponding to any , + /// in addition to the usual lookup of a s by a . + /// + [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = IDictionaryNoLongerImplemented)] + [SuppressMessage("Design", "CA1010:Collections should implement generic interface", Justification = IDictionaryNoLongerImplemented)] + public class AliasBiDictionary : ProxyAdder where TFirst : notnull where TSecond : notnull { + const string IDictionaryNoLongerImplemented = "This is two dictionaries in one so a single IReadOnlyDictionary interface isn't appropriate. Instead both are provided."; + public AliasBiDictionary(Action? extraCommandToPerformWhenAdding = null) : base(extraCommandToPerformWhenAdding) => + Added += (first, second) => { + switch (firstToSecond.ContainsKey(first), secondToFirst.ContainsKey(second)) { + case (true, _): + // There cannot be multiple TSeconds linked to the same TFirst + throw new Exception($"Key already exists in {nameof(AliasBiDictionary)}."); + case (false, true): + firstToSecond.Add(first, second); + break; + case (false, false): + firstToSecond.Add(first, second); + secondToFirst.Add(second, first); + break; + } + }; + + readonly Dictionary firstToSecond = new Dictionary(); + readonly Dictionary secondToFirst = new Dictionary(); + public Dictionary.Enumerator GetEnumerator() => + firstToSecond.GetEnumerator(); + public bool RemoveByFirst(TFirst first) { + bool exists = firstToSecond.TryGetValue(first, out var svalue); + if (exists) { + firstToSecond.Remove(first); + // if first is currently mapped to from svalue, + // then try to reconnect svalue to another TFirst mapping to it; + // otherwise delete the svalue record in secondToFirst + if (secondToFirst[svalue].Equals(first)) { + TFirst[] otherFirsts = + firstToSecond + .Where(kvp => EqualityComparer.Default.Equals(kvp.Value,svalue)) + .Select(kvp => kvp.Key).ToArray(); + if (otherFirsts.IsEmpty()) + secondToFirst.Remove(svalue); + else secondToFirst[svalue] = otherFirsts[0]; + } + } + return exists; + } + public bool RemoveBySecond(TSecond second) { + bool exists = secondToFirst.TryGetValue(second, out var _); + if (exists) { + secondToFirst.Remove(second); + // Remove all TFirsts pointing to second + var firsts = + firstToSecond + .Where(kvp => EqualityComparer.Default.Equals(kvp.Value,second)) + .Select(kvp => kvp.Key).ToArray(); + foreach (var first in firsts) + firstToSecond.Remove(first); + } + return exists; + } + public IReadOnlyDictionary FirstToSecond => firstToSecond; + public IReadOnlyDictionary SecondToFirst => secondToFirst; + } +} diff --git a/CSharpMath/Structures/Result.cs b/CSharpMath/Structures/Result.cs index cd6db7cf..875a0282 100644 --- a/CSharpMath/Structures/Result.cs +++ b/CSharpMath/Structures/Result.cs @@ -1,40 +1,36 @@ using System; +using CSharpMath.Structures; #pragma warning disable CA1815 // Override equals and operator equals on value types // Justification for CA1815: Results are not meant to be equated #pragma warning disable CA2225 // Operator overloads have named alternates // Justification for CA2225: Use the constructors instead + namespace CSharpMath.Structures { //For Result where both implicit conversions fight over each other, //use Err(string) there instead public readonly struct ResultImplicitError { public string Error { get; } - public ResultImplicitError(string error) => - Error = error ?? throw new ArgumentNullException(nameof(error)); + public ResultImplicitError(string error) => Error = error ?? throw new ArgumentNullException(nameof(error)); } public readonly struct Result { public static Result Ok() => new Result(); public static Result Ok(T value) => new Result(value); public static SpanResult Ok(ReadOnlySpan value) => new SpanResult(value); public static ResultImplicitError Err(string error) => new ResultImplicitError(error); - public Result(string error) => - Error = error ?? throw new ArgumentNullException(nameof(error)); + public Result(string error) => Error = error ?? throw new ArgumentNullException(nameof(error)); public string? Error { get; } public void Match(Action successAction, Action errorAction) { if (Error != null) errorAction(Error); else successAction(); } - public TResult Match(Func successFunc, Func errorFunc) { - if (Error != null) return errorFunc(Error); else return successFunc(); - } - public Result Bind(Func successAction) { - if (Error != null) return Error; else return successAction(); - } - public Result Bind(Func successAction) { - if (Error != null) return Error; else return successAction(); - } - public Result Bind(Func> successAction) { - if (Error != null) return Error; else return successAction(); + public TResult Match(Func successFunc, Func errorFunc) => + Error != null ? errorFunc(Error) : successFunc(); + public Result Bind(Action successAction) { + if (Error != null) return Error; else { successAction(); return Ok(); } } + public Result Bind(Func successAction) => Error ?? (Result)successAction(); + public Result Bind(Func successAction) => Error ?? successAction(); + public Result Bind(Func> successAction) => Error ?? successAction(); public static implicit operator Result(string error) => new Result(error); public static implicit operator Result(ResultImplicitError error) => new Result(error.Error); } @@ -47,14 +43,10 @@ public Result(string error) => public void Deconstruct(out T value, out string? error) => (value, error) = (_value, Error); public void Match(Action successAction, Action errorAction) { - if (successAction is null) throw new ArgumentNullException(nameof(successAction)); - if (errorAction is null) throw new ArgumentNullException(nameof(errorAction)); if (Error != null) errorAction(Error); else successAction(_value); } public TResult Match(Func successFunc, Func errorFunc) => - successFunc is null ? throw new ArgumentNullException(nameof(successFunc)) - : errorFunc is null ? throw new ArgumentNullException(nameof(errorFunc)) - : Error != null ? errorFunc(Error) : successFunc(_value); + Error != null ? errorFunc(Error) : successFunc(_value); public static implicit operator Result(T value) => new Result(value); public static implicit operator Result(string error) => @@ -62,26 +54,13 @@ public static implicit operator Result(string error) => public static implicit operator Result(ResultImplicitError error) => new Result(error.Error); public Result Bind(Action method) { - if (method is null) throw new ArgumentNullException(nameof(method)); if (Error is string error) return error; else method(_value); return Result.Ok(); } - public Result Bind(Func method) { - if (method is null) throw new ArgumentNullException(nameof(method)); - if (Error is string error) return error; - else return method(_value); - } - public Result Bind(Func method) { - if (method is null) throw new ArgumentNullException(nameof(method)); - if (Error is string error) return error; - else return method(_value); - } - public Result Bind(Func> method) { - if (method is null) throw new ArgumentNullException(nameof(method)); - if (Error is string error) return error; - else return method(_value); - } + public Result Bind(Func method) => Error ?? method(_value); + public Result Bind(Func method) => Error ?? (Result)method(_value); + public Result Bind(Func> method) => Error ?? method(_value); } public readonly ref struct SpanResult { public SpanResult(ReadOnlySpan value) { @@ -99,14 +78,10 @@ public void Deconstruct(out ReadOnlySpan value, out string? error) { error = Error; } public void Match(Action successAction, System.Action errorAction) { - if (successAction is null) throw new ArgumentNullException(nameof(successAction)); - if (errorAction is null) throw new ArgumentNullException(nameof(errorAction)); if (Error != null) errorAction(Error); else successAction(_value); } public TResult Match(Func successAction, System.Func errorAction) => - successAction is null ? throw new ArgumentNullException(nameof(successAction)) - : errorAction is null ? throw new ArgumentNullException(nameof(errorAction)) - : Error != null ? errorAction(Error) : successAction(_value); + Error != null ? errorAction(Error) : successAction(_value); public static implicit operator SpanResult(ReadOnlySpan value) => new SpanResult(value); public static implicit operator SpanResult(string error) => @@ -118,52 +93,12 @@ public static implicit operator SpanResult(ResultImplicitError error) => public delegate TResult Func(ReadOnlySpan result); public delegate TResult Func(ReadOnlySpan thisResult, TOther otherResult); public Result Bind(Action method) { - if (method is null) throw new ArgumentNullException(nameof(method)); if (Error is string error) return error; else method(_value); return Result.Ok(); } - public Result Bind(Func method) { - if (method is null) throw new ArgumentNullException(nameof(method)); - if (Error is string error) return error; - else return method(_value); - } - public Result Bind(Func method) { - if (method is null) throw new ArgumentNullException(nameof(method)); - if (Error is string error) return error; - else return method(_value); - } - public Result Bind(Func> method) { - if (method is null) throw new ArgumentNullException(nameof(method)); - if (Error is string error) return error; - else return method(_value); - } - public Result Bind(Result other, Action method) { - if (method is null) throw new ArgumentNullException(nameof(method)); - if (Error is string error) return error; - else if (other.Error is string otherError) return otherError; - else method(_value, other._value); - return Result.Ok(); - } - public Result Bind(Result other, Func method) { - if (method is null) throw new ArgumentNullException(nameof(method)); - if (Error is string error) return error; - else if (other.Error is string otherError) return otherError; - else return method(_value, other._value); - } - public Result Bind - (Result other, Func method) { - if (method is null) throw new ArgumentNullException(nameof(method)); - if (Error is string error) return error; - else if (other.Error is string otherError) return otherError; - else return method(_value, other._value); - } - public Result Bind - (Result other, Func> method) { - if (method is null) throw new ArgumentNullException(nameof(method)); - if (Error is string error) return error; - else if (other.Error is string otherError) return otherError; - else return method(_value, other._value); - } + public Result Bind(Func method) => Error ?? method(_value); + public Result Bind(Func method) => Error ?? (Result)method(_value); + public Result Bind(Func> method) => Error ?? method(_value); } } \ No newline at end of file diff --git a/CSharpMath/Structures/Space.cs b/CSharpMath/Structures/Space.cs index f5b2eab2..5c1c8aac 100644 --- a/CSharpMath/Structures/Space.cs +++ b/CSharpMath/Structures/Space.cs @@ -33,11 +33,11 @@ var _ when PredefinedLengthUnits.TryGetValue(unit, out var space) => space * val ? "Only the length unit mu is allowed in math mode" : (Result)(MathUnit * value); private static bool UnifyIsMu(Space left, Space right) => - left.IsMu && right.IsMu ? true - : left.IsMu || right.IsMu - ? throw new ArgumentException("The IsMu property of two Spaces must not differ " + - "in order to perform addition or subtraction on them.") - : false; + left.IsMu && right.IsMu + || (left.IsMu || right.IsMu + ? throw new ArgumentException("The IsMu property of two Spaces must not differ " + + "in order to perform addition or subtraction on them.") + : false); public override bool Equals(object obj) => obj is Space s && this == s; public bool EqualsSpace(Space otherSpace) => this == otherSpace; bool IEquatable.Equals(Space other) => EqualsSpace(other); diff --git a/Directory.Build.props b/Directory.Build.props index e105cdee..2e4d3f92 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,6 +8,7 @@ CA1062, CA1303, + nullable $(MSBuildProjectName)