Add mixins.

This commit is contained in:
Natalie Weizenbaum 2016-08-27 00:26:53 -07:00 committed by Natalie Weizenbaum
parent 7692d61ef4
commit 4e0a511570
9 changed files with 208 additions and 29 deletions

View File

@ -18,6 +18,10 @@ class ArgumentDeclaration implements SassNode {
{this.restArgument, this.span})
: arguments = new List.unmodifiable(arguments);
ArgumentDeclaration.empty({this.span})
: arguments = const [],
restArgument = null;
String toString() => arguments.join(', ') +
(restArgument == null ? '' : ", $restArgument...");
}

View File

@ -25,6 +25,12 @@ class ArgumentInvocation implements SassNode {
assert(rest != null || keywordRest == null);
}
ArgumentInvocation.empty({this.span})
: positional = const [],
named = const {},
rest = null,
keywordRest = null;
String toString() {
var components = new List<Object>.from(positional)
..addAll(named.keys.map((name) => "$name: ${named[name]}"));

View File

@ -0,0 +1,29 @@
// 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_invocation.dart';
import 'statement.dart';
class Include implements Statement {
final String name;
final ArgumentInvocation arguments;
final List<Statement> children;
final FileSpan span;
Include(this.name, this.arguments, {Iterable<Statement> children,
this.span})
: children = children == null ? null : new List.unmodifiable(children);
/*=T*/ accept/*<T>*/(StatementVisitor/*<T>*/ visitor) =>
visitor.visitInclude(this);
String toString() => "@include $name($arguments)" +
(children == null ? ";" : " {${children.join(' ')}}");
}

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 MixinDeclaration implements Statement {
final String name;
final ArgumentDeclaration arguments;
final List<Statement> children;
final FileSpan span;
MixinDeclaration(this.name, this.arguments, Iterable<Statement> children,
{this.span})
: children = new List.unmodifiable(children);
/*=T*/ accept/*<T>*/(StatementVisitor/*<T>*/ visitor) =>
visitor.visitMixinDeclaration(this);
String toString() => "@mixin $name($arguments) {${children.join(' ')}}";
}

View File

@ -13,9 +13,11 @@ export 'comment.dart';
export 'declaration.dart';
export 'extend_rule.dart';
export 'function_declaration.dart';
export 'include.dart';
export 'interpolation.dart';
export 'media_query.dart';
export 'media_rule.dart';
export 'mixin_declaration.dart';
export 'return.dart';
export 'style_rule.dart';
export 'stylesheet.dart';

View File

@ -17,20 +17,28 @@ class Environment {
final Map<String, int> _functionIndices;
final List<Map<String, Callable>> _mixins;
final Map<String, int> _mixinIndices;
Environment()
: _variables = [normalizedMap()],
_variableIndices = normalizedMap(),
_functions = [normalizedMap()],
_functionIndices = normalizedMap();
_functionIndices = normalizedMap(),
_mixins = [normalizedMap()],
_mixinIndices = normalizedMap();
Environment._(this._variables, this._variableIndices, this._functions,
this._functionIndices);
this._functionIndices, this._mixins, this._mixinIndices);
Environment closure() => new Environment._(
_variables.toList(),
new Map.from(_variableIndices),
_functions.toList(),
new Map.from(_functionIndices));
new Map.from(_functionIndices),
_mixins.toList(),
new Map.from(_mixinIndices));
Value getVariable(String name) =>
_variables[_variableIndices[name] ?? 0][name];
@ -52,6 +60,16 @@ class Environment {
_functions[index][name] = callable;
}
Callable getMixin(String name) =>
_mixins[_mixinIndices[name] ?? 0][name];
void setMixin(String name, Callable callable) {
var index = _mixins.length == 1
? 0
: _mixinIndices.putIfAbsent(name, () => _mixins.length - 1);
_mixins[index][name] = callable;
}
/*=T*/ scope/*<T>*/(/*=T*/ callback()) {
// TODO: avoid creating a new scope if no variables are declared.
_variables.add({});

View File

@ -24,6 +24,8 @@ final _prefixedSelectorPseudoClasses =
class Parser {
final SpanScanner _scanner;
bool _inMixin = false;
Parser(String contents, {url})
: _scanner = new SpanScanner(contents, sourceUrl: url);
@ -115,21 +117,15 @@ class Parser {
_ignoreComments();
switch (name) {
case "media":
return new MediaRule(_mediaQueryList(), _ruleChildren(),
span: _scanner.spanFrom(start));
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,
case "function": return _functionDeclaration(start);
case "include": return _include(start);
case "media":
return new MediaRule(_mediaQueryList(), _ruleChildren(),
span: _scanner.spanFrom(start));
case "mixin": return _mixinDeclaration(start);
}
Interpolation value;
@ -145,6 +141,66 @@ class Parser {
span: _scanner.spanFrom(start));
}
Include _include(LineScannerState start) {
var name = _identifier();
_ignoreComments();
var arguments = _scanner.peekChar() == $lparen
? _argumentInvocation()
: new ArgumentInvocation.empty(span: _scanner.emptySpan);
_ignoreComments();
List<Statement> children;
if (_scanner.peekChar() == $lbrace) {
_inMixin = true;
children = _ruleChildren();
_inMixin = false;
}
return new Include(name, arguments,
children: children, span: _scanner.spanFrom(start));
}
MixinDeclaration _mixinDeclaration(LineScannerState start) {
var name = _identifier();
_ignoreComments();
var arguments = _scanner.peekChar() == $lparen
? _argumentDeclaration()
: new ArgumentDeclaration.empty(span: _scanner.emptySpan);
if (_inMixin) {
throw new StringScannerException(
"Mixins may not contain mixin declarations.",
_scanner.spanFrom(start), _scanner.string);
}
_ignoreComments();
_inMixin = true;
var children = _ruleChildren();
_inMixin = false;
return new MixinDeclaration(name, arguments, children,
span: _scanner.spanFrom(start));
}
FunctionDeclaration _functionDeclaration(LineScannerState start) {
var name = _identifier();
_ignoreComments();
var arguments = _argumentDeclaration();
if (_inMixin) {
throw new StringScannerException(
"Mixins may not contain function declarations.",
_scanner.spanFrom(start), _scanner.string);
}
_ignoreComments();
var children = _children(_functionAtRule);
// TODO: ensure there aren't duplicate argument names.
return new FunctionDeclaration(name, arguments, children,
span: _scanner.spanFrom(start));
}
Statement _functionAtRule() {
var start = _scanner.state;
_scanner.expectChar($at);

View File

@ -33,6 +33,21 @@ abstract class StatementVisitor<T> {
return null;
}
T visitInclude(Include node) {
if (node.children == null) return null;
for (var child in node.children) {
child.accept(this);
}
return null;
}
T visitMixinDeclaration(MixinDeclaration 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

@ -129,6 +129,26 @@ class PerformVisitor extends StatementVisitor
span: node.span));
}
void visitInclude(Include node) {
var mixin = _environment.getMixin(node.name);
if (mixin == null) throw node.span.message("Undefined mixin.");
if (node.children != null) {
throw node.span.message("Mixin doesn't accept a content block.");
}
_runCallable(node.arguments, mixin, node.span, () {
for (var statement in mixin.children) {
statement.accept(this);
}
});
}
void visitMixinDeclaration(MixinDeclaration node) {
_environment.setMixin(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
@ -262,7 +282,14 @@ class PerformVisitor extends StatementVisitor
if (plainName != null) {
var function = _environment.getFunction(plainName);
if (function != null) {
return _runCallable(node.arguments, function, node.span);
return _runCallable(node.arguments, function, node.span, () {
for (var statement in function.children) {
var returnValue = statement.accept(this);
if (returnValue is Value) return returnValue;
}
throw function.span.message("Function finished without @return.");
});
}
}
@ -280,8 +307,8 @@ class PerformVisitor extends StatementVisitor
return new SassIdentifier("$name(${arguments.join(', ')})");
}
Value _runCallable(ArgumentInvocation arguments, Callable callable,
FileSpan span) {
/*=T*/ _runCallable/*<T>*/(ArgumentInvocation arguments, Callable callable,
FileSpan span, /*=T*/ run()) {
return _withEnvironment(callable.environment, () => _environment.scope(() {
var positional = arguments.positional
.map((expression) => expression.accept(this)).toList();
@ -342,22 +369,16 @@ class PerformVisitor extends StatementVisitor
new SassList(rest, ListSeparator.comma));
} else if (i < positional.length) {
throw span.message(
"Function takes ${callableArguments.length} arguments but "
"Only ${callableArguments.length} arguments are allowed, but "
"${positional.length} were passed.");
} else if (named.isNotEmpty) {
throw span.message(
"Function doesn't have an argument named \$${named.keys.first}.");
throw span.message("No 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.");
// TODO: if we get here and there are no rest params involved, mark the
// callable as fast-path and don't do error checking or extra allocations
// for future calls.
return run();
}));
}