diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 673eb51901..aca39dd8b0 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -3,6 +3,12 @@ netstandard2.0 Avalonia + + + + + + diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs new file mode 100644 index 0000000000..b8308155fe --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -0,0 +1,386 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers.Nodes; +using Avalonia.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Markup.Parsers +{ + internal enum SourceMode + { + Data, + Control + } + internal static class BindingExpressionGrammar + { + public static (IList Nodes, SourceMode Mode) Parse(ref CharacterReader r) + { + var nodes = new List(); + var state = State.Start; + var mode = SourceMode.Data; + + while (!r.End && state != State.End) + { + switch (state) + { + case State.Start: + state = ParseStart(ref r, nodes); + break; + + case State.AfterMember: + state = ParseAfterMember(ref r, nodes); + break; + + case State.BeforeMember: + state = ParseBeforeMember(ref r, nodes); + break; + + case State.AttachedProperty: + state = ParseAttachedProperty(ref r, nodes); + break; + + case State.Indexer: + state = ParseIndexer(ref r, nodes); + break; + + case State.ElementName: + state = ParseElementName(ref r, nodes); + mode = SourceMode.Control; + break; + + case State.RelativeSource: + state = ParseRelativeSource(ref r, nodes); + mode = SourceMode.Control; + break; + } + } + + if (state == State.BeforeMember) + { + throw new ExpressionParseException(r.Position, "Unexpected end of expression."); + } + + return (nodes, mode); + } + + private static State ParseStart(ref CharacterReader r, IList nodes) + { + if (ParseNot(ref r)) + { + nodes.Add(new NotNode()); + return State.Start; + } + + else if (ParseSharp(ref r)) + { + return State.ElementName; + } + else if (ParseDollarSign(ref r)) + { + return State.RelativeSource; + } + else if (ParseOpenBrace(ref r)) + { + return State.AttachedProperty; + } + else if (PeekOpenBracket(ref r)) + { + return State.Indexer; + } + else if (ParseDot(ref r)) + { + nodes.Add(new EmptyExpressionNode()); + return State.End; + } + else + { + var identifier = r.ParseIdentifier(); + + if (!identifier.IsEmpty) + { + nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + return State.AfterMember; + } + } + + return State.End; + } + + private static State ParseAfterMember(ref CharacterReader r, IList nodes) + { + if (ParseMemberAccessor(ref r)) + { + return State.BeforeMember; + } + else if (ParseStreamOperator(ref r)) + { + nodes.Add(new StreamNode()); + return State.AfterMember; + } + else if (PeekOpenBracket(ref r)) + { + return State.Indexer; + } + + return State.End; + } + + private static State ParseBeforeMember(ref CharacterReader r, IList nodes) + { + if (ParseOpenBrace(ref r)) + { + return State.AttachedProperty; + } + else + { + var identifier = r.ParseIdentifier(); + + if (!identifier.IsEmpty) + { + nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + return State.AfterMember; + } + + return State.End; + } + } + + private static State ParseAttachedProperty(ref CharacterReader r, List nodes) + { + var (ns, owner) = ParseTypeName(ref r); + + if (r.End || !r.TakeIf('.')) + { + throw new ExpressionParseException(r.Position, "Invalid attached property name."); + } + + var name = r.ParseIdentifier(); + + if (r.End || !r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, "Expected ')'."); + } + + nodes.Add(new AttachedPropertyNameNode + { + Namespace = ns.ToString(), + TypeName = owner.ToString(), + PropertyName = name.ToString() + }); + return State.AfterMember; + } + + private static State ParseIndexer(ref CharacterReader r, List nodes) + { + var args = r.ParseArguments('[', ']'); + + if (args.Count == 0) + { + throw new ExpressionParseException(r.Position, "Indexer may not be empty."); + } + + nodes.Add(new IndexerNode { Arguments = args }); + return State.AfterMember; + } + + private static State ParseElementName(ref CharacterReader r, List nodes) + { + var name = r.ParseIdentifier(); + + if (name == null) + { + throw new ExpressionParseException(r.Position, "Element name expected after '#'."); + } + + nodes.Add(new NameNode { Name = name.ToString() }); + return State.AfterMember; + } + + private static State ParseRelativeSource(ref CharacterReader r, List nodes) + { + var mode = r.ParseIdentifier(); + + if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture)) + { + nodes.Add(new SelfNode()); + } + else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture)) + { + string ancestorNamespace = null; + string ancestorType = null; + var ancestorLevel = 0; + if (PeekOpenBracket(ref r)) + { + var args = r.ParseArguments('[', ']', ';'); + if (args.Count > 2 || args.Count == 0) + { + throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar"); + } + else if (args.Count == 1) + { + if (int.TryParse(args[0], out int level)) + { + ancestorType = null; + ancestorLevel = level; + } + else + { + var reader = new CharacterReader(args[0].AsSpan()); + (ancestorNamespace, ancestorType) = ParseTypeName(ref reader); + } + } + else + { + var reader = new CharacterReader(args[0].AsSpan()); + (ancestorNamespace, ancestorType) = ParseTypeName(ref reader); + ancestorLevel = int.Parse(args[1]); + } + } + nodes.Add(new AncestorNode + { + Namespace = ancestorNamespace, + TypeName = ancestorType, + Level = ancestorLevel + }); + } + else + { + throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode."); + } + + return State.AfterMember; + } + + private static TypeName ParseTypeName(ref CharacterReader r) + { + ReadOnlySpan ns, typeName; + ns = ReadOnlySpan.Empty; + var typeNameOrNamespace = r.ParseIdentifier(); + + if (!r.End && r.TakeIf(':')) + { + ns = typeNameOrNamespace; + typeName = r.ParseIdentifier(); + } + else + { + typeName = typeNameOrNamespace; + } + + return new TypeName(ns, typeName); + } + + private static bool ParseNot(ref CharacterReader r) + { + return !r.End && r.TakeIf('!'); + } + + private static bool ParseMemberAccessor(ref CharacterReader r) + { + return !r.End && r.TakeIf('.'); + } + + private static bool ParseOpenBrace(ref CharacterReader r) + { + return !r.End && r.TakeIf('('); + } + + private static bool PeekOpenBracket(ref CharacterReader r) + { + return !r.End && r.PeekOneOrThrow == '['; + } + + private static bool ParseStreamOperator(ref CharacterReader r) + { + return !r.End && r.TakeIf('^'); + } + + private static bool ParseDollarSign(ref CharacterReader r) + { + return !r.End && r.TakeIf('$'); + } + + private static bool ParseSharp(ref CharacterReader r) + { + return !r.End && r.TakeIf('#'); + } + + private static bool ParseDot(ref CharacterReader r) + { + return !r.End && r.TakeIf('.'); + } + + private enum State + { + Start, + RelativeSource, + ElementName, + AfterMember, + BeforeMember, + AttachedProperty, + Indexer, + End, + } + + private readonly ref struct TypeName + { + public TypeName(ReadOnlySpan ns, ReadOnlySpan typeName) + { + Namespace = ns; + Type = typeName; + } + + public readonly ReadOnlySpan Namespace; + public readonly ReadOnlySpan Type; + + public void Deconstruct(out string ns, out string typeName) + { + ns = Namespace.ToString(); + typeName = Type.ToString(); + } + } + + public interface INode {} + + public class EmptyExpressionNode : INode { } + + public class PropertyNameNode : INode + { + public string PropertyName { get; set; } + } + + public class AttachedPropertyNameNode : INode + { + public string Namespace { get; set; } + public string TypeName { get; set; } + public string PropertyName { get; set; } + } + + public class IndexerNode : INode + { + public IList Arguments { get; set; } + } + + public class NotNode : INode {} + + public class StreamNode : INode {} + + public class SelfNode : INode {} + + public class NameNode : INode + { + public string Name { get; set; } + } + + public class AncestorNode : INode + { + public string Namespace { get; set; } + public string TypeName { get; set; } + public int Level { get; set; } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 2a737d36e9..fe58ac139e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -10,12 +10,6 @@ using System.Linq; namespace Avalonia.Markup.Parsers { - internal enum SourceMode - { - Data, - Control - } - internal class ExpressionParser { private readonly bool _enableValidation; @@ -29,332 +23,83 @@ namespace Avalonia.Markup.Parsers public (ExpressionNode Node, SourceMode Mode) Parse(ref CharacterReader r) { - var nodes = new List(); - var state = State.Start; - var mode = SourceMode.Data; + ExpressionNode node = null; + var (astNodes, mode) = BindingExpressionGrammar.Parse(ref r); - while (!r.End && state != State.End) + foreach (var astNode in astNodes) { - switch (state) + ExpressionNode nextNode = null; + switch (astNode) { - case State.Start: - state = ParseStart(ref r, nodes); + case BindingExpressionGrammar.EmptyExpressionNode _: + nextNode = new EmptyExpressionNode(); break; - - case State.AfterMember: - state = ParseAfterMember(ref r, nodes); + case BindingExpressionGrammar.NotNode _: + nextNode = new LogicalNotNode(); break; - - case State.BeforeMember: - state = ParseBeforeMember(ref r, nodes); + case BindingExpressionGrammar.StreamNode _: + nextNode = new StreamNode(); break; - - case State.AttachedProperty: - state = ParseAttachedProperty(ref r, nodes); + case BindingExpressionGrammar.PropertyNameNode propName: + nextNode = new PropertyAccessorNode(propName.PropertyName, _enableValidation); break; - - case State.Indexer: - state = ParseIndexer(ref r, nodes); + case BindingExpressionGrammar.IndexerNode indexer: + nextNode = new StringIndexerNode(indexer.Arguments); break; - - case State.ElementName: - state = ParseElementName(ref r, nodes); - mode = SourceMode.Control; + case BindingExpressionGrammar.AttachedPropertyNameNode attachedProp: + nextNode = ParseAttachedProperty(attachedProp); break; - - case State.RelativeSource: - state = ParseRelativeSource(ref r, nodes); - mode = SourceMode.Control; + case BindingExpressionGrammar.SelfNode _: + nextNode = new SelfNode(); + break; + case BindingExpressionGrammar.AncestorNode ancestor: + nextNode = ParseFindAncestor(ancestor); + break; + case BindingExpressionGrammar.NameNode elementName: + nextNode = new ElementNameNode(elementName.Name); break; } - } - - if (state == State.BeforeMember) - { - throw new ExpressionParseException(r.Position, "Unexpected end of expression."); - } - - for (int n = 0; n < nodes.Count - 1; ++n) - { - nodes[n].Next = nodes[n + 1]; - } - - return (nodes.FirstOrDefault(), mode); - } - - private State ParseStart(ref CharacterReader r, IList nodes) - { - if (ParseNot(ref r)) - { - nodes.Add(new LogicalNotNode()); - return State.Start; - } - - else if (ParseSharp(ref r)) - { - return State.ElementName; - } - else if (ParseDollarSign(ref r)) - { - return State.RelativeSource; - } - else if (ParseOpenBrace(ref r)) - { - return State.AttachedProperty; - } - else if (PeekOpenBracket(ref r)) - { - return State.Indexer; - } - else if (ParseDot(ref r)) - { - nodes.Add(new EmptyExpressionNode()); - return State.End; - } - else - { - var identifier = r.ParseIdentifier(); - - if (!identifier.IsEmpty) + if (node is null) { - nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); - return State.AfterMember; + node = nextNode; } - } - - return State.End; - } - - private static State ParseAfterMember(ref CharacterReader r, IList nodes) - { - if (ParseMemberAccessor(ref r)) - { - return State.BeforeMember; - } - else if (ParseStreamOperator(ref r)) - { - nodes.Add(new StreamNode()); - return State.AfterMember; - } - else if (PeekOpenBracket(ref r)) - { - return State.Indexer; - } - - return State.End; - } - - private State ParseBeforeMember(ref CharacterReader r, IList nodes) - { - if (ParseOpenBrace(ref r)) - { - return State.AttachedProperty; - } - else - { - var identifier = r.ParseIdentifier(); - - if (!identifier.IsEmpty) + else { - nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); - return State.AfterMember; + node.Next = nextNode; } - - return State.End; - } - } - - private State ParseAttachedProperty(ref CharacterReader r, List nodes) - { - var (ns, owner) = ParseTypeName(ref r); - - if (r.End || !r.TakeIf('.')) - { - throw new ExpressionParseException(r.Position, "Invalid attached property name."); - } - - var name = r.ParseIdentifier(); - - if (r.End || !r.TakeIf(')')) - { - throw new ExpressionParseException(r.Position, "Expected ')'."); - } - - if (_typeResolver == null) - { - throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); - } - - var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns.ToString(), owner.ToString()), name.ToString()); - - nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation)); - return State.AfterMember; - } - - private State ParseIndexer(ref CharacterReader r, List nodes) - { - var args = r.ParseArguments('[', ']'); - - if (args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Indexer may not be empty."); } - nodes.Add(new StringIndexerNode(args)); - return State.AfterMember; + return (node, mode); } - private State ParseElementName(ref CharacterReader r, List nodes) + private FindAncestorNode ParseFindAncestor(BindingExpressionGrammar.AncestorNode node) { - var name = r.ParseIdentifier(); + Type ancestorType = null; + var ancestorLevel = node.Level; - if (name == null) + if (!(node.Namespace is null) && !(node.TypeName is null)) { - throw new ExpressionParseException(r.Position, "Element name expected after '#'."); - } - - nodes.Add(new ElementNameNode(name.ToString())); - return State.AfterMember; - } - - private State ParseRelativeSource(ref CharacterReader r, List nodes) - { - var mode = r.ParseIdentifier(); - - if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture)) - { - nodes.Add(new SelfNode()); - } - else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture)) - { - Type ancestorType = null; - var ancestorLevel = 0; - if (PeekOpenBracket(ref r)) + if (_typeResolver == null) { - var args = r.ParseArguments('[', ']', ';'); - if (args.Count > 2 || args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar"); - } - else if (args.Count == 1) - { - if (int.TryParse(args[0], out int level)) - { - ancestorType = null; - ancestorLevel = level; - } - else - { - var reader = new CharacterReader(args[0].AsSpan()); - var typeName = ParseTypeName(ref reader); - ancestorType = _typeResolver(typeName.Namespace.ToString(), typeName.Type.ToString()); - } - } - else - { - var reader = new CharacterReader(args[0].AsSpan()); - var typeName = ParseTypeName(ref reader); - ancestorType = _typeResolver(typeName.Namespace.ToString(), typeName.Type.ToString()); - ancestorLevel = int.Parse(args[1]); - } + throw new InvalidOperationException("Cannot parse a binding path with a typed FindAncestor without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); } - nodes.Add(new FindAncestorNode(ancestorType, ancestorLevel)); - } - else - { - throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode."); - } - return State.AfterMember; - } - - private static TypeName ParseTypeName(ref CharacterReader r) - { - ReadOnlySpan ns, typeName; - ns = ReadOnlySpan.Empty; - var typeNameOrNamespace = r.ParseIdentifier(); - - if (!r.End && r.TakeIf(':')) - { - ns = typeNameOrNamespace; - typeName = r.ParseIdentifier(); + ancestorType = _typeResolver(node.Namespace, node.TypeName); } - else - { - typeName = typeNameOrNamespace; - } - - return new TypeName(ns, typeName); - } - - private static bool ParseNot(ref CharacterReader r) - { - return !r.End && r.TakeIf('!'); - } - - private static bool ParseMemberAccessor(ref CharacterReader r) - { - return !r.End && r.TakeIf('.'); - } - - private static bool ParseOpenBrace(ref CharacterReader r) - { - return !r.End && r.TakeIf('('); - } - - private static bool PeekOpenBracket(ref CharacterReader r) - { - return !r.End && r.PeekOneOrThrow == '['; - } - - private static bool ParseStreamOperator(ref CharacterReader r) - { - return !r.End && r.TakeIf('^'); - } - - private static bool ParseDollarSign(ref CharacterReader r) - { - return !r.End && r.TakeIf('$'); - } - - private static bool ParseSharp(ref CharacterReader r) - { - return !r.End && r.TakeIf('#'); - } - private static bool ParseDot(ref CharacterReader r) - { - return !r.End && r.TakeIf('.'); - } - - private enum State - { - Start, - RelativeSource, - ElementName, - AfterMember, - BeforeMember, - AttachedProperty, - Indexer, - End, + return new FindAncestorNode(ancestorType, ancestorLevel); } - private readonly ref struct TypeName + private AvaloniaPropertyAccessorNode ParseAttachedProperty(BindingExpressionGrammar.AttachedPropertyNameNode node) { - public TypeName(ReadOnlySpan ns, ReadOnlySpan typeName) + if (_typeResolver == null) { - Namespace = ns; - Type = typeName; + throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); } - public readonly ReadOnlySpan Namespace; - public readonly ReadOnlySpan Type; + var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(node.Namespace, node.TypeName), node.PropertyName); - public void Deconstruct(out ReadOnlySpan ns, out ReadOnlySpan typeName) - { - ns = Namespace; - typeName = Type; - } + return new AvaloniaPropertyAccessorNode(property, _enableValidation); } } }