Browse Source

Merge pull request #1668 from jkoritzinsky/selector-parse-no-sprache

Remove Sprache dependency
pull/1810/head
Jeremy Koritzinsky 8 years ago
committed by GitHub
parent
commit
343efcda9a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      Avalonia.sln
  2. 5
      build/Sprache.props
  3. 5
      build/System.Memory.props
  4. 1
      build/readme.md
  5. 8
      packages.cake
  6. 3
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  7. 2
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  8. 3
      src/Avalonia.Base/Avalonia.Base.csproj
  9. 1
      src/Avalonia.Base/Data/Core/ExpressionParseException.cs
  10. 65
      src/Avalonia.Base/Utilities/CharacterReader.cs
  11. 17
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  12. 3
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  13. 10
      src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs
  14. 4
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  15. 13
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  16. 6
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  17. 140
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  18. 395
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  19. 118
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  20. 2
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  21. 34
      tests/Avalonia.Benchmarks/Markup/Parsing.cs
  22. 44
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  23. 4
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs
  24. 1
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  25. 74
      tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs

2
Avalonia.sln

@ -150,7 +150,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\Sprache.props = build\Sprache.props
build\System.Memory.props = build\System.Memory.props
build\XUnit.props = build\XUnit.props
EndProjectSection
EndProject

5
build/Sprache.props

@ -1,5 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="Sprache" Version="2.1.0" />
</ItemGroup>
</Project>

5
build/System.Memory.props

@ -1,5 +1,8 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.0" />
<PackageReference Include="System.Memory" Version="4.5.1" />
<!-- WORKAROUND: The packages below are transitively referenced by System.Memory, but Xamarin.Android needs them directly referenced for the linker. -->
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.1" />
<PackageReference Include="System.Buffers" Version="4.5.0" />
</ItemGroup>
</Project>

1
build/readme.md

