dart-sass/lib/src/extend/extender.dart

381 lines
14 KiB
Dart
Raw Normal View History

2016-07-16 02:27:14 +02:00
// Copyright 2016 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
2016-08-13 00:24:27 +02:00
import 'dart:math' as math;
2016-08-05 02:25:57 +02:00
import 'package:source_span/source_span.dart';
2016-08-28 01:12:17 +02:00
import '../ast/css.dart';
2016-08-05 02:04:37 +02:00
import '../ast/selector.dart';
2016-09-03 16:09:15 +02:00
import '../ast/sass.dart';
2016-09-03 16:42:29 +02:00
import '../exception.dart';
2016-09-03 12:29:01 +02:00
import 'source.dart';
2016-08-05 02:04:37 +02:00
import 'functions.dart';
2016-07-16 02:27:14 +02:00
class Extender {
/// A map from all simple selectors in the stylesheet to the rules that
/// contain them.
///
/// This is used to find which rules an `@extend` applies to.
final _selectors = <SimpleSelector, Set<CssStyleRule>>{};
2016-09-03 12:29:01 +02:00
final _extensions = <SimpleSelector, Set<ExtendSource>>{};
2016-07-16 02:27:14 +02:00
final _sources = new Expando<ComplexSelector>();
2016-07-16 02:27:14 +02:00
2016-09-22 20:45:56 +02:00
static SelectorList extend(
SelectorList selector, SelectorList source, SimpleSelector target) =>
new Extender()._extendList(
selector, {target: new Set()..add(new ExtendSource(source, null))});
2016-09-22 23:09:35 +02:00
static SelectorList replace(
SelectorList selector, SelectorList source, SimpleSelector target) =>
new Extender()._extendList(
selector, {target: new Set()..add(new ExtendSource(source, null))},
replace: true);
2016-09-24 22:05:12 +02:00
CssStyleRule addSelector(CssValue<SelectorList> selector, FileSpan span) {
for (var complex in selector.value.components) {
2016-08-05 00:39:53 +02:00
for (var component in complex.components) {
if (component is CompoundSelector) {
for (var simple in component.components) {
2016-08-05 02:25:57 +02:00
_sources[simple] = complex;
2016-08-05 00:39:53 +02:00
}
}
}
}
2016-09-22 22:58:10 +02:00
if (_extensions.isNotEmpty) {
2016-09-24 22:05:12 +02:00
selector =
new CssValue(_extendList(selector.value, _extensions), selector.span);
2016-09-22 22:58:10 +02:00
}
2016-09-24 22:05:12 +02:00
var rule = new CssStyleRule(selector, span);
_registerSelector(selector.value, rule);
2016-07-16 02:27:14 +02:00
2016-09-24 22:05:12 +02:00
return rule;
}
void _registerSelector(SelectorList list, CssStyleRule rule) {
for (var complex in list.components) {
2016-07-29 02:33:09 +02:00
for (var component in complex.components) {
if (component is CompoundSelector) {
for (var simple in component.components) {
2016-07-16 02:27:14 +02:00
_selectors.putIfAbsent(simple, () => new Set()).add(rule);
2016-09-24 22:05:12 +02:00
if (simple is PseudoSelector && simple.selector != null) {
_registerSelector(simple.selector, rule);
}
2016-07-16 02:27:14 +02:00
}
}
}
}
}
2016-09-03 12:29:01 +02:00
void addExtension(
2016-09-07 16:10:37 +02:00
SelectorList sourceList, SimpleSelector target, ExtendRule extend) {
2016-09-03 16:09:15 +02:00
var source = new ExtendSource(sourceList, extend.span);
source.isUsed = extend.isOptional;
2016-09-03 12:29:01 +02:00
_extensions.putIfAbsent(target, () => new Set()).add(source);
2016-08-05 00:39:53 +02:00
var rules = _selectors[target];
if (rules == null) return;
2016-09-03 12:29:01 +02:00
var extensions = {target: new Set()..add(source)};
2016-08-05 00:39:53 +02:00
for (var rule in rules) {
var list = rule.selector.value;
2016-08-05 02:25:57 +02:00
rule.selector.value = _extendList(list, extensions);
2016-08-05 00:39:53 +02:00
}
}
2016-09-03 12:29:01 +02:00
void finalize() {
for (var sources in _extensions.values) {
for (var source in sources) {
if (source.isUsed) continue;
2016-09-03 16:42:29 +02:00
throw new SassException(
'The target selector was not found.\n'
'Use "@extend %foo !optional" to avoid this error.',
source.span);
2016-09-03 12:29:01 +02:00
}
}
}
2016-08-29 00:04:48 +02:00
SelectorList _extendList(
2016-09-22 23:09:35 +02:00
SelectorList list, Map<SimpleSelector, Set<ExtendSource>> extensions,
{bool replace: false}) {
2016-07-16 02:27:14 +02:00
// This could be written more simply using [List.map], but we want to avoid
// any allocations in the common case where no extends apply.
var changed = false;
2016-07-29 02:33:09 +02:00
List<ComplexSelector> newList;
for (var i = 0; i < list.components.length; i++) {
var complex = list.components[i];
2016-09-22 23:09:35 +02:00
var extended = _extendComplex(complex, extensions, replace: replace);
2016-07-16 02:27:14 +02:00
if (extended == null) {
2016-07-29 02:33:09 +02:00
if (changed) newList.add(complex);
2016-07-16 02:27:14 +02:00
} else {
2016-07-29 02:33:09 +02:00
if (!changed) newList = list.components.take(i).toList();
2016-07-16 02:27:14 +02:00
changed = true;
2016-07-29 02:33:09 +02:00
newList.addAll(extended);
2016-07-16 02:27:14 +02:00
}
}
2016-07-29 02:33:09 +02:00
if (!changed) return list;
2016-07-16 02:27:14 +02:00
2016-07-29 02:33:09 +02:00
return new SelectorList(newList.where((complex) => complex != null));
2016-07-16 02:27:14 +02:00
}
Iterable<ComplexSelector> _extendComplex(ComplexSelector complex,
2016-09-22 23:09:35 +02:00
Map<SimpleSelector, Set<ExtendSource>> extensions,
{bool replace: false}) {
2016-07-16 02:27:14 +02:00
// This could be written more simply using [List.map], but we want to avoid
// any allocations in the common case where no extends apply.
var changed = false;
2016-09-24 17:30:08 +02:00
List<List<ComplexSelector>> extendedNotExpanded;
2016-07-29 02:33:09 +02:00
for (var i = 0; i < complex.components.length; i++) {
var component = complex.components[i];
2016-07-16 02:27:14 +02:00
if (component is CompoundSelector) {
2016-09-22 23:09:35 +02:00
var extended = _extendCompound(component, extensions, replace: replace);
2016-07-16 02:27:14 +02:00
if (extended == null) {
2016-09-22 23:04:34 +02:00
if (changed) {
2016-08-29 00:04:48 +02:00
extendedNotExpanded.add([
2016-09-24 17:30:08 +02:00
new ComplexSelector([component])
2016-08-29 00:04:48 +02:00
]);
2016-09-22 23:04:34 +02:00
}
2016-07-16 02:27:14 +02:00
} else {
if (!changed) {
extendedNotExpanded = complex.components
.take(i)
2016-08-29 00:04:48 +02:00
.map((component) => [
2016-09-24 17:30:08 +02:00
new ComplexSelector([component],
lineBreak: complex.lineBreak)
2016-08-29 00:04:48 +02:00
])
.toList();
2016-07-16 02:27:14 +02:00
}
changed = true;
extendedNotExpanded.add(extended);
}
} else {
2016-09-22 23:04:34 +02:00
if (changed) {
2016-08-29 00:04:48 +02:00
extendedNotExpanded.add([
2016-09-24 17:30:08 +02:00
new ComplexSelector([component])
2016-08-29 00:04:48 +02:00
]);
2016-09-22 23:04:34 +02:00
}
2016-07-16 02:27:14 +02:00
}
}
if (!changed) return null;
2016-09-24 17:30:08 +02:00
return _trim(paths(extendedNotExpanded).map((path) {
return weave(path.map((complex) => complex.components).toList())
.map((outputComplex) {
return new ComplexSelector(outputComplex,
lineBreak: complex.lineBreak ||
path.any((inputComplex) => inputComplex.lineBreak));
});
}).toList());
2016-07-16 02:27:14 +02:00
}
2016-09-24 17:30:08 +02:00
List<ComplexSelector> _extendCompound(CompoundSelector compound,
2016-09-22 23:09:35 +02:00
Map<SimpleSelector, Set<ExtendSource>> extensions,
{bool replace: false}) {
2016-09-24 22:05:12 +02:00
var original = compound;
2016-09-24 17:30:08 +02:00
List<ComplexSelector> extended;
2016-07-29 02:33:09 +02:00
for (var i = 0; i < compound.components.length; i++) {
var simple = compound.components[i];
2016-07-16 02:27:14 +02:00
2016-09-24 22:05:12 +02:00
var extendedPseudo = simple is PseudoSelector && simple.selector != null
? _extendPseudo(simple, extensions, replace: replace)
: null;
if (extendedPseudo != null) {
var simples = new List<SimpleSelector>(
compound.components.length - 1 + extendedPseudo.length);
simples.setRange(0, i, compound.components);
simples.setRange(i, i + extendedPseudo.length, extendedPseudo);
simples.setRange(
i + extendedPseudo.length, simples.length, compound.components, i);
original = new CompoundSelector(simples);
}
2016-07-16 02:27:14 +02:00
2016-09-03 12:29:01 +02:00
var sources = extensions[simple];
if (sources == null) continue;
2016-07-16 02:27:14 +02:00
2016-09-24 22:05:12 +02:00
var compoundWithoutSimple =
new List<SimpleSelector>(compound.components.length - 1);
compoundWithoutSimple.setRange(0, i, compound.components);
compoundWithoutSimple.setRange(
i, compound.components.length - 1, compound.components, i);
2016-09-03 12:29:01 +02:00
for (var source in sources) {
for (var complex in source.extender.components) {
2016-08-05 02:25:57 +02:00
var extenderBase = complex.components.last as CompoundSelector;
2016-08-14 22:57:27 +02:00
var unified = compoundWithoutSimple.isEmpty
? extenderBase
2016-09-22 23:50:25 +02:00
: unifyCompound(extenderBase.components, compoundWithoutSimple);
2016-07-16 02:27:14 +02:00
if (unified == null) continue;
2016-09-24 22:05:12 +02:00
extended ??= replace
? []
: [
new ComplexSelector([compound])
];
2016-09-24 17:30:08 +02:00
extended.add(new ComplexSelector(
complex.components.take(complex.components.length - 1).toList()
..add(unified),
lineBreak: complex.lineBreak));
2016-09-03 12:29:01 +02:00
source.isUsed = true;
2016-07-16 02:27:14 +02:00
}
}
}
2016-09-24 22:05:12 +02:00
if (extended == null) {
return identical(original, compound)
? null
: [
new ComplexSelector([original])
];
} else if (!identical(original, compound)) {
if (replace) {
extended.insert(0, new ComplexSelector([original]));
} else {
extended[0] = new ComplexSelector([original]);
}
}
2016-07-16 02:27:14 +02:00
return extended;
}
2016-09-24 22:05:12 +02:00
List<PseudoSelector> _extendPseudo(
PseudoSelector pseudo, Map<SimpleSelector, Set<ExtendSource>> extensions,
{bool replace: false}) {
// TODO: avoid recursive loops when extending.
var extended = _extendList(pseudo.selector, extensions, replace: replace);
if (extended == null) return null;
// TODO: what do we do about placeholders in the selector? If we just
// eliminate them here, what happens to future extends?
// 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.
Iterable<ComplexSelector> complexes = extended.components;
if (pseudo.normalizedName == "not" &&
!pseudo.selector.components
.any((complex) => complex.components.length > 1) &&
extended.components.any((complex) => complex.components.length == 1)) {
complexes = extended.components
.where((complex) => complex.components.length <= 1);
}
complexes = complexes.expand((complex) {
if (complex.components.length != 1) return [complex];
if (complex.components.first is! CompoundSelector) return [complex];
var compound = complex.components.first as CompoundSelector;
if (compound.components.length != 1) return [complex];
if (compound.components.first is! PseudoSelector) return [complex];
var innerPseudo = compound.components.first as PseudoSelector;
if (innerPseudo.selector == null) return [complex];
switch (pseudo.normalizedName) {
case '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.normalizedName != 'matches') return [];
return innerPseudo.selector.components;
case 'matches':
case 'any':
case 'current':
case 'nth-child':
case '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 (innerPseudo.argument != pseudo.argument) return [];
return innerPseudo.selector.components;
case 'has':
case 'host':
case 'host-context':
// 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];
default:
return [];
}
});
// 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.normalizedName == 'not' &&
pseudo.selector.components.length == 1) {
return complexes
.map((complex) => pseudo.withSelector(new SelectorList([complex])))
.toList();
} else {
return [pseudo.withSelector(new SelectorList(complexes))];
}
}
2016-09-24 17:30:08 +02:00
List<ComplexSelector> _trim(List<List<ComplexSelector>> lists) {
2016-07-29 00:45:29 +02:00
// 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.
2016-08-13 00:24:27 +02:00
if (lists.length > 100) return lists.expand((selectors) => selectors);
2016-07-29 00:45:29 +02:00
// This is n² on the sequences, but only comparing between separate
// sequences should limit the quadratic behavior.
2016-09-24 17:30:08 +02:00
var result = <ComplexSelector>[];
2016-07-29 02:33:09 +02:00
for (var i = 0; i < lists.length; i++) {
for (var complex1 in lists[i]) {
// 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.
var maxSpecificity = 0;
2016-09-24 17:30:08 +02:00
for (var component in complex1.components) {
if (component is CompoundSelector) {
for (var simple in component.components) {
var source = _sources[simple];
if (source == null) continue;
maxSpecificity = math.max(maxSpecificity, source.maxSpecificity);
}
}
}
2016-07-29 00:45:29 +02:00
2016-07-29 02:33:09 +02:00
// Look in [result] rather than [lists] for selectors before [i]. This
2016-07-29 00:45:29 +02:00
// ensures that 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.
2016-07-29 02:33:09 +02:00
if (result.any((complex2) =>
2016-09-24 17:30:08 +02:00
complex2.minSpecificity >= maxSpecificity &&
complex2.isSuperselector(complex1))) {
2016-08-13 00:24:27 +02:00
continue;
2016-07-29 00:45:29 +02:00
}
2016-08-13 00:24:27 +02:00
// We intentionally don't compare [complex1] against other selectors in
// `lists[i]`, since they come from the same source.
if (lists.skip(i + 1).any((list) => list.any((complex2) =>
2016-09-24 17:30:08 +02:00
complex2.minSpecificity >= maxSpecificity &&
complex2.isSuperselector(complex1)))) {
2016-08-13 00:24:27 +02:00
continue;
2016-07-29 00:45:29 +02:00
}
2016-07-29 02:33:09 +02:00
result.add(complex1);
2016-07-29 00:45:29 +02:00
}
}
return result;
}
2016-07-16 02:27:14 +02:00
}