Support @at-root.

This commit is contained in:
Natalie Weizenbaum 2016-08-31 00:28:55 -07:00 committed by Natalie Weizenbaum
parent 17c5814a15
commit e607e7914f
11 changed files with 250 additions and 30 deletions

View File

@ -22,6 +22,9 @@ class CssAtRule extends CssParentNode {
/*=T*/ accept/*<T>*/(CssVisitor/*<T>*/ visitor) => visitor.visitAtRule(this);
CssAtRule copyWithoutChildren() =>
new CssAtRule(name, span, childless: isChildless, value: value);
void addChild(CssNode child) {
assert(!isChildless);
super.addChild(child);

View File

@ -17,4 +17,6 @@ class CssMediaRule extends CssParentNode {
/*=T*/ accept/*<T>*/(CssVisitor/*<T>*/ visitor) =>
visitor.visitMediaRule(this);
CssMediaRule copyWithoutChildren() => new CssMediaRule(queries, span);
}

View File

@ -27,6 +27,7 @@ abstract class CssNode extends AstNode {
}
}
// New at-rule implementations should add themselves to at-root's exclude logic.
abstract class CssParentNode extends CssNode {
final List<CssNode> children;
final List<CssNode> _children;
@ -37,6 +38,8 @@ abstract class CssParentNode extends CssNode {
: _children = children,
children = new UnmodifiableListView<CssNode>(children);
CssParentNode copyWithoutChildren();
void addChild(CssNode child) {
child._parent = this;
child._indexInParent = _children.length;

View File

@ -19,5 +19,7 @@ class CssStyleRule extends CssParentNode {
/*=T*/ accept/*<T>*/(CssVisitor/*<T>*/ visitor) =>
visitor.visitStyleRule(this);
CssStyleRule copyWithoutChildren() => new CssStyleRule(selector, span);
String toString() => "$selector {${children.join(" ")}}";
}

View File

@ -15,5 +15,7 @@ class CssStylesheet extends CssParentNode {
/*=T*/ accept/*<T>*/(CssVisitor/*<T>*/ visitor) =>
visitor.visitStylesheet(this);
CssStylesheet copyWithoutChildren() => new CssStylesheet(span);
String toString() => children.join(" ");
}

View File

@ -17,4 +17,6 @@ class CssSupportsRule extends CssParentNode {
/*=T*/ accept/*<T>*/(CssVisitor/*<T>*/ visitor) =>
visitor.visitSupportsRule(this);
CssSupportsRule copyWithoutChildren() => new CssSupportsRule(condition, span);
}

View File

@ -22,6 +22,7 @@ export 'sass/interpolation.dart';
export 'sass/media_query.dart';
export 'sass/node.dart';
export 'sass/statement.dart';
export 'sass/statement/at_root.dart';
export 'sass/statement/at_rule.dart';
export 'sass/statement/comment.dart';
export 'sass/statement/content.dart';

View File

@ -0,0 +1,73 @@
// 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:collection/collection.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
import '../../css.dart';
import '../interpolation.dart';
import '../statement.dart';
class AtRoot implements Statement {
final Interpolation query;
final List<Statement> children;
final FileSpan span;
AtRoot(Iterable<Statement> children, this.span, {this.query})
: children = new List.from(children);
/*=T*/ accept/*<T>*/(StatementVisitor/*<T>*/ visitor) =>
visitor.visitAtRoot(this);
String toString() {
var buffer = new StringBuffer("@at-root ");
if (query != null) buffer.write("$query ");
return "$buffer {${children.join(' ')}}";
}
}
class AtRootQuery {
static const defaultQuery = const AtRootQuery._default();
final bool include;
final Set<String> names;
bool get excludesMedia => _all ? !include : _excludesName("media");
bool get excludesRule => (_all || _rule) != include;
final bool _all;
final bool _rule;
AtRootQuery(this.include, Set<String> names)
: names = names,
_all = names.contains("all"),
_rule = names.contains("rule");
const AtRootQuery._default()
: include = false,
names = const UnmodifiableSetView.empty(),
_all = false,
_rule = true;
bool excludes(CssParentNode node) {
if (_all) return !include;
if (_rule && node is CssStyleRule) return !include;
return _excludesName(_nameFor(node));
}
bool _excludesName(String name) => names.contains(name) != include;
String _nameFor(CssParentNode node) {
if (node is CssMediaRule) return "media";
if (node is CssSupportsRule) return "supports";
if (node is CssAtRule) return node.name.toLowerCase();
return null;
}
}

View File

@ -65,6 +65,24 @@ class Parser {
return simple;
}
AtRootQuery parseAtRootQuery() {
_scanner.expectChar($lparen);
_ignoreComments();
_expectCaseInsensitive("with");
var include = !_scanCaseInsensitive("out");
_ignoreComments();
_scanner.expectChar($colon);
_ignoreComments();
var atRules = new Set<String>();
do {
atRules.add(_identifier().toLowerCase());
_ignoreComments();
} while (_lookingAtIdentifier());
return new AtRootQuery(include, atRules);
}
VariableDeclaration _variableDeclaration() {
if (!_scanner.scanChar($dollar)) return null;
@ -294,6 +312,8 @@ class Parser {
var name = _atRuleName();
switch (name) {
case "at-root":
return _atRoot(start);
case "content":
return _content(start);
case "extend":
@ -354,6 +374,14 @@ class Parser {
return name;
}
AtRoot _atRoot(LineScannerState start) {
var next = _scanner.peekChar();
var query = next == $hash || next == $lparen ? _queryExpression() : null;
_ignoreComments();
return new AtRoot(_children(_topLevelStatement), _scanner.spanFrom(start),
query: query);
}
Content _content(LineScannerState start) {
if (_inMixin) {
_mixinHasContent = true;
@ -1122,6 +1150,34 @@ class Parser {
return expression;
}
/// A query expression of the form `(foo: bar)`.
Interpolation _queryExpression() {
if (_scanner.peekChar() == $hash) {
var interpolation = _singleInterpolation();
return new Interpolation([interpolation], interpolation.span);
}
var start = _scanner.state;
var buffer = new InterpolationBuffer();
_scanner.expectChar($lparen);
buffer.writeCharCode($lparen);
_ignoreComments();
buffer.add(_expression());
if (_scanner.scanChar($colon)) {
_ignoreComments();
buffer.writeCharCode($colon);
buffer.writeCharCode($space);
buffer.add(_expression());
}
_scanner.expectChar($rparen);
_ignoreComments();
buffer.writeCharCode($rparen);
return buffer.interpolation(_scanner.spanFrom(start));
}
// ## Selectors
SelectorList _selectorList() {
@ -1483,7 +1539,7 @@ class Parser {
var features = <Interpolation>[];
do {
_ignoreComments();
features.add(_mediaExpression());
features.add(_queryExpression());
_ignoreComments();
} while (_scanCaseInsensitive("and"));
@ -1494,33 +1550,6 @@ class Parser {
}
}
Interpolation _mediaExpression() {
if (_scanner.peekChar() == $hash) {
var interpolation = _singleInterpolation();
return new Interpolation([interpolation], interpolation.span);
}
var start = _scanner.state;
var buffer = new InterpolationBuffer();
_scanner.expectChar($lparen);
buffer.writeCharCode($lparen);
_ignoreComments();
buffer.add(_expression());
if (_scanner.scanChar($colon)) {
_ignoreComments();
buffer.writeCharCode($colon);
buffer.writeCharCode($space);
buffer.add(_expression());
}
_scanner.expectChar($rparen);
_ignoreComments();
buffer.writeCharCode($rparen);
return buffer.interpolation(_scanner.spanFrom(start));
}
// ## Supports Conditions
SupportsCondition _supportsCondition() {
@ -1536,7 +1565,7 @@ class Parser {
var condition = _supportsConditionInParens();
_ignoreComments();
while (_lookingAtInterpolatedIdentifier()) {
while (_lookingAtIdentifier()) {
String operator;
if (_scanCaseInsensitive("or")) {
operator = "or";
@ -1801,13 +1830,22 @@ class Parser {
if (first == $hash) return _scanner.peekChar(1) == $lbrace;
if (first != $dash) return false;
var second = _scanner.peekChar();
var second = _scanner.peekChar(1);
if (isNameStart(second) || second == $dash || second == $backslash) {
return true;
}
return second == $hash && _scanner.peekChar(2) == $lbrace;
}
bool _lookingAtIdentifier() {
var first = _scanner.peekChar();
if (isNameStart(first) || first == $backslash) return true;
if (first != $dash) return false;
var second = _scanner.peekChar(1);
return isNameStart(second) || second == $dash || second == $backslash;
}
bool _lookingAtExpression() {
var character = _scanner.peekChar();
if (character == null) return false;

View File

@ -5,6 +5,7 @@
import '../../ast/sass.dart';
abstract class StatementVisitor<T> {
T visitAtRoot(AtRoot node);
T visitAtRule(AtRule node);
T visitComment(Comment node);
T visitContent(Content node);

View File

@ -20,6 +20,8 @@ import '../value.dart';
import 'interface/statement.dart';
import 'interface/expression.dart';
typedef _ScopeCallback(callback());
class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
final List<String> _loadPaths;
@ -69,6 +71,92 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
return _root;
}
void visitAtRoot(AtRoot node) {
var query = node.query == null
? AtRootQuery.defaultQuery
: new Parser(_performInterpolation(node.query)).parseAtRootQuery();
var parent = _parent;
var included = <CssParentNode>[];
while (parent is! CssStylesheet) {
if (!query.excludes(parent)) included.add(parent);
parent = parent.parent;
}
var root = _trimIncluded(included);
// If we didn't exclude any rules, we don't need to use the copies we might
// have created.
if (root == _parent) {
for (var child in node.children) {
child.accept(this);
}
return;
}
var innerCopy =
included.isEmpty ? null : included.first.copyWithoutChildren();
var outerCopy = innerCopy;
for (var node in included.skip(1)) {
var copy = node.copyWithoutChildren();
copy.addChild(outerCopy);
outerCopy = copy;
}
if (outerCopy != null) root.addChild(outerCopy);
_scopeForAtRule(innerCopy ?? root, query)(() {
for (var child in node.children) {
child.accept(this);
}
});
if (innerCopy == null) return;
while (innerCopy != root && innerCopy.children.isEmpty) {
innerCopy.remove();
innerCopy = innerCopy.parent;
}
}
CssParentNode _trimIncluded(List<CssParentNode> nodes) {
var parent = _parent;
int innermostContiguous;
var i = 0;
for (; i < nodes.length; i++) {
while (parent != nodes[i]) {
innermostContiguous = null;
parent = parent.parent;
}
innermostContiguous ??= i;
parent = parent.parent;
}
if (parent != _root) return _root;
var root = nodes[innermostContiguous];
nodes.removeRange(innermostContiguous, nodes.length);
return root;
}
_ScopeCallback _scopeForAtRule(CssNode newParent, AtRootQuery query) {
var scope = (callback()) {
// We can't use [_withParent] here because it'll add the node to the tree
// in the wrong place.
var oldParent = _parent;
_parent = newParent;
_environment.scope(callback);
_parent = oldParent;
};
if (query.excludesMedia) {
var innerScope = scope;
scope = (callback) => _withMediaQueries(null, () => innerScope(callback));
}
if (query.excludesRule) {
var innerScope = scope;
scope = (callback) => _withSelector(null, () => innerScope(callback));
}
return scope;
}
void visitComment(Comment node) {
if (node.isSilent) return;
_parent.addChild(new CssComment(node.text, node.span));
@ -86,6 +174,11 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
}
void visitDeclaration(Declaration node) {
if (_selector == null) {
throw node.span
.message("Declarations may only be used within style rules.");
}
var name = _interpolationToValue(node.name);
if (_declarationName != null) {
name = new CssValue("$_declarationName-${name.value}", name.span);