@ -16,7 +16,6 @@
<Import Project="..\..\build\SkiaSharp.Desktop.props" />
<Import Project="..\..\build\SkiaSharp.props" />
<Import Project="..\..\build\Splat.props" />
<Import Project="..\..\build\Sprache.props" />
<Import Project="..\..\build\XUnit.props" />
```

8
packages.cake

@ -110,7 +110,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;
@ -121,10 +120,10 @@ 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;
var SystemComponentModelAnnotationsVersion = packageVersions["System.ComponentModel.Annotations"].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);
@ -135,6 +134,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");
@ -253,9 +253,9 @@ 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 },
new NuSpecDependency() { Id = "System.Memory", Version = SystemMemoryVersion },
new NuSpecDependency() { Id = "System.ComponentModel.Annotations", Version = SystemComponentModelAnnotationsVersion },
//.NET Core
new NuSpecDependency() { Id = "System.Threading.ThreadPool", TargetFramework = "netcoreapp2.0", Version = "4.3.0" },
@ -264,9 +264,9 @@ 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 },
new NuSpecDependency() { Id = "System.Memory", TargetFramework = "netcoreapp2.0", Version = SystemMemoryVersion },
}
.Deps(new string[]{null, "netcoreapp2.0"},
"System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives",

3
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@ -174,6 +174,5 @@
<Name>ControlCatalog</Name>
</ProjectReference>
</ItemGroup>
<Import Project="..\..\build\Sprache.props" />
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
</Project>
</Project>

2
src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj

@ -150,6 +150,6 @@
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<Import Project="..\..\..\build\Serilog.props" />
<Import Project="..\..\..\build\Sprache.props" />
<Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\System.Memory.props" />
</Project>

3
src/Avalonia.Base/Avalonia.Base.csproj

@ -8,4 +8,5 @@
<Import Project="..\..\build\Binding.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\JetBrains.Annotations.props" />
</Project>
<Import Project="..\..\build\System.Memory.props" />
</Project>

1
src/Avalonia.Base/Data/Core/ExpressionParseException.cs

@ -17,6 +17,7 @@ namespace Avalonia.Data.Core
/// </summary>
/// <param name="column">The column position of the error.</param>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The exception that caused the parsing failure.</param>
public ExpressionParseException(int column, string message, Exception innerException = null)
: base(message, innerException)
{

65
src/Avalonia.Base/Utilities/CharacterReader.cs

@ -2,30 +2,37 @@
// 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.Utilities
{
public class CharacterReader
public ref struct CharacterReader
{
private readonly string _s;
private int _i;
private ReadOnlySpan<char> _s;
public CharacterReader(string s)
public CharacterReader(ReadOnlySpan<char> 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)
@ -40,5 +47,39 @@ namespace Avalonia.Utilities
return false;
}
}
public bool TakeIf(Func<char, bool> condition)
{
if (condition(Peek))
{
Take();
return true;
}
return false;
}
public ReadOnlySpan<char> TakeUntil(char c)
{
int len;
for (len = 0; len < _s.Length && _s[len] != c; len++)
{
}
var span = _s.Slice(0, len);
_s = _s.Slice(len);
Position += len;
return span;
}
public ReadOnlySpan<char> TakeWhile(Func<char, bool> condition)
{
int len;
for (len = 0; len < _s.Length && condition(_s[len]); len++)
{
}
var span = _s.Slice(0, len);
_s = _s.Slice(len);
Position += len;
return span;
}
}
}

17
src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// 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;
@ -8,22 +10,15 @@ namespace Avalonia.Utilities
{
public static class IdentifierParser
{
public static string Parse(CharacterReader r)
public static ReadOnlySpan<char> ParseIdentifier(this ref CharacterReader r)
{
if (IsValidIdentifierStart(r.Peek))
{
var result = new StringBuilder();
while (!r.End && IsValidIdentifierChar(r.Peek))
{
result.Append(r.Take());
}
return result.ToString();
return r.TakeWhile(IsValidIdentifierChar);
}
else
{
return null;
return ReadOnlySpan<char>.Empty;
}
}

3
src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs

@ -26,8 +26,7 @@ namespace Avalonia.Markup.Xaml.Converters
{
var registry = AvaloniaPropertyRegistry.Instance;
var parser = new PropertyParser();
var reader = new CharacterReader((string)value);
var (ns, owner, propertyName) = parser.Parse(reader);
var (ns, owner, propertyName) = parser.Parse(new CharacterReader(((string)value).AsSpan()));
var ownerType = TryResolveOwnerByName(context, ns, owner);
var targetType = context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ??
context.GetFirstAmbientValue<Style>()?.Selector?.TargetType ??

10
src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs

@ -22,9 +22,9 @@ namespace Avalonia.Markup.Xaml.Parsers
do
{
var token = IdentifierParser.Parse(r);
var token = r.ParseIdentifier();
if (token == null)
if (token.IsEmpty)
{
if (r.End)
{
@ -47,18 +47,18 @@ namespace Avalonia.Markup.Xaml.Parsers
else if (!r.End && r.TakeIf(':'))
{
ns = ns == null ?
token :
token.ToString() :
throw new ExpressionParseException(r.Position, "Unexpected ':'.");
}
else if (!r.End && r.TakeIf('.'))
{
owner = owner == null ?
token :
token.ToString() :
throw new ExpressionParseException(r.Position, "Unexpected '.'.");
}
else
{
name = token;
name = token.ToString();
}
} while (!r.End);

4
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -9,5 +9,5 @@
<ProjectReference Include="..\..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\Sprache.props" />
</Project>
<Import Project="..\..\..\build\System.Memory.props" />
</Project>

13
src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

@ -11,7 +11,7 @@ namespace Avalonia.Markup.Parsers
{
internal static class ArgumentListParser
{
public static IList<string> Parse(CharacterReader r, char open, char close, char delimiter = ',')
public static IList<string> ParseArguments(this ref CharacterReader r, char open, char close, char delimiter = ',')
{
if (r.Peek == open)
{
@ -21,16 +21,13 @@ namespace Avalonia.Markup.Parsers
while (!r.End)
{
var builder = new StringBuilder();
while (!r.End && r.Peek != delimiter && r.Peek != close && !char.IsWhiteSpace(r.Peek))
{
builder.Append(r.Take());
}
if (builder.Length == 0)
var argument = r.TakeWhile(c => c != delimiter && c != close && !char.IsWhiteSpace(c));
if (argument.IsEmpty)
{
throw new ExpressionParseException(r.Position, "Expected indexer argument.");
}
result.Add(builder.ToString());
result.Add(argument.ToString());
r.SkipWhitespace();

6
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs

@ -15,10 +15,10 @@ namespace Avalonia.Markup.Parsers
{
return (new EmptyExpressionNode(), default);
}
var reader = new CharacterReader(expression);
var reader = new CharacterReader(expression.AsSpan());
var parser = new ExpressionParser(enableValidation, typeResolver);
var node = parser.Parse(reader);
var node = parser.Parse(ref reader);
if (!reader.End)
{

140
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs

@ -27,7 +27,7 @@ namespace Avalonia.Markup.Parsers
_enableValidation = enableValidation;
}
public (ExpressionNode Node, SourceMode Mode) Parse(CharacterReader r)
public (ExpressionNode Node, SourceMode Mode) Parse(ref CharacterReader r)
{
var nodes = new List<ExpressionNode>();
var state = State.Start;
@ -38,32 +38,32 @@ 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;
case State.ElementName:
state = ParseElementName(r, nodes);
state = ParseElementName(ref r, nodes);
mode = SourceMode.Control;
break;
case State.RelativeSource:
state = ParseRelativeSource(r, nodes);
state = ParseRelativeSource(ref r, nodes);
mode = SourceMode.Control;
break;
}
@ -82,36 +82,37 @@ namespace Avalonia.Markup.Parsers
return (nodes.FirstOrDefault(), mode);
}
private State ParseStart(CharacterReader r, IList<ExpressionNode> nodes)
private State ParseStart(ref CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseNot(r))
if (ParseNot(ref r))
{
nodes.Add(new LogicalNotNode());
return State.Start;
}
else if (ParseSharp(r))
else if (ParseSharp(ref r))
{
return State.ElementName;
}
else if (ParseDollarSign(r))
else if (ParseDollarSign(ref r))
{
return State.RelativeSource;
}
else if (ParseOpenBrace(r))
else if (ParseOpenBrace(ref r))
{
return State.AttachedProperty;
}
else if (PeekOpenBracket(r))
else if (PeekOpenBracket(ref r))
{
return State.Indexer;
}
else
{
var identifier = IdentifierParser.Parse(r);
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;
}
}
@ -119,18 +120,18 @@ namespace Avalonia.Markup.Parsers
return State.End;
}
private static State ParseAfterMember(CharacterReader r, IList<ExpressionNode> nodes)
private static State ParseAfterMember(ref CharacterReader r, IList<ExpressionNode> 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;
}
@ -138,19 +139,19 @@ namespace Avalonia.Markup.Parsers
return State.End;
}
private State ParseBeforeMember(CharacterReader r, IList<ExpressionNode> nodes)
private State ParseBeforeMember(ref CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseOpenBrace(r))
if (ParseOpenBrace(ref r))
{
return State.AttachedProperty;
}
else
{
var identifier = IdentifierParser.Parse(r);
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;
}
@ -158,16 +159,16 @@ namespace Avalonia.Markup.Parsers
}
}
private State ParseAttachedProperty(CharacterReader r, List<ExpressionNode> nodes)
private State ParseAttachedProperty(ref CharacterReader r, List<ExpressionNode> nodes)
{
var (ns, owner) = ParseTypeName(r);
var (ns, owner) = ParseTypeName(ref r);
if (r.End || !r.TakeIf('.'))
{
throw new ExpressionParseException(r.Position, "Invalid attached property name.");
}
var name = IdentifierParser.Parse(r);
var name = r.ParseIdentifier();
if (r.End || !r.TakeIf(')'))
{
@ -179,15 +180,15 @@ namespace Avalonia.Markup.Parsers
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, owner), name);
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns.ToString(), owner.ToString()), name.ToString());
nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation));
return State.AfterMember;
}
private State ParseIndexer(CharacterReader r, List<ExpressionNode> nodes)
private State ParseIndexer(ref CharacterReader r, List<ExpressionNode> nodes)
{
var args = ArgumentListParser.Parse(r, '[', ']');
var args = r.ParseArguments('[', ']');
if (args.Count == 0)
{
@ -197,36 +198,35 @@ namespace Avalonia.Markup.Parsers
nodes.Add(new StringIndexerNode(args));
return State.AfterMember;
}
private State ParseElementName(CharacterReader r, List<ExpressionNode> nodes)
private State ParseElementName(ref CharacterReader r, List<ExpressionNode> nodes)
{
var name = IdentifierParser.Parse(r);
var name = r.ParseIdentifier();
if (name == null)
{
throw new ExpressionParseException(r.Position, "Element name expected after '#'.");
}
nodes.Add(new ElementNameNode(name));
nodes.Add(new ElementNameNode(name.ToString()));
return State.AfterMember;
}
private State ParseRelativeSource(CharacterReader r, List<ExpressionNode> nodes)
private State ParseRelativeSource(ref CharacterReader r, List<ExpressionNode> nodes)
{
var mode = IdentifierParser.Parse(r);
var mode = r.ParseIdentifier();
if (mode == "self")
if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture))
{
nodes.Add(new SelfNode());
}
else if (mode == "parent")
else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture))
{
Type ancestorType = null;
var ancestorLevel = 0;
if (PeekOpenBracket(r))
if (PeekOpenBracket(ref r))
{
var args = ArgumentListParser.Parse(r, '[', ']', ';');
var args = r.ParseArguments('[', ']', ';');
if (args.Count > 2 || args.Count == 0)
{
throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar");
@ -240,14 +240,16 @@ namespace Avalonia.Markup.Parsers
}
else
{
var typeName = ParseTypeName(new CharacterReader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
var reader = new CharacterReader(args[0].AsSpan());
var typeName = ParseTypeName(ref reader);
ancestorType = _typeResolver(typeName.Namespace.ToString(), typeName.Type.ToString());
}
}
else
{
var typeName = ParseTypeName(new CharacterReader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
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]);
}
}
@ -261,56 +263,56 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember;
}
private static (string ns, string typeName) ParseTypeName(CharacterReader r)
private static TypeName ParseTypeName(ref CharacterReader r)
{
string ns, typeName;
ns = string.Empty;
var typeNameOrNamespace = IdentifierParser.Parse(r);
ReadOnlySpan<char> ns, typeName;
ns = ReadOnlySpan<char>.Empty;
var typeNameOrNamespace = r.ParseIdentifier();
if (!r.End && r.TakeIf(':'))
{
ns = typeNameOrNamespace;
typeName = IdentifierParser.Parse(r);
typeName = r.ParseIdentifier();
}
else
{
typeName = typeNameOrNamespace;
}
return (ns, typeName);
return new TypeName(ns, typeName);
}
private static bool ParseNot(CharacterReader r)
private static bool ParseNot(ref CharacterReader r)
{
return !r.End && r.TakeIf('!');
}
private static bool ParseMemberAccessor(CharacterReader r)
private static bool ParseMemberAccessor(ref CharacterReader r)
{
return !r.End && r.TakeIf('.');
}
private static bool ParseOpenBrace(CharacterReader r)
private static bool ParseOpenBrace(ref CharacterReader r)
{
return !r.End && r.TakeIf('(');
}
private static bool PeekOpenBracket(CharacterReader r)
private static bool PeekOpenBracket(ref CharacterReader r)
{
return !r.End && r.Peek == '[';
}
private static bool ParseStreamOperator(CharacterReader r)
private static bool ParseStreamOperator(ref CharacterReader r)
{
return !r.End && r.TakeIf('^');
}
private static bool ParseDollarSign(CharacterReader r)
private static bool ParseDollarSign(ref CharacterReader r)
{
return !r.End && r.TakeIf('$');
}
private static bool ParseSharp(CharacterReader r)
private static bool ParseSharp(ref CharacterReader r)
{
return !r.End && r.TakeIf('#');
}
@ -326,5 +328,23 @@ namespace Avalonia.Markup.Parsers
Indexer,
End,
}
private readonly ref struct TypeName
{
public TypeName(ReadOnlySpan<char> ns, ReadOnlySpan<char> typeName)
{
Namespace = ns;
Type = typeName;
}
public readonly ReadOnlySpan<char> Namespace;
public readonly ReadOnlySpan<char> Type;
public void Deconstruct(out ReadOnlySpan<char> ns, out ReadOnlySpan<char> typeName)
{
ns = Namespace;
typeName = Type;
}
}
}
}

395
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

@ -1,9 +1,11 @@
// 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 Sprache;
using Avalonia.Data.Core;
using Avalonia.Utilities;
// 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.
@ -11,123 +13,292 @@ using Sprache;
namespace Avalonia.Markup.Parsers
{
internal class SelectorGrammar
internal static class SelectorGrammar
{
public static readonly Parser<char> CombiningCharacter = Parse.Char(
c =>
{
var cat = CharUnicodeInfo.GetUnicodeCategory(c);
return cat == UnicodeCategory.NonSpacingMark ||
cat == UnicodeCategory.SpacingCombiningMark;
},
"Connecting Character");
public static readonly Parser<char> ConnectingCharacter = Parse.Char(
c => CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.ConnectorPunctuation,
"Connecting Character");
public static readonly Parser<char> FormattingCharacter = Parse.Char(
c => CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.Format,
"Connecting Character");
public static readonly Parser<char> IdentifierStart = Parse.Letter.Or(Parse.Char('_'));
public static readonly Parser<char> IdentifierChar = Parse
.LetterOrDigit
.Or(ConnectingCharacter)
.Or(CombiningCharacter)
.Or(FormattingCharacter);
public static readonly Parser<string> Identifier =
from start in IdentifierStart.Once().Text()
from @char in IdentifierChar.Many().Text()
select start + @char;
public static readonly Parser<string> Namespace =
from ns in Parse.Letter.Many().Text()
from bar in Parse.Char('|')
select ns;
public static readonly Parser<OfTypeSyntax> OfType =
from ns in Namespace.Optional()
from identifier in Identifier
select new OfTypeSyntax
{
TypeName = identifier,
Xmlns = ns.GetOrDefault(),
};
public static readonly Parser<NameSyntax> Name =
from hash in Parse.Char('#')
from identifier in Identifier
select new NameSyntax { Name = identifier };
public static readonly Parser<char> ClassStart = Parse.Char('_').Or(Parse.Letter);
public static readonly Parser<char> ClassChar = ClassStart.Or(Parse.Numeric);
public static readonly Parser<string> ClassIdentifier =
from start in ClassStart.Once().Text()
from @char in ClassChar.Many().Text()
select start + @char;
public static readonly Parser<ClassSyntax> StandardClass =
from dot in Parse.Char('.').Once()
from identifier in ClassIdentifier
select new ClassSyntax { Class = identifier };
public static readonly Parser<ClassSyntax> Pseduoclass =
from colon in Parse.Char(':').Once()
from identifier in ClassIdentifier
select new ClassSyntax { Class = ':' + identifier };
public static readonly Parser<ClassSyntax> Class = StandardClass.Or(Pseduoclass);
public static readonly Parser<PropertySyntax> 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<ChildSyntax> Child = Parse.Char('>').Token().Return(new ChildSyntax());
public static readonly Parser<DescendantSyntax> Descendant =
from child in Parse.WhiteSpace.Many()
select new DescendantSyntax();
public static readonly Parser<TemplateSyntax> Template =
from template in Parse.String("/template/").Token()
select new TemplateSyntax();
public static readonly Parser<IsSyntax> 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<ISyntax> SingleSelector =
OfType
.Or<ISyntax>(Is)
.Or<ISyntax>(Name)
.Or<ISyntax>(Class)
.Or<ISyntax>(Property)
.Or<ISyntax>(Child)
.Or<ISyntax>(Template)
.Or<ISyntax>(Descendant);
public static readonly Parser<IEnumerable<ISyntax>> Selector = SingleSelector.Many().End();
private enum State
{
Start,
Middle,
Colon,
Class,
Name,
CanHaveType,
Traversal,
TypeName,
Property,
Template,
End,
}
public static IEnumerable<ISyntax> Parse(string s)
{
var r = new CharacterReader(s.AsSpan());
var state = State.Start;
var selector = new List<ISyntax>();
while (!r.End && state != State.End)
{
ISyntax syntax = null;
switch (state)
{
case State.Start:
state = ParseStart(ref r);
break;
case State.Middle:
state = ParseMiddle(ref r);
break;
case State.CanHaveType:
state = ParseCanHaveType(ref r);
break;
case State.Colon:
(state, syntax) = ParseColon(ref r);
break;
case State.Class:
(state, syntax) = ParseClass(ref r);
break;
case State.Traversal:
(state, syntax) = ParseTraversal(ref r);
break;
case State.TypeName:
(state, syntax) = ParseTypeName(ref r);
break;
case State.Property:
(state, syntax) = ParseProperty(ref r);
break;
case State.Template:
(state, syntax) = ParseTemplate(ref r);
break;
case State.Name:
(state, syntax) = ParseName(ref 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 ParseStart(ref CharacterReader r)
{
r.SkipWhitespace();
if (r.End)
{
return State.End;
}
if (r.TakeIf(':'))
{
return State.Colon;
}
else if (r.TakeIf('.'))
{
return State.Class;
}
else if (r.TakeIf('#'))
{
return State.Name;
}
return State.TypeName;
}
private static State ParseMiddle(ref CharacterReader r)
{
if (r.TakeIf(':'))
{
return State.Colon;
}
else if (r.TakeIf('.'))
{
return State.Class;
}
else if (r.TakeIf(char.IsWhiteSpace) || r.Peek == '>')
{
return State.Traversal;
}
else if (r.TakeIf('/'))
{
return State.Template;
}
else if (r.TakeIf('#'))
{
return State.Name;
}
return State.TypeName;
}
private static State ParseCanHaveType(ref CharacterReader r)
{
if (r.TakeIf('['))
{
return State.Property;
}
return State.Middle;
}
private static (State, ISyntax) ParseColon(ref CharacterReader r)
{
var identifier = r.ParseIdentifier();
if (identifier.IsEmpty)
{
throw new ExpressionParseException(r.Position, "Expected class name or is selector after ':'.");
}
const string IsKeyword = "is";
if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('('))
{
var syntax = ParseType(ref r, new IsSyntax());
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.ToString()
});
}
}
private static (State, ISyntax) ParseTraversal(ref CharacterReader 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(ref CharacterReader r)
{
var @class = r.ParseIdentifier();
if (@class.IsEmpty)
{
throw new ExpressionParseException(r.Position, $"Expected a class name after '.'.");
}
return (State.CanHaveType, new ClassSyntax { Class = @class.ToString() });
}
private static (State, ISyntax) ParseTemplate(ref CharacterReader r)
{
var template = r.ParseIdentifier();
const string TemplateKeyword = "template";
if (!template.SequenceEqual(TemplateKeyword.AsSpan()))
{
throw new ExpressionParseException(r.Position, $"Expected 'template', got '{template.ToString()}'");
}
else if (!r.TakeIf('/'))
{
throw new ExpressionParseException(r.Position, "Expected '/'");
}
return (State.Start, new TemplateSyntax());
}
private static (State, ISyntax) ParseName(ref CharacterReader r)
{
var name = r.ParseIdentifier();
if (name.IsEmpty)
{
throw new ExpressionParseException(r.Position, $"Expected a name after '#'.");
}
return (State.CanHaveType, new NameSyntax { Name = name.ToString() });
}
private static (State, ISyntax) ParseTypeName(ref CharacterReader r)
{
return (State.CanHaveType, ParseType(ref r, new OfTypeSyntax()));
}
private static (State, ISyntax) ParseProperty(ref CharacterReader 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.ToString(), Value = value.ToString() });
}
private static TSyntax ParseType<TSyntax>(ref CharacterReader r, TSyntax syntax)
where TSyntax : ITypeSyntax
{
ReadOnlySpan<char> ns = null;
ReadOnlySpan<char> type;
var namespaceOrTypeName = r.ParseIdentifier();
if (namespaceOrTypeName.IsEmpty)
{
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;
}
syntax.Xmlns = ns.ToString();
syntax.TypeName = type.ToString();
return syntax;
}
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; }
public string Xmlns { get; set; }
public string Xmlns { get; set; } = string.Empty;
public override bool Equals(object obj)
{
@ -136,11 +307,11 @@ namespace Avalonia.Markup.Parsers
}
}
public class IsSyntax : ISyntax
public class IsSyntax : ISyntax, ITypeSyntax
{
public string TypeName { get; set; }
public string Xmlns { get; set; }
public string Xmlns { get; set; } = string.Empty;
public override bool Equals(object obj)
{

118
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

@ -2,10 +2,11 @@
// 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;
namespace Avalonia.Markup.Parsers
{
@ -36,79 +37,68 @@ namespace Avalonia.Markup.Parsers
/// <returns>The parsed selector.</returns>
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;
}
}

2
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -1,5 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<TargetFramework>netcoreapp2.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>

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

44
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,17 +13,17 @@ 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 } },
new[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button", Xmlns = "" } },
result);
}
[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,17 +57,17 @@ 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 } },
new[] { new SelectorGrammar.IsSyntax { TypeName = "Button", Xmlns = "" } },
result);
}
[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<ParseException>(() => SelectorGrammar.Selector.Parse("ns|").ToList());
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse("ns|"));
}
[Fact]
public void Dot_Alone_Fails()
{
Assert.Throws<ParseException>(() => SelectorGrammar.Selector.Parse(". dot").ToList());
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(". dot"));
}
[Fact]
public void Invalid_Identifier_Fails()
{
Assert.Throws<ParseException>(() => SelectorGrammar.Selector.Parse("%foo").ToList());
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse("%foo"));
}
[Fact]
public void Invalid_Class_Fails()
{
Assert.Throws<ParseException>(() => SelectorGrammar.Selector.Parse(".%foo").ToList());
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(".%foo"));
}
}
}

4
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]");
}
}

1
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -8,7 +8,6 @@
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
<Import Project="..\..\build\Sprache.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />

74
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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(new CharacterReader(ReadOnlySpan<char>.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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => 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<ExpressionParseException>(() => target.Parse(reader));
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(new CharacterReader("Foo.Bar)".AsSpan())));
Assert.Equal(7, ex.Column);
Assert.Equal("Unexpected ')'.", ex.Message);
}

Loading…
Cancel
Save