Skip to content

Commit

Permalink
Ensure that selectors like :root always unify to the beginning (#1759)
Browse files Browse the repository at this point in the history
Closes #1811
  • Loading branch information
nex3 committed Aug 2, 2022
1 parent bc8df44 commit 42d6fbb
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 28 deletions.
6 changes: 6 additions & 0 deletions 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
Expand Down
56 changes: 31 additions & 25 deletions lib/src/extend/functions.dart
Expand Up @@ -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].
///
Expand Down Expand Up @@ -231,19 +235,22 @@ Iterable<ComplexSelector>? _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);
Expand Down Expand Up @@ -289,14 +296,19 @@ Iterable<ComplexSelector>? _weaveParents(
];
}

/// If the first element of [queue] has a `:root` selector, removes and returns
/// that element.
ComplexSelectorComponent? _firstIfRoot(Queue<ComplexSelectorComponent> 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<ComplexSelectorComponent> 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]
Expand Down Expand Up @@ -543,12 +555,6 @@ QueueList<List<ComplexSelectorComponent>> _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
Expand Down
4 changes: 4 additions & 0 deletions 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:
Expand Down
4 changes: 2 additions & 2 deletions pkg/sass_api/pubspec.yaml
Expand Up @@ -2,15 +2,15 @@ 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

environment:
sdk: ">=2.12.0 <3.0.0"

dependencies:
sass: 1.54.0
sass: 1.54.1

dev_dependencies:
dartdoc: ^5.0.0
Expand Down
2 changes: 1 addition & 1 deletion 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

Expand Down

0 comments on commit 42d6fbb

Please sign in to comment.