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
10 changes: 7 additions & 3 deletions CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1108,10 +1108,14 @@ public void TestFont(string inputCommand, bool readsToEnd, FontStyle style, stri
public void TestFontRecursive(string input) {
var list = ParseLaTeX(input);
Assert.Collection(list,
CheckAtom<Variable>("x", variable => Assert.Equal(FontStyle.Roman, variable.FontStyle)),
CheckAtom<Variable>("x", variable => {
Assert.Equal(FontStyle.Roman, variable.FontStyle);
Assert.Equal(FontFamily.Roman, variable.TextStyle.Family);
Assert.Equal(FontPosture.Italic, variable.TextStyle.Posture);
}),
CheckAtom<Variable>("y", variable => Assert.Equal(FontStyle.Default, variable.FontStyle))
);
Assert.Equal(@"\mathrm{x}y", LaTeXParser.MathListToLaTeX(list).ToString());
Assert.Equal(@"\mathrm{\mathit{x}}y", LaTeXParser.MathListToLaTeX(list).ToString());
}

[Fact]
Expand Down Expand Up @@ -1634,4 +1638,4 @@ public void TestErrorSurrogates() {
}
}
}
}
}
104 changes: 104 additions & 0 deletions CSharpMath.Core.Tests/Atom/TextStyleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using CSharpMath.Atom;
using CSharpMath.Atom.Atoms;
using Xunit;

