// // 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 /// `` 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 `` CSS /// type definition. /// /// ``` /// = [ ? * /// [ * ]* ]! /// ``` /// /// @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 ``. /// /// @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 ``. /// /// @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 ``. /// /// @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 ``. /// /// @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 ``. /// /// @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 ``. /// /// @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 ``. /// /// @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; }