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