From 67fecff83ae67d68875da6f3f07ff79d594de5da Mon Sep 17 00:00:00 2001 From: "Carlos (Goodwine)" <2022649+Goodwine@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:23:23 -0700 Subject: [PATCH] unifyComound() and unifyComplex() no longer move pseudo-classes across pseudo-element boundaries (#2350) * unifyComound() and unifyComplex() no longer move pseudo-classes across pseudo-element boundaries * Fix crash in TypeSelector.unify() when unifying an empty list --- CHANGELOG.md | 12 ++++++++ lib/src/ast/selector/type.dart | 2 +- lib/src/extend/functions.dart | 51 ++++++++++++++++++++++++---------- pkg/sass-parser/CHANGELOG.md | 4 +++ pkg/sass-parser/package.json | 2 +- pkg/sass_api/CHANGELOG.md | 4 +++ pkg/sass_api/pubspec.yaml | 4 +-- pubspec.yaml | 2 +- 8 files changed, 62 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a4e11fc5..b56e92291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 1.79.5 + +* Changes to how `selector.unify()` and `@extend` combine selectors: + + * The relative order of pseudo-classes (like `:hover`) and pseudo-elements + (like `::before`) within each original selector is now preserved when + they're combined. + + * Pseudo selectors are now consistently placed at the end of the combined + selector, regardless of which selector they came from. Previously, this + reordering only applied to pseudo-selectors in the second selector. + ## 1.79.4 ### JS API diff --git a/lib/src/ast/selector/type.dart b/lib/src/ast/selector/type.dart index d65f94a0c..a5ada4b8f 100644 --- a/lib/src/ast/selector/type.dart +++ b/lib/src/ast/selector/type.dart @@ -32,7 +32,7 @@ final class TypeSelector extends SimpleSelector { /// @nodoc @internal List? unify(List compound) { - if (compound.first case UniversalSelector() || TypeSelector()) { + if (compound.firstOrNull case UniversalSelector() || TypeSelector()) { var unified = unifyUniversalAndElement(this, compound.first); if (unified == null) return null; return [unified, ...compound.skip(1)]; diff --git a/lib/src/extend/functions.dart b/lib/src/extend/functions.dart index ca1d8c123..190bf4429 100644 --- a/lib/src/extend/functions.dart +++ b/lib/src/extend/functions.dart @@ -36,7 +36,7 @@ List? unifyComplex( List complexes, FileSpan span) { if (complexes.length == 1) return complexes; - List? unifiedBase; + CompoundSelector? unifiedBase; CssValue? leadingCombinator; CssValue? trailingCombinator; for (var complex in complexes) { @@ -67,12 +67,10 @@ List? unifyComplex( } if (unifiedBase == null) { - unifiedBase = base.selector.components; + unifiedBase = base.selector; } else { - for (var simple in base.selector.components) { - unifiedBase = simple.unify(unifiedBase!); // dart-lang/sdk#45348 - if (unifiedBase == null) return null; - } + unifiedBase = unifyCompound(unifiedBase, base.selector); + if (unifiedBase == null) return null; } } @@ -87,7 +85,7 @@ List? unifyComplex( var base = ComplexSelector( leadingCombinator == null ? const [] : [leadingCombinator], [ - ComplexSelectorComponent(CompoundSelector(unifiedBase!, span), + ComplexSelectorComponent(unifiedBase!, trailingCombinator == null ? const [] : [trailingCombinator], span) ], span, @@ -106,19 +104,44 @@ List? unifyComplex( /// Returns a [CompoundSelector] that matches only elements that are matched by /// both [compound1] and [compound2]. /// -/// The [span] will be used for the new unified selector. +/// The [compound1]'s `span` will be used for the new unified selector. +/// +/// This function ensures that the relative order of pseudo-classes (`:`) and +/// pseudo-elements (`::`) within each input selector is preserved in the +/// resulting combined selector. +/// +/// This function enforces a general rule that pseudo-classes (`:`) should come +/// before pseudo-elements (`::`), but it won't change their order if they were +/// originally interleaved within a single input selector. This prevents +/// unintended changes to the selector's meaning. For example, unifying +/// `::foo:bar` and `:baz` results in `:baz::foo:bar`. `:baz` is a pseudo-class, +/// so it is moved before the pseudo-class `::foo`. Meanwhile, `:bar` is not +/// moved before `::foo` because it appeared after `::foo` in the original +/// selector. /// /// If no such selector can be produced, returns `null`. CompoundSelector? unifyCompound( CompoundSelector compound1, CompoundSelector compound2) { - var result = compound2.components; - for (var simple in compound1.components) { - var unified = simple.unify(result); - if (unified == null) return null; - result = unified; + var result = compound1.components; + var pseudoResult = []; + var pseudoElementFound = false; + + for (var simple in compound2.components) { + // All pseudo-classes are unified separately after a pseudo-element to + // preserve their relative order with the pseudo-element. + if (pseudoElementFound && simple is PseudoSelector) { + var unified = simple.unify(pseudoResult); + if (unified == null) return null; + pseudoResult = unified; + } else { + pseudoElementFound |= simple is PseudoSelector && simple.isElement; + var unified = simple.unify(result); + if (unified == null) return null; + result = unified; + } } - return CompoundSelector(result, compound1.span); + return CompoundSelector([...result, ...pseudoResult], compound1.span); } /// Returns a [SimpleSelector] that matches only elements that are matched by diff --git a/pkg/sass-parser/CHANGELOG.md b/pkg/sass-parser/CHANGELOG.md index 6285e7eb6..ff13b901d 100644 --- a/pkg/sass-parser/CHANGELOG.md +++ b/pkg/sass-parser/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.4 + +* No user-visible changes. + ## 0.2.3 * No user-visible changes. diff --git a/pkg/sass-parser/package.json b/pkg/sass-parser/package.json index 3c8800b22..b0797a86c 100644 --- a/pkg/sass-parser/package.json +++ b/pkg/sass-parser/package.json @@ -1,6 +1,6 @@ { "name": "sass-parser", - "version": "0.2.3", + "version": "0.2.4", "description": "A PostCSS-compatible wrapper of the official Sass parser", "repository": "sass/sass", "author": "Google Inc.", diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 98d4f53ee..3d9f81d7f 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 12.0.5 + +* No user-visible changes. + ## 12.0.4 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 7c95cbf64..2a7e30b20 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -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: 12.0.4 +version: 12.0.5 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - sass: 1.79.4 + sass: 1.79.5 dev_dependencies: dartdoc: ^8.0.14 diff --git a/pubspec.yaml b/pubspec.yaml index 2d3c7153f..fd1b48e60 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.79.4 +version: 1.79.5 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass