Support @for.

This commit is contained in:
Natalie Weizenbaum 2016-09-07 08:40:58 -07:00 committed by Natalie Weizenbaum
parent 202fcad40f
commit 80f0afb484
8 changed files with 164 additions and 12 deletions

View File

@ -30,6 +30,7 @@ export 'sass/statement/debug_rule.dart';
export 'sass/statement/declaration.dart';
export 'sass/statement/error_rule.dart';
export 'sass/statement/extend_rule.dart';
export 'sass/statement/for_rule.dart';
export 'sass/statement/function_rule.dart';
export 'sass/statement/if_rule.dart';
export 'sass/statement/import_rule.dart';

View File

@ -0,0 +1,36 @@
// 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 '../../../visitor/interface/statement.dart';
import '../expression.dart';
import '../statement.dart';
class ForRule implements Statement {
final String variable;
final Expression from;
final Expression to;
final bool isExclusive;
final List<Statement> children;
final FileSpan span;
ForRule(this.variable, this.from, this.to, Iterable<Statement> children,
this.span,
{bool exclusive: true})
: children = new List.unmodifiable(children),
isExclusive = exclusive;
/*=T*/ accept/*<T>*/(StatementVisitor/*<T>*/ visitor) =>
visitor.visitForRule(this);
String toString() =>
"@for \$$variable from $from ${isExclusive ? 'to' : 'through'} $to "
"{${children.join(" ")}}";
}

View File

