From ffcaa545bb64db5489de3cb135e4f831afb07a8f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 10:45:07 +0200 Subject: [PATCH] Added PropertyParser. So we don't need to use a regex to parse property strings. --- .../Avalonia.Markup.Xaml.csproj | 1 + .../Parsers/PropertyParser.cs | 84 +++++++ .../Markup/Parsers/IdentifierParser.cs | 2 +- .../Avalonia.Markup/Markup/Parsers/Reader.cs | 2 +- .../Parsers/PropertyParserTests.cs | 225 ++++++++++++++++++ 5 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index cdc22f4102..8c843a4b49 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs new file mode 100644 index 0000000000..ce82ffe0a1 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs @@ -0,0 +1,84 @@ +using System; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; + +namespace Avalonia.Markup.Xaml.Parsers +{ + internal class PropertyParser + { + public (string ns, string owner, string name) Parse(Reader 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); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs index f86f2db321..9431dab45e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs @@ -6,7 +6,7 @@ using System.Text; namespace Avalonia.Markup.Parsers { - internal static class IdentifierParser + public static class IdentifierParser { public static string Parse(Reader r) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs index 9355bc9aa3..4a3d6aa277 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs @@ -5,7 +5,7 @@ using System; namespace Avalonia.Markup.Parsers { - internal class Reader + public class Reader { private readonly string _s; private int _i; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs new file mode 100644 index 0000000000..a05485f55b --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs @@ -0,0 +1,225 @@ +using System; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.Parsers; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Parsers +{ + public class PropertyParserTests + { + [Fact] + public void Parses_Name() + { + var target = new PropertyParser(); + var reader = new Reader("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 Reader("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 Reader("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 Reader("(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 Reader("(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 Reader(""); + + var ex = Assert.Throws(() => 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 Reader(" "); + + var ex = Assert.Throws(() => 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 Reader(" Foo"); + + var ex = Assert.Throws(() => 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 Reader("Foo "); + + var ex = Assert.Throws(() => 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 Reader("123"); + + var ex = Assert.Throws(() => 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 Reader("Foo%"); + + var ex = Assert.Throws(() => 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 Reader("Foo.123"); + + var ex = Assert.Throws(() => 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 Reader("Foo. Bar"); + + var ex = Assert.Throws(() => 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 Reader("Foo.Bar.Baz"); + + var ex = Assert.Throws(() => 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 Reader("foo:bar:Baz"); + + var ex = Assert.Throws(() => 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 Reader("(Foo)"); + + var ex = Assert.Throws(() => 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 Reader("(foo:Bar)"); + + var ex = Assert.Throws(() => 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 Reader("(Foo.Bar"); + + var ex = Assert.Throws(() => 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 Reader("Foo.Bar)"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(7, ex.Column); + Assert.Equal("Unexpected ')'.", ex.Message); + } + } +}