Skip to content
Merged
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
108 changes: 108 additions & 0 deletions CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,114 @@ public void ControlWordDoesNotConsumeFollowingStar() {
Assert.Equal(@"\sin *", LaTeXParser.MathListToLaTeX(list).ToString());
}

[Theory]
[InlineData(@"\not=", "≠", @"\neq ")]
[InlineData(@"\not<", "≮", @"\nless ")]
[InlineData(@"\not>", "≯", @"\ngtr ")]
[InlineData(@"\not\leq", "≰", @"\nleq ")]
[InlineData(@"\not\le", "≰", @"\nleq ")]
[InlineData(@"\not\leqslant", "⩽\u0338", @"\nleqslant ")]
[InlineData(@"\not\leqq", "≦\u0338", @"\nleqq ")]
[InlineData(@"\not\in", "∉", @"\notin ")]
[InlineData(@"\not\geq", "≱", @"\ngeq ")]
[InlineData(@"\not\ge", "≱", @"\ngeq ")]
[InlineData(@"\not\geqslant", "⩾\u0338", @"\ngeqslant ")]
[InlineData(@"\not\geqq", "≧\u0338", @"\ngeqq ")]
[InlineData(@"\not\prec", "⊀", @"\nprec ")]
[InlineData(@"\not\preceq", "⪯\u0338", @"\npreceq ")]
[InlineData(@"\not\preccurlyeq", "⋠", @"\npreccurlyeq ")]
[InlineData(@"\not\sim", "≁", @"\nsim ")]
[InlineData(@"\not\mid", "∤", @"\nshortmid ")]
[InlineData(@"\not\shortmid", "∤", @"\nshortmid ")]
[InlineData(@"\not\vdash", "⊬", @"\nvdash ")]
[InlineData(@"\not\vDash", "⊭", @"\nvDash ")]
[InlineData(@"\not\Vdash", "⊮", @"\nVdash ")]
[InlineData(@"\not\subseteq", "⊈", @"\nsubseteq ")]
[InlineData(@"\not\supseteq", "⊉", @"\nsupseteq ")]
[InlineData(@"\not\succ", "⊁", @"\nsucc ")]
[InlineData(@"\not\succeq", "⪰\u0338", @"\nsucceq ")]
[InlineData(@"\not\succcurlyeq", "⋡", @"\nsucccurlyeq ")]
[InlineData(@"\not\parallel", "∦", @"\nshortparallel ")]
[InlineData(@"\not\shortparallel", "∦", @"\nshortparallel ")]
[InlineData(@"\not\vartriangleleft", "⋪", @"\ntriangleleft ")]
[InlineData(@"\not\trianglelefteq", "⋬", @"\ntrianglelefteq ")]
[InlineData(@"\not\vartriangleright", "⋫", @"\ntriangleright ")]
[InlineData(@"\not\trianglerighteq", "⋭", @"\ntrianglerighteq ")]
[InlineData(@"\not\cong", "≇", @"\ncong ")]
[InlineData(@"\not\gets", "↚", @"\nleftarrow ")]
[InlineData(@"\not\leftarrow", "↚", @"\nleftarrow ")]
[InlineData(@"\not\Leftarrow", "⇍", @"\nLeftarrow ")]
[InlineData(@"\not\rightarrow", "↛", @"\nrightarrow ")]
[InlineData(@"\not\to", "↛", @"\nrightarrow ")]
[InlineData(@"\not\Rightarrow", "⇏", @"\nRightarrow ")]
[InlineData(@"\not\leftrightarrow", "↮", @"\nleftrightarrow ")]
[InlineData(@"\not\Leftrightarrow", "⇎", @"\nLeftrightarrow ")]
public void NotNegatesRelation(string input, string nucleus, string output) {
var list = ParseLaTeX(input);

var relation = Assert.IsType<Relation>(Assert.Single(list));
Assert.Equal(nucleus, relation.Nucleus);
Assert.Equal(output, LaTeXParser.MathListToLaTeX(list).ToString());
}

[Theory]
[InlineData(@"\not x")]
[InlineData(@"\not\frac12")]
public void NotRejectsNonRelation(string input) {
var parser = new LaTeXParser(input);
var (_, error) = parser.Build();

Assert.Contains(@"\not must be followed by a relation", error);
}

