|
|
|
@ -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<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(), |
|
|
|
private enum State |
|
|
|
{ |
|
|
|
Start, |
|
|
|
Middle, |
|
|
|
Colon, |
|
|
|
Class, |
|
|
|
Name, |
|
|
|
CanHaveType, |
|
|
|
Traversal, |
|
|
|
TypeName, |
|
|
|
Property, |
|
|
|
Template, |
|
|
|
End, |
|
|
|
} |
|
|
|
|
|
|
|
public static IEnumerable<ISyntax> Parse(string s) |
|
|
|
{ |
|
|
|
var r = new Reader(s); |
|
|
|
var state = State.Start; |
|
|
|
var selector = new List<ISyntax>(); |
|
|
|
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<IsSyntax>(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<OfTypeSyntax>(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<TSyntax>(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<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(); |
|
|
|
|
|
|
|
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; } |
|
|
|
|
|
|
|
|