Add support for functions.

Argument list objects are still not implemented.
This commit is contained in:
Natalie Weizenbaum 2016-08-19 16:40:44 -07:00
parent cbe3709914
commit 50912350af
18 changed files with 562 additions and 51 deletions

View File

@ -0,0 +1,20 @@
// 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 'expression.dart';
import 'node.dart';
class Argument implements SassNode {
final String name;
final Expression defaultValue;
final FileSpan span;
Argument(this.name, {this.defaultValue, this.span});
String toString() => defaultValue == null ? name : "$name: $defaultValue";
}

View File

@ -0,0 +1,23 @@
// 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 'argument.dart';
import 'node.dart';
class ArgumentDeclaration implements SassNode {
final List<Argument> arguments;
final String restArgument;
final FileSpan span;
ArgumentDeclaration(Iterable<Argument> arguments,
{this.restArgument, this.span})
: arguments = new List.unmodifiable(arguments);
String toString() => arguments.join(', ') +
(restArgument == null ? '' : ", $restArgument...");
}

View File

@ -0,0 +1,35 @@
// 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 'expression.dart';
import 'node.dart';
class ArgumentInvocation implements SassNode {
final List<Expression> positional;
final Map<String, Expression> named;
final Expression rest;
final Expression keywordRest;
final FileSpan span;
ArgumentInvocation(Iterable<Expression> positional,
Map<String, Expression> named, {this.rest, this.keywordRest, this.span})
: positional = new List.unmodifiable(positional),
named = new Map.unmodifiable(named) {
assert(rest != null || keywordRest == null);
}
String toString() {
var components = new List<Object>.from(positional)
..addAll(named.keys.map((name) => "$name: ${named[name]}"));
if (rest != null) components.add("$rest...");
if (keywordRest != null) components.add("$keywordRest...");
return "(${components.join(', ')})";
}
}

View File

@ -7,6 +7,7 @@ import 'node.dart';
export 'expression/boolean.dart';
export 'expression/color.dart';
export 'expression/function.dart';
export 'expression/identifier.dart';
export 'expression/interpolation.dart';
export 'expression/list.dart';

View File

@ -0,0 +1,15 @@
// 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:charcode/charcode.dart';
import 'package:source_span/source_span.dart';
import '../../../utils.dart';
import '../../../value/list.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
class ArgumentInvocation {
}

View File

@ -0,0 +1,25 @@
// 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 '../../../utils.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
import '../statement.dart';
class FunctionExpression implements Expression {
final InterpolationExpression name;
final ArgumentInvocation arguments;
FileSpan get span => spanForList([name, arguments]);
FunctionExpression(this.name, this.arguments);
/*=T*/ accept/*<T>*/(ExpressionVisitor/*<T>*/ visitor) =>
visitor.visitFunctionExpression(this);
String toString() => "$name$arguments";
}

View File

@ -0,0 +1,28 @@
// 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 'argument_declaration.dart';
import 'statement.dart';
class FunctionDeclaration implements Statement {
final String name;
final ArgumentDeclaration arguments;
final List<Statement> children;
final FileSpan span;
FunctionDeclaration(this.name, this.arguments, Iterable<Statement> children,
{this.span})
: children = new List.unmodifiable(children);
/*=T*/ accept/*<T>*/(StatementVisitor/*<T>*/ visitor) =>
visitor.visitFunctionDeclaration(this);
String toString() => "@function $name($arguments) {${children.join(' ')}}";
}

View File

@ -0,0 +1,22 @@
// 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 Return implements Statement {
final Expression expression;
final FileSpan span;
Return(this.expression, {this.span});
/*=T*/ accept/*<T>*/(StatementVisitor/*<T>*/ visitor) =>
visitor.visitReturn(this);
String toString() => "@return $expression;";
}

View File

@ -6,11 +6,16 @@ import '../../visitor/interface/statement.dart';
import 'node.dart';
export 'at_rule.dart';
export 'argument_declaration.dart';
export 'argument_invocation.dart';
export 'argument.dart';
export 'comment.dart';
export 'declaration.dart';
export 'extend_rule.dart';
export 'function_declaration.dart';
export 'media_query.dart';
export 'media_rule.dart';
export 'return.dart';
export 'style_rule.dart';
export 'stylesheet.dart';
export 'variable_declaration.dart';

24
lib/src/callable.dart Normal file
View File