@ -13,6 +13,7 @@ class Environment {
/// Base is global scope.
final List<Map<String, Value>> _variables;
// Note: this is not necessarily complete
final Map<String, int> _variableIndices;
final List<Map<String, Callable>> _functions;
@ -68,8 +69,23 @@ class Environment {
Environment global() => new Environment._([_variables.first], {},
[_functions.first], {}, [_mixins.first], {}, null, null);
Value getVariable(String name) =>
_variables[_variableIndices[name] ?? 0][name];
Value getVariable(String name) {
var index = _variableIndices[name];
if (index != null) _variables[index][name];
index = _variableIndex(name);
if (index == null) return null;
_variableIndices[name] = index;
return _variables[index][name];
}
int _variableIndex(String name) {
for (var i = _variables.length - 1; i >= 0; i--) {
if (_variables[i].containsKey(name)) return i;
}
return null;
}
void setVariable(String name, Value value, {bool global: false}) {
var index = global || _variables.length == 1
@ -79,6 +95,12 @@ class Environment {
_variables[index][name] = value;
}
void setLocalVariable(String name, Value value) {
var index = _variables.length - 1;
_variableIndices[name] = index;
_variables[index][name] = value;
}
Callable getFunction(String name) {
var index = _functionIndices[name];
if (index != null) _functions[index][name];
@ -135,7 +157,7 @@ class Environment {
}
/*=T*/ scope/*<T>*/(/*=T*/ callback(), {bool semiGlobal: false}) {
assert(!semiGlobal || _inSemiGlobalScope || _variables.length == 1);
semiGlobal = semiGlobal && (_inSemiGlobalScope || _variables.length == 1);
// TODO: avoid creating a new scope if no variables are declared.
var wasInSemiGlobalScope = _inSemiGlobalScope;

View File

@ -94,10 +94,8 @@ class Parser {
}
VariableDeclaration _variableDeclaration() {
if (!_scanner.scanChar($dollar)) return null;
var start = _scanner.state;
var name = _identifier();
var name = _variableName();
_ignoreComments();
_scanner.expectChar($colon);
_ignoreComments();
@ -333,6 +331,8 @@ class Parser {
return _errorRule(start);
case "extend":
return _extendRule(start);
case "for":
return _forRule(start, child);
case "function":
return _functionRule(start);
case "if":
@ -367,6 +367,8 @@ class Parser {
return _debugRule(start);
case "error":
return _errorRule(start);
case "for":
return _forRule(start, _declarationAtRule);
case "if":
return _ifRule(start, _declarationChild);
case "include":
@ -385,6 +387,8 @@ class Parser {
return _debugRule(start);
case "error":
return _errorRule(start);
case "for":
return _forRule(start, _functionAtRule);
case "if":
return _ifRule(start, _functionAtRule);
case "return":
@ -456,6 +460,32 @@ class Parser {
name, arguments, children, _scanner.spanFrom(start));
}
ForRule _forRule(LineScannerState start, Statement child()) {
var wasInControlDirective = _inControlDirective;
_inControlDirective = true;
var variable = _variableName();
_ignoreComments();
_scanner.expect("from");
_ignoreComments();
var from = _expressionUntil(() {
if (!_scanner.matches("to") && !_scanner.matches("through")) return false;
var after = _scanner.peekChar(_scanner.lastMatch[0].length);
return after == null || isWhitespace(after);
});
var exclusive = _scanner.scan("to");
if (!exclusive) _scanner.expect("through", name: '"to" or "through"');
_ignoreComments();
var to = _expression();
var children = _children(child);
_inControlDirective = wasInControlDirective;
return new ForRule(variable, from, to, children, _scanner.spanFrom(start),
exclusive: exclusive);
}
IfRule _ifRule(LineScannerState start, Statement child()) {
var wasInControlDirective = _inControlDirective;
_inControlDirective = true;
@ -676,6 +706,31 @@ class Parser {
return new ListExpression(commaExpressions, ListSeparator.comma);
}
Expression _expressionUntil(bool isDone()) {
if (isDone()) _scanner.error("Expected expression.");
var first = _singleExpression();
_ignoreComments();
if (!isDone() && _lookingAtExpression()) {
var spaceExpressions = [first];
do {
spaceExpressions.add(_singleExpression());
_ignoreComments();
} while (!isDone() && _lookingAtExpression());
first = new ListExpression(spaceExpressions, ListSeparator.space);
}
if (!_scanner.scanChar($comma)) return first;
var commaExpressions = [first];
do {
_ignoreComments();
if (isDone() || !_lookingAtExpression()) break;
commaExpressions.add(_spaceListOrValue());
} while (_scanner.scanChar($comma));
return new ListExpression(commaExpressions, ListSeparator.comma);
}
ListExpression _bracketedList() {
var start = _scanner.state;
_scanner.expectChar($lbracket);

View File

@ -2,6 +2,7 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'exception.dart';
import 'value/boolean.dart';
import 'value/identifier.dart';
import 'visitor/interface/value.dart';
@ -21,6 +22,8 @@ abstract class Value {
bool get isTruthy => true;
int get asInt => throw new InternalException("$this is not an int.");
const Value();
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor);

View File

@ -2,6 +2,8 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import '../exception.dart';
import '../utils.dart';
import '../visitor/interface/value.dart';
import '../value.dart';
@ -10,6 +12,13 @@ class SassNumber extends Value {
final num value;
bool get isInt => value is int || almostEquals(value % 1, 0.0);
int get asInt {
if (!isInt) throw new InternalException("$this is not an int.");
return value.round();
}
SassNumber(this.value);
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor) =>

View File

@ -13,6 +13,7 @@ abstract class StatementVisitor<T> {
T visitDeclaration(Declaration node);
T visitErrorRule(ErrorRule node);
T visitExtendRule(ExtendRule node);
T visitForRule(ForRule node);
T visitFunctionRule(FunctionRule node);
T visitIfRule(IfRule node);
T visitImportRule(ImportRule node);

View File

@ -264,6 +264,26 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
}, through: (node) => node is CssStyleRule);
}
void visitForRule(ForRule node) {
var from =
_addExceptionSpan(() => node.from.accept(this).asInt, node.from.span);
var to = _addExceptionSpan(() => node.to.accept(this).asInt, node.to.span);
// TODO: coerce units
var direction = from > to ? -1 : 1;
if (!node.isExclusive) to += direction;
if (from == to) return;
_environment.scope(() {
for (var i = from; i != to; i += direction) {
_environment.setLocalVariable(node.variable, new SassNumber(i));
for (var child in node.children) {
child.accept(this);
}
}
}, semiGlobal: true);
}
void visitFunctionRule(FunctionRule node) {
_environment
.setFunction(new UserDefinedCallable(node, _environment.closure()));
@ -427,12 +447,9 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
var selectorText = _interpolationToValue(node.selector, trim: true);
var parsedSelector = new Parser(selectorText.value).parseSelector();
try {
parsedSelector = parsedSelector.resolveParentSelectors(_selector?.value);
} on InternalException catch (error) {
throw _exception(error.message, node.selector.span);
}
parsedSelector = _addExceptionSpan(
() => parsedSelector.resolveParentSelectors(_selector?.value),
node.selector.span);
// TODO: catch errors and re-contextualize them relative to
// [node.selector.span.start].
@ -860,4 +877,12 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
SassRuntimeException _exception(String message, FileSpan span) =>
new SassRuntimeException(message, span, _stackTrace(span));
/*=T*/ _addExceptionSpan/*<T>*/(/*=T*/ callback(), FileSpan span) {
try {
return callback();
} on InternalException catch (error) {
throw _exception(error.message, span);
}
}
}