Browse Source

Merge pull request #1685 from jkoritzinsky/spanify-PathMarkupParser

Span-ify PathMarkupParser
Steven Kirk 7 years ago
parent
commit
8b8ba122e3

+ 6 - 2
.gitignore

@@ -176,5 +176,9 @@ nuget
 Avalonia.XBuild.sln
 project.lock.json
 .idea/*
-**/obj-Skia/*
-**/obj-Direct2D1/*
+
+
+##################
+## BenchmarkDotNet
+##################
+BenchmarkDotNet.Artifacts/

+ 5 - 0
build/System.Memory.props

@@ -0,0 +1,5 @@
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <PackageReference Include="System.Memory" Version="4.5.0" />
+  </ItemGroup>
+</Project>

+ 1 - 1
packages.cake

@@ -253,7 +253,7 @@ public class Packages
                 }
                 .Deps(new string[]{null, "netcoreapp2.0"},
                     "System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives",
-                    "System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter")
+                    "System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter", "System.Memory")
                 .ToArray(),
                 Files = coreLibrariesNuSpecContent
                     .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform)

+ 1 - 0
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@@ -8,4 +8,5 @@
     <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
   </ItemGroup>
   <Import Project="..\..\build\Rx.props" />
+  <Import Project="..\..\build\System.Memory.props" />
 </Project>

+ 264 - 395
src/Avalonia.Visuals/Media/PathMarkupParser.cs

@@ -5,9 +5,6 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
 using Avalonia.Platform;
 
 namespace Avalonia.Media
@@ -17,7 +14,6 @@ namespace Avalonia.Media
     /// </summary>
     public class PathMarkupParser : IDisposable
     {
-        private static readonly string s_separatorPattern;
         private static readonly Dictionary<char, Command> s_commands =
             new Dictionary<char, Command>
                 {
@@ -37,14 +33,9 @@ namespace Avalonia.Media
         private IGeometryContext _geometryContext;
         private Point _currentPoint;
         private Point? _previousControlPoint;
-        private bool? _isOpen;
+        private bool _isOpen;
         private bool _isDisposed;
 
-        static PathMarkupParser()
-        {
-            s_separatorPattern = CreatesSeparatorPattern();
-        }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="PathMarkupParser"/> class.
         /// </summary>
@@ -76,18 +67,6 @@ namespace Avalonia.Media
             Close
         }
 
-        /// <summary>
-        /// Parses the specified path data and writes the result to the geometryContext of this instance.
-        /// </summary>
-        /// <param name="pathData">The path data.</param>
-        public void Parse(string pathData)
-        {
-            var normalizedPathData = NormalizeWhiteSpaces(pathData);
-            var tokens = ParseTokens(normalizedPathData);
-
-            CreateGeometry(tokens);
-        }
-
         void IDisposable.Dispose()
         {
             Dispose(true);
@@ -108,66 +87,6 @@ namespace Avalonia.Media
             _isDisposed = true;
         }
 
-        private static string NormalizeWhiteSpaces(string s)
-        {
-            int length = s.Length,
-                index = 0,
-                i = 0;
-            var source = s.ToCharArray();
-            var skip = false;
-
-            for (; i < length; i++)
-            {
-                var c = source[i];
-
-                if (char.IsWhiteSpace(c))
-                {
-                    if (skip)
-                    {
-                        continue;
-                    }
-
-                    source[index++] = c;
-
-                    skip = true;
-
-                    continue;
-                }
-
-                skip = false;
-
-                source[index++] = c;
-            }
-
-            if (char.IsWhiteSpace(source[index - 1]))
-            {
-                index--;
-            }
-
-            return char.IsWhiteSpace(source[0]) ? new string(source, 1, index) : new string(source, 0, index);
-        }
-
-        private static string CreatesSeparatorPattern()
-        {
-            var stringBuilder = new StringBuilder();
-
-            foreach (var command in s_commands.Keys)
-            {
-                stringBuilder.Append(command);
-
-                stringBuilder.Append(char.ToLower(command));
-            }
-
-            return @"(?=[" + stringBuilder + "])";
-        }
-
-        private static IEnumerable<CommandToken> ParseTokens(string s)
-        {
-            var expressions = Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t));
-
-            return expressions.Select(CommandToken.Parse);
-        }
-
         private static Point MirrorControlPoint(Point controlPoint, Point center)
         {
             var dir = controlPoint - center;
@@ -175,76 +94,78 @@ namespace Avalonia.Media
             return center + -dir;
         }
 
-        private void CreateGeometry(IEnumerable<CommandToken> commandTokens)
+        /// <summary>
+        /// Parses the specified path data and writes the result to the geometryContext of this instance.
+        /// </summary>
+        /// <param name="pathData">The path data.</param>
+        public void Parse(string pathData)
         {
+            var span = pathData.AsSpan();
             _currentPoint = new Point();
 
-            foreach (var commandToken in commandTokens)
+            while(!span.IsEmpty)
             {
-                try
-                {
-                    while (true)
-                    {
-                        switch (commandToken.Command)
-                        {
-                            case Command.None:
-                                break;
-                            case Command.FillRule:
-                                SetFillRule(commandToken);
-                                break;
-                            case Command.Move:
-                                AddMove(commandToken);
-                                break;
-                            case Command.Line:
-                                AddLine(commandToken);
-                                break;
-                            case Command.HorizontalLine:
-                                AddHorizontalLine(commandToken);
-                                break;
-                            case Command.VerticalLine:
-                                AddVerticalLine(commandToken);
-                                break;
-                            case Command.CubicBezierCurve:
-                                AddCubicBezierCurve(commandToken);
-                                break;
-                            case Command.QuadraticBezierCurve:
-                                AddQuadraticBezierCurve(commandToken);
-                                break;
-                            case Command.SmoothCubicBezierCurve:
-                                AddSmoothCubicBezierCurve(commandToken);
-                                break;
-                            case Command.SmoothQuadraticBezierCurve:
-                                AddSmoothQuadraticBezierCurve(commandToken);
-                                break;
-                            case Command.Arc:
-                                AddArc(commandToken);
-                                break;
-                            case Command.Close:
-                                CloseFigure();
-                                break;
-                            default:
-                                throw new NotSupportedException("Unsupported command");
-                        }
-
-                        if (commandToken.HasImplicitCommands)
-                        {
-                            continue;
-                        }
-
-                        break;
-                    }
-                }
-                catch (InvalidDataException)
+                if(!ReadCommand(ref span, out var command, out var relative))
                 {
-                    break;
+                    return;
                 }
-                catch (NotSupportedException)
+
+                bool initialCommand = true;
+                
+                do
                 {
-                    break;
-                }
+                    if (!initialCommand)
+                    {
+                        span = ReadSeparator(span);
+                    }
+
+                    switch (command)
+                    {
+                        case Command.None:
+                            break;
+                        case Command.FillRule:
+                            SetFillRule(ref span);
+                            break;
+                        case Command.Move:
+                            AddMove(ref span, relative);
+                            break;
+                        case Command.Line:
+                            AddLine(ref span, relative);
+                            break;
+                        case Command.HorizontalLine:
+                            AddHorizontalLine(ref span, relative);
+                            break;
+                        case Command.VerticalLine:
+                            AddVerticalLine(ref span, relative);
+                            break;
+                        case Command.CubicBezierCurve:
+                            AddCubicBezierCurve(ref span, relative);
+                            break;
+                        case Command.QuadraticBezierCurve:
+                            AddQuadraticBezierCurve(ref span, relative);
+                            break;
+                        case Command.SmoothCubicBezierCurve:
+                            AddSmoothCubicBezierCurve(ref span, relative);
+                            break;
+                        case Command.SmoothQuadraticBezierCurve:
+                            AddSmoothQuadraticBezierCurve(ref span, relative);
+                            break;
+                        case Command.Arc:
+                            AddArc(ref span, relative);
+                            break;
+                        case Command.Close:
+                            CloseFigure();
+                            break;
+                        default:
+                            throw new NotSupportedException("Unsupported command");
+                    }
+
+                    initialCommand = false;
+                } while (PeekArgument(span));
+                
             }
 
-            if (_isOpen != null)
+            if (_isOpen)
             {
                 _geometryContext.EndFigure(false);
             }
@@ -252,7 +173,7 @@ namespace Avalonia.Media
 
         private void CreateFigure()
         {
-            if (_isOpen != null)
+            if (_isOpen)
             {
                 _geometryContext.EndFigure(false);
             }
@@ -262,62 +183,72 @@ namespace Avalonia.Media
             _isOpen = true;
         }
 
-        private void SetFillRule(CommandToken commandToken)
+        private void SetFillRule(ref ReadOnlySpan<char> span)
         {
-            var fillRule = commandToken.ReadFillRule();
+            if (!ReadArgument(ref span, out var fillRule) || fillRule.Length != 1)
+            {
+                throw new InvalidDataException("Invalid fill rule.");
+            }
+
+            FillRule rule;
 
-            _geometryContext.SetFillRule(fillRule);
+            switch (fillRule[0])
+            {
+                case '0':
+                    rule = FillRule.EvenOdd;
+                    break;
+                case '1':
+                    rule = FillRule.NonZero;
+                    break;
+                default:
+                    throw new InvalidDataException("Invalid fill rule");
+            }
+
+            _geometryContext.SetFillRule(rule);
         }
 
         private void CloseFigure()
         {
-            if (_isOpen == true)
+            if (_isOpen)
             {
                 _geometryContext.EndFigure(true);
             }
 
             _previousControlPoint = null;
 
-            _isOpen = null;
+            _isOpen = false;
         }
 
-        private void AddMove(CommandToken commandToken)
+        private void AddMove(ref ReadOnlySpan<char> span, bool relative)
         {
-            var currentPoint = commandToken.IsRelative
-                                   ? commandToken.ReadRelativePoint(_currentPoint)
-                                   : commandToken.ReadPoint();
+            var currentPoint = relative
+                                ? ReadRelativePoint(ref span, _currentPoint)
+                                : ReadPoint(ref span);
 
             _currentPoint = currentPoint;
 
             CreateFigure();
 
-            if (!commandToken.HasImplicitCommands)
+            while (PeekArgument(span))
             {
-                return;
-            }
+                span = ReadSeparator(span);
+                AddLine(ref span, relative);
 
-            while (commandToken.HasImplicitCommands)
-            {
-                AddLine(commandToken);
-
-                if (commandToken.IsRelative)
+                if (!relative)
                 {
-                    continue;
+                    _currentPoint = currentPoint;
+                    CreateFigure();
                 }
-
-                _currentPoint = currentPoint;
-
-                CreateFigure();
             }
         }
 
-        private void AddLine(CommandToken commandToken)
+        private void AddLine(ref ReadOnlySpan<char> span, bool relative)
         {
-            _currentPoint = commandToken.IsRelative
-                                ? commandToken.ReadRelativePoint(_currentPoint)
-                                : commandToken.ReadPoint();
+            _currentPoint = relative
+                                ? ReadRelativePoint(ref span, _currentPoint)
+                                : ReadPoint(ref span);
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -325,13 +256,13 @@ namespace Avalonia.Media
             _geometryContext.LineTo(_currentPoint);
         }
 
-        private void AddHorizontalLine(CommandToken commandToken)
+        private void AddHorizontalLine(ref ReadOnlySpan<char> span, bool relative)
         {
-            _currentPoint = commandToken.IsRelative
-                                ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y)
-                                : _currentPoint.WithX(commandToken.ReadDouble());
+            _currentPoint = relative
+                                ? new Point(_currentPoint.X + ReadDouble(ref span), _currentPoint.Y)
+                                : _currentPoint.WithX(ReadDouble(ref span));
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -339,13 +270,13 @@ namespace Avalonia.Media
             _geometryContext.LineTo(_currentPoint);
         }
 
-        private void AddVerticalLine(CommandToken commandToken)
+        private void AddVerticalLine(ref ReadOnlySpan<char> span, bool relative)
         {
-            _currentPoint = commandToken.IsRelative
-                                ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble())
-                                : _currentPoint.WithY(commandToken.ReadDouble());
+            _currentPoint = relative
+                                ? new Point(_currentPoint.X, _currentPoint.Y + ReadDouble(ref span))
+                                : _currentPoint.WithY(ReadDouble(ref span));
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -353,23 +284,27 @@ namespace Avalonia.Media
             _geometryContext.LineTo(_currentPoint);
         }
 
-        private void AddCubicBezierCurve(CommandToken commandToken)
+        private void AddCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
         {
-            var point1 = commandToken.IsRelative
-                             ? commandToken.ReadRelativePoint(_currentPoint)
-                             : commandToken.ReadPoint();
+            var point1 = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
-            var point2 = commandToken.IsRelative
-                             ? commandToken.ReadRelativePoint(_currentPoint)
-                             : commandToken.ReadPoint();
+            span = ReadSeparator(span);
+
+            var point2 = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
             _previousControlPoint = point2;
 
-            var point3 = commandToken.IsRelative
-                             ? commandToken.ReadRelativePoint(_currentPoint)
-                             : commandToken.ReadPoint();
+            span = ReadSeparator(span);
+
+            var point3 = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -379,19 +314,21 @@ namespace Avalonia.Media
             _currentPoint = point3;
         }
 
-        private void AddQuadraticBezierCurve(CommandToken commandToken)
+        private void AddQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
         {
-            var start = commandToken.IsRelative
-                            ? commandToken.ReadRelativePoint(_currentPoint)
-                            : commandToken.ReadPoint();
+            var start = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
             _previousControlPoint = start;
 
-            var end = commandToken.IsRelative
-                          ? commandToken.ReadRelativePoint(_currentPoint)
-                          : commandToken.ReadPoint();
+            span = ReadSeparator(span);
+
+            var end = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -401,22 +338,24 @@ namespace Avalonia.Media
             _currentPoint = end;
         }
 
-        private void AddSmoothCubicBezierCurve(CommandToken commandToken)
+        private void AddSmoothCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
         {
-            var point2 = commandToken.IsRelative
-                             ? commandToken.ReadRelativePoint(_currentPoint)
-                             : commandToken.ReadPoint();
+            var point2 = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
-            var end = commandToken.IsRelative
-                          ? commandToken.ReadRelativePoint(_currentPoint)
-                          : commandToken.ReadPoint();
+            span = ReadSeparator(span);
+
+            var end = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
             if (_previousControlPoint != null)
             {
                 _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
             }
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -428,18 +367,18 @@ namespace Avalonia.Media
             _currentPoint = end;
         }
 
-        private void AddSmoothQuadraticBezierCurve(CommandToken commandToken)
+        private void AddSmoothQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
         {
-            var end = commandToken.IsRelative
-                          ? commandToken.ReadRelativePoint(_currentPoint)
-                          : commandToken.ReadPoint();
+            var end = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
             if (_previousControlPoint != null)
             {
                 _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
             }
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -449,21 +388,27 @@ namespace Avalonia.Media
             _currentPoint = end;
         }
 
-        private void AddArc(CommandToken commandToken)
+        private void AddArc(ref ReadOnlySpan<char> span, bool relative)
         {
-            var size = commandToken.ReadSize();
+            var size = ReadSize(ref span);
+
+            span = ReadSeparator(span);
 
-            var rotationAngle = commandToken.ReadDouble();
+            var rotationAngle = ReadDouble(ref span);
+            span = ReadSeparator(span);
+            var isLargeArc = ReadBool(ref span);
 
-            var isLargeArc = commandToken.ReadBool();
+            span = ReadSeparator(span);
 
-            var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
+            var sweepDirection = ReadBool(ref span) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
+            
+            span = ReadSeparator(span);
 
-            var end = commandToken.IsRelative
-                          ? commandToken.ReadRelativePoint(_currentPoint)
-                          : commandToken.ReadPoint();
+            var end = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -475,210 +420,134 @@ namespace Avalonia.Media
             _previousControlPoint = null;
         }
 
-        private class CommandToken
+        private static bool PeekArgument(ReadOnlySpan<char> span)
         {
-            private const string ArgumentExpression = @"-?[0-9]*\.?\d+";
-
-            private CommandToken(Command command, bool isRelative, IEnumerable<string> arguments)
-            {
-                Command = command;
-
-                IsRelative = isRelative;
-
-                Arguments = new List<string>(arguments);
-            }
-
-            public Command Command { get; }
+            span = SkipWhitespace(span);
 
-            public bool IsRelative { get; }
+            return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || char.IsDigit(span[0]));
+        }
 
-            public bool HasImplicitCommands
+        private static bool ReadArgument(ref ReadOnlySpan<char> remaining, out ReadOnlySpan<char> argument)
+        {
+            remaining = SkipWhitespace(remaining);
+            if (remaining.IsEmpty)
             {
-                get
-                {
-                    if (CurrentPosition == 0 && Arguments.Count > 0)
-                    {
-                        return true;
-                    }
-
-                    return CurrentPosition < Arguments.Count - 1;
-                }
+                argument = ReadOnlySpan<char>.Empty;
+                return false;
             }
 
-            private int CurrentPosition { get; set; }
-
-            private List<string> Arguments { get; }
-
-            public static CommandToken Parse(string s)
-            {              
-                using (var reader = new StringReader(s))
-                {
-                    var command = Command.None;
-
-                    var isRelative = false;
-
-                    if (!ReadCommand(reader, ref command, ref isRelative))
-                    {
-                        throw new InvalidDataException("No path command declared.");
-                    }
-
-                    var commandArguments = reader.ReadToEnd();
-
-                    var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression);
-
-                    var arguments = new List<string>();
-
-                    foreach (Match match in argumentMatches)
-                    {
-                        arguments.Add(match.Value);
-                    }
-
-                    return new CommandToken(command, isRelative, arguments);
-                }
-            }
-
-            public FillRule ReadFillRule()
+            var valid = false;
+            int i = 0;
+            if (remaining[i] == '-')
             {
-                if (CurrentPosition == Arguments.Count)
-                {
-                    throw new InvalidDataException("Invalid fill rule");
-                }
-
-                var value = Arguments[CurrentPosition];
-
-                CurrentPosition++;
-
-                switch (value)
-                {
-                    case "0":
-                        {
-                            return FillRule.EvenOdd;
-                        }
-
-                    case "1":
-                        {
-                            return FillRule.NonZero;
-                        }
-
-                    default:
-                        throw new InvalidDataException("Invalid fill rule");
-                }
+                i++;
             }
+            for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
 
-            public bool ReadBool()
+            if (i < remaining.Length && remaining[i] == '.')
             {
-                if (CurrentPosition == Arguments.Count)
-                {
-                    throw new InvalidDataException("Invalid boolean value");
-                }
-
-                var value = Arguments[CurrentPosition];
-
-                CurrentPosition++;
-
-                switch (value)
-                {
-                    case "1":
-                        {
-                            return true;
-                        }
-
-                    case "0":
-                        {
-                            return false;
-                        }
-
-                    default:
-                        throw new InvalidDataException("Invalid boolean value");
-                }
+                valid = false;
+                i++;
             }
+            for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
 
-            public double ReadDouble()
+            if (!valid)
             {
-                if (CurrentPosition == Arguments.Count)
-                {
-                    throw new InvalidDataException("Invalid double value");
-                }
-
-                var value = Arguments[CurrentPosition];
-
-                CurrentPosition++;
-
-                return double.Parse(value, CultureInfo.InvariantCulture);
+                argument = ReadOnlySpan<char>.Empty;
+                return false;
             }
+            argument = remaining.Slice(0, i);
+            remaining = remaining.Slice(i);
+            return true;
+        }
 
-            public Size ReadSize()
-            {
-                var width = ReadDouble();
-
-                var height = ReadDouble();
-
-                return new Size(width, height);
-            }
 
-            public Point ReadPoint()
+        private static ReadOnlySpan<char> ReadSeparator(ReadOnlySpan<char> span)
+        {
+            span = SkipWhitespace(span);
+            if (!span.IsEmpty && span[0] == ',')
             {
-                var x = ReadDouble();
-
-                var y = ReadDouble();
-
-                return new Point(x, y);
+                span = span.Slice(1);
             }
+            return span;
+        }
 
-            public Point ReadRelativePoint(Point origin)
-            {
-                var x = ReadDouble();
-
-                var y = ReadDouble();
-
-                return new Point(origin.X + x, origin.Y + y);
-            }          
+        private static ReadOnlySpan<char> SkipWhitespace(ReadOnlySpan<char> span)
+        {
+            int i = 0;
+            for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ;
+            return span.Slice(i);
+        }
 
-            private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative)
+        private bool ReadBool(ref ReadOnlySpan<char> span)
+        {
+            if (!ReadArgument(ref span, out var boolValue) || boolValue.Length != 1)
             {
-                ReadWhitespace(reader);
-
-                var i = reader.Peek();
-
-                if (i == -1)
-                {
+                throw new InvalidDataException("Invalid bool rule.");
+            }
+            
+            switch (boolValue[0])
+            {
+                case '0':
                     return false;
-                }
+                case '1':
+                    return true;
+                default:
+                    throw new InvalidDataException("Invalid bool rule");
+            }
+        }
 
-                var c = (char)i;
+        private double ReadDouble(ref ReadOnlySpan<char> span)
+        {
+            if (!ReadArgument(ref span, out var doubleValue))
+            {
+                throw new InvalidDataException("Invalid double value");
+            }
 
-                if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next))
-                {
-                    throw new InvalidDataException("Unexpected path command '" + c + "'.");
-                }
+            return double.Parse(doubleValue.ToString(), CultureInfo.InvariantCulture);
+        }
 
-                command = next;
+        private Size ReadSize(ref ReadOnlySpan<char> span)
+        {
+            var width = ReadDouble(ref span);
+            span = ReadSeparator(span);
+            var height = ReadDouble(ref span);
+            return new Size(width, height);
+        }
 
-                relative = char.IsLower(c);
+        private Point ReadPoint(ref ReadOnlySpan<char> span)
+        {
+            var x = ReadDouble(ref span);
+            span = ReadSeparator(span);
+            var y = ReadDouble(ref span);
+            return new Point(x, y);
+        }
 
-                reader.Read();
+        private Point ReadRelativePoint(ref ReadOnlySpan<char> span, Point origin)
+        {
+            var x = ReadDouble(ref span);
+            span = ReadSeparator(span);
+            var y = ReadDouble(ref span);
+            return new Point(origin.X + x, origin.Y + y);
+        }
 
-                return true;
+        private bool ReadCommand(ref ReadOnlySpan<char> span, out Command command, out bool relative)
+        {
+            span = SkipWhitespace(span);
+            if (span.IsEmpty)
+            {
+                command = default;
+                relative = false;
+                return false;
             }
-
-            private static void ReadWhitespace(TextReader reader)
+            var c = span[0];
+            if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command))
             {
-                int i;
-
-                while ((i = reader.Peek()) != -1)
-                {
-                    var c = (char)i;
-
-                    if (char.IsWhiteSpace(c))
-                    {
-                        reader.Read();
-                    }
-                    else
-                    {
-                        break;
-                    }
-                }
+                throw new InvalidDataException("Unexpected path command '" + c + "'.");
             }
+            relative = char.IsLower(c);
+            span = span.Slice(1);
+            return true;
         }
     }
 }

+ 1 - 1
tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

@@ -69,7 +69,7 @@ namespace Avalonia.Visuals.UnitTests.Media
             using (var context = new PathGeometryContext(pathGeometry))
             using (var parser = new PathMarkupParser(context))
             {
-                parser.Parse("F 1M0,0");             
+                parser.Parse("F 1M0,0");
 
                 Assert.Equal(FillRule.NonZero, pathGeometry.FillRule);
             }