@ -0,0 +1,24 @@
// 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/sass/statement.dart';
import 'environment.dart';
class Callable {
final String name;
final ArgumentDeclaration arguments;
final List<Statement> children;
final Environment environment;
final FileSpan span;
Callable(this.name, this.arguments, Iterable<Statement> children,
this.environment, {this.span})
: children = new List.unmodifiable(children);
}

View File

@ -2,14 +2,35 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'callable.dart';
import 'value.dart';
import 'utils.dart';
// Lexical environment only
class Environment {
/// Base is global scope.
final _variables = [separatorIndependentMap/*<Value>*/()];
final List<Map<String, Value>> _variables;
final _variableIndices = separatorIndependentMap/*<int>*/();
final Map<String, int> _variableIndices;
final List<Map<String, Callable>> _functions;
final Map<String, int> _functionIndices;
Environment()
: _variables = [separatorIndependentMap()],
_variableIndices = separatorIndependentMap(),
_functions = [separatorIndependentMap()],
_functionIndices = separatorIndependentMap();
Environment._(this._variables, this._variableIndices, this._functions,
this._functionIndices);
Environment closure() => new Environment._(
_variables.toList(),
new Map.from(_variableIndices),
_functions.toList(),
new Map.from(_functionIndices));
Value getVariable(String name) =>
_variables[_variableIndices[name] ?? 0][name];
@ -21,6 +42,16 @@ class Environment {
_variables[index][name] = value;
}
Callable getFunction(String name) =>
_functions[_functionIndices[name] ?? 0][name];
void setFunction(String name, Callable callable) {
var index = _variables.length == 1
? 0
: _variableIndices.putIfAbsent(name, () => _variables.length - 1);
_functions[index][name] = callable;
}
/*=T*/ scope/*<T>*/(/*=T*/ callback()) {
// TODO: avoid creating a new scope if no variables are declared.
_variables.add({});

View File

@ -121,6 +121,15 @@ class Parser {
case "extend":
return new ExtendRule(_almostAnyValue(),
span: _scanner.spanFrom(start));
case "function":
var name = _identifier();
_ignoreComments();
var arguments = _argumentDeclaration();
_ignoreComments();
var children = _children(_functionAtRule);
// TODO: ensure there aren't duplicate argument names.
return new FunctionDeclaration(name, arguments, children,
span: _scanner.spanFrom(start));
}
InterpolationExpression value;
@ -136,6 +145,58 @@ class Parser {
span: _scanner.spanFrom(start));
}
Statement _functionAtRule() {
var start = _scanner.state;
_scanner.expectChar($at);
var name = _identifier();
_ignoreComments();
switch (name) {
case "return":
_ignoreComments();
return new Return(_expression(), span: _scanner.spanFrom(start));
}
_scanner.error(
"Functions can only contain variable declarations and control "
"directives.",
position: start.position,
length: name.length + 1);
return null;
}
ArgumentDeclaration _argumentDeclaration() {
var start = _scanner.state;
_scanner.expectChar($lparen);
_ignoreComments();
var arguments = <Argument>[];
String restArgument;
while (_scanner.peekChar() == $dollar) {
var variableStart = _scanner.state;
var name = _variableName();
_ignoreComments();
Expression defaultValue;
if (_scanner.scanChar($colon)) {
_ignoreComments();
defaultValue = _spaceListOrValue();
} else if (_scanner.scanChar($dot)) {
_scanner.expectChar($dot);
_scanner.expectChar($dot);
restArgument = name;
break;
}
arguments.add(new Argument(name,
defaultValue: defaultValue, span: _scanner.spanFrom(variableStart)));
if (!_scanner.scanChar($comma)) break;
_ignoreComments();
}
_scanner.expectChar($rparen);
return new ArgumentDeclaration(arguments,
restArgument: restArgument, span: _scanner.spanFrom(start));
}
StyleRule _styleRule() {
var start = _scanner.state;
var selector = _almostAnyValue();
@ -144,37 +205,10 @@ class Parser {
span: _scanner.spanFrom(start));
}
List<Statement> _ruleChildren() {
_scanner.expectChar($lbrace);
var children = <Statement>[];
loop: while (true) {
children.addAll(_comments());
switch (_scanner.peekChar()) {
case $dollar:
children.add(_variableDeclaration());
break;
case $at:
children.add(_atRule());
break;
case $semicolon:
_scanner.readChar();
break;
case $rbrace:
break loop;
default:
children.add(_declarationOrStyleRule());
break;
}
}
children.addAll(_comments());
_scanner.expectChar($rbrace);
return children;
}
List<Statement> _ruleChildren() => _children(() {
if (_scanner.peekChar() == $at) return _atRule();
return _declarationOrStyleRule();
});
Expression _declarationExpression() {
if (_scanner.peekChar() == $lbrace) {
@ -387,6 +421,48 @@ class Parser {
// ## Expressions
ArgumentInvocation _argumentInvocation() {
var start = _scanner.state;
_scanner.expectChar($lparen);
_ignoreComments();
var positional = <Expression>[];
var named = <String, Expression>{};
Expression rest;
Expression keywordRest;
while (_lookingAtExpression()) {
var expression = _spaceListOrValue();
_ignoreComments();
if (expression is VariableExpression && _scanner.scanChar($colon)) {
_ignoreComments();
named[expression.name] = _spaceListOrValue();
} else if (_scanner.scanChar($dot)) {
_scanner.expectChar($dot);
_scanner.expectChar($dot);
if (rest == null) {
rest = expression;
} else {
keywordRest = expression;
_ignoreComments();
break;
}
} else if (named.isNotEmpty) {
_scanner.expect("...");
} else {
positional.add(expression);
}
_ignoreComments();
if (!_scanner.scanChar($comma)) break;
_ignoreComments();
}
_scanner.expectChar($rparen);
return new ArgumentInvocation(positional, named,
rest: rest, keywordRest: keywordRest, span: _scanner.spanFrom(start));
}
Expression _expression() {
var first = _singleExpression();
_ignoreComments();
@ -446,7 +522,7 @@ class Parser {
var first = _scanner.peekChar();
switch (first) {
// Note: when adding a new case, make sure it's reflected in
// [isExpressionStart].
// [lookingAtExpression].
case $lparen: return _parentheses();
case $slash: return _unaryOperator();
case $dot: return _number();
@ -601,9 +677,8 @@ class Parser {
VariableExpression _variable() {
var start = _scanner.state;
_scanner.expectChar($dollar);
var name = _identifier();
return new VariableExpression(name, span: _scanner.spanFrom(start));
return new VariableExpression(_variableName(),
span: _scanner.spanFrom(start));
}
StringExpression _string({bool static: false}) {
@ -695,7 +770,7 @@ class Parser {
}
Expression _identifierLike() {
// TODO: url(), functions
// TODO: url()
var identifier = _interpolatedIdentifier();
switch (identifier.asPlain) {
case "not":
@ -705,10 +780,11 @@ class Parser {
case "true": return new BooleanExpression(true, span: identifier.span);
case "false": return new BooleanExpression(false, span: identifier.span);
default:
return new IdentifierExpression(identifier);
}
return _scanner.peekChar() == $lparen
? new FunctionExpression(identifier, _argumentInvocation())
: new IdentifierExpression(identifier);
}
/// Consumes tokens up to "{", "}", ";", or "!".
@ -915,6 +991,7 @@ class Parser {
Expression _singleInterpolation() {
_scanner.expect('#{');
_ignoreComments();
var expression = _expression();
_scanner.expectChar($rbrace);
return expression;
@ -1401,6 +1478,11 @@ class Parser {
return text.toString();
}
String _variableName() {
_scanner.expectChar($dollar);
return _identifier();
}
// ## Characters
UnaryOperator _unaryOperatorFor(int character) {
@ -1500,8 +1582,41 @@ class Parser {
return second == $hash && _scanner.peekChar(2) == $lbrace;
}
bool _lookingAtExpression() =>
!_scanner.isDone && isExpressionStart(_scanner.peekChar());
bool _lookingAtExpression() {
var character = _scanner.peekChar();
if (character == null) return false;
if (character == $dot) return _scanner.peekChar(1) != $dot;
return character == $lparen || character == $slash ||
character == $lbracket || character == $single_quote ||
character == $double_quote || character == $hash ||
character == $plus || character == $minus || character == $backslash ||
character == $dollar || isNameStart(character) || isDigit(character);
}
List<Statement> _children(Statement consumeChild()) {
_scanner.expectChar($lbrace);
var children = <Statement>[];
loop: while (true) {
children.addAll(_comments());
switch (_scanner.peekChar()) {
case $dollar:
children.add(_variableDeclaration());
break;
case $semicolon:
_scanner.readChar();
continue loop;
case $rbrace:
break loop;
}
children.add(consumeChild());
}
_scanner.expectChar($rbrace);
return children;
}
String _rawText(void consumer()) {
var start = _scanner.position;

View File

@ -30,13 +30,6 @@ bool isHex(int character) =>
(character >= $a && character <= $f) ||
(character >= $A && character <= $F);
bool isExpressionStart(int character) =>
character == $lparen || character == $slash || character == $dot ||
character == $lbracket || character == $single_quote ||
character == $double_quote || character == $hash || character == $plus ||
character == $minus || character == $backslash || character == $dollar ||
isNameStart(character) || isDigit(character);
// Does not include type selectors
bool isSimpleSelectorStart(int character) =>
character == $asterisk || character == $lbracket || character == $dot ||

View File

@ -100,6 +100,20 @@ Map/*<V>*/ separatorIndependentMap/*<V>*/() =>
new LinkedHashMap(
equals: equalsIgnoreSeparator, hashCode: hashCodeIgnoreSeparator);
Map/*<String, V2>*/ separatorIndependentMapMap/*<K, V1, V2>*/(
Map/*<K, V1>*/ map,
{String key(/*=K*/ key, /*=V1*/ value),
/*=V2*/ value(/*=K*/ key, /*=V1*/ value)}) {
key ??= (mapKey, _) => mapKey as String;
value ??= (_, mapValue) => mapValue as dynamic/*=V2*/;
var result = separatorIndependentMap/*<V2>*/();
map.forEach((mapKey, mapValue) {
result[key(mapKey, mapValue)] = value(mapKey, mapValue);
});
return result;
}
bool almostEquals(num number1, num number2) =>
(number1 - number2).abs() < _epsilon;

View File

@ -23,6 +23,8 @@ abstract class Value {
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor);
List<Value> asList() => [this];
Value unaryPlus() => new SassIdentifier("+${valueToCss(this)}");
Value unaryMinus() => new SassIdentifier("-${valueToCss(this)}");

View File

@ -35,6 +35,18 @@ abstract class ExpressionVisitor<T> {
return null;
}
T visitFunctionExpression(FunctionExpression node) {
for (var expression in node.arguments.positional) {
expression.accept(this);
}
for (var expression in node.arguments.named.values) {
expression.accept(this);
}
node.arguments.rest?.accept(this);
node.arguments.keywordRest?.accept(this);
return null;
}
T visitStringExpression(StringExpression node) {
visitInterpolationExpression(node.text);
return null;

View File

@ -7,6 +7,7 @@ import '../../ast/sass/statement.dart';
abstract class StatementVisitor<T> {
T visitComment(Comment node) => null;
T visitExtendRule(ExtendRule node) => null;
T visitReturn(Return node) => null;
T visitVariableDeclaration(VariableDeclaration node) => null;
T visitDeclaration(Declaration node) {
@ -25,6 +26,13 @@ abstract class StatementVisitor<T> {
return null;
}
T visitFunctionDeclaration(FunctionDeclaration node) {
for (var child in node.children) {
child.accept(this);
}
return null;
}
T visitMediaRule(MediaRule node) {
for (var child in node.children) {
child.accept(this);

View File

@ -2,20 +2,24 @@
// 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/css/node.dart';
import '../ast/sass/expression.dart';
import '../ast/sass/statement.dart';
import '../ast/selector.dart';
import '../callable.dart';
import '../environment.dart';
import '../extend/extender.dart';
import '../parser.dart';
import '../utils.dart';
import '../value.dart';
import 'interface/statement.dart';
import 'interface/expression.dart';
class PerformVisitor extends StatementVisitor
implements ExpressionVisitor<Value> {
final Environment _environment;
Environment _environment;
/// The current selector, if any.
CssValue<SelectorList> _selector;
@ -117,6 +121,12 @@ class PerformVisitor extends StatementVisitor
through: (node) => node is CssStyleRule);
}
void visitFunctionDeclaration(FunctionDeclaration node) {
_environment.setFunction(node.name, new Callable(
node.name, node.arguments, node.children, _environment.closure(),
span: node.span));
}
void visitMediaRule(MediaRule node) {
var queryIterable = node.queries.map(_visitMediaQuery);
var queries = _mediaQueries == null
@ -168,6 +178,8 @@ class PerformVisitor extends StatementVisitor
return new CssMediaQuery(type, modifier: modifier, features: features);
}
Value visitReturn(Return node) => node.expression.accept(this);
void visitStyleRule(StyleRule node) {
var selectorText = _performInterpolation(node.selector, trim: true);
var parsedSelector = new Parser(selectorText.value).parseSelector();
@ -250,11 +262,137 @@ class PerformVisitor extends StatementVisitor
return new SassMap(map);
}
Value visitFunctionExpression(FunctionExpression node) {
var plainName = node.name.asPlain;
if (plainName != null) {
var function = _environment.getFunction(plainName);
if (function != null) {
return _runCallable(node.arguments, function, node.span);
}
}
if (node.arguments.named.isNotEmpty || node.arguments.keywordRest != null) {
throw node.span.message(
"Plain CSS functions don't support keyword arguments.");
}
var name = node.name.accept(this);
var arguments = node.arguments.positional
.map((expression) => expression.accept(this)).toList();
// TODO: if rest is an arglist that has keywords, error out.
var rest = node.arguments.rest?.accept(this);
if (rest != null) arguments.add(rest);
return new SassIdentifier("$name(${arguments.join(', ')})");
}
Value _runCallable(ArgumentInvocation arguments, Callable callable,
FileSpan span) {
return _withEnvironment(callable.environment, () => _environment.scope(() {
var positional = arguments.positional
.map((expression) => expression.accept(this)).toList();
var named = separatorIndependentMapMap/*<String, Expression, Value>*/(
arguments.named,
value: (_, expression) => expression.accept(this));
if (arguments.rest != null) {
var value = arguments.rest.accept(this);
if (value is SassMap) {
_addRestMap(named, value, span);
} else if (value is SassList) {
positional.addAll(value.asList());
} else {
positional.add(value);
}
}
if (arguments.keywordRest != null) {
var value = arguments.keywordRest.accept(this);
if (value is SassMap) {
_addRestMap(named, value, span);
} else {
span.message(
"Variable keyword arguments must be a map (was $value).");
}
}
var callableArguments = callable.arguments.arguments;
var i = 0;
for (; i < positional.length && i < callableArguments.length; i++) {
var name = callableArguments[i].name;
if (named.containsKey(name)) {
throw span.message(
"Argument \$$name was passed both by position and by name.");
}
_environment.setVariable(name, positional[i]);
}
for (; i < callableArguments.length; i++) {
var argument = callableArguments[i];
var value = named.remove(argument.name) ??
argument.defaultValue?.accept(this);
if (value == null) {
throw span.message("Missing argument \$${argument.name}.");
} else {
_environment.setVariable(argument.name, value);
}
}
if (callable.arguments.restArgument != null) {
// TODO: use a full ArgList object
var rest =
i < positional.length ? positional.sublist(i) : const <Value>[];
_environment.setVariable(callable.arguments.restArgument,
new SassList(rest, ListSeparator.comma));
} else if (i < positional.length) {
throw span.message(
"Function takes ${callableArguments.length} arguments but "
"${positional.length} were passed.");
} else if (named.isNotEmpty) {
throw span.message(
"Function doesn't have an argument named \$${named.keys.first}.");
}
// TODO: if we get here and there are no rest params involved, mark them
// as fast-path and don't do error checking or extra allocations for
// future calls.
for (var statement in callable.children) {
var returnValue = statement.accept(this);
if (returnValue is Value) return returnValue;
}
throw callable.span.message("Function finished without @return.");
}));
}
void _addRestMap(Map<String, Value> values, SassMap map, FileSpan span) {
map.contents.forEach((key, value) {
if (key is SassIdentifier) {
values[key.text] = value;
} else if (key is SassString) {
values[key.text] = value;
} else {
throw span.message(
"Variable keyword argument map must have string keys.\n"
"$key is not a string in $value.");
}
});
}
SassString visitStringExpression(StringExpression node) =>
visitInterpolationExpression(node.text);
// ## Utilities
/*=T*/ _withEnvironment/*<T>*/(Environment environment, /*=T*/ callback()) {
var oldEnvironment = _environment;
_environment = environment;
var result = callback();
_environment = oldEnvironment;
return result;
}
CssValue<String> _performInterpolation(
InterpolationExpression interpolation, {bool trim: false}) {
var result = visitInterpolationExpression(interpolation).text;