From 42d6fbb3edbc26b9e9c593b88f44253f04ceccef Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 2 Aug 2022 16:37:08 -0700 Subject: [PATCH] Ensure that selectors like :root always unify to the beginning (#1759) Closes #1811 --- CHANGELOG.md | 6 ++++ lib/src/extend/functions.dart | 56 +++++++++++++++++++---------------- pkg/sass_api/CHANGELOG.md | 4 +++ pkg/sass_api/pubspec.yaml | 4 +-- pubspec.yaml | 2 +- 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 734f2cbd2..cd7b27449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.54.1 + +* When unifying selectors for `@extend` and `selector.unify()`, ensure that + `:root`, `:scope`, `:host`, and `:host-context` only appear at the beginning + of complex selectors. + ## 1.54.0 * Deprecate selectors with leading or trailing combinators, or with multiple diff --git a/lib/src/extend/functions.dart b/lib/src/extend/functions.dart index 266d4cc2e..b6c433d3b 100644 --- a/lib/src/extend/functions.dart +++ b/lib/src/extend/functions.dart @@ -18,6 +18,10 @@ import 'package:tuple/tuple.dart'; import '../ast/selector.dart'; import '../utils.dart'; +/// Pseudo-selectors that can only meaningfully appear in the first component of +/// a complex selector. +final _rootishPseudoClasses = {'root', 'scope', 'host', 'host-context'}; + /// Returns the contents of a [SelectorList] that matches only elements that are /// matched by every complex selector in [complexes]. /// @@ -231,19 +235,22 @@ Iterable? _weaveParents( var trailingCombinators = _mergeTrailingCombinators(queue1, queue2); if (trailingCombinators == null) return null; - // Make sure there's at most one `:root` in the output. - var root1 = _firstIfRoot(queue1); - var root2 = _firstIfRoot(queue2); - if (root1 != null && root2 != null) { - var root = - unifyCompound(root1.selector.components, root2.selector.components); - if (root == null) return null; - queue1.addFirst(ComplexSelectorComponent(root, root1.combinators)); - queue2.addFirst(ComplexSelectorComponent(root, root2.combinators)); - } else if (root1 != null) { - queue2.addFirst(root1); - } else if (root2 != null) { - queue1.addFirst(root2); + // Make sure all selectors that are required to be at the root + var rootish1 = _firstIfRootish(queue1); + var rootish2 = _firstIfRootish(queue2); + if (rootish1 != null && rootish2 != null) { + var rootish = + unifyCompound(rootish1.selector.components, rootish2.selector.components); + if (rootish == null) return null; + queue1.addFirst(ComplexSelectorComponent(rootish, rootish1.combinators)); + queue2.addFirst(ComplexSelectorComponent(rootish, rootish2.combinators)); + } else if (rootish1 != null || rootish2 != null) { + // If there's only one rootish selector, it should only appear in the first + // position of the resulting selector. We can ensure that happens by adding + // it to the beginning of _both_ queues. + var rootish = (rootish1 ?? rootish2)!; + queue1.addFirst(rootish); + queue2.addFirst(rootish); } var groups1 = _groupSelectors(queue1); @@ -289,14 +296,19 @@ Iterable? _weaveParents( ]; } -/// If the first element of [queue] has a `:root` selector, removes and returns -/// that element. -ComplexSelectorComponent? _firstIfRoot(Queue queue) { +/// If the first element of [queue] has a selector like `:root` that can only +/// appear in a complex selector's first component, removes and returns that +/// element. +ComplexSelectorComponent? _firstIfRootish(Queue queue) { if (queue.isEmpty) return null; var first = queue.first; - if (!_hasRoot(first.selector)) return null; - queue.removeFirst(); - return first; + for (var simple in first.selector.components) { + if (simple is PseudoSelector && simple.isClass && _rootishPseudoClasses.contains(simple.normalizedName)) { + queue.removeFirst(); + return first; + } + } + return null; } /// Returns a leading combinator list that's compatible with both [combinators1] @@ -543,12 +555,6 @@ QueueList> _groupSelectors( return groups; } -/// Returns whether or not [compound] contains a `::root` selector. -bool _hasRoot(CompoundSelector compound) => compound.components.any((simple) => - simple is PseudoSelector && - simple.isClass && - simple.normalizedName == 'root'); - /// Returns whether [list1] is a superselector of [list2]. /// /// That is, whether [list1] matches every element that [list2] matches, as well diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 0a4de2b49..90431b2e4 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.1 + +* No user-visible changes. + ## 2.0.0 * Refactor the `CssMediaQuery` API to support new logical operators: diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 326ca2be7..e6eb28032 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -2,7 +2,7 @@ name: sass_api # Note: Every time we add a new Sass AST node, we need to bump the *major* # version because it's a breaking change for anyone who's implementing the # visitor interface(s). -version: 2.0.0 +version: 2.0.1 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - sass: 1.54.0 + sass: 1.54.1 dev_dependencies: dartdoc: ^5.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index da9e16616..4d3486caf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.54.0 +version: 1.54.1 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass