diff --git a/src/Avalonia.Base/Animation/KeySpline.cs b/src/Avalonia.Base/Animation/KeySpline.cs index 218a7599d5..a21c9cd329 100644 --- a/src/Avalonia.Base/Animation/KeySpline.cs +++ b/src/Avalonia.Base/Animation/KeySpline.cs @@ -83,7 +83,7 @@ namespace Avalonia.Animation { culture ??= CultureInfo.InvariantCulture; - using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\"."); + using var tokenizer = new SpanStringTokenizer(value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\"."); return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); } diff --git a/src/Avalonia.Base/Animation/Spring.cs b/src/Avalonia.Base/Animation/Spring.cs index 3117a9c18a..21a707e3e4 100644 --- a/src/Avalonia.Base/Animation/Spring.cs +++ b/src/Avalonia.Base/Animation/Spring.cs @@ -61,7 +61,7 @@ internal class Spring culture = CultureInfo.InvariantCulture; } - using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid Spring string: \"{value}\"."); + using var tokenizer = new SpanStringTokenizer(value, culture, exceptionMessage: $"Invalid Spring string: \"{value}\"."); return new Spring(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); } diff --git a/src/Avalonia.Base/CornerRadius.cs b/src/Avalonia.Base/CornerRadius.cs index 44b45c2a73..f2c51eb4ee 100644 --- a/src/Avalonia.Base/CornerRadius.cs +++ b/src/Avalonia.Base/CornerRadius.cs @@ -94,7 +94,7 @@ namespace Avalonia { const string exceptionMessage = "Invalid CornerRadius."; - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage)) + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage)) { if (tokenizer.TryReadDouble(out var a)) { diff --git a/src/Avalonia.Base/Matrix.cs b/src/Avalonia.Base/Matrix.cs index 5f5aab4f47..69e3063ac0 100644 --- a/src/Avalonia.Base/Matrix.cs +++ b/src/Avalonia.Base/Matrix.cs @@ -504,7 +504,7 @@ namespace Avalonia double v8 = 0; double v9 = 0; - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix.")) + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix.")) { var v1 = tokenizer.ReadDouble(); var v2 = tokenizer.ReadDouble(); diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index 9a34eaf643..4f8376d267 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Text; using Avalonia.Platform; using Avalonia.Utilities; @@ -181,7 +182,7 @@ namespace Avalonia.Media.Fonts glyphTypeface = null; var weight = (int)key.Weight; - //If the target weight given is between 400 and 500 inclusive + //If the target weight given is between 400 and 500 inclusive if (weight >= 400 && weight <= 500) { //Look for available weights between the target and 500, in ascending order. @@ -212,7 +213,7 @@ namespace Avalonia.Media.Fonts } } - //If a weight less than 400 is given, look for available weights less than the target, in descending order. + //If a weight less than 400 is given, look for available weights less than the target, in descending order. if (weight < 400) { for (var i = 0; weight - i >= 100; i += 50) @@ -271,109 +272,54 @@ namespace Avalonia.Media.Fonts var weight = typeface.Weight; var stretch = typeface.Stretch; - if(TryGetStyle(ref normalizedFamilyName, out var foundStyle)) - { - style = foundStyle; - } - - if(TryGetWeight(ref normalizedFamilyName, out var foundWeight)) - { - weight = foundWeight; - } - - if(TryGetStretch(ref normalizedFamilyName, out var foundStretch)) - { - stretch = foundStretch; - } - - //Preserve old font source - return new Typeface(typeface.FontFamily, style, weight, stretch); - - } - - internal static bool TryGetWeight(ref string familyName, out FontWeight weight) - { - weight = FontWeight.Normal; + StringBuilder? normalizedFamilyNameBuilder = null; + var totalCharsRemoved = 0; - var tokenizer = new StringTokenizer(familyName, ' '); + var tokenizer = new SpanStringTokenizer(normalizedFamilyName, ' '); + // Skip initial family name. tokenizer.ReadSpan(); - while (tokenizer.TryReadString(out var weightString)) + while (tokenizer.TryReadSpan(out var token)) { - if (new StringTokenizer(weightString).TryReadInt32(out _)) + // Don't try to match numbers. + if (new SpanStringTokenizer(token).TryReadInt32(out _)) { continue; } - if (!Enum.TryParse(weightString, true, out weight)) - { - continue; - } - - familyName = familyName.Replace(" " + weightString, "").TrimEnd(); - - return true; - } - - return false; - } - - internal static bool TryGetStyle(ref string familyName, out FontStyle style) - { - style = FontStyle.Normal; - - var tokenizer = new StringTokenizer(familyName, ' '); - - tokenizer.ReadSpan(); - - while (tokenizer.TryReadString(out var styleString)) - { - //Do not try to parse an integer - if (new StringTokenizer(styleString).TryReadInt32(out _)) + // Try match with font style, weight or stretch and update accordingly. + var match = false; + if (EnumHelper.TryParse(token, true, out var newStyle)) { - continue; + style = newStyle; + match = true; } - - if (!Enum.TryParse(styleString, true, out style)) + else if (EnumHelper.TryParse(token, true, out var newWeight)) { - continue; + weight = newWeight; + match = true; } - - familyName = familyName.Replace(" " + styleString, "").TrimEnd(); - - return true; - } - - return false; - } - - internal static bool TryGetStretch(ref string familyName, out FontStretch stretch) - { - stretch = FontStretch.Normal; - - var tokenizer = new StringTokenizer(familyName, ' '); - - tokenizer.ReadSpan(); - - while (tokenizer.TryReadString(out var stretchString)) - { - if (new StringTokenizer(stretchString).TryReadInt32(out _)) + else if (EnumHelper.TryParse(token, true, out var newStretch)) { - continue; + stretch = newStretch; + match = true; } - if (!Enum.TryParse(stretchString, true, out stretch)) + if (match) { - continue; + // Carve out matched word from the normalized name. + normalizedFamilyNameBuilder ??= new StringBuilder(normalizedFamilyName); + normalizedFamilyNameBuilder.Remove(tokenizer.CurrentTokenIndex - totalCharsRemoved, token.Length); + totalCharsRemoved += token.Length; } - - familyName = familyName.Replace(" " + stretchString, "").TrimEnd(); - - return true; } - return false; + // Get rid of any trailing spaces. + normalizedFamilyName = (normalizedFamilyNameBuilder?.ToString() ?? normalizedFamilyName).TrimEnd(); + + //Preserve old font source + return new Typeface(typeface.FontFamily, style, weight, stretch); } } } diff --git a/src/Avalonia.Base/Media/TextDecorationCollection.cs b/src/Avalonia.Base/Media/TextDecorationCollection.cs index b703eaba57..ada62aef06 100644 --- a/src/Avalonia.Base/Media/TextDecorationCollection.cs +++ b/src/Avalonia.Base/Media/TextDecorationCollection.cs @@ -29,7 +29,7 @@ namespace Avalonia.Media { var locations = new List(); - using (var tokenizer = new StringTokenizer(s, ',', "Invalid text decoration.")) + using (var tokenizer = new SpanStringTokenizer(s, ',', "Invalid text decoration.")) { while (tokenizer.TryReadSpan(out var name)) { diff --git a/src/Avalonia.Base/PixelPoint.cs b/src/Avalonia.Base/PixelPoint.cs index 502eb205a6..b1e94c7d82 100644 --- a/src/Avalonia.Base/PixelPoint.cs +++ b/src/Avalonia.Base/PixelPoint.cs @@ -117,7 +117,7 @@ namespace Avalonia /// The . public static PixelPoint Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelPoint.")) + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelPoint.")) { return new PixelPoint( tokenizer.ReadInt32(), diff --git a/src/Avalonia.Base/PixelRect.cs b/src/Avalonia.Base/PixelRect.cs index e777d52164..88eee61263 100644 --- a/src/Avalonia.Base/PixelRect.cs +++ b/src/Avalonia.Base/PixelRect.cs @@ -433,7 +433,7 @@ namespace Avalonia /// The parsed . public static PixelRect Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelRect.")) + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelRect.")) { return new PixelRect( tokenizer.ReadInt32(), diff --git a/src/Avalonia.Base/PixelSize.cs b/src/Avalonia.Base/PixelSize.cs index 3d4896a45d..797988e659 100644 --- a/src/Avalonia.Base/PixelSize.cs +++ b/src/Avalonia.Base/PixelSize.cs @@ -92,7 +92,7 @@ namespace Avalonia { return false; } - using (var tokenizer = new StringTokenizer(source, exceptionMessage: "Invalid PixelSize.")) + using (var tokenizer = new SpanStringTokenizer(source, exceptionMessage: "Invalid PixelSize.")) { if (tokenizer.TryReadInt32(out var w) && tokenizer.TryReadInt32(out var h)) { diff --git a/src/Avalonia.Base/Point.cs b/src/Avalonia.Base/Point.cs index e3ea21d1eb..936f6aa5a9 100644 --- a/src/Avalonia.Base/Point.cs +++ b/src/Avalonia.Base/Point.cs @@ -184,7 +184,7 @@ namespace Avalonia /// The . public static Point Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point.")) + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point.")) { return new Point( tokenizer.ReadDouble(), diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index 433f46b66f..58a8c56c8b 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/src/Avalonia.Base/Rect.cs @@ -592,7 +592,7 @@ namespace Avalonia /// The parsed . public static Rect Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect.")) + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect.")) { return new Rect( tokenizer.ReadDouble(), diff --git a/src/Avalonia.Base/RelativePoint.cs b/src/Avalonia.Base/RelativePoint.cs index 2ab3996200..d5a7094092 100644 --- a/src/Avalonia.Base/RelativePoint.cs +++ b/src/Avalonia.Base/RelativePoint.cs @@ -169,7 +169,7 @@ namespace Avalonia /// The parsed . public static RelativePoint Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint.")) + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint.")) { var x = tokenizer.ReadString(); var y = tokenizer.ReadString(); diff --git a/src/Avalonia.Base/RelativeRect.cs b/src/Avalonia.Base/RelativeRect.cs index 7109ec41e9..2ec286a9d5 100644 --- a/src/Avalonia.Base/RelativeRect.cs +++ b/src/Avalonia.Base/RelativeRect.cs @@ -176,7 +176,7 @@ namespace Avalonia /// The parsed . public static RelativeRect Parse(string s) { - using (var tokenizer = new StringTokenizer(s, exceptionMessage: "Invalid RelativeRect.")) + using (var tokenizer = new SpanStringTokenizer(s, exceptionMessage: "Invalid RelativeRect.")) { var x = tokenizer.ReadSpan(); var y = tokenizer.ReadSpan(); diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs index 5ee4541571..587d5a53e4 100644 --- a/src/Avalonia.Base/Size.cs +++ b/src/Avalonia.Base/Size.cs @@ -161,7 +161,7 @@ namespace Avalonia /// The . public static Size Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size.")) + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size.")) { return new Size( tokenizer.ReadDouble(), diff --git a/src/Avalonia.Base/Thickness.cs b/src/Avalonia.Base/Thickness.cs index 9673898d09..aa982a4ffa 100644 --- a/src/Avalonia.Base/Thickness.cs +++ b/src/Avalonia.Base/Thickness.cs @@ -197,7 +197,7 @@ namespace Avalonia { const string exceptionMessage = "Invalid Thickness."; - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage)) + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage)) { if (tokenizer.TryReadDouble(out var a)) { diff --git a/src/Avalonia.Base/Utilities/EnumHelper.cs b/src/Avalonia.Base/Utilities/EnumHelper.cs index c857033ef1..fd9176985e 100644 --- a/src/Avalonia.Base/Utilities/EnumHelper.cs +++ b/src/Avalonia.Base/Utilities/EnumHelper.cs @@ -9,11 +9,26 @@ namespace Avalonia.Utilities { return Enum.Parse(key, ignoreCase); } + + public static bool TryParse(ReadOnlySpan key, bool ignoreCase, out T result) where T : struct + { + return Enum.TryParse(key, ignoreCase, out result); + } #else public static T Parse(string key, bool ignoreCase) where T : struct { return (T)Enum.Parse(typeof(T), key, ignoreCase); } + + public static bool TryParse(string key, bool ignoreCase, out T result) where T : struct + { + return Enum.TryParse(key, ignoreCase, out result); + } + + public static bool TryParse(ReadOnlySpan key, bool ignoreCase, out T result) where T : struct + { + return Enum.TryParse(key.ToString(), ignoreCase, out result); + } #endif } } diff --git a/src/Avalonia.Base/Utilities/SpanStringTokenizer.cs b/src/Avalonia.Base/Utilities/SpanStringTokenizer.cs new file mode 100644 index 0000000000..4e79bb5e2f --- /dev/null +++ b/src/Avalonia.Base/Utilities/SpanStringTokenizer.cs @@ -0,0 +1,252 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using static System.Char; + +namespace Avalonia.Utilities +{ + internal ref struct SpanStringTokenizer + { + private const char DefaultSeparatorChar = ','; + + private readonly ReadOnlySpan _s; + private readonly int _length; + private readonly char _separator; + private readonly string? _exceptionMessage; + private readonly IFormatProvider _formatProvider; + private int _index; + private int _tokenIndex; + private int _tokenLength; + + public SpanStringTokenizer(string s, IFormatProvider formatProvider, string? exceptionMessage = null) + : this(s.AsSpan(), GetSeparatorFromFormatProvider(formatProvider), exceptionMessage) + { + _formatProvider = formatProvider; + } + + public SpanStringTokenizer(string s, char separator = DefaultSeparatorChar, string? exceptionMessage = null) + : this(s.AsSpan(), separator, exceptionMessage) + { + } + + public SpanStringTokenizer(ReadOnlySpan s, IFormatProvider formatProvider, string? exceptionMessage = null) + : this(s, GetSeparatorFromFormatProvider(formatProvider), exceptionMessage) + { + _formatProvider = formatProvider; + } + + public SpanStringTokenizer(ReadOnlySpan s, char separator = DefaultSeparatorChar, string? exceptionMessage = null) + { + _s = s; + _length = s.Length; + _separator = separator; + _exceptionMessage = exceptionMessage; + _formatProvider = CultureInfo.InvariantCulture; + _index = 0; + _tokenIndex = -1; + _tokenLength = 0; + + while (_index < _length && IsWhiteSpace(_s[_index])) + { + _index++; + } + } + + public int CurrentTokenIndex => _tokenIndex; + + public string? CurrentToken => _tokenIndex < 0 ? null : _s.Slice(_tokenIndex, _tokenLength).ToString(); + + public ReadOnlySpan CurrentTokenSpan => _tokenIndex < 0 ? ReadOnlySpan.Empty : _s.Slice(_tokenIndex, _tokenLength); + + public void Dispose() + { + if (_index != _length) + { + throw GetFormatException(); + } + } + + public bool TryReadInt32(out Int32 result, char? separator = null) + { + if (TryReadSpan(out var stringResult, separator) && + SpanHelpers.TryParseInt(stringResult, NumberStyles.Integer, _formatProvider, out result)) + { + return true; + } + else + { + result = default; + return false; + } + } + + public int ReadInt32(char? separator = null) + { + if (!TryReadInt32(out var result, separator)) + { + throw GetFormatException(); + } + + return result; + } + + public bool TryReadDouble(out double result, char? separator = null) + { + if (TryReadSpan(out var stringResult, separator) && + SpanHelpers.TryParseDouble(stringResult, NumberStyles.Float, _formatProvider, out result)) + { + return true; + } + else + { + result = default; + return false; + } + } + + public double ReadDouble(char? separator = null) + { + if (!TryReadDouble(out var result, separator)) + { + throw GetFormatException(); + } + + return result; + } + + public bool TryReadString([NotNull] out string result, char? separator = null) + { + var success = TryReadToken(separator ?? _separator); + result = CurrentTokenSpan.ToString(); + return success; + } + + public string ReadString(char? separator = null) + { + if (!TryReadString(out var result, separator)) + { + throw GetFormatException(); + } + + return result; + } + + public bool TryReadSpan(out ReadOnlySpan result, char? separator = null) + { + var success = TryReadToken(separator ?? _separator); + result = CurrentTokenSpan; + return success; + } + + public ReadOnlySpan ReadSpan(char? separator = null) + { + if (!TryReadSpan(out var result, separator)) + { + throw GetFormatException(); + } + + return result; + } + + private bool TryReadToken(char separator) + { + _tokenIndex = -1; + + if (_index >= _length) + { + return false; + } + + var c = _s[_index]; + + var index = _index; + var length = 0; + + while (_index < _length) + { + c = _s[_index]; + + if (IsWhiteSpace(c) || c == separator) + { + break; + } + + _index++; + length++; + } + + SkipToNextToken(separator); + + _tokenIndex = index; + _tokenLength = length; + + if (_tokenLength < 1) + { + throw GetFormatException(); + } + + return true; + } + + private void SkipToNextToken(char separator) + { + if (_index < _length) + { + var c = _s[_index]; + + if (c != separator && !IsWhiteSpace(c)) + { + throw GetFormatException(); + } + + var length = 0; + + while (_index < _length) + { + c = _s[_index]; + + if (c == separator) + { + length++; + _index++; + + if (length > 1) + { + throw GetFormatException(); + } + } + else + { + if (!IsWhiteSpace(c)) + { + break; + } + + _index++; + } + } + + if (length > 0 && _index >= _length) + { + throw GetFormatException(); + } + } + } + + private FormatException GetFormatException() => + _exceptionMessage != null ? new FormatException(_exceptionMessage) : new FormatException(); + + private static char GetSeparatorFromFormatProvider(IFormatProvider provider) + { + var c = DefaultSeparatorChar; + + var formatInfo = NumberFormatInfo.GetInstance(provider); + if (formatInfo.NumberDecimalSeparator.Length > 0 && c == formatInfo.NumberDecimalSeparator[0]) + { + c = ';'; + } + + return c; + } + } +} diff --git a/src/Avalonia.Base/Utilities/StringTokenizer.cs b/src/Avalonia.Base/Utilities/StringTokenizer.cs index 027817be2c..e83ef8c479 100644 --- a/src/Avalonia.Base/Utilities/StringTokenizer.cs +++ b/src/Avalonia.Base/Utilities/StringTokenizer.cs @@ -5,6 +5,9 @@ using static System.Char; namespace Avalonia.Utilities { + // TODO12: Remove this struct in 12.0 (breaking change) + + [Obsolete("This type has been superseded by SpanStringTokenizer.")] #if !BUILDTASK public #endif diff --git a/src/Avalonia.Base/Vector.cs b/src/Avalonia.Base/Vector.cs index fffe206835..468d20ad19 100644 --- a/src/Avalonia.Base/Vector.cs +++ b/src/Avalonia.Base/Vector.cs @@ -101,7 +101,7 @@ namespace Avalonia /// The . public static Vector Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector.")) + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector.")) { return new Vector( tokenizer.ReadDouble(), diff --git a/src/Avalonia.Base/Vector3D.cs b/src/Avalonia.Base/Vector3D.cs index 61f55dd477..9502898677 100644 --- a/src/Avalonia.Base/Vector3D.cs +++ b/src/Avalonia.Base/Vector3D.cs @@ -15,7 +15,7 @@ public readonly record struct Vector3D(double X, double Y, double Z) /// The . public static Vector3D Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector.")) + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector.")) { return new Vector3D( tokenizer.ReadDouble(), diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 04b89ed4d9..cddf32d6b2 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -20,6 +20,9 @@ Shared/AvaloniaResourcesIndex.cs + + Shared/SpanStringTokenizer.cs + Shared/Constants.cs diff --git a/src/Avalonia.Controls/GridLength.cs b/src/Avalonia.Controls/GridLength.cs index 0e2f8e7d0c..bea57fdda6 100644 --- a/src/Avalonia.Controls/GridLength.cs +++ b/src/Avalonia.Controls/GridLength.cs @@ -217,13 +217,17 @@ namespace Avalonia.Controls /// The . public static IEnumerable ParseLengths(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture)) + var result = new List(); + + using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture)) { while (tokenizer.TryReadString(out var item)) { - yield return Parse(item); + result.Add(Parse(item)); } } + + return result; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs index 6a7d2a07d4..fb03d05e02 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs @@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters { var points = new List(); - using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PointsList.")) + using (var tokenizer = new SpanStringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PointsList.")) { while (tokenizer.TryReadDouble(out double x)) { diff --git a/tests/Avalonia.Base.UnitTests/Utilities/SpanStringTokenizerTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/SpanStringTokenizerTests.cs new file mode 100644 index 0000000000..bac8ff33ca --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Utilities/SpanStringTokenizerTests.cs @@ -0,0 +1,100 @@ +using System; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Utilities +{ + public class SpanStringTokenizerTests + { + // Explicit delegate because C# generics do not allow ref structs. + private delegate void TokenizerAction(SpanStringTokenizer tokenizer); + + private static TException AssertThrows(SpanStringTokenizer tokenizer, TokenizerAction action) + where TException : Exception + { + try + { + action(tokenizer); + } + catch (Exception ex) + { + if (ex.GetType() == typeof(TException)) + return (TException)ex; + + throw Xunit.Sdk.ThrowsException.ForIncorrectExceptionType(typeof(TException), ex); + } + + throw Xunit.Sdk.ThrowsException.ForNoException(typeof(TException)); + } + + [Fact] + public void ReadInt32_Reads_Values() + { + var target = new SpanStringTokenizer("123,456"); + + Assert.Equal(123, target.ReadInt32()); + Assert.Equal(456, target.ReadInt32()); + AssertThrows(target, t => t.ReadInt32()); + } + + [Fact] + public void ReadDouble_Reads_Values() + { + var target = new SpanStringTokenizer("12.3,45.6"); + + Assert.Equal(12.3, target.ReadDouble()); + Assert.Equal(45.6, target.ReadDouble()); + AssertThrows(target, t => t.ReadDouble()); + } + + [Fact] + public void TryReadInt32_Reads_Values() + { + var target = new SpanStringTokenizer("123,456"); + + Assert.True(target.TryReadInt32(out var value)); + Assert.Equal(123, value); + Assert.True(target.TryReadInt32(out value)); + Assert.Equal(456, value); + Assert.False(target.TryReadInt32(out value)); + } + + [Fact] + public void TryReadInt32_Doesnt_Throw() + { + var target = new SpanStringTokenizer("abc"); + + Assert.False(target.TryReadInt32(out var value)); + } + + [Fact] + public void TryReadDouble_Reads_Values() + { + var target = new SpanStringTokenizer("12.3,45.6"); + + Assert.True(target.TryReadDouble(out var value)); + Assert.Equal(12.3, value); + Assert.True(target.TryReadDouble(out value)); + Assert.Equal(45.6, value); + Assert.False(target.TryReadDouble(out value)); + } + + [Fact] + public void TryReadDouble_Doesnt_Throw() + { + var target = new SpanStringTokenizer("abc"); + + Assert.False(target.TryReadDouble(out var value)); + } + + [Fact] + public void ReadSpan_And_ReadString_Reads_Same() + { + var target1 = new SpanStringTokenizer("abc,def"); + var target2 = new SpanStringTokenizer("abc,def"); + + Assert.Equal(target1.ReadString(), target2.ReadSpan().ToString()); + Assert.True(target1.ReadSpan().SequenceEqual(target2.ReadString())); + } + } +}