Skip to content

Commit c39af8a

Browse files
committed
Working on CSS nesting
1 parent 0c3aa78 commit c39af8a

File tree

8 files changed

+137
-10
lines changed

8 files changed

+137
-10
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Released on tbd.
1414
- Fixed computation of priority in CSS rules using multi selector
1515
- Fixed `GetInnerText` multi-line / text node behavior (#155) @Seyden
1616
- Added further compactification of CSS tuples (#89, #93)
17+
- Added support for CSS nesting in style rules (#148)
1718
- Added support for 8-digit hex color codes (#132)
1819
- Added more CSSOM possibilities and helpers (#6)
1920
- Added parts of recent color spec update such as `rgb` with spaces (#131)

src/AngleSharp.Css.Tests/Extensions/AnalysisWindow.cs

-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ namespace AngleSharp.Css.Tests.Extensions
22
{
33
using AngleSharp.Css.Dom;
44
using AngleSharp.Dom;
5-
using AngleSharp.Html.Dom;
65
using NUnit.Framework;
7-
using System.Linq;
86
using System.Text;
97
using System.Threading.Tasks;
108
using static CssConstructionFunctions;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace AngleSharp.Css.Tests.Extensions
2+
{
3+
using AngleSharp.Css.Dom;
4+
using AngleSharp.Dom;
5+
using NUnit.Framework;
6+
using static CssConstructionFunctions;
7+
8+
[TestFixture]
9+
public class NestingTests
10+
{
11+
[Test]
12+
public void SimpleSelectorNesting()
13+
{
14+
var source = @"<!doctype html><head><style>.foo {
15+
color: green;
16+
.bar {
17+
font-size: 1.4rem;
18+
}
19+
}</style></head><body class='foo'><div class='bar'>Larger and green";
20+
var document = ParseDocument(source);
21+
var window = document.DefaultView;
22+
Assert.IsNotNull(document);
23+
24+
var element = document.QuerySelector(".bar");
25+
Assert.IsNotNull(element);
26+
27+
var style = window.GetComputedStyle(element);
28+
Assert.IsNotNull(style);
29+
30+
Assert.AreEqual("1.4rem", style.GetFontSize());
31+
}
32+
}
33+
}

src/AngleSharp.Css/Dom/ICssStyleRule.cs

+7
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,12 @@ public interface ICssStyleRule : ICssRule
3232
/// Gets the selector for matching elements.
3333
/// </summary>
3434
Boolean TryMatch(IElement element, IElement? scope, out Priority specificity);
35+
36+
/// <summary>
37+
/// Gets a CSSRuleList of the CSS rules in the style sheet.
38+
/// </summary>
39+
[DomName("cssRules")]
40+
[DomName("rules")]
41+
ICssRuleList Rules { get; }
3542
}
3643
}

src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs

+16
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ sealed class CssStyleRule : CssRule, ICssStyleRule, ISelectorVisitor
1717
#region Fields
1818

1919
private readonly CssStyleDeclaration _style;
20+
private readonly CssRuleList _rules;
2021
private ISelector _selector;
2122
private IEnumerable<ISelector> _selectorList;
2223

@@ -28,6 +29,7 @@ internal CssStyleRule(ICssStyleSheet owner)
2829
: base(owner, CssRuleType.Style)
2930
{
3031
_style = new CssStyleDeclaration(this);
32+
_rules = new CssRuleList();
3133
_selectorList = null;
3234
}
3335

@@ -51,10 +53,24 @@ public String SelectorText
5153

5254
public CssStyleDeclaration Style => _style;
5355

56+
public ICssRuleList Rules => _rules;
57+
5458
#endregion
5559

5660
#region Methods
5761

62+
public void Add(ICssRule rule)
63+
{
64+
_rules.Add(rule);
65+
rule.SetParent(this);
66+
}
67+
68+
public void Remove(ICssRule rule)
69+
{
70+
_rules.Remove(rule);
71+
rule.SetParent(null);
72+
}
73+
5874
internal void SetInvalidSelector(String selectorText)
5975
{
6076
_selector = new InvalidSelector(selectorText);

src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs

+14-7
Original file line numberDiff line numberDiff line change
@@ -111,21 +111,28 @@ public static ICssStyleDeclaration ComputeCascadedStyle(this IEnumerable<ICssSty
111111

112112
private static IEnumerable<ICssStyleRule> SortBySpecificity(this IEnumerable<ICssStyleRule> rules, IElement element)
113113
{
114-
Tuple<ICssStyleRule, Priority> MapPriority(ICssStyleRule rule)
114+
IEnumerable<Tuple<ICssStyleRule, Priority>> MapPriority(ICssStyleRule rule)
115115
{
116116
if (rule.TryMatch(element, null, out var specificity))
117117
{
118-
return Tuple.Create(rule, specificity);
118+
yield return Tuple.Create(rule, specificity);
119+
120+
foreach (var subRule in rule.Rules)
121+
{
122+
if (subRule is ICssStyleRule style)
123+
{
124+
foreach (var item in MapPriority(style))
125+
{
126+
yield return item;
127+
}
128+
}
129+
}
119130
}
120-
121-
return null;
122131
}
123132

124-
return rules.Select(MapPriority).Where(IsNotNull).OrderBy(GetPriority).Select(GetRule);
133+
return rules.SelectMany(MapPriority).OrderBy(GetPriority).Select(GetRule);
125134
}
126135

127-
private static Boolean IsNotNull(Tuple<ICssStyleRule, Priority> item) => item is not null;
128-
129136
private static Priority GetPriority(Tuple<ICssStyleRule, Priority> item) => item.Item2;
130137

131138
private static ICssStyleRule GetRule(Tuple<ICssStyleRule, Priority> item) => item.Item1;

src/AngleSharp.Css/Parser/CssBuilder.cs

+13
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,19 @@ public void CreateDeclarationWith(ICssProperties properties, ref CssToken token)
524524
CollectTrivia(ref token);
525525
var start = token.Position;
526526

527+
if (token.IsPotentiallyNested() && properties is ICssStyleDeclaration decl && decl.Parent is CssStyleRule style)
528+
{
529+
var rule = new CssStyleRule(style.Owner);
530+
var result = CreateStyle(rule, token);
531+
532+
if (result is not null)
533+
{
534+
style.Add(result);
535+
token = NextToken();
536+
return;
537+
}
538+
}
539+
527540
if (token.IsNot(CssTokenType.EndOfFile, CssTokenType.CurlyBracketClose, CssTokenType.Colon) &&
528541
token.IsNot(CssTokenType.Semicolon, CssTokenType.CurlyBracketOpen))
529542
{

src/AngleSharp.Css/Parser/CssTokenExtensions.cs

+53-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
1-
namespace AngleSharp.Css.Parser
1+
namespace AngleSharp.Css.Parser
22
{
33
using AngleSharp.Css.Parser.Tokens;
4+
using AngleSharp.Text;
45
using System;
56

67
/// <summary>
78
/// Extensions to be used exclusively by the tokenizer.
89
/// </summary>
910
static class CssTokenExtensions
1011
{
12+
public static Boolean IsPotentiallyNested(this CssToken token)
13+
{
14+
if (token.Is(CssTokenType.Hash, CssTokenType.Colon, CssTokenType.SquareBracketOpen))
15+
{
16+
return true;
17+
}
18+
else if (token.Type == CssTokenType.Delim && token.Data.Length == 1)
19+
{
20+
return token.Data[0] switch
21+
{
22+
Symbols.Asterisk or Symbols.Plus or Symbols.Dollar or Symbols.Dot or Symbols.Tilde or Symbols.GreaterThan or Symbols.Ampersand => true,
23+
_ => false,
24+
};
25+
}
26+
27+
return false;
28+
}
29+
1130
/// <summary>
1231
/// Checks if the provided token is either of the first or the second
1332
/// type of token.
@@ -22,6 +41,39 @@ public static Boolean Is(this CssToken token, CssTokenType a, CssTokenType b)
2241
return type == a || type == b;
2342
}
2443

44+
/// <summary>
45+
/// Checks if the provided token is one of the list of tokens.
46+
/// </summary>
47+
/// <param name="token">The token to examine.</param>
48+
/// <param name="a">The 1st type to match.</param>
49+
/// <param name="b">The 2nd type to match.</param>
50+
/// <param name="c">The 3rd type to match.</param>
51+
/// <returns>Result of the examination.</returns>
52+
public static Boolean Is(this CssToken token, CssTokenType a, CssTokenType b, CssTokenType c)
53+
{
54+
var type = token.Type;
55+
return type == a || type == b || type == c;
56+
}
57+
58+
/// <summary>
59+
/// Checks if the provided token is one of the list of tokens.
60+
/// </summary>
61+
/// <param name="token">The token to examine.</param>
62+
/// <param name="a">The 1st type to match.</param>
63+
/// <param name="b">The 2nd type to match.</param>
64+
/// <param name="c">The 3rd type to match.</param>
65+
/// <param name="d">The 4th type to match.</param>
66+
/// <param name="e">The 5th type to match.</param>
67+
/// <param name="f">The 6th type to match.</param>
68+
/// <param name="g">The 7th type to match.</param>
69+
/// <param name="h">The 8th type to match.</param>
70+
/// <returns>Result of the examination.</returns>
71+
public static Boolean Is(this CssToken token, CssTokenType a, CssTokenType b, CssTokenType c, CssTokenType d, CssTokenType e, CssTokenType f, CssTokenType g, CssTokenType h)
72+
{
73+
var type = token.Type;
74+
return type == a || type == b || type == c || type == d || type == e || type == f || type == g || type == h;
75+
}
76+
2577
/// <summary>
2678
/// Checks if the provided token is neither of the first nor the second
2779
/// type of token.

0 commit comments

Comments
 (0)