浏览代码

Implemented PropertyPathGrammar

Nikita Tsukanov 6 年之前
父节点
当前提交
048660ed8b

+ 21 - 3
src/Avalonia.Base/Utilities/CharacterReader.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Utilities
         }
 
         public bool End => _s.IsEmpty;
-        public char Peek => _s[0];
+        public char PeekOneOrThrow => _s[0];
         public int Position { get; private set; }
         public char Take()
         {
@@ -38,7 +38,7 @@ namespace Avalonia.Utilities
 
         public bool TakeIf(char c)
         {
-            if (Peek == c)
+            if (PeekOneOrThrow == c)
             {
                 Take();
                 return true;
@@ -51,7 +51,7 @@ namespace Avalonia.Utilities
 
         public bool TakeIf(Func<char, bool> condition)
         {
-            if (condition(Peek))
+            if (condition(PeekOneOrThrow))
             {
                 Take();
                 return true;
@@ -82,5 +82,23 @@ namespace Avalonia.Utilities
             Position += len;
             return span;
         }
+
+        public ReadOnlySpan<char> TryPeek(int count)
+        {
+            return _s.Slice(0, count);
+        }
+
+        public ReadOnlySpan<char> PeekWhitespace()
+        {
+            var trimmed = _s.TrimStart();
+            return _s.Slice(0, _s.Length - trimmed.Length);
+        }
+
+        public void Skip(int count)
+        {
+            if (_s.Length < count)
+                throw new IndexOutOfRangeException();
+            _s = _s.Slice(count);
+        }
     }
 }

+ 1 - 1
src/Avalonia.Base/Utilities/IdentifierParser.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Utilities
     {
         public static ReadOnlySpan<char> ParseIdentifier(this ref CharacterReader r)
         {
-            if (IsValidIdentifierStart(r.Peek))
+            if (IsValidIdentifierStart(r.PeekOneOrThrow))
             {
                 return r.TakeWhile(IsValidIdentifierChar);
             }

+ 34 - 0
src/Avalonia.Base/Utilities/KeywordParser.cs

@@ -0,0 +1,34 @@
+using System;
+
+namespace Avalonia.Utilities
+{
+#if !BUILDTASK
+    public
+#endif
+    static class KeywordParser
+    {
+        public static bool CheckKeyword(this ref CharacterReader r, string keyword)
+        {
+            return (CheckKeywordInternal(ref r, keyword) >= 0);
+        }
+        
+        static int CheckKeywordInternal(this ref CharacterReader r, string keyword)
+        {
+            var ws = r.PeekWhitespace();
+
+            var chars = r.TryPeek(ws.Length + keyword.Length);
+            if (chars.Slice(ws.Length).Equals(keyword.AsSpan(), StringComparison.Ordinal))
+                return chars.Length;
+            return -1;
+        }
+
+        public static bool TakeIfKeyword(this ref CharacterReader r, string keyword)
+        {
+            var l = CheckKeywordInternal(ref r, keyword);
+            if (l < 0)
+                return false;
+            r.Skip(l);
+            return true;
+        }
+    }
+}

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs

@@ -41,7 +41,7 @@ namespace Avalonia.Markup.Xaml.Parsers
                             throw new ExpressionParseException(r.Position, $"Expected ')'.");
                         }
 
-                        throw new ExpressionParseException(r.Position, $"Unexpected '{r.Peek}'.");
+                        throw new ExpressionParseException(r.Position, $"Unexpected '{r.PeekOneOrThrow}'.");
                     }
                 }
                 else if (!r.End && r.TakeIf(':'))

+ 1 - 1
src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Markup.Parsers
     {
         public static IList<string> ParseArguments(this ref CharacterReader r, char open, char close, char delimiter = ',')
         {
-            if (r.Peek == open)
+            if (r.PeekOneOrThrow == open)
             {
                 var result = new List<string>();
 

+ 1 - 1
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs

@@ -304,7 +304,7 @@ namespace Avalonia.Markup.Parsers
 
         private static bool PeekOpenBracket(ref CharacterReader r)
         {
-            return !r.End && r.Peek == '[';
+            return !r.End && r.PeekOneOrThrow == '[';
         }
 
         private static bool ParseStreamOperator(ref CharacterReader r)

+ 217 - 0
src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs

@@ -0,0 +1,217 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Data.Core;
+using Avalonia.Utilities;
+
+namespace Avalonia.Markup.Parsers
+{
+#if !BUILDTASK
+    public
+#endif
+    class PropertyPathGrammar
+    {
+        private enum State
+        {
+            Start,
+            Next,
+            AfterProperty,
+            End
+        }
+
+        public static IEnumerable<ISyntax> Parse(string s)
+        {
+            var r = new CharacterReader(s.AsSpan());
+            return Parse(ref r);
+        }
+
+        private static IEnumerable<ISyntax> Parse(ref CharacterReader r)
+        {
+            var state = State.Start;
+            var parsed = new List<ISyntax>();
+            while (state != State.End)
+            {
+                ISyntax syntax = null;
+                if (state == State.Start)
+                    (state, syntax) = ParseStart(ref r);
+                else if (state == State.Next)
+                    (state, syntax) = ParseNext(ref r);
+                else if (state == State.AfterProperty)
+                    (state, syntax) = ParseAfterProperty(ref r);
+                
+                
+                if (syntax != null)
+                {
+                    parsed.Add(syntax);
+                }
+            }
+
+            if (state != State.End && r.End)
+            {
+                throw new ExpressionParseException(r.Position, "Unexpected end of property path");
+            }
+
+            return parsed;
+        }
+        
+        private static (State, ISyntax) ParseNext(ref CharacterReader r)
+        {
+            r.SkipWhitespace();
+            if (r.End)
+                return (State.End, null);
+            
+            return ParseStart(ref r);
+        }
+        
+        private static (State, ISyntax) ParseStart(ref CharacterReader r)
+        {
+            r.SkipWhitespace();
+
+            if (r.TakeIf('('))
+                return ParseTypeQualifiedProperty(ref r);
+
+            return ParseProperty(ref r);
+        }
+
+        private static (State, ISyntax) ParseTypeQualifiedProperty(ref CharacterReader r)
+        {
+            r.SkipWhitespace();
+            const string error =
+                "Unable to parse qualified property name, expected `(ns:TypeName.PropertyName)` or `(TypeName.PropertyName)` after `(`";
+
+            var typeName = ParseXamlIdentifier(ref r);
+            
+            
+            if (!r.TakeIf('.'))
+                throw new ExpressionParseException(r.Position, error);
+
+            var propertyName = r.ParseIdentifier();
+            if (propertyName.IsEmpty)
+                throw new ExpressionParseException(r.Position, error);
+
+            r.SkipWhitespace();
+            if (!r.TakeIf(')'))
+                throw new ExpressionParseException(r.Position,
+                    "Expected ')' after qualified property name "
+                    + typeName.ns + ':' + typeName.name +
+                    "." + propertyName.ToString());
+
+            return (State.AfterProperty,
+                new TypeQualifiedPropertySyntax
+                {
+                    Name = propertyName.ToString(),
+                    TypeName = typeName.name,
+                    TypeNamespace = typeName.ns
+                });
+        }
+
+        static (string ns, string name) ParseXamlIdentifier(ref CharacterReader r)
+        {
+            var ident = r.ParseIdentifier();
+            if (ident.IsEmpty)
+                throw new ExpressionParseException(r.Position, "Expected identifier");
+            if (r.TakeIf(':'))
+            {
+                var part2 = r.ParseIdentifier();
+                if (part2.IsEmpty)
+                    throw new ExpressionParseException(r.Position,
+                        "Expected the rest of the identifier after " + ident.ToString() + ":");
+                return (ident.ToString(), part2.ToString());
+            }
+
+            return (null, ident.ToString());
+        }
+        
+        private static (State, ISyntax) ParseProperty(ref CharacterReader r)
+        {
+            r.SkipWhitespace();
+            var prop = r.ParseIdentifier();
+            if (prop.IsEmpty)
+                throw new ExpressionParseException(r.Position, "Unable to parse property name");
+            return (State.AfterProperty, new PropertySyntax {Name = prop.ToString()});
+        }
+
+
+        private static (State, ISyntax) ParseAfterProperty(ref CharacterReader r)
+        {
+            r.SkipWhitespace();
+            if (r.End)
+                return (State.End, null);
+            if (r.TakeIf('.'))
+                return (State.Next, ChildTraversalSyntax.Instance);
+            if (r.TakeIfKeyword(":="))
+                return ParseEnsureType(ref r);
+
+            if (r.TakeIfKeyword(":>") || r.TakeIfKeyword("as "))
+                return ParseCastType(ref r);
+            
+            throw new ExpressionParseException(r.Position, "Unexpected character " + r.PeekOneOrThrow + " after property name");
+        }
+
+        private static (State, ISyntax) ParseEnsureType(ref CharacterReader r)
+        {
+            r.SkipWhitespace();
+            var type = ParseXamlIdentifier(ref r);
+            return (State.AfterProperty, new EnsureTypeSyntax {TypeName = type.name, TypeNamespace = type.ns});
+        }
+        
+        private static (State, ISyntax) ParseCastType(ref CharacterReader r)
+        {
+            r.SkipWhitespace();
+            var type = ParseXamlIdentifier(ref r);
+            return (State.AfterProperty, new CastTypeSyntax {TypeName = type.name, TypeNamespace = type.ns});
+        }
+
+        public interface ISyntax
+        {
+            
+        }
+
+        public class PropertySyntax : ISyntax
+        {
+            public string Name { get; set; }
+
+            public override bool Equals(object obj)
+                => obj is PropertySyntax other
+                   && other.Name == Name;
+        }
+        
+        public class TypeQualifiedPropertySyntax : ISyntax
+        {
+            public string Name { get; set; }
+            public string TypeName { get; set; }
+            public string TypeNamespace { get; set; }
+
+            public override bool Equals(object obj)
+                => obj is TypeQualifiedPropertySyntax other
+                   && other.Name == Name
+                   && other.TypeName == TypeName
+                   && other.TypeNamespace == TypeNamespace;
+        }
+        
+        public class ChildTraversalSyntax : ISyntax
+        {
+            public static ChildTraversalSyntax Instance { get;  } = new ChildTraversalSyntax();
+            public override bool Equals(object obj) => obj is ChildTraversalSyntax;
+        }
+        
+        public class EnsureTypeSyntax : ISyntax
+        {
+            public string TypeName { get; set; }
+            public string TypeNamespace { get; set; }
+            public override bool Equals(object obj)
+                => obj is EnsureTypeSyntax other
+                   && other.TypeName == TypeName
+                   && other.TypeNamespace == TypeNamespace;
+        }
+        
+        public class CastTypeSyntax : ISyntax
+        {
+            public string TypeName { get; set; }
+            public string TypeNamespace { get; set; }
+            public override bool Equals(object obj)
+                => obj is CastTypeSyntax other
+                   && other.TypeName == TypeName
+                   && other.TypeNamespace == TypeNamespace;
+        }
+    }
+}

+ 5 - 5
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

@@ -123,7 +123,7 @@ namespace Avalonia.Markup.Parsers
             {
                 return (State.Class, null);
             }
-            else if (r.TakeIf(char.IsWhiteSpace) || r.Peek == '>')
+            else if (r.TakeIf(char.IsWhiteSpace) || r.PeekOneOrThrow == '>')
             {
                 return (State.Traversal, null);
             }
@@ -139,7 +139,7 @@ namespace Avalonia.Markup.Parsers
             {
                 return (State.Start, new CommaSyntax());
             }
-            else if (end.HasValue && !r.End && r.Peek == end.Value)
+            else if (end.HasValue && !r.End && r.PeekOneOrThrow == end.Value)
             {
                 return (State.End, null);
             }
@@ -262,7 +262,7 @@ namespace Avalonia.Markup.Parsers
 
             if (!r.TakeIf('='))
             {
-                throw new ExpressionParseException(r.Position, $"Expected '=', got '{r.Peek}'");
+                throw new ExpressionParseException(r.Position, $"Expected '=', got '{r.PeekOneOrThrow}'");
             }
 
             var value = r.TakeUntil(']');
@@ -281,7 +281,7 @@ namespace Avalonia.Markup.Parsers
 
             if (namespaceOrTypeName.IsEmpty)
             {
-                throw new ExpressionParseException(r.Position, $"Expected an identifier, got '{r.Peek}");
+                throw new ExpressionParseException(r.Position, $"Expected an identifier, got '{r.PeekOneOrThrow}");
             }
 
             if (!r.End && r.TakeIf('|'))
@@ -311,7 +311,7 @@ namespace Avalonia.Markup.Parsers
             }
             else if (!r.TakeIf(')'))
             {
-                throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'.");
+                throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.PeekOneOrThrow}'.");
             }
         }
 

+ 103 - 0
tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs

@@ -0,0 +1,103 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Markup.Parsers;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Data.Core
+{
+    public class PropertyPathGrammarTests
+    {
+        static void Check(string s, params PropertyPathGrammar.ISyntax[] expected)
+        {
+            var parsed = PropertyPathGrammar.Parse(s).ToList();
+            Assert.Equal(expected.Length, parsed.Count);
+            for (var c = 0; c < parsed.Count; c++)
+                Assert.Equal(expected[c], parsed[c]);
+        }
+
+        [Fact]
+        public void PropertyPath_Should_Support_Simple_Properties()
+        {
+            Check("SomeProperty", new PropertyPathGrammar.PropertySyntax {Name = "SomeProperty"});
+        }
+
+        [Fact]
+        public void PropertyPath_Should_Ignore_Trailing_Whitespace()
+        {
+            Check("  SomeProperty   ", new PropertyPathGrammar.PropertySyntax {Name = "SomeProperty"});
+        }
+
+        [Fact]
+        public void PropertyPath_Should_Support_Qualified_Properties()
+        {
+            Check(" ( somens:SomeType.SomeProperty ) ",
+                new PropertyPathGrammar.TypeQualifiedPropertySyntax()
+                {
+                    Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens"
+                });
+        }
+        
+        [Fact]
+        public void PropertyPath_Should_Support_Property_Paths()
+        {
+            Check(" ( somens:SomeType.SomeProperty ).Child . SubChild ",
+                new PropertyPathGrammar.TypeQualifiedPropertySyntax()
+                {
+                    Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens"
+                },
+                PropertyPathGrammar.ChildTraversalSyntax.Instance,
+                new PropertyPathGrammar.PropertySyntax {Name = "Child"},
+                PropertyPathGrammar.ChildTraversalSyntax.Instance,
+                new PropertyPathGrammar.PropertySyntax {Name = "SubChild"}
+            );
+        }
+        
+        [Fact]
+        public void PropertyPath_Should_Support_Casts()
+        {
+            Check(" ( somens:SomeType.SomeProperty ) :> SomeType.Child as somens:SomeType . SubChild ",
+                new PropertyPathGrammar.TypeQualifiedPropertySyntax()
+                {
+                    Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens"
+                },
+                new PropertyPathGrammar.CastTypeSyntax
+                {
+                    TypeName = "SomeType"
+                },
+                PropertyPathGrammar.ChildTraversalSyntax.Instance,
+                new PropertyPathGrammar.PropertySyntax {Name = "Child"},
+                new PropertyPathGrammar.CastTypeSyntax
+                {
+                    TypeName = "SomeType",
+                    TypeNamespace = "somens"
+                },
+                PropertyPathGrammar.ChildTraversalSyntax.Instance,
+                new PropertyPathGrammar.PropertySyntax {Name = "SubChild"}
+            );
+        }
+        
+        [Fact]
+        public void PropertyPath_Should_Support_Ensure_Type()
+        {
+            Check(" ( somens:SomeType.SomeProperty ) := SomeType.Child := somens:SomeType . SubChild ",
+                new PropertyPathGrammar.TypeQualifiedPropertySyntax()
+                {
+                    Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens"
+                },
+                new PropertyPathGrammar.EnsureTypeSyntax
+                {
+                    TypeName = "SomeType"
+                },
+                PropertyPathGrammar.ChildTraversalSyntax.Instance,
+                new PropertyPathGrammar.PropertySyntax {Name = "Child"},
+                new PropertyPathGrammar.EnsureTypeSyntax
+                {
+                    TypeName = "SomeType",
+                    TypeNamespace = "somens"
+                },
+                PropertyPathGrammar.ChildTraversalSyntax.Instance,
+                new PropertyPathGrammar.PropertySyntax {Name = "SubChild"}
+            );
+        }
+    }
+}