diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e073e41..0ae2a913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Released on Monday, November 2 2026. - Improved the parser with a nesting limit for values and conditions (`MaxValueNestingDepth`) (#260) @michaelthyregod +- Improved indexing rules with deeply nested `:is()`, `:where()`, or `:matches()` (#261) @michaelthyregod # 1.3.1 diff --git a/src/AngleSharp.Css.Tests/Styling/StyleRuleIndexTests.cs b/src/AngleSharp.Css.Tests/Styling/StyleRuleIndexTests.cs index fcd55a33..a74b71d4 100644 --- a/src/AngleSharp.Css.Tests/Styling/StyleRuleIndexTests.cs +++ b/src/AngleSharp.Css.Tests/Styling/StyleRuleIndexTests.cs @@ -72,6 +72,7 @@ public async Task GroupedAndUngroupedRulesKeepTheCascadeOrder(string css) [TestCase(":where(.x .a, div) { color: green }", "
A
")] @@ -89,6 +90,7 @@ public async Task RulesAreFoundViaTheirSubject(string css, string body) [TestCase(".a .b { color: red }", "A
A
")] [TestCase(":is(.x .a, .b) { color: red }", "A
")] + [TestCase(":is(:is(:is(:is(:is(:is(:is(:is(:is(.x))))))))) { color: red }", "A
")] public async Task RulesForOtherSubjectsDoNotApply(string css, string body) { var color = await ColorOfAsync(css, body); @@ -102,6 +104,9 @@ public async Task RulesForOtherSubjectsDoNotApply(string css, string body) [TestCase(":is(.x, .a) { color: red }", 1)] [TestCase(":where(.x .y, #z) { color: red }", 0)] [TestCase(":is(.x, :not(.y)) { color: red }", 1)] + // the index looks into 8 levels of :is(); a deeper rule stays ungrouped, i.e., a candidate + [TestCase(":is(:is(:is(:is(:is(:is(:is(:is(.x)))))))) { color: red }", 0)] + [TestCase(":is(:is(:is(:is(:is(:is(:is(:is(:is(.x))))))))) { color: red }", 1)] [TestCase("! { color: red }", 0)] public void OnlyRulesThatMayMatchAreCandidates(string css, int count) { diff --git a/src/AngleSharp.Css/Dom/Internal/StyleRuleIndex.cs b/src/AngleSharp.Css/Dom/Internal/StyleRuleIndex.cs index b1c59514..6e860eb6 100644 --- a/src/AngleSharp.Css/Dom/Internal/StyleRuleIndex.cs +++ b/src/AngleSharp.Css/Dom/Internal/StyleRuleIndex.cs @@ -355,17 +355,27 @@ private sealed class SubjectKeyVisitor : ISelectorVisitor PseudoClassNames.Matches, }; + // How many :is() levels are looked into. Each one parses its arguments again, so a + // deeply nested selector would cost quadratic time (and stack) to index; real ones + // nest a level or two. Deeper, the rule just stays ungrouped, which is always correct. + private const Int32 MaxArgumentDepth = 8; + private readonly ISelector _selector; private readonly ICssSelectorParser? _parser; + private readonly Int32 _depth; private SubjectKey[]? _keys; - private SubjectKeyVisitor(ISelector selector, ICssSelectorParser? parser) + private SubjectKeyVisitor(ISelector selector, ICssSelectorParser? parser, Int32 depth) { _selector = selector; _parser = parser; + _depth = depth; } - public static SubjectKey[]? GetKeys(ISelector? selector, ICssSelectorParser? parser) + public static SubjectKey[]? GetKeys(ISelector? selector, ICssSelectorParser? parser) => + GetKeys(selector, parser, 0); + + private static SubjectKey[]? GetKeys(ISelector? selector, ICssSelectorParser? parser, Int32 depth) { // a forced pseudo-class matches independent of its own selector if (selector is null || selector is ForcingPseudoClassSelector) @@ -373,7 +383,7 @@ private SubjectKeyVisitor(ISelector selector, ICssSelectorParser? parser) return null; } - var visitor = new SubjectKeyVisitor(selector, parser); + var visitor = new SubjectKeyVisitor(selector, parser, depth); selector.Accept(visitor); return visitor._keys; } @@ -409,7 +419,7 @@ void ISelectorVisitor.PseudoClass(String name) { // :is(.a .b, .c) only exposes its text, but an element matching it matches one // of the arguments, i.e., it requires one of their keys - just as a list does - var arguments = GetArguments(name); + var arguments = _depth < MaxArgumentDepth ? GetArguments(name) : null; if (arguments is not null && _parser is not null) { @@ -419,7 +429,7 @@ void ISelectorVisitor.PseudoClass(String name) // are valid only where the rule is (e.g., nested); then the keys are incomplete if (selector is not null && selector.Text.Equals(arguments, StringComparison.Ordinal)) { - _keys = GetKeys(selector, _parser); + _keys = GetKeys(selector, _parser, _depth + 1); } } } @@ -434,7 +444,7 @@ void ISelectorVisitor.List(IEnumerable