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
|
|
|
|
2016-08-05 00:14:09 +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-08-29 00:04:48 +02:00
|
|
|
CssStyleRule addSelector(
|
|
|
|
CssValue<SelectorList> selectorValue, FileSpan span) {
|
2016-08-13 00:24:27 +02:00
|
|
|
var selector = selectorValue.value;
|
2016-08-13 00:33:36 +02:00
|
|
|
for (var complex in selector.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) {
|
|
|
|
selector = _extendList(selector, _extensions);
|
|
|
|
selectorValue = new CssValue(selector, selectorValue.span);
|
|
|
|
}
|
2016-08-28 23:53:20 +02:00
|
|
|
var rule = new CssStyleRule(selectorValue, span);
|
2016-07-16 02:27:14 +02:00
|
|
|
|
|
|
|
for (var complex in selector.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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return rule;
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2016-08-13 02:51:17 +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) {
|
2016-08-13 02:51:17 +02:00
|
|
|
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
|
|
|
])
|
2016-08-13 02:51:17 +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-07-16 02:27:14 +02:00
|
|
|
var changed = false;
|
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-08 03:31:52 +02:00
|
|
|
// TODO: handle extending into pseudo selectors
|
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-08-29 00:04:48 +02:00
|
|
|
var compoundWithoutSimple = compound.components.toList()..removeAt(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-03 12:29:01 +02:00
|
|
|
if (!changed) {
|
2016-09-22 23:09:35 +02:00
|
|
|
extended = replace
|
|
|
|
? []
|
|
|
|
: [
|
2016-09-24 17:30:08 +02:00
|
|
|
new ComplexSelector([compound])
|
2016-09-22 23:09:35 +02:00
|
|
|
];
|
2016-09-03 12:29:01 +02:00
|
|
|
}
|
2016-07-16 02:27:14 +02:00
|
|
|
changed = true;
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return extended;
|
|
|
|
}
|
|
|
|
|
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
|
2016-08-05 00:14:09 +02:00
|
|
|
// 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) {
|
2016-08-05 00:14:09 +02:00
|
|
|
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
|
|
|
}
|