namespace CSharpMath.Core.AtomTests {
public class TextStyleTests {
[Theory]
[InlineData(FontStyle.Default)]
[InlineData(FontStyle.Roman)]
[InlineData(FontStyle.Bold)]
[InlineData(FontStyle.Caligraphic)]
[InlineData(FontStyle.Typewriter)]
[InlineData(FontStyle.Italic)]
[InlineData(FontStyle.SansSerif)]
[InlineData(FontStyle.Fraktur)]
[InlineData(FontStyle.Blackboard)]
[InlineData(FontStyle.BoldItalic)]
public void LegacyStylesRoundTripExactly(FontStyle legacy) {
var style = TextStyle.FromFontStyle(legacy);
Assert.True(style.TryGetFontStyle(out var roundTrip));
Assert.Equal(legacy, roundTrip);
Assert.Equal(legacy, style.ToFontStyle());
}

[Fact]
public void IndependentAxesComposeWithoutErasingEachOther() {
var style = TextStyleChange.FromFontStyleCommand(FontStyle.SansSerif).ApplyTo(TextStyle.Default);
style = TextStyleChange.FromFontStyleCommand(FontStyle.Bold).ApplyTo(style);
style = style.WithPosture(FontPosture.Slanted).WithCapitals(FontCapitals.SmallCapitals);

Assert.Equal(FontFamily.SansSerif, style.Family);
Assert.Equal(FontWeight.Bold, style.Weight);
Assert.Equal(FontPosture.Slanted, style.Posture);
Assert.Equal(FontCapitals.SmallCapitals, style.Capitals);
Assert.False(style.TryGetFontStyle(out _));
Assert.Equal(FontStyle.SansSerif, style.ToFontStyle());
}

[Fact]
public void NestedMathCommandsComposeRestoreAndRoundTrip() {
var parsed = LaTeXParserTest.ParseLaTeX(@$"a\mathsf{{\mathbf{{\mathit{{b}}}}c}}d");

Assert.Equal(TextStyle.Default, parsed[0].TextStyle);
Assert.Equal(new TextStyle(FontFamily.SansSerif, FontWeight.Bold, FontPosture.Italic, FontCapitals.Normal),
parsed[1].TextStyle);
Assert.Equal(new TextStyle(FontFamily.SansSerif, FontWeight.Regular, FontPosture.Upright, FontCapitals.Normal),
parsed[2].TextStyle);
Assert.Equal(TextStyle.Default, parsed[3].TextStyle);

var serialized = LaTeXParser.MathListToLaTeX(parsed).ToString();
Assert.Equal(@$"a\mathsf{{\mathbf{{\mathit{{b}}}}}}\mathsf{{c}}d", serialized);
Assert.Equal(parsed, LaTeXParserTest.ParseLaTeX(serialized));
}

[Fact]
public void CloneAndEqualityPreserveSemanticRequests() {
var original = new Variable("x") {
TextStyle = new TextStyle(FontFamily.Roman, FontWeight.Bold, FontPosture.Slanted, FontCapitals.SmallCapitals)
};
var clone = original.Clone(false);
var different = new Variable("x") { TextStyle = original.TextStyle.WithCapitals(FontCapitals.Normal) };

Assert.Equal(original, clone);
Assert.Equal(original.TextStyle, clone.TextStyle);
Assert.NotEqual(original, different);
Assert.Equal("x", original.Nucleus);
}

[Fact]
public void LegacyPropertyRemainsACompatibilityLayer() {
var atom = new Variable("x") { FontStyle = FontStyle.BoldItalic };
Assert.Equal(FontStyle.BoldItalic, atom.FontStyle);
Assert.Equal(FontWeight.Bold, atom.TextStyle.Weight);
Assert.Equal(FontPosture.Italic, atom.TextStyle.Posture);
}

[Fact]
public void InvalidSemanticAxesAreRejected() {
Assert.Throws<ArgumentOutOfRangeException>(() => new TextStyle((FontFamily)99, FontWeight.Regular, FontPosture.Upright, FontCapitals.Normal));
Assert.Throws<ArgumentOutOfRangeException>(() => TextStyle.Default.WithWeight((FontWeight)99));
Assert.Throws<ArgumentOutOfRangeException>(() => TextStyle.Default.WithPosture((FontPosture)99));
Assert.Throws<ArgumentOutOfRangeException>(() => TextStyle.Default.WithCapitals((FontCapitals)99));
Assert.Throws<ArgumentOutOfRangeException>(() => new TextStyleChange((FontFamily)99, null, null, null));
Assert.Throws<ArgumentOutOfRangeException>(() => new TextStyleChange(null, (FontWeight)99, null, null));
}

[Fact]
public void StyleScopeRestoresStateAfterMalformedArgument() {
var parser = new LaTeXParser(@"\mathbf{");
Assert.NotNull(parser.Build().Error);
Assert.Equal(TextStyle.Default, parser.CurrentTextStyle);
Assert.False(parser.TextMode);
}

[Fact]
public void UnsupportedAxesFailSerializationRatherThanProjecting() {
var atom = new Variable("x") {
TextStyle = TextStyle.Default.WithPosture(FontPosture.Slanted).WithCapitals(FontCapitals.SmallCapitals)
};
Assert.Throws<InvalidOperationException>(() => LaTeXParser.MathListToLaTeX(new MathList(atom)));
}
}
}
55 changes: 53 additions & 2 deletions CSharpMath.Rendering.Text.Tests/TextLaTeXParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,62 @@ public void StyleDefault() {
var input = @"\textnormal abc";
var atom = Parse(input);
Assert.Equal(new TextAtom.List(new TextAtom[] {
new TextAtom.Style(new TextAtom.Text("a"), Atom.FontStyle.Roman),
new TextAtom.Style(new TextAtom.Text("a"), new Atom.TextStyleChange(
Atom.FontFamily.Roman, Atom.FontWeight.Regular, Atom.FontPosture.Upright, Atom.FontCapitals.Normal)),
new TextAtom.Text("bc"),
}), atom);
Assert.Equal(@"\textrm{a}bc", TextLaTeXParser.TextAtomToLaTeX(atom).ToString());
}
[Fact]
public void NestedStylesComposeOnlyTheirOwnAxes() {
const string input = @"\textsf{\textbf{\textit{x}}y}z";
var root = Assert.IsType<TextAtom.List>(Parse(input));
var sans = Assert.IsType<TextAtom.Style>(root.Content[0]);
var sansContent = Assert.IsType<TextAtom.List>(sans.Content);
var bold = Assert.IsType<TextAtom.Style>(sansContent.Content[0]);
var italic = Assert.IsType<TextAtom.Style>(bold.Content);
var text = Assert.IsType<TextAtom.Text>(italic.Content);

var normal = Atom.TextStyle.Default.WithFamily(Atom.FontFamily.Roman);
var sansStyle = sans.StyleChange.ApplyTo(normal);
var boldStyle = bold.StyleChange.ApplyTo(sansStyle);
var italicStyle = italic.StyleChange.ApplyTo(boldStyle);
Assert.Equal(Atom.FontFamily.SansSerif, italicStyle.Family);
Assert.Equal(Atom.FontWeight.Bold, italicStyle.Weight);
Assert.Equal(Atom.FontPosture.Italic, italicStyle.Posture);
Assert.Equal("x", text.Content);
Assert.Equal(input, TextLaTeXParser.TextAtomToLaTeX(root).ToString());
}
[Fact]
public void SlantedSmallCapRequestPreservesSourceAndStructuralEquality() {
var change = new Atom.TextStyleChange(null, null,
Atom.FontPosture.Slanted, Atom.FontCapitals.SmallCapitals);
var styled = new TextAtom.Style(new TextAtom.Text("lowercase"), change);
var same = new TextAtom.Style(new TextAtom.Text("lowercase"), change);
var italic = new TextAtom.Style(new TextAtom.Text("lowercase"),
new Atom.TextStyleChange(null, null, Atom.FontPosture.Italic, Atom.FontCapitals.SmallCapitals));

Assert.Equal(styled, same);
Assert.NotEqual(styled, italic);
Assert.Equal("lowercase", Assert.IsType<TextAtom.Text>(styled.Content).Content);
Assert.Equal(Atom.FontPosture.Slanted, styled.StyleChange.Posture);
Assert.Equal(Atom.FontCapitals.SmallCapitals, styled.StyleChange.Capitals);
}

