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("△") },