Browse Source

Added `:not()` style selector.

pull/2268/head
Steven Kirk 7 years ago
parent
commit
233adc9ca5
  1. 72
      src/Avalonia.Styling/Styling/NotSelector.cs
  2. 11
      src/Avalonia.Styling/Styling/Selectors.cs
  3. 51
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  4. 13
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  5. 73
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  6. 28
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  7. 114
      tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs

72
src/Avalonia.Styling/Styling/NotSelector.cs

@ -0,0 +1,72 @@
// 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.Reactive.Linq;
namespace Avalonia.Styling
{
/// <summary>
/// The `:not()` style selector.
/// </summary>
internal class NotSelector : Selector
{
private readonly Selector _previous;
private readonly Selector _argument;
private string _selectorString;
/// <summary>
/// Initializes a new instance of the <see cref="NotSelector"/> class.
/// </summary>
/// <param name="previous">The previous selector.</param>
/// <param name="argument">The selector to be not-ed.</param>
public NotSelector(Selector previous, Selector argument)
{
_previous = previous;
_argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument.");
}
/// <inheritdoc/>
public override bool InTemplate => _argument.InTemplate;
/// <inheritdoc/>
public override bool IsCombinator => false;
/// <inheritdoc/>
public override Type TargetType => _previous?.TargetType;
/// <inheritdoc/>
public override string ToString()
{
if (_selectorString == null)
{
_selectorString = ":not(" + _argument.ToString() + ")";
}
return _selectorString;
}
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
{
var innerResult = _argument.Match(control, subscribe);
switch (innerResult.Result)
{
case SelectorMatchResult.AlwaysThisInstance:
return SelectorMatch.NeverThisInstance;
case SelectorMatchResult.AlwaysThisType:
return SelectorMatch.NeverThisType;
case SelectorMatchResult.NeverThisInstance:
return SelectorMatch.AlwaysThisInstance;
case SelectorMatchResult.NeverThisType:
return SelectorMatch.AlwaysThisType;
case SelectorMatchResult.Sometimes:
return new SelectorMatch(innerResult.Activator.Select(x => !x));
default:
throw new InvalidOperationException("Invalid SelectorMatchResult.");
}
}
protected override Selector MovePrevious() => _previous;
}
}

11
src/Avalonia.Styling/Styling/Selectors.cs

@ -94,6 +94,17 @@ namespace Avalonia.Styling
}
}
/// <summary>
/// Returns a selector which inverts the results of selector argument.
/// </summary>
/// <param name="previous">The previous selector.</param>
/// <param name="argument">The selector to be not-ed.</param>
/// <returns>The selector.</returns>
public static Selector Not(this Selector previous, Func<Selector, Selector> argument)
{
return new NotSelector(previous, argument(null));
}
/// <summary>
/// Returns a selector which matches a type.
/// </summary>

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

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Data.Core;
using Avalonia.Utilities;
@ -32,6 +33,11 @@ namespace Avalonia.Markup.Parsers
public static IEnumerable<ISyntax> Parse(string s)
{
var r = new CharacterReader(s.AsSpan());
return Parse(ref r, null);
}
private static IEnumerable<ISyntax> Parse(ref CharacterReader r, char? end)
{
var state = State.Start;
var selector = new List<ISyntax>();
while (!r.End && state != State.End)
@ -43,7 +49,7 @@ namespace Avalonia.Markup.Parsers
state = ParseStart(ref r);
break;
case State.Middle:
state = ParseMiddle(ref r);
state = ParseMiddle(ref r, end);
break;
case State.CanHaveType:
state = ParseCanHaveType(ref r);
@ -107,7 +113,7 @@ namespace Avalonia.Markup.Parsers
return State.TypeName;
}
private static State ParseMiddle(ref CharacterReader r)
private static State ParseMiddle(ref CharacterReader r, char? end)
{
if (r.TakeIf(':'))
{
@ -129,6 +135,10 @@ namespace Avalonia.Markup.Parsers
{
return State.Name;
}
else if (end.HasValue && !r.End && r.Peek == end.Value)
{
return State.End;
}
return State.TypeName;
}
@ -151,16 +161,23 @@ namespace Avalonia.Markup.Parsers
}
const string IsKeyword = "is";
const string NotKeyword = "not";
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}");
}
Expect(ref r, ')');
return (State.CanHaveType, syntax);
}
if (identifier.SequenceEqual(NotKeyword.AsSpan()) && r.TakeIf('('))
{
var argument = Parse(ref r, ')');
Expect(ref r, ')');
var syntax = new NotSyntax { Argument = argument };
return (State.Middle, syntax);
}
else
{
return (
@ -282,6 +299,18 @@ namespace Avalonia.Markup.Parsers
return syntax;
}
private static void Expect(ref CharacterReader r, char c)
{
if (r.End)
{
throw new ExpressionParseException(r.Position, $"Expected '{c}', got end of selector.");
}
else if (!r.TakeIf(')'))
{
throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'.");
}
}
public interface ISyntax
{
}
@ -376,5 +405,15 @@ namespace Avalonia.Markup.Parsers
return obj is TemplateSyntax;
}
}
public class NotSyntax : ISyntax
{
public IEnumerable<ISyntax> Argument { get; set; }
public override bool Equals(object obj)
{
return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument);
}
}
}
}

