Skip to content
Open
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
95 changes: 95 additions & 0 deletions src/ExCSS.Tests/CascadeLayerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Linq;
using Xunit;

namespace ExCSS.Tests
{
/// <summary>
/// <c>@layer</c> cascade layers (<see href="https://www.w3.org/TR/css-cascade-5/#layering">CSS
/// Cascade 5</see>): the block form (<c>@layer name { … }</c>, incl. anonymous) and the
/// order-declaring statement form (<c>@layer a, b, c;</c>). These assert the parsed object model;
/// cascade precedence between layers is left to the consumer.
/// </summary>
public class CascadeLayerTests : CssConstructionFunctions
{
[Fact]
public void NamedBlockLayerCapturesNameAndRules()
{
var sheet = ParseStyleSheet("@layer utilities { .a { color: red } }");
var rule = Assert.IsAssignableFrom<ILayerRule>(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<ILayerRule>(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<ILayerRule>(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<ILayerRule>(sheet.Rules.Skip(1).First());
}
}
}
1 change: 1 addition & 0 deletions src/ExCSS/Enumerations/RuleNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ 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";
}
}
4 changes: 3 additions & 1 deletion src/ExCSS/Enumerations/RuleType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public enum RuleType : byte
Viewport,
RegionStyle,
Container,
Property
Property,
Layer,
LayerStatement
}
}
74 changes: 74 additions & 0 deletions src/ExCSS/Parser/StylesheetComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public Rule CreateAtRule(Token token)

if (token.Data.Is(RuleNames.Property)) return CreateProperty(token);

if (token.Data.Is(RuleNames.Layer)) return CreateLayer(token);

return token.Data.Is(RuleNames.Document) ? CreateDocument(token) : CreateUnknown(token);
}

Expand Down Expand Up @@ -171,6 +173,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;
}

/// <summary>
/// Reads a <c>@layer</c> prelude: zero or more comma-separated layer names, each a dotted
/// ident sequence (e.g. <c>framework.utilities</c>). Advances <paramref name="token"/> up to
/// (but not past) the terminating <c>{</c>, <c>;</c>, or end of file.
/// </summary>
private List<string> ReadLayerNames(ref Token token)
{
var names = new List<string>();
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 CreateImport(Token current)
{
var rule = new ImportRule(_parser);
Expand Down
13 changes: 13 additions & 0 deletions src/ExCSS/Rules/ILayerRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace ExCSS
{
/// <summary>
/// The block form of the <c>@layer</c> cascade-layer at-rule (<c>@layer name { … }</c>, or an
/// anonymous <c>@layer { … }</c>). Groups the style rules that belong to a named cascade layer,
/// per <see href="https://www.w3.org/TR/css-cascade-5/#layering">CSS Cascade 5</see>.
/// </summary>
public interface ILayerRule : IGroupingRule
{
/// <summary>The layer's (optionally dotted) name; empty for an anonymous layer.</summary>
string Name { get; }
}
}
26 changes: 26 additions & 0 deletions src/ExCSS/Rules/LayerRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.IO;

namespace ExCSS
{
/// <summary>
/// The block form of <c>@layer</c> (<c>@layer name { rules }</c> or anonymous <c>@layer { rules }</c>).
/// 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.
/// </summary>
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));
}
}
}
26 changes: 26 additions & 0 deletions src/ExCSS/Rules/LayerStatementRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.IO;

namespace ExCSS
{
/// <summary>
/// The statement form of <c>@layer</c> (<c>@layer a, b, c;</c>). It declares the relative order of
/// one or more named cascade layers without providing any rules; a later block-form
/// <see cref="LayerRule"/> attaches rules to a layer already ordered by this statement, per
/// <see href="https://www.w3.org/TR/css-cascade-5/#layer-empty">CSS Cascade 5</see>.
/// </summary>
internal sealed class LayerStatementRule : Rule
{
internal LayerStatementRule(StylesheetParser parser)
: base(RuleType.LayerStatement, parser)
{
}

public List<string> Names { get; } = new List<string>();

public override void ToCss(TextWriter writer, IStyleFormatter formatter)
{
writer.Write(formatter.Rule("@layer", string.Join(", ", Names)));
}
}
}