From 727a2add4d21be7ee55e9abe38a13444d5583717 Mon Sep 17 00:00:00 2001 From: Simone Bortolin Date: Tue, 10 Nov 2026 00:24:46 +0100 Subject: [PATCH] LaTeX writer: menclose notations, letterlike symbols and MTEF angle brackets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - menclose in standard LaTeX: horizontalstrike as \sout (ulem, KaTeX), radical as \sqrt, the notations without a command (longdiv, roundedbox, circle, actuarial, left, right, verticalstrike, madruwb, combinations) as \enclose of MathJax instead of \boxed; no notation and the four sides stay \boxed. Word's LaTeX keeps TeX.js's mapping. - The reader reads \sout, \hcancel and \enclose{notation}[attributes]{body} back. - Letterlike symbols with a command win over the math alphanumerics: ℑ \Im, ℜ \Re; the reader reads \Re and \Im as ℜ and ℑ, and the function names Re and Im are written as \operatorname{Re} and \operatorname{Im}. - 〈 〉 (U+2329/U+232A, as MTEF writes them) and U+3008/U+3009 grow with \left and \right around tall content, as ⟨ ⟩ do. - mtef2mml.xsl: MTEF ℓ (0x2113) is ℓ, not a script l, and a duplicate template no longer turns ℘ (0x2118) into 𝒫. - Tests from MathML, LaTeX and the MathType objects of NPOI's equations-ole-all.doc. Co-Authored-By: Claude Opus 5.5 --- BuildVersion.props | 2 +- .../Internal/LaTeX/LaTeXReader.cs | 51 ++++++++++++++++++ .../Internal/LaTeX/LaTeXSerializer.cs | 43 +++++++++++---- .../Internal/LaTeX/LaTeXSymbols.cs | 4 +- src/SharpEquations/Resources/mtef2mml.xsl | 9 +--- .../LaTeX/LaTeXReaderTests.cs | 8 ++- .../MathML/MathMLWriterTests.cs | 27 +++++++++- .../Mtef/EquationNativeStreamTests.cs | 37 +++++++++++++ .../UnicodeMath/UnicodeMathTests.cs | 2 +- tests/data/README.md | 6 +++ tests/data/mtef/npoi/bra-ket.eqn | Bin 0 -> 66 bytes tests/data/mtef/npoi/fraction-and-rad.eqn | Bin 0 -> 188 bytes tests/data/mtef/npoi/nested-fences.eqn | Bin 0 -> 263 bytes tests/data/mtef/npoi/operators.eqn | Bin 0 -> 133 bytes tests/data/mtef/npoi/overlines.eqn | Bin 0 -> 61 bytes tests/data/mtef/npoi/parentheses.eqn | Bin 0 -> 337 bytes tests/data/mtef/npoi/symbols.eqn | Bin 0 -> 209 bytes 17 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 tests/data/mtef/npoi/bra-ket.eqn create mode 100644 tests/data/mtef/npoi/fraction-and-rad.eqn create mode 100644 tests/data/mtef/npoi/nested-fences.eqn create mode 100644 tests/data/mtef/npoi/operators.eqn create mode 100644 tests/data/mtef/npoi/overlines.eqn create mode 100644 tests/data/mtef/npoi/parentheses.eqn create mode 100644 tests/data/mtef/npoi/symbols.eqn diff --git a/BuildVersion.props b/BuildVersion.props index 83c5ff3..4730ad9 100644 --- a/BuildVersion.props +++ b/BuildVersion.props @@ -2,7 +2,7 @@ - 1.0.5 + 1.0.6 diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs index f24899c..ad18073 100644 --- a/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs +++ b/src/SharpEquations/Internal/LaTeX/LaTeXReader.cs @@ -385,6 +385,8 @@ private static (string Text, int Next) ScriptArgument(Text tex, int i) ["intop"] = "\\int", ["smallint"] = "\\int", ["diagup"] = "\\╱", ["diagdown"] = "\\╲", ["thickspace"] = "\\;", ["And"] = "\\&", ["lvert"] = "|", ["rvert"] = "|", ["idotsint"] = "\\∫⋯\\∫", ["mathscr"] = "\\mathcal", ["operatornamewithlimits"] = "\\operatorname", ["mathstrut"] = "\\vphantom{\\(}", + // \Re and \Im, the symbols ℜ and ℑ of LaTeX, which UnicodeMath reads as the function names Re and Im + ["Re"] = "ℜ", ["Im"] = "ℑ", }; private static readonly HashSet OpeningBrackets = new HashSet(StringComparer.Ordinal) { "(", "[", "{", "⟨", "⌈", "⌊" }; @@ -891,6 +893,21 @@ private static string RewriteTeXOnly(string latex) continue; } } + else if (cw is "sout" or "hcancel" && Argument(tex, j) is var (struck, afterStruck) && afterStruck >= 0) + { + // a horizontal strike: \sout of ulem (and of KaTeX), \hcancel + result.Append(Enclosure("horizontalstrike", RewriteTeXOnly(struck))); + i = afterStruck; + continue; + } + else if (cw == "enclose" && BracedArgument(tex, j) is var (notation, afterNotation) && afterNotation >= 0 && + BracedArgument(tex, OptionalArgument(tex, afterNotation).Next) is var (enclosed, afterEnclosed) && afterEnclosed >= 0) + { + // \enclose{notation}[attributes]{body} of MathJax: the notations of MathML menclose, parted by commas + result.Append(Enclosure(notation, RewriteTeXOnly(enclosed))); + i = afterEnclosed; + continue; + } else if (FontSwitches.TryGetValue(cw, out var font)) { // an old font switch, {\rm rxn}, styles the rest of its group: \mathrm{rxn} @@ -905,6 +922,40 @@ private static string RewriteTeXOnly(string latex) return result.ToString(); } + // the notations of menclose that UnicodeMath has an operator for + private static readonly Dictionary EnclosureOperators = new Dictionary(StringComparer.Ordinal) + { + ["box"] = "▭", ["roundedbox"] = "▢", ["circle"] = "◯", ["longdiv"] = "⟌", + }; + + // the notations of menclose that UnicodeMath writes as the bits of the mask of ▭ (the sides are those it hides) + private static readonly Dictionary EnclosureBits = new Dictionary(StringComparer.Ordinal) + { + ["top"] = 1, ["bottom"] = 2, ["left"] = 4, ["right"] = 8, ["horizontalstrike"] = 16, ["verticalstrike"] = 32, + ["downdiagonalstrike"] = 64, ["updiagonalstrike"] = 128, ["box"] = 15, ["madruwb"] = 10, + }; + + private static readonly char[] NotationSeparators = { ',', ' ' }; + + /// + /// The UnicodeMath of a body in an enclosure of MathML notations (\enclose{top,bottom}{x}): its operator (⟌{x}), the + /// mask of ▭ (▭(28&{x})), or the square root of the radical; a box when none is known. + /// + private static string Enclosure(string notation, string body) + { + var notations = notation.Split(NotationSeparators, StringSplitOptions.RemoveEmptyEntries); + if (notations.Length == 1 && notations[0] == "radical") + return "\\sqrt{" + body + "}"; + if (notations.Length == 1 && EnclosureOperators.TryGetValue(notations[0], out var op)) + return op + "{" + body + "}"; + // the actuarial, a combining mark before its body in UnicodeMath, and the ▭ of a mask, held through the passes + // (which would put the mark after a base, and take the mask for the argument of ▭) in a group of their own + if (notations.Length == 1 && notations[0] == "actuarial") + return "{{" + HoldMark("⃧") + "(" + body + ")}}"; + int mask = notations.Sum(n => EnclosureBits.TryGetValue(n, out var bits) ? bits : 0); + return mask == 0 || mask == 15 ? "▭{" + body + "}" : "{" + HoldMark("▭") + "(" + (mask ^ 15) + "&{" + body + "})}"; + } + // the font switches of plain TeX, which style the rest of their group, as the LaTeX commands with an argument private static readonly Dictionary FontSwitches = new Dictionary(StringComparer.Ordinal) { diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs b/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs index dee8921..d27ec3e 100644 --- a/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs +++ b/src/SharpEquations/Internal/LaTeX/LaTeXSerializer.cs @@ -28,13 +28,26 @@ internal sealed partial class LaTeXSerializer ["⎵"] = "underbracket", ["⏝"] = "underparen", ["⏟"] = "underbrace", ["⏡"] = "undershell", ["▁"] = "underbar", }; + // the enclosures of Word's LaTeX, as TeX.js writes them (the others are boxed) private static readonly Dictionary Enclosures = new Dictionary { - ["box"] = "boxed", ["top"] = "", ["bottom"] = "", ["roundedbox"] = "", ["circle"] = "circle", ["longdiv"] = "", ["actuarial"] = "", - ["cancel"] = "cancel", ["bcancel"] = "bcancel", ["xcancel"] = "xcancel", ["updiagonalstrike"] = "cancel", + ["box"] = "boxed", ["circle"] = "circle", ["cancel"] = "cancel", ["bcancel"] = "bcancel", ["xcancel"] = "xcancel", ["updiagonalstrike"] = "cancel", ["downdiagonalstrike"] = "bcancel", ["updiagonalstrike downdiagonalstrike"] = "xcancel", ["downdiagonalstrike updiagonalstrike"] = "xcancel", }; + // the enclosures of standard LaTeX that have a command: \boxed, those of the cancel package, \sout of ulem (and + // KaTeX) and \sqrt; the others are \enclose of MathJax + private static readonly Dictionary StandardEnclosures = new Dictionary + { + ["box"] = "boxed", ["cancel"] = "cancel", ["bcancel"] = "bcancel", ["xcancel"] = "xcancel", ["updiagonalstrike"] = "cancel", + ["downdiagonalstrike"] = "bcancel", ["updiagonalstrike downdiagonalstrike"] = "xcancel", ["downdiagonalstrike updiagonalstrike"] = "xcancel", + ["horizontalstrike"] = "sout", ["radical"] = "sqrt", + }; + + private static readonly char[] NotationSeparators = { ' ' }; + + private static readonly string[] Sides = { "top", "bottom", "left", "right" }; + private static readonly (string Environment, string Intent)[] MatrixIntents = { ("pmatrix", ":parenthesized-matrix"), ("vmatrix", ":determinant"), ("Vmatrix", ":normed-matrix"), ("bmatrix", ":bracketed-matrix"), @@ -333,14 +346,18 @@ private string TeX(XElement value, bool noAddParens = false) ret = Unary(value, notation == "top" ? "\\bar" : "\\underline"); break; } - if (standard && notation == "circle") + // no notation is a box, as UnicodeMath writes it (▭), and so are the four sides + var notations = (notation ?? "").Split(NotationSeparators, StringSplitOptions.RemoveEmptyEntries); + if (notations.Length == 0 || notations.Length == 4 && Sides.All(notations.Contains)) notation = "box"; - if (!string.IsNullOrEmpty(notation) && Enclosures.TryGetValue(notation, out var enclosure) && enclosure.Length > 0) + if ((standard ? StandardEnclosures : Enclosures).TryGetValue(notation, out var enclosure)) { ret = Unary(value, "\\" + enclosure); break; } - ret = Unary(value, "\\boxed"); + // in standard LaTeX the others (longdiv, roundedbox, circle, left, verticalstrike...) are \enclose of + // MathJax, with its notations parted by commas + ret = standard ? "\\enclose{" + string.Join(",", notations) + "}{" + Nary(value, "", cNode) + "}" : Unary(value, "\\boxed"); break; } @@ -558,7 +575,7 @@ private string TeX(XElement value, bool noAddParens = false) if (standard && val == "&") return "\\&"; if (standard && IsFunctionName(val)) - return "\\" + val + " "; + return FunctionName(val); if (OperatorMap.TryGetValue(val, out var mapped)) return mapped; // escaped for the reader, in standard LaTeX its command if it has one (Ⓢ, \circledS) @@ -649,7 +666,7 @@ private string TeX(XElement value, bool noAddParens = false) } else if (IsFunctionName(Text(value))) { - ret = "\\" + Text(value) + " "; + ret = FunctionName(Text(value)); break; } else if (Len(Text(value)) > 1 && SharpEquations.Internal.Text.AsciiPatterns.Letters().IsMatch(Text(value))) @@ -805,8 +822,14 @@ private static bool IsTallFence(XElement value, string intent) middle.Any(e => e.DescendantsAndSelf().Any(d => TallElements.Contains(Name(d)))); } - private const string OpeningFences = "([{⟨|‖⌈⌊"; - private const string ClosingFences = ")]}⟩|‖⌉⌋"; + // the angle brackets also as MTEF writes them, 〈 and 〉 (U+2329 and U+232A), and as CJK brackets (U+3008 and U+3009) + private const string OpeningFences = "([{⟨〈〈|‖⌈⌊"; + private const string ClosingFences = ")]}⟩〉〉|‖⌉⌋"; + + /// + /// A function name as its command (\sin); Re and Im as \operatorname, \Re and \Im being the symbols ℜ and ℑ. + /// + private static string FunctionName(string name) => name is "Re" or "Im" ? "\\operatorname{" + name + "}" : "\\" + name + " "; /// An mrow between fences that do not pair (]𝑎[, or one side only), read back only with \left and \right. private static bool IsUnpairedFence(XElement value, string intent) @@ -916,7 +939,7 @@ private static string StandardIdentifier(XElement value) if (text.Length == 0) return "{}"; if (IsFunctionName(text)) - return "\\" + text + " "; + return FunctionName(text); var plain = string.Concat(Characters(text).Where(c => c.Length == 1 && (c[0] >= 'A' && c[0] <= 'Z' || c[0] >= 'a' && c[0] <= 'z'))); return plain.Length == text.Length ? "\\operatorname{" + text + "}" : LaTeXSymbols.Math(text); } diff --git a/src/SharpEquations/Internal/LaTeX/LaTeXSymbols.cs b/src/SharpEquations/Internal/LaTeX/LaTeXSymbols.cs index 99f4ecd..7a8df54 100644 --- a/src/SharpEquations/Internal/LaTeX/LaTeXSymbols.cs +++ b/src/SharpEquations/Internal/LaTeX/LaTeXSymbols.cs @@ -208,7 +208,9 @@ private static string Character(string ch) case "_": return "\\" + ch; } - if (Alphanumerics.TryGetValue(ch, out var styled)) + // a letterlike symbol with a command of its own is the command (ℑ is \Im, not \mathfrak{I}) + bool letterlike = ch.Length == 1 && ch[0] is >= '℀' and <= '⅏' && Commands.ContainsKey(ch); + if (!letterlike && Alphanumerics.TryGetValue(ch, out var styled)) { var letter = Commands.TryGetValue(styled.Plain, out var greek) ? "\\" + greek + " " : styled.Plain; // a capital Greek letter is upright in TeX (\Gamma): italic, it is \mathit{\Gamma} diff --git a/src/SharpEquations/Resources/mtef2mml.xsl b/src/SharpEquations/Resources/mtef2mml.xsl index 9041644..cd49f92 100644 --- a/src/SharpEquations/Resources/mtef2mml.xsl +++ b/src/SharpEquations/Resources/mtef2mml.xsl @@ -2330,11 +2330,6 @@ &#x2118; - - - &#x1D4AB; - - P @@ -2480,9 +2475,9 @@ k - + - l + &#x2113; diff --git a/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs b/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs index 83f19c0..9ba6a3e 100644 --- a/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs +++ b/tests/SharpEquations.Tests/LaTeX/LaTeXReaderTests.cs @@ -90,7 +90,13 @@ public class LaTeXReaderTests [Arguments(@"x ^ { 2 } _ i", @"x_i^2", @"𝑥_𝑖^2")] [Arguments(@"\sum_{\substack{ix", @"\cancel{x}", @"\cancel{𝑥}", @"▭(143&𝑥)")] [Arguments(@"x", @"\bcancel{x}", @"\bcancel{𝑥}", @"▭(79&𝑥)")] [Arguments(@"x", @"\xcancel{x}", @"\xcancel{𝑥}", @"▭(207&𝑥)")] - [Arguments(@"x", null, null, @"◯𝑥")] + [Arguments(@"x", @"\enclose{circle}{x}", @"\circle{𝑥}", @"◯𝑥")] + [Arguments(@"x", @"\sout{x}", @"\boxed{𝑥}", @"▭(31&𝑥)")] + [Arguments(@"x", @"\enclose{verticalstrike}{x}", @"\boxed{𝑥}", @"▭(47&𝑥)")] + [Arguments(@"x", @"\enclose{longdiv}{x}", @"\boxed{𝑥}", @"⟌𝑥")] + [Arguments(@"x", @"\enclose{roundedbox}{x}", @"\boxed{𝑥}", @"▢𝑥")] + [Arguments(@"x", @"\enclose{actuarial}{x}", @"\boxed{𝑥}", "⃧𝑥")] + [Arguments(@"x", @"\enclose{left}{x}", @"\boxed{𝑥}", @"▭(11&𝑥)")] + [Arguments(@"x", @"\enclose{right}{x}", @"\boxed{𝑥}", @"▭(7&𝑥)")] + [Arguments(@"x", @"\enclose{top,bottom}{x}", @"\boxed{𝑥}", @"▭(12&𝑥)")] + [Arguments(@"x", @"\enclose{madruwb}{x}", @"\boxed{𝑥}", @"▭𝑥")] + [Arguments(@"x", @"\sqrt{x}", @"\boxed{𝑥}", @"▭𝑥")] + [Arguments(@"xy", @"\boxed{x}\boxed{y}", @"\boxed{𝑥}\boxed{𝑦}", @"▭(0&𝑥)▭𝑦")] + [Arguments(@"ℓℑℜ℘ℏℵ", @"\ell\Im\Re\wp\hbar\aleph", @"ℓℑℜ℘ℏℵ", @"ℓℑℜ℘ℏℵ")] + [Arguments(@"Re⁡z+Im⁡z", @"\operatorname{Re}z+\operatorname{Im}z", @"\operatorname{Re}𝑧+\operatorname{Im}𝑧", @"Re⁡𝑧+Im⁡𝑧")] + [Arguments("m〈n〉", @"m\langle n\rangle", "𝑚〈𝑛〉", "𝑚〈𝑛〉")] + [Arguments("〈ab〉〈ab〉", @"\left\langle\frac{a}{b}\right\rangle\left\langle\frac{a}{b}\right\rangle", null, "〈𝑎/𝑏 〉〈𝑎/𝑏 〉")] + [Arguments("|a〉〈b|", @"|a\rangle\langle b|", null, "|𝑎〉〈𝑏|")] + [Arguments("〈a|b〉", @"\langle a|b\rangle", null, "〈𝑎|𝑏〉")] [Arguments(@"Cbac", @"{}_a^cC_b", @"{}_𝑎^𝑐𝐶_𝑏", @"_𝑎^𝑐 𝐶_𝑏")] [Arguments(@"abc", @"a\,b\quad c", @"𝑎 𝑏 𝑐", @"𝑎 𝑏 𝑐")] [Arguments(@"x", @"\phantom{x}", @"\phantom{𝑥}", @"⟡𝑥")] @@ -80,10 +97,18 @@ public async Task WritesMathML(string mathML, string? standard, string? word, st [Arguments("x", @"\bcancel{x}")] [Arguments("x", @"\xcancel{x}")] [Arguments("x", @"\boxed{x}")] + [Arguments("a+b", @"\sout{a+b}")] + [Arguments("x", @"\enclose{verticalstrike}{x}")] + [Arguments("ab", @"\enclose{longdiv}{\frac{a}{b}}")] + [Arguments("x", @"\enclose{roundedbox}{x}")] + [Arguments("x", @"\enclose{circle}{x}")] + [Arguments("n", @"\enclose{actuarial}{n}")] + [Arguments("x", @"\enclose{left}{x}")] public async Task StrikesAndBoxesReadBack(string mathML, string latex) { var equation = Equation.Parse(mathML, EquationFormat.MathML); await Assert.That(equation.ToString(EquationFormat.LaTeX)).IsEqualTo(latex); + await Assert.That(Equation.Parse(latex, EquationFormat.LaTeX)).IsEqualTo(equation); await Assert.That(Equation.Parse(latex, EquationFormat.LaTeX).ToString(EquationFormat.LaTeX)).IsEqualTo(latex); await Assert.That(Equation.Parse(equation.ToString(EquationFormat.UnicodeMath), EquationFormat.UnicodeMath).ToString(EquationFormat.LaTeX)).IsEqualTo(latex); } diff --git a/tests/SharpEquations.Tests/Mtef/EquationNativeStreamTests.cs b/tests/SharpEquations.Tests/Mtef/EquationNativeStreamTests.cs index 101596e..cd8e394 100644 --- a/tests/SharpEquations.Tests/Mtef/EquationNativeStreamTests.cs +++ b/tests/SharpEquations.Tests/Mtef/EquationNativeStreamTests.cs @@ -69,6 +69,43 @@ public async Task ConvertsSummationOperatorsWithBothLimits() await Assert.That(math.Value.Any(IsArrow)).IsFalse(); } + /// + /// MathType objects of the palettes of strikes, symbols, fractions and fences, as standard LaTeX: the horizontal + /// strike (\sout), the long division (\enclose{longdiv}), the letterlike symbols (ℓ, ℑ, ℜ, ℘) and the angle brackets + /// of MTEF (〈 and 〉, U+2329 and U+232A); the LaTeX reads back to itself. + /// + [Test] + [Arguments("overlines.eqn", @"\sout{a}\sout{b}\sout{c}")] + [Arguments("symbols.eqn", @"a\partial b\nabla c\infty d\Im e\Re f\aleph g\angle h\perp i\diamond l\ell m\wp n\degree o\hbar pƛq\int r\hbar sƛt\int u\sum a\prod b\coprod c")] + [Arguments("fraction-and-rad.eqn", @"\frac{a}{b}\frac{a}{b}\frac{a}{b}\frac{a}{b}a/b\sqrt{a}\sqrt[b]{a}\enclose{longdiv}{a}\begin{matrix}a\\\enclose{longdiv}{b}\end{matrix}")] + [Arguments("operators.eqn", @"a\pm b\mp c\times d\div e\ast f\cdot g\circ h•i\otimes l\oplus m\langle n\rangle")] + [Arguments("bra-ket.eqn", @"\langle a|b\rangle")] + [Arguments("parentheses.eqn", @"(ab)[ab]\{ab\}\langle ab\rangle|ab|\|ab\||a\rangle\langle b|")] + public async Task WritesMathTypePalettesAsLaTeX(string file, string latex) + { + var equation = Read("npoi/" + file); + await Assert.That(equation.ToString(EquationFormat.LaTeX)).IsEqualTo(latex); + await Assert.That(Equation.Parse(latex, EquationFormat.LaTeX).ToString(EquationFormat.LaTeX)).IsEqualTo(latex); + } + + /// Angle brackets of MTEF around other fences, one of them a bar alone. + [Test] + public async Task WritesTheAngleBracketsOfMathTypeAmongOtherFences() + { + await Assert.That(Read("npoi/nested-fences.eqn").ToString(EquationFormat.LaTeX)) + .IsEqualTo(@"(0[a\{b\langle c\left|d\|e\lfloor f\lceil gc\rceil d\rfloor e\|f\right|g\rangle h\}i]0)"); + } + + [Test] + public async Task ReadsTheLetterlikeSymbolsOfMathType() + { + var identifiers = Read("npoi/symbols.eqn").ToMathML().Descendants(Mml + "mi").Select(e => e.Value).ToList(); + await Assert.That(identifiers).Contains("ℓ"); + await Assert.That(identifiers).Contains("℘"); + await Assert.That(identifiers).DoesNotContain("𝓁"); + await Assert.That(identifiers).DoesNotContain("𝒫"); + } + /// /// The spaces of Equation Editor 3.0 (0xEB00 to 0xEB08 in the space typeface), also in MathType equations first /// written with it, are spaces, not the arrows of MathType's encoding with the same codes. diff --git a/tests/SharpEquations.Tests/UnicodeMath/UnicodeMathTests.cs b/tests/SharpEquations.Tests/UnicodeMath/UnicodeMathTests.cs index deb1049..dd0fbaa 100644 --- a/tests/SharpEquations.Tests/UnicodeMath/UnicodeMathTests.cs +++ b/tests/SharpEquations.Tests/UnicodeMath/UnicodeMathTests.cs @@ -16,7 +16,7 @@ public class UnicodeMathTests [Arguments(@"a=b#(1)", @"𝑎=𝑏#(1)", @"a=b\tag{1}")] [Arguments(@"⒱(a&b@c&d)", @"⒱(𝑎&𝑏@𝑐&𝑑)", @"\begin{vmatrix}a&b\\c&d\end{vmatrix}")] [Arguments(@"⬍x+⬆y+⬇z+⬌w", @"⬍𝑥+⬆𝑦+⬇𝑧+⬌𝑤", @"\smash{x}+\smash[t]{y}+\smash[b]{z}+\mathrlap{w}")] - [Arguments(@"▭(1&x)+▭(2&x)+▭(4&x)+▭(8&x)+▭(16&x)+▭(32&x)", @"▭(1&𝑥)+▭(2&𝑥)+▭(4&𝑥)+▭(8&𝑥)+▭(16&𝑥)+▭(32&𝑥)", @"\boxed{x}+\boxed{x}+\boxed{x}+\boxed{x}+\boxed{x}+\boxed{x}")] + [Arguments(@"▭(1&x)+▭(2&x)+▭(4&x)+▭(8&x)+▭(16&x)+▭(32&x)", @"▭(1&𝑥)+▭(2&𝑥)+▭(4&𝑥)+▭(8&𝑥)+▭(16&𝑥)+▭(32&𝑥)", @"\enclose{right,left,bottom}{x}+\enclose{right,left,top}{x}+\enclose{right,bottom,top}{x}+\enclose{left,bottom,top}{x}+\enclose{horizontalstrike,right,left,bottom,top}{x}+\enclose{verticalstrike,right,left,bottom,top}{x}")] [Arguments(@"_a^b x", @"_𝑎^𝑏 𝑥", @"{}_a^bx")] [Arguments(@"_a x", @"_𝑎 𝑥", @"{}_ax")] [Arguments(@"^b x", @"^𝑏 𝑥", @"{}^bx")] diff --git a/tests/data/README.md b/tests/data/README.md index 52df365..6021a41 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -43,6 +43,12 @@ Thilo Planz, and Javier Fernandez. `mtef/go`: two "Equation Native" streams extracted from MathType OLE objects supplied by the user and attributed to the Go project in the pasted source. `expected-MathML/` contains the corresponding MathML produced by SharpEquations. +`mtef/npoi`: seven "Equation Native" streams (`*.eqn`) of the MathType objects of `equations-ole-all.doc` of the test +data of NPOI (`testcases/test-data/document`), one paragraph of MathType's palettes each: `overlines` (horizontal +strikes), `operators`, `symbols` (ℓ, ℑ, ℜ, ℘, ℏ, ℵ...), `fraction-and-rad` (with two long divisions), `bra-ket`, +`parentheses` and `nested-fences` (angle brackets as MTEF writes them, U+2329 and U+232A), extracted from the +`ObjectPool` of the document. + `mtef/mtef-rs`: the "Equation Native" stream extracted from the MathType OLE object with the duplicate filename, supplied by the user and attributed to [hiro93/mtef-rs](https://github.com/hiro93/mtef-rs). `expected-MathML/` contains the MathML produced by SharpEquations. diff --git a/tests/data/mtef/npoi/bra-ket.eqn b/tests/data/mtef/npoi/bra-ket.eqn new file mode 100644 index 0000000000000000000000000000000000000000..9630b246e3ffee176173b581dbfa8207f85fd797 GIT binary patch literal 66 zcmb1PU|?Wk*l<`4NP~cbN)f}HNd;gwGb1B27Z)S5E&~IjP;(-fOadyOrm4&{jr|SN JG%aPIIsht#3A_LR literal 0 HcmV?d00001 diff --git a/tests/data/mtef/npoi/fraction-and-rad.eqn b/tests/data/mtef/npoi/fraction-and-rad.eqn new file mode 100644 index 0000000000000000000000000000000000000000..d2ea51b2dbf950454d660461bb4b8bf1ae4b0e35 GIT binary patch literal 188 zcmb1PU|?Wk*l>6OkOlz>l_Ca~DFt9QGb1B27Z)Ql9|HrUP;(-fOajU<^D#1Tqlj=Z tYoe>sWWrR#h-MBiOcS>t0~a$dOd4c1$Q~|c4VaK11G5Iqeu&LLc>wjE6%+sf literal 0 HcmV?d00001 diff --git a/tests/data/mtef/npoi/nested-fences.eqn b/tests/data/mtef/npoi/nested-fences.eqn new file mode 100644 index 0000000000000000000000000000000000000000..35ff0dd2301355fa49cdad70c145679ae4e8f1ad GIT binary patch literal 263 zcmW;HEe--P5QgEe9bkWgT7|%2SQPgmEA|(#APH&(oS^664jAqNA;={dzL_SIzE4dv za{wN<`;)hx$K{CW>|iaO6LLcB!ZfW|$;*fIMRlr(id9=xzVKnSpmwSg=L~AE P(jN*2*I_L=x$gQ0Y|1jf literal 0 HcmV?d00001 diff --git a/tests/data/mtef/npoi/operators.eqn b/tests/data/mtef/npoi/operators.eqn new file mode 100644 index 0000000000000000000000000000000000000000..27d570cd93e817a9afb3078225a259b20b528ecb GIT binary patch literal 133 zcmb1PU|?Wk*l;)#NQ1x!knURs1i;K~yIgx=)fPulNISEQ9gJ}i^00K-0 A)Bpeg literal 0 HcmV?d00001 diff --git a/tests/data/mtef/npoi/parentheses.eqn b/tests/data/mtef/npoi/parentheses.eqn new file mode 100644 index 0000000000000000000000000000000000000000..2bf89d597e58e88ed4c68cc397944ff57ce10707 GIT binary patch literal 337 zcmYL_F%H5o3`Kt&P$6Y$CnUOcp-7z&f)j9wiUE2C#DJK3hn#~0uyLK90r4G>9LbTN zpJOGd00^G%onyka+}`jyUQ_O!^C38&1Fjs-@Czn>rCaqcA*bnqAo1`)qD!JZN#dGu z0?L6!i5;T>IQBO$H4sHHb*S}bJ4Lgcf=vrSDjyCF`@W+Tz;j5swrS2P=l at)NJsz)NSNM4i{B6TEoAchy?ww|xO>K`~1J literal 0 HcmV?d00001 diff --git a/tests/data/mtef/npoi/symbols.eqn b/tests/data/mtef/npoi/symbols.eqn new file mode 100644 index 0000000000000000000000000000000000000000..d348945ef12b31f88ec34885b84746a79bf9e119 GIT binary patch literal 209 zcmXZWs}90I5QX8{tsto>9ZW?-fZ)(P5R_}er4(?uNibDtG`@x>fCu9**)`wJnGq)! z5iiZ>jqS_EmE0cX{~tLQ#j#VHiNE`i+8jGrU>_Hl4%L>J;|iy^#sT{czHp0Q+=;Jy q?DxzW9}$S2%aAwP%w67nmi?Au-m(yQ)bLG2$?_blN6 literal 0 HcmV?d00001