Ignore duplicate extensions.

Closes #76
This commit is contained in:
Natalie Weizenbaum 2016-12-20 17:30:20 -08:00
parent 1d68c7a1c4
commit 69762ff0b0
3 changed files with 82 additions and 50 deletions

View File

@ -10,7 +10,7 @@ import '../ast/css.dart';
import '../ast/selector.dart';
import '../ast/sass.dart';
import '../exception.dart';
import 'source.dart';
import 'state.dart';
import 'functions.dart';
/// Tracks style rules and extensions, and applies the latter to the former.
@ -23,7 +23,7 @@ class Extender {
/// A map from all extended simple selectors to the sources of those
/// extensions.
final _extensions = <SimpleSelector, Set<ExtendSource>>{};
final _extensions = <SimpleSelector, Map<SelectorList, ExtendState>>{};
/// An expando from [SimpleSelector]s to integers.
///
@ -50,14 +50,18 @@ class Extender {
/// the stylesheet.
static SelectorList extend(
SelectorList selector, SelectorList source, SimpleSelector target) =>
new Extender()._extendList(
selector, {target: new Set()..add(new ExtendSource(source, null))});
new Extender()._extendList(selector, {
target: {source: new ExtendState.optional()}
});
/// Returns a copy of [selector] with [source] replaced by [target].
static SelectorList replace(
SelectorList selector, SelectorList source, SimpleSelector target) =>
new Extender()._extendList(
selector, {target: new Set()..add(new ExtendSource(source, null))},
selector,
{
target: {source: new ExtendState.optional()}
},
replace: true);
/// Adds [selector] to this extender, associated with [span].
@ -105,6 +109,21 @@ class Extender {
/// provides the extend span and indicates whether the extension is optional.
void addExtension(
SelectorList extender, SimpleSelector target, ExtendRule extend) {
var sources = _extensions.putIfAbsent(target, () => {});
var existingState = sources[extender];
if (existingState != null) {
// 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.
if (!extend.isOptional) existingState.makeMandatory(extend.span);
return;
}
var state = extend.isOptional
? new ExtendState.optional()
: new ExtendState.mandatory(extend.span);
sources[extender] = state;
for (var complex in extender.components) {
for (var component in complex.components) {
if (component is CompoundSelector) {
@ -115,13 +134,11 @@ class Extender {
}
}
var source = new ExtendSource(extender, extend.span);
source.isUsed = extend.isOptional;
_extensions.putIfAbsent(target, () => new Set()).add(source);
var rules = _selectors[target];
if (rules == null) return;
var extensions = {target: new Set<ExtendSource>()..add(source)};
var extensions = {
target: {extender: state}
};
for (var rule in rules.toList()) {
rule.selector.value = _extendList(rule.selector.value, extensions);
_registerSelector(rule.selector.value, rule);
@ -132,21 +149,21 @@ class Extender {
/// any selectors.
void finalize() {
_extensions.forEach((target, sources) {
for (var source in sources) {
if (source.isUsed) continue;
sources.forEach((_, state) {
if (state.isOptional || state.isUsed) return;
throw new SassException(
'The target selector was not found.\n'
'Use "@extend $target !optional" to avoid this error.',
source.span);
}
state.span);
});
});
}
/// Extends [list] using [extensions].
///
/// If [replace] is `true`, this doesn't preserve the original selectors.
SelectorList _extendList(
SelectorList list, Map<SimpleSelector, Set<ExtendSource>> extensions,
SelectorList _extendList(SelectorList list,
Map<SimpleSelector, Map<SelectorList, ExtendState>> extensions,
{bool replace: false}) {
// This could be written more simply using [List.map], but we want to avoid
// any allocations in the common case where no extends apply.
@ -172,7 +189,7 @@ class Extender {
///
/// If [replace] is `true`, this doesn't preserve the original selectors.
List<List<ComplexSelector>> _extendComplex(ComplexSelector complex,
Map<SimpleSelector, Set<ExtendSource>> extensions,
Map<SimpleSelector, Map<SelectorList, ExtendState>> extensions,
{bool replace: false}) {
// This could be written more simply using [List.map], but we want to avoid
// any allocations in the common case where no extends apply.
@ -237,7 +254,7 @@ class Extender {
///
/// If [replace] is `true`, this doesn't preserve the original selectors.
List<ComplexSelector> _extendCompound(CompoundSelector compound,
Map<SimpleSelector, Set<ExtendSource>> extensions,
Map<SimpleSelector, Map<SelectorList, ExtendState>> extensions,
{bool replace: false}) {
var original = compound;
@ -267,8 +284,8 @@ class Extender {
compoundWithoutSimple.setRange(0, i, compound.components);
compoundWithoutSimple.setRange(
i, compound.components.length - 1, compound.components, i + 1);
for (var source in sources) {
for (var complex in source.extender.components) {
sources.forEach((extender, state) {
for (var complex in extender.components) {
var extenderBase = complex.components.last as CompoundSelector;
var unified = compoundWithoutSimple.isEmpty
? extenderBase
@ -291,9 +308,9 @@ class Extender {
math.max(
_sourceSpecificityFor(compound), complex.maxSpecificity));
extended.add(newComplex);
source.isUsed = true;
state.isUsed = true;
}
}
});
}
if (extended == null) {
@ -317,8 +334,8 @@ class Extender {
/// pseudo selectors.
///
/// If [replace] is `true`, this doesn't preserve the original selectors.
List<PseudoSelector> _extendPseudo(
PseudoSelector pseudo, Map<SimpleSelector, Set<ExtendSource>> extensions,
List<PseudoSelector> _extendPseudo(PseudoSelector pseudo,
Map<SimpleSelector, Map<SelectorList, ExtendState>> extensions,
{bool replace: false}) {
var extended = _extendList(pseudo.selector, extensions, replace: replace);
if (extended == null) return null;

View File

@ -1,26 +0,0 @@
// 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.
import 'package:source_span/source_span.dart';
import '../ast/selector.dart';
/// The source of an `@extend` declaration.
class ExtendSource {
/// The selector for the style rule in which this `@extend` was declared.
final SelectorList extender;
/// The span for the `@extend` rule that declared this extension.
final FileSpan span;
/// Whether this extension matched a selector, or was marked optional.
var isUsed = false;
ExtendSource(this.extender, this.span);
int get hashCode => extender.hashCode;
bool operator ==(Object other) =>
other is ExtendSource && other.extender == extender;
}

41
lib/src/extend/state.dart Normal file
View File

@ -0,0 +1,41 @@
// 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.
import 'package:source_span/source_span.dart';
/// The state of an extension for a given source and target.
///
/// The source and target are represented externally, in the nested map that
/// contains this state.
class ExtendState {
/// Whether this extension is optional.
bool get isOptional => _span == null;
/// Whether this extension matched a selector.
var isUsed = false;
/// The span for the `@extend` rule that should produce an error if this
/// extension doesn't match anything.
///
/// This is `null` if and only if this extension is optional.
FileSpan get span => _span;
FileSpan _span;
/// Creates a new optional extend state.
ExtendState.optional();
/// Creates a new mandatory extend state.
///
/// The [span] is used in the error that's thrown if this extension doesn't
/// match anything.
ExtendState.mandatory(this._span);
/// Marks this extension as mandatory.
///
/// The [span] is used in the error that's thrown if this extension doesn't
/// match anything.
void makeMandatory(FileSpan span) {
_span = span;
}
}