// sass.hpp must go before all system headers to get the // __EXTENSIONS__ fix on Solaris. #include "sass.hpp" #include "ast.hpp" #include "extender.hpp" #include "permutate.hpp" #include "dart_helpers.hpp" namespace Sass { // ########################################################################## // Constructor without default [mode]. // [traces] are needed to throw errors. // ########################################################################## Extender::Extender(Backtraces& traces) : mode(NORMAL), traces(traces), selectors(), extensions(), extensionsByExtender(), mediaContexts(), sourceSpecificity(), originals() {} // ########################################################################## // Constructor with specific [mode]. // [traces] are needed to throw errors. // ########################################################################## Extender::Extender(ExtendMode mode, Backtraces& traces) : mode(mode), traces(traces), selectors(), extensions(), extensionsByExtender(), mediaContexts(), sourceSpecificity(), originals() {} // ########################################################################## // Extends [selector] with [source] extender and [targets] extendees. // This works as though `source {@extend target}` were written in the // stylesheet, with the exception that [target] can contain compound // selectors which must be extended as a unit. // ########################################################################## SelectorListObj Extender::extend( SelectorListObj& selector, const SelectorListObj& source, const SelectorListObj& targets, Backtraces& traces) { return extendOrReplace(selector, source, targets, ExtendMode::TARGETS, traces); } // EO Extender::extend // ########################################################################## // Returns a copy of [selector] with [targets] replaced by [source]. // ########################################################################## SelectorListObj Extender::replace( SelectorListObj& selector, const SelectorListObj& source, const SelectorListObj& targets, Backtraces& traces) { return extendOrReplace(selector, source, targets, ExtendMode::REPLACE, traces); } // EO Extender::replace // ########################################################################## // A helper function for [extend] and [replace]. // ########################################################################## SelectorListObj Extender::extendOrReplace( SelectorListObj& selector, const SelectorListObj& source, const SelectorListObj& targets, const ExtendMode mode, Backtraces& traces) { ExtSelExtMapEntry extenders; for (auto complex : source->elements()) { // Extension.oneOff(complex as ComplexSelector) extenders.insert(complex, Extension(complex)); } for (auto complex : targets->elements()) { // This seems superfluous, check is done before!? // if (complex->length() != 1) { // error("complex selectors may not be extended.", complex->pstate(), traces); // } if (const CompoundSelector* compound = complex->first()->getCompound()) { ExtSelExtMap extensions; for (const SimpleSelectorObj& simple : compound->elements()) { extensions.insert(std::make_pair(simple, extenders)); } Extender extender(mode, traces); if (!selector->is_invisible()) { for (auto sel : selector->elements()) { extender.originals.insert(sel); } } selector = extender.extendList(selector, extensions, {}); } } return selector; } // EO extendOrReplace // ########################################################################## // The set of all simple selectors in style rules handled // by this extender. This includes simple selectors that // were added because of downstream extensions. // ########################################################################## ExtSmplSelSet Extender::getSimpleSelectors() const { ExtSmplSelSet set; for (auto& entry : selectors) { set.insert(entry.first); } return set; } // EO getSimpleSelectors // ########################################################################## // Check for extends that have not been satisfied. // Returns true if any non-optional extension did not // extend any selector. Updates the passed reference // to point to that Extension for further analysis. // ########################################################################## bool Extender::checkForUnsatisfiedExtends(Extension& unsatisfied) const { if (selectors.empty()) return false; ExtSmplSelSet originals = getSimpleSelectors(); for (auto target : extensions) { SimpleSelector* key = target.first; ExtSelExtMapEntry& val = target.second; if (val.empty()) continue; if (originals.find(key) == originals.end()) { const Extension& extension = val.front().second; if (extension.isOptional) continue; unsatisfied = extension; return true; } } return false; } // EO checkUnsatisfiedExtends // ########################################################################## // Adds [selector] to this extender, with [selectorSpan] as the span covering // the selector and [ruleSpan] as the span covering the entire style rule. // Extends [selector] using any registered extensions, then returns an empty // [ModifiableCssStyleRule] with the resulting selector. If any more relevant // extensions are added, the returned rule is automatically updated. // The [mediaContext] is the media query context in which the selector was // defined, or `null` if it was defined at the top level of the document. // ########################################################################## void Extender::addSelector( const SelectorListObj& selector, const CssMediaRuleObj& mediaContext) { // Note: dart-sass makes a copy here AFAICT // Note: probably why we have originalStack // SelectorListObj original = selector; if (!selector->isInvisible()) { for (auto complex : selector->elements()) { originals.insert(complex); } } if (!extensions.empty()) { SelectorListObj res = extendList(selector, extensions, mediaContext); selector->elements(res->elements()); } if (!mediaContext.isNull()) { mediaContexts.insert(selector, mediaContext); } registerSelector(selector, selector); } // EO addSelector // ########################################################################## // Registers the [SimpleSelector]s in [list] // to point to [rule] in [selectors]. // ########################################################################## void Extender::registerSelector( const SelectorListObj& list, const SelectorListObj& rule) { if (list.isNull() || list->empty()) return; for (auto complex : list->elements()) { for (auto component : complex->elements()) { if (auto compound = component->getCompound()) { for (SimpleSelector* simple : compound->elements()) { selectors[simple].insert(rule); if (auto pseudo = simple->getPseudoSelector()) { if (pseudo->selector()) { auto sel = pseudo->selector(); registerSelector(sel, rule); } } } } } } } // EO registerSelector // ########################################################################## // Returns an extension that combines [left] and [right]. Throws // a [SassException] if [left] and [right] have incompatible // media contexts. Throws an [ArgumentError] if [left] // and [right] don't have the same extender and target. // ########################################################################## Extension Extender::mergeExtension( const Extension& lhs, const Extension& rhs) { // If one extension is optional and doesn't add a // special media context, it doesn't need to be merged. if (rhs.isOptional && rhs.mediaContext.isNull()) return lhs; if (lhs.isOptional && lhs.mediaContext.isNull()) return rhs; Extension rv(lhs); // ToDo: is this right? rv.isOptional = true; rv.isOriginal = false; return rv; } // EO mergeExtension // ########################################################################## // Helper function to copy extension between maps // ########################################################################## // Seems only relevant for sass 4.0 modules // ########################################################################## /* void mapCopyExts( ExtSelExtMap& dest, const ExtSelExtMap& source) { for (auto it : source) { SimpleSelectorObj key = it.first; ExtSelExtMapEntry& inner = it.second; ExtSelExtMap::iterator dmap = dest.find(key); if (dmap == dest.end()) { dest.insert(std::make_pair(key, inner)); } else { ExtSelExtMapEntry& imap = dmap->second; // ToDo: optimize ordered_map API! // ToDo: we iterate and fetch the value for (ComplexSelectorObj& it2 : inner) { imap.insert(it2, inner.get(it2)); } } } } */ // EO mapCopyExts // ########################################################################## // Adds an extension to this extender. The [extender] is the selector for the // style rule in which the extension is defined, and [target] is the selector // passed to `@extend`. The [extend] provides the extend span and indicates // whether the extension is optional. The [mediaContext] defines the media query // context in which the extension is defined. It can only extend selectors // within the same context. A `null` context indicates no media queries. // ########################################################################## // ToDo: rename extender to parent, since it is not involved in extending stuff // ToDo: check why dart sass passes the ExtendRule around (is this the main selector?) // ########################################################################## // Note: this function could need some logic cleanup // ########################################################################## void Extender::addExtension( const SelectorListObj& extender, const SimpleSelectorObj& target, const CssMediaRuleObj& mediaQueryContext, bool is_optional) { auto rules = selectors.find(target); bool hasRule = rules != selectors.end(); ExtSelExtMapEntry newExtensions; // ToDo: we check this here first and fetch the same? item again after the loop!? bool hasExistingExtensions = extensionsByExtender.find(target) != extensionsByExtender.end(); ExtSelExtMapEntry& sources = extensions[target]; for (auto& complex : extender->elements()) { Extension state(complex); // ToDo: fine-tune public API state.target = target; state.isOptional = is_optional; state.mediaContext = mediaQueryContext; if (sources.hasKey(complex)) { // If there's already an extend from [extender] to [target], // we don't need to re-run the extension. We may need to // mark the extension as mandatory, though. // sources.insert(complex, mergeExtension(existingState->second, state); // ToDo: implement behavior once use case is found!? continue; } sources.insert(complex, state); for (auto& component : complex->elements()) { if (auto compound = component->getCompound()) { for (auto& simple : compound->elements()) { extensionsByExtender[simple].push_back(state); if (sourceSpecificity.find(simple) == sourceSpecificity.end()) { // Only source specificity for the original selector is relevant. // Selectors generated by `@extend` don't get new specificity. sourceSpecificity[simple] = complex->maxSpecificity(); } } } } if (hasRule || hasExistingExtensions) { newExtensions.insert(complex, state); } } // EO foreach complex if (newExtensions.empty()) { return; } ExtSelExtMap newExtensionsByTarget; newExtensionsByTarget.insert(std::make_pair(target, newExtensions)); // ToDo: do we really need to fetch again (see top off fn) auto existingExtensions = extensionsByExtender.find(target); if (existingExtensions != extensionsByExtender.end()) { if (hasExistingExtensions && !existingExtensions->second.empty()) { // Seems only relevant for sass 4.0 modules // auto additionalExtensions = extendExistingExtensions(existingExtensions->second, newExtensionsByTarget); // Seems only relevant for sass 4.0 modules /* if (!additionalExtensions.empty()) { mapCopyExts(newExtensionsByTarget, additionalExtensions); } */ } } if (hasRule) { extendExistingStyleRules(selectors[target], newExtensionsByTarget); } } // EO addExtension // ########################################################################## // Extend [extensions] using [newExtensions]. // ########################################################################## // Note: dart-sass throws an error in here // ########################################################################## void Extender::extendExistingStyleRules( const ExtListSelSet& rules, const ExtSelExtMap& newExtensions) { // Is a modifyableCssStyleRUle in dart sass for (const SelectorListObj& rule : rules) { const SelectorListObj& oldValue = SASS_MEMORY_COPY(rule); CssMediaRuleObj mediaContext; if (mediaContexts.hasKey(rule)) mediaContext = mediaContexts.get(rule); SelectorListObj ext = extendList(rule, newExtensions, mediaContext); // If no extends actually happened (for example because unification // failed), we don't need to re-register the selector. if (ObjEqualityFn(oldValue, ext)) continue; rule->elements(ext->elements()); registerSelector(rule, rule); } } // EO extendExistingStyleRules // ########################################################################## // Extend [extensions] using [newExtensions]. Note that this does duplicate // some work done by [_extendExistingStyleRules], but it's necessary to // expand each extension's extender separately without reference to the full // selector list, so that relevant results don't get trimmed too early. // // Returns extensions that should be added to [newExtensions] before // extending selectors in order to properly handle extension loops such as: // // .c {x: y; @extend .a} // .x.y.a {@extend .b} // .z.b {@extend .c} // // Returns `null` (Note: empty map) if there are no extensions to add. // ########################################################################## // Note: maybe refactor to return `bool` (and pass reference) // Note: dart-sass throws an error in here // ########################################################################## ExtSelExtMap Extender::extendExistingExtensions( // Taking in a reference here makes MSVC debug stuck!? const sass::vector& oldExtensions, const ExtSelExtMap& newExtensions) { ExtSelExtMap additionalExtensions; // During the loop `oldExtensions` vector might be changed. // Callers normally pass this from `extensionsByExtender` and // that points back to the `sources` vector from `extensions`. for (size_t i = 0, iL = oldExtensions.size(); i < iL; i += 1) { const Extension& extension = oldExtensions[i]; ExtSelExtMapEntry& sources = extensions[extension.target]; sass::vector selectors(extendComplex( extension.extender, newExtensions, extension.mediaContext )); if (selectors.empty()) { continue; } // ToDo: "catch" error from extend bool first = false, containsExtension = ObjEqualityFn(selectors.front(), extension.extender); for (const ComplexSelectorObj& complex : selectors) { // If the output contains the original complex // selector, there's no need to recreate it. if (containsExtension && first) { first = false; continue; } const Extension withExtender = extension.withExtender(complex); if (sources.hasKey(complex)) { sources.insert(complex, mergeExtension( sources.get(complex), withExtender)); } else { sources.insert(complex, withExtender); /* // Seems only relevant for sass 4.0 modules for (auto& component : complex->elements()) { if (auto compound = component->getCompound()) { for (auto& simple : compound->elements()) { extensionsByExtender[simple].push_back(withExtender); } } } if (newExtensions.find(extension.target) != newExtensions.end()) { additionalExtensions[extension.target].insert(complex, withExtender); } */ } } // If [selectors] doesn't contain [extension.extender], // for example if it was replaced due to :not() expansion, // we must get rid of the old version. /* // Seems only relevant for sass 4.0 modules if (!containsExtension) { sources.erase(extension.extender); } */ } return additionalExtensions; } // EO extendExistingExtensions // ########################################################################## // Extends [list] using [extensions]. // ########################################################################## SelectorListObj Extender::extendList( const SelectorListObj& list, const ExtSelExtMap& extensions, const CssMediaRuleObj& mediaQueryContext) { // This could be written more simply using [List.map], but we want to // avoid any allocations in the common case where no extends apply. sass::vector extended; for (size_t i = 0; i < list->length(); i++) { const ComplexSelectorObj& complex = list->get(i); sass::vector result = extendComplex(complex, extensions, mediaQueryContext); if (result.empty()) { if (!extended.empty()) { extended.push_back(complex); } } else { if (extended.empty()) { for (size_t n = 0; n < i; n += 1) { extended.push_back(list->get(n)); } } for (auto sel : result) { extended.push_back(sel); } } } if (extended.empty()) { return list; } SelectorListObj rv = SASS_MEMORY_NEW(SelectorList, list->pstate()); rv->concat(trim(extended, originals)); return rv; } // EO extendList // ########################################################################## // Extends [complex] using [extensions], and // returns the contents of a [SelectorList]. // ########################################################################## sass::vector Extender::extendComplex( // Taking in a reference here makes MSVC debug stuck!? const ComplexSelectorObj& complex, const ExtSelExtMap& extensions, const CssMediaRuleObj& mediaQueryContext) { // The complex selectors that each compound selector in [complex.components] // can expand to. // // For example, given // // .a .b {...} // .x .y {@extend .b} // // this will contain // // [ // [.a], // [.b, .x .y] // ] // // This could be written more simply using [List.map], but we want to avoid // any allocations in the common case where no extends apply. sass::vector result; sass::vector> extendedNotExpanded; bool isOriginal = originals.find(complex) != originals.end(); for (size_t i = 0; i < complex->length(); i += 1) { const SelectorComponentObj& component = complex->get(i); if (CompoundSelector* compound = Cast(component)) { sass::vector extended = extendCompound( compound, extensions, mediaQueryContext, isOriginal); if (extended.empty()) { if (!extendedNotExpanded.empty()) { extendedNotExpanded.push_back({ compound->wrapInComplex() }); } } else { // Note: dart-sass checks for null!? if (extendedNotExpanded.empty()) { for (size_t n = 0; n < i; n++) { extendedNotExpanded.push_back({ complex->at(n)->wrapInComplex() }); } } extendedNotExpanded.push_back(extended); } } else { // Note: dart-sass checks for null!? if (!extendedNotExpanded.empty()) { extendedNotExpanded.push_back({ component->wrapInComplex() }); } } } // Note: dart-sass checks for null!? if (extendedNotExpanded.empty()) { return {}; } bool first = true; // ToDo: either change weave or paths to work with the same data? sass::vector> paths = permutate(extendedNotExpanded); for (const sass::vector& path : paths) { // Unpack the inner complex selector to component list sass::vector> _paths; for (const ComplexSelectorObj& sel : path) { _paths.insert(_paths.end(), sel->elements()); } sass::vector> weaved = weave(_paths); for (sass::vector& components : weaved) { ComplexSelectorObj cplx = SASS_MEMORY_NEW(ComplexSelector, complex->pstate()); cplx->hasPreLineFeed(complex->hasPreLineFeed()); for (auto& pp : path) { if (pp->hasPreLineFeed()) { cplx->hasPreLineFeed(true); } } cplx->elements(components); // Make sure that copies of [complex] retain their status // as "original" selectors. This includes selectors that // are modified because a :not() was extended into. if (first && originals.find(complex) != originals.end()) { originals.insert(cplx); } first = false; auto it = result.begin(); while (it != result.end()) { if (ObjEqualityFn(*it, cplx)) break; it += 1; } if (it == result.end()) { result.push_back(cplx); } if (result.size() > 500) { throw Exception::EndlessExtendError(traces, complex); } } } return result; } // EO extendComplex // ########################################################################## // Returns a one-off [Extension] whose // extender is composed solely of [simple]. // ########################################################################## Extension Extender::extensionForSimple( const SimpleSelectorObj& simple) const { Extension extension(simple->wrapInComplex()); extension.specificity = maxSourceSpecificity(simple); extension.isOriginal = true; return extension; } // Extender::extensionForSimple // ########################################################################## // Returns a one-off [Extension] whose extender is composed // solely of a compound selector containing [simples]. // ########################################################################## Extension Extender::extensionForCompound( // Taking in a reference here makes MSVC debug stuck!? const sass::vector& simples) const { CompoundSelectorObj compound = SASS_MEMORY_NEW(CompoundSelector, SourceSpan("[ext]")); compound->concat(simples); Extension extension(compound->wrapInComplex()); // extension.specificity = sourceSpecificity[simple]; extension.isOriginal = true; return extension; } // EO extensionForCompound // ########################################################################## // Extends [compound] using [extensions], and returns the // contents of a [SelectorList]. The [inOriginal] parameter // indicates whether this is in an original complex selector, // meaning that [compound] should not be trimmed out. // ########################################################################## sass::vector Extender::extendCompound( const CompoundSelectorObj& compound, const ExtSelExtMap& extensions, const CssMediaRuleObj& mediaQueryContext, bool inOriginal) { // If there's more than one target and they all need to // match, we track which targets are actually extended. ExtSmplSelSet targetsUsed2; ExtSmplSelSet* targetsUsed = nullptr; if (mode != ExtendMode::NORMAL && extensions.size() > 1) { targetsUsed = &targetsUsed2; } sass::vector result; // The complex selectors produced from each component of [compound]. sass::vector> options; for (size_t i = 0; i < compound->length(); i++) { const SimpleSelectorObj& simple = compound->get(i); auto extended = extendSimple(simple, extensions, mediaQueryContext, targetsUsed); if (extended.empty()) { if (!options.empty()) { options.push_back({ extensionForSimple(simple) }); } } else { if (options.empty()) { if (i != 0) { sass::vector in; for (size_t n = 0; n < i; n += 1) { in.push_back(compound->get(n)); } options.push_back({ extensionForCompound(in) }); } } options.insert(options.end(), extended.begin(), extended.end()); } } if (options.empty()) { return {}; } // If [_mode] isn't [ExtendMode.normal] and we didn't use all // the targets in [extensions], extension fails for [compound]. if (targetsUsed != nullptr) { if (targetsUsed->size() != extensions.size()) { if (!targetsUsed->empty()) { return {}; } } } // Optimize for the simple case of a single simple // selector that doesn't need any unification. if (options.size() == 1) { sass::vector exts = options[0]; for (size_t n = 0; n < exts.size(); n += 1) { exts[n].assertCompatibleMediaContext(mediaQueryContext, traces); // To fix invalid css we need to re-order some // Therefore we need to make copies for them if (exts[n].extender->isInvalidCss()) { exts[n].extender = SASS_MEMORY_COPY(exts[n].extender); for (SelectorComponentObj& component : exts[n].extender->elements()) { if (CompoundSelector* compound = component->getCompound()) { if (compound->isInvalidCss()) { CompoundSelector* copy = SASS_MEMORY_COPY(compound); copy->sortChildren(); component = copy; } } } } result.push_back(exts[n].extender); } return result; } // Find all paths through [options]. In this case, each path represents a // different unification of the base selector. For example, if we have: // // .a.b {...} // .w .x {@extend .a} // .y .z {@extend .b} // // then [options] is `[[.a, .w .x], [.b, .y .z]]` and `paths(options)` is // // [ // [.a, .b], // [.a, .y .z], // [.w .x, .b], // [.w .x, .y .z] // ] // // We then unify each path to get a list of complex selectors: // // [ // [.a.b], // [.y .a.z], // [.w .x.b], // [.w .y .x.z, .y .w .x.z] // ] bool first = mode != ExtendMode::REPLACE; sass::vector unifiedPaths; sass::vector> prePaths = permutate(options); for (size_t i = 0; i < prePaths.size(); i += 1) { sass::vector> complexes; const sass::vector& path = prePaths[i]; if (first) { // The first path is always the original selector. We can't just // return [compound] directly because pseudo selectors may be // modified, but we don't have to do any unification. first = false; CompoundSelectorObj mergedSelector = SASS_MEMORY_NEW(CompoundSelector, "[ext]"); for (size_t n = 0; n < path.size(); n += 1) { const ComplexSelectorObj& sel = path[n].extender; if (CompoundSelectorObj compound = Cast(sel->last())) { mergedSelector->concat(compound->elements()); } } complexes.push_back({ mergedSelector }); } else { sass::vector originals; sass::vector> toUnify; for (auto& state : path) { if (state.isOriginal) { const ComplexSelectorObj& sel = state.extender; if (const CompoundSelector* compound = Cast(sel->last())) { originals.insert(originals.end(), compound->last()); } } else { toUnify.push_back(state.extender->elements()); } } if (!originals.empty()) { CompoundSelectorObj merged = SASS_MEMORY_NEW(CompoundSelector, "[compound]"); merged->concat(originals); toUnify.insert(toUnify.begin(), { merged }); } complexes = unifyComplex(toUnify); if (complexes.empty()) { return {}; } } bool lineBreak = false; // var specificity = _sourceSpecificityFor(compound); for (const Extension& state : path) { state.assertCompatibleMediaContext(mediaQueryContext, traces); lineBreak = lineBreak || state.extender->hasPreLineFeed(); // specificity = math.max(specificity, state.specificity); } for (sass::vector& components : complexes) { auto sel = SASS_MEMORY_NEW(ComplexSelector, "[unified]"); sel->hasPreLineFeed(lineBreak); sel->elements(components); /* This seems to do too much in regard of previous behavior for (SelectorComponentObj& component : sel->elements()) { if (CompoundSelector* compound = component->getCompound()) { if (compound->isInvalidCss()) { CompoundSelector* copy = SASS_MEMORY_COPY(compound); copy->sortChildren(); component = copy; } } }*/ unifiedPaths.push_back(sel); } } return unifiedPaths; } // EO extendCompound // ########################################################################## // Extends [simple] without extending the // contents of any selector pseudos it contains. // ########################################################################## sass::vector Extender::extendWithoutPseudo( const SimpleSelectorObj& simple, const ExtSelExtMap& extensions, ExtSmplSelSet* targetsUsed) const { auto extension = extensions.find(simple); if (extension == extensions.end()) return {}; const ExtSelExtMapEntry& extenders = extension->second; if (targetsUsed != nullptr) { targetsUsed->insert(simple); } if (mode == ExtendMode::REPLACE) { return extenders.values(); } const sass::vector& values = extenders.values(); sass::vector result; result.reserve(values.size() + 1); result.push_back(extensionForSimple(simple)); result.insert(result.end(), values.begin(), values.end()); return result; } // EO extendWithoutPseudo // ########################################################################## // Extends [simple] and also extending the // contents of any selector pseudos it contains. // ########################################################################## sass::vector> Extender::extendSimple( const SimpleSelectorObj& simple, const ExtSelExtMap& extensions, const CssMediaRuleObj& mediaQueryContext, ExtSmplSelSet* targetsUsed) { if (PseudoSelector* pseudo = Cast(simple)) { if (pseudo->selector()) { sass::vector> merged; sass::vector extended = extendPseudo(pseudo, extensions, mediaQueryContext); for (PseudoSelectorObj& extend : extended) { SimpleSelectorObj simple = extend; sass::vector result = extendWithoutPseudo(simple, extensions, targetsUsed); if (result.empty()) result = { extensionForSimple(extend) }; merged.push_back(result); } if (!extended.empty()) { return merged; } } } sass::vector result = extendWithoutPseudo(simple, extensions, targetsUsed); if (result.empty()) return {}; return { result }; } // extendSimple // ########################################################################## // Inner loop helper for [extendPseudo] function // ########################################################################## sass::vector Extender::extendPseudoComplex( const ComplexSelectorObj& complex, const PseudoSelectorObj& pseudo, const CssMediaRuleObj& mediaQueryContext) { if (complex->length() != 1) { return { complex }; } auto compound = Cast(complex->get(0)); if (compound == nullptr) { return { complex }; } if (compound->length() != 1) { return { complex }; } auto innerPseudo = Cast(compound->get(0)); if (innerPseudo == nullptr) { return { complex }; } if (!innerPseudo->selector()) { return { complex }; } sass::string name(pseudo->normalized()); if (name == "not") { // In theory, if there's a `:not` nested within another `:not`, the // inner `:not`'s contents should be unified with the return value. // For example, if `:not(.foo)` extends `.bar`, `:not(.bar)` should // become `.foo:not(.bar)`. However, this is a narrow edge case and // supporting it properly would make this code and the code calling it // a lot more complicated, so it's not supported for now. if (innerPseudo->normalized() != "matches") return {}; return innerPseudo->selector()->elements(); } else if (name == "matches" || name == "any" || name == "current" || name == "nth-child" || name == "nth-last-child") { // As above, we could theoretically support :not within :matches, but // doing so would require this method and its callers to handle much // more complex cases that likely aren't worth the pain. if (innerPseudo->name() != pseudo->name()) return {}; if (!ObjEquality()(innerPseudo->argument(), pseudo->argument())) return {}; return innerPseudo->selector()->elements(); } else if (name == "has" || name == "host" || name == "host-context" || name == "slotted") { // We can't expand nested selectors here, because each layer adds an // additional layer of semantics. For example, `:has(:has(img))` // doesn't match `
` but `:has(img)` does. return { complex }; } return {}; } // EO extendPseudoComplex // ########################################################################## // Extends [pseudo] using [extensions], and returns // a list of resulting pseudo selectors. // ########################################################################## sass::vector Extender::extendPseudo( const PseudoSelectorObj& pseudo, const ExtSelExtMap& extensions, const CssMediaRuleObj& mediaQueryContext) { auto selector = pseudo->selector(); SelectorListObj extended = extendList( selector, extensions, mediaQueryContext); if (!extended || !pseudo || !pseudo->selector()) { return {}; } if (ObjEqualityFn(pseudo->selector(), extended)) { return {}; } // For `:not()`, we usually want to get rid of any complex selectors because // that will cause the selector to fail to parse on all browsers at time of // writing. We can keep them if either the original selector had a complex // selector, or the result of extending has only complex selectors, because // either way we aren't breaking anything that isn't already broken. sass::vector complexes = extended->elements(); if (pseudo->normalized() == "not") { if (!hasAny(pseudo->selector()->elements(), hasMoreThanOne)) { if (hasAny(extended->elements(), hasExactlyOne)) { complexes.clear(); for (auto& complex : extended->elements()) { if (complex->length() <= 1) { complexes.push_back(complex); } } } } } sass::vector expanded = expand( complexes, extendPseudoComplex, pseudo, mediaQueryContext); // Older browsers support `:not`, but only with a single complex selector. // In order to support those browsers, we break up the contents of a `:not` // unless it originally contained a selector list. if (pseudo->normalized() == "not") { if (pseudo->selector()->length() == 1) { sass::vector pseudos; for (size_t i = 0; i < expanded.size(); i += 1) { pseudos.push_back(pseudo->withSelector( expanded[i]->wrapInList() )); } return pseudos; } } SelectorListObj list = SASS_MEMORY_NEW(SelectorList, "[pseudo]"); list->concat(expanded); return { pseudo->withSelector(list) }; } // EO extendPseudo // ########################################################################## // Rotates the element in list from [start] (inclusive) to [end] (exclusive) // one index higher, looping the final element back to [start]. // ########################################################################## void Extender::rotateSlice( sass::vector& list, size_t start, size_t end) { auto element = list[end - 1]; for (size_t i = start; i < end; i++) { auto next = list[i]; list[i] = element; element = next; } } // EO rotateSlice // ########################################################################## // Removes elements from [selectors] if they're subselectors of other // elements. The [isOriginal] callback indicates which selectors are // original to the document, and thus should never be trimmed. // ########################################################################## // Note: for adaption I pass in the set directly, there is some // code path in selector-trim that might need this special callback // ########################################################################## sass::vector Extender::trim( const sass::vector& selectors, const ExtCplxSelSet& existing) const { // Avoid truly horrific quadratic behavior. // TODO(nweiz): I think there may be a way to get perfect trimming // without going quadratic by building some sort of trie-like // data structure that can be used to look up superselectors. // TODO(mgreter): Check how this performs in C++ (up the limit) if (selectors.size() > 100) return selectors; // This is n² on the sequences, but only comparing between separate sequences // should limit the quadratic behavior. We iterate from last to first and reverse // the result so that, if two selectors are identical, we keep the first one. sass::vector result; size_t numOriginals = 0; size_t i = selectors.size(); outer: // Use label to continue loop while (--i != sass::string::npos) { const ComplexSelectorObj& complex1 = selectors[i]; // Check if selector in known in existing "originals" // For custom behavior dart-sass had `isOriginal(complex1)` if (existing.find(complex1) != existing.end()) { // Make sure we don't include duplicate originals, which could // happen if a style rule extends a component of its own selector. for (size_t j = 0; j < numOriginals; j++) { if (ObjEqualityFn(result[j], complex1)) { rotateSlice(result, 0, j + 1); goto outer; } } result.insert(result.begin(), complex1); numOriginals++; continue; } // The maximum specificity of the sources that caused [complex1] // to be generated. In order for [complex1] to be removed, there // must be another selector that's a superselector of it *and* // that has specificity greater or equal to this. size_t maxSpecificity = 0; for (const SelectorComponentObj& component : complex1->elements()) { if (const CompoundSelectorObj compound = Cast(component)) { maxSpecificity = std::max(maxSpecificity, maxSourceSpecificity(compound)); } } // Look in [result] rather than [selectors] for selectors after [i]. This // ensures we aren't comparing against a selector that's already been trimmed, // and thus that if there are two identical selectors only one is trimmed. if (hasAny(result, dontTrimComplex, complex1, maxSpecificity)) { continue; } // Check if any element (up to [i]) from [selector] returns true // when passed to [dontTrimComplex]. The arguments [complex1] and // [maxSepcificity] will be passed to the invoked function. if (hasSubAny(selectors, i, dontTrimComplex, complex1, maxSpecificity)) { continue; } // ToDo: Maybe use deque for front insert? result.insert(result.begin(), complex1); } return result; } // EO trim // ########################################################################## // Returns the maximum specificity of the given [simple] source selector. // ########################################################################## size_t Extender::maxSourceSpecificity(const SimpleSelectorObj& simple) const { auto it = sourceSpecificity.find(simple); if (it == sourceSpecificity.end()) return 0; return it->second; } // EO maxSourceSpecificity(SimpleSelectorObj) // ########################################################################## // Returns the maximum specificity for sources that went into producing [compound]. // ########################################################################## size_t Extender::maxSourceSpecificity(const CompoundSelectorObj& compound) const { size_t specificity = 0; for (auto simple : compound->elements()) { size_t src = maxSourceSpecificity(simple); specificity = std::max(specificity, src); } return specificity; } // EO maxSourceSpecificity(CompoundSelectorObj) // ########################################################################## // Helper function used as callbacks on lists // ########################################################################## bool Extender::dontTrimComplex( const ComplexSelector* complex2, const ComplexSelector* complex1, const size_t maxSpecificity) { if (complex2->minSpecificity() < maxSpecificity) return false; return complex2->isSuperselectorOf(complex1); } // EO dontTrimComplex // ########################################################################## // Helper function used as callbacks on lists // ########################################################################## bool Extender::hasExactlyOne(const ComplexSelectorObj& vec) { return vec->length() == 1; } // EO hasExactlyOne // ########################################################################## // Helper function used as callbacks on lists // ########################################################################## bool Extender::hasMoreThanOne(const ComplexSelectorObj& vec) { return vec->length() > 1; } // hasMoreThanOne }