mirror of
https://github.com/danog/dart-sass.git
synced 2024-11-26 20:24:42 +01:00
Add mixins.
This commit is contained in:
parent
7692d61ef4
commit
4e0a511570
@ -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...");
|
||||
}
|
||||
|
@ -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]}"));
|
||||
|
29
lib/src/ast/sass/include.dart
Normal file
29
lib/src/ast/sass/include.dart
Normal 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(' ')}}");
|
||||
}
|
28
lib/src/ast/sass/mixin_declaration.dart
Normal file
28
lib/src/ast/sass/mixin_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 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(' ')}}";
|
||||
}
|
@ -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';
|
||||
|
@ -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({});
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}));
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user