mirror of
https://github.com/danog/dart-sass.git
synced 2025-01-21 21:31:11 +01:00
Support @at-root.
This commit is contained in:
parent
17c5814a15
commit
e607e7914f
@ -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);
|
||||
|
@ -17,4 +17,6 @@ class CssMediaRule extends CssParentNode {
|
||||
|
||||
/*=T*/ accept/*<T>*/(CssVisitor/*<T>*/ visitor) =>
|
||||
visitor.visitMediaRule(this);
|
||||
|
||||
CssMediaRule copyWithoutChildren() => new CssMediaRule(queries, span);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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(" ")}}";
|
||||
}
|
||||
|
@ -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(" ");
|
||||
}
|
||||
|
@ -17,4 +17,6 @@ class CssSupportsRule extends CssParentNode {
|
||||
|
||||
/*=T*/ accept/*<T>*/(CssVisitor/*<T>*/ visitor) =>
|
||||
visitor.visitSupportsRule(this);
|
||||
|
||||
CssSupportsRule copyWithoutChildren() => new CssSupportsRule(condition, span);
|
||||
}
|
||||
|
@ -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';
|
||||
|
73
lib/src/ast/sass/statement/at_root.dart
Normal file
73
lib/src/ast/sass/statement/at_root.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user