1 | //
|
2 | // Copyright 2021 Google Inc.
|
3 | //
|
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
|
5 | // of this software and associated documentation files (the "Software"), to deal
|
6 | // in the Software without restriction, including without limitation the rights
|
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8 | // copies of the Software, and to permit persons to whom the Software is
|
9 | // furnished to do so, subject to the following conditions:
|
10 | //
|
11 | // The above copyright notice and this permission notice shall be included in
|
12 | // all copies or substantial portions of the Software.
|
13 | //
|
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20 | // THE SOFTWARE.
|
21 | //
|
22 |
|
23 | @use 'sass:list';
|
24 | @use 'sass:math';
|
25 | @use 'sass:selector';
|
26 | @use 'sass:string';
|
27 |
|
28 | // A collection of extensions to the sass:selector module
|
29 | // https://sass-lang.com/documentation/modules/selector
|
30 |
|
31 | /// Recursively negates and flattens a compound selector.
|
32 | ///
|
33 | /// This is useful for IE11 support when appending two selectors when the second
|
34 | /// selector may have another nested `:not()`, since IE11 does not supported
|
35 | /// complex selector lists within `:not()`.
|
36 | ///
|
37 | /// @example - scss
|
38 | /// $selector1: '.foo';
|
39 | /// $selector2: '.bar:not(.baz)';
|
40 | ///
|
41 | /// #{selector.append($selector1, ':not(#{$selector2})'} {
|
42 | /// /* Modern browsers */
|
43 | /// }
|
44 | ///
|
45 | /// #{selector.append($selector1, negate($selector2)} {
|
46 | /// /* IE11 support */
|
47 | /// }
|
48 | ///
|
49 | /// @example - css
|
50 | /// .foo:not(.bar:not(.baz)) {
|
51 | /// /* Modern browsers */
|
52 | /// }
|
53 | ///
|
54 | /// .foo:not(.bar), .foo.baz {
|
55 | /// /* IE11 support */
|
56 | /// }
|
57 | ///
|
58 | /// @param {String} $compound-selector - A compound selector to negate.
|
59 | /// @return {List} The negated selector in selector value format.
|
60 | @function negate($compound-selector) {
|
61 | $result: null;
|
62 | @each $simple-selector in selector.simple-selectors($compound-selector) {
|
63 | $to-append: null;
|
64 | @if string.index($simple-selector, ':not(') == 1 {
|
65 | $inside-not: string.slice($simple-selector, 6, -2);
|
66 | $inside-not-simple-selectors: selector.simple-selectors($inside-not);
|
67 | $inside-not-result: null;
|
68 |
|
69 | @each $inside-not-simple-selector
|
70 | in selector.simple-selectors($inside-not)
|
71 | {
|
72 | @if $inside-not-result == null {
|
73 | // Skip the first simple selector, which has already been negated by
|
74 | // removing :not() when parsing $inside-not.
|
75 | $inside-not-result: selector.parse($inside-not-simple-selector);
|
76 | } @else {
|
77 | // Flatten nested :not()s ".foo:not(.bar:not(.baz))" (IE11 support)
|
78 | @if string.index($inside-not-simple-selector, ':not(') == 1 {
|
79 | $inside-not-result: list.join(
|
80 | $inside-not-result,
|
81 | _negate($inside-not-simple-selector)
|
82 | );
|
83 | } @else {
|
84 | $inside-not-result: selector.append(
|
85 | $inside-not-result,
|
86 | $inside-not-simple-selector
|
87 | );
|
88 | }
|
89 | }
|
90 | }
|
91 |
|
92 | $result: if(
|
93 | $result,
|
94 | list.join($result, $inside-not-result),
|
95 | $inside-not-result
|
96 | );
|
97 | } @else {
|
98 | $to-append: string.unquote(':not(#{$simple-selector})');
|
99 | $result: if($result, selector.append($result, $to-append), $to-append);
|
100 | }
|
101 | }
|
102 |
|
103 | @return $result;
|
104 | }
|
105 |
|
106 | /// Identical to `selector.append()`, but adheres strictly to CSS compound
|
107 | /// selector order.
|
108 | ///
|
109 | /// @example - scss
|
110 | /// .foo::before {
|
111 | /// &[dir=rtl] { /* Invalid */ }
|
112 | /// }
|
113 | ///
|
114 | /// .foo::before {
|
115 | /// @include append-strict(&, '[dir=rtl]') { /* Valid */ }
|
116 | /// }
|
117 | ///
|
118 | /// @example - css
|
119 | /// .foo::before[dir=rtl] {
|
120 | /// /* Invalid */
|
121 | /// }
|
122 | ///
|
123 | /// .foo[dir=rtl]::before {
|
124 | /// /* Valid */
|
125 | /// }
|
126 | ///
|
127 | /// This is useful for mixins where the parent selector is unknown and the
|
128 | /// appended selector's position is critical to maintain valid CSS.
|
129 | ///
|
130 | /// @param {List} $selectors - One or more selectors to append.
|
131 | @mixin append-strict($selectors...) {
|
132 | @at-root {
|
133 | #{append-strict($selectors...)} {
|
134 | @content;
|
135 | }
|
136 | }
|
137 | }
|
138 |
|
139 | /// Function version of `append-strict()`. Use this instead of the mixin along
|
140 | /// with `@at-root` when combining the result of `append-strict()` with other
|
141 | /// selectors.
|
142 | ///
|
143 | /// @example - scss
|
144 | /// .foo::before {
|
145 | /// // Cannot add a list of other selectors with an @include mixin.
|
146 | /// // @include append-strict(&, ':hover'), & {}
|
147 | ///
|
148 | /// @at-root {
|
149 | /// // Use @at-root and interpolation to add additional selectors
|
150 | /// #{append-strict(&, ':hover')},
|
151 | /// & {
|
152 | /// color: inherit;
|
153 | /// }
|
154 | /// }
|
155 | /// }
|
156 | ///
|
157 | /// @example - css
|
158 | /// .foo:hover::before,
|
159 | /// .foo::before {
|
160 | /// color: inherit;
|
161 | /// }
|
162 | ///
|
163 | /// @see {mixin} append-strict
|
164 | ///
|
165 | /// @param {List} $selectors - One or more selectors to append.
|
166 | /// @return {List} The appended selectors in selector value format.
|
167 | @function append-strict($selectors...) {
|
168 | $selector-lists: ();
|
169 | @each $selector in $selectors {
|
170 | $selector-lists: list.append($selector-lists, selector.parse($selector));
|
171 | }
|
172 |
|
173 | @return _append-strict($selector-lists);
|
174 | }
|
175 |
|
176 | /// Iterates through multiple selector Lists and strictly appends every
|
177 | /// combination of each selector lists' complex selectors.
|
178 | ///
|
179 | /// @see {mixin} _append-strict-complex-selectors
|
180 | ///
|
181 | /// @param {List} $selector-lists - A List of selector lists to append.
|
182 | /// @return {List} A single selector List resulting from appending all the
|
183 | /// provided selector lists.
|
184 | @function _append-strict($selector-lists) {
|
185 | $length: list.length($selector-lists);
|
186 | // Track the current index of each complex selector (within each selector
|
187 | // list) that we are creating a combination of.
|
188 | //
|
189 | // Selectors: ((1), (2a, 2b), (3))
|
190 | // Combinations: (1, 2a, 3), (1, 2b, 3)
|
191 | //
|
192 | // Initialize it to the first complex selector index for each selector list.
|
193 | $current-indices: ();
|
194 | @for $i from 1 through $length {
|
195 | $current-indices: list.append($current-indices, 1);
|
196 | }
|
197 |
|
198 | // The final result: a single selector list resulting from appending the
|
199 | // provided selector lists.
|
200 | $selector-list-result: ();
|
201 |
|
202 | $has-more-combinations: true;
|
203 | @while $has-more-combinations {
|
204 | // A combination of complex selectors to add to the selector list result.
|
205 | $complex-selector-combination: ();
|
206 | @for $i from 1 through $length {
|
207 | $selector-list: list.nth($selector-lists, $i);
|
208 | $current-index: list.nth($current-indices, $i);
|
209 | $complex-selector: list.nth($selector-list, $current-index);
|
210 | $complex-selector-combination: _append-strict-complex-selectors(
|
211 | $complex-selector-combination,
|
212 | $complex-selector
|
213 | );
|
214 | }
|
215 |
|
216 | $selector-list-result: list.append(
|
217 | $selector-list-result,
|
218 | $complex-selector-combination,
|
219 | $separator: comma
|
220 | );
|
221 |
|
222 | // Increase the index of the last selector list's complex selector to its
|
223 | // next one. If it reaches the length of the array, reset to 1 and bump the
|
224 | // next selector list index.
|
225 | //
|
226 | // Given selector lists: ((1), (2a, 2b), (3))
|
227 | // At indices: ((1), (1), (1))
|
228 | // Try bumping: ((1), (1), (2*)) *This index is >length of 1 for the list
|
229 | // Bump the next: ((1), (2), (1))
|
230 | $bump-next-list-index: true;
|
231 | @for $neg-i from $length * -1 through -1 {
|
232 | @if $bump-next-list-index {
|
233 | $i: math.abs($neg-i);
|
234 | $selector-list: list.nth($selector-lists, $i);
|
235 | $current-index: list.nth($current-indices, $i);
|
236 | $next-index: $current-index + 1;
|
237 | @if $next-index > list.length($selector-list) {
|
238 | // Reset to start for the list and bump the next list (technically
|
239 | // previous since we're iterating backwards).
|
240 | $next-index: 1;
|
241 | $bump-next-list-index: true;
|
242 | } @else {
|
243 | // If we bumped to the next index for this selector list, we can
|
244 | // "break" the loop and continue to form the next combination.
|
245 | $bump-next-list-index: false;
|
246 | }
|
247 |
|
248 | // Save the new current index for this selector list.
|
249 | $current-indices: list.set-nth($current-indices, $i, $next-index);
|
250 | }
|
251 | }
|
252 |
|
253 | // When the first selector list reaches its length, it will ask to bump the
|
254 | // next selector list index. There are no more selector lists, which means
|
255 | // there are no more combinations and the loop may end.
|
256 | @if $bump-next-list-index {
|
257 | $has-more-combinations: false;
|
258 | }
|
259 | }
|
260 |
|
261 | @return $selector-list-result;
|
262 | }
|
263 |
|
264 | /// Appends two complex selectors together, strictly adhering to the CSS
|
265 | /// `<compound-selector>` type definition to avoid forming invalid resulting
|
266 | /// compound selectors.
|
267 | ///
|
268 | /// @param {List} $complex-selector-a - The first List of space-separated
|
269 | /// compound selectors.
|
270 | /// @param {List} $complex-selector-a - The second List of space-separated
|
271 | /// compound selectors.
|
272 | /// @return {List} A resulting appended complex selector.
|
273 | @function _append-strict-complex-selectors(
|
274 | $complex-selector-a,
|
275 | $complex-selector-b
|
276 | ) {
|
277 | // If one of the lists is empty, return the other list.
|
278 | @if list.length($complex-selector-a) < 1 {
|
279 | @return $complex-selector-b;
|
280 | }
|
281 |
|
282 | @if list.length($complex-selector-b) < 1 {
|
283 | @return $complex-selector-a;
|
284 | }
|
285 |
|
286 | // The "joining" of A and B happens at the last compound selector of A and the
|
287 | // first compound selector of B.
|
288 | //
|
289 | // Example:
|
290 | // ".foo .bar" and ".baz :hover" will append as
|
291 | // ".foo .bar.baz :hover"
|
292 | $last-compound-selector-a: list.nth(
|
293 | $complex-selector-a,
|
294 | list.length($complex-selector-a)
|
295 | );
|
296 | $first-compound-selector-b: list.nth($complex-selector-b, 1);
|
297 |
|
298 | // The compound selector CSS joining (".bar" and ".baz") and their sorting
|
299 | // only needs to happen on the last/first of A/B.
|
300 | $simple-selectors-a: selector.simple-selectors($last-compound-selector-a);
|
301 | $simple-selectors-b: selector.simple-selectors($first-compound-selector-b);
|
302 | $sorted-simple-selectors: _sort-simple-selectors(
|
303 | list.join($simple-selectors-a, $simple-selectors-b)
|
304 | );
|
305 |
|
306 | // The result can start to form by setting the last compound selector of A to
|
307 | // the result of the sorted and joined ".bar.baz"...
|
308 | $result: list.set-nth(
|
309 | $complex-selector-a,
|
310 | list.length($complex-selector-a),
|
311 | _join-simple-selectors($sorted-simple-selectors)
|
312 | );
|
313 |
|
314 | // ...then adding the remaining compound selectors (excluding the first one,
|
315 | // which was already appended) from B.
|
316 | @if list.length($complex-selector-b) > 1 {
|
317 | @for $i from 2 through list.length($complex-selector-b) {
|
318 | $result: list.append(list.nth($complex-selector-b, $i));
|
319 | }
|
320 | }
|
321 |
|
322 | @return $result;
|
323 | }
|
324 |
|
325 | /// Combines a List of simple selectors together to form a compound selector.
|
326 | /// If there are any pseudo class function selectors that should nest their
|
327 | /// selectors within their parentheses, this function will do so.
|
328 | ///
|
329 | /// @param {List} $simple-selectors - A List of simple selectors to join.
|
330 | /// @return {String} A compound selector.
|
331 | @function _join-simple-selectors($simple-selectors) {
|
332 | $compound-selector: '';
|
333 | $parens-index: _get-nestable-parens-index($simple-selectors);
|
334 | @if $parens-index {
|
335 | // Contains a selector, such as :host() that other selectors must be placed
|
336 | // within the parentheses of. This selector should be moved to the front of
|
337 | // the compound selector.
|
338 | $compound-selector: list.nth($simple-selectors, $parens-index);
|
339 | @if string.index($compound-selector, '(') != null {
|
340 | // Already has parens. Remove the final closing parens so that additional
|
341 | // selectors are placed within the parentheses.
|
342 | $compound-selector: string.slice(
|
343 | $compound-selector,
|
344 | 1,
|
345 | string.length($compound-selector) - 1
|
346 | );
|
347 | } @else {
|
348 | // Otherwise, add an opening parens.
|
349 | $compound-selector: #{$compound-selector}#{string.unquote('(')};
|
350 | }
|
351 | }
|
352 |
|
353 | @for $i from 1 through list.length($simple-selectors) {
|
354 | @if $i != $parens-index {
|
355 | // Skip the parens selector that was moved to the front, if any
|
356 | $simple-selector: list.nth($simple-selectors, $i);
|
357 | $compound-selector: #{$compound-selector}#{$simple-selector};
|
358 | }
|
359 | }
|
360 |
|
361 | @if $parens-index {
|
362 | // Add the closing parens
|
363 | $compound-selector: #{$compound-selector}#{string.unquote(')')};
|
364 | }
|
365 |
|
366 | @return $compound-selector;
|
367 | }
|
368 |
|
369 | /// Searches a List of simple selectors for any pseudo class functions that can
|
370 | /// and should be nested with other selectors. If one is found, the index is
|
371 | /// returned.
|
372 | ///
|
373 | /// @see {mixin} _can-and-should-nest-pseudo-class
|
374 | ///
|
375 | /// @param {List} $simple-selectors - A List of simple selectors to search.
|
376 | /// @return {Number} The index of the selector with parens to nest, or null if
|
377 | /// there is none.
|
378 | @function _get-nestable-parens-index($simple-selectors) {
|
379 | @for $i from 1 through list.length($simple-selectors) {
|
380 | $simple-selector: list.nth($simple-selectors, $i);
|
381 | @if _can-and-should-nest-pseudo-class($simple-selector) {
|
382 | @return $i;
|
383 | }
|
384 | }
|
385 |
|
386 | @return null;
|
387 | }
|
388 |
|
389 | /// Checks two things:
|
390 | ///
|
391 | /// 1. If a simple selector is a pseudo class function that accepts selectors
|
392 | /// as its arguments.
|
393 | /// 2. If this selector is commonly used for nesting within.
|
394 | ///
|
395 | /// For example, `:host` satisfies both #1 and #2, but the `:not()` pseudo class
|
396 | /// is not commonly used in abstract nesting within Sass.
|
397 | ///
|
398 | /// @example - scss
|
399 | /// :host(:hover) {
|
400 | /// :enabled {
|
401 | /// // commonly expect :host(:hover:enabled),
|
402 | /// // since :host(:hover):enabled is invalid CSS
|
403 | /// }
|
404 | /// }
|
405 | ///
|
406 | /// :not(:hover) {
|
407 | /// :enabled {
|
408 | /// // commonly expect :not(:hover):enabled
|
409 | /// // do not expect :not(:hover:enabled) as the intention
|
410 | /// }
|
411 | /// }
|
412 | ///
|
413 | /// @param {String} $simple-selector - The simple selector to check.
|
414 | /// @return {Bool} True if the simple selector is a pseudo class function that
|
415 | /// should nest additional selectors within its parentheses.
|
416 | @function _can-and-should-nest-pseudo-class($simple-selector) {
|
417 | @return string.index($simple-selector, ':host') != null or
|
418 | string.index($simple-selector, '::slotted') != null;
|
419 | }
|
420 |
|
421 | /// Sorts a List of simple selectors according to the `<compound-selector>` CSS
|
422 | /// type definition.
|
423 | ///
|
424 | /// ```
|
425 | /// <compound-selector> = [ <type-selector>? <subclass-selector>*
|
426 | /// [ <pseudo-element-selector> <pseudo-class-selector>* ]* ]!
|
427 | /// ```
|
428 | ///
|
429 | /// @example - scss
|
430 | /// $unsorted: (':hover', '::before', 'h1');
|
431 | /// #{selector.append(_sort-simple-selectors($unsorted...))} {}
|
432 | ///
|
433 | /// @example - css
|
434 | /// h1::before:hover {}
|
435 | ///
|
436 | /// @link https://drafts.csswg.org/selectors/#typedef-compound-selector
|
437 | ///
|
438 | /// @param {List} $simple-selectors - A List of simple selectors.
|
439 | /// @return {List} A List of sorted simple selectors.
|
440 | @function _sort-simple-selectors($simple-selectors) {
|
441 | @if list.length($simple-selectors) <= 1 {
|
442 | @return $simple-selectors;
|
443 | }
|
444 |
|
445 | $type-selectors: ();
|
446 | $pseudo-element-selectors: ();
|
447 | $subclass-selectors: ();
|
448 |
|
449 | @each $simple-selector in $simple-selectors {
|
450 | @if _is-type-selector($simple-selector) {
|
451 | $type-selectors: list.append($type-selectors, $simple-selector);
|
452 | } @else if _is-pseudo-element-selector($simple-selector) {
|
453 | $pseudo-element-selectors: list.append(
|
454 | $pseudo-element-selectors,
|
455 | $simple-selector
|
456 | );
|
457 | } @else {
|
458 | $subclass-selectors: list.append($subclass-selectors, $simple-selector);
|
459 | }
|
460 | }
|
461 |
|
462 | @return list.join(
|
463 | $type-selectors,
|
464 | list.join($subclass-selectors, $pseudo-element-selectors)
|
465 | );
|
466 | }
|
467 |
|
468 | /// Checks if a simple selector is a `<type-selector>`.
|
469 | ///
|
470 | /// @link https://drafts.csswg.org/selectors/#typedef-type-selector
|
471 | ///
|
472 | /// @param {String} $simple-selector - The simple selector to check.
|
473 | /// @return {Bool} True if the selector is a type selector.
|
474 | @function _is-type-selector($simple-selector) {
|
475 | @return not _is-subclass-selector($simple-selector);
|
476 | }
|
477 |
|
478 | /// Checks if a simple selector is a `<subclass-selector>`.
|
479 | ///
|
480 | /// @link https://drafts.csswg.org/selectors/#typedef-subclass-selector
|
481 | ///
|
482 | /// @param {String} $simple-selector - The simple selector to check.
|
483 | /// @return {Bool} True if the selector is a subclass selector.
|
484 | @function _is-subclass-selector($simple-selector) {
|
485 | @return _is-id-selector($simple-selector) or
|
486 | _is-class-selector($simple-selector) or
|
487 | _is-attribute-selector($simple-selector) or
|
488 | _is-pseudo-class-selector($simple-selector);
|
489 | }
|
490 |
|
491 | /// Checks if a simple selector is an `<id-selector>`.
|
492 | ///
|
493 | /// @link https://drafts.csswg.org/selectors/#typedef-id-selector
|
494 | ///
|
495 | /// @param {String} $simple-selector - The simple selector to check.
|
496 | /// @return {Bool} True if the selector is an ID selector.
|
497 | @function _is-id-selector($simple-selector) {
|
498 | @return string.index($simple-selector, '#') == 1;
|
499 | }
|
500 |
|
501 | /// Checks if a simple selector is a `<class-selector>`.
|
502 | ///
|
503 | /// @link https://drafts.csswg.org/selectors/#typedef-class-selector
|
504 | ///
|
505 | /// @param {String} $simple-selector - The simple selector to check.
|
506 | /// @return {Bool} True if the selector is a class selector.
|
507 | @function _is-class-selector($simple-selector) {
|
508 | @return string.index($simple-selector, '.') == 1;
|
509 | }
|
510 |
|
511 | /// Checks if a simple selector is an `<attribute-selector>`.
|
512 | ///
|
513 | /// @link https://drafts.csswg.org/selectors/#typedef-attribute-selector
|
514 | ///
|
515 | /// @param {String} $simple-selector - The simple selector to check.
|
516 | /// @return {Bool} True if the selector is an attribute selector.
|
517 | @function _is-attribute-selector($simple-selector) {
|
518 | @return string.index($simple-selector, '[') == 1;
|
519 | }
|
520 |
|
521 | /// Checks if a simple selector is a `<pseudo-class-selector>`.
|
522 | ///
|
523 | /// @link https://drafts.csswg.org/selectors/#typedef-pseudo-class-selector
|
524 | ///
|
525 | /// @param {String} $simple-selector - The simple selector to check.
|
526 | /// @return {Bool} True if the selector is a pseudo class selector.
|
527 | @function _is-pseudo-class-selector($simple-selector) {
|
528 | @return string.index($simple-selector, ':') == 1;
|
529 | }
|
530 |
|
531 | /// Checks if a simple selector is a `<pseudo-element-selector>`.
|
532 | ///
|
533 | /// @link https://drafts.csswg.org/selectors/#typedef-pseudo-element-selector
|
534 | ///
|
535 | /// @param {String} $simple-selector - The simple selector to check.
|
536 | /// @return {Bool} True if the selector is a pseudo element selector.
|
537 | @function _is-pseudo-element-selector($simple-selector) {
|
538 | @return string.index($simple-selector, '::') == 1;
|
539 | }
|