mirror of
https://github.com/danog/dart-sass.git
synced 2024-11-26 20:24:42 +01:00
Add support for functions.
Argument list objects are still not implemented.
This commit is contained in:
parent
cbe3709914
commit
50912350af
20
lib/src/ast/sass/argument.dart
Normal file
20
lib/src/ast/sass/argument.dart
Normal 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";
|
||||
}
|
23
lib/src/ast/sass/argument_declaration.dart
Normal file
23
lib/src/ast/sass/argument_declaration.dart
Normal 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...");
|
||||
}
|
35
lib/src/ast/sass/argument_invocation.dart
Normal file
35
lib/src/ast/sass/argument_invocation.dart
Normal 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(', ')})";
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
15
lib/src/ast/sass/expression/argument_list.dart
Normal file
15
lib/src/ast/sass/expression/argument_list.dart
Normal 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 {
|
||||
|
||||
}
|
25
lib/src/ast/sass/expression/function.dart
Normal file
25
lib/src/ast/sass/expression/function.dart
Normal 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";
|
||||
}
|
28
lib/src/ast/sass/function_declaration.dart
Normal file
28
lib/src/ast/sass/function_declaration.dart
Normal 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(' ')}}";
|
||||
}
|
22
lib/src/ast/sass/return.dart
Normal file
22
lib/src/ast/sass/return.dart
Normal 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;";
|
||||
}
|
@ -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
24
lib/src/callable.dart
Normal 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);
|
||||
}
|
@ -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({});
|
||||
|
@ -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;
|
||||
|
@ -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 ||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)}");
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user