From 80f0afb484c0d1900dd51e04d895b34058b7a43e Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 7 Sep 2016 08:40:58 -0700 Subject: [PATCH] Support @for. --- lib/src/ast/sass.dart | 1 + lib/src/ast/sass/statement/for_rule.dart | 36 ++++++++++++++ lib/src/environment.dart | 28 +++++++++-- lib/src/parser.dart | 61 ++++++++++++++++++++++-- lib/src/value.dart | 3 ++ lib/src/value/number.dart | 9 ++++ lib/src/visitor/interface/statement.dart | 1 + lib/src/visitor/perform.dart | 37 +++++++++++--- 8 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 lib/src/ast/sass/statement/for_rule.dart diff --git a/lib/src/ast/sass.dart b/lib/src/ast/sass.dart index 21584eb8..5cefcd71 100644 --- a/lib/src/ast/sass.dart +++ b/lib/src/ast/sass.dart @@ -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'; diff --git a/lib/src/ast/sass/statement/for_rule.dart b/lib/src/ast/sass/statement/for_rule.dart new file mode 100644 index 00000000..6d37b9ed --- /dev/null +++ b/lib/src/ast/sass/statement/for_rule.dart @@ -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 children; + + final FileSpan span; + + ForRule(this.variable, this.from, this.to, Iterable children, + this.span, + {bool exclusive: true}) + : children = new List.unmodifiable(children), + isExclusive = exclusive; + + /*=T*/ accept/**/(StatementVisitor/**/ visitor) => + visitor.visitForRule(this); + + String toString() => + "@for \$$variable from $from ${isExclusive ? 'to' : 'through'} $to " + "{${children.join(" ")}}"; +} diff --git a/lib/src/environment.dart b/lib/src/environment.dart index b45b2aa5..f896f36f 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -13,6 +13,7 @@ class Environment { /// Base is global scope. final List> _variables; + // Note: this is not necessarily complete final Map _variableIndices; final List> _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*/ 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; diff --git a/lib/src/parser.dart b/lib/src/parser.dart index e7b90313..32d84bbe 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -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); diff --git a/lib/src/value.dart b/lib/src/value.dart index 48a2dcf4..6eb9629b 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -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/**/(ValueVisitor/**/ visitor); diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index 44b0ae21..0b878eb6 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -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/**/(ValueVisitor/**/ visitor) => diff --git a/lib/src/visitor/interface/statement.dart b/lib/src/visitor/interface/statement.dart index b95da769..c6c7aa24 100644 --- a/lib/src/visitor/interface/statement.dart +++ b/lib/src/visitor/interface/statement.dart @@ -13,6 +13,7 @@ abstract class StatementVisitor { 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); diff --git a/lib/src/visitor/perform.dart b/lib/src/visitor/perform.dart index d45fc0a5..3bc73954 100644 --- a/lib/src/visitor/perform.dart +++ b/lib/src/visitor/perform.dart @@ -264,6 +264,26 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor { }, 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 { 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 { SassRuntimeException _exception(String message, FileSpan span) => new SassRuntimeException(message, span, _stackTrace(span)); + + /*=T*/ _addExceptionSpan/**/(/*=T*/ callback(), FileSpan span) { + try { + return callback(); + } on InternalException catch (error) { + throw _exception(error.message, span); + } + } }