UNPKG

18.7 kBSCSSView Raw
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}