[Theory]
[InlineData(@"\not\approx", "≉", @"\not \approx ")]
[InlineData(@"\not\equiv", "≢", @"\not \equiv ")]
[InlineData(@"\not\subset", "⊄", @"\not \subset ")]
[InlineData(@"\not\ni", "∌", @"\not \ni ")]
public void NotUsesUnicodePrecomposedNegationWhenAvailable(string input, string nucleus, string output) {
var list = ParseLaTeX(input);

var relation = Assert.IsType<Relation>(Assert.Single(list));
Assert.Equal(nucleus, relation.Nucleus);
var serialized = LaTeXParser.MathListToLaTeX(list).ToString();
Assert.Equal(output, serialized);
Assert.Equal(list, ParseLaTeX(serialized));
}

[Fact]
public void NotUsesCombiningOverlayWhenNoPrecomposedNegationExists() {
var list = ParseLaTeX(@"\not\propto");
var relation = Assert.IsType<Relation>(Assert.Single(list));
Assert.Equal("∝\u0338", relation.Nucleus);
var serialized = LaTeXParser.MathListToLaTeX(list).ToString();
Assert.Equal(@"\not \propto ", serialized);
Assert.Equal(list, ParseLaTeX(serialized));
}

[Fact]
public void NotSerializesScriptsOnOuterRelation() {
var list = ParseLaTeX(@"\not\propto^2");

var relation = Assert.IsType<Relation>(Assert.Single(list));
Assert.Equal("∝\u0338", relation.Nucleus);
Assert.Equal("2", Assert.Single(relation.Superscript).Nucleus);
var serialized = LaTeXParser.MathListToLaTeX(list).ToString();
Assert.Equal(@"\not \propto ^2", serialized);
Assert.Equal(list, ParseLaTeX(serialized));
}

[Fact]
public void NotRepeatedNegationRoundTrips() {
var list = ParseLaTeX(@"\not\not=");

var relation = Assert.IsType<Relation>(Assert.Single(list));
Assert.Equal("≠\u0338", relation.Nucleus);
var serialized = LaTeXParser.MathListToLaTeX(list).ToString();
Assert.Equal(@"\not \neq ", serialized);
Assert.Equal(list, ParseLaTeX(serialized));
}

/// 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) })]
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 46 additions & 1 deletion CSharpMath.Rendering.Tests/TestCommandDisplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CSharpMath.Atom;
using CSharpMath.Display;
using CSharpMath.Display.Displays;
using CSharpMath.Display.FrontEnd;
using Xunit;

namespace CSharpMath.Rendering.Tests {
Expand All @@ -20,5 +24,46 @@ public TestCommandDisplay() =>
[MemberData(nameof(AllCommandValues))]
public void CommandsAreDisplayable(Rune ch) =>
Assert.Contains(typefaces, font => font.GetGlyphIndex(ch.Value) != 0);

[Fact]
public void NotProportionalToOverlaysProportionalTo() {
var fonts = new Fonts(Array.Empty<Typography.OpenFont.Typeface>(), 20);
var negatedLine = Assert.Single(ParseLine(@"\not\propto", fonts).Displays);
var baseLine = Assert.Single(ParseLine(@"\propto", fonts).Displays);
Assert.IsType<TextLineDisplay<Fonts, Glyph>>(negatedLine);
Assert.IsType<TextLineDisplay<Fonts, Glyph>>(baseLine);
var negatedTextLine = (TextLineDisplay<Fonts, Glyph>)negatedLine;
var baseTextLine = (TextLineDisplay<Fonts, Glyph>)baseLine;
var negatedRun = Assert.Single(negatedTextLine.Runs);
Assert.Single(baseTextLine.Runs);
Assert.Equal(2, negatedRun.Run.Length);

var expectedBase = GlyphFinder.Instance.Lookup(fonts, 0x221D);
var expectedOverlay = GlyphFinder.Instance.Lookup(fonts, 0x0338);
Assert.Equal(expectedBase.Info.GlyphIndex, negatedRun.Run.GlyphInfos[0].Glyph.Info.GlyphIndex);
Assert.Same(expectedBase.Typeface, negatedRun.Run.GlyphInfos[0].Glyph.Typeface);
Assert.Equal(expectedOverlay.Info.GlyphIndex, negatedRun.Run.GlyphInfos[1].Glyph.Info.GlyphIndex);
Assert.Same(expectedOverlay.Typeface, negatedRun.Run.GlyphInfos[1].Glyph.Typeface);

var advances = GlyphBoundsProvider.Instance.GetAdvancesForGlyphs(
fonts, negatedRun.Run.Glyphs, negatedRun.Run.Length).Advances.ToArray();
Assert.Equal(0, advances[1]);
Assert.Equal(baseTextLine.Width, negatedTextLine.Width);

var bounds = GlyphBoundsProvider.Instance.GetBoundingRectsForGlyphs(
fonts, negatedRun.Run.Glyphs, negatedRun.Run.Length).ToArray();
var baseInk = bounds[0];
var overlayInk = bounds[1];
overlayInk.Offset(advances[0], 0);
Assert.True(overlayInk.IntersectsWith(baseInk));
}

static ListDisplay<Fonts, Glyph> ParseLine(string latex, Fonts fonts) {
var result = Atom.LaTeXParser.MathListFromLaTeX(latex);
Assert.Null(result.Error);
return Assert.IsType<ListDisplay<Fonts, Glyph>>(
Typesetter.CreateLine(result.Match(list => list, _ => throw new InvalidOperationException()),
fonts, TypesettingContext.Instance, LineStyle.Display));
}
}
}
}
5 changes: 4 additions & 1 deletion CSharpMath.Rendering.Tests/TestRenderingMathData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ public sealed class TestRenderingMathData : TestRenderingSharedData<TestRenderin
//public const string MiddleDelimiter = @"A = \left\{ \frac{x_i}{i} \middle| i\in \mathcal{I} \right\}";