13
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.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.Collections.Generic;
using System.Globalization;
using Avalonia.Styling;
using Avalonia.Utilities;
@ -25,7 +26,7 @@ namespace Avalonia.Markup.Parsers
/// </param>
public SelectorParser(Func<string, string, Type> typeResolver)
{
this._typeResolver = typeResolver;
_typeResolver = typeResolver;
}
/// <summary>
@ -36,6 +37,11 @@ namespace Avalonia.Markup.Parsers
public Selector Parse(string s)
{
var syntax = SelectorGrammar.Parse(s);
return Create(syntax);
}
private Selector Create(IEnumerable<SelectorGrammar.ISyntax> syntax)
{
var result = default(Selector);
foreach (var i in syntax)
@ -97,6 +103,11 @@ namespace Avalonia.Markup.Parsers
case SelectorGrammar.TemplateSyntax template:
result = result.Template();
break;
case SelectorGrammar.NotSyntax not:
result = result.Not(x => Create(not.Argument));
break;
default:
throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'.");
}
}

73
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@ -200,6 +200,67 @@ namespace Avalonia.Markup.UnitTests.Parsers
result);
}
[Fact]
public void Not_OfType()
{
var result = SelectorGrammar.Parse(":not(Button)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NotSyntax
{
Argument = new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
},
}
},
result);
}
[Fact]
public void OfType_Not_Class()
{
var result = SelectorGrammar.Parse("Button:not(.foo)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NotSyntax
{
Argument = new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.ClassSyntax { Class = "foo" },
},
}
},
result);
}
[Fact]
public void Is_Descendent_Not_OfType_Class()
{
var result = SelectorGrammar.Parse(":is(Control) :not(Button.foo)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.IsSyntax { TypeName = "Control" },
new SelectorGrammar.DescendantSyntax { },
new SelectorGrammar.NotSyntax
{
Argument = new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.ClassSyntax { Class = "foo" },
},
}
},
result);
}
[Fact]
public void Namespace_Alone_Fails()
{
@ -223,5 +284,17 @@ namespace Avalonia.Markup.UnitTests.Parsers
{
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(".%foo"));
}
[Fact]
public void Not_Without_Argument_Fails()
{
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(":not()"));
}
[Fact]
public void Not_Without_Closing_Parenthesis_Fails()
{
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(":not(Button"));
}
}
}

28
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -198,5 +198,33 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
ex.InnerException.Message);
}
}
[Fact]
public void Style_Can_Use_Not_Selector()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border:not(.foo)'>
<Setter Property='Background' Value='Red'/>
</Style>
</Window.Styles>
<StackPanel>
<Border Name='foo' Classes='foo bar'/>
<Border Name='notFoo' Classes='bar'/>
</StackPanel>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var foo = window.FindControl<Border>("foo");
var notFoo = window.FindControl<Border>("notFoo");
Assert.Null(foo.Background);
Assert.Equal(Colors.Red, ((ISolidColorBrush)notFoo.Background).Color);
}
}
}
}

114
tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs

@ -0,0 +1,114 @@
// 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.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class SelectorTests_Not
{
[Fact]
public void Not_Selector_Should_Have_Correct_String_Representation()
{
var target = default(Selector).Not(x => x.Class("foo"));
Assert.Equal(":not(.foo)", target.ToString());
}
[Fact]
public void Not_OfType_Matches_Control_Of_Incorrect_Type()
{
var control = new Control1();
var target = default(Selector).Not(x => x.OfType<Control1>());
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result);
}
[Fact]
public void Not_OfType_Doesnt_Match_Control_Of_Correct_Type()
{
var control = new Control2();
var target = default(Selector).Not(x => x.OfType<Control1>());
Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result);
}
[Fact]
public async Task Not_Class_Doesnt_Match_Control_With_Class()
{
var control = new Control1
{
Classes = new Classes { "foo" },
};
var target = default(Selector).Not(x => x.Class("foo"));
var match = target.Match(control);
Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
Assert.False(await match.Activator.Take(1));
}
[Fact]
public async Task Not_Class_Matches_Control_Without_Class()
{
var control = new Control1
{
Classes = new Classes { "bar" },
};
var target = default(Selector).Not(x => x.Class("foo"));
var match = target.Match(control);
Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
Assert.True(await match.Activator.Take(1));
}
[Fact]
public async Task OfType_Not_Class_Matches_Control_Without_Class()
{
var control = new Control1
{
Classes = new Classes { "bar" },
};
var target = default(Selector).OfType<Control1>().Not(x => x.Class("foo"));
var match = target.Match(control);
Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
Assert.True(await match.Activator.Take(1));
}
[Fact]
public void OfType_Not_Class_Doesnt_Match_Control_Of_Wrong_Type()
{
var control = new Control2
{
Classes = new Classes { "foo" },
};
var target = default(Selector).OfType<Control1>().Not(x => x.Class("foo"));
var match = target.Match(control);
Assert.Equal(SelectorMatchResult.NeverThisType, match.Result);
}
[Fact]
public void Returns_Correct_TargetType()
{
var target = default(Selector).OfType<Control1>().Not(x => x.Class("foo"));
Assert.Equal(typeof(Control1), target.TargetType);
}
public class Control1 : TestControlBase
{
}
public class Control2 : TestControlBase
{
}
}
}
Loading…
Cancel
Save