mirror of
https://github.com/danog/dart-sass.git
synced 2025-01-22 22:02:00 +01:00
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:
parent
c850501621
commit
76953320aa
@ -6,6 +6,8 @@
|
||||
* Properly consider `b > c` to be a superselector of `a > b > c`, and similarly
|
||||
for other combinators.
|
||||
|
||||
* Properly calculate specificity for selector pseudoclasses.
|
||||
|
||||
* Deprecate use of `random()` when `$limit` has units to make it explicit that
|
||||
`random()` currently ignores units. A future version will no longer ignore
|
||||
units.
|
||||
|
@ -45,27 +45,13 @@ class ComplexSelector extends Selector {
|
||||
@internal
|
||||
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()`,
|
||||
/// can have a range of possible specificities.
|
||||
int get minSpecificity {
|
||||
if (_minSpecificity == null) _computeSpecificity();
|
||||
return _minSpecificity!;
|
||||
}
|
||||
|
||||
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;
|
||||
/// Specificity is represented in base 1000. The spec says this should be
|
||||
/// "sufficiently high"; it's extremely unlikely that any single selector
|
||||
/// sequence will contain 1000 simple selectors.
|
||||
late final int specificity = components.fold(
|
||||
0, (sum, component) => sum + component.selector.specificity);
|
||||
|
||||
/// If this compound selector is composed of a single compound selector with
|
||||
/// no combinators, returns it.
|
||||
@ -115,18 +101,6 @@ class ComplexSelector extends Selector {
|
||||
other.leadingCombinators.isEmpty &&
|
||||
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
|
||||
/// component in [components].
|
||||
///
|
||||
|
@ -25,27 +25,13 @@ class CompoundSelector extends Selector {
|
||||
/// This is never empty.
|
||||
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()`,
|
||||
/// can have a range of possible specificities.
|
||||
int get minSpecificity {
|
||||
if (_minSpecificity == null) _computeSpecificity();
|
||||
return _minSpecificity!;
|
||||
}
|
||||
|
||||
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;
|
||||
/// Specificity is represented in base 1000. The spec says this should be
|
||||
/// "sufficiently high"; it's extremely unlikely that any single selector
|
||||
/// sequence will contain 1000 simple selectors.
|
||||
late final int specificity =
|
||||
components.fold(0, (sum, component) => sum + component.specificity);
|
||||
|
||||
/// If this compound selector is composed of a single simple selector, returns
|
||||
/// it.
|
||||
@ -87,18 +73,6 @@ class CompoundSelector extends Selector {
|
||||
bool isSuperselector(CompoundSelector 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);
|
||||
|
||||
bool operator ==(Object other) =>
|
||||
|
@ -19,7 +19,7 @@ class IDSelector extends SimpleSelector {
|
||||
/// The ID name this selects for.
|
||||
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);
|
||||
|
||||
|
@ -2,9 +2,8 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:charcode/charcode.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../../utils.dart';
|
||||
@ -80,19 +79,30 @@ class PseudoSelector extends SimpleSelector {
|
||||
/// both non-`null`, the selector follows the argument.
|
||||
final SelectorList? selector;
|
||||
|
||||
int get minSpecificity {
|
||||
if (_minSpecificity == null) _computeSpecificity();
|
||||
return _minSpecificity!;
|
||||
}
|
||||
late final int specificity = () {
|
||||
if (isElement) return 1;
|
||||
var selector = this.selector;
|
||||
if (selector == null) return super.specificity;
|
||||
|
||||
int? _minSpecificity;
|
||||
|
||||
int get maxSpecificity {
|
||||
if (_maxSpecificity == null) _computeSpecificity();
|
||||
return _maxSpecificity!;
|
||||
}
|
||||
|
||||
int? _maxSpecificity;
|
||||
// https://drafts.csswg.org/selectors/#specificity-rules
|
||||
switch (normalizedName) {
|
||||
case 'where':
|
||||
return 0;
|
||||
case 'is':
|
||||
case 'not':
|
||||
case 'has':
|
||||
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,
|
||||
{bool element = false, this.argument, this.selector})
|
||||
@ -193,43 +203,6 @@ class PseudoSelector extends SimpleSelector {
|
||||
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);
|
||||
|
||||
// This intentionally uses identity for the selector list, if one is available.
|
||||
|
@ -27,21 +27,12 @@ final _subselectorPseudos = {
|
||||
/// {@category AST}
|
||||
/// {@category Parsing}
|
||||
abstract class SimpleSelector extends Selector {
|
||||
/// The minimum possible specificity that this selector can have.
|
||||
///
|
||||
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`,
|
||||
/// can have a range of possible specificities.
|
||||
/// This selector's specificity.
|
||||
///
|
||||
/// Specificity is represented in base 1000. The spec says this should be
|
||||
/// "sufficiently high"; it's extremely unlikely that any single selector
|
||||
/// sequence will contain 1000 simple selectors.
|
||||
int get minSpecificity => 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;
|
||||
int get specificity => 1000;
|
||||
|
||||
SimpleSelector();
|
||||
|
||||
|
@ -18,7 +18,7 @@ class TypeSelector extends SimpleSelector {
|
||||
/// The element name being selected.
|
||||
final QualifiedName name;
|
||||
|
||||
int get minSpecificity => 1;
|
||||
int get specificity => 1;
|
||||
|
||||
TypeSelector(this.name);
|
||||
|
||||
|
@ -21,7 +21,7 @@ class UniversalSelector extends SimpleSelector {
|
||||
/// Otherwise, it matches all elements in the given namespace.
|
||||
final String? namespace;
|
||||
|
||||
int get minSpecificity => 0;
|
||||
int get specificity => 0;
|
||||
|
||||
UniversalSelector({this.namespace});
|
||||
|
||||
|
@ -32,10 +32,10 @@ class 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,
|
||||
{this.mediaContext, int? specificity, bool original = false})
|
||||
: specificity = specificity ?? selector.maxSpecificity,
|
||||
: specificity = specificity ?? selector.specificity,
|
||||
isOriginal = original;
|
||||
|
||||
/// Asserts that the [mediaContext] for a selector is compatible with the
|
||||
|
@ -34,8 +34,6 @@ class Extension {
|
||||
final FileSpan span;
|
||||
|
||||
/// Creates a new extension.
|
||||
///
|
||||
/// If [specificity] isn't passed, it defaults to `extender.maxSpecificity`.
|
||||
Extension(
|
||||
ComplexSelector extender, FileSpan extenderSpan, this.target, this.span,
|
||||
{this.mediaContext, bool optional = false})
|
||||
@ -77,9 +75,9 @@ class 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})
|
||||
: specificity = specificity ?? selector.maxSpecificity,
|
||||
: specificity = specificity ?? selector.specificity,
|
||||
isOriginal = original;
|
||||
|
||||
/// Asserts that the [mediaContext] for a selector is compatible with the
|
||||
|
@ -261,7 +261,7 @@ class ExtensionStore {
|
||||
_extensionsByExtender.putIfAbsent(simple, () => []).add(extension);
|
||||
// Only source specificity for the original selector is relevant.
|
||||
// Selectors generated by `@extend` don't get new specificity.
|
||||
_sourceSpecificity.putIfAbsent(simple, () => complex.maxSpecificity);
|
||||
_sourceSpecificity.putIfAbsent(simple, () => complex.specificity);
|
||||
}
|
||||
|
||||
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.
|
||||
if (result.any((complex2) =>
|
||||
complex2.minSpecificity >= maxSpecificity &&
|
||||
complex2.specificity >= maxSpecificity &&
|
||||
complex2.isSuperselector(complex1))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selectors.take(i).any((complex2) =>
|
||||
complex2.minSpecificity >= maxSpecificity &&
|
||||
complex2.specificity >= maxSpecificity &&
|
||||
complex2.isSuperselector(complex1))) {
|
||||
continue;
|
||||
}
|
||||
|
@ -657,10 +657,8 @@ bool complexIsSuperselector(List<ComplexSelectorComponent> complex1,
|
||||
if (combinator1 == Combinator.followingSibling) {
|
||||
// The selector `.foo ~ .bar` is only a superselector of selectors that
|
||||
// *exclusively* contain subcombinators of `~`.
|
||||
if (!complex2
|
||||
.take(complex2.length - 1)
|
||||
.skip(i2)
|
||||
.every((component) => _isSupercombinator(
|
||||
if (!complex2.take(complex2.length - 1).skip(i2).every((component) =>
|
||||
_isSupercombinator(
|
||||
combinator1, component.combinators.firstOrNull))) {
|
||||
return false;
|
||||
}
|
||||
|
@ -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,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.5
|
||||
version: 3.0.0
|
||||
description: Additional APIs for Dart Sass.
|
||||
homepage: https://github.com/sass/dart-sass
|
||||
|
||||
|
@ -15,7 +15,7 @@ dependencies:
|
||||
async: ^2.5.0
|
||||
charcode: ^1.2.0
|
||||
cli_repl: ^0.2.1
|
||||
collection: ^1.15.0
|
||||
collection: ^1.16.0
|
||||
meta: ^1.3.0
|
||||
node_interop: ^2.1.0
|
||||
js: ^0.6.3
|
||||
|
Loading…
x
Reference in New Issue
Block a user