Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update specificity calculation for selector pseudos #1781

Merged
merged 3 commits into from Aug 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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.
Expand Down
38 changes: 6 additions & 32 deletions lib/src/ast/selector/complex.dart
Expand Up @@ -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.
Expand Down Expand Up @@ -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].
///
Expand Down
38 changes: 6 additions & 32 deletions lib/src/ast/selector/compound.dart
Expand Up @@ -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.
Expand Down Expand Up @@ -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) =>
Expand Down
2 changes: 1 addition & 1 deletion lib/src/ast/selector/id.dart
Expand Up @@ -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);

Expand Down
77 changes: 25 additions & 52 deletions lib/src/ast/selector/pseudo.dart
Expand Up @@ -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';
Expand Down Expand Up @@ -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!;
}

int? _minSpecificity;

int get maxSpecificity {
if (_maxSpecificity == null) _computeSpecificity();
return _maxSpecificity!;
}

int? _maxSpecificity;
late final int specificity = () {
if (isElement) return 1;
var selector = this.selector;
if (selector == null) return super.specificity;

// https://drafts.csswg.org/selectors/#specificity-rules
Goodwine marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linking to https://www.w3.org/TR/selectors-4/#specificity-rules seems better than using the draft IMO.

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})
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 2 additions & 11 deletions lib/src/ast/selector/simple.dart
Expand Up @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion lib/src/ast/selector/type.dart
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion lib/src/ast/selector/universal.dart
Expand Up @@ -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});

Expand Down
4 changes: 2 additions & 2 deletions lib/src/extend/extender.dart
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions lib/src/extend/extension.dart
Expand Up @@ -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})
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions lib/src/extend/extension_store.dart
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 2 additions & 4 deletions lib/src/extend/functions.dart
Expand Up @@ -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;
}
Expand Down
5 changes: 3 additions & 2 deletions pkg/sass_api/CHANGELOG.md
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion pkg/sass_api/pubspec.yaml
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Expand Up @@ -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
Expand Down