// 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<Extension>& 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<ComplexSelectorObj> 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<ComplexSelectorObj> extended;
for (size_t i = 0; i < list->length(); i++) {
const ComplexSelectorObj& complex = list->get(i);
sass::vector<ComplexSelectorObj> 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<ComplexSelectorObj> 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<ComplexSelectorObj> result;
sass::vector<sass::vector<ComplexSelectorObj>> 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<CompoundSelector>(component)) {
sass::vector<ComplexSelectorObj> 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<sass::vector<ComplexSelectorObj>>
paths = permutate(extendedNotExpanded);
for (const sass::vector<ComplexSelectorObj>& path : paths) {
// Unpack the inner complex selector to component list
sass::vector<sass::vector<SelectorComponentObj>> _paths;
for (const ComplexSelectorObj& sel : path) {
_paths.insert(_paths.end(), sel->elements());
}
sass::vector<sass::vector<SelectorComponentObj>> weaved = weave(_paths);
for (sass::vector<SelectorComponentObj>& 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<SimpleSelectorObj>& 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<ComplexSelectorObj> 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<ComplexSelectorObj> result;
// The complex selectors produced from each component of [compound].
sass::vector<sass::vector<Extension>> 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<SimpleSelectorObj> 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<Extension> 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<ComplexSelectorObj> unifiedPaths;
sass::vector<sass::vector<Extension>> prePaths = permutate(options);
for (size_t i = 0; i < prePaths.size(); i += 1) {
sass::vector<sass::vector<SelectorComponentObj>> complexes;
const sass::vector<Extension>& 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<CompoundSelector>(sel->last())) {
mergedSelector->concat(compound->elements());
}
}
complexes.push_back({ mergedSelector });
}
else {
sass::vector<SimpleSelectorObj> originals;
sass::vector<sass::vector<SelectorComponentObj>> toUnify;
for (auto& state : path) {
if (state.isOriginal) {
const ComplexSelectorObj& sel = state.extender;
if (const CompoundSelector* compound = Cast<CompoundSelector>(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<SelectorComponentObj>& 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<Extension> 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<Extension>&
values = extenders.values();
sass::vector<Extension> 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<sass::vector<Extension>> Extender::extendSimple(
const SimpleSelectorObj& simple,
const ExtSelExtMap& extensions,
const CssMediaRuleObj& mediaQueryContext,
ExtSmplSelSet* targetsUsed)
{
if (PseudoSelector* pseudo = Cast<PseudoSelector>(simple)) {
if (pseudo->selector()) {
sass::vector<sass::vector<Extension>> merged;
sass::vector<PseudoSelectorObj> extended =
extendPseudo(pseudo, extensions, mediaQueryContext);
for (PseudoSelectorObj& extend : extended) {
SimpleSelectorObj simple = extend;
sass::vector<Extension> result =
extendWithoutPseudo(simple, extensions, targetsUsed);
if (result.empty()) result = { extensionForSimple(extend) };
merged.push_back(result);
}
if (!extended.empty()) {
return merged;
}
}
}
sass::vector<Extension> result =
extendWithoutPseudo(simple, extensions, targetsUsed);
if (result.empty()) return {};
return { result };
}
// extendSimple
// ##########################################################################
// Inner loop helper for [extendPseudo] function
// ##########################################################################
sass::vector<ComplexSelectorObj> Extender::extendPseudoComplex(
const ComplexSelectorObj& complex,
const PseudoSelectorObj& pseudo,
const CssMediaRuleObj& mediaQueryContext)
{
if (complex->length() != 1) { return { complex }; }
auto compound = Cast<CompoundSelector>(complex->get(0));
if (compound == nullptr) { return { complex }; }
if (compound->length() != 1) { return { complex }; }
auto innerPseudo = Cast<PseudoSelector>(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 `<div><img></div>` but `:has(img)` does.
return { complex };
}
return {};
}
// EO extendPseudoComplex
// ##########################################################################
// Extends [pseudo] using [extensions], and returns
// a list of resulting pseudo selectors.
// ##########################################################################
sass::vector<PseudoSelectorObj> 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<ComplexSelectorObj> 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<ComplexSelectorObj> 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<PseudoSelectorObj> 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<ComplexSelectorObj>& 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<ComplexSelectorObj> Extender::trim(
const sass::vector<ComplexSelectorObj>& 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<ComplexSelectorObj> 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<CompoundSelector>(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
}