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
|
* 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.
|
||||||
|
@ -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].
|
||||||
///
|
///
|
||||||
|
@ -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) =>
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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});
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user