diff --git a/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs b/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs index 30e2cb41..bb6a4b32 100644 --- a/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs +++ b/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs @@ -1300,6 +1300,35 @@ public void TestOperatorName(string operatorname, string output) { Assert.Equal(output, LaTeXParser.MathListToLaTeX(list).ToString()); } + /// + /// A letter in an operator name may be written as a command. AngouriMath emits + /// \operatorname{\varphi} for Euler's totient, which is the reason this exists. + /// + [Theory] + [InlineData(@"\varphi", "φ")] + [InlineData(@"\Gamma", "Γ")] + [InlineData(@"ma\chi ", "maχ")] + public void TestOperatorNameWithCommands(string operatorname, string name) { + var list = ParseLaTeX(@$"\operatorname{{{operatorname}}}"); + Assert.Collection(list, CheckAtom(name)); + var output = LaTeXParser.MathListToLaTeX(list).ToString(); + Assert.Equal(@$"\operatorname{{{name}}} ", output); + // The name comes back out as the letter rather than as the command, so reading that is the + // round trip that matters -- and it is why the name is read with char.IsLetter. + Assert.Collection(ParseLaTeX(output), CheckAtom(name)); + } + + /// \bmod is a binary operator whose nucleus is the word "mod". + [Fact] + public void TestModulo() { + var list = ParseLaTeX(@"x\bmod y"); + Assert.Collection(list, + CheckAtom("x"), + CheckAtom("mod"), + CheckAtom("y")); + Assert.Equal(@"x\bmod y", LaTeXParser.MathListToLaTeX(list).ToString()); + } + [Theory] [InlineData(@"\TeX")] [InlineData(@"\left.\mathrm{T\! \raisebox{-4.5mu}{E}\mkern-2.25muX}\right.")] @@ -1561,6 +1590,9 @@ public void TestHelpfulErrorMessage(string input, int index, string expected) { InlineData(@"\operatorname {a|}", @"Error: Expected } \operatorname {a|} ↑ (pos 16)"), + InlineData(@"\operatorname{\pm}", @"Error: Invalid command \pm in an operator name +\operatorname{\pm} + ↑ (pos 17)"), ] public void TestErrors(string badInput, string expected) { var (list, actual) = LaTeXParser.MathListFromLaTeX(badInput); diff --git a/CSharpMath.Evaluation.Tests/AngouriMathLatexSweepTests.cs b/CSharpMath.Evaluation.Tests/AngouriMathLatexSweepTests.cs new file mode 100644 index 00000000..a4ead6f8 --- /dev/null +++ b/CSharpMath.Evaluation.Tests/AngouriMathLatexSweepTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace CSharpMath.EvaluationTests { + using Atom; + + /// + /// throws InvalidCodePathException on any LaTeX it + /// cannot read, and says why in its own source: "CSharpMath must handle all LaTeX coming from + /// AngouriMath or a bug is present!". Nothing checked that, on either side — AngouriMath's own + /// Docs/Usage/Syntax.md says the LaTeX round trip "is checked in someone else's + /// repository", meaning this one. So this sweeps every node AngouriMath can print and asserts + /// the LaTeX comes back through the parser. Each failure here is a crash waiting for a user. + /// + public class AngouriMathLatexSweepTests { + /// + /// False for nodes built by reflection: filling every argument with x can produce a node + /// that is not well-formed, so a throw out of Latexize is not evidence of a defect. The + /// hand-built shapes below are strict, because those are known-good expressions. + /// + record Case(string Name, Entity Node, bool Strict); + + static IEnumerable Nodes() { + var x = MathS.Var("x"); + var y = MathS.Var("y"); + foreach (var t in typeof(Entity).Assembly.GetTypes() + .Where(t => typeof(Entity).IsAssignableFrom(t) && !t.IsAbstract && !t.IsGenericTypeDefinition) + .OrderBy(t => t.Name, StringComparer.Ordinal)) { + Entity? built = null; + foreach (var ctor in t.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .OrderBy(c => c.GetParameters().Length)) { + var ps = ctor.GetParameters(); + if (ps.Length is 0 or > 4) continue; + if (!ps.All(p => typeof(Entity).IsAssignableFrom(p.ParameterType))) continue; + try { built = (Entity)ctor.Invoke(ps.Select(_ => (object)x).ToArray()); break; } catch { } + } + if (built is not null) yield return new(t.Name, built, false); + } + // Shapes reflection cannot reach, listed by hand because they are exactly the ones whose + // LaTeX is unusual. + Case Strict(string name, Entity node) => new(name, node, true); + yield return Strict("Matrix", MathS.Vector(1, 2, 3)); + yield return Strict("Matrix2x2", MathS.Matrix(new Entity[,] { { 1, 2 }, { 3, 4 } })); + yield return Strict("Piecewise", MathS.Piecewise((x, x > 0), (y, y > 0))); + yield return Strict("Integral", MathS.Integral(x, x)); + yield return Strict("IntegralRanged", MathS.Integral(x, x, 0, 1)); + yield return Strict("Derivative", MathS.Derivative(x, x)); + yield return Strict("Limit", MathS.Limit(x, x, 0)); + yield return Strict("Interval", new Entity.Set.Interval(0, true, 1, true)); + yield return Strict("FiniteSet", new Entity.Set.FiniteSet(1, 2, 3)); + yield return Strict("ConditionalSet", "{ x : x > 0 }".ToEntity()); + yield return Strict("Integers", "ZZ".ToEntity()); + yield return Strict("Reals", "RR".ToEntity()); + yield return Strict("Complexes", "CC".ToEntity()); + yield return Strict("Rationals", "QQ".ToEntity()); + yield return Strict("Booleans", "BB".ToEntity()); + yield return Strict("Rational", "3/2".ToEntity()); + yield return Strict("ComplexNumber", MathS.Numbers.Create(1, 2)); + yield return Strict("Factorial", MathS.Factorial(x)); + yield return Strict("Union", MathS.Union("A".ToEntity(), "B".ToEntity())); + yield return Strict("Intersection", MathS.Intersection("A".ToEntity(), "B".ToEntity())); + yield return Strict("SetMinus", MathS.SetSubtraction("A".ToEntity(), "B".ToEntity())); + yield return Strict("In", "x in RR".ToEntity()); + yield return Strict("Provided", "x provided x > 0".ToEntity()); + yield return Strict("Apply", "apply(f, x)".ToEntity()); + yield return Strict("Lambda", "lambda(x, x^2)".ToEntity()); + yield return Strict("Domain", "domain(x, RR)".ToEntity()); + yield return Strict("Signum", MathS.Signum(x)); + yield return Strict("Abs", MathS.Abs(x)); + yield return Strict("Modulo", MathS.Mod(x, y)); + yield return Strict("EulerTotient", MathS.NumberTheory.Phi(x)); + } + + [Fact] + public void EveryAngouriMathNodeLatexizesIntoSomethingCSharpMathCanParse() { + var failures = new List(); + var checkedCount = 0; + foreach (var (name, node, strict) in Nodes()) { + string latex; + try { + latex = node.Latexize(); + } catch (Exception e) { + if (strict) failures.Add($"{name}: Latexize threw {e.GetType().Name}: {e.Message}"); + continue; + } + checkedCount++; + LaTeXParser.MathListFromLaTeX(latex) + .Match(_ => { }, err => failures.Add($"{name}: {latex}{Environment.NewLine} -> {err}")); + } + // A guard that checks nothing would also report no failures. + Assert.True(checkedCount > 50, $"only {checkedCount} nodes reached the parser"); + Assert.True(failures.Count == 0, + $"checked {checkedCount} nodes, {failures.Count} unparseable:{Environment.NewLine} " + + string.Join(Environment.NewLine + " ", failures)); + } + } +} diff --git a/CSharpMath.Evaluation.Tests/EvaluationTests.cs b/CSharpMath.Evaluation.Tests/EvaluationTests.cs index 254cd57c..687e0d77 100644 --- a/CSharpMath.Evaluation.Tests/EvaluationTests.cs +++ b/CSharpMath.Evaluation.Tests/EvaluationTests.cs @@ -179,6 +179,20 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"a / bc / d", @"\frac{\frac{a}{bc}}{d}", @"\frac{a}{bcd}")] [InlineData(@"-2/\sin x/y", @"\frac{\frac{-2}{\sin \left( x\right) }}{y}", @"\frac{-2}{\sin \left( x\right) \cdot y}")] public void BinaryOperators(string latex, string converted, string result) => Test(latex, converted, result); + /// + /// The two forms AngouriMath emits that had no reading here: \bmod for its modulo node + /// and \operatorname{\varphi} for Euler's totient. \bmod binds like multiplication + /// and division, which is AngouriMath's Priority.Mul, so the grouping cases below are + /// the point rather than decoration. + /// + [Theory] + [InlineData(@"x\bmod y", @"x\bmod y", @"x\bmod y")] + [InlineData(@"7\bmod 3", @"7\bmod 3", @"1")] + [InlineData(@"x+y\bmod z", @"x+y\bmod z", @"x+y\bmod z")] + [InlineData(@"x\bmod y+z", @"x\bmod y+z", @"x\bmod y+z")] + [InlineData(@"\operatorname{\varphi}(10)", @"\operatorname{φ} \left( 10\right) ", @"4")] + [InlineData(@"\operatorname{\varphi}(x)", @"\operatorname{φ} \left( x\right) ", @"\operatorname{φ} \left( x\right) ")] + public void AngouriMathOnlyForms(string latex, string converted, string result) => Test(latex, converted, result); [Theory] [InlineData(@"+i", @"\mathrm{i}", @"\mathrm{i}")] [InlineData(@"-i", @"-\mathrm{i}", @"-\mathrm{i}")] @@ -341,7 +355,7 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"\sin \frac\pi2", @"\sin \left( \frac{\mathrm{\pi }}{2}\right) ", @"1")] [InlineData(@"\sin \frac\pi2+1", @"\sin \left( \frac{\mathrm{\pi }}{2}\right) +1", @"2")] [InlineData(@"\cos +x", @"\cos \left( x\right) ", @"\cos \left( x\right) ")] - [InlineData(@"\cos -x", @"\cos \left( -x\right) ", @"\cos \left( -x\right) ")] + [InlineData(@"\cos -x", @"\cos \left( -x\right) ", @"\cos \left( x\right) ")] // 2.2.0 uses evenness [InlineData(@"\tan x\%", @"\tan \left( \frac{x}{100}\right) ", @"\tan \left( \frac{x}{100}\right) ")] [InlineData(@"\tan x\%^2", @"\tan \left( \left( \frac{x}{100}\right) ^2\right) ", @"\tan \left( \left( \frac{x}{100}\right) ^2\right) ")] [InlineData(@"\cot x\times y", @"\cot \left( x\right) \cdot y", @"\cot \left( x\right) \cdot y")] @@ -391,7 +405,8 @@ public void Numbers(string input, string converted, string output) => [InlineData(@"\tan^{-1} x\%^2", @"\arctan \left( \left( \frac{x}{100}\right) ^2\right) ", @"\arctan \left( \left( \frac{x}{100}\right) ^2\right) ")] [InlineData(@"\cot^{-1} x\times y", @"\arccot \left( x\right) \cdot y", @"\arccot \left( x\right) \cdot y")] [InlineData(@"\cot^{-1} x/y", @"\frac{\arccot \left( x\right) }{y}", @"\frac{\arccot \left( x\right) }{y}")] - [InlineData(@"\cos^{-1} \arccos^{-1} x", @"\arccos \left( \cos \left( x\right) \right) ", @"x")] + // arccos(cos x) is x only on [0, pi]; AngouriMath 2.2.0 no longer claims it in general. + [InlineData(@"\cos^{-1} \arccos^{-1} x", @"\arccos \left( \cos \left( x\right) \right) ", @"\arccos \left( \cos \left( x\right) \right) ")] [InlineData(@"\sin^1 x", @"\sin \left( x\right) ^1", @"\sin \left( x\right) ")] [InlineData(@"\sin^{+1} x", @"\sin \left( x\right) ^1", @"\sin \left( x\right) ")] [InlineData(@"\sin^{+-1} x", @"\sin \left( x\right) ^{-1}", @"\csc \left( x\right) ")] @@ -869,9 +884,10 @@ public void SimpleArithmeticSyntax(string simpleSyntax, string latex) => [InlineData(@"\top\nleftrightarrow\bot", @"\top \veebar \bot ", @"\top ")] [InlineData(@"\bot\nleftrightarrow\bot", @"\bot \veebar \bot ", @"\bot ")] [InlineData(@"x=x", @"x=x", @"\top ")] - [InlineData(@"x\le x", @"x\leq x", @"\top ")] - [InlineData(@"x\leq x", @"x\leq x", @"\top ")] - [InlineData(@"x\leqslant x", @"x\leq x", @"\top ")] + // AngouriMath 2.2.0 carries the domain <= needs rather than asserting the tautology outright. + [InlineData(@"x\le x", @"x\leq x", @"\top \quad \mathrm{for}\quad x\in \mathbb{R}")] + [InlineData(@"x\leq x", @"x\leq x", @"\top \quad \mathrm{for}\quad x\in \mathbb{R}")] + [InlineData(@"x\leqslant x", @"x\leq x", @"\top \quad \mathrm{for}\quad x\in \mathbb{R}")] [InlineData(@"x\neq y", @"x\neq y", @"x\neq y")] // Cannot simplify without knowing x and y [InlineData(@"1<2", @"1<2", @"\top ")] [InlineData(@"2<1", @"2<1", @"\bot ")] @@ -951,7 +967,8 @@ public void ChainedComparisons(string latex, string converted, string result, st [InlineData(@"-\operatorname{abs}(-1)", @"-\left| -1\right| ", @"-1")] [InlineData(@"-\operatorname{abs}\left|-1\right|", @"-\left| \left| -1\right| \right| ", @"-1")] [InlineData(@"-\left|1\right|^2", @"-\left| 1\right| ^2", @"-1")] - [InlineData(@"\operatorname{sgn}\operatorname{abs} x", @"\operatorname{sgn} \left( \left| x\right| \right) ", @"1")] + // AngouriMath 2.2.0 no longer answers 1 here: sgn(|x|) is 1 away from zero but 0 at zero. + [InlineData(@"\operatorname{sgn}\operatorname{abs} x", @"\operatorname{sgn} \left( \left| x\right| \right) ", @"\operatorname{sgn} \left( \left| x\right| \right) ")] public void Abs(string latex, string converted, string result) => Test(latex, converted, result); [Theory] [InlineData(@"\lim_{x\to2}x+1", @"\lim _{x\rightarrow 2}x+1", @"3")] diff --git a/CSharpMath.Evaluation.Tests/InterpretTests.cs b/CSharpMath.Evaluation.Tests/InterpretTests.cs index f4675fce..a30bd493 100644 --- a/CSharpMath.Evaluation.Tests/InterpretTests.cs +++ b/CSharpMath.Evaluation.Tests/InterpretTests.cs @@ -10,7 +10,7 @@ public class InterpretTests { [InlineData(@"1+2", @"\underline\mathrm{Input}\\1+2\\\\\underline\mathrm{Simplified}\\3\\\\\underline\mathrm{Value\ (100\ digits)}\\3")] [InlineData(@"1+\sqrt", @"\color{red}\text{Missing radicand}")] [InlineData(@"1+\sqrt2", @"\underline\mathrm{Input}\\1+\sqrt{2}\\\\\underline\mathrm{Simplified}\\1+\sqrt{2}\\\\\underline\mathrm{Value\ (100\ digits)}\\2.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573")] - [InlineData(@"1+\sqrt{2x}", @"\underline\mathrm{Input}\\1+\sqrt{2 x}\\\\\underline\mathrm{Simplified}\\1+\sqrt{2 x}\\\\\underline\mathrm{Expanded}\\1+\sqrt{2 x}\\\\\underline\mathrm{Factorized}\\1+\sqrt{2 x}")] + [InlineData(@"1+\sqrt{2x}", @"\underline\mathrm{Input}\\1+\sqrt{2 x}\\\\\underline\mathrm{Simplified}\\1+\sqrt{2 x}\\\\\underline\mathrm{Expanded}\\1+\sqrt{2} \sqrt{x}\\\\\underline\mathrm{Factorized}\\1+\sqrt{2 x}")] [InlineData(@"1+\sqrt{2xy}", @"\underline\mathrm{Input}\\1+\sqrt{2 x y}\\\\\underline\mathrm{Simplified}\\1+\sqrt{2 x y}\\\\\underline\mathrm{Expanded}\\1+\sqrt{2 x y}\\\\\underline\mathrm{Factorized}\\1+\sqrt{2 x y}")] [InlineData(@"=1+\sqrt{2xy}", @"\color{red}\text{Missing left side of equation}")] [InlineData(@"1+\sqrt{2xy}=", @"\color{red}\text{Missing right side of equation}")] diff --git a/CSharpMath.Evaluation/CSharpMath.Evaluation.csproj b/CSharpMath.Evaluation/CSharpMath.Evaluation.csproj index 942028b7..9b5aa3db 100644 --- a/CSharpMath.Evaluation/CSharpMath.Evaluation.csproj +++ b/CSharpMath.Evaluation/CSharpMath.Evaluation.csproj @@ -13,7 +13,7 @@ - + diff --git a/CSharpMath.Evaluation/Evaluation.cs b/CSharpMath.Evaluation/Evaluation.cs index ec6d1f16..7fe4bcdd 100644 --- a/CSharpMath.Evaluation/Evaluation.cs +++ b/CSharpMath.Evaluation/Evaluation.cs @@ -42,16 +42,20 @@ enum Precedence { Postfix // Highest } - public abstract record MathItem : ILatexiseable { + public abstract record MathItem : ILatexizeable { private protected MathItem() { } public abstract string Latexise(); + // AngouriMath 2.0 renamed ILatexiseable.Latexise to ILatexizeable.Latexize. Implementing + // it explicitly keeps MathItem.Latexise as the public name here, so this package's own + // surface is unchanged by their rename. + string ILatexizeable.Latexize() => Latexise(); public static implicit operator MathItem(AngouriMath.Entity content) => new Entity(content); public static explicit operator AngouriMath.Entity(MathItem item) => ((Entity)item).Content; /// A real number, complex number, variable, function call, vector, matrix, higher-dimensional tensor, or set public sealed record Entity : MathItem { public Entity(AngouriMath.Entity content) => Content = content; public AngouriMath.Entity Content { get; } - public override string Latexise() => Content.Latexise(); + public override string Latexise() => Content.Latexize(); } /// A linked list of comma-delimited items public sealed record Comma : MathItem, IEnumerable { @@ -494,6 +498,12 @@ string GreekToLaTeXCommandName(string n) => handleFunction = MathS.Signum; handleFunctionInverse = arg => MathS.NaN; goto handleFunction; + // Euler's totient, which AngouriMath writes \operatorname{\varphi}. It is not injective + // (φ(1) = φ(2) = 1), so there is no inverse to offer -- as for abs and sgn above. + case Atoms.LargeOperator { Nucleus: "φ" }: + handleFunction = MathS.NumberTheory.Phi; + handleFunctionInverse = arg => MathS.NaN; + goto handleFunction; case Atoms.LargeOperator { Nucleus: "lim", Subscript: var limitSubscript }: Entity limitVariable, limitTarget; int limitSubscriptIndex = 0; @@ -563,6 +573,12 @@ string GreekToLaTeXCommandName(string n) => handlePrecedence = Precedence.MultiplicationDivision; handleBinary = (a, b) => a / b; goto handleBinary; + // \bmod, which AngouriMath emits for its modulo node. It binds like multiplication and + // division there too (Priority.Mul), so a+b \bmod c is a+(b mod c) on both sides. + case Atoms.BinaryOperator { Nucleus: "mod" }: + handlePrecedence = Precedence.MultiplicationDivision; + handleBinary = MathS.Mod; + goto handleBinary; case Atoms.Ordinary { Nucleus: "%" }: handlePostfix = x => x / 100; goto handlePostfix; diff --git a/CSharpMath.Evaluation/Interpret.cs b/CSharpMath.Evaluation/Interpret.cs index 1f61f10d..8ad7bd15 100644 --- a/CSharpMath.Evaluation/Interpret.cs +++ b/CSharpMath.Evaluation/Interpret.cs @@ -3,8 +3,8 @@ namespace CSharpMath { static partial class Evaluation { - static StringBuilder AppendLaTeX(this StringBuilder sb, AngouriMath.Core.ILatexiseable latex) => - sb.Append(latex.Latexise()); + static StringBuilder AppendLaTeX(this StringBuilder sb, AngouriMath.Core.ILatexizeable latex) => + sb.Append(latex.Latexize()); static StringBuilder AppendLaTeXHeader(this StringBuilder sb, string header, bool includeNewlineBefore = true) { if (includeNewlineBefore) sb.Append(@"\\\\"); return sb.Append(@"\underline\mathrm{").Append(header).Append(@"}\\"); diff --git a/CSharpMath.Rendering.Tests/TestAngouriMathForms.cs b/CSharpMath.Rendering.Tests/TestAngouriMathForms.cs new file mode 100644 index 00000000..23452f75 --- /dev/null +++ b/CSharpMath.Rendering.Tests/TestAngouriMathForms.cs @@ -0,0 +1,37 @@ +using Xunit; + +namespace CSharpMath.Rendering.Tests { + /// + /// \bmod is the first binary operator here whose nucleus is a word rather than a symbol, + /// and \operatorname{φ} the first operator name written with a non-ASCII letter. Both come + /// from AngouriMath. Glyph coverage is already asserted by + /// ; what is new is the layout, so this + /// measures rather than comparing against a baseline image. + /// + public class TestAngouriMathForms { + static System.Drawing.RectangleF Measure(string latex) { + var painter = new SkiaSharp.MathPainter { LaTeX = latex }; + Assert.Null(painter.ErrorMessage); + return painter.Measure(FrontEnd.TextPainter.DefaultCanvasWidth); + } + + [Theory] + [InlineData(@"x\bmod y")] + [InlineData(@"\operatorname{φ}\left( x\right) ")] + [InlineData(@"\operatorname{φ}\left( x\bmod y\right) ")] + public void TheyLayOut(string latex) { + var measured = Measure(latex); + Assert.True(measured.Width > 0, $"zero width for {latex}"); + Assert.True(measured.Height > 0, $"zero height for {latex}"); + } + + /// + /// The three letters of "mod" and the spacing around a binary operator are actually laid out, + /// rather than the atom occupying no room -- which a zero-width check alone would not catch, + /// since x and y are there either way. + /// + [Fact] + public void ModuloTakesUpRoom() => + Assert.True(Measure(@"x\bmod y").Width > Measure(@"xy").Width * 2); + } +} diff --git a/CSharpMath/Atom/LaTeXSettings.cs b/CSharpMath/Atom/LaTeXSettings.cs index 3b4f86f0..3ef5599a 100644 --- a/CSharpMath/Atom/LaTeXSettings.cs +++ b/CSharpMath/Atom/LaTeXSettings.cs @@ -195,9 +195,30 @@ public static class LaTeXSettings { } }, { @"\operatorname", (parser, accumulate, stopChar) => { if (!parser.ReadCharIfAvailable('{')) return "Expected {"; - var operatorname = parser.ReadString(); + // An operator name is a word, so letters -- but a letter may be written as a command: + // AngouriMath emits \operatorname{\varphi} for Euler's totient, and φ is a letter like + // any other. Commands standing for anything else are refused rather than spliced in, + // which keeps \operatorname{a|} the error it has always been. + // + // Letters are taken as char.IsLetter and not as parser.ReadString(), which is ASCII-only: + // this method writes the name back out as \operatorname{φ}, so refusing to read φ would + // mean refusing to read its own output. + var operatorname = new StringBuilder(); + while (true) { + while (parser.HasCharacters) { + var ch = parser.ReadChar(); + if (char.IsLetter(ch)) operatorname.Append(ch); + else { parser.UndoReadChar(); break; } + } + if (!parser.ReadCharIfAvailable('\\')) break; + var command = parser.ReadString(); + if (!(AtomForCommand(@"\" + command) is { } letter) + || letter.Nucleus.Length == 0 || !letter.Nucleus.All(char.IsLetter)) + return $@"Invalid command \{command} in an operator name"; + operatorname.Append(letter.Nucleus); + } if (!parser.ReadCharIfAvailable('}')) return "Expected }"; - return Ok(new LargeOperator(operatorname, null)); + return Ok(new LargeOperator(operatorname.ToString(), null)); } }, // Bra and Ket implementations are derived from Donald Arseneau's braket LaTeX package. // See: https://www.ctan.org/pkg/braket @@ -689,6 +710,11 @@ atom is Accent accent { @"\wedge", @"\land", new BinaryOperator("∧") }, { @"\setminus", new BinaryOperator("∖") }, { @"\wr", new BinaryOperator("≀") }, + // Not a symbol but a word: \bmod is \mathbin{\operatorname{mod}}, so binary spacing with an + // upright nucleus. A BinaryOperator gives both -- only Variable and Number are put through + // the italicising font changer, so "mod" stays upright. AngouriMath emits this for its + // modulo node, e.g. `x \bmod y`. + { @"\bmod", new BinaryOperator("mod") }, { @"-", new BinaryOperator("−") }, // Use the math minus sign, not hyphen { @"\diamond", new BinaryOperator("⋄") }, { @"\bigtriangleup", new BinaryOperator("△") },