1 | // sass.hpp must go before all system headers to get the |
2 | // __EXTENSIONS__ fix on Solaris. |
3 | #include "sass.hpp" |
4 | #include "ast.hpp" |
5 | |
6 | #include "extender.hpp" |
7 | #include "permutate.hpp" |
8 | #include "dart_helpers.hpp" |
9 | |
10 | namespace Sass { |
11 | |
12 | // ########################################################################## |
13 | // Constructor without default [mode]. |
14 | // [traces] are needed to throw errors. |
15 | // ########################################################################## |
16 | Extender::Extender(Backtraces& traces) : |
17 | mode(NORMAL), |
18 | traces(traces), |
19 | selectors(), |
20 | extensions(), |
21 | extensionsByExtender(), |
22 | mediaContexts(), |
23 | sourceSpecificity(), |
24 | originals() |
25 | {} |
26 | |
27 | // ########################################################################## |
28 | // Constructor with specific [mode]. |
29 | // [traces] are needed to throw errors. |
30 | // ########################################################################## |
31 | Extender::Extender(ExtendMode mode, Backtraces& traces) : |
32 | mode(mode), |
33 | traces(traces), |
34 | selectors(), |
35 | extensions(), |
36 | extensionsByExtender(), |
37 | mediaContexts(), |
38 | sourceSpecificity(), |
39 | originals() |
40 | {} |
41 | |
42 | // ########################################################################## |
43 | // Extends [selector] with [source] extender and [targets] extendees. |
44 | // This works as though `source {@extend target}` were written in the |
45 | // stylesheet, with the exception that [target] can contain compound |
46 | // selectors which must be extended as a unit. |
47 | // ########################################################################## |
48 | SelectorListObj Extender::extend( |
49 | SelectorListObj& selector, |
50 | const SelectorListObj& source, |
51 | const SelectorListObj& targets, |
52 | Backtraces& traces) |
53 | { |
54 | return extendOrReplace(selector, source, target: targets, mode: ExtendMode::TARGETS, traces); |
55 | } |
56 | // EO Extender::extend |
57 | |
58 | // ########################################################################## |
59 | // Returns a copy of [selector] with [targets] replaced by [source]. |
60 | // ########################################################################## |
61 | SelectorListObj Extender::replace( |
62 | SelectorListObj& selector, |
63 | const SelectorListObj& source, |
64 | const SelectorListObj& targets, |
65 | Backtraces& traces) |
66 | { |
67 | return extendOrReplace(selector, source, target: targets, mode: ExtendMode::REPLACE, traces); |
68 | } |
69 | // EO Extender::replace |
70 | |
71 | // ########################################################################## |
72 | // A helper function for [extend] and [replace]. |
73 | // ########################################################################## |
74 | SelectorListObj Extender::extendOrReplace( |
75 | SelectorListObj& selector, |
76 | const SelectorListObj& source, |
77 | const SelectorListObj& targets, |
78 | const ExtendMode mode, |
79 | Backtraces& traces) |
80 | { |
81 | ExtSelExtMapEntry extenders; |
82 | |
83 | for (auto complex : source->elements()) { |
84 | // Extension.oneOff(complex as ComplexSelector) |
85 | extenders.insert(key: complex, val: Extension(complex)); |
86 | } |
87 | |
88 | for (auto complex : targets->elements()) { |
89 | |
90 | // This seems superfluous, check is done before!? |
91 | // if (complex->length() != 1) { |
92 | // error("complex selectors may not be extended.", complex->pstate(), traces); |
93 | // } |
94 | |
95 | if (const CompoundSelector* compound = complex->first()->getCompound()) { |
96 | |
97 | ExtSelExtMap extensions; |
98 | |
99 | for (const SimpleSelectorObj& simple : compound->elements()) { |
100 | extensions.insert(x: std::make_pair(x: simple, y&: extenders)); |
101 | } |
102 | |
103 | Extender extender(mode, traces); |
104 | |
105 | if (!selector->is_invisible()) { |
106 | for (auto sel : selector->elements()) { |
107 | extender.originals.insert(x: sel); |
108 | } |
109 | } |
110 | |
111 | selector = extender.extendList(list: selector, extensions, mediaContext: {}); |
112 | |
113 | } |
114 | |
115 | } |
116 | |
117 | return selector; |
118 | |
119 | } |
120 | // EO extendOrReplace |
121 | |
122 | // ########################################################################## |
123 | // The set of all simple selectors in style rules handled |
124 | // by this extender. This includes simple selectors that |
125 | // were added because of downstream extensions. |
126 | // ########################################################################## |
127 | ExtSmplSelSet Extender::getSimpleSelectors() const |
128 | { |
129 | ExtSmplSelSet set; |
130 | for (auto& entry : selectors) { |
131 | set.insert(x: entry.first); |
132 | } |
133 | return set; |
134 | } |
135 | // EO getSimpleSelectors |
136 | |
137 | // ########################################################################## |
138 | // Check for extends that have not been satisfied. |
139 | // Returns true if any non-optional extension did not |
140 | // extend any selector. Updates the passed reference |
141 | // to point to that Extension for further analysis. |
142 | // ########################################################################## |
143 | bool Extender::checkForUnsatisfiedExtends(Extension& unsatisfied) const |
144 | { |
145 | if (selectors.empty()) return false; |
146 | ExtSmplSelSet originals = getSimpleSelectors(); |
147 | for (auto target : extensions) { |
148 | SimpleSelector* key = target.first; |
149 | ExtSelExtMapEntry& val = target.second; |
150 | if (val.empty()) continue; |
151 | if (originals.find(x: key) == originals.end()) { |
152 | const Extension& extension = val.front().second; |
153 | if (extension.isOptional) continue; |
154 | unsatisfied = extension; |
155 | return true; |
156 | } |
157 | } |
158 | return false; |
159 | } |
160 | // EO checkUnsatisfiedExtends |
161 | |
162 | // ########################################################################## |
163 | // Adds [selector] to this extender, with [selectorSpan] as the span covering |
164 | // the selector and [ruleSpan] as the span covering the entire style rule. |
165 | // Extends [selector] using any registered extensions, then returns an empty |
166 | // [ModifiableCssStyleRule] with the resulting selector. If any more relevant |
167 | // extensions are added, the returned rule is automatically updated. |
168 | // The [mediaContext] is the media query context in which the selector was |
169 | // defined, or `null` if it was defined at the top level of the document. |
170 | // ########################################################################## |
171 | void Extender::addSelector( |
172 | const SelectorListObj& selector, |
173 | const CssMediaRuleObj& mediaContext) |
174 | { |
175 | |
176 | // Note: dart-sass makes a copy here AFAICT |
177 | // Note: probably why we have originalStack |
178 | // SelectorListObj original = selector; |
179 | |
180 | if (!selector->isInvisible()) { |
181 | for (auto complex : selector->elements()) { |
182 | originals.insert(x: complex); |
183 | } |
184 | } |
185 | |
186 | if (!extensions.empty()) { |
187 | |
188 | SelectorListObj res = extendList(list: selector, extensions, mediaContext); |
189 | |
190 | selector->elements(e: res->elements()); |
191 | |
192 | } |
193 | |
194 | if (!mediaContext.isNull()) { |
195 | mediaContexts.insert(key: selector, val: mediaContext); |
196 | } |
197 | |
198 | registerSelector(list: selector, rule: selector); |
199 | |
200 | } |
201 | // EO addSelector |
202 | |
203 | // ########################################################################## |
204 | // Registers the [SimpleSelector]s in [list] |
205 | // to point to [rule] in [selectors]. |
206 | // ########################################################################## |
207 | void Extender::registerSelector( |
208 | const SelectorListObj& list, |
209 | const SelectorListObj& rule) |
210 | { |
211 | if (list.isNull() || list->empty()) return; |
212 | for (auto complex : list->elements()) { |
213 | for (auto component : complex->elements()) { |
214 | if (auto compound = component->getCompound()) { |
215 | for (SimpleSelector* simple : compound->elements()) { |
216 | selectors[simple].insert(x: rule); |
217 | if (auto pseudo = simple->getPseudoSelector()) { |
218 | if (pseudo->selector()) { |
219 | auto sel = pseudo->selector(); |
220 | registerSelector(list: sel, rule); |
221 | } |
222 | } |
223 | } |
224 | } |
225 | } |
226 | } |
227 | } |
228 | // EO registerSelector |
229 | |
230 | // ########################################################################## |
231 | // Returns an extension that combines [left] and [right]. Throws |
232 | // a [SassException] if [left] and [right] have incompatible |
233 | // media contexts. Throws an [ArgumentError] if [left] |
234 | // and [right] don't have the same extender and target. |
235 | // ########################################################################## |
236 | Extension Extender::mergeExtension( |
237 | const Extension& lhs, |
238 | const Extension& rhs) |
239 | { |
240 | // If one extension is optional and doesn't add a |
241 | // special media context, it doesn't need to be merged. |
242 | if (rhs.isOptional && rhs.mediaContext.isNull()) return lhs; |
243 | if (lhs.isOptional && lhs.mediaContext.isNull()) return rhs; |
244 | |
245 | Extension rv(lhs); |
246 | // ToDo: is this right? |
247 | rv.isOptional = true; |
248 | rv.isOriginal = false; |
249 | return rv; |
250 | } |
251 | // EO mergeExtension |
252 | |
253 | // ########################################################################## |
254 | // Helper function to copy extension between maps |
255 | // ########################################################################## |
256 | // Seems only relevant for sass 4.0 modules |
257 | // ########################################################################## |
258 | /* void mapCopyExts( |
259 | ExtSelExtMap& dest, |
260 | const ExtSelExtMap& source) |
261 | { |
262 | for (auto it : source) { |
263 | SimpleSelectorObj key = it.first; |
264 | ExtSelExtMapEntry& inner = it.second; |
265 | ExtSelExtMap::iterator dmap = dest.find(key); |
266 | if (dmap == dest.end()) { |
267 | dest.insert(std::make_pair(key, inner)); |
268 | } |
269 | else { |
270 | ExtSelExtMapEntry& imap = dmap->second; |
271 | // ToDo: optimize ordered_map API! |
272 | // ToDo: we iterate and fetch the value |
273 | for (ComplexSelectorObj& it2 : inner) { |
274 | imap.insert(it2, inner.get(it2)); |
275 | } |
276 | } |
277 | } |
278 | } */ |
279 | // EO mapCopyExts |
280 | |
281 | // ########################################################################## |
282 | // Adds an extension to this extender. The [extender] is the selector for the |
283 | // style rule in which the extension is defined, and [target] is the selector |
284 | // passed to `@extend`. The [extend] provides the extend span and indicates |
285 | // whether the extension is optional. The [mediaContext] defines the media query |
286 | // context in which the extension is defined. It can only extend selectors |
287 | // within the same context. A `null` context indicates no media queries. |
288 | // ########################################################################## |
289 | // ToDo: rename extender to parent, since it is not involved in extending stuff |
290 | // ToDo: check why dart sass passes the ExtendRule around (is this the main selector?) |
291 | // ########################################################################## |
292 | // Note: this function could need some logic cleanup |
293 | // ########################################################################## |
294 | void Extender::addExtension( |
295 | const SelectorListObj& extender, |
296 | const SimpleSelectorObj& target, |
297 | const CssMediaRuleObj& mediaQueryContext, |
298 | bool is_optional) |
299 | { |
300 | |
301 | auto rules = selectors.find(x: target); |
302 | bool hasRule = rules != selectors.end(); |
303 | |
304 | ExtSelExtMapEntry newExtensions; |
305 | |
306 | // ToDo: we check this here first and fetch the same? item again after the loop!? |
307 | bool hasExistingExtensions = extensionsByExtender.find(x: target) != extensionsByExtender.end(); |
308 | |
309 | ExtSelExtMapEntry& sources = extensions[target]; |
310 | |
311 | for (auto& complex : extender->elements()) { |
312 | |
313 | Extension state(complex); |
314 | // ToDo: fine-tune public API |
315 | state.target = target; |
316 | state.isOptional = is_optional; |
317 | state.mediaContext = mediaQueryContext; |
318 | |
319 | if (sources.hasKey(key: complex)) { |
320 | // If there's already an extend from [extender] to [target], |
321 | // we don't need to re-run the extension. We may need to |
322 | // mark the extension as mandatory, though. |
323 | // sources.insert(complex, mergeExtension(existingState->second, state); |
324 | // ToDo: implement behavior once use case is found!? |
325 | continue; |
326 | } |
327 | |
328 | sources.insert(key: complex, val: state); |
329 | |
330 | for (auto& component : complex->elements()) { |
331 | if (auto compound = component->getCompound()) { |
332 | for (auto& simple : compound->elements()) { |
333 | extensionsByExtender[simple].push_back(x: state); |
334 | if (sourceSpecificity.find(x: simple) == sourceSpecificity.end()) { |
335 | // Only source specificity for the original selector is relevant. |
336 | // Selectors generated by `@extend` don't get new specificity. |
337 | sourceSpecificity[simple] = complex->maxSpecificity(); |
338 | } |
339 | } |
340 | } |
341 | } |
342 | |
343 | if (hasRule || hasExistingExtensions) { |
344 | newExtensions.insert(key: complex, val: state); |
345 | } |
346 | |
347 | } |
348 | // EO foreach complex |
349 | |
350 | if (newExtensions.empty()) { |
351 | return; |
352 | } |
353 | |
354 | ExtSelExtMap newExtensionsByTarget; |
355 | newExtensionsByTarget.insert(x: std::make_pair(x: target, y&: newExtensions)); |
356 | // ToDo: do we really need to fetch again (see top off fn) |
357 | auto existingExtensions = extensionsByExtender.find(x: target); |
358 | if (existingExtensions != extensionsByExtender.end()) { |
359 | if (hasExistingExtensions && !existingExtensions->second.empty()) { |
360 | // Seems only relevant for sass 4.0 modules |
361 | // auto additionalExtensions = |
362 | extendExistingExtensions(extensions: existingExtensions->second, newExtensions: newExtensionsByTarget); |
363 | // Seems only relevant for sass 4.0 modules |
364 | /* if (!additionalExtensions.empty()) { |
365 | mapCopyExts(newExtensionsByTarget, additionalExtensions); |
366 | } */ |
367 | } |
368 | } |
369 | |
370 | if (hasRule) { |
371 | extendExistingStyleRules(rules: selectors[target], newExtensions: newExtensionsByTarget); |
372 | } |
373 | |
374 | } |
375 | // EO addExtension |
376 | |
377 | // ########################################################################## |
378 | // Extend [extensions] using [newExtensions]. |
379 | // ########################################################################## |
380 | // Note: dart-sass throws an error in here |
381 | // ########################################################################## |
382 | void Extender::extendExistingStyleRules( |
383 | const ExtListSelSet& rules, |
384 | const ExtSelExtMap& newExtensions) |
385 | { |
386 | // Is a modifyableCssStyleRUle in dart sass |
387 | for (const SelectorListObj& rule : rules) { |
388 | const SelectorListObj& oldValue = SASS_MEMORY_COPY(rule); |
389 | CssMediaRuleObj mediaContext; |
390 | if (mediaContexts.hasKey(key: rule)) mediaContext = mediaContexts.get(key: rule); |
391 | SelectorListObj ext = extendList(list: rule, extensions: newExtensions, mediaContext); |
392 | // If no extends actually happened (for example because unification |
393 | // failed), we don't need to re-register the selector. |
394 | if (ObjEqualityFn(lhs: oldValue, rhs: ext)) continue; |
395 | rule->elements(e: ext->elements()); |
396 | registerSelector(list: rule, rule); |
397 | |
398 | } |
399 | } |
400 | // EO extendExistingStyleRules |
401 | |
402 | // ########################################################################## |
403 | // Extend [extensions] using [newExtensions]. Note that this does duplicate |
404 | // some work done by [_extendExistingStyleRules], but it's necessary to |
405 | // expand each extension's extender separately without reference to the full |
406 | // selector list, so that relevant results don't get trimmed too early. |
407 | // |
408 | // Returns extensions that should be added to [newExtensions] before |
409 | // extending selectors in order to properly handle extension loops such as: |
410 | // |
411 | // .c {x: y; @extend .a} |
412 | // .x.y.a {@extend .b} |
413 | // .z.b {@extend .c} |
414 | // |
415 | // Returns `null` (Note: empty map) if there are no extensions to add. |
416 | // ########################################################################## |
417 | // Note: maybe refactor to return `bool` (and pass reference) |
418 | // Note: dart-sass throws an error in here |
419 | // ########################################################################## |
420 | ExtSelExtMap Extender::extendExistingExtensions( |
421 | // Taking in a reference here makes MSVC debug stuck!? |
422 | const sass::vector<Extension>& oldExtensions, |
423 | const ExtSelExtMap& newExtensions) |
424 | { |
425 | |
426 | ExtSelExtMap additionalExtensions; |
427 | |
428 | // During the loop `oldExtensions` vector might be changed. |
429 | // Callers normally pass this from `extensionsByExtender` and |
430 | // that points back to the `sources` vector from `extensions`. |
431 | for (size_t i = 0, iL = oldExtensions.size(); i < iL; i += 1) { |
432 | const Extension& extension = oldExtensions[i]; |
433 | ExtSelExtMapEntry& sources = extensions[extension.target]; |
434 | sass::vector<ComplexSelectorObj> selectors(extendComplex( |
435 | list: extension.extender, |
436 | extensions: newExtensions, |
437 | mediaQueryContext: extension.mediaContext |
438 | )); |
439 | |
440 | if (selectors.empty()) { |
441 | continue; |
442 | } |
443 | |
444 | // ToDo: "catch" error from extend |
445 | |
446 | bool first = false, containsExtension = |
447 | ObjEqualityFn(lhs: selectors.front(), rhs: extension.extender); |
448 | for (const ComplexSelectorObj& complex : selectors) { |
449 | // If the output contains the original complex |
450 | // selector, there's no need to recreate it. |
451 | if (containsExtension && first) { |
452 | first = false; |
453 | continue; |
454 | } |
455 | |
456 | const Extension withExtender = |
457 | extension.withExtender(newExtender: complex); |
458 | if (sources.hasKey(key: complex)) { |
459 | sources.insert(key: complex, val: mergeExtension( |
460 | lhs: sources.get(key: complex), rhs: withExtender)); |
461 | } |
462 | else { |
463 | sources.insert(key: complex, val: withExtender); |
464 | /* |
465 | // Seems only relevant for sass 4.0 modules |
466 | for (auto& component : complex->elements()) { |
467 | if (auto compound = component->getCompound()) { |
468 | for (auto& simple : compound->elements()) { |
469 | extensionsByExtender[simple].push_back(withExtender); |
470 | } |
471 | } |
472 | } |
473 | if (newExtensions.find(extension.target) != newExtensions.end()) { |
474 | additionalExtensions[extension.target].insert(complex, withExtender); |
475 | } |
476 | */ |
477 | } |
478 | } |
479 | |
480 | // If [selectors] doesn't contain [extension.extender], |
481 | // for example if it was replaced due to :not() expansion, |
482 | // we must get rid of the old version. |
483 | /* |
484 | // Seems only relevant for sass 4.0 modules |
485 | if (!containsExtension) { |
486 | sources.erase(extension.extender); |
487 | } |
488 | */ |
489 | |
490 | } |
491 | |
492 | return additionalExtensions; |
493 | |
494 | } |
495 | // EO extendExistingExtensions |
496 | |
497 | // ########################################################################## |
498 | // Extends [list] using [extensions]. |
499 | // ########################################################################## |
500 | SelectorListObj Extender::extendList( |
501 | const SelectorListObj& list, |
502 | const ExtSelExtMap& extensions, |
503 | const CssMediaRuleObj& mediaQueryContext) |
504 | { |
505 | |
506 | // This could be written more simply using [List.map], but we want to |
507 | // avoid any allocations in the common case where no extends apply. |
508 | sass::vector<ComplexSelectorObj> extended; |
509 | for (size_t i = 0; i < list->length(); i++) { |
510 | const ComplexSelectorObj& complex = list->get(i); |
511 | sass::vector<ComplexSelectorObj> result = |
512 | extendComplex(list: complex, extensions, mediaQueryContext); |
513 | if (result.empty()) { |
514 | if (!extended.empty()) { |
515 | extended.push_back(x: complex); |
516 | } |
517 | } |
518 | else { |
519 | if (extended.empty()) { |
520 | for (size_t n = 0; n < i; n += 1) { |
521 | extended.push_back(x: list->get(i: n)); |
522 | } |
523 | } |
524 | for (auto sel : result) { |
525 | extended.push_back(x: sel); |
526 | } |
527 | } |
528 | } |
529 | |
530 | if (extended.empty()) { |
531 | return list; |
532 | } |
533 | |
534 | SelectorListObj rv = SASS_MEMORY_NEW(SelectorList, list->pstate()); |
535 | rv->concat(v: trim(selectors: extended, set: originals)); |
536 | return rv; |
537 | |
538 | } |
539 | // EO extendList |
540 | |
541 | // ########################################################################## |
542 | // Extends [complex] using [extensions], and |
543 | // returns the contents of a [SelectorList]. |
544 | // ########################################################################## |
545 | sass::vector<ComplexSelectorObj> Extender::extendComplex( |
546 | // Taking in a reference here makes MSVC debug stuck!? |
547 | const ComplexSelectorObj& complex, |
548 | const ExtSelExtMap& extensions, |
549 | const CssMediaRuleObj& mediaQueryContext) |
550 | { |
551 | |
552 | // The complex selectors that each compound selector in [complex.components] |
553 | // can expand to. |
554 | // |
555 | // For example, given |
556 | // |
557 | // .a .b {...} |
558 | // .x .y {@extend .b} |
559 | // |
560 | // this will contain |
561 | // |
562 | // [ |
563 | // [.a], |
564 | // [.b, .x .y] |
565 | // ] |
566 | // |
567 | // This could be written more simply using [List.map], but we want to avoid |
568 | // any allocations in the common case where no extends apply. |
569 | |
570 | sass::vector<ComplexSelectorObj> result; |
571 | sass::vector<sass::vector<ComplexSelectorObj>> extendedNotExpanded; |
572 | bool isOriginal = originals.find(x: complex) != originals.end(); |
573 | for (size_t i = 0; i < complex->length(); i += 1) { |
574 | const SelectorComponentObj& component = complex->get(i); |
575 | if (CompoundSelector* compound = Cast<CompoundSelector>(ptr: component)) { |
576 | sass::vector<ComplexSelectorObj> extended = extendCompound( |
577 | compound, extensions, mediaQueryContext, inOriginal: isOriginal); |
578 | if (extended.empty()) { |
579 | if (!extendedNotExpanded.empty()) { |
580 | extendedNotExpanded.push_back(x: { |
581 | compound->wrapInComplex() |
582 | }); |
583 | } |
584 | } |
585 | else { |
586 | // Note: dart-sass checks for null!? |
587 | if (extendedNotExpanded.empty()) { |
588 | for (size_t n = 0; n < i; n++) { |
589 | extendedNotExpanded.push_back(x: { |
590 | complex->at(i: n)->wrapInComplex() |
591 | }); |
592 | } |
593 | } |
594 | extendedNotExpanded.push_back(x: extended); |
595 | } |
596 | } |
597 | else { |
598 | // Note: dart-sass checks for null!? |
599 | if (!extendedNotExpanded.empty()) { |
600 | extendedNotExpanded.push_back(x: { |
601 | component->wrapInComplex() |
602 | }); |
603 | } |
604 | } |
605 | } |
606 | |
607 | // Note: dart-sass checks for null!? |
608 | if (extendedNotExpanded.empty()) { |
609 | return {}; |
610 | } |
611 | |
612 | bool first = true; |
613 | |
614 | // ToDo: either change weave or paths to work with the same data? |
615 | sass::vector<sass::vector<ComplexSelectorObj>> |
616 | paths = permutate(in: extendedNotExpanded); |
617 | |
618 | for (const sass::vector<ComplexSelectorObj>& path : paths) { |
619 | // Unpack the inner complex selector to component list |
620 | sass::vector<sass::vector<SelectorComponentObj>> _paths; |
621 | for (const ComplexSelectorObj& sel : path) { |
622 | _paths.insert(position: _paths.end(), x: sel->elements()); |
623 | } |
624 | |
625 | sass::vector<sass::vector<SelectorComponentObj>> weaved = weave(complexes: _paths); |
626 | |
627 | for (sass::vector<SelectorComponentObj>& components : weaved) { |
628 | |
629 | ComplexSelectorObj cplx = SASS_MEMORY_NEW(ComplexSelector, "[phony]" ); |
630 | cplx->hasPreLineFeed(hasPreLineFeed__: complex->hasPreLineFeed()); |
631 | for (auto& pp : path) { |
632 | if (pp->hasPreLineFeed()) { |
633 | cplx->hasPreLineFeed(hasPreLineFeed__: true); |
634 | } |
635 | } |
636 | cplx->elements(e: components); |
637 | |
638 | // Make sure that copies of [complex] retain their status |
639 | // as "original" selectors. This includes selectors that |
640 | // are modified because a :not() was extended into. |
641 | if (first && originals.find(x: complex) != originals.end()) { |
642 | originals.insert(x: cplx); |
643 | } |
644 | first = false; |
645 | |
646 | result.push_back(x: cplx); |
647 | |
648 | } |
649 | |
650 | } |
651 | |
652 | return result; |
653 | } |
654 | // EO extendComplex |
655 | |
656 | // ########################################################################## |
657 | // Returns a one-off [Extension] whose |
658 | // extender is composed solely of [simple]. |
659 | // ########################################################################## |
660 | Extension Extender::extensionForSimple( |
661 | const SimpleSelectorObj& simple) const |
662 | { |
663 | Extension extension(simple->wrapInComplex()); |
664 | extension.specificity = maxSourceSpecificity(simple); |
665 | extension.isOriginal = true; |
666 | return extension; |
667 | } |
668 | // Extender::extensionForSimple |
669 | |
670 | // ########################################################################## |
671 | // Returns a one-off [Extension] whose extender is composed |
672 | // solely of a compound selector containing [simples]. |
673 | // ########################################################################## |
674 | Extension Extender::extensionForCompound( |
675 | // Taking in a reference here makes MSVC debug stuck!? |
676 | const sass::vector<SimpleSelectorObj>& simples) const |
677 | { |
678 | CompoundSelectorObj compound = SASS_MEMORY_NEW(CompoundSelector, SourceSpan("[ext]" )); |
679 | compound->concat(v: simples); |
680 | Extension extension(compound->wrapInComplex()); |
681 | // extension.specificity = sourceSpecificity[simple]; |
682 | extension.isOriginal = true; |
683 | return extension; |
684 | } |
685 | // EO extensionForCompound |
686 | |
687 | // ########################################################################## |
688 | // Extends [compound] using [extensions], and returns the |
689 | // contents of a [SelectorList]. The [inOriginal] parameter |
690 | // indicates whether this is in an original complex selector, |
691 | // meaning that [compound] should not be trimmed out. |
692 | // ########################################################################## |
693 | sass::vector<ComplexSelectorObj> Extender::extendCompound( |
694 | const CompoundSelectorObj& compound, |
695 | const ExtSelExtMap& extensions, |
696 | const CssMediaRuleObj& mediaQueryContext, |
697 | bool inOriginal) |
698 | { |
699 | |
700 | // If there's more than one target and they all need to |
701 | // match, we track which targets are actually extended. |
702 | ExtSmplSelSet targetsUsed2; |
703 | |
704 | ExtSmplSelSet* targetsUsed = nullptr; |
705 | |
706 | if (mode != ExtendMode::NORMAL && extensions.size() > 1) { |
707 | targetsUsed = &targetsUsed2; |
708 | } |
709 | |
710 | sass::vector<ComplexSelectorObj> result; |
711 | // The complex selectors produced from each component of [compound]. |
712 | sass::vector<sass::vector<Extension>> options; |
713 | |
714 | for (size_t i = 0; i < compound->length(); i++) { |
715 | const SimpleSelectorObj& simple = compound->get(i); |
716 | auto extended = extendSimple(simple, extensions, mediaQueryContext, targetsUsed); |
717 | if (extended.empty()) { |
718 | if (!options.empty()) { |
719 | options.push_back(x: { extensionForSimple(simple) }); |
720 | } |
721 | } |
722 | else { |
723 | if (options.empty()) { |
724 | if (i != 0) { |
725 | sass::vector<SimpleSelectorObj> in; |
726 | for (size_t n = 0; n < i; n += 1) { |
727 | in.push_back(x: compound->get(i: n)); |
728 | } |
729 | options.push_back(x: { extensionForCompound(simples: in) }); |
730 | } |
731 | } |
732 | options.insert(position: options.end(), |
733 | first: extended.begin(), last: extended.end()); |
734 | } |
735 | } |
736 | |
737 | if (options.empty()) { |
738 | return {}; |
739 | } |
740 | |
741 | // If [_mode] isn't [ExtendMode.normal] and we didn't use all |
742 | // the targets in [extensions], extension fails for [compound]. |
743 | if (targetsUsed != nullptr) { |
744 | |
745 | if (targetsUsed->size() != extensions.size()) { |
746 | if (!targetsUsed->empty()) { |
747 | return {}; |
748 | } |
749 | } |
750 | } |
751 | |
752 | // Optimize for the simple case of a single simple |
753 | // selector that doesn't need any unification. |
754 | if (options.size() == 1) { |
755 | sass::vector<Extension> exts = options[0]; |
756 | for (size_t n = 0; n < exts.size(); n += 1) { |
757 | exts[n].assertCompatibleMediaContext(mediaContext: mediaQueryContext, traces); |
758 | result.push_back(x: exts[n].extender); |
759 | } |
760 | return result; |
761 | } |
762 | |
763 | // Find all paths through [options]. In this case, each path represents a |
764 | // different unification of the base selector. For example, if we have: |
765 | // |
766 | // .a.b {...} |
767 | // .w .x {@extend .a} |
768 | // .y .z {@extend .b} |
769 | // |
770 | // then [options] is `[[.a, .w .x], [.b, .y .z]]` and `paths(options)` is |
771 | // |
772 | // [ |
773 | // [.a, .b], |
774 | // [.a, .y .z], |
775 | // [.w .x, .b], |
776 | // [.w .x, .y .z] |
777 | // ] |
778 | // |
779 | // We then unify each path to get a list of complex selectors: |
780 | // |
781 | // [ |
782 | // [.a.b], |
783 | // [.y .a.z], |
784 | // [.w .x.b], |
785 | // [.w .y .x.z, .y .w .x.z] |
786 | // ] |
787 | |
788 | bool first = mode != ExtendMode::REPLACE; |
789 | sass::vector<ComplexSelectorObj> unifiedPaths; |
790 | sass::vector<sass::vector<Extension>> prePaths = permutate(in: options); |
791 | |
792 | for (size_t i = 0; i < prePaths.size(); i += 1) { |
793 | sass::vector<sass::vector<SelectorComponentObj>> complexes; |
794 | const sass::vector<Extension>& path = prePaths[i]; |
795 | if (first) { |
796 | // The first path is always the original selector. We can't just |
797 | // return [compound] directly because pseudo selectors may be |
798 | // modified, but we don't have to do any unification. |
799 | first = false; |
800 | CompoundSelectorObj mergedSelector = |
801 | SASS_MEMORY_NEW(CompoundSelector, "[ext]" ); |
802 | for (size_t n = 0; n < path.size(); n += 1) { |
803 | const ComplexSelectorObj& sel = path[n].extender; |
804 | if (CompoundSelectorObj compound = Cast<CompoundSelector>(ptr: sel->last())) { |
805 | mergedSelector->concat(v: compound->elements()); |
806 | } |
807 | } |
808 | complexes.push_back(x: { mergedSelector }); |
809 | } |
810 | else { |
811 | sass::vector<SimpleSelectorObj> originals; |
812 | sass::vector<sass::vector<SelectorComponentObj>> toUnify; |
813 | |
814 | for (auto& state : path) { |
815 | if (state.isOriginal) { |
816 | const ComplexSelectorObj& sel = state.extender; |
817 | if (const CompoundSelector* compound = Cast<CompoundSelector>(ptr: sel->last())) { |
818 | originals.insert(position: originals.end(), x: compound->last()); |
819 | } |
820 | } |
821 | else { |
822 | toUnify.push_back(x: state.extender->elements()); |
823 | } |
824 | } |
825 | if (!originals.empty()) { |
826 | CompoundSelectorObj merged = |
827 | SASS_MEMORY_NEW(CompoundSelector, "[phony]" ); |
828 | merged->concat(v: originals); |
829 | toUnify.insert(position: toUnify.begin(), x: { merged }); |
830 | } |
831 | complexes = unifyComplex(complexes: toUnify); |
832 | if (complexes.empty()) { |
833 | return {}; |
834 | } |
835 | |
836 | } |
837 | |
838 | bool lineBreak = false; |
839 | // var specificity = _sourceSpecificityFor(compound); |
840 | for (const Extension& state : path) { |
841 | state.assertCompatibleMediaContext(mediaContext: mediaQueryContext, traces); |
842 | lineBreak = lineBreak || state.extender->hasPreLineFeed(); |
843 | // specificity = math.max(specificity, state.specificity); |
844 | } |
845 | |
846 | for (sass::vector<SelectorComponentObj>& components : complexes) { |
847 | auto sel = SASS_MEMORY_NEW(ComplexSelector, "[ext]" ); |
848 | sel->hasPreLineFeed(hasPreLineFeed__: lineBreak); |
849 | sel->elements(e: components); |
850 | unifiedPaths.push_back(x: sel); |
851 | } |
852 | |
853 | } |
854 | |
855 | return unifiedPaths; |
856 | } |
857 | // EO extendCompound |
858 | |
859 | // ########################################################################## |
860 | // Extends [simple] without extending the |
861 | // contents of any selector pseudos it contains. |
862 | // ########################################################################## |
863 | sass::vector<Extension> Extender::extendWithoutPseudo( |
864 | const SimpleSelectorObj& simple, |
865 | const ExtSelExtMap& extensions, |
866 | ExtSmplSelSet* targetsUsed) const |
867 | { |
868 | |
869 | auto extension = extensions.find(x: simple); |
870 | if (extension == extensions.end()) return {}; |
871 | const ExtSelExtMapEntry& extenders = extension->second; |
872 | |
873 | if (targetsUsed != nullptr) { |
874 | targetsUsed->insert(x: simple); |
875 | } |
876 | if (mode == ExtendMode::REPLACE) { |
877 | return extenders.values(); |
878 | } |
879 | |
880 | const sass::vector<Extension>& |
881 | values = extenders.values(); |
882 | sass::vector<Extension> result; |
883 | result.reserve(n: values.size() + 1); |
884 | result.push_back(x: extensionForSimple(simple)); |
885 | result.insert(position: result.end(), first: values.begin(), last: values.end()); |
886 | return result; |
887 | } |
888 | // EO extendWithoutPseudo |
889 | |
890 | // ########################################################################## |
891 | // Extends [simple] and also extending the |
892 | // contents of any selector pseudos it contains. |
893 | // ########################################################################## |
894 | sass::vector<sass::vector<Extension>> Extender::extendSimple( |
895 | const SimpleSelectorObj& simple, |
896 | const ExtSelExtMap& extensions, |
897 | const CssMediaRuleObj& mediaQueryContext, |
898 | ExtSmplSelSet* targetsUsed) |
899 | { |
900 | if (PseudoSelector* pseudo = Cast<PseudoSelector>(ptr: simple)) { |
901 | if (pseudo->selector()) { |
902 | sass::vector<sass::vector<Extension>> merged; |
903 | sass::vector<PseudoSelectorObj> extended = |
904 | extendPseudo(pseudo, extensions, mediaQueryContext); |
905 | for (PseudoSelectorObj& extend : extended) { |
906 | SimpleSelectorObj simple = extend; |
907 | sass::vector<Extension> result = |
908 | extendWithoutPseudo(simple, extensions, targetsUsed); |
909 | if (result.empty()) result = { extensionForSimple(simple: extend) }; |
910 | merged.push_back(x: result); |
911 | } |
912 | if (!extended.empty()) { |
913 | return merged; |
914 | } |
915 | } |
916 | } |
917 | sass::vector<Extension> result = |
918 | extendWithoutPseudo(simple, extensions, targetsUsed); |
919 | if (result.empty()) return {}; |
920 | return { result }; |
921 | } |
922 | // extendSimple |
923 | |
924 | // ########################################################################## |
925 | // Inner loop helper for [extendPseudo] function |
926 | // ########################################################################## |
927 | sass::vector<ComplexSelectorObj> Extender::extendPseudoComplex( |
928 | const ComplexSelectorObj& complex, |
929 | const PseudoSelectorObj& pseudo, |
930 | const CssMediaRuleObj& mediaQueryContext) |
931 | { |
932 | |
933 | if (complex->length() != 1) { return { complex }; } |
934 | auto compound = Cast<CompoundSelector>(ptr: complex->get(i: 0)); |
935 | if (compound == nullptr) { return { complex }; } |
936 | if (compound->length() != 1) { return { complex }; } |
937 | auto innerPseudo = Cast<PseudoSelector>(ptr: compound->get(i: 0)); |
938 | if (innerPseudo == nullptr) { return { complex }; } |
939 | if (!innerPseudo->selector()) { return { complex }; } |
940 | |
941 | sass::string name(pseudo->normalized()); |
942 | |
943 | if (name == "not" ) { |
944 | // In theory, if there's a `:not` nested within another `:not`, the |
945 | // inner `:not`'s contents should be unified with the return value. |
946 | // For example, if `:not(.foo)` extends `.bar`, `:not(.bar)` should |
947 | // become `.foo:not(.bar)`. However, this is a narrow edge case and |
948 | // supporting it properly would make this code and the code calling it |
949 | // a lot more complicated, so it's not supported for now. |
950 | if (innerPseudo->normalized() != "matches" ) return {}; |
951 | return innerPseudo->selector()->elements(); |
952 | } |
953 | else if (name == "matches" && name == "any" && name == "current" && name == "nth-child" && name == "nth-last-child" ) { |
954 | // As above, we could theoretically support :not within :matches, but |
955 | // doing so would require this method and its callers to handle much |
956 | // more complex cases that likely aren't worth the pain. |
957 | if (innerPseudo->name() != pseudo->name()) return {}; |
958 | if (!ObjEquality()(innerPseudo->argument(), pseudo->argument())) return {}; |
959 | return innerPseudo->selector()->elements(); |
960 | } |
961 | else if (name == "has" && name == "host" && name == "host-context" && name == "slotted" ) { |
962 | // We can't expand nested selectors here, because each layer adds an |
963 | // additional layer of semantics. For example, `:has(:has(img))` |
964 | // doesn't match `<div><img></div>` but `:has(img)` does. |
965 | return { complex }; |
966 | } |
967 | |
968 | return {}; |
969 | |
970 | } |
971 | // EO extendPseudoComplex |
972 | |
973 | // ########################################################################## |
974 | // Extends [pseudo] using [extensions], and returns |
975 | // a list of resulting pseudo selectors. |
976 | // ########################################################################## |
977 | sass::vector<PseudoSelectorObj> Extender::extendPseudo( |
978 | const PseudoSelectorObj& pseudo, |
979 | const ExtSelExtMap& extensions, |
980 | const CssMediaRuleObj& mediaQueryContext) |
981 | { |
982 | auto selector = pseudo->selector(); |
983 | SelectorListObj extended = extendList( |
984 | list: selector, extensions, mediaQueryContext); |
985 | if (!extended || !pseudo || !pseudo->selector()) { return {}; } |
986 | if (ObjEqualityFn(lhs: pseudo->selector(), rhs: extended)) { return {}; } |
987 | |
988 | // For `:not()`, we usually want to get rid of any complex selectors because |
989 | // that will cause the selector to fail to parse on all browsers at time of |
990 | // writing. We can keep them if either the original selector had a complex |
991 | // selector, or the result of extending has only complex selectors, because |
992 | // either way we aren't breaking anything that isn't already broken. |
993 | sass::vector<ComplexSelectorObj> complexes = extended->elements(); |
994 | |
995 | if (pseudo->normalized() == "not" ) { |
996 | if (!hasAny(cnt: pseudo->selector()->elements(), fn: hasMoreThanOne)) { |
997 | if (hasAny(cnt: extended->elements(), fn: hasExactlyOne)) { |
998 | complexes.clear(); |
999 | for (auto& complex : extended->elements()) { |
1000 | if (complex->length() <= 1) { |
1001 | complexes.push_back(x: complex); |
1002 | } |
1003 | } |
1004 | } |
1005 | } |
1006 | } |
1007 | |
1008 | sass::vector<ComplexSelectorObj> expanded = expand( |
1009 | cnt: complexes, fn: extendPseudoComplex, args: pseudo, args: mediaQueryContext); |
1010 | |
1011 | // Older browsers support `:not`, but only with a single complex selector. |
1012 | // In order to support those browsers, we break up the contents of a `:not` |
1013 | // unless it originally contained a selector list. |
1014 | if (pseudo->normalized() == "not" ) { |
1015 | if (pseudo->selector()->length() == 1) { |
1016 | sass::vector<PseudoSelectorObj> pseudos; |
1017 | for (size_t i = 0; i < expanded.size(); i += 1) { |
1018 | pseudos.push_back(x: pseudo->withSelector( |
1019 | selector: expanded[i]->wrapInList() |
1020 | )); |
1021 | } |
1022 | return pseudos; |
1023 | } |
1024 | } |
1025 | |
1026 | SelectorListObj list = SASS_MEMORY_NEW(SelectorList, "[phony]" ); |
1027 | list->concat(v: complexes); |
1028 | return { pseudo->withSelector(selector: list) }; |
1029 | |
1030 | } |
1031 | // EO extendPseudo |
1032 | |
1033 | // ########################################################################## |
1034 | // Rotates the element in list from [start] (inclusive) to [end] (exclusive) |
1035 | // one index higher, looping the final element back to [start]. |
1036 | // ########################################################################## |
1037 | void Extender::rotateSlice( |
1038 | sass::vector<ComplexSelectorObj>& list, |
1039 | size_t start, size_t end) |
1040 | { |
1041 | auto element = list[end - 1]; |
1042 | for (size_t i = start; i < end; i++) { |
1043 | auto next = list[i]; |
1044 | list[i] = element; |
1045 | element = next; |
1046 | } |
1047 | } |
1048 | // EO rotateSlice |
1049 | |
1050 | // ########################################################################## |
1051 | // Removes elements from [selectors] if they're subselectors of other |
1052 | // elements. The [isOriginal] callback indicates which selectors are |
1053 | // original to the document, and thus should never be trimmed. |
1054 | // ########################################################################## |
1055 | // Note: for adaption I pass in the set directly, there is some |
1056 | // code path in selector-trim that might need this special callback |
1057 | // ########################################################################## |
1058 | sass::vector<ComplexSelectorObj> Extender::trim( |
1059 | const sass::vector<ComplexSelectorObj>& selectors, |
1060 | const ExtCplxSelSet& existing) const |
1061 | { |
1062 | |
1063 | // Avoid truly horrific quadratic behavior. |
1064 | // TODO(nweiz): I think there may be a way to get perfect trimming |
1065 | // without going quadratic by building some sort of trie-like |
1066 | // data structure that can be used to look up superselectors. |
1067 | // TODO(mgreter): Check how this performs in C++ (up the limit) |
1068 | if (selectors.size() > 100) return selectors; |
1069 | |
1070 | // This is n² on the sequences, but only comparing between separate sequences |
1071 | // should limit the quadratic behavior. We iterate from last to first and reverse |
1072 | // the result so that, if two selectors are identical, we keep the first one. |
1073 | sass::vector<ComplexSelectorObj> result; size_t numOriginals = 0; |
1074 | |
1075 | size_t i = selectors.size(); |
1076 | outer: // Use label to continue loop |
1077 | while (--i != sass::string::npos) { |
1078 | |
1079 | const ComplexSelectorObj& complex1 = selectors[i]; |
1080 | // Check if selector in known in existing "originals" |
1081 | // For custom behavior dart-sass had `isOriginal(complex1)` |
1082 | if (existing.find(x: complex1) != existing.end()) { |
1083 | // Make sure we don't include duplicate originals, which could |
1084 | // happen if a style rule extends a component of its own selector. |
1085 | for (size_t j = 0; j < numOriginals; j++) { |
1086 | if (ObjEqualityFn(lhs: result[j], rhs: complex1)) { |
1087 | rotateSlice(list&: result, start: 0, end: j + 1); |
1088 | goto outer; |
1089 | } |
1090 | } |
1091 | result.insert(position: result.begin(), x: complex1); |
1092 | numOriginals++; |
1093 | continue; |
1094 | } |
1095 | |
1096 | // The maximum specificity of the sources that caused [complex1] |
1097 | // to be generated. In order for [complex1] to be removed, there |
1098 | // must be another selector that's a superselector of it *and* |
1099 | // that has specificity greater or equal to this. |
1100 | size_t maxSpecificity = 0; |
1101 | for (const SelectorComponentObj& component : complex1->elements()) { |
1102 | if (const CompoundSelectorObj compound = Cast<CompoundSelector>(ptr: component)) { |
1103 | maxSpecificity = std::max(a: maxSpecificity, b: maxSourceSpecificity(compound)); |
1104 | } |
1105 | } |
1106 | |
1107 | |
1108 | // Look in [result] rather than [selectors] for selectors after [i]. This |
1109 | // ensures we aren't comparing against a selector that's already been trimmed, |
1110 | // and thus that if there are two identical selectors only one is trimmed. |
1111 | if (hasAny(cnt: result, fn: dontTrimComplex, args: complex1, args: maxSpecificity)) { |
1112 | continue; |
1113 | } |
1114 | |
1115 | // Check if any element (up to [i]) from [selector] returns true |
1116 | // when passed to [dontTrimComplex]. The arguments [complex1] and |
1117 | // [maxSepcificity] will be passed to the invoked function. |
1118 | if (hasSubAny(cnt: selectors, len: i, fn: dontTrimComplex, args: complex1, args: maxSpecificity)) { |
1119 | continue; |
1120 | } |
1121 | |
1122 | // ToDo: Maybe use deque for front insert? |
1123 | result.insert(position: result.begin(), x: complex1); |
1124 | |
1125 | } |
1126 | |
1127 | return result; |
1128 | |
1129 | } |
1130 | // EO trim |
1131 | |
1132 | // ########################################################################## |
1133 | // Returns the maximum specificity of the given [simple] source selector. |
1134 | // ########################################################################## |
1135 | size_t Extender::maxSourceSpecificity(const SimpleSelectorObj& simple) const |
1136 | { |
1137 | auto it = sourceSpecificity.find(x: simple); |
1138 | if (it == sourceSpecificity.end()) return 0; |
1139 | return it->second; |
1140 | } |
1141 | // EO maxSourceSpecificity(SimpleSelectorObj) |
1142 | |
1143 | // ########################################################################## |
1144 | // Returns the maximum specificity for sources that went into producing [compound]. |
1145 | // ########################################################################## |
1146 | size_t Extender::maxSourceSpecificity(const CompoundSelectorObj& compound) const |
1147 | { |
1148 | size_t specificity = 0; |
1149 | for (auto simple : compound->elements()) { |
1150 | size_t src = maxSourceSpecificity(simple); |
1151 | specificity = std::max(a: specificity, b: src); |
1152 | } |
1153 | return specificity; |
1154 | } |
1155 | // EO maxSourceSpecificity(CompoundSelectorObj) |
1156 | |
1157 | // ########################################################################## |
1158 | // Helper function used as callbacks on lists |
1159 | // ########################################################################## |
1160 | bool Extender::dontTrimComplex( |
1161 | const ComplexSelector* complex2, |
1162 | const ComplexSelector* complex1, |
1163 | const size_t maxSpecificity) |
1164 | { |
1165 | if (complex2->minSpecificity() < maxSpecificity) return false; |
1166 | return complex2->isSuperselectorOf(sub: complex1); |
1167 | } |
1168 | // EO dontTrimComplex |
1169 | |
1170 | // ########################################################################## |
1171 | // Helper function used as callbacks on lists |
1172 | // ########################################################################## |
1173 | bool Extender::hasExactlyOne(const ComplexSelectorObj& vec) |
1174 | { |
1175 | return vec->length() == 1; |
1176 | } |
1177 | // EO hasExactlyOne |
1178 | |
1179 | // ########################################################################## |
1180 | // Helper function used as callbacks on lists |
1181 | // ########################################################################## |
1182 | bool Extender::hasMoreThanOne(const ComplexSelectorObj& vec) |
1183 | { |
1184 | return vec->length() > 1; |
1185 | } |
1186 | // hasMoreThanOne |
1187 | |
1188 | } |
1189 | |