Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions src/SharpEquations/Internal/LaTeX/LaTeXReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ private LaTeXReader(IDictionary<string, string> macros, bool display)
}

/// <summary>
/// TeX2UnicodeMath: the UnicodeMath of LaTeX math (without its $ or \[ delimiters). Macros defined with \def
/// or \newcommand, and environments defined with \newenvironment, go to <paramref name="macros"/>, where later conversions find them. In a display equation the
/// TeX2UnicodeMath: the UnicodeMath of LaTeX math (without its $ or \[ delimiters). Macros defined with \def,
/// \newcommand or \DeclareMathOperator, and environments defined with \newenvironment, go to <paramref name="macros"/>, where later conversions find them. In a display equation the
/// scripts of a large operator other than an integral (\sum, \prod, \bigcup...) go under and over it, as TeX
/// puts them. The \ce and \pu of mhchem are rewritten as plain LaTeX first (<see cref="Mhchem"/>).
/// </summary>
Expand Down Expand Up @@ -605,6 +605,15 @@ private static string RewriteTeXOnly(string latex)
i = end;
continue;
}
if (cw == "DeclareMathOperator" && Argument(tex, tex[j] == "*" ? j + 1 : j) is var (operatorName, afterOperatorName) && afterOperatorName >= 0 &&
BracedArgument(tex, afterOperatorName) is var (operatorBody, afterOperatorBody) && afterOperatorBody >= 0)
{
// \DeclareMathOperator{\name}{text}: \name defined as \operatorname{text}, \operatorname*{text} when starred
result.Append("\\newcommand{").Append(operatorName.Trim()).Append("}{")
.Append(RewriteTeXOnly("\\operatorname" + (tex[j] == "*" ? "*" : "") + "{" + operatorBody + "}")).Append('}');
i = afterOperatorBody;
continue;
}
if (cw == "renewcommand")
{
result.Append("\\newcommand");
Expand Down Expand Up @@ -1394,6 +1403,9 @@ private string Convert(string latex)
var tex = new Text(EscapeOddBars(RewriteTeXOnly(Limits(NewEnvironments(latex)))));
var cwPrev = "";
bool needMacroPass = false;
// whether a macro was named and its body is next, and where that body starts: an empty body expands to nothing
bool macroNamed = false;
int macroBody = -1;
var uniTeX = new StringBuilder();

for (int i = 0; i < tex.Length;)
Expand Down Expand Up @@ -1440,6 +1452,7 @@ private string Convert(string latex)
i++;
needMacroPass = true;
macros[cw] = "";
macroNamed = true;
continue;
}
cwPrev = cw;
Expand Down Expand Up @@ -1517,10 +1530,16 @@ private string Convert(string latex)
i++;
continue;
}

