Browse Source

Implemented PropertyPathGrammar

property-path
Nikita Tsukanov 7 years ago
parent
commit
048660ed8b
  1. 24
      src/Avalonia.Base/Utilities/CharacterReader.cs
  2. 2
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  3. 34
      src/Avalonia.Base/Utilities/KeywordParser.cs
  4. 2
      src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs
  5. 2
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  6. 2
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  7. 217
      src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs
  8. 10
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  9. 103
      tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs

24
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);
}
}
}

2
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
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;
}
}
}

2
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(':'))

2
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>();

2
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
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;
}
}
}

10
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
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"}
);
}
}
}
Loading…
Cancel
Save