public const string Nothing = @"";
public const string NotEquals = @"\not=";
public const string NotApproximately = @"\not\approx";
public const string NotProportionalTo = @"\not\propto";
public const string Overline = @"\overline{Overline}";
public const string Pi = @"\pi";
public const string Phi = @"\phi";
Expand Down Expand Up @@ -135,4 +138,4 @@ public sealed class TestRenderingMathData : TestRenderingSharedData<TestRenderin

public const string VectorProjection = @"Proj_\vec{v}\vec{u}=|\vec u|\cos\theta\times\frac\vec v{|\vec v|}=|\vec u|\frac{\vec u \cdot \vec v}{|\vec u||\vec v|}\times\frac\vec v{|\vec v|}\\\text{Suppose \mathit{u} and \mathit v are unit vectors, }Proj_\vec v\vec u = (\vec u\cdot\vec v)\vec v";
}
}
}
17 changes: 16 additions & 1 deletion CSharpMath/Atom/LaTeXParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,21 @@ static bool MathAtomToLaTeX(MathAtom atom, StringBuilder builder,
MathListToLaTeX(r.InnerList, builder, currentFontStyle);
builder.Append('}');
break;
case Relation { Nucleus: { } nucleus }
when LaTeXSettings.CommandForAtom(atom) is null
&& nucleus.Normalize(NormalizationForm.FormD) is { } decomposed
&& decomposed.Length > 0 && decomposed[decomposed.Length - 1] == '\u0338':
builder.Append(@"\not ");
var baseRelation = new Relation(
decomposed.Substring(0, decomposed.Length - 1).Normalize(NormalizationForm.FormC));
if (LaTeXSettings.CommandForAtom(baseRelation) is string baseCommand) {
builder.Append(baseCommand);
if (baseCommand.AsSpan().StartsWithInvariant(@"\"))
builder.Append(' ');
} else {
builder.Append(baseRelation.Nucleus);
}
break;
case var _ when MathAtomToLaTeX(atom, builder, out _):
break;
case Atoms.Space space:
Expand Down Expand Up @@ -731,4 +746,4 @@ public static StringBuilder MathListToLaTeX(MathList mathList, StringBuilder? sb
return sb;
}
}
}
}
46 changes: 45 additions & 1 deletion CSharpMath/Atom/LaTeXSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,32 @@ namespace CSharpMath.Atom {
using Atoms;
//https://mirror.hmc.edu/ctan/macros/latex/contrib/unicode-math/unimath-symbols.pdf
public static class LaTeXSettings {
// Unlike LaTeX, where \not is a relation glyph that overstrikes the next
// atom (see the kernel's \neq/\ne definitions and \not declaration:
// https://github.com/latex3/latex2e/blob/develop/base/fontdef.dtx#L1122-L1128,
// https://github.com/latex3/latex2e/blob/develop/base/fontdef.dtx#L1167-L1170),
// CSharpMath consumes one Relation and chooses the best Unicode negation.
// This follows UTR #25's preference for precomposed operators, using U+0338
// only when no precomposed form exists: https://www.unicode.org/reports/tr25/.
static readonly IReadOnlyDictionary<string, string> NegatedRelationCommands =
new Dictionary<string, string> {
["="] = @"\neq", ["<"] = @"\nless", [">"] = @"\ngtr",
[@"\in"] = @"\notin", [@"\leq"] = @"\nleq", [@"\leqslant"] = @"\nleqslant",
[@"\leqq"] = @"\nleqq", [@"\prec"] = @"\nprec", [@"\preceq"] = @"\npreceq",
[@"\preccurlyeq"] = @"\npreccurlyeq",
[@"\sim"] = @"\nsim", [@"\mid"] = @"\nmid", [@"\shortmid"] = @"\nshortmid",
[@"\vdash"] = @"\nvdash", [@"\vDash"] = @"\nvDash", [@"\Vdash"] = @"\nVdash",
[@"\vartriangleleft"] = @"\ntriangleleft", [@"\trianglelefteq"] = @"\ntrianglelefteq",
[@"\subseteq"] = @"\nsubseteq", [@"\geq"] = @"\ngeq", [@"\geqslant"] = @"\ngeqslant",
[@"\geqq"] = @"\ngeqq", [@"\succ"] = @"\nsucc", [@"\succeq"] = @"\nsucceq",
[@"\succcurlyeq"] = @"\nsucccurlyeq",
[@"\parallel"] = @"\nparallel", [@"\shortparallel"] = @"\nshortparallel",
[@"\vartriangleright"] = @"\ntriangleright", [@"\trianglerighteq"] = @"\ntrianglerighteq",
[@"\supseteq"] = @"\nsupseteq", [@"\cong"] = @"\ncong",
[@"\leftarrow"] = @"\nleftarrow", [@"\Leftarrow"] = @"\nLeftarrow",
[@"\rightarrow"] = @"\nrightarrow", [@"\Rightarrow"] = @"\nRightarrow",
[@"\leftrightarrow"] = @"\nleftrightarrow", [@"\Leftrightarrow"] = @"\nLeftrightarrow",
};
static readonly Dictionary<Boundary, string> boundaryDelimitersReverse = new Dictionary<Boundary, string>();
public static IReadOnlyDictionary<Boundary, string> BoundaryDelimitersReverse => boundaryDelimitersReverse;
public static LaTeXCommandDictionary<Boundary> BoundaryDelimiters { get; } =
Expand Down Expand Up @@ -193,6 +219,24 @@ public static class LaTeXSettings {
Ok(new RaiseBox(raise, innerList)));
});
} },
{ @"\not", (parser, accumulate, stopChar) =>
parser.ReadArgument().Bind(target => {
if (target.Count != 1 || target[0] is not Relation relation
|| relation.Subscript.IsNonEmpty() || relation.Superscript.IsNonEmpty())
return Err(@"\not must be followed by a relation");

// Prefer an explicit, semantically exact named negation. In particular,
// do not infer names such as \lneq or \lnapprox: those are refinements,
// not simply \not applied to the positive relation.
var command = CommandForAtom(relation);
if (command != null && NegatedRelationCommands.TryGetValue(command, out var negatedCommand)
&& AtomForCommand(negatedCommand) is Relation negated)
return Ok(negated);

// UTR #25 recommends a precomposed operator where Unicode provides
// one, and the combining long solidus otherwise.
return Ok(new Relation((relation.Nucleus + "\u0338").Normalize(NormalizationForm.FormC)));
}) },
{ @"\operatorname", (parser, accumulate, stopChar) => {
if (!parser.ReadCharIfAvailable('{')) return "Expected {";
// An operator name is a word, so letters -- but a letter may be written as a command:
Expand Down Expand Up @@ -1189,4 +1233,4 @@ atom is Accent accent
// \varsupsetneqq -> ⫌ + U+FE00 (Variation Selector 1) Not dealing with variation selectors, thank you very much
};
}
}
}
Loading