if (macroNamed)
{
macroNamed = false;
macroBody = i;
}
uniTeX.Append(tex[i++]);
break;
case "}":
if (i > 0 && tex[i - 1] == "{")
if (i > 0 && tex[i - 1] == "{" && i - 1 != macroBody)
uniTeX.Append('⬚');
uniTeX.Append(tex[i++]);
break;
Expand Down Expand Up @@ -1659,12 +1678,20 @@ private static (string, int) ApplyMacro(Text tex, int i, bool accentsAfter)
{
int k;
int cArg = 0;
string optional = null;
if (tex[i] == "[")
{
cArg = Js.CodeAt(tex[i + 1], 0) - '0';
if (tex[i + 2] != "]" || cArg < 0 || cArg > 9)
return ("", i);
k = i + 3;
// [n][default]: the first argument optional, with that default
if (tex[k] == "[" && cArg > 0)
{
(optional, k) = OptionalArgument(tex, k);
if (optional == null)
return ("", i);
}
}
else
{
Expand Down Expand Up @@ -1692,6 +1719,13 @@ private static (string, int) ApplyMacro(Text tex, int i, bool accentsAfter)
var arg = "";
while (tex[i] == " ")
i++;
if (j == 0 && optional != null)
{
var (given, afterGiven) = OptionalArgument(tex, i);
args.Add(given ?? optional);
i = afterGiven;
continue;
}
if (tex[i] == "{")
{
k = FindClosingBrace(tex, i + 1);
Expand All @@ -1712,7 +1746,7 @@ private static (string, int) ApplyMacro(Text tex, int i, bool accentsAfter)
args.Add(arg);
}
while (args.Count < cArg)
args.Add("");
args.Add(args.Count == 0 && optional != null ? optional : "");

var val = new StringBuilder();
// an accent passed as an argument (\hat, a combining mark) goes on the character after it, as in LaTeX
Expand Down
9 changes: 9 additions & 0 deletions tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,15 @@ public async Task ReadsDelimitersAsDisplay(string latex, bool display)
[Arguments(@"\def\R{\mathbb{R}} x\in\R", @"x\in\mathbb{R}")]
[Arguments(@"\newcommand{\pd}[2]{\frac{\partial #1}{\partial #2}}\pd{f}{x}", @"\frac{\partial f}{\partial x}")]
[Arguments(@"\newcommand{\N}{\mathbb{N}}\N", @"\mathbb{N}")]
[Arguments(@"\newcommand{\f}[2][y]{#1+#2} \f{a}+\f[b]{c}", @"y+a+b+c")]
[Arguments(@"\newcommand{\g}[1][Hello]{#1} \g{world}", @"Helloworld")]
[Arguments(@"\newcommand{\g}[1][a]{#1^2} \g", @"a^2")]
[Arguments(@"\newcommand{\e}{} \e x", @"x")]
[Arguments(@"\def\e{}\e x+\e y", @"x+y")]
[Arguments(@"\newcommand{\h}[2]{} \h{a}{b}x", @"x")]
[Arguments(@"\DeclareMathOperator{\Res}{Res} \Res_{z=0}", @"\operatorname{Res}_{z=0}")]
[Arguments(@"\DeclareMathOperator\Tr{Tr} \Tr A", @"\operatorname{Tr}A")]
[Arguments(@"\DeclareMathOperator*{\argmax}{argmax} \argmax_x f", @"\operatorname{argmax}_xf")]
public async Task ExpandsMacros(string latex, string expected)
{
await Assert.That(Equation.Parse(latex, EquationFormat.LaTeX).ToString(EquationFormat.LaTeX)).IsEqualTo(expected);
Expand Down
11 changes: 4 additions & 7 deletions tests/SharpEquations.Tests/LaTeX/Latex2MathMLCorpusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,9 @@ private static string Canonical(Equation equation)

private static readonly HashSet<string> ReadOtherwise = new HashSet<string>(NotInWordLaTeX.Concat(HeldOtherwise).Concat(NotLaTeX));

// the macros the reader does not expand as LaTeX does: \DeclareMathOperator, the default of an optional argument,
// and an empty body (which leaves an empty group)
private static readonly HashSet<string> NotExpanded = new HashSet<string>
{
"declare-math-operator", "newcommand-optional-default", "newcommand-empty-body",
};
// the macros that latex2mathml expands otherwise than LaTeX: \greeting{world}, whose one argument is optional,
// which LaTeX gives its default (Hello), leaving {world} after it, where latex2mathml takes world as the argument
private static readonly HashSet<string> ExpandedOtherwise = new HashSet<string> { "newcommand-optional-default" };

private static string? Check(Func<string?> check)
{
Expand Down Expand Up @@ -290,7 +287,7 @@ public async Task LaTeXReadsAsLatex2MathMLReadsIt()
[Test]
public async Task MacrosExpandToTheirEquivalent()
{
var unexpected = Unexpected(Macros(), m => m.id, m => m.latex, NotExpanded, m =>
var unexpected = Unexpected(Macros(), m => m.id, m => m.latex, ExpandedOtherwise, m =>
{
var expected = Equation.Parse(m.equivalent, EquationFormat.LaTeX, Options(m.display));
var actual = Equation.Parse(m.latex, EquationFormat.LaTeX, Options(m.display));
Expand Down
Loading