Update specificity calculation for selector pseudos (#1781)

This is very close to invisible to the user and actually making it
visible would require a complex and hard-to-read test, so I'm electing
to avoid testing it.

Closes #2528
This commit is contained in:
Natalie Weizenbaum 2022-08-18 18:31:31 -07:00 committed by GitHub
parent c850501621
commit 76953320aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 57 additions and 146 deletions

View File

@ -6,6 +6,8 @@
* Properly consider `b > c` to be a superselector of `a > b > c`, and similarly * Properly consider `b > c` to be a superselector of `a > b > c`, and similarly
for other combinators. for other combinators.
* Properly calculate specificity for selector pseudoclasses.
* Deprecate use of `random()` when `$limit` has units to make it explicit that * Deprecate use of `random()` when `$limit` has units to make it explicit that
`random()` currently ignores units. A future version will no longer ignore `random()` currently ignores units. A future version will no longer ignore
units. units.

View File

@ -45,27 +45,13 @@ class ComplexSelector extends Selector {
@internal @internal
final bool lineBreak; final bool lineBreak;
/// The minimum possible specificity that this selector can have. /// This selector's specificity.
/// ///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`, /// Specificity is represented in base 1000. The spec says this should be
/// can have a range of possible specificities. /// "sufficiently high"; it's extremely unlikely that any single selector
int get minSpecificity { /// sequence will contain 1000 simple selectors.
if (_minSpecificity == null) _computeSpecificity(); late final int specificity = components.fold(
return _minSpecificity!; 0, (sum, component) => sum + component.selector.specificity);
}
int? _minSpecificity;
/// The maximum possible specificity that this selector can have.
///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`,
/// can have a range of possible specificities.
int get maxSpecificity {
if (_maxSpecificity == null) _computeSpecificity();
return _maxSpecificity!;
}
int? _maxSpecificity;
/// If this compound selector is composed of a single compound selector with /// If this compound selector is composed of a single compound selector with
/// no combinators, returns it. /// no combinators, returns it.
@ -115,18 +101,6 @@ class ComplexSelector extends Selector {
other.leadingCombinators.isEmpty && other.leadingCombinators.isEmpty &&
complexIsSuperselector(components, other.components); complexIsSuperselector(components, other.components);
/// Computes [_minSpecificity] and [_maxSpecificity].
void _computeSpecificity() {
var minSpecificity = 0;
var maxSpecificity = 0;
for (var component in components) {
minSpecificity += component.selector.minSpecificity;
maxSpecificity += component.selector.maxSpecificity;
}
_minSpecificity = minSpecificity;
_maxSpecificity = maxSpecificity;
}
/// Returns a copy of `this` with [combinators] added to the end of the final /// Returns a copy of `this` with [combinators] added to the end of the final
/// component in [components]. /// component in [components].
/// ///

View File

@ -25,27 +25,13 @@ class CompoundSelector extends Selector {
/// This is never empty. /// This is never empty.
final List<SimpleSelector> components; final List<SimpleSelector> components;
/// The minimum possible specificity that this selector can have. /// This selector's specificity.
/// ///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`, /// Specificity is represented in base 1000. The spec says this should be
/// can have a range of possible specificities. /// "sufficiently high"; it's extremely unlikely that any single selector
int get minSpecificity { /// sequence will contain 1000 simple selectors.
if (_minSpecificity == null) _computeSpecificity(); late final int specificity =
return _minSpecificity!; components.fold(0, (sum, component) => sum + component.specificity);
}
int? _minSpecificity;
/// The maximum possible specificity that this selector can have.
///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`,
/// can have a range of possible specificities.
int get maxSpecificity {
if (_maxSpecificity == null) _computeSpecificity();
return _maxSpecificity!;
}
int? _maxSpecificity;
/// If this compound selector is composed of a single simple selector, returns /// If this compound selector is composed of a single simple selector, returns
/// it. /// it.
@ -87,18 +73,6 @@ class CompoundSelector extends Selector {
bool isSuperselector(CompoundSelector other) => bool isSuperselector(CompoundSelector other) =>
compoundIsSuperselector(this, other); compoundIsSuperselector(this, other);
/// Computes [_minSpecificity] and [_maxSpecificity].
void _computeSpecificity() {
var minSpecificity = 0;
var maxSpecificity = 0;
for (var simple in components) {
minSpecificity += simple.minSpecificity;
maxSpecificity += simple.maxSpecificity;
}
_minSpecificity = minSpecificity;
_maxSpecificity = maxSpecificity;
}
int get hashCode => listHash(components); int get hashCode => listHash(components);
bool operator ==(Object other) => bool operator ==(Object other) =>

View File

@ -19,7 +19,7 @@ class IDSelector extends SimpleSelector {
/// The ID name this selects for. /// The ID name this selects for.
final String name; final String name;
int get minSpecificity => math.pow(super.minSpecificity, 2) as int; int get specificity => math.pow(super.specificity, 2) as int;
IDSelector(this.name); IDSelector(this.name);

View File

@ -2,9 +2,8 @@
// MIT-style license that can be found in the LICENSE file or at // MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
import 'dart:math' as math;
import 'package:charcode/charcode.dart'; import 'package:charcode/charcode.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import '../../utils.dart'; import '../../utils.dart';
@ -80,19 +79,30 @@ class PseudoSelector extends SimpleSelector {
/// both non-`null`, the selector follows the argument. /// both non-`null`, the selector follows the argument.
final SelectorList? selector; final SelectorList? selector;
int get minSpecificity { late final int specificity = () {
if (_minSpecificity == null) _computeSpecificity(); if (isElement) return 1;
return _minSpecificity!; var selector = this.selector;
} if (selector == null) return super.specificity;
int? _minSpecificity; // https://drafts.csswg.org/selectors/#specificity-rules
switch (normalizedName) {
int get maxSpecificity { case 'where':
if (_maxSpecificity == null) _computeSpecificity(); return 0;
return _maxSpecificity!; case 'is':
} case 'not':
case 'has':
int? _maxSpecificity; case 'matches':
return selector.components
.map((component) => component.specificity)
.max;
case 'nth-child':
case 'nth-last-child':
return super.specificity +
selector.components.map((component) => component.specificity).max;
default:
return super.specificity;
}
}();
PseudoSelector(this.name, PseudoSelector(this.name,
{bool element = false, this.argument, this.selector}) {bool element = false, this.argument, this.selector})
@ -193,43 +203,6 @@ class PseudoSelector extends SimpleSelector {
return CompoundSelector([this]).isSuperselector(CompoundSelector([other])); return CompoundSelector([this]).isSuperselector(CompoundSelector([other]));
} }
/// Computes [_minSpecificity] and [_maxSpecificity].
void _computeSpecificity() {
if (isElement) {
_minSpecificity = 1;
_maxSpecificity = 1;
return;
}
var selector = this.selector;
if (selector == null) {
_minSpecificity = super.minSpecificity;
_maxSpecificity = super.maxSpecificity;
return;
}
if (name == 'not') {
var minSpecificity = 0;
var maxSpecificity = 0;
for (var complex in selector.components) {
minSpecificity = math.max(minSpecificity, complex.minSpecificity);
maxSpecificity = math.max(maxSpecificity, complex.maxSpecificity);
}
_minSpecificity = minSpecificity;
_maxSpecificity = maxSpecificity;
} else {
// This is higher than any selector's specificity can actually be.
var minSpecificity = math.pow(super.minSpecificity, 3) as int;
var maxSpecificity = 0;
for (var complex in selector.components) {
minSpecificity = math.min(minSpecificity, complex.minSpecificity);
maxSpecificity = math.max(maxSpecificity, complex.maxSpecificity);
}
_minSpecificity = minSpecificity;
_maxSpecificity = maxSpecificity;
}
}
T accept<T>(SelectorVisitor<T> visitor) => visitor.visitPseudoSelector(this); T accept<T>(SelectorVisitor<T> visitor) => visitor.visitPseudoSelector(this);
// This intentionally uses identity for the selector list, if one is available. // This intentionally uses identity for the selector list, if one is available.

View File

@ -27,21 +27,12 @@ final _subselectorPseudos = {
/// {@category AST} /// {@category AST}
/// {@category Parsing} /// {@category Parsing}
abstract class SimpleSelector extends Selector { abstract class SimpleSelector extends Selector {
/// The minimum possible specificity that this selector can have. /// This selector's specificity.
///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`,
/// can have a range of possible specificities.
/// ///
/// Specificity is represented in base 1000. The spec says this should be /// Specificity is represented in base 1000. The spec says this should be
/// "sufficiently high"; it's extremely unlikely that any single selector /// "sufficiently high"; it's extremely unlikely that any single selector
/// sequence will contain 1000 simple selectors. /// sequence will contain 1000 simple selectors.
int get minSpecificity => 1000; int get specificity => 1000;
/// The maximum possible specificity that this selector can have.
///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`,
/// can have a range of possible specificities.
int get maxSpecificity => minSpecificity;
SimpleSelector(); SimpleSelector();

View File

@ -18,7 +18,7 @@ class TypeSelector extends SimpleSelector {
/// The element name being selected. /// The element name being selected.
final QualifiedName name; final QualifiedName name;
int get minSpecificity => 1; int get specificity => 1;
TypeSelector(this.name); TypeSelector(this.name);

View File

@ -21,7 +21,7 @@ class UniversalSelector extends SimpleSelector {
/// Otherwise, it matches all elements in the given namespace. /// Otherwise, it matches all elements in the given namespace.
final String? namespace; final String? namespace;
int get minSpecificity => 0; int get specificity => 0;
UniversalSelector({this.namespace}); UniversalSelector({this.namespace});

View File

@ -32,10 +32,10 @@ class Extender {
/// Creates a new extender. /// Creates a new extender.
/// ///
/// If [specificity] isn't passed, it defaults to `extender.maxSpecificity`. /// If [specificity] isn't passed, it defaults to `extender.specificity`.
Extender(this.selector, this.span, Extender(this.selector, this.span,
{this.mediaContext, int? specificity, bool original = false}) {this.mediaContext, int? specificity, bool original = false})
: specificity = specificity ?? selector.maxSpecificity, : specificity = specificity ?? selector.specificity,
isOriginal = original; isOriginal = original;
/// Asserts that the [mediaContext] for a selector is compatible with the /// Asserts that the [mediaContext] for a selector is compatible with the

View File

@ -34,8 +34,6 @@ class Extension {
final FileSpan span; final FileSpan span;
/// Creates a new extension. /// Creates a new extension.
///
/// If [specificity] isn't passed, it defaults to `extender.maxSpecificity`.
Extension( Extension(
ComplexSelector extender, FileSpan extenderSpan, this.target, this.span, ComplexSelector extender, FileSpan extenderSpan, this.target, this.span,
{this.mediaContext, bool optional = false}) {this.mediaContext, bool optional = false})
@ -77,9 +75,9 @@ class Extender {
/// Creates a new extender. /// Creates a new extender.
/// ///
/// If [specificity] isn't passed, it defaults to `extender.maxSpecificity`. /// If [specificity] isn't passed, it defaults to `extender.specificity`.
Extender(this.selector, this.span, {int? specificity, bool original = false}) Extender(this.selector, this.span, {int? specificity, bool original = false})
: specificity = specificity ?? selector.maxSpecificity, : specificity = specificity ?? selector.specificity,
isOriginal = original; isOriginal = original;
/// Asserts that the [mediaContext] for a selector is compatible with the /// Asserts that the [mediaContext] for a selector is compatible with the

View File

@ -261,7 +261,7 @@ class ExtensionStore {
_extensionsByExtender.putIfAbsent(simple, () => []).add(extension); _extensionsByExtender.putIfAbsent(simple, () => []).add(extension);
// Only source specificity for the original selector is relevant. // Only source specificity for the original selector is relevant.
// Selectors generated by `@extend` don't get new specificity. // Selectors generated by `@extend` don't get new specificity.
_sourceSpecificity.putIfAbsent(simple, () => complex.maxSpecificity); _sourceSpecificity.putIfAbsent(simple, () => complex.specificity);
} }
if (selectors != null || existingExtensions != null) { if (selectors != null || existingExtensions != null) {
@ -970,13 +970,13 @@ class ExtensionStore {
// trimmed, and thus that if there are two identical selectors only one is // trimmed, and thus that if there are two identical selectors only one is
// trimmed. // trimmed.
if (result.any((complex2) => if (result.any((complex2) =>
complex2.minSpecificity >= maxSpecificity && complex2.specificity >= maxSpecificity &&
complex2.isSuperselector(complex1))) { complex2.isSuperselector(complex1))) {
continue; continue;
} }
if (selectors.take(i).any((complex2) => if (selectors.take(i).any((complex2) =>
complex2.minSpecificity >= maxSpecificity && complex2.specificity >= maxSpecificity &&
complex2.isSuperselector(complex1))) { complex2.isSuperselector(complex1))) {
continue; continue;
} }

View File

@ -657,10 +657,8 @@ bool complexIsSuperselector(List<ComplexSelectorComponent> complex1,
if (combinator1 == Combinator.followingSibling) { if (combinator1 == Combinator.followingSibling) {
// The selector `.foo ~ .bar` is only a superselector of selectors that // The selector `.foo ~ .bar` is only a superselector of selectors that
// *exclusively* contain subcombinators of `~`. // *exclusively* contain subcombinators of `~`.
if (!complex2 if (!complex2.take(complex2.length - 1).skip(i2).every((component) =>
.take(complex2.length - 1) _isSupercombinator(
.skip(i2)
.every((component) => _isSupercombinator(
combinator1, component.combinators.firstOrNull))) { combinator1, component.combinators.firstOrNull))) {
return false; return false;
} }

View File

@ -1,6 +1,7 @@
## 2.0.5 ## 3.0.0
* No user-visible changes. * Replace the `minSpecificity` and `maxSpecificity` fields on `ComplexSelector`,
`CompoundSelector`, and `SimpleSelector` with a single `specificity` field.
## 2.0.4 ## 2.0.4

View File

@ -2,7 +2,7 @@ name: sass_api
# Note: Every time we add a new Sass AST node, we need to bump the *major* # 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 # version because it's a breaking change for anyone who's implementing the
# visitor interface(s). # visitor interface(s).
version: 2.0.5 version: 3.0.0
description: Additional APIs for Dart Sass. description: Additional APIs for Dart Sass.
homepage: https://github.com/sass/dart-sass homepage: https://github.com/sass/dart-sass

View File

@ -15,7 +15,7 @@ dependencies:
async: ^2.5.0 async: ^2.5.0
charcode: ^1.2.0 charcode: ^1.2.0
cli_repl: ^0.2.1 cli_repl: ^0.2.1
collection: ^1.15.0 collection: ^1.16.0
meta: ^1.3.0 meta: ^1.3.0
node_interop: ^2.1.0 node_interop: ^2.1.0
js: ^0.6.3 js: ^0.6.3