committed by
GitHub
55 changed files with 1514 additions and 719 deletions
@ -0,0 +1,58 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// FIFO Queue optimized for holding zero or one items.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of items held in the queue.</typeparam>
|
|||
public class SingleOrQueue<T> |
|||
{ |
|||
private T _head; |
|||
private Queue<T> _tail; |
|||
|
|||
private Queue<T> Tail => _tail ?? (_tail = new Queue<T>()); |
|||
|
|||
private bool HasTail => _tail != null; |
|||
|
|||
public bool Empty { get; private set; } = true; |
|||
|
|||
public void Enqueue(T value) |
|||
{ |
|||
if (Empty) |
|||
{ |
|||
_head = value; |
|||
} |
|||
else |
|||
{ |
|||
Tail.Enqueue(value); |
|||
} |
|||
|
|||
Empty = false; |
|||
} |
|||
|
|||
public T Dequeue() |
|||
{ |
|||
if (Empty) |
|||
{ |
|||
throw new InvalidOperationException("Cannot dequeue from an empty queue!"); |
|||
} |
|||
|
|||
var result = _head; |
|||
|
|||
if (HasTail && Tail.Count != 0) |
|||
{ |
|||
_head = Tail.Dequeue(); |
|||
} |
|||
else |
|||
{ |
|||
_head = default; |
|||
Empty = true; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using System; |
|||
using Avalonia.Data.Core; |
|||
using Avalonia.Markup.Parsers; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Markup.Xaml.Parsers |
|||
{ |
|||
internal class PropertyParser |
|||
{ |
|||
public (string ns, string owner, string name) Parse(CharacterReader r) |
|||
{ |
|||
if (r.End) |
|||
{ |
|||
throw new ExpressionParseException(0, "Expected property name."); |
|||
} |
|||
|
|||
var openParens = r.TakeIf('('); |
|||
bool closeParens = false; |
|||
string ns = null; |
|||
string owner = null; |
|||
string name = null; |
|||
|
|||
do |
|||
{ |
|||
var token = IdentifierParser.Parse(r); |
|||
|
|||
if (token == null) |
|||
{ |
|||
if (r.End) |
|||
{ |
|||
break; |
|||
} |
|||
else |
|||
{ |
|||
if (openParens && !r.End && (closeParens = r.TakeIf(')'))) |
|||
{ |
|||
break; |
|||
} |
|||
else if (openParens) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, $"Expected ')'."); |
|||
} |
|||
|
|||
throw new ExpressionParseException(r.Position, $"Unexpected '{r.Peek}'."); |
|||
} |
|||
} |
|||
else if (!r.End && r.TakeIf(':')) |
|||
{ |
|||
ns = ns == null ? |
|||
token : |
|||
throw new ExpressionParseException(r.Position, "Unexpected ':'."); |
|||
} |
|||
else if (!r.End && r.TakeIf('.')) |
|||
{ |
|||
owner = owner == null ? |
|||
token : |
|||
throw new ExpressionParseException(r.Position, "Unexpected '.'."); |
|||
} |
|||
else |
|||
{ |
|||
name = token; |
|||
} |
|||
} while (!r.End); |
|||
|
|||
if (name == null) |
|||
{ |
|||
throw new ExpressionParseException(0, "Expected property name."); |
|||
} |
|||
else if (openParens && owner == null) |
|||
{ |
|||
throw new ExpressionParseException(1, "Expected property owner."); |
|||
} |
|||
else if (openParens && !closeParens) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Expected ')'."); |
|||
} |
|||
else if (!r.End) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Expected end of expression."); |
|||
} |
|||
|
|||
return (ns, owner, name); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Avalonia.Data.Core; |
|||
using Avalonia.LogicalTree; |
|||
|
|||
namespace Avalonia.Markup.Parsers.Nodes |
|||
{ |
|||
internal class ElementNameNode : ExpressionNode |
|||
{ |
|||
private readonly string _name; |
|||
private IDisposable _subscription; |
|||
|
|||
public ElementNameNode(string name) |
|||
{ |
|||
_name = name; |
|||
} |
|||
|
|||
public override string Description => $"#{_name}"; |
|||
|
|||
protected override void StartListeningCore(WeakReference reference) |
|||
{ |
|||
if (reference.Target is ILogical logical) |
|||
{ |
|||
_subscription = ControlLocator.Track(logical, _name).Subscribe(ValueChanged); |
|||
} |
|||
else |
|||
{ |
|||
_subscription = null; |
|||
} |
|||
} |
|||
|
|||
protected override void StopListeningCore() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Avalonia.Data.Core; |
|||
using Avalonia.LogicalTree; |
|||
|
|||
namespace Avalonia.Markup.Parsers.Nodes |
|||
{ |
|||
internal class FindAncestorNode : ExpressionNode |
|||
{ |
|||
private readonly int _level; |
|||
private readonly Type _ancestorType; |
|||
private IDisposable _subscription; |
|||
|
|||
public FindAncestorNode(Type ancestorType, int level) |
|||
{ |
|||
_level = level; |
|||
_ancestorType = ancestorType; |
|||
} |
|||
|
|||
public override string Description |
|||
{ |
|||
get |
|||
{ |
|||
if (_ancestorType == null) |
|||
{ |
|||
return $"$parent[{_level}]"; |
|||
} |
|||
else |
|||
{ |
|||
return $"$parent[{_ancestorType.Name}, {_level}]"; |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected override void StartListeningCore(WeakReference reference) |
|||
{ |
|||
if (reference.Target is ILogical logical) |
|||
{ |
|||
_subscription = ControlLocator.Track(logical, _level, _ancestorType).Subscribe(ValueChanged); |
|||
} |
|||
else |
|||
{ |
|||
_subscription = null; |
|||
} |
|||
} |
|||
|
|||
protected override void StopListeningCore() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Avalonia.Data.Core; |
|||
|
|||
namespace Avalonia.Markup.Parsers.Nodes |
|||
{ |
|||
internal class SelfNode : ExpressionNode |
|||
{ |
|||
public override string Description => "$self"; |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
using Avalonia.Utilities; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Utilities |
|||
{ |
|||
public class SingleOrQueueTests |
|||
{ |
|||
[Fact] |
|||
public void New_SingleOrQueue_Is_Empty() |
|||
{ |
|||
Assert.True(new SingleOrQueue<object>().Empty); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Dequeue_Throws_When_Empty() |
|||
{ |
|||
var queue = new SingleOrQueue<object>(); |
|||
|
|||
Assert.Throws<InvalidOperationException>(() => queue.Dequeue()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Enqueue_Adds_Element() |
|||
{ |
|||
var queue = new SingleOrQueue<int>(); |
|||
|
|||
queue.Enqueue(1); |
|||
|
|||
Assert.False(queue.Empty); |
|||
|
|||
Assert.Equal(1, queue.Dequeue()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Multiple_Elements_Dequeued_In_Correct_Order() |
|||
{ |
|||
var queue = new SingleOrQueue<int>(); |
|||
|
|||
queue.Enqueue(1); |
|||
queue.Enqueue(2); |
|||
queue.Enqueue(3); |
|||
Assert.Equal(1, queue.Dequeue()); |
|||
Assert.Equal(2, queue.Dequeue()); |
|||
Assert.Equal(3, queue.Dequeue()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests |
|||
{ |
|||
public class SliderTests |
|||
{ |
|||
[Fact] |
|||
public void Default_Orientation_Should_Be_Horizontal() |
|||
{ |
|||
var slider = new Slider(); |
|||
Assert.Equal(Orientation.Horizontal, slider.Orientation); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Set_Horizontal_Class() |
|||
{ |
|||
var slider = new Slider |
|||
{ |
|||
Orientation = Orientation.Horizontal |
|||
}; |
|||
|
|||
Assert.Contains(slider.Classes, ":horizontal".Equals); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Set_Vertical_Class() |
|||
{ |
|||
var slider = new Slider |
|||
{ |
|||
Orientation = Orientation.Vertical |
|||
}; |
|||
|
|||
Assert.Contains(slider.Classes, ":vertical".Equals); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,226 @@ |
|||
using System; |
|||
using Avalonia.Data.Core; |
|||
using Avalonia.Markup.Parsers; |
|||
using Avalonia.Markup.Xaml.Parsers; |
|||
using Avalonia.Utilities; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Markup.Xaml.UnitTests.Parsers |
|||
{ |
|||
public class PropertyParserTests |
|||
{ |
|||
[Fact] |
|||
public void Parses_Name() |
|||
{ |
|||
var target = new PropertyParser(); |
|||
var reader = new CharacterReader("Foo"); |
|||
var (ns, owner, name) = target.Parse(reader); |
|||
|
|||
Assert.Null(ns); |
|||
Assert.Null(owner); |
|||
Assert.Equal("Foo", name); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Parses_Owner_And_Name() |
|||
{ |
|||
var target = new PropertyParser(); |
|||
var reader = new CharacterReader("Foo.Bar"); |
|||
var (ns, owner, name) = target.Parse(reader); |
|||
|
|||
Assert.Null(ns); |
|||
Assert.Equal("Foo", owner); |
|||
Assert.Equal("Bar", name); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Parses_Namespace_Owner_And_Name() |
|||
{ |
|||
var target = new PropertyParser(); |
|||
var reader = new CharacterReader("foo:Bar.Baz"); |
|||
var (ns, owner, name) = target.Parse(reader); |
|||
|
|||
Assert.Equal("foo", ns); |
|||
Assert.Equal("Bar", owner); |
|||
Assert.Equal("Baz", name); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Parses_Owner_And_Name_With_Parentheses() |
|||
{ |
|||
var target = new PropertyParser(); |
|||
var reader = new CharacterReader("(Foo.Bar)"); |
|||
var (ns, owner, name) = target.Parse(reader); |
|||
|
|||
Assert.Null(ns); |
|||
Assert.Equal("Foo", owner); |
|||
Assert.Equal("Bar", name); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Parses_Namespace_Owner_And_Name_With_Parentheses() |
|||
{ |
|||
var target = new PropertyParser(); |
|||
var reader = new CharacterReader("(foo:Bar.Baz)"); |
|||
var (ns, owner, name) = target.Parse(reader); |
|||
|
|||
Assert.Equal("foo", ns); |
|||
Assert.Equal("Bar", owner); |
|||
Assert.Equal("Baz", name); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Fails_With_Empty_String() |
|||
{ |
|||
var target = new PropertyParser(); |
|||
var reader = new CharacterReader(""); |
|||
|
|||
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader)); |
|||
Assert.Equal(0, ex.Column); |
|||
Assert.Equal("Expected property name.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Fails_With_Only_Whitespace() |
|||
{ |
|||
var target = new PropertyParser(); |
|||
var reader = new CharacterReader(" "); |
|||
|
|||
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader)); |
|||
Assert.Equal(0, ex.Column); |
|||
Assert.Equal("Unexpected ' '.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Fails_With_Leading_Whitespace() |
|||
{ |
|||
var target = new PropertyParser(); |
|||
var reader = new CharacterReader(" Foo"); |
|||
|
|||
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader)); |
|||
Assert.Equal(0, ex.Column); |
|||
Assert.Equal("Unexpected ' '.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Fails_With_Trailing_Whitespace() |
|||
{ |
|||
var target = new PropertyParser(); |
|||
var reader = new CharacterReader("Foo "); |
|||
|
|||
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader)); |
|||
Assert.Equal(3, ex.Column); |
|||
Assert.Equal("Unexpected ' '.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Fails_With_Invalid_Property_Name() |
|||
{ |
|||
var target = new PropertyParser(); |
|||
var reader = new CharacterReader("123"); |
|||
|
|||
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader)); |
|||
Assert.Equal(0, ex.Column); |
|||
Assert.Equal("Unexpected '1'.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Fails_With_Trailing_Junk() |
|||
{ |
|||
var target = new PropertyParser(); |
|||
var reader = new CharacterReader("Foo%"); |
|||
|
|||
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader)); |
|||
Assert.Equal(3, ex.Column); |
|||
Assert.Equal("Unexpected '%'.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
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)); |
|||
Assert.Equal(4, ex.Column); |
|||
Assert.Equal("Unexpected '1'.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
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)); |
|||
Assert.Equal(4, ex.Column); |
|||
Assert.Equal("Unexpected ' '.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
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)); |
|||
Assert.Equal(8, ex.Column); |
|||
Assert.Equal("Unexpected '.'.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
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)); |
|||
Assert.Equal(8, ex.Column); |
|||
Assert.Equal("Unexpected ':'.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
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)); |
|||
Assert.Equal(1, ex.Column); |
|||
Assert.Equal("Expected property owner.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
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)); |
|||
Assert.Equal(1, ex.Column); |
|||
Assert.Equal("Expected property owner.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
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)); |
|||
Assert.Equal(8, ex.Column); |
|||
Assert.Equal("Expected ')'.", ex.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
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)); |
|||
Assert.Equal(7, ex.Column); |
|||
Assert.Equal("Unexpected ')'.", ex.Message); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue