Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/ExCSS.Tests/AtPropertyTests.cs
Original file line number Diff line number Diff line change
@@ -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: '<color>'; initial-value: red; inherits: false; }");

Assert.Equal(RuleType.Property, rule.Type);
Assert.Equal("--my-color", rule.Name);
Assert.Equal("\"<color>\"", rule.Syntax);
Assert.Equal("red", rule.InitialValue);
Assert.Equal("false", rule.Inherits);
}

[Fact]
public void AtPropertyRoundTrips()
{
var rule = Parse("@property --gap { syntax: '<length>'; initial-value: 0px; inherits: true; }");

Assert.Equal("@property --gap { syntax: \"<length>\"; 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: '<color>'; inherits: false; }");
var rule = Assert.IsAssignableFrom<IPropertyRule>(sheet.Rules.First());

Assert.Equal("--c", rule.Name);
}

[Fact]
public void AtPropertyAmongOtherRules()
{
var sheet = ParseStyleSheet(
".a { color: red } @property --c { syntax: '<color>'; inherits: false; } .b { color: blue }");

Assert.Equal(3, sheet.Rules.Length);
Assert.IsType<StyleRule>(sheet.Rules[0]);
Assert.IsType<PropertyRule>(sheet.Rules[1]);
Assert.IsType<StyleRule>(sheet.Rules[2]);
}
}
}
4 changes: 4 additions & 0 deletions src/ExCSS/Enumerations/PropertyNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down
1 change: 1 addition & 0 deletions src/ExCSS/Enumerations/RuleNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
3 changes: 2 additions & 1 deletion src/ExCSS/Enumerations/RuleType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public enum RuleType : byte
FontFeatureValues,
Viewport,
RegionStyle,
Container
Container,
Property
}
}
14 changes: 14 additions & 0 deletions src/ExCSS/Factories/PropertyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
21 changes: 17 additions & 4 deletions src/ExCSS/Parser/Lexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
24 changes: 24 additions & 0 deletions src/ExCSS/Parser/StylesheetComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down
21 changes: 21 additions & 0 deletions src/ExCSS/Rules/IPropertyRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace ExCSS
{
/// <summary>
/// A registered custom property declared with an <c>@property</c> at-rule (CSS Properties and Values
/// API 1 §3). Exposes the three descriptors.
/// </summary>
public interface IPropertyRule : IRule, IProperties
{
/// <summary>The registered custom property name, e.g. <c>--my-color</c>.</summary>
string Name { get; set; }

/// <summary>The raw <c>syntax</c> descriptor value (e.g. <c>"&lt;color&gt;"</c> or <c>"*"</c>).</summary>
string Syntax { get; set; }

/// <summary>The raw <c>initial-value</c> descriptor value.</summary>
string InitialValue { get; set; }

/// <summary>The raw <c>inherits</c> descriptor value (<c>true</c> or <c>false</c>).</summary>
string Inherits { get; set; }
}
}
50 changes: 50 additions & 0 deletions src/ExCSS/Rules/PropertyRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.IO;
using System.Linq;

namespace ExCSS
{
/// <summary>
/// An <c>@property</c> at-rule (CSS Properties and Values API 1 §3): registers a typed custom property
/// with <c>syntax</c>, <c>initial-value</c> and <c>inherits</c> descriptors. The block is stored like a
/// <see cref="FontFaceRule"/>; the registered name (a dashed-ident such as <c>--my-color</c>) is
/// captured from the prelude like <see cref="KeyframesRule.Name"/>.
/// </summary>
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, " }"));
}
}
}