540 lines
18 KiB
SCSS
540 lines
18 KiB
SCSS
|
//
|
||
|
// Copyright 2021 Google Inc.
|
||
|
//
|
||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
// of this software and associated documentation files (the "Software"), to deal
|
||
|
// in the Software without restriction, including without limitation the rights
|
||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
// copies of the Software, and to permit persons to whom the Software is
|
||
|
// furnished to do so, subject to the following conditions:
|
||
|
//
|
||
|
// The above copyright notice and this permission notice shall be included in
|
||
|
// all copies or substantial portions of the Software.
|
||
|
//
|
||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
|
// THE SOFTWARE.
|
||
|
//
|
||
|
|
||
|
@use 'sass:list';
|
||
|
@use 'sass:math';
|
||
|
@use 'sass:selector';
|
||
|
@use 'sass:string';
|
||
|
|
||
|
// A collection of extensions to the sass:selector module
|
||
|
// https://sass-lang.com/documentation/modules/selector
|
||
|
|
||
|
/// Recursively negates and flattens a compound selector.
|
||
|
///
|
||
|
/// This is useful for IE11 support when appending two selectors when the second
|
||
|
/// selector may have another nested `:not()`, since IE11 does not supported
|
||
|
/// complex selector lists within `:not()`.
|
||
|
///
|
||
|
/// @example - scss
|
||
|
/// $selector1: '.foo';
|
||
|
/// $selector2: '.bar:not(.baz)';
|
||
|
///
|
||
|
/// #{selector.append($selector1, ':not(#{$selector2})'} {
|
||
|
/// /* Modern browsers */
|
||
|
/// }
|
||
|
///
|
||
|
/// #{selector.append($selector1, negate($selector2)} {
|
||
|
/// /* IE11 support */
|
||
|
/// }
|
||
|
///
|
||
|
/// @example - css
|
||
|
/// .foo:not(.bar:not(.baz)) {
|
||
|
/// /* Modern browsers */
|
||
|
/// }
|
||
|
///
|
||
|
/// .foo:not(.bar), .foo.baz {
|
||
|
/// /* IE11 support */
|
||
|
/// }
|
||
|
///
|
||
|
/// @param {String} $compound-selector - A compound selector to negate.
|
||
|
/// @return {List} The negated selector in selector value format.
|
||
|
@function negate($compound-selector) {
|
||
|
$result: null;
|
||
|
@each $simple-selector in selector.simple-selectors($compound-selector) {
|
||
|
$to-append: null;
|
||
|
@if string.index($simple-selector, ':not(') == 1 {
|
||
|
$inside-not: string.slice($simple-selector, 6, -2);
|
||
|
$inside-not-simple-selectors: selector.simple-selectors($inside-not);
|
||
|
$inside-not-result: null;
|
||
|
|
||
|
@each $inside-not-simple-selector
|
||
|
in selector.simple-selectors($inside-not)
|
||
|
{
|
||
|
@if $inside-not-result == null {
|
||
|
// Skip the first simple selector, which has already been negated by
|
||
|
// removing :not() when parsing $inside-not.
|
||
|
$inside-not-result: selector.parse($inside-not-simple-selector);
|
||
|
} @else {
|
||
|
// Flatten nested :not()s ".foo:not(.bar:not(.baz))" (IE11 support)
|
||
|
@if string.index($inside-not-simple-selector, ':not(') == 1 {
|
||
|
$inside-not-result: list.join(
|
||
|
$inside-not-result,
|
||
|
_negate($inside-not-simple-selector)
|
||
|
);
|
||
|
} @else {
|
||
|
$inside-not-result: selector.append(
|
||
|
$inside-not-result,
|
||
|
$inside-not-simple-selector
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$result: if(
|
||
|
$result,
|
||
|
list.join($result, $inside-not-result),
|
||
|
$inside-not-result
|
||
|
);
|
||
|
} @else {
|
||
|
$to-append: string.unquote(':not(#{$simple-selector})');
|
||
|
$result: if($result, selector.append($result, $to-append), $to-append);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@return $result;
|
||
|
}
|
||
|
|
||
|
/// Identical to `selector.append()`, but adheres strictly to CSS compound
|
||
|
/// selector order.
|
||
|
///
|
||
|
/// @example - scss
|
||
|
/// .foo::before {
|
||
|
/// &[dir=rtl] { /* Invalid */ }
|
||
|
/// }
|
||
|
///
|
||
|
/// .foo::before {
|
||
|
/// @include append-strict(&, '[dir=rtl]') { /* Valid */ }
|
||
|
/// }
|
||
|
///
|
||
|
/// @example - css
|
||
|
/// .foo::before[dir=rtl] {
|
||
|
/// /* Invalid */
|
||
|
/// }
|
||
|
///
|
||
|
/// .foo[dir=rtl]::before {
|
||
|
/// /* Valid */
|
||
|
/// }
|
||
|
///
|
||
|
/// This is useful for mixins where the parent selector is unknown and the
|
||
|
/// appended selector's position is critical to maintain valid CSS.
|
||
|
///
|
||
|
/// @param {List} $selectors - One or more selectors to append.
|
||
|
@mixin append-strict($selectors...) {
|
||
|
@at-root {
|
||
|
#{append-strict($selectors...)} {
|
||
|
@content;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Function version of `append-strict()`. Use this instead of the mixin along
|
||
|
/// with `@at-root` when combining the result of `append-strict()` with other
|
||
|
/// selectors.
|
||
|
///
|
||
|
/// @example - scss
|
||
|
/// .foo::before {
|
||
|
/// // Cannot add a list of other selectors with an @include mixin.
|
||
|
/// // @include append-strict(&, ':hover'), & {}
|
||
|
///
|
||
|
/// @at-root {
|
||
|
/// // Use @at-root and interpolation to add additional selectors
|
||
|
/// #{append-strict(&, ':hover')},
|
||
|
/// & {
|
||
|
/// color: inherit;
|
||
|
/// }
|
||
|
/// }
|
||
|
/// }
|
||
|
///
|
||
|
/// @example - css
|
||
|
/// .foo:hover::before,
|
||
|
/// .foo::before {
|
||
|
/// color: inherit;
|
||
|
/// }
|
||
|
///
|
||
|
/// @see {mixin} append-strict
|
||
|
///
|
||
|
/// @param {List} $selectors - One or more selectors to append.
|
||
|
/// @return {List} The appended selectors in selector value format.
|
||
|
@function append-strict($selectors...) {
|
||
|
$selector-lists: ();
|
||
|
@each $selector in $selectors {
|
||
|
$selector-lists: list.append($selector-lists, selector.parse($selector));
|
||
|
}
|
||
|
|
||
|
@return _append-strict($selector-lists);
|
||
|
}
|
||
|
|
||
|
/// Iterates through multiple selector Lists and strictly appends every
|
||
|
/// combination of each selector lists' complex selectors.
|
||
|
///
|
||
|
/// @see {mixin} _append-strict-complex-selectors
|
||
|
///
|
||
|
/// @param {List} $selector-lists - A List of selector lists to append.
|
||
|
/// @return {List} A single selector List resulting from appending all the
|
||
|
/// provided selector lists.
|
||
|
@function _append-strict($selector-lists) {
|
||
|
$length: list.length($selector-lists);
|
||
|
// Track the current index of each complex selector (within each selector
|
||
|
// list) that we are creating a combination of.
|
||
|
//
|
||
|
// Selectors: ((1), (2a, 2b), (3))
|
||
|
// Combinations: (1, 2a, 3), (1, 2b, 3)
|
||
|
//
|
||
|
// Initialize it to the first complex selector index for each selector list.
|
||
|
$current-indices: ();
|
||
|
@for $i from 1 through $length {
|
||
|
$current-indices: list.append($current-indices, 1);
|
||
|
}
|
||
|
|
||
|
// The final result: a single selector list resulting from appending the
|
||
|
// provided selector lists.
|
||
|
$selector-list-result: ();
|
||
|
|
||
|
$has-more-combinations: true;
|
||
|
@while $has-more-combinations {
|
||
|
// A combination of complex selectors to add to the selector list result.
|
||
|
$complex-selector-combination: ();
|
||
|
@for $i from 1 through $length {
|
||
|
$selector-list: list.nth($selector-lists, $i);
|
||
|
$current-index: list.nth($current-indices, $i);
|
||
|
$complex-selector: list.nth($selector-list, $current-index);
|
||
|
$complex-selector-combination: _append-strict-complex-selectors(
|
||
|
$complex-selector-combination,
|
||
|
$complex-selector
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$selector-list-result: list.append(
|
||
|
$selector-list-result,
|
||
|
$complex-selector-combination,
|
||
|
$separator: comma
|
||
|
);
|
||
|
|
||
|
// Increase the index of the last selector list's complex selector to its
|
||
|
// next one. If it reaches the length of the array, reset to 1 and bump the
|
||
|
// next selector list index.
|
||
|
//
|
||
|
// Given selector lists: ((1), (2a, 2b), (3))
|
||
|
// At indices: ((1), (1), (1))
|
||
|
// Try bumping: ((1), (1), (2*)) *This index is >length of 1 for the list
|
||
|
// Bump the next: ((1), (2), (1))
|
||
|
$bump-next-list-index: true;
|
||
|
@for $neg-i from $length * -1 through -1 {
|
||
|
@if $bump-next-list-index {
|
||
|
$i: math.abs($neg-i);
|
||
|
$selector-list: list.nth($selector-lists, $i);
|
||
|
$current-index: list.nth($current-indices, $i);
|
||
|
$next-index: $current-index + 1;
|
||
|
@if $next-index > list.length($selector-list) {
|
||
|
// Reset to start for the list and bump the next list (technically
|
||
|
// previous since we're iterating backwards).
|
||
|
$next-index: 1;
|
||
|
$bump-next-list-index: true;
|
||
|
} @else {
|
||
|
// If we bumped to the next index for this selector list, we can
|
||
|
// "break" the loop and continue to form the next combination.
|
||
|
$bump-next-list-index: false;
|
||
|
}
|
||
|
|
||
|
// Save the new current index for this selector list.
|
||
|
$current-indices: list.set-nth($current-indices, $i, $next-index);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// When the first selector list reaches its length, it will ask to bump the
|
||
|
// next selector list index. There are no more selector lists, which means
|
||
|
// there are no more combinations and the loop may end.
|
||
|
@if $bump-next-list-index {
|
||
|
$has-more-combinations: false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@return $selector-list-result;
|
||
|
}
|
||
|
|
||
|
/// Appends two complex selectors together, strictly adhering to the CSS
|
||
|
/// `<compound-selector>` type definition to avoid forming invalid resulting
|
||
|
/// compound selectors.
|
||
|
///
|
||
|
/// @param {List} $complex-selector-a - The first List of space-separated
|
||
|
/// compound selectors.
|
||
|
/// @param {List} $complex-selector-a - The second List of space-separated
|
||
|
/// compound selectors.
|
||
|
/// @return {List} A resulting appended complex selector.
|
||
|
@function _append-strict-complex-selectors(
|
||
|
$complex-selector-a,
|
||
|
$complex-selector-b
|
||
|
) {
|
||
|
// If one of the lists is empty, return the other list.
|
||
|
@if list.length($complex-selector-a) < 1 {
|
||
|
@return $complex-selector-b;
|
||
|
}
|
||
|
|
||
|
@if list.length($complex-selector-b) < 1 {
|
||
|
@return $complex-selector-a;
|
||
|
}
|
||
|
|
||
|
// The "joining" of A and B happens at the last compound selector of A and the
|
||
|
// first compound selector of B.
|
||
|
//
|
||
|
// Example:
|
||
|
// ".foo .bar" and ".baz :hover" will append as
|
||
|
// ".foo .bar.baz :hover"
|
||
|
$last-compound-selector-a: list.nth(
|
||
|
$complex-selector-a,
|
||
|
list.length($complex-selector-a)
|
||
|
);
|
||
|
$first-compound-selector-b: list.nth($complex-selector-b, 1);
|
||
|
|
||
|
// The compound selector CSS joining (".bar" and ".baz") and their sorting
|
||
|
// only needs to happen on the last/first of A/B.
|
||
|
$simple-selectors-a: selector.simple-selectors($last-compound-selector-a);
|
||
|
$simple-selectors-b: selector.simple-selectors($first-compound-selector-b);
|
||
|
$sorted-simple-selectors: _sort-simple-selectors(
|
||
|
list.join($simple-selectors-a, $simple-selectors-b)
|
||
|
);
|
||
|
|
||
|
// The result can start to form by setting the last compound selector of A to
|
||
|
// the result of the sorted and joined ".bar.baz"...
|
||
|
$result: list.set-nth(
|
||
|
$complex-selector-a,
|
||
|
list.length($complex-selector-a),
|
||
|
_join-simple-selectors($sorted-simple-selectors)
|
||
|
);
|
||
|
|
||
|
// ...then adding the remaining compound selectors (excluding the first one,
|
||
|
// which was already appended) from B.
|
||
|
@if list.length($complex-selector-b) > 1 {
|
||
|
@for $i from 2 through list.length($complex-selector-b) {
|
||
|
$result: list.append(list.nth($complex-selector-b, $i));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@return $result;
|
||
|
}
|
||
|
|
||
|
/// Combines a List of simple selectors together to form a compound selector.
|
||
|
/// If there are any pseudo class function selectors that should nest their
|
||
|
/// selectors within their parentheses, this function will do so.
|
||
|
///
|
||
|
/// @param {List} $simple-selectors - A List of simple selectors to join.
|
||
|
/// @return {String} A compound selector.
|
||
|
@function _join-simple-selectors($simple-selectors) {
|
||
|
$compound-selector: '';
|
||
|
$parens-index: _get-nestable-parens-index($simple-selectors);
|
||
|
@if $parens-index {
|
||
|
// Contains a selector, such as :host() that other selectors must be placed
|
||
|
// within the parentheses of. This selector should be moved to the front of
|
||
|
// the compound selector.
|
||
|
$compound-selector: list.nth($simple-selectors, $parens-index);
|
||
|
@if string.index($compound-selector, '(') != null {
|
||
|
// Already has parens. Remove the final closing parens so that additional
|
||
|
// selectors are placed within the parentheses.
|
||
|
$compound-selector: string.slice(
|
||
|
$compound-selector,
|
||
|
1,
|
||
|
string.length($compound-selector) - 1
|
||
|
);
|
||
|
} @else {
|
||
|
// Otherwise, add an opening parens.
|
||
|
$compound-selector: #{$compound-selector}#{string.unquote('(')};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@for $i from 1 through list.length($simple-selectors) {
|
||
|
@if $i != $parens-index {
|
||
|
// Skip the parens selector that was moved to the front, if any
|
||
|
$simple-selector: list.nth($simple-selectors, $i);
|
||
|
$compound-selector: #{$compound-selector}#{$simple-selector};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@if $parens-index {
|
||
|
// Add the closing parens
|
||
|
$compound-selector: #{$compound-selector}#{string.unquote(')')};
|
||
|
}
|
||
|
|
||
|
@return $compound-selector;
|
||
|
}
|
||
|
|
||
|
/// Searches a List of simple selectors for any pseudo class functions that can
|
||
|
/// and should be nested with other selectors. If one is found, the index is
|
||
|
/// returned.
|
||
|
///
|
||
|
/// @see {mixin} _can-and-should-nest-pseudo-class
|
||
|
///
|
||
|
/// @param {List} $simple-selectors - A List of simple selectors to search.
|
||
|
/// @return {Number} The index of the selector with parens to nest, or null if
|
||
|
/// there is none.
|
||
|
@function _get-nestable-parens-index($simple-selectors) {
|
||
|
@for $i from 1 through list.length($simple-selectors) {
|
||
|
$simple-selector: list.nth($simple-selectors, $i);
|
||
|
@if _can-and-should-nest-pseudo-class($simple-selector) {
|
||
|
@return $i;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@return null;
|
||
|
}
|
||
|
|
||
|
/// Checks two things:
|
||
|
///
|
||
|
/// 1. If a simple selector is a pseudo class function that accepts selectors
|
||
|
/// as its arguments.
|
||
|
/// 2. If this selector is commonly used for nesting within.
|
||
|
///
|
||
|
/// For example, `:host` satisfies both #1 and #2, but the `:not()` pseudo class
|
||
|
/// is not commonly used in abstract nesting within Sass.
|
||
|
///
|
||
|
/// @example - scss
|
||
|
/// :host(:hover) {
|
||
|
/// :enabled {
|
||
|
/// // commonly expect :host(:hover:enabled),
|
||
|
/// // since :host(:hover):enabled is invalid CSS
|
||
|
/// }
|
||
|
/// }
|
||
|
///
|
||
|
/// :not(:hover) {
|
||
|
/// :enabled {
|
||
|
/// // commonly expect :not(:hover):enabled
|
||
|
/// // do not expect :not(:hover:enabled) as the intention
|
||
|
/// }
|
||
|
/// }
|
||
|
///
|
||
|
/// @param {String} $simple-selector - The simple selector to check.
|
||
|
/// @return {Bool} True if the simple selector is a pseudo class function that
|
||
|
/// should nest additional selectors within its parentheses.
|
||
|
@function _can-and-should-nest-pseudo-class($simple-selector) {
|
||
|
@return string.index($simple-selector, ':host') != null or
|
||
|
string.index($simple-selector, '::slotted') != null;
|
||
|
}
|
||
|
|
||
|
/// Sorts a List of simple selectors according to the `<compound-selector>` CSS
|
||
|
/// type definition.
|
||
|
///
|
||
|
/// ```
|
||
|
/// <compound-selector> = [ <type-selector>? <subclass-selector>*
|
||
|
/// [ <pseudo-element-selector> <pseudo-class-selector>* ]* ]!
|
||
|
/// ```
|
||
|
///
|
||
|
/// @example - scss
|
||
|
/// $unsorted: (':hover', '::before', 'h1');
|
||
|
/// #{selector.append(_sort-simple-selectors($unsorted...))} {}
|
||
|
///
|
||
|
/// @example - css
|
||
|
/// h1::before:hover {}
|
||
|
///
|
||
|
/// @link https://drafts.csswg.org/selectors/#typedef-compound-selector
|
||
|
///
|
||
|
/// @param {List} $simple-selectors - A List of simple selectors.
|
||
|
/// @return {List} A List of sorted simple selectors.
|
||
|
@function _sort-simple-selectors($simple-selectors) {
|
||
|
@if list.length($simple-selectors) <= 1 {
|
||
|
@return $simple-selectors;
|
||
|
}
|
||
|
|
||
|
$type-selectors: ();
|
||
|
$pseudo-element-selectors: ();
|
||
|
$subclass-selectors: ();
|
||
|
|
||
|
@each $simple-selector in $simple-selectors {
|
||
|
@if _is-type-selector($simple-selector) {
|
||
|
$type-selectors: list.append($type-selectors, $simple-selector);
|
||
|
} @else if _is-pseudo-element-selector($simple-selector) {
|
||
|
$pseudo-element-selectors: list.append(
|
||
|
$pseudo-element-selectors,
|
||
|
$simple-selector
|
||
|
);
|
||
|
} @else {
|
||
|
$subclass-selectors: list.append($subclass-selectors, $simple-selector);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@return list.join(
|
||
|
$type-selectors,
|
||
|
list.join($subclass-selectors, $pseudo-element-selectors)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/// Checks if a simple selector is a `<type-selector>`.
|
||
|
///
|
||
|
/// @link https://drafts.csswg.org/selectors/#typedef-type-selector
|
||
|
///
|
||
|
/// @param {String} $simple-selector - The simple selector to check.
|
||
|
/// @return {Bool} True if the selector is a type selector.
|
||
|
@function _is-type-selector($simple-selector) {
|
||
|
@return not _is-subclass-selector($simple-selector);
|
||
|
}
|
||
|
|
||
|
/// Checks if a simple selector is a `<subclass-selector>`.
|
||
|
///
|
||
|
/// @link https://drafts.csswg.org/selectors/#typedef-subclass-selector
|
||
|
///
|
||
|
/// @param {String} $simple-selector - The simple selector to check.
|
||
|
/// @return {Bool} True if the selector is a subclass selector.
|
||
|
@function _is-subclass-selector($simple-selector) {
|
||
|
@return _is-id-selector($simple-selector) or
|
||
|
_is-class-selector($simple-selector) or
|
||
|
_is-attribute-selector($simple-selector) or
|
||
|
_is-pseudo-class-selector($simple-selector);
|
||
|
}
|
||
|
|
||
|
/// Checks if a simple selector is an `<id-selector>`.
|
||
|
///
|
||
|
/// @link https://drafts.csswg.org/selectors/#typedef-id-selector
|
||
|
///
|
||
|
/// @param {String} $simple-selector - The simple selector to check.
|
||
|
/// @return {Bool} True if the selector is an ID selector.
|
||
|
@function _is-id-selector($simple-selector) {
|
||
|
@return string.index($simple-selector, '#') == 1;
|
||
|
}
|
||
|
|
||
|
/// Checks if a simple selector is a `<class-selector>`.
|
||
|
///
|
||
|
/// @link https://drafts.csswg.org/selectors/#typedef-class-selector
|
||
|
///
|
||
|
/// @param {String} $simple-selector - The simple selector to check.
|
||
|
/// @return {Bool} True if the selector is a class selector.
|
||
|
@function _is-class-selector($simple-selector) {
|
||
|
@return string.index($simple-selector, '.') == 1;
|
||
|
}
|
||
|
|
||
|
/// Checks if a simple selector is an `<attribute-selector>`.
|
||
|
///
|
||
|
/// @link https://drafts.csswg.org/selectors/#typedef-attribute-selector
|
||
|
///
|
||
|
/// @param {String} $simple-selector - The simple selector to check.
|
||
|
/// @return {Bool} True if the selector is an attribute selector.
|
||
|
@function _is-attribute-selector($simple-selector) {
|
||
|
@return string.index($simple-selector, '[') == 1;
|
||
|
}
|
||
|
|
||
|
/// Checks if a simple selector is a `<pseudo-class-selector>`.
|
||
|
///
|
||
|
/// @link https://drafts.csswg.org/selectors/#typedef-pseudo-class-selector
|
||
|
///
|
||
|
/// @param {String} $simple-selector - The simple selector to check.
|
||
|
/// @return {Bool} True if the selector is a pseudo class selector.
|
||
|
@function _is-pseudo-class-selector($simple-selector) {
|
||
|
@return string.index($simple-selector, ':') == 1;
|
||
|
}
|
||
|
|
||
|
/// Checks if a simple selector is a `<pseudo-element-selector>`.
|
||
|
///
|
||
|
/// @link https://drafts.csswg.org/selectors/#typedef-pseudo-element-selector
|
||
|
///
|
||
|
/// @param {String} $simple-selector - The simple selector to check.
|
||
|
/// @return {Bool} True if the selector is a pseudo element selector.
|
||
|
@function _is-pseudo-element-selector($simple-selector) {
|
||
|
@return string.index($simple-selector, '::') == 1;
|
||
|
}
|