From 2f087c350cb2460adb95718f100415981fc9b94d Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 9 Jun 2018 16:52:42 -0500 Subject: [PATCH 01/12] Create new Selector parser that does not have a dependency on Sprache. --- .../Markup/Parsers/ExpressionParser.cs | 10 +- .../Markup/Parsers/IdentifierParser.cs | 2 +- .../Avalonia.Markup/Markup/Parsers/Reader.cs | 21 + .../Markup/Parsers/SelectorGrammar.cs | 377 +++++++++++++----- .../Markup/Parsers/SelectorParser.cs | 117 +++--- .../Parsers/SelectorGrammarTests.cs | 40 +- .../Parsers/SelectorParserTests.cs | 4 +- 7 files changed, 374 insertions(+), 197 deletions(-) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 6919eeeb0d..8f609ba2bd 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -81,7 +81,7 @@ namespace Avalonia.Markup.Parsers } else { - var identifier = IdentifierParser.Parse(r); + var identifier = r.ParseIdentifier(); if (identifier != null) { @@ -120,7 +120,7 @@ namespace Avalonia.Markup.Parsers } else { - var identifier = IdentifierParser.Parse(r); + var identifier = r.ParseIdentifier(); if (identifier != null) { @@ -136,12 +136,12 @@ namespace Avalonia.Markup.Parsers { string ns = string.Empty; string owner; - var ownerOrNamespace = IdentifierParser.Parse(r); + var ownerOrNamespace = r.ParseIdentifier(); if (r.TakeIf(':')) { ns = ownerOrNamespace; - owner = IdentifierParser.Parse(r); + owner = r.ParseIdentifier(); } else { @@ -153,7 +153,7 @@ namespace Avalonia.Markup.Parsers throw new ExpressionParseException(r.Position, "Invalid attached property name."); } - var name = IdentifierParser.Parse(r); + var name = r.ParseIdentifier(); if (r.End || !r.TakeIf(')')) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs index f86f2db321..470a4c8203 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs @@ -8,7 +8,7 @@ namespace Avalonia.Markup.Parsers { internal static class IdentifierParser { - public static string Parse(Reader r) + public static string ParseIdentifier(this Reader r) { if (IsValidIdentifierStart(r.Peek)) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs index 9355bc9aa3..6511773bea 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Text; namespace Avalonia.Markup.Parsers { @@ -40,5 +41,25 @@ namespace Avalonia.Markup.Parsers return false; } } + + public bool TakeIf(Func condition) + { + if (condition(Peek)) + { + Take(); + return true; + } + return false; + } + + public string TakeUntil(char c) + { + var builder = new StringBuilder(); + while (!End && Peek != c) + { + builder.Append(Take()); + } + return builder.ToString(); + } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 014f08d995..20e49341b7 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Globalization; -using Sprache; +using Avalonia.Data.Core; // Don't need to override GetHashCode as the ISyntax objects will not be stored in a hash; the // only reason they have overridden Equals methods is for unit testing. @@ -13,117 +13,280 @@ namespace Avalonia.Markup.Parsers { internal class SelectorGrammar { - public static readonly Parser CombiningCharacter = Parse.Char( - c => - { - var cat = CharUnicodeInfo.GetUnicodeCategory(c); - return cat == UnicodeCategory.NonSpacingMark || - cat == UnicodeCategory.SpacingCombiningMark; - }, - "Connecting Character"); - - public static readonly Parser ConnectingCharacter = Parse.Char( - c => CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.ConnectorPunctuation, - "Connecting Character"); - - public static readonly Parser FormattingCharacter = Parse.Char( - c => CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.Format, - "Connecting Character"); - - public static readonly Parser IdentifierStart = Parse.Letter.Or(Parse.Char('_')); - - public static readonly Parser IdentifierChar = Parse - .LetterOrDigit - .Or(ConnectingCharacter) - .Or(CombiningCharacter) - .Or(FormattingCharacter); - - public static readonly Parser Identifier = - from start in IdentifierStart.Once().Text() - from @char in IdentifierChar.Many().Text() - select start + @char; - - public static readonly Parser Namespace = - from ns in Parse.Letter.Many().Text() - from bar in Parse.Char('|') - select ns; - - public static readonly Parser OfType = - from ns in Namespace.Optional() - from identifier in Identifier - select new OfTypeSyntax - { - TypeName = identifier, - Xmlns = ns.GetOrDefault(), + private enum State + { + Start, + Middle, + Colon, + Class, + Name, + CanHaveType, + Traversal, + TypeName, + Property, + Template, + End, + } + + public static IEnumerable Parse(string s) + { + var r = new Reader(s); + var state = State.Start; + var selector = new List(); + while (!r.End && state != State.End) + { + ISyntax syntax = null; + switch (state) + { + case State.Start: + (state, syntax) = ParseStart(r); + break; + case State.Middle: + (state, syntax) = ParseMiddle(r); + break; + case State.Colon: + (state, syntax) = ParseColon(r); + break; + case State.Class: + (state, syntax) = ParseClass(r); + break; + case State.Traversal: + (state, syntax) = ParseTraversal(r); + break; + case State.TypeName: + (state, syntax) = ParseTypeName(r); + break; + case State.CanHaveType: + (state, syntax) = ParseCanHaveType(r); + break; + case State.Property: + (state, syntax) = ParseProperty(r); + break; + case State.Template: + (state, syntax) = ParseTemplate(r); + break; + case State.Name: + (state, syntax) = ParseName(r); + break; + } + if (syntax != null) + { + selector.Add(syntax); + } + } + + if (state != State.Start && state != State.Middle && state != State.End && state != State.CanHaveType) + { + throw new ExpressionParseException(r.Position, "Unexpected end of selector"); + } + + return selector; + } + + private static (State, ISyntax) ParseStart(Reader r) + { + r.SkipWhitespace(); + if (r.TakeIf(':')) + { + return (State.Colon, null); + } + else if (r.TakeIf('.')) + { + return (State.Class, null); + } + else if (r.TakeIf('#')) + { + return (State.Name, null); + } + return (State.TypeName, null); + } + + private static (State, ISyntax) ParseMiddle(Reader r) + { + if (r.TakeIf(':')) + { + return (State.Colon, null); + } + else if (r.TakeIf('.')) + { + return (State.Class, null); + } + else if (r.TakeIf(char.IsWhiteSpace) || r.Peek == '>') + { + return (State.Traversal, null); + } + else if (r.TakeIf('/')) + { + return (State.Template, null); + } + else if (r.TakeIf('#')) + { + return (State.Name, null); + } + return (State.TypeName, null); + } + + private static (State, ISyntax) ParseCanHaveType(Reader r) + { + if (r.TakeIf('[')) + { + return (State.Property, null); + } + return (State.Middle, null); + } + + private static (State, ISyntax) ParseColon(Reader r) + { + var identifier = r.ParseIdentifier(); + + if (string.IsNullOrEmpty(identifier)) + { + throw new ExpressionParseException(r.Position, "Expected class name or is selector after ':'."); + } + + if (identifier == "is" && r.TakeIf('(')) + { + var syntax = ParseType(r); + if (r.End || !r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, $"Expected ')', got {r.Peek}"); + } + + return (State.CanHaveType, syntax); + } + else + { + return ( + State.CanHaveType, + new ClassSyntax + { + Class = ":" + identifier + }); + } + } + + private static (State, ISyntax) ParseTraversal(Reader r) + { + r.SkipWhitespace(); + if (r.TakeIf('>')) + { + r.SkipWhitespace(); + return (State.Middle, new ChildSyntax()); + } + else if (r.TakeIf('/')) + { + return (State.Template, null); + } + else if (!r.End) + { + return (State.Middle, new DescendantSyntax()); + } + else + { + return (State.End, null); + } + } + + private static (State, ISyntax) ParseClass(Reader r) + { + var @class = r.ParseIdentifier(); + if (string.IsNullOrEmpty(@class)) + { + throw new ExpressionParseException(r.Position, $"Expected a class name after '.'."); + } + + return (State.CanHaveType, new ClassSyntax { Class = @class }); + } + + private static (State, ISyntax) ParseTemplate(Reader r) + { + var template = r.ParseIdentifier(); + if (template != nameof(template)) + { + throw new ExpressionParseException(r.Position, $"Expected 'template', got {template}"); + } + else if (!r.TakeIf('/')) + { + throw new ExpressionParseException(r.Position, "Expected '/'"); + } + return (State.Start, new TemplateSyntax()); + } + + private static (State, ISyntax) ParseName(Reader r) + { + var name = r.ParseIdentifier(); + if (string.IsNullOrEmpty(name)) + { + throw new ExpressionParseException(r.Position, $"Expected a name after '#'."); + } + return (State.CanHaveType, new NameSyntax { Name = name }); + } + + private static (State, ISyntax) ParseTypeName(Reader r) + { + return (State.CanHaveType, ParseType(r)); + } + + private static (State, ISyntax) ParseProperty(Reader r) + { + var property = r.ParseIdentifier(); + + if (!r.TakeIf('=')) + { + throw new ExpressionParseException(r.Position, $"Expected '=', got '{r.Peek}'"); + } + + var value = r.TakeUntil(']'); + + r.Take(); + + return (State.CanHaveType, new PropertySyntax { Property = property, Value = value }); + } + + private static TSyntax ParseType(Reader r) + where TSyntax : ITypeSyntax, new() + { + string ns = null; + string type; + var namespaceOrTypeName = r.ParseIdentifier(); + + if (string.IsNullOrEmpty(namespaceOrTypeName)) + { + throw new ExpressionParseException(r.Position, $"Expected an identifier, got '{r.Peek}"); + } + + if (!r.End && r.TakeIf('|')) + { + ns = namespaceOrTypeName; + if (r.End) + { + throw new ExpressionParseException(r.Position, $"Unexpected end of selector."); + } + type = r.ParseIdentifier(); + } + else + { + type = namespaceOrTypeName; + } + return new TSyntax + { + Xmlns = ns, + TypeName = type }; + } - public static readonly Parser Name = - from hash in Parse.Char('#') - from identifier in Identifier - select new NameSyntax { Name = identifier }; - - public static readonly Parser ClassStart = Parse.Char('_').Or(Parse.Letter); - - public static readonly Parser ClassChar = ClassStart.Or(Parse.Numeric); - - public static readonly Parser ClassIdentifier = - from start in ClassStart.Once().Text() - from @char in ClassChar.Many().Text() - select start + @char; - - public static readonly Parser StandardClass = - from dot in Parse.Char('.').Once() - from identifier in ClassIdentifier - select new ClassSyntax { Class = identifier }; - - public static readonly Parser Pseduoclass = - from colon in Parse.Char(':').Once() - from identifier in ClassIdentifier - select new ClassSyntax { Class = ':' + identifier }; - - public static readonly Parser Class = StandardClass.Or(Pseduoclass); - - public static readonly Parser Property = - from open in Parse.Char('[').Once() - from identifier in Identifier - from eq in Parse.Char('=').Once() - from value in Parse.CharExcept(']').Many().Text() - from close in Parse.Char(']').Once() - select new PropertySyntax { Property = identifier, Value = value }; - - public static readonly Parser Child = Parse.Char('>').Token().Return(new ChildSyntax()); - - public static readonly Parser Descendant = - from child in Parse.WhiteSpace.Many() - select new DescendantSyntax(); - - public static readonly Parser Template = - from template in Parse.String("/template/").Token() - select new TemplateSyntax(); - - public static readonly Parser Is = - from function in Parse.String(":is(") - from type in OfType - from close in Parse.Char(')') - select new IsSyntax { TypeName = type.TypeName, Xmlns = type.Xmlns }; - - public static readonly Parser SingleSelector = - OfType - .Or(Is) - .Or(Name) - .Or(Class) - .Or(Property) - .Or(Child) - .Or(Template) - .Or(Descendant); - - public static readonly Parser> Selector = SingleSelector.Many().End(); - public interface ISyntax { } - public class OfTypeSyntax : ISyntax + public interface ITypeSyntax + { + string TypeName { get; set; } + + string Xmlns { get; set; } + } + + public class OfTypeSyntax : ISyntax, ITypeSyntax { public string TypeName { get; set; } @@ -136,7 +299,7 @@ namespace Avalonia.Markup.Parsers } } - public class IsSyntax : ISyntax + public class IsSyntax : ISyntax, ITypeSyntax { public string TypeName { get; set; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index bb76387e61..ec97571149 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Globalization; +using Avalonia.Data.Core; using Avalonia.Styling; using Avalonia.Utilities; using Sprache; @@ -36,79 +38,68 @@ namespace Avalonia.Markup.Parsers /// The parsed selector. public Selector Parse(string s) { - var syntax = SelectorGrammar.Selector.Parse(s); + var syntax = SelectorGrammar.Parse(s); var result = default(Selector); foreach (var i in syntax) { - var ofType = i as SelectorGrammar.OfTypeSyntax; - var @is = i as SelectorGrammar.IsSyntax; - var @class = i as SelectorGrammar.ClassSyntax; - var name = i as SelectorGrammar.NameSyntax; - var property = i as SelectorGrammar.PropertySyntax; - var child = i as SelectorGrammar.ChildSyntax; - var descendant = i as SelectorGrammar.DescendantSyntax; - var template = i as SelectorGrammar.TemplateSyntax; - - if (ofType != null) - { - result = result.OfType(_typeResolver(ofType.Xmlns, ofType.TypeName)); - } - if (@is != null) - { - result = result.Is(_typeResolver(@is.Xmlns, @is.TypeName)); - } - else if (@class != null) - { - result = result.Class(@class.Class); - } - else if (name != null) - { - result = result.Name(name.Name); - } - else if (property != null) + switch (i) { - var type = result?.TargetType; - if (type == null) - { - throw new InvalidOperationException("Property selectors must be applied to a type."); - } + case SelectorGrammar.OfTypeSyntax ofType: + result = result.OfType(_typeResolver(ofType.Xmlns, ofType.TypeName)); + break; + case SelectorGrammar.IsSyntax @is: + result = result.Is(_typeResolver(@is.Xmlns, @is.TypeName)); + break; + case SelectorGrammar.ClassSyntax @class: + result = result.Class(@class.Class); + break; + case SelectorGrammar.NameSyntax name: + result = result.Name(name.Name); + break; + case SelectorGrammar.PropertySyntax property: + { + var type = result?.TargetType; - var targetProperty = AvaloniaPropertyRegistry.Instance.FindRegistered(type, property.Property); + if (type == null) + { + throw new InvalidOperationException("Property selectors must be applied to a type."); + } - if (targetProperty == null) - { - throw new InvalidOperationException($"Cannot find '{property.Property}' on '{type}"); - } + var targetProperty = AvaloniaPropertyRegistry.Instance.FindRegistered(type, property.Property); - object typedValue; + if (targetProperty == null) + { + throw new InvalidOperationException($"Cannot find '{property.Property}' on '{type}"); + } - if (TypeUtilities.TryConvert( - targetProperty.PropertyType, - property.Value, - CultureInfo.InvariantCulture, - out typedValue)) - { - result = result.PropertyEquals(targetProperty, typedValue); - } - else - { - throw new InvalidOperationException( - $"Could not convert '{property.Value}' to '{targetProperty.PropertyType}"); - } - } - else if (child != null) - { - result = result.Child(); - } - else if (descendant != null) - { - result = result.Descendant(); - } - else if (template != null) - { - result = result.Template(); + object typedValue; + + if (TypeUtilities.TryConvert( + targetProperty.PropertyType, + property.Value, + CultureInfo.InvariantCulture, + out typedValue)) + { + result = result.PropertyEquals(targetProperty, typedValue); + } + else + { + throw new InvalidOperationException( + $"Could not convert '{property.Value}' to '{targetProperty.PropertyType}"); + } + break; + } + case SelectorGrammar.ChildSyntax child: + result = result.Child(); + break; + case SelectorGrammar.DescendantSyntax descendant: + result = result.Descendant(); + break; + case SelectorGrammar.TemplateSyntax template: + result = result.Template(); + break; } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 62a9e80585..dc0fd94b36 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Linq; +using Avalonia.Data.Core; using Avalonia.Markup.Parsers; -using Sprache; using Xunit; namespace Avalonia.Markup.UnitTests.Parsers @@ -13,7 +13,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void OfType() { - var result = SelectorGrammar.Selector.Parse("Button").ToList(); + var result = SelectorGrammar.Parse("Button"); Assert.Equal( new[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button", Xmlns = null } }, @@ -23,7 +23,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void NamespacedOfType() { - var result = SelectorGrammar.Selector.Parse("x|Button").ToList(); + var result = SelectorGrammar.Parse("x|Button"); Assert.Equal( new[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button", Xmlns = "x" } }, @@ -33,7 +33,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void Name() { - var result = SelectorGrammar.Selector.Parse("#foo").ToList(); + var result = SelectorGrammar.Parse("#foo"); Assert.Equal( new[] { new SelectorGrammar.NameSyntax { Name = "foo" }, }, @@ -43,7 +43,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void OfType_Name() { - var result = SelectorGrammar.Selector.Parse("Button#foo").ToList(); + var result = SelectorGrammar.Parse("Button#foo"); Assert.Equal( new SelectorGrammar.ISyntax[] @@ -57,7 +57,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void Is() { - var result = SelectorGrammar.Selector.Parse(":is(Button)").ToList(); + var result = SelectorGrammar.Parse(":is(Button)"); Assert.Equal( new[] { new SelectorGrammar.IsSyntax { TypeName = "Button", Xmlns = null } }, @@ -67,7 +67,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void Is_Name() { - var result = SelectorGrammar.Selector.Parse(":is(Button)#foo").ToList(); + var result = SelectorGrammar.Parse(":is(Button)#foo"); Assert.Equal( new SelectorGrammar.ISyntax[] @@ -81,7 +81,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void NamespacedIs_Name() { - var result = SelectorGrammar.Selector.Parse(":is(x|Button)#foo").ToList(); + var result = SelectorGrammar.Parse(":is(x|Button)#foo"); Assert.Equal( new SelectorGrammar.ISyntax[] @@ -95,7 +95,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void Class() { - var result = SelectorGrammar.Selector.Parse(".foo").ToList(); + var result = SelectorGrammar.Parse(".foo"); Assert.Equal( new[] { new SelectorGrammar.ClassSyntax { Class = "foo" } }, @@ -105,7 +105,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void Pseudoclass() { - var result = SelectorGrammar.Selector.Parse(":foo").ToList(); + var result = SelectorGrammar.Parse(":foo"); Assert.Equal( new[] { new SelectorGrammar.ClassSyntax { Class = ":foo" } }, @@ -115,7 +115,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void OfType_Class() { - var result = SelectorGrammar.Selector.Parse("Button.foo").ToList(); + var result = SelectorGrammar.Parse("Button.foo"); Assert.Equal( new SelectorGrammar.ISyntax[] @@ -129,7 +129,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void OfType_Child_Class() { - var result = SelectorGrammar.Selector.Parse("Button > .foo").ToList(); + var result = SelectorGrammar.Parse("Button > .foo"); Assert.Equal( new SelectorGrammar.ISyntax[] @@ -144,7 +144,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void OfType_Child_Class_No_Spaces() { - var result = SelectorGrammar.Selector.Parse("Button>.foo").ToList(); + var result = SelectorGrammar.Parse("Button>.foo"); Assert.Equal( new SelectorGrammar.ISyntax[] @@ -159,7 +159,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void OfType_Descendant_Class() { - var result = SelectorGrammar.Selector.Parse("Button .foo").ToList(); + var result = SelectorGrammar.Parse("Button .foo"); Assert.Equal( new SelectorGrammar.ISyntax[] @@ -174,7 +174,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void OfType_Template_Class() { - var result = SelectorGrammar.Selector.Parse("Button /template/ .foo").ToList(); + var result = SelectorGrammar.Parse("Button /template/ .foo"); Assert.Equal( new SelectorGrammar.ISyntax[] @@ -189,7 +189,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void OfType_Property() { - var result = SelectorGrammar.Selector.Parse("Button[Foo=bar]").ToList(); + var result = SelectorGrammar.Parse("Button[Foo=bar]"); Assert.Equal( new SelectorGrammar.ISyntax[] @@ -203,25 +203,25 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void Namespace_Alone_Fails() { - Assert.Throws(() => SelectorGrammar.Selector.Parse("ns|").ToList()); + Assert.Throws(() => SelectorGrammar.Parse("ns|")); } [Fact] public void Dot_Alone_Fails() { - Assert.Throws(() => SelectorGrammar.Selector.Parse(". dot").ToList()); + Assert.Throws(() => SelectorGrammar.Parse(". dot")); } [Fact] public void Invalid_Identifier_Fails() { - Assert.Throws(() => SelectorGrammar.Selector.Parse("%foo").ToList()); + Assert.Throws(() => SelectorGrammar.Parse("%foo")); } [Fact] public void Invalid_Class_Fails() { - Assert.Throws(() => SelectorGrammar.Selector.Parse(".%foo").ToList()); + Assert.Throws(() => SelectorGrammar.Parse(".%foo")); } } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs index f5a08b6d70..dba4573473 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs @@ -1,6 +1,8 @@ using System; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.Markup.Parsers; +using Avalonia.Styling; using Xunit; namespace Avalonia.Markup.UnitTests.Parsers @@ -10,7 +12,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void Parses_Boolean_Property_Selector() { - var target = new SelectorParser((type, ns) => typeof(TextBlock)); + var target = new SelectorParser((ns, type) => typeof(TextBlock)); var result = target.Parse("TextBlock[IsPointerOver=True]"); } } From 79f9158c6da1f53aff7a9012266f3b5feee94d03 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 9 Jun 2018 16:59:16 -0500 Subject: [PATCH 02/12] Remove Sprache. --- Avalonia.sln | 5 ++--- build/Sprache.props | 5 ----- packages.cake | 4 ---- src/Markup/Avalonia.Markup/Avalonia.Markup.csproj | 1 - src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs | 1 - .../Avalonia.Markup.Xaml.UnitTests.csproj | 1 - 6 files changed, 2 insertions(+), 15 deletions(-) delete mode 100644 build/Sprache.props diff --git a/Avalonia.sln b/Avalonia.sln index 9cf93e8a84..e65143c6df 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -105,9 +105,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Desktop", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{57E0455D-D565-44BB-B069-EE1AA20F8337}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{52F55355-D120-42AC-8116-8410A7D602FA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{52F55355-D120-42AC-8116-8410A7D602FA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" EndProject @@ -149,7 +149,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\SharpDX.props = build\SharpDX.props build\SkiaSharp.props = build\SkiaSharp.props build\Splat.props = build\Splat.props - build\Sprache.props = build\Sprache.props build\XUnit.props = build\XUnit.props EndProjectSection EndProject diff --git a/build/Sprache.props b/build/Sprache.props deleted file mode 100644 index 8ea84de163..0000000000 --- a/build/Sprache.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages.cake b/packages.cake index bcb135f6ad..5d20d974bb 100644 --- a/packages.cake +++ b/packages.cake @@ -109,7 +109,6 @@ public class Packages var SerilogVersion = packageVersions["Serilog"].FirstOrDefault().Item1; var SerilogSinksDebugVersion = packageVersions["Serilog.Sinks.Debug"].FirstOrDefault().Item1; var SerilogSinksTraceVersion = packageVersions["Serilog.Sinks.Trace"].FirstOrDefault().Item1; - var SpracheVersion = packageVersions["Sprache"].FirstOrDefault().Item1; var SystemReactiveVersion = packageVersions["System.Reactive"].FirstOrDefault().Item1; var ReactiveUIVersion = packageVersions["reactiveui"].FirstOrDefault().Item1; var SystemValueTupleVersion = packageVersions["System.ValueTuple"].FirstOrDefault().Item1; @@ -122,7 +121,6 @@ public class Packages var SharpDXDXGIVersion = packageVersions["SharpDX.DXGI"].FirstOrDefault().Item1; context.Information("Package: Serilog, version: {0}", SerilogVersion); - context.Information("Package: Sprache, version: {0}", SpracheVersion); context.Information("Package: System.Reactive, version: {0}", SystemReactiveVersion); context.Information("Package: reactiveui, version: {0}", ReactiveUIVersion); context.Information("Package: System.ValueTuple, version: {0}", SystemValueTupleVersion); @@ -235,7 +233,6 @@ public class Packages new NuSpecDependency() { Id = "Serilog", Version = SerilogVersion }, new NuSpecDependency() { Id = "Serilog.Sinks.Debug", Version = SerilogSinksDebugVersion }, new NuSpecDependency() { Id = "Serilog.Sinks.Trace", Version = SerilogSinksTraceVersion }, - new NuSpecDependency() { Id = "Sprache", Version = SpracheVersion }, new NuSpecDependency() { Id = "System.Reactive", Version = SystemReactiveVersion }, new NuSpecDependency() { Id = "Avalonia.Remote.Protocol", Version = parameters.Version }, //.NET Core @@ -245,7 +242,6 @@ public class Packages new NuSpecDependency() { Id = "Serilog", TargetFramework = "netcoreapp2.0", Version = SerilogVersion }, new NuSpecDependency() { Id = "Serilog.Sinks.Debug", TargetFramework = "netcoreapp2.0", Version = SerilogSinksDebugVersion }, new NuSpecDependency() { Id = "Serilog.Sinks.Trace", TargetFramework = "netcoreapp2.0", Version = SerilogSinksTraceVersion }, - new NuSpecDependency() { Id = "Sprache", TargetFramework = "netcoreapp2.0", Version = SpracheVersion }, new NuSpecDependency() { Id = "System.Reactive", TargetFramework = "netcoreapp2.0", Version = SystemReactiveVersion }, new NuSpecDependency() { Id = "Avalonia.Remote.Protocol", TargetFramework = "netcoreapp2.0", Version = parameters.Version }, } diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 6e10509de6..9180e31039 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -9,5 +9,4 @@ - \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index ec97571149..94ce61b5c3 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -7,7 +7,6 @@ using System.Globalization; using Avalonia.Data.Core; using Avalonia.Styling; using Avalonia.Utilities; -using Sprache; namespace Avalonia.Markup.Parsers { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index 0915684cf1..735d5c421a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -8,7 +8,6 @@ - From 4040216ff865714abe6623d67cd6146685c67a23 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 9 Jun 2018 17:47:08 -0500 Subject: [PATCH 03/12] Span-ify our homebrew parser's 'public' API. --- Avalonia.sln | 1 + build/System.Memory.props | 5 ++ packages.cake | 4 ++ .../Avalonia.Markup/Avalonia.Markup.csproj | 1 + .../Markup/Parsers/ArgumentListParser.cs | 11 ++-- .../Markup/Parsers/ExpressionParser.cs | 14 ++--- .../Markup/Parsers/IdentifierParser.cs | 52 ------------------- .../Avalonia.Markup/Markup/Parsers/Reader.cs | 50 ++++++++++++++++-- .../Markup/Parsers/SelectorGrammar.cs | 35 +++++++------ .../Parsers/SelectorGrammarTests.cs | 4 +- 10 files changed, 88 insertions(+), 89 deletions(-) create mode 100644 build/System.Memory.props delete mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs diff --git a/Avalonia.sln b/Avalonia.sln index e65143c6df..54ffa00d92 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -149,6 +149,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\SharpDX.props = build\SharpDX.props build\SkiaSharp.props = build\SkiaSharp.props build\Splat.props = build\Splat.props + build\System.Memory.props = build\System.Memory.props build\XUnit.props = build\XUnit.props EndProjectSection EndProject diff --git a/build/System.Memory.props b/build/System.Memory.props new file mode 100644 index 0000000000..f3253f8882 --- /dev/null +++ b/build/System.Memory.props @@ -0,0 +1,5 @@ + + + + + diff --git a/packages.cake b/packages.cake index 5d20d974bb..84a04b7238 100644 --- a/packages.cake +++ b/packages.cake @@ -119,6 +119,7 @@ public class Packages var SharpDXDirect3D11Version = packageVersions["SharpDX.Direct3D11"].FirstOrDefault().Item1; var SharpDXDirect3D9Version = packageVersions["SharpDX.Direct3D9"].FirstOrDefault().Item1; var SharpDXDXGIVersion = packageVersions["SharpDX.DXGI"].FirstOrDefault().Item1; + var SystemMemoryVersion = packageVersions["System.Memory"].FirstOrDefault().Item1; context.Information("Package: Serilog, version: {0}", SerilogVersion); context.Information("Package: System.Reactive, version: {0}", SystemReactiveVersion); @@ -131,6 +132,7 @@ public class Packages context.Information("Package: SharpDX.Direct3D11, version: {0}", SharpDXDirect3D11Version); context.Information("Package: SharpDX.Direct3D9, version: {0}", SharpDXDirect3D9Version); context.Information("Package: SharpDX.DXGI, version: {0}", SharpDXDXGIVersion); + context.Information("Package: System.Memory, version: {0}", SystemMemoryVersion); var nugetPackagesDir = System.Environment.GetEnvironmentVariable("NUGET_HOME") ?? System.IO.Path.Combine(System.Environment.GetEnvironmentVariable("USERPROFILE") ?? System.Environment.GetEnvironmentVariable("HOME"), ".nuget"); @@ -235,6 +237,7 @@ public class Packages new NuSpecDependency() { Id = "Serilog.Sinks.Trace", Version = SerilogSinksTraceVersion }, new NuSpecDependency() { Id = "System.Reactive", Version = SystemReactiveVersion }, new NuSpecDependency() { Id = "Avalonia.Remote.Protocol", Version = parameters.Version }, + new NuSpecDependency() { Id = "System.Memory", Version = SystemMemoryVersion }, //.NET Core new NuSpecDependency() { Id = "System.Threading.ThreadPool", TargetFramework = "netcoreapp2.0", Version = "4.3.0" }, new NuSpecDependency() { Id = "Microsoft.Extensions.DependencyModel", TargetFramework = "netcoreapp2.0", Version = "1.1.0" }, @@ -244,6 +247,7 @@ public class Packages new NuSpecDependency() { Id = "Serilog.Sinks.Trace", TargetFramework = "netcoreapp2.0", Version = SerilogSinksTraceVersion }, new NuSpecDependency() { Id = "System.Reactive", TargetFramework = "netcoreapp2.0", Version = SystemReactiveVersion }, new NuSpecDependency() { Id = "Avalonia.Remote.Protocol", TargetFramework = "netcoreapp2.0", Version = parameters.Version }, + new NuSpecDependency() { Id = "System.Memory", TargetFramework = "netcoreapp2.0", Version = SystemMemoryVersion }, } .Deps(new string[]{null, "netcoreapp2.0"}, "System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives", diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 9180e31039..41ffaedef4 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -9,4 +9,5 @@ + \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index ae48657c01..d3f850fbf6 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -20,16 +20,13 @@ namespace Avalonia.Markup.Parsers while (!r.End) { - var builder = new StringBuilder(); - while (!r.End && r.Peek != ',' && r.Peek != close && !char.IsWhiteSpace(r.Peek)) - { - builder.Append(r.Take()); - } - if (builder.Length == 0) + var argument = r.TakeUntil(','); + if (argument.IsEmpty) { throw new ExpressionParseException(r.Position, "Expected indexer argument."); } - result.Add(builder.ToString()); + + result.Add(argument.ToString()); r.SkipWhitespace(); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 8f609ba2bd..9ac70df56b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -83,9 +83,9 @@ namespace Avalonia.Markup.Parsers { var identifier = r.ParseIdentifier(); - if (identifier != null) + if (!identifier.IsEmpty) { - nodes.Add(new PropertyAccessorNode(identifier, _enableValidation)); + nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); return State.AfterMember; } } @@ -122,9 +122,9 @@ namespace Avalonia.Markup.Parsers { var identifier = r.ParseIdentifier(); - if (identifier != null) + if (!identifier.IsEmpty) { - nodes.Add(new PropertyAccessorNode(identifier, _enableValidation)); + nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); return State.AfterMember; } @@ -134,8 +134,8 @@ namespace Avalonia.Markup.Parsers private State ParseAttachedProperty(Reader r, List nodes) { - string ns = string.Empty; - string owner; + ReadOnlySpan ns = ReadOnlySpan.Empty; + ReadOnlySpan owner; var ownerOrNamespace = r.ParseIdentifier(); if (r.TakeIf(':')) @@ -160,7 +160,7 @@ namespace Avalonia.Markup.Parsers throw new ExpressionParseException(r.Position, "Expected ')'."); } - var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns, owner), name); + var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns.ToString(), owner.ToString()), name.ToString()); nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation)); return State.AfterMember; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs deleted file mode 100644 index 470a4c8203..0000000000 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs +++ /dev/null @@ -1,52 +0,0 @@ -// 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 System.Globalization; -using System.Text; - -namespace Avalonia.Markup.Parsers -{ - internal static class IdentifierParser - { - public static string ParseIdentifier(this Reader r) - { - if (IsValidIdentifierStart(r.Peek)) - { - var result = new StringBuilder(); - - while (!r.End && IsValidIdentifierChar(r.Peek)) - { - result.Append(r.Take()); - } - - return result.ToString(); - } - else - { - return null; - } - } - - private static bool IsValidIdentifierStart(char c) - { - return char.IsLetter(c) || c == '_'; - } - - private static bool IsValidIdentifierChar(char c) - { - if (IsValidIdentifierStart(c)) - { - return true; - } - else - { - var cat = CharUnicodeInfo.GetUnicodeCategory(c); - return cat == UnicodeCategory.NonSpacingMark || - cat == UnicodeCategory.SpacingCombiningMark || - cat == UnicodeCategory.ConnectorPunctuation || - cat == UnicodeCategory.Format || - cat == UnicodeCategory.DecimalDigitNumber; - } - } - } -} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs index 6511773bea..82a02b10ab 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Globalization; using System.Text; namespace Avalonia.Markup.Parsers @@ -52,14 +53,55 @@ namespace Avalonia.Markup.Parsers return false; } - public string TakeUntil(char c) + public ReadOnlySpan TakeUntil(char c) { - var builder = new StringBuilder(); + int startIndex = Position; while (!End && Peek != c) { - builder.Append(Take()); + Take(); + } + return _s.AsSpan(startIndex, Position - startIndex); + } + + public ReadOnlySpan ParseIdentifier() + { + if (IsValidIdentifierStart(Peek)) + { + int startIndex = Position; + + while (!End && IsValidIdentifierChar(Peek)) + { + Take(); + } + + return _s.AsSpan(startIndex, Position - startIndex); + } + else + { + return ReadOnlySpan.Empty; + } + } + + private static bool IsValidIdentifierStart(char c) + { + return char.IsLetter(c) || c == '_'; + } + + private static bool IsValidIdentifierChar(char c) + { + if (IsValidIdentifierStart(c)) + { + return true; + } + else + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark || + cat == UnicodeCategory.ConnectorPunctuation || + cat == UnicodeCategory.Format || + cat == UnicodeCategory.DecimalDigitNumber; } - return builder.ToString(); } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 20e49341b7..02123b11f2 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -1,6 +1,7 @@ // 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 System; using System.Collections.Generic; using System.Globalization; using Avalonia.Data.Core; @@ -139,12 +140,12 @@ namespace Avalonia.Markup.Parsers { var identifier = r.ParseIdentifier(); - if (string.IsNullOrEmpty(identifier)) + if (identifier.IsEmpty) { throw new ExpressionParseException(r.Position, "Expected class name or is selector after ':'."); } - if (identifier == "is" && r.TakeIf('(')) + if (identifier.SequenceEqual("is".AsSpan()) && r.TakeIf('(')) { var syntax = ParseType(r); if (r.End || !r.TakeIf(')')) @@ -160,7 +161,7 @@ namespace Avalonia.Markup.Parsers State.CanHaveType, new ClassSyntax { - Class = ":" + identifier + Class = ":" + identifier.ToString() }); } } @@ -190,20 +191,20 @@ namespace Avalonia.Markup.Parsers private static (State, ISyntax) ParseClass(Reader r) { var @class = r.ParseIdentifier(); - if (string.IsNullOrEmpty(@class)) + if (@class.IsEmpty) { throw new ExpressionParseException(r.Position, $"Expected a class name after '.'."); } - return (State.CanHaveType, new ClassSyntax { Class = @class }); + return (State.CanHaveType, new ClassSyntax { Class = @class.ToString() }); } private static (State, ISyntax) ParseTemplate(Reader r) { var template = r.ParseIdentifier(); - if (template != nameof(template)) + if (!template.SequenceEqual(nameof(template).AsSpan())) { - throw new ExpressionParseException(r.Position, $"Expected 'template', got {template}"); + throw new ExpressionParseException(r.Position, $"Expected 'template', got '{template.ToString()}'"); } else if (!r.TakeIf('/')) { @@ -215,11 +216,11 @@ namespace Avalonia.Markup.Parsers private static (State, ISyntax) ParseName(Reader r) { var name = r.ParseIdentifier(); - if (string.IsNullOrEmpty(name)) + if (name.IsEmpty) { throw new ExpressionParseException(r.Position, $"Expected a name after '#'."); } - return (State.CanHaveType, new NameSyntax { Name = name }); + return (State.CanHaveType, new NameSyntax { Name = name.ToString() }); } private static (State, ISyntax) ParseTypeName(Reader r) @@ -240,17 +241,17 @@ namespace Avalonia.Markup.Parsers r.Take(); - return (State.CanHaveType, new PropertySyntax { Property = property, Value = value }); + return (State.CanHaveType, new PropertySyntax { Property = property.ToString(), Value = value.ToString() }); } private static TSyntax ParseType(Reader r) where TSyntax : ITypeSyntax, new() { - string ns = null; - string type; + ReadOnlySpan ns = null; + ReadOnlySpan type; var namespaceOrTypeName = r.ParseIdentifier(); - if (string.IsNullOrEmpty(namespaceOrTypeName)) + if (namespaceOrTypeName.IsEmpty) { throw new ExpressionParseException(r.Position, $"Expected an identifier, got '{r.Peek}"); } @@ -270,8 +271,8 @@ namespace Avalonia.Markup.Parsers } return new TSyntax { - Xmlns = ns, - TypeName = type + Xmlns = ns.ToString(), + TypeName = type.ToString() }; } @@ -290,7 +291,7 @@ namespace Avalonia.Markup.Parsers { public string TypeName { get; set; } - public string Xmlns { get; set; } + public string Xmlns { get; set; } = string.Empty; public override bool Equals(object obj) { @@ -303,7 +304,7 @@ namespace Avalonia.Markup.Parsers { public string TypeName { get; set; } - public string Xmlns { get; set; } + public string Xmlns { get; set; } = string.Empty; public override bool Equals(object obj) { diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index dc0fd94b36..88fe5a2a12 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -16,7 +16,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button"); Assert.Equal( - new[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button", Xmlns = null } }, + new[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button", Xmlns = "" } }, result); } @@ -60,7 +60,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse(":is(Button)"); Assert.Equal( - new[] { new SelectorGrammar.IsSyntax { TypeName = "Button", Xmlns = null } }, + new[] { new SelectorGrammar.IsSyntax { TypeName = "Button", Xmlns = "" } }, result); } From 841efbcfcee25a84c9a42696368fd53a7ab93a41 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 9 Jun 2018 18:46:16 -0500 Subject: [PATCH 04/12] Make Reader a ref struct and have it operate directly on ReadOnlySpans to use less memory. --- .../Markup/Parsers/ArgumentListParser.cs | 4 +- .../Parsers/ExpressionObserverBuilder.cs | 4 +- .../Markup/Parsers/ExpressionParser.cs | 48 ++++----- .../Markup/Parsers/IdentifierParser.cs | 47 +++++++++ .../Avalonia.Markup/Markup/Parsers/Reader.cs | 82 ++++++---------- .../Markup/Parsers/SelectorGrammar.cs | 98 ++++++++++--------- 6 files changed, 157 insertions(+), 126 deletions(-) create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index d3f850fbf6..6a0bde6f83 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Parsers { internal static class ArgumentListParser { - public static IList Parse(Reader r, char open, char close) + public static IList ParseArguments(this ref Reader r, char open, char close) { if (r.Peek == open) { @@ -20,7 +20,7 @@ namespace Avalonia.Markup.Parsers while (!r.End) { - var argument = r.TakeUntil(','); + var argument = r.TakeWhile(c => c != ',' && c != close); if (argument.IsEmpty) { throw new ExpressionParseException(r.Position, "Expected indexer argument."); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index 7141a62cd9..b115fc0cdf 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -15,9 +15,9 @@ namespace Avalonia.Markup.Parsers return new EmptyExpressionNode(); } - var reader = new Reader(expression); + var reader = new Reader(expression.AsSpan()); var parser = new ExpressionParser(enableValidation, typeResolver); - var node = parser.Parse(reader); + var node = parser.Parse(ref reader); if (!reader.End) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 9ac70df56b..bb22046053 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.Parsers _enableValidation = enableValidation; } - public ExpressionNode Parse(Reader r) + public ExpressionNode Parse(ref Reader r) { var nodes = new List(); var state = State.Start; @@ -30,23 +30,23 @@ namespace Avalonia.Markup.Parsers switch (state) { case State.Start: - state = ParseStart(r, nodes); + state = ParseStart(ref r, nodes); break; case State.AfterMember: - state = ParseAfterMember(r, nodes); + state = ParseAfterMember(ref r, nodes); break; case State.BeforeMember: - state = ParseBeforeMember(r, nodes); + state = ParseBeforeMember(ref r, nodes); break; case State.AttachedProperty: - state = ParseAttachedProperty(r, nodes); + state = ParseAttachedProperty(ref r, nodes); break; case State.Indexer: - state = ParseIndexer(r, nodes); + state = ParseIndexer(ref r, nodes); break; } } @@ -64,18 +64,18 @@ namespace Avalonia.Markup.Parsers return nodes.FirstOrDefault(); } - private State ParseStart(Reader r, IList nodes) + private State ParseStart(ref Reader r, IList nodes) { - if (ParseNot(r)) + if (ParseNot(ref r)) { nodes.Add(new LogicalNotNode()); return State.Start; } - else if (ParseOpenBrace(r)) + else if (ParseOpenBrace(ref r)) { return State.AttachedProperty; } - else if (PeekOpenBracket(r)) + else if (PeekOpenBracket(ref r)) { return State.Indexer; } @@ -93,18 +93,18 @@ namespace Avalonia.Markup.Parsers return State.End; } - private static State ParseAfterMember(Reader r, IList nodes) + private static State ParseAfterMember(ref Reader r, IList nodes) { - if (ParseMemberAccessor(r)) + if (ParseMemberAccessor(ref r)) { return State.BeforeMember; } - else if (ParseStreamOperator(r)) + else if (ParseStreamOperator(ref r)) { nodes.Add(new StreamNode()); return State.AfterMember; } - else if (PeekOpenBracket(r)) + else if (PeekOpenBracket(ref r)) { return State.Indexer; } @@ -112,9 +112,9 @@ namespace Avalonia.Markup.Parsers return State.End; } - private State ParseBeforeMember(Reader r, IList nodes) + private State ParseBeforeMember(ref Reader r, IList nodes) { - if (ParseOpenBrace(r)) + if (ParseOpenBrace(ref r)) { return State.AttachedProperty; } @@ -132,7 +132,7 @@ namespace Avalonia.Markup.Parsers } } - private State ParseAttachedProperty(Reader r, List nodes) + private State ParseAttachedProperty(ref Reader r, List nodes) { ReadOnlySpan ns = ReadOnlySpan.Empty; ReadOnlySpan owner; @@ -166,9 +166,9 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private State ParseIndexer(Reader r, List nodes) + private State ParseIndexer(ref Reader r, List nodes) { - var args = ArgumentListParser.Parse(r, '[', ']'); + var args = r.ParseArguments('[', ']'); if (args.Count == 0) { @@ -179,27 +179,27 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private static bool ParseNot(Reader r) + private static bool ParseNot(ref Reader r) { return !r.End && r.TakeIf('!'); } - private static bool ParseMemberAccessor(Reader r) + private static bool ParseMemberAccessor(ref Reader r) { return !r.End && r.TakeIf('.'); } - private static bool ParseOpenBrace(Reader r) + private static bool ParseOpenBrace(ref Reader r) { return !r.End && r.TakeIf('('); } - private static bool PeekOpenBracket(Reader r) + private static bool PeekOpenBracket(ref Reader r) { return !r.End && r.Peek == '['; } - private static bool ParseStreamOperator(Reader r) + private static bool ParseStreamOperator(ref Reader r) { return !r.End && r.TakeIf('^'); } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs new file mode 100644 index 0000000000..a2f6c97608 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs @@ -0,0 +1,47 @@ +// 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 System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace Avalonia.Markup.Parsers +{ + internal static class IdentifierParser + { + public static ReadOnlySpan ParseIdentifier(this ref Reader r) + { + if (IsValidIdentifierStart(r.Peek)) + { + return r.TakeWhile(IsValidIdentifierChar); + } + else + { + return ReadOnlySpan.Empty; + } + } + + private static bool IsValidIdentifierStart(char c) + { + return char.IsLetter(c) || c == '_'; + } + + private static bool IsValidIdentifierChar(char c) + { + if (IsValidIdentifierStart(c)) + { + return true; + } + else + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark || + cat == UnicodeCategory.ConnectorPunctuation || + cat == UnicodeCategory.Format || + cat == UnicodeCategory.DecimalDigitNumber; + } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs index 82a02b10ab..dae05eedf9 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs @@ -7,27 +7,32 @@ using System.Text; namespace Avalonia.Markup.Parsers { - internal class Reader + internal ref struct Reader { - private readonly string _s; - private int _i; + private ReadOnlySpan _s; - public Reader(string s) + public Reader(ReadOnlySpan s) + :this() { _s = s; } - public bool End => _i == _s.Length; - public char Peek => _s[_i]; - public int Position => _i; - public char Take() => _s[_i++]; + public bool End => _s.IsEmpty; + public char Peek => _s[0]; + public int Position { get; private set; } + public char Take() + { + Position++; + char taken = _s[0]; + _s = _s.Slice(1); + return taken; + } public void SkipWhitespace() { - while (!End && char.IsWhiteSpace(Peek)) - { - Take(); - } + var trimmed = _s.TrimStart(); + Position += _s.Length - trimmed.Length; + _s = trimmed; } public bool TakeIf(char c) @@ -55,53 +60,26 @@ namespace Avalonia.Markup.Parsers public ReadOnlySpan TakeUntil(char c) { - int startIndex = Position; - while (!End && Peek != c) + int len; + for (len = 0; len < _s.Length && _s[len] != c; len++) { - Take(); } - return _s.AsSpan(startIndex, Position - startIndex); + var span = _s.Slice(0, len); + _s = _s.Slice(len); + Position += len; + return span; } - public ReadOnlySpan ParseIdentifier() + public ReadOnlySpan TakeWhile(Func condition) { - if (IsValidIdentifierStart(Peek)) - { - int startIndex = Position; - - while (!End && IsValidIdentifierChar(Peek)) - { - Take(); - } - - return _s.AsSpan(startIndex, Position - startIndex); - } - else - { - return ReadOnlySpan.Empty; - } - } - - private static bool IsValidIdentifierStart(char c) - { - return char.IsLetter(c) || c == '_'; - } - - private static bool IsValidIdentifierChar(char c) - { - if (IsValidIdentifierStart(c)) - { - return true; - } - else + int len; + for (len = 0; len < _s.Length && condition(_s[len]); len++) { - var cat = CharUnicodeInfo.GetUnicodeCategory(c); - return cat == UnicodeCategory.NonSpacingMark || - cat == UnicodeCategory.SpacingCombiningMark || - cat == UnicodeCategory.ConnectorPunctuation || - cat == UnicodeCategory.Format || - cat == UnicodeCategory.DecimalDigitNumber; } + var span = _s.Slice(0, len); + _s = _s.Slice(len); + Position += len; + return span; } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 02123b11f2..d00f28a85a 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -31,7 +31,7 @@ namespace Avalonia.Markup.Parsers public static IEnumerable Parse(string s) { - var r = new Reader(s); + var r = new Reader(s.AsSpan()); var state = State.Start; var selector = new List(); while (!r.End && state != State.End) @@ -40,34 +40,34 @@ namespace Avalonia.Markup.Parsers switch (state) { case State.Start: - (state, syntax) = ParseStart(r); + state = ParseStart(ref r); break; case State.Middle: - (state, syntax) = ParseMiddle(r); + state = ParseMiddle(ref r); + break; + case State.CanHaveType: + state = ParseCanHaveType(ref r); break; case State.Colon: - (state, syntax) = ParseColon(r); + (state, syntax) = ParseColon(ref r); break; case State.Class: - (state, syntax) = ParseClass(r); + (state, syntax) = ParseClass(ref r); break; case State.Traversal: - (state, syntax) = ParseTraversal(r); + (state, syntax) = ParseTraversal(ref r); break; case State.TypeName: - (state, syntax) = ParseTypeName(r); - break; - case State.CanHaveType: - (state, syntax) = ParseCanHaveType(r); + (state, syntax) = ParseTypeName(ref r); break; case State.Property: - (state, syntax) = ParseProperty(r); + (state, syntax) = ParseProperty(ref r); break; case State.Template: - (state, syntax) = ParseTemplate(r); + (state, syntax) = ParseTemplate(ref r); break; case State.Name: - (state, syntax) = ParseName(r); + (state, syntax) = ParseName(ref r); break; } if (syntax != null) @@ -84,59 +84,64 @@ namespace Avalonia.Markup.Parsers return selector; } - private static (State, ISyntax) ParseStart(Reader r) + private static State ParseStart(ref Reader r) { r.SkipWhitespace(); + if (r.End) + { + return State.End; + } + if (r.TakeIf(':')) { - return (State.Colon, null); + return State.Colon; } else if (r.TakeIf('.')) { - return (State.Class, null); + return State.Class; } else if (r.TakeIf('#')) { - return (State.Name, null); + return State.Name; } - return (State.TypeName, null); + return State.TypeName; } - private static (State, ISyntax) ParseMiddle(Reader r) + private static State ParseMiddle(ref Reader r) { if (r.TakeIf(':')) { - return (State.Colon, null); + return State.Colon; } else if (r.TakeIf('.')) { - return (State.Class, null); + return State.Class; } else if (r.TakeIf(char.IsWhiteSpace) || r.Peek == '>') { - return (State.Traversal, null); + return State.Traversal; } else if (r.TakeIf('/')) { - return (State.Template, null); + return State.Template; } else if (r.TakeIf('#')) { - return (State.Name, null); + return State.Name; } - return (State.TypeName, null); + return State.TypeName; } - private static (State, ISyntax) ParseCanHaveType(Reader r) + private static State ParseCanHaveType(ref Reader r) { if (r.TakeIf('[')) { - return (State.Property, null); + return State.Property; } - return (State.Middle, null); + return State.Middle; } - private static (State, ISyntax) ParseColon(Reader r) + private static (State, ISyntax) ParseColon(ref Reader r) { var identifier = r.ParseIdentifier(); @@ -145,9 +150,10 @@ namespace Avalonia.Markup.Parsers throw new ExpressionParseException(r.Position, "Expected class name or is selector after ':'."); } - if (identifier.SequenceEqual("is".AsSpan()) && r.TakeIf('(')) + const string IsKeyword = "is"; + if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('(')) { - var syntax = ParseType(r); + var syntax = ParseType(ref r, new IsSyntax()); if (r.End || !r.TakeIf(')')) { throw new ExpressionParseException(r.Position, $"Expected ')', got {r.Peek}"); @@ -166,7 +172,7 @@ namespace Avalonia.Markup.Parsers } } - private static (State, ISyntax) ParseTraversal(Reader r) + private static (State, ISyntax) ParseTraversal(ref Reader r) { r.SkipWhitespace(); if (r.TakeIf('>')) @@ -188,7 +194,7 @@ namespace Avalonia.Markup.Parsers } } - private static (State, ISyntax) ParseClass(Reader r) + private static (State, ISyntax) ParseClass(ref Reader r) { var @class = r.ParseIdentifier(); if (@class.IsEmpty) @@ -199,10 +205,11 @@ namespace Avalonia.Markup.Parsers return (State.CanHaveType, new ClassSyntax { Class = @class.ToString() }); } - private static (State, ISyntax) ParseTemplate(Reader r) + private static (State, ISyntax) ParseTemplate(ref Reader r) { var template = r.ParseIdentifier(); - if (!template.SequenceEqual(nameof(template).AsSpan())) + const string TemplateKeyword = "template"; + if (!template.SequenceEqual(TemplateKeyword.AsSpan())) { throw new ExpressionParseException(r.Position, $"Expected 'template', got '{template.ToString()}'"); } @@ -213,7 +220,7 @@ namespace Avalonia.Markup.Parsers return (State.Start, new TemplateSyntax()); } - private static (State, ISyntax) ParseName(Reader r) + private static (State, ISyntax) ParseName(ref Reader r) { var name = r.ParseIdentifier(); if (name.IsEmpty) @@ -223,12 +230,12 @@ namespace Avalonia.Markup.Parsers return (State.CanHaveType, new NameSyntax { Name = name.ToString() }); } - private static (State, ISyntax) ParseTypeName(Reader r) + private static (State, ISyntax) ParseTypeName(ref Reader r) { - return (State.CanHaveType, ParseType(r)); + return (State.CanHaveType, ParseType(ref r, new OfTypeSyntax())); } - private static (State, ISyntax) ParseProperty(Reader r) + private static (State, ISyntax) ParseProperty(ref Reader r) { var property = r.ParseIdentifier(); @@ -244,8 +251,8 @@ namespace Avalonia.Markup.Parsers return (State.CanHaveType, new PropertySyntax { Property = property.ToString(), Value = value.ToString() }); } - private static TSyntax ParseType(Reader r) - where TSyntax : ITypeSyntax, new() + private static TSyntax ParseType(ref Reader r, TSyntax syntax) + where TSyntax : ITypeSyntax { ReadOnlySpan ns = null; ReadOnlySpan type; @@ -269,11 +276,10 @@ namespace Avalonia.Markup.Parsers { type = namespaceOrTypeName; } - return new TSyntax - { - Xmlns = ns.ToString(), - TypeName = type.ToString() - }; + + syntax.Xmlns = ns.ToString(); + syntax.TypeName = type.ToString(); + return syntax; } public interface ISyntax From 1133c3e46eec07c7354c6639269a05cf28de5929 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 9 Jun 2018 20:40:48 -0500 Subject: [PATCH 05/12] Added parsing benchmark. --- .../Avalonia.Benchmarks.csproj | 1 + tests/Avalonia.Benchmarks/Markup/Parsing.cs | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/Avalonia.Benchmarks/Markup/Parsing.cs diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 9e92baf0ff..0510963ee1 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -1,5 +1,6 @@  + Exe netcoreapp2.0 diff --git a/tests/Avalonia.Benchmarks/Markup/Parsing.cs b/tests/Avalonia.Benchmarks/Markup/Parsing.cs new file mode 100644 index 0000000000..71826986b2 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Markup/Parsing.cs @@ -0,0 +1,34 @@ +using Avalonia.Controls; +using Avalonia.Markup.Parsers; +using BenchmarkDotNet.Attributes; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Benchmarks.Markup +{ + [MemoryDiagnoser] + public class Parsing + { + [Benchmark] + public void ParseComplexSelector() + { + var selectorString = "ListBox > TextBox /template/ TextBlock[IsFocused=True]"; + var parser = new SelectorParser((ns, s) => + { + switch (s) + { + case "ListBox": + return typeof(ListBox); + case "TextBox": + return typeof(TextBox); + case "TextBlock": + return typeof(TextBlock); + default: + return null; + } + }); + var selector = parser.Parse(selectorString); + } + } +} From 69055c627c83295621ce1c9dcc64f9f7b244404b Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 9 Jun 2018 23:01:32 -0500 Subject: [PATCH 06/12] Remove some remaining references to Sprache. --- build/readme.md | 1 - .../Avalonia.AndroidTestApplication.csproj | 1 - 2 files changed, 2 deletions(-) diff --git a/build/readme.md b/build/readme.md index aed508a508..3575f0ab94 100644 --- a/build/readme.md +++ b/build/readme.md @@ -16,7 +16,6 @@ - ``` diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 359adaa1f0..a30627c765 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -150,6 +150,5 @@ - \ No newline at end of file From 399c58b141705f828379479c20dcf326fdc9c6ba Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 9 Jun 2018 23:10:53 -0500 Subject: [PATCH 07/12] Add direct System.Memory reference to AndroidTestApplication. --- .../Avalonia.AndroidTestApplication.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index a30627c765..06008ef02e 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -151,4 +151,5 @@ + \ No newline at end of file From 066e73ce847d8d8489ef2d28f21e450eda281336 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 14 Aug 2018 15:13:38 -0500 Subject: [PATCH 08/12] Fix struct == null comparison. --- src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs index 08243f7f72..ea94012096 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs @@ -24,7 +24,7 @@ namespace Avalonia.Markup.Xaml.Parsers { var token = r.ParseIdentifier(); - if (token == null) + if (token.IsEmpty) { if (r.End) { From 2cd66f4d49289e39aa7a9c1e11e9d4cccdef0368 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 14 Aug 2018 15:47:36 -0500 Subject: [PATCH 09/12] Fix PropertyParserTests. --- .../Parsers/PropertyParserTests.cs | 74 ++++++++----------- 1 file changed, 30 insertions(+), 44 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs index cae6449722..2a4468a750 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Name() { var target = new PropertyParser(); - var reader = new CharacterReader("Foo"); + var reader = new CharacterReader("Foo".AsSpan()); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -25,7 +25,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Owner_And_Name() { var target = new PropertyParser(); - var reader = new CharacterReader("Foo.Bar"); + var reader = new CharacterReader("Foo.Bar".AsSpan()); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -37,7 +37,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Namespace_Owner_And_Name() { var target = new PropertyParser(); - var reader = new CharacterReader("foo:Bar.Baz"); + var reader = new CharacterReader("foo:Bar.Baz".AsSpan()); var (ns, owner, name) = target.Parse(reader); Assert.Equal("foo", ns); @@ -49,7 +49,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Owner_And_Name_With_Parentheses() { var target = new PropertyParser(); - var reader = new CharacterReader("(Foo.Bar)"); + var reader = new CharacterReader("(Foo.Bar)".AsSpan()); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -61,7 +61,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Namespace_Owner_And_Name_With_Parentheses() { var target = new PropertyParser(); - var reader = new CharacterReader("(foo:Bar.Baz)"); + var reader = new CharacterReader("(foo:Bar.Baz)".AsSpan()); var (ns, owner, name) = target.Parse(reader); Assert.Equal("foo", ns); @@ -73,9 +73,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Empty_String() { var target = new PropertyParser(); - var reader = new CharacterReader(""); - var ex = Assert.Throws(() => target.Parse(reader)); + var ex = Assert.Throws(() => target.Parse(new CharacterReader(ReadOnlySpan.Empty))); Assert.Equal(0, ex.Column); Assert.Equal("Expected property name.", ex.Message); } @@ -84,9 +83,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Only_Whitespace() { var target = new PropertyParser(); - var reader = new CharacterReader(" "); - var ex = Assert.Throws(() => target.Parse(reader)); + var ex = Assert.Throws(() => target.Parse(new CharacterReader(" ".AsSpan()))); Assert.Equal(0, ex.Column); Assert.Equal("Unexpected ' '.", ex.Message); } @@ -95,9 +93,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Leading_Whitespace() { var target = new PropertyParser(); - var reader = new CharacterReader(" Foo"); - - var ex = Assert.Throws(() => target.Parse(reader)); + + var ex = Assert.Throws(() => target.Parse(new CharacterReader(" Foo".AsSpan()))); Assert.Equal(0, ex.Column); Assert.Equal("Unexpected ' '.", ex.Message); } @@ -106,9 +103,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Trailing_Whitespace() { var target = new PropertyParser(); - var reader = new CharacterReader("Foo "); - - var ex = Assert.Throws(() => target.Parse(reader)); + + var ex = Assert.Throws(() => target.Parse(new CharacterReader("Foo ".AsSpan()))); Assert.Equal(3, ex.Column); Assert.Equal("Unexpected ' '.", ex.Message); } @@ -117,9 +113,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Invalid_Property_Name() { var target = new PropertyParser(); - var reader = new CharacterReader("123"); - - var ex = Assert.Throws(() => target.Parse(reader)); + + var ex = Assert.Throws(() => target.Parse(new CharacterReader("123".AsSpan()))); Assert.Equal(0, ex.Column); Assert.Equal("Unexpected '1'.", ex.Message); } @@ -128,9 +123,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Trailing_Junk() { var target = new PropertyParser(); - var reader = new CharacterReader("Foo%"); - - var ex = Assert.Throws(() => target.Parse(reader)); + + var ex = Assert.Throws(() => target.Parse(new CharacterReader("Foo%".AsSpan()))); Assert.Equal(3, ex.Column); Assert.Equal("Unexpected '%'.", ex.Message); } @@ -139,9 +133,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Invalid_Property_Name_After_Owner() { var target = new PropertyParser(); - var reader = new CharacterReader("Foo.123"); - - var ex = Assert.Throws(() => target.Parse(reader)); + + var ex = Assert.Throws(() => target.Parse(new CharacterReader("Foo.123".AsSpan()))); Assert.Equal(4, ex.Column); Assert.Equal("Unexpected '1'.", ex.Message); } @@ -150,9 +143,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Whitespace_Between_Owner_And_Name() { var target = new PropertyParser(); - var reader = new CharacterReader("Foo. Bar"); - - var ex = Assert.Throws(() => target.Parse(reader)); + + var ex = Assert.Throws(() => target.Parse(new CharacterReader("Foo. Bar".AsSpan()))); Assert.Equal(4, ex.Column); Assert.Equal("Unexpected ' '.", ex.Message); } @@ -161,9 +153,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Too_Many_Segments() { var target = new PropertyParser(); - var reader = new CharacterReader("Foo.Bar.Baz"); - - var ex = Assert.Throws(() => target.Parse(reader)); + + var ex = Assert.Throws(() => target.Parse(new CharacterReader("Foo.Bar.Baz".AsSpan()))); Assert.Equal(8, ex.Column); Assert.Equal("Unexpected '.'.", ex.Message); } @@ -172,9 +163,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Too_Many_Namespaces() { var target = new PropertyParser(); - var reader = new CharacterReader("foo:bar:Baz"); - - var ex = Assert.Throws(() => target.Parse(reader)); + + var ex = Assert.Throws(() => target.Parse(new CharacterReader("foo:bar:Baz".AsSpan()))); Assert.Equal(8, ex.Column); Assert.Equal("Unexpected ':'.", ex.Message); } @@ -183,9 +173,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Parens_But_No_Owner() { var target = new PropertyParser(); - var reader = new CharacterReader("(Foo)"); - - var ex = Assert.Throws(() => target.Parse(reader)); + + var ex = Assert.Throws(() => target.Parse(new CharacterReader("(Foo)".AsSpan()))); Assert.Equal(1, ex.Column); Assert.Equal("Expected property owner.", ex.Message); } @@ -194,9 +183,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Parens_And_Namespace_But_No_Owner() { var target = new PropertyParser(); - var reader = new CharacterReader("(foo:Bar)"); - - var ex = Assert.Throws(() => target.Parse(reader)); + + var ex = Assert.Throws(() => target.Parse(new CharacterReader("(foo:Bar)".AsSpan()))); Assert.Equal(1, ex.Column); Assert.Equal("Expected property owner.", ex.Message); } @@ -205,9 +193,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Missing_Close_Parens() { var target = new PropertyParser(); - var reader = new CharacterReader("(Foo.Bar"); - - var ex = Assert.Throws(() => target.Parse(reader)); + + var ex = Assert.Throws(() => target.Parse(new CharacterReader("(Foo.Bar".AsSpan()))); Assert.Equal(8, ex.Column); Assert.Equal("Expected ')'.", ex.Message); } @@ -216,9 +203,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Unexpected_Close_Parens() { var target = new PropertyParser(); - var reader = new CharacterReader("Foo.Bar)"); - var ex = Assert.Throws(() => target.Parse(reader)); + var ex = Assert.Throws(() => target.Parse(new CharacterReader("Foo.Bar)".AsSpan()))); Assert.Equal(7, ex.Column); Assert.Equal("Unexpected ')'.", ex.Message); } From 7faf079961ddb0956b31bb8788b7cf4cc88b990d Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 15 Aug 2018 12:05:25 -0500 Subject: [PATCH 10/12] Fix tests and fix ControlCatalog.iOS to not reference Sprache. --- samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj | 3 +-- src/Avalonia.Base/Data/Core/ExpressionParseException.cs | 1 + src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index a60fd242e4..368bc19abf 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -174,6 +174,5 @@ ControlCatalog - - \ No newline at end of file + diff --git a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs index 1845b1b52a..195c9b7660 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs @@ -17,6 +17,7 @@ namespace Avalonia.Data.Core /// /// The column position of the error. /// The exception message. + /// The exception that caused the parsing failure. public ExpressionParseException(int column, string message, Exception innerException = null) : base(message, innerException) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 0da0d863f6..bf6ebe837b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -216,11 +216,11 @@ namespace Avalonia.Markup.Parsers { var mode = r.ParseIdentifier(); - if (mode == "self".AsSpan()) + if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture)) { nodes.Add(new SelfNode()); } - else if (mode == "parent".AsSpan()) + else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture)) { Type ancestorType = null; var ancestorLevel = 0; From ceac3c496e7bf60abb27859bc340cc820cca36ed Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 15 Aug 2018 12:45:58 -0500 Subject: [PATCH 11/12] Update System.Memory to 4.5.1 --- build/System.Memory.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/System.Memory.props b/build/System.Memory.props index f3253f8882..b328f7fd97 100644 --- a/build/System.Memory.props +++ b/build/System.Memory.props @@ -1,5 +1,5 @@ - + From 371b8f748658e6c67b03f291b77bc8b7bd02e672 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 15 Aug 2018 17:16:18 -0500 Subject: [PATCH 12/12] Add package references required for Xamarin.Android 15.8 to compile applications --- build/System.Memory.props | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/System.Memory.props b/build/System.Memory.props index b328f7fd97..df7956af84 100644 --- a/build/System.Memory.props +++ b/build/System.Memory.props @@ -1,5 +1,8 @@ + + +