diff --git a/src/ExCSS.Tests/CascadeLayerTests.cs b/src/ExCSS.Tests/CascadeLayerTests.cs
new file mode 100644
index 0000000..9bb9d63
--- /dev/null
+++ b/src/ExCSS.Tests/CascadeLayerTests.cs
@@ -0,0 +1,95 @@
+using System.Linq;
+using Xunit;
+
+namespace ExCSS.Tests
+{
+ ///
+ /// @layer cascade layers (CSS
+ /// Cascade 5): the block form (@layer name { … }, incl. anonymous) and the
+ /// order-declaring statement form (@layer a, b, c;). These assert the parsed object model;
+ /// cascade precedence between layers is left to the consumer.
+ ///
+ public class CascadeLayerTests : CssConstructionFunctions
+ {
+ [Fact]
+ public void NamedBlockLayerCapturesNameAndRules()
+ {
+ var sheet = ParseStyleSheet("@layer utilities { .a { color: red } }");
+ var rule = Assert.IsAssignableFrom(sheet.Rules.First());
+
+ Assert.Equal(RuleType.Layer, rule.Type);
+ Assert.Equal("utilities", rule.Name);
+ var inner = Assert.Single(rule.Rules);
+ Assert.Equal(".a", ((StyleRule)inner).SelectorText);
+ }
+
+ [Fact]
+ public void AnonymousBlockLayerHasEmptyName()
+ {
+ var sheet = ParseStyleSheet("@layer { .a { color: red } }");
+ var rule = Assert.IsAssignableFrom(sheet.Rules.First());
+
+ Assert.Equal(string.Empty, rule.Name);
+ Assert.Single(rule.Rules);
+ }
+
+ [Fact]
+ public void DottedLayerNameIsPreserved()
+ {
+ var sheet = ParseStyleSheet("@layer framework.utilities { .a { color: red } }");
+ var rule = Assert.IsAssignableFrom(sheet.Rules.First());
+
+ Assert.Equal("framework.utilities", rule.Name);
+ }
+
+ [Fact]
+ public void StatementLayerCapturesOrderedNames()
+ {
+ var sheet = ParseStyleSheet("@layer reset, base, utilities;");
+ var rule = (LayerStatementRule)sheet.Rules.First();
+
+ Assert.Equal(RuleType.LayerStatement, rule.Type);
+ Assert.Equal(new[] { "reset", "base", "utilities" }, rule.Names.ToArray());
+ }
+
+ [Fact]
+ public void StatementLayerWithDottedNames()
+ {
+ var sheet = ParseStyleSheet("@layer a.b, c;");
+ var rule = (LayerStatementRule)sheet.Rules.First();
+
+ Assert.Equal(new[] { "a.b", "c" }, rule.Names.ToArray());
+ }
+
+ [Fact]
+ public void BlockLayerRoundTrips()
+ {
+ var sheet = ParseStyleSheet("@layer utilities { .a { color: red } }");
+ Assert.Equal("@layer utilities { .a { color: rgb(255, 0, 0) } }", sheet.Rules.First().ToCss());
+ }
+
+ [Fact]
+ public void AnonymousBlockLayerRoundTrips()
+ {
+ var sheet = ParseStyleSheet("@layer { .a { color: red } }");
+ Assert.Equal("@layer { .a { color: rgb(255, 0, 0) } }", sheet.Rules.First().ToCss());
+ }
+
+ [Fact]
+ public void StatementLayerRoundTrips()
+ {
+ var sheet = ParseStyleSheet("@layer reset, base, utilities;");
+ Assert.Equal("@layer reset, base, utilities;", sheet.Rules.First().ToCss());
+ }
+
+ [Fact]
+ public void LayerAmongOtherRules()
+ {
+ var sheet = ParseStyleSheet(
+ ".a { color: red } @layer utils { .b { color: blue } } .c { color: green }");
+
+ Assert.Equal(3, sheet.Rules.Length);
+ Assert.IsAssignableFrom(sheet.Rules.Skip(1).First());
+ }
+ }
+}
diff --git a/src/ExCSS/Enumerations/RuleNames.cs b/src/ExCSS/Enumerations/RuleNames.cs
index be6dc8e..119401b 100644
--- a/src/ExCSS/Enumerations/RuleNames.cs
+++ b/src/ExCSS/Enumerations/RuleNames.cs
@@ -14,6 +14,7 @@ public static class RuleNames
public static readonly string Page = "page";
public static readonly string Container = "container";
public static readonly string Property = "property";
+ public static readonly string Layer = "layer";
public static readonly string FontPaletteValues = "font-palette-values";
}
}
\ No newline at end of file
diff --git a/src/ExCSS/Enumerations/RuleType.cs b/src/ExCSS/Enumerations/RuleType.cs
index 5d39a6a..618c06b 100644
--- a/src/ExCSS/Enumerations/RuleType.cs
+++ b/src/ExCSS/Enumerations/RuleType.cs
@@ -21,6 +21,8 @@ public enum RuleType : byte
RegionStyle,
Container,
Property,
+ Layer,
+ LayerStatement,
FontPaletteValues
}
}
\ No newline at end of file
diff --git a/src/ExCSS/Parser/StylesheetComposer.cs b/src/ExCSS/Parser/StylesheetComposer.cs
index b9ca26a..9d037ba 100644
--- a/src/ExCSS/Parser/StylesheetComposer.cs
+++ b/src/ExCSS/Parser/StylesheetComposer.cs
@@ -47,6 +47,8 @@ public Rule CreateAtRule(Token token)
if (token.Data.Is(RuleNames.Property)) return CreateProperty(token);
+ if (token.Data.Is(RuleNames.Layer)) return CreateLayer(token);
+
if (token.Data.Is(RuleNames.FontPaletteValues)) return CreateFontPaletteValues(token);
return token.Data.Is(RuleNames.Document) ? CreateDocument(token) : CreateUnknown(token);
@@ -179,6 +181,78 @@ public Rule CreateProperty(Token current)
return SkipDeclarations(token);
}
+ public Rule CreateLayer(Token current)
+ {
+ var start = current.Position;
+ var token = NextToken();
+ ParseComments(ref token);
+
+ // Read the (optional) comma-separated layer name list, stopping at '{' (block form) or
+ // ';'/EOF (statement form).
+ var names = ReadLayerNames(ref token);
+
+ if (token.Type == TokenType.CurlyBracketOpen)
+ {
+ // Block form: `@layer name { rules }` (at most one name; anonymous if none).
+ var rule = new LayerRule(_parser)
+ {
+ Name = names.Count > 0 ? names[0] : string.Empty
+ };
+ _nodes.Push(rule);
+ var end = FillRules(rule);
+ rule.StylesheetText = CreateView(start, end);
+ _nodes.Pop();
+ return rule;
+ }
+
+ // Statement form: `@layer a, b, c;` — declares layer order only, no rules. token is at ';'/EOF.
+ var statement = new LayerStatementRule(_parser);
+ statement.Names.AddRange(names);
+ statement.StylesheetText = CreateView(start, token.Position);
+ return statement;
+ }
+
+ ///
+ /// Reads a @layer prelude: zero or more comma-separated layer names, each a dotted
+ /// ident sequence (e.g. framework.utilities). Advances up to
+ /// (but not past) the terminating {, ;, or end of file.
+ ///
+ private List ReadLayerNames(ref Token token)
+ {
+ var names = new List();
+ var current = new System.Text.StringBuilder();
+
+ void Flush()
+ {
+ if (current.Length <= 0) return;
+ names.Add(current.ToString());
+ current.Clear();
+ }
+
+ while (token.IsNot(TokenType.EndOfFile, TokenType.CurlyBracketOpen, TokenType.Semicolon))
+ {
+ switch (token.Type)
+ {
+ case TokenType.Ident:
+ current.Append(token.Data);
+ break;
+ case TokenType.Delim when token.Data == ".":
+ current.Append('.');
+ break;
+ case TokenType.Comma:
+ Flush();
+ break;
+ // Whitespace/comments and anything else separating names are ignored.
+ }
+
+ token = NextToken();
+ ParseComments(ref token);
+ }
+
+ Flush();
+ return names;
+ }
+
public Rule CreateFontPaletteValues(Token current)
{
var rule = new FontPaletteValuesRule(_parser);
diff --git a/src/ExCSS/Rules/ILayerRule.cs b/src/ExCSS/Rules/ILayerRule.cs
new file mode 100644
index 0000000..af95b93
--- /dev/null
+++ b/src/ExCSS/Rules/ILayerRule.cs
@@ -0,0 +1,13 @@
+namespace ExCSS
+{
+ ///
+ /// The block form of the @layer cascade-layer at-rule (@layer name { … }, or an
+ /// anonymous @layer { … }). Groups the style rules that belong to a named cascade layer,
+ /// per CSS Cascade 5.
+ ///
+ public interface ILayerRule : IGroupingRule
+ {
+ /// The layer's (optionally dotted) name; empty for an anonymous layer.
+ string Name { get; }
+ }
+}
diff --git a/src/ExCSS/Rules/LayerRule.cs b/src/ExCSS/Rules/LayerRule.cs
new file mode 100644
index 0000000..f838b3e
--- /dev/null
+++ b/src/ExCSS/Rules/LayerRule.cs
@@ -0,0 +1,26 @@
+using System.IO;
+
+namespace ExCSS
+{
+ ///
+ /// The block form of @layer (@layer name { rules } or anonymous @layer { rules }).
+ /// A grouping rule holding the style rules assigned to a cascade layer; cascade precedence between
+ /// layers is resolved by the consumer when the rules are indexed, not here.
+ ///
+ internal sealed class LayerRule : GroupingRule, ILayerRule
+ {
+ internal LayerRule(StylesheetParser parser)
+ : base(RuleType.Layer, parser)
+ {
+ }
+
+ public string Name { get; set; } = string.Empty;
+
+ public override void ToCss(TextWriter writer, IStyleFormatter formatter)
+ {
+ var rules = formatter.Block(Rules);
+ var name = string.IsNullOrEmpty(Name) ? "@layer" : $"@layer {Name}";
+ writer.Write(formatter.Rule(name, null, rules));
+ }
+ }
+}
diff --git a/src/ExCSS/Rules/LayerStatementRule.cs b/src/ExCSS/Rules/LayerStatementRule.cs
new file mode 100644
index 0000000..4422d80
--- /dev/null
+++ b/src/ExCSS/Rules/LayerStatementRule.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace ExCSS
+{
+ ///
+ /// The statement form of @layer (@layer a, b, c;). It declares the relative order of
+ /// one or more named cascade layers without providing any rules; a later block-form
+ /// attaches rules to a layer already ordered by this statement, per
+ /// CSS Cascade 5.
+ ///
+ internal sealed class LayerStatementRule : Rule
+ {
+ internal LayerStatementRule(StylesheetParser parser)
+ : base(RuleType.LayerStatement, parser)
+ {
+ }
+
+ public List Names { get; } = new List();
+
+ public override void ToCss(TextWriter writer, IStyleFormatter formatter)
+ {
+ writer.Write(formatter.Rule("@layer", string.Join(", ", Names)));
+ }
+ }
+}