[Fact]
public void MultiAxisStyleSerializesWithoutLegacyProjectionLoss() {
var change = new Atom.TextStyleChange(
Atom.FontFamily.SansSerif, Atom.FontWeight.Bold, Atom.FontPosture.Italic, null);
var atom = new TextAtom.Style(new TextAtom.Text("x"), change);
Assert.Equal(@"\textsf{\textbf{\textit{x}}}", TextLaTeXParser.TextAtomToLaTeX(atom).ToString());
}

[Fact]
public void UnsupportedTextAxesFailSerializationExplicitly() {
var atom = new TextAtom.Style(new TextAtom.Text("x"), new Atom.TextStyleChange(
null, null, Atom.FontPosture.Slanted, Atom.FontCapitals.SmallCapitals));
Assert.Throws<InvalidOperationException>(() => TextLaTeXParser.TextAtomToLaTeX(atom));
}
[Theory]
[InlineData(@"\! ", -3, true)]
[InlineData(@"\, ", 3, true)]
Expand Down Expand Up @@ -504,4 +555,4 @@ public void Error(string badInput, string expected) {
Assert.Equal(expected.Replace("\r", null), actual);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ CSharpMath.Rendering.Text.TextAtom.RelativeSize.RelativeSize(CSharpMath.Renderin
CSharpMath.Rendering.Text.TextAtom.RelativeSize.Content.get -> CSharpMath.Rendering.Text.TextAtom!
CSharpMath.Rendering.Text.TextAtom.RelativeSize.Declaration.get -> string!
CSharpMath.Rendering.Text.TextAtom.RelativeSize.Magnification.get -> float
CSharpMath.Rendering.Text.TextAtom.Style.Style(CSharpMath.Rendering.Text.TextAtom! content, CSharpMath.Atom.TextStyleChange styleChange) -> void
CSharpMath.Rendering.Text.TextAtom.Style.StyleChange.get -> CSharpMath.Atom.TextStyleChange
CSharpMath.Rendering.Text.TextAtomListBuilder.Style(CSharpMath.Rendering.Text.TextAtom! atom, CSharpMath.Atom.TextStyleChange styleChange) -> void
override CSharpMath.Rendering.Text.TextAtom.RelativeSize.SingleChar(CSharpMath.Atom.FontStyle style) -> int?
override CSharpMath.Rendering.Text.TextAtom.RelativeSize.Equals(CSharpMath.Rendering.Text.TextAtom! atom) -> bool
override CSharpMath.Rendering.Text.TextAtom.RelativeSize.GetHashCode() -> int
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ CSharpMath.Rendering.Text.TextAtom.RelativeSize.Magnification.get -> float
override CSharpMath.Rendering.Text.TextAtom.RelativeSize.SingleChar(CSharpMath.Atom.FontStyle style) -> int?
override CSharpMath.Rendering.Text.TextAtom.RelativeSize.Equals(CSharpMath.Rendering.Text.TextAtom! atom) -> bool
override CSharpMath.Rendering.Text.TextAtom.RelativeSize.GetHashCode() -> int
CSharpMath.Rendering.Text.TextAtom.Style.Style(CSharpMath.Rendering.Text.TextAtom! content, CSharpMath.Atom.TextStyleChange styleChange) -> void
CSharpMath.Rendering.Text.TextAtom.Style.StyleChange.get -> CSharpMath.Atom.TextStyleChange
CSharpMath.Rendering.Text.TextAtomListBuilder.Style(CSharpMath.Rendering.Text.TextAtom! atom, CSharpMath.Atom.TextStyleChange styleChange) -> void
48 changes: 38 additions & 10 deletions CSharpMath.Rendering/Text/TextAtom.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,23 @@

namespace CSharpMath.Rendering.Text {
using Atom;
static class TextStyleProjection {
internal static FontStyle ToFontStyleForText(this TextStyle style) {
if (style.Family == FontFamily.Roman &&
(style.Weight != FontWeight.Regular || style.Posture != FontPosture.Upright))
return (style.Weight, style.Posture) switch {
(FontWeight.Bold, FontPosture.Upright) => FontStyle.Bold,
(FontWeight.Regular, FontPosture.Italic or FontPosture.Slanted) => FontStyle.Italic,
(FontWeight.Bold, _) => FontStyle.BoldItalic,
_ => FontStyle.Roman
};
return style.ToFontStyle();
}
}
//Base type
public abstract class TextAtom : System.IEquatable<TextAtom> {
public abstract int? SingleChar(FontStyle style);
internal virtual int? SingleChar(TextStyle style) => SingleChar(style.ToFontStyleForText());
public abstract bool Equals(TextAtom a);
public override bool Equals(object obj) => obj is TextAtom a && Equals(a);
public abstract override int GetHashCode();
Expand Down Expand Up @@ -47,6 +61,7 @@ public Accent(TextAtom content, string accent) =>
public TextAtom Content { get; }
public string AccentChar { get; }
public override int? SingleChar(FontStyle style) => Content.SingleChar(style);
internal override int? SingleChar(TextStyle style) => Content.SingleChar(style);
public override bool Equals(TextAtom atom) => atom is Accent a && a.AccentChar == AccentChar && a.Content.Equals(Content);
public override int GetHashCode() => (AccentChar, Content).GetHashCode();
}
Expand All @@ -60,23 +75,32 @@ public Math(MathList content, bool displayStyle) =>
public override int GetHashCode() => (DisplayStyle, Content).GetHashCode();
}
public sealed class Style : TextAtom {
public Style(TextAtom content, FontStyle style) =>
(Content, FontStyle) =
(content, style == FontStyle.Default
//FontStyle.Default is FontStyle.Italic, FontStyle.Roman is no change to characters
? FontStyle.Roman
: style);
readonly FontStyle legacyFontStyle;
public Style(TextAtom content, FontStyle style) {
Content = content;
legacyFontStyle = style == FontStyle.Default ? FontStyle.Roman : style;
StyleChange = TextStyleChange.FromFontStyleCommand(
style == FontStyle.Default ? FontStyle.Roman : style);
}
public Style(TextAtom content, TextStyleChange styleChange) {
Content = content;
StyleChange = styleChange;
legacyFontStyle = styleChange.ApplyTo(TextStyle.Default).ToFontStyle();
}
public TextAtom Content { get; }
public FontStyle FontStyle { get; }
public override int? SingleChar(FontStyle style) => Content.SingleChar(FontStyle);
public override bool Equals(TextAtom atom) => atom is Style s && s.FontStyle == FontStyle && s.Content.Equals(Content);
public override int GetHashCode() => (FontStyle, Content).GetHashCode();
public FontStyle FontStyle => legacyFontStyle;
public TextStyleChange StyleChange { get; }
public override int? SingleChar(FontStyle style) => SingleChar(TextStyle.FromFontStyle(style));
internal override int? SingleChar(TextStyle style) => Content.SingleChar(StyleChange.ApplyTo(style));
public override bool Equals(TextAtom atom) => atom is Style s && s.StyleChange.Equals(StyleChange) && s.Content.Equals(Content);
public override int GetHashCode() => (StyleChange, Content).GetHashCode();
}
public sealed class Size : TextAtom {
public Size(TextAtom content, float pointSize) => (Content, PointSize) = (content, pointSize);
public TextAtom Content { get; }
public float PointSize { get; }
public override int? SingleChar(FontStyle style) => Content.SingleChar(style);
internal override int? SingleChar(TextStyle style) => Content.SingleChar(style);
public override bool Equals(TextAtom atom) => atom is Size s && s.PointSize == PointSize && s.Content.Equals(Content);
public override int GetHashCode() => (PointSize, Content).GetHashCode();
}
Expand All @@ -92,6 +116,7 @@ public RelativeSize(TextAtom content, string declaration) {
public string Declaration { get; }
public float Magnification { get; }
public override int? SingleChar(FontStyle style) => Content.SingleChar(style);
internal override int? SingleChar(TextStyle style) => Content.SingleChar(style);
public override bool Equals(TextAtom atom) => atom is RelativeSize s && s.Declaration == Declaration &&
s.Magnification == Magnification && s.Content.Equals(Content);
public override int GetHashCode() => (Declaration, Magnification, Content).GetHashCode();
Expand All @@ -101,6 +126,7 @@ public sealed class Colored : TextAtom {
public TextAtom Content { get; }
public System.Drawing.Color Colour { get; }
public override int? SingleChar(FontStyle style) => Content.SingleChar(style);
internal override int? SingleChar(TextStyle style) => Content.SingleChar(style);
public override bool Equals(TextAtom atom) => atom is Colored c && c.Colour == Colour && c.Content.Equals(Content);
public override int GetHashCode() => (Colour, Content).GetHashCode();
}
Expand All @@ -109,6 +135,8 @@ public sealed class List : TextAtom {
public IReadOnlyList<TextAtom> Content { get; }
public override int? SingleChar(FontStyle style) =>
Content.Count == 1 ? Content[0].SingleChar(style) : null;
internal override int? SingleChar(TextStyle style) =>
Content.Count == 1 ? Content[0].SingleChar(style) : null;
public override bool Equals(TextAtom atom) =>
atom is List l
&& l.Content.Count == Content.Count
Expand Down
1 change: 1 addition & 0 deletions CSharpMath.Rendering/Text/TextAtomListBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public void Text(string text) {
}
public void Space(Atom.Length space) => Add(new TextAtom.Space(space));
public void Style(TextAtom atom, Atom.FontStyle style) => Add(new TextAtom.Style(atom, style));
public void Style(TextAtom atom, Atom.TextStyleChange styleChange) => Add(new TextAtom.Style(atom, styleChange));
public void Size(TextAtom atom, float fontSize) => Add(new TextAtom.Size(atom, fontSize));
public void Color(TextAtom atom, Color color) => Add(new TextAtom.Colored(atom, color));
public Atom.Result Math(string mathLaTeX, bool displayStyle, int startAt, ref int endAt) {
Expand Down
Loading
Loading