diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj
index 037546b186..0b1e778cf0 100644
--- a/src/Avalonia.Controls/Avalonia.Controls.csproj
+++ b/src/Avalonia.Controls/Avalonia.Controls.csproj
@@ -26,6 +26,7 @@
true
+
Properties\SharedAssemblyInfo.cs
diff --git a/src/Avalonia.Controls/GridLength.cs b/src/Avalonia.Controls/GridLength.cs
index c711553e05..b17dc584bd 100644
--- a/src/Avalonia.Controls/GridLength.cs
+++ b/src/Avalonia.Controls/GridLength.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -210,7 +211,13 @@ namespace Avalonia.Controls
/// The .
public static IEnumerable ParseLengths(string s, CultureInfo culture)
{
- return s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(x => Parse(x, culture));
+ using (var tokenizer = new StringTokenizer(s, culture))
+ {
+ while (tokenizer.NextString(out var item))
+ {
+ yield return Parse(item, culture);
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs
index 10549b967d..6c7d104680 100644
--- a/src/Avalonia.Visuals/Matrix.cs
+++ b/src/Avalonia.Visuals/Matrix.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Utilities;
using System;
using System.Globalization;
using System.Linq;
@@ -305,23 +306,16 @@ namespace Avalonia
/// The .
public static Matrix Parse(string s, CultureInfo culture)
{
- var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Trim())
- .ToArray();
-
- if (parts.Length == 6)
+ using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Matrix"))
{
return new Matrix(
- double.Parse(parts[0], culture),
- double.Parse(parts[1], culture),
- double.Parse(parts[2], culture),
- double.Parse(parts[3], culture),
- double.Parse(parts[4], culture),
- double.Parse(parts[5], culture));
- }
- else
- {
- throw new FormatException("Invalid Matrix.");
+ tokenizer.NextDoubleRequired(),
+ tokenizer.NextDoubleRequired(),
+ tokenizer.NextDoubleRequired(),
+ tokenizer.NextDoubleRequired(),
+ tokenizer.NextDoubleRequired(),
+ tokenizer.NextDoubleRequired()
+ );
}
}
}
diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs
index 5fbd082967..49d2a401bf 100644
--- a/src/Avalonia.Visuals/Point.cs
+++ b/src/Avalonia.Visuals/Point.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Utilities;
using System;
using System.Globalization;
using System.Linq;
@@ -173,17 +174,12 @@ namespace Avalonia
/// The .
public static Point Parse(string s, CultureInfo culture)
{
- var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Trim())
- .ToList();
-
- if (parts.Count == 2)
- {
- return new Point(double.Parse(parts[0], culture), double.Parse(parts[1], culture));
- }
- else
+ using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Point"))
{
- throw new FormatException("Invalid Point.");
+ return new Point(
+ tokenizer.NextDoubleRequired(),
+ tokenizer.NextDoubleRequired()
+ );
}
}
diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs
index d562429fc7..11f3db31da 100644
--- a/src/Avalonia.Visuals/Rect.cs
+++ b/src/Avalonia.Visuals/Rect.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Utilities;
using System;
using System.Globalization;
using System.Linq;
@@ -490,21 +491,14 @@ namespace Avalonia
/// The parsed .
public static Rect Parse(string s, CultureInfo culture)
{
- var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Trim())
- .ToList();
-
- if (parts.Count == 4)
+ using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Rect"))
{
return new Rect(
- double.Parse(parts[0], culture),
- double.Parse(parts[1], culture),
- double.Parse(parts[2], culture),
- double.Parse(parts[3], culture));
- }
- else
- {
- throw new FormatException("Invalid Rect.");
+ tokenizer.NextDoubleRequired(),
+ tokenizer.NextDoubleRequired(),
+ tokenizer.NextDoubleRequired(),
+ tokenizer.NextDoubleRequired()
+ );
}
}
}
diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs
index cc34feb5f3..625ee61439 100644
--- a/src/Avalonia.Visuals/RelativePoint.cs
+++ b/src/Avalonia.Visuals/RelativePoint.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Utilities;
using System;
using System.Globalization;
using System.Linq;
@@ -157,37 +158,32 @@ namespace Avalonia
/// The parsed .
public static RelativePoint Parse(string s, CultureInfo culture)
{
- var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Trim())
- .ToList();
-
- if (parts.Count == 2)
+ using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid RelativePoint"))
{
+ var x = tokenizer.NextStringRequired();
+ var y = tokenizer.NextStringRequired();
+
var unit = RelativeUnit.Absolute;
var scale = 1.0;
- if (parts[0].EndsWith("%"))
+ if (x.EndsWith("%"))
{
- if (!parts[1].EndsWith("%"))
+ if (!y.EndsWith("%"))
{
throw new FormatException("If one coordinate is relative, both must be.");
}
- parts[0] = parts[0].TrimEnd('%');
- parts[1] = parts[1].TrimEnd('%');
+ x = x.TrimEnd('%');
+ y = y.TrimEnd('%');
unit = RelativeUnit.Relative;
scale = 0.01;
}
return new RelativePoint(
- double.Parse(parts[0], culture) * scale,
- double.Parse(parts[1], culture) * scale,
+ double.Parse(x, culture) * scale,
+ double.Parse(y, culture) * scale,
unit);
}
- else
- {
- throw new FormatException("Invalid Point.");
- }
}
}
}
diff --git a/src/Avalonia.Visuals/RelativeRect.cs b/src/Avalonia.Visuals/RelativeRect.cs
index a11f080e94..05e344f42b 100644
--- a/src/Avalonia.Visuals/RelativeRect.cs
+++ b/src/Avalonia.Visuals/RelativeRect.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Utilities;
using System;
using System.Globalization;
using System.Linq;
@@ -12,6 +13,8 @@ namespace Avalonia
///
public struct RelativeRect : IEquatable
{
+ private static readonly char[] PercentChar = { '%' };
+
///
/// A rectangle that represents 100% of an area.
///
@@ -159,7 +162,7 @@ namespace Avalonia
Rect.Width * size.Width,
Rect.Height * size.Height);
}
-
+
///
/// Parses a string.
///
@@ -168,43 +171,43 @@ namespace Avalonia
/// The parsed .
public static RelativeRect Parse(string s, CultureInfo culture)
{
- var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Trim())
- .ToList();
-
- if (parts.Count == 4)
+ using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid RelativeRect"))
{
+ var x = tokenizer.NextStringRequired();
+ var y = tokenizer.NextStringRequired();
+ var width = tokenizer.NextStringRequired();
+ var height = tokenizer.NextStringRequired();
+
var unit = RelativeUnit.Absolute;
var scale = 1.0;
- if (parts[0].EndsWith("%"))
+ var xRelative = x.EndsWith("%", StringComparison.Ordinal);
+ var yRelative = y.EndsWith("%", StringComparison.Ordinal);
+ var widthRelative = width.EndsWith("%", StringComparison.Ordinal);
+ var heightRelative = height.EndsWith("%", StringComparison.Ordinal);
+
+ if (xRelative && yRelative && widthRelative && heightRelative)
{
- if (!parts[1].EndsWith("%")
- || !parts[2].EndsWith("%")
- || !parts[3].EndsWith("%"))
- {
- throw new FormatException("If one coordinate is relative, all other must be too.");
- }
-
- parts[0] = parts[0].TrimEnd('%');
- parts[1] = parts[1].TrimEnd('%');
- parts[2] = parts[2].TrimEnd('%');
- parts[3] = parts[3].TrimEnd('%');
+ x = x.TrimEnd(PercentChar);
+ y = y.TrimEnd(PercentChar);
+ width = width.TrimEnd(PercentChar);
+ height = height.TrimEnd(PercentChar);
+
unit = RelativeUnit.Relative;
scale = 0.01;
}
+ else if (xRelative || yRelative || widthRelative || heightRelative)
+ {
+ throw new FormatException("If one coordinate is relative, all must be.");
+ }
return new RelativeRect(
- double.Parse(parts[0], culture) * scale,
- double.Parse(parts[1], culture) * scale,
- double.Parse(parts[2], culture) * scale,
- double.Parse(parts[3], culture) * scale,
+ double.Parse(x, culture) * scale,
+ double.Parse(y, culture) * scale,
+ double.Parse(width, culture) * scale,
+ double.Parse(height, culture) * scale,
unit);
}
- else
- {
- throw new FormatException("Invalid RelativeRect.");
- }
}
}
}
diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs
index 6ad87c6120..ff734146c2 100644
--- a/src/Avalonia.Visuals/Size.cs
+++ b/src/Avalonia.Visuals/Size.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Utilities;
using System;
using System.Globalization;
using System.Linq;
@@ -153,17 +154,11 @@ namespace Avalonia
/// The .
public static Size Parse(string s, CultureInfo culture)
{
- var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Trim())
- .ToList();
-
- if (parts.Count == 2)
- {
- return new Size(double.Parse(parts[0], culture), double.Parse(parts[1], culture));
- }
- else
+ using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Size"))
{
- throw new FormatException("Invalid Size.");
+ return new Size(
+ tokenizer.NextDoubleRequired(),
+ tokenizer.NextDoubleRequired());
}
}
diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs
index dc9be7341d..2a1cf6ac57 100644
--- a/src/Avalonia.Visuals/Thickness.cs
+++ b/src/Avalonia.Visuals/Thickness.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Utilities;
using System;
using System.Globalization;
using System.Linq;
@@ -163,28 +164,22 @@ namespace Avalonia
/// The .
public static Thickness Parse(string s, CultureInfo culture)
{
- var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Trim())
- .ToList();
-
- switch (parts.Count)
+ using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Thickness"))
{
- case 1:
- var uniform = double.Parse(parts[0], culture);
- return new Thickness(uniform);
- case 2:
- var horizontal = double.Parse(parts[0], culture);
- var vertical = double.Parse(parts[1], culture);
- return new Thickness(horizontal, vertical);
- case 4:
- var left = double.Parse(parts[0], culture);
- var top = double.Parse(parts[1], culture);
- var right = double.Parse(parts[2], culture);
- var bottom = double.Parse(parts[3], culture);
- return new Thickness(left, top, right, bottom);
+ var a = tokenizer.NextDoubleRequired();
+
+ if (tokenizer.NextDouble(out var b))
+ {
+ if (tokenizer.NextDouble(out var c))
+ {
+ return new Thickness(a, b, c, tokenizer.NextDoubleRequired());
+ }
+
+ return new Thickness(a, b);
+ }
+
+ return new Thickness(a);
}
-
- throw new FormatException("Invalid Thickness.");
}
///
diff --git a/src/Avalonia.Visuals/Utilities/StringTokenizer.cs b/src/Avalonia.Visuals/Utilities/StringTokenizer.cs
new file mode 100644
index 0000000000..2f378f44df
--- /dev/null
+++ b/src/Avalonia.Visuals/Utilities/StringTokenizer.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Globalization;
+using static System.Char;
+
+namespace Avalonia.Utilities
+{
+ internal struct StringTokenizer : IDisposable
+ {
+ private const char DefaultSeparatorChar = ',';
+
+ private readonly string _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 StringTokenizer(string s, IFormatProvider formatProvider, string exceptionMessage = null)
+ : this(s, GetSeparatorFromFormatProvider(formatProvider), exceptionMessage)
+ {
+ _formatProvider = formatProvider;
+ }
+
+ public StringTokenizer(string s, char separator = DefaultSeparatorChar, string exceptionMessage = null)
+ {
+ _s = s ?? throw new ArgumentNullException(nameof(s));
+ _length = s?.Length ?? 0;
+ _separator = separator;
+ _exceptionMessage = exceptionMessage;
+ _formatProvider = CultureInfo.InvariantCulture;
+ _index = 0;
+ _tokenIndex = -1;
+ _tokenLength = 0;
+
+ while (_index < _length && IsWhiteSpace(_s, _index))
+ {
+ _index++;
+ }
+ }
+
+ public string CurrentToken => _tokenIndex < 0 ? null : _s.Substring(_tokenIndex, _tokenLength);
+
+ public void Dispose()
+ {
+ if (_index != _length)
+ {
+ throw GetFormatException();
+ }
+ }
+
+ public bool NextInt32(out Int32 result, char? separator = null)
+ {
+ var success = NextString(out var stringResult, separator);
+ result = success ? int.Parse(stringResult, _formatProvider) : 0;
+ return success;
+ }
+
+ public int NextInt32Required(char? separator = null)
+ {
+ if (!NextInt32(out var result, separator))
+ {
+ throw GetFormatException();
+ }
+
+ return result;
+ }
+
+ public bool NextDouble(out double result, char? separator = null)
+ {
+ var success = NextString(out var stringResult, separator);
+ result = success ? double.Parse(stringResult, _formatProvider) : 0;
+ return success;
+ }
+
+ public double NextDoubleRequired(char? separator = null)
+ {
+ if (!NextDouble(out var result, separator))
+ {
+ throw GetFormatException();
+ }
+
+ return result;
+ }
+
+ public bool NextString(out string result, char? separator = null)
+ {
+ var success = NextToken(separator ?? _separator);
+ result = CurrentToken;
+ return success;
+ }
+
+ public string NextStringRequired(char? separator = null)
+ {
+ if (!NextString(out var result, separator))
+ {
+ throw GetFormatException();
+ }
+
+ return result;
+ }
+
+ private bool NextToken(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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs b/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs
index 8ba4f3b739..9f25dcd413 100644
--- a/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using System;
using System.Globalization;
using Xunit;
@@ -25,5 +26,12 @@ namespace Avalonia.Visuals.UnitTests
Assert.Equal(new RelativeRect(0.1, 0.2, 0.4, 0.7, RelativeUnit.Relative), result, Compare);
}
+
+ [Fact]
+ public void Parse_Should_Throw_Mixed_Values()
+ {
+ Assert.Throws(() =>
+ RelativeRect.Parse("10%, 20%, 40, 70%", CultureInfo.InvariantCulture));
+ }
}
}