9 changed files with 384 additions and 12 deletions
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue