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