diff --git a/src/ExCSS.Tests/AtPropertyTests.cs b/src/ExCSS.Tests/AtPropertyTests.cs new file mode 100644 index 00000000..2f7b6178 --- /dev/null +++ b/src/ExCSS.Tests/AtPropertyTests.cs @@ -0,0 +1,66 @@ +using System.Linq; +using Xunit; + +namespace ExCSS.Tests +{ + public class AtPropertyTests : CssConstructionFunctions + { + private static PropertyRule Parse(string source) + { + var sheet = ParseStyleSheet(source); + return (PropertyRule)sheet.Rules.First(); + } + + [Fact] + public void AtPropertyCapturesNameAndDescriptors() + { + var rule = Parse("@property --my-color { syntax: ''; initial-value: red; inherits: false; }"); + + Assert.Equal(RuleType.Property, rule.Type); + Assert.Equal("--my-color", rule.Name); + Assert.Equal("\"\"", rule.Syntax); + Assert.Equal("red", rule.InitialValue); + Assert.Equal("false", rule.Inherits); + } + + [Fact] + public void AtPropertyRoundTrips() + { + var rule = Parse("@property --gap { syntax: ''; initial-value: 0px; inherits: true; }"); + + Assert.Equal("@property --gap { syntax: \"\"; initial-value: 0px; inherits: true }", + rule.ToCss()); + } + + [Fact] + public void AtPropertyWithUniversalSyntax() + { + var rule = Parse("@property --x { syntax: '*'; inherits: false; }"); + + Assert.Equal("\"*\"", rule.Syntax); + Assert.Equal("false", rule.Inherits); + Assert.Equal(string.Empty, rule.InitialValue); + } + + [Fact] + public void AtPropertyIsExposedAsIPropertyRule() + { + var sheet = ParseStyleSheet("@property --c { syntax: ''; inherits: false; }"); + var rule = Assert.IsAssignableFrom(sheet.Rules.First()); + + Assert.Equal("--c", rule.Name); + } + + [Fact] + public void AtPropertyAmongOtherRules() + { + var sheet = ParseStyleSheet( + ".a { color: red } @property --c { syntax: ''; inherits: false; } .b { color: blue }"); + + Assert.Equal(3, sheet.Rules.Length); + Assert.IsType(sheet.Rules[0]); + Assert.IsType(sheet.Rules[1]); + Assert.IsType(sheet.Rules[2]); + } + } +} diff --git a/src/ExCSS/Enumerations/PropertyNames.cs b/src/ExCSS/Enumerations/PropertyNames.cs index 854654d9..07450acd 100644 --- a/src/ExCSS/Enumerations/PropertyNames.cs +++ b/src/ExCSS/Enumerations/PropertyNames.cs @@ -237,6 +237,10 @@ public static class PropertyNames public static readonly string Zoom = "zoom"; public static readonly string UnicodeRange = "unicode-range"; public static readonly string Src = "src"; + // @property descriptors (CSS Properties and Values API 1 3) + public static readonly string Syntax = "syntax"; + public static readonly string InitialValue = "initial-value"; + public static readonly string Inherits = "inherits"; public static readonly string ObjectFit = "object-fit"; public static readonly string ObjectPosition = "object-position"; } diff --git a/src/ExCSS/Enumerations/RuleNames.cs b/src/ExCSS/Enumerations/RuleNames.cs index c4d6bce2..06934227 100644 --- a/src/ExCSS/Enumerations/RuleNames.cs +++ b/src/ExCSS/Enumerations/RuleNames.cs @@ -13,5 +13,6 @@ public static class RuleNames public static readonly string Namespace = "namespace"; public static readonly string Page = "page"; public static readonly string Container = "container"; + public static readonly string Property = "property"; } } \ No newline at end of file diff --git a/src/ExCSS/Enumerations/RuleType.cs b/src/ExCSS/Enumerations/RuleType.cs index c24b9cbe..358e8df6 100644 --- a/src/ExCSS/Enumerations/RuleType.cs +++ b/src/ExCSS/Enumerations/RuleType.cs @@ -19,6 +19,7 @@ public enum RuleType : byte FontFeatureValues, Viewport, RegionStyle, - Container + Container, + Property } } \ No newline at end of file diff --git a/src/ExCSS/Factories/PropertyFactory.cs b/src/ExCSS/Factories/PropertyFactory.cs index 22316405..9a30c099 100644 --- a/src/ExCSS/Factories/PropertyFactory.cs +++ b/src/ExCSS/Factories/PropertyFactory.cs @@ -367,6 +367,20 @@ public Property CreateFont(string name) return _fonts.TryGetValue(name, out var propertyCreator) ? propertyCreator() : null; } + // The @property descriptors (syntax / initial-value / inherits). Their values have no fixed grammar + // - initial-value depends on the syntax, syntax is an arbitrary string - so each is stored raw via + // an UnknownProperty (Converters.Any). + public Property CreatePropertyDescriptor(string name) + { + return PropertyFactory.IsPropertyDescriptor(name) ? new UnknownProperty(name) : null; + } + + private static bool IsPropertyDescriptor(string name) + { + return name.Is(PropertyNames.Syntax) || name.Is(PropertyNames.InitialValue) || + name.Is(PropertyNames.Inherits); + } + public Property CreateViewport(string name) { var feature = MediaFeatureFactory.Instance.Create(name); diff --git a/src/ExCSS/Parser/Lexer.cs b/src/ExCSS/Parser/Lexer.cs index 177fc564..84369431 100644 --- a/src/ExCSS/Parser/Lexer.cs +++ b/src/ExCSS/Parser/Lexer.cs @@ -94,10 +94,21 @@ private Token Data(char current) if (c1.IsNameStart()) return IdentStart(current); if (c1 == Symbols.ReverseSolidus && !c2.IsLineBreak() && c2 != Symbols.EndOfFile) return IdentStart(current); - if (c1 != Symbols.Minus || c2 != Symbols.GreaterThan) return NewDelimiter(current); - Advance(2); - return NewCloseComment(); + if (c1 == Symbols.Minus) + { + // "-->" closes an HTML-style comment, but any other "--" starts an ident, so a + // custom property name "--foo" (CSS Variables 1 2) is one ident. + if (c2 == Symbols.GreaterThan) + { + Advance(2); + return NewCloseComment(); + } + + return IdentStart(current); + } + + return NewDelimiter(current); } Back(); @@ -451,7 +462,9 @@ private Token IdentStart(char current) if (current == Symbols.Minus) { current = GetNext(); - if (current.IsNameStart() || IsValidEscape(current)) + // A second '-' also starts an ident, so a custom property name "--foo" (CSS Variables 1 2) + // lexes as one ident rather than a '-' delimiter followed by "-foo". + if (current.IsNameStart() || current == Symbols.Minus || IsValidEscape(current)) { StringBuffer.Append(Symbols.Minus); return IdentRest(current); diff --git a/src/ExCSS/Parser/StylesheetComposer.cs b/src/ExCSS/Parser/StylesheetComposer.cs index 25c7eb38..93afe506 100644 --- a/src/ExCSS/Parser/StylesheetComposer.cs +++ b/src/ExCSS/Parser/StylesheetComposer.cs @@ -39,6 +39,8 @@ public Rule CreateAtRule(Token token) if (token.Data.Is(RuleNames.Container)) return CreateContainer(token); + if (token.Data.Is(RuleNames.Property)) return CreateProperty(token); + return token.Data.Is(RuleNames.Document) ? CreateDocument(token) : CreateUnknown(token); } @@ -147,6 +149,28 @@ public Rule CreateFontFace(Token current) return SkipDeclarations(token); } + public Rule CreateProperty(Token current) + { + var rule = new PropertyRule(_parser); + var start = current.Position; + var token = NextToken(); + _nodes.Push(rule); + ParseComments(ref token); + rule.Name = GetRuleName(ref token); + ParseComments(ref token); + + if (token.Type == TokenType.CurlyBracketOpen) + { + var end = FillDeclarations(rule, PropertyFactory.Instance.CreatePropertyDescriptor); + rule.StylesheetText = CreateView(start, end); + _nodes.Pop(); + return rule; + } + + _nodes.Pop(); + return SkipDeclarations(token); + } + public Rule CreateImport(Token current) { var rule = new ImportRule(_parser); diff --git a/src/ExCSS/Rules/IPropertyRule.cs b/src/ExCSS/Rules/IPropertyRule.cs new file mode 100644 index 00000000..328c2d32 --- /dev/null +++ b/src/ExCSS/Rules/IPropertyRule.cs @@ -0,0 +1,21 @@ +namespace ExCSS +{ + /// + /// A registered custom property declared with an @property at-rule (CSS Properties and Values + /// API 1 §3). Exposes the three descriptors. + /// + public interface IPropertyRule : IRule, IProperties + { + /// The registered custom property name, e.g. --my-color. + string Name { get; set; } + + /// The raw syntax descriptor value (e.g. "<color>" or "*"). + string Syntax { get; set; } + + /// The raw initial-value descriptor value. + string InitialValue { get; set; } + + /// The raw inherits descriptor value (true or false). + string Inherits { get; set; } + } +} diff --git a/src/ExCSS/Rules/PropertyRule.cs b/src/ExCSS/Rules/PropertyRule.cs new file mode 100644 index 00000000..72f95f98 --- /dev/null +++ b/src/ExCSS/Rules/PropertyRule.cs @@ -0,0 +1,50 @@ +using System.IO; +using System.Linq; + +namespace ExCSS +{ + /// + /// An @property at-rule (CSS Properties and Values API 1 §3): registers a typed custom property + /// with syntax, initial-value and inherits descriptors. The block is stored like a + /// ; the registered name (a dashed-ident such as --my-color) is + /// captured from the prelude like . + /// + internal sealed class PropertyRule : DeclarationRule, IPropertyRule + { + internal PropertyRule(StylesheetParser parser) + : base(RuleType.Property, RuleNames.Property, parser) + { + } + + protected override Property CreateNewProperty(string name) + { + return PropertyFactory.Instance.CreatePropertyDescriptor(name); + } + + public string Name { get; set; } + + public string Syntax + { + get => GetValue(PropertyNames.Syntax); + set => SetValue(PropertyNames.Syntax, value); + } + + public string InitialValue + { + get => GetValue(PropertyNames.InitialValue); + set => SetValue(PropertyNames.InitialValue, value); + } + + public string Inherits + { + get => GetValue(PropertyNames.Inherits); + set => SetValue(PropertyNames.Inherits, value); + } + + public override void ToCss(TextWriter writer, IStyleFormatter formatter) + { + var declarations = formatter.Declarations(Declarations.Where(d => d.HasValue).Select(d => d.ToCss(formatter))); + writer.Write(string.Concat("@property ", Name, " { ", declarations, " }")); + } + } +}