mirror of
https://github.com/danog/dart-sass.git
synced 2025-01-21 21:31:11 +01:00
Merge branch 'feature.async'
This commit is contained in:
commit
c1b6c117cc
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,3 +8,4 @@ packages
|
||||
.packages
|
||||
pubspec.lock
|
||||
/benchmark/source
|
||||
node_modules/
|
||||
|
@ -4,6 +4,7 @@ env:
|
||||
# Language specs, defined in sass/sass-spec
|
||||
- TASK=specs DART_CHANNEL=dev DART_VERSION=latest
|
||||
- TASK=specs DART_CHANNEL=stable DART_VERSION=latest
|
||||
- TASK=specs DART_CHANNEL=stable DART_VERSION=latest ASYNC=true
|
||||
|
||||
# Unit tests, defined in test/.
|
||||
- TASK=tests DART_CHANNEL=dev DART_VERSION=latest
|
||||
@ -42,7 +43,7 @@ install:
|
||||
- if-node . "$HOME/.nvm/nvm.sh"
|
||||
- if-node nvm install "$NODE_VERSION"
|
||||
- if-node nvm use "$NODE_VERSION"
|
||||
- SASS_MINIFY_JS=false if-node pub run grinder npm_package
|
||||
- SASS_MINIFY_JS=false if-node pub run grinder before_test
|
||||
|
||||
# Download sass-spec and install its dependencies if we're running specs.
|
||||
- if-specs() { if [ "$TASK" = specs ]; then "$@"; fi }
|
||||
@ -69,5 +70,8 @@ script:
|
||||
fi;
|
||||
else
|
||||
echo "${bold}Running sass-spec against $(dart --version &> /dev/stdout).$none";
|
||||
(cd sass-spec; bundle exec sass-spec.rb --dart ..);
|
||||
if [ "$ASYNC" = true ]; then
|
||||
extra_args=--dart-args --async;
|
||||
fi;
|
||||
(cd sass-spec; bundle exec sass-spec.rb --dart .. $extra_args);
|
||||
fi
|
||||
|
13
CHANGELOG.md
13
CHANGELOG.md
@ -3,6 +3,19 @@
|
||||
* Fix a crash when `:not(...)` extends a selector that appears in
|
||||
`:not(:not(...))`.
|
||||
|
||||
### Node JS API
|
||||
|
||||
* Add support for asynchronous importers to `render()` and `renderSync()`.
|
||||
|
||||
### Dart API
|
||||
|
||||
* Add `compileAsync()` and `compileStringAsync()` methods. These run
|
||||
asynchronously, which allows them to take asynchronous importers (see below).
|
||||
|
||||
* Add an `AsyncImporter` class. This allows imports to be resolved
|
||||
asynchronously in case no synchronous APIs are available. `AsyncImporter`s are
|
||||
only compatible with `compileAysnc()` and `compileStringAsync()`.
|
||||
|
||||
## 1.0.0-beta.3
|
||||
|
||||
* Properly parse numbers with exponents.
|
||||
|
33
README.md
33
README.md
@ -113,7 +113,32 @@ That's it!
|
||||
When installed via NPM, Dart Sass supports a JavaScript API that aims to be
|
||||
compatible with [Node Sass](https://github.com/sass/node-sass#usage). Full
|
||||
compatibility is a work in progress, but Dart Sass currently supports the
|
||||
`render()` and `renderSync()` functions with the following options:
|
||||
`render()` and `renderSync()` functions. Note however that by default,
|
||||
**`renderSync()` is more than twice as fast as `render()`**, due to the overhead
|
||||
of asynchronous callbacks.
|
||||
|
||||
To avoid this performance hit, `render()` can use the [`fibers`][fibers] package
|
||||
to call asynchronous importers from the synchronous code path. To enable this,
|
||||
pass the `Fiber` class to the `fiber` option:
|
||||
|
||||
[fibers]: https://www.npmjs.com/package/fibers
|
||||
|
||||
```js
|
||||
var sass = require("sass");
|
||||
var Fiber = require("fibers");
|
||||
|
||||
render({
|
||||
file: "input.scss",
|
||||
importer: function(url, prev, done) {
|
||||
// ...
|
||||
},
|
||||
fiber: Fiber
|
||||
}, function(err, result) {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
Both `render()` and `renderSync()` support the following options:
|
||||
|
||||
* [`file`](https://github.com/sass/node-sass#file)
|
||||
* [`data`](https://github.com/sass/node-sass#data)
|
||||
@ -122,13 +147,9 @@ compatibility is a work in progress, but Dart Sass currently supports the
|
||||
* [`indentType`](https://github.com/sass/node-sass#indenttype)
|
||||
* [`indentWidth`](https://github.com/sass/node-sass#indentwidth)
|
||||
* [`linefeed`](https://github.com/sass/node-sass#linefeed)
|
||||
* [`importer`](https://github.com/sass/node-sass#importer--v200---experimental)
|
||||
* Only the `"expanded"` value of
|
||||
[`outputStyle`](https://github.com/sass/node-sass#outputstyle) is supported.
|
||||
* [`importer`][importer option] is supported, but only for importers that return
|
||||
values synchronously. The `done()` callback is currently not passed to any
|
||||
importers, even when running the asynchronous `render()` function.
|
||||
|
||||
[importer option]: https://github.com/sass/node-sass#importer--v200---experimental
|
||||
|
||||
The following options are not yet supported, but are intended:
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'src/compile.dart' as c;
|
||||
import 'src/exception.dart';
|
||||
import 'src/importer.dart';
|
||||
@ -87,6 +89,48 @@ String compileString(String source,
|
||||
return result.css;
|
||||
}
|
||||
|
||||
/// Like [compile], except it runs asynchronously.
|
||||
///
|
||||
/// Running asynchronously allows this to take [AsyncImporter]s rather than
|
||||
/// synchronous [Importer]s. However, running asynchronously is also somewhat
|
||||
/// slower, so [compile] should be preferred if possible.
|
||||
Future<String> compileAsync(String path,
|
||||
{bool color: false,
|
||||
Iterable<AsyncImporter> importers,
|
||||
Iterable<String> loadPaths,
|
||||
SyncPackageResolver packageResolver}) async {
|
||||
var result = await c.compileAsync(path,
|
||||
color: color,
|
||||
importers: importers,
|
||||
loadPaths: loadPaths,
|
||||
packageResolver: packageResolver);
|
||||
return result.css;
|
||||
}
|
||||
|
||||
/// Like [compileString], except it runs asynchronously.
|
||||
///
|
||||
/// Running asynchronously allows this to take [AsyncImporter]s rather than
|
||||
/// synchronous [Importer]s. However, running asynchronously is also somewhat
|
||||
/// slower, so [compileString] should be preferred if possible.
|
||||
Future<String> compileStringAsync(String source,
|
||||
{bool indented: false,
|
||||
bool color: false,
|
||||
Iterable<AsyncImporter> importers,
|
||||
Iterable<String> loadPaths,
|
||||
SyncPackageResolver packageResolver,
|
||||
AsyncImporter importer,
|
||||
url}) async {
|
||||
var result = await c.compileStringAsync(source,
|
||||
indented: indented,
|
||||
color: color,
|
||||
importers: importers,
|
||||
loadPaths: loadPaths,
|
||||
packageResolver: packageResolver,
|
||||
importer: importer,
|
||||
url: url);
|
||||
return result.css;
|
||||
}
|
||||
|
||||
/// Use [compile] instead.
|
||||
@Deprecated('Will be removed in 1.0.0')
|
||||
String render(String path,
|
||||
|
306
lib/src/async_environment.dart
Normal file
306
lib/src/async_environment.dart
Normal file
@ -0,0 +1,306 @@
|
||||
// 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 'dart:async';
|
||||
|
||||
import 'ast/sass.dart';
|
||||
import 'callable.dart';
|
||||
import 'functions.dart';
|
||||
import 'value.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// The lexical environment in which Sass is executed.
|
||||
///
|
||||
/// This tracks lexically-scoped information, such as variables, functions, and
|
||||
/// mixins.
|
||||
class AsyncEnvironment {
|
||||
/// A list of variables defined at each lexical scope level.
|
||||
///
|
||||
/// Each scope maps the names of declared variables to their values. These
|
||||
/// maps are *normalized*, meaning that they treat hyphens and underscores in
|
||||
/// its keys interchangeably.
|
||||
///
|
||||
/// The first element is the global scope, and each successive element is
|
||||
/// deeper in the tree.
|
||||
final List<Map<String, Value>> _variables;
|
||||
|
||||
/// A map of variable names to their indices in [_variables].
|
||||
///
|
||||
/// This map is *normalized*, meaning that it treats hyphens and underscores
|
||||
/// in its keys interchangeably.
|
||||
///
|
||||
/// This map is filled in as-needed, and may not be complete.
|
||||
final Map<String, int> _variableIndices;
|
||||
|
||||
/// A list of functions defined at each lexical scope level.
|
||||
///
|
||||
/// Each scope maps the names of declared functions to their values. These
|
||||
/// maps are *normalized*, meaning that they treat hyphens and underscores in
|
||||
/// its keys interchangeably.
|
||||
///
|
||||
/// The first element is the global scope, and each successive element is
|
||||
/// deeper in the tree.
|
||||
final List<Map<String, AsyncCallable>> _functions;
|
||||
|
||||
/// A map of function names to their indices in [_functions].
|
||||
///
|
||||
/// This map is *normalized*, meaning that it treats hyphens and underscores
|
||||
/// in its keys interchangeably.
|
||||
///
|
||||
/// This map is filled in as-needed, and may not be complete.
|
||||
final Map<String, int> _functionIndices;
|
||||
|
||||
/// A list of mixins defined at each lexical scope level.
|
||||
///
|
||||
/// Each scope maps the names of declared mixins to their values. These
|
||||
/// maps are *normalized*, meaning that they treat hyphens and underscores in
|
||||
/// its keys interchangeably.
|
||||
///
|
||||
/// The first element is the global scope, and each successive element is
|
||||
/// deeper in the tree.
|
||||
final List<Map<String, AsyncCallable>> _mixins;
|
||||
|
||||
/// A map of mixin names to their indices in [_mixins].
|
||||
///
|
||||
/// This map is *normalized*, meaning that it treats hyphens and underscores
|
||||
/// in its keys interchangeably.
|
||||
///
|
||||
/// This map is filled in as-needed, and may not be complete.
|
||||
final Map<String, int> _mixinIndices;
|
||||
|
||||
/// The content block passed to the lexically-enclosing mixin, or `null` if this is not
|
||||
/// in a mixin, or if no content block was passed.
|
||||
List<Statement> get contentBlock => _contentBlock;
|
||||
List<Statement> _contentBlock;
|
||||
|
||||
/// The environment in which [_contentBlock] should be executed.
|
||||
AsyncEnvironment get contentEnvironment => _contentEnvironment;
|
||||
AsyncEnvironment _contentEnvironment;
|
||||
|
||||
/// Whether the environment is lexically within a mixin.
|
||||
bool get inMixin => _inMixin;
|
||||
var _inMixin = false;
|
||||
|
||||
/// Whether the environment is currently in a semi-global scope.
|
||||
///
|
||||
/// A semi-global scope can assign to global variables, but it doesn't declare
|
||||
/// them by default.
|
||||
var _inSemiGlobalScope = false;
|
||||
|
||||
AsyncEnvironment()
|
||||
: _variables = [normalizedMap()],
|
||||
_variableIndices = normalizedMap(),
|
||||
_functions = [normalizedMap()],
|
||||
_functionIndices = normalizedMap(),
|
||||
_mixins = [normalizedMap()],
|
||||
_mixinIndices = normalizedMap() {
|
||||
coreFunctions.forEach(setFunction);
|
||||
}
|
||||
|
||||
AsyncEnvironment._(this._variables, this._functions, this._mixins,
|
||||
this._contentBlock, this._contentEnvironment)
|
||||
// Lazily fill in the indices rather than eagerly copying them from the
|
||||
// existing environment in closure() and global() because the copying took a
|
||||
// lot of time and was rarely helpful. This saves a bunch of time on Susy's
|
||||
// tests.
|
||||
: _variableIndices = normalizedMap(),
|
||||
_functionIndices = normalizedMap(),
|
||||
_mixinIndices = normalizedMap();
|
||||
|
||||
/// Creates a closure based on this environment.
|
||||
///
|
||||
/// Any scope changes in this environment will not affect the closure.
|
||||
/// However, any new declarations or assignments in scopes that are visible
|
||||
/// when the closure was created will be reflected.
|
||||
AsyncEnvironment closure() => new AsyncEnvironment._(
|
||||
_variables.toList(),
|
||||
_functions.toList(),
|
||||
_mixins.toList(),
|
||||
_contentBlock,
|
||||
_contentEnvironment);
|
||||
|
||||
/// Returns a new environment.
|
||||
///
|
||||
/// The returned environment shares this environment's global, but is
|
||||
/// otherwise independent.
|
||||
AsyncEnvironment global() => new AsyncEnvironment._(
|
||||
[_variables.first], [_functions.first], [_mixins.first], null, null);
|
||||
|
||||
/// Returns the value of the variable named [name], or `null` if no such
|
||||
/// variable is declared.
|
||||
Value getVariable(String name) {
|
||||
var index = _variableIndices[name];
|
||||
if (index != null) return _variables[index][name];
|
||||
|
||||
index = _variableIndex(name);
|
||||
if (index == null) return null;
|
||||
|
||||
_variableIndices[name] = index;
|
||||
return _variables[index][name];
|
||||
}
|
||||
|
||||
/// Returns whether a variable named [name] exists.
|
||||
bool variableExists(String name) => getVariable(name) != null;
|
||||
|
||||
/// Returns whether a global variable named [name] exists.
|
||||
bool globalVariableExists(String name) => _variables.first.containsKey(name);
|
||||
|
||||
/// Returns the index of the last map in [_variables] that has a [name] key,
|
||||
/// or `null` if none exists.
|
||||
int _variableIndex(String name) {
|
||||
for (var i = _variables.length - 1; i >= 0; i--) {
|
||||
if (_variables[i].containsKey(name)) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Sets the variable named [name] to [value].
|
||||
///
|
||||
/// If [global] is `true`, this sets the variable at the top-level scope.
|
||||
/// Otherwise, if the variable was already defined, it'll set it in the
|
||||
/// previous scope. If it's undefined, it'll set it in the current scope.
|
||||
void setVariable(String name, Value value, {bool global: false}) {
|
||||
if (global || _variables.length == 1) {
|
||||
// Don't set the index if there's already a variable with the given name,
|
||||
// since local accesses should still return the local variable.
|
||||
_variableIndices.putIfAbsent(name, () => 0);
|
||||
_variables.first[name] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
var index = _variableIndices.putIfAbsent(
|
||||
name, () => _variableIndex(name) ?? _variables.length - 1);
|
||||
if (!_inSemiGlobalScope && index == 0) {
|
||||
index = _variables.length - 1;
|
||||
_variableIndices[name] = index;
|
||||
}
|
||||
|
||||
_variables[index][name] = value;
|
||||
}
|
||||
|
||||
/// Sets the variable named [name] to [value] in the current scope.
|
||||
///
|
||||
/// Unlike [setVariable], this will declare the variable in the current scope
|
||||
/// even if a declaration already exists in an outer scope.
|
||||
void setLocalVariable(String name, Value value) {
|
||||
var index = _variables.length - 1;
|
||||
_variableIndices[name] = index;
|
||||
_variables[index][name] = value;
|
||||
}
|
||||
|
||||
/// Returns the value of the function named [name], or `null` if no such
|
||||
/// function is declared.
|
||||
AsyncCallable getFunction(String name) {
|
||||
var index = _functionIndices[name];
|
||||
if (index != null) return _functions[index][name];
|
||||
|
||||
index = _functionIndex(name);
|
||||
if (index == null) return null;
|
||||
|
||||
_functionIndices[name] = index;
|
||||
return _functions[index][name];
|
||||
}
|
||||
|
||||
/// Returns the index of the last map in [_functions] that has a [name] key,
|
||||
/// or `null` if none exists.
|
||||
int _functionIndex(String name) {
|
||||
for (var i = _functions.length - 1; i >= 0; i--) {
|
||||
if (_functions[i].containsKey(name)) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns whether a function named [name] exists.
|
||||
bool functionExists(String name) => getFunction(name) != null;
|
||||
|
||||
/// Sets the variable named [name] to [value] in the current scope.
|
||||
void setFunction(AsyncCallable callable) {
|
||||
var index = _functions.length - 1;
|
||||
_functionIndices[callable.name] = index;
|
||||
_functions[index][callable.name] = callable;
|
||||
}
|
||||
|
||||
/// Returns the value of the mixin named [name], or `null` if no such mixin is
|
||||
/// declared.
|
||||
AsyncCallable getMixin(String name) {
|
||||
var index = _mixinIndices[name];
|
||||
if (index != null) return _mixins[index][name];
|
||||
|
||||
index = _mixinIndex(name);
|
||||
if (index == null) return null;
|
||||
|
||||
_mixinIndices[name] = index;
|
||||
return _mixins[index][name];
|
||||
}
|
||||
|
||||
/// Returns the index of the last map in [_mixins] that has a [name] key, or
|
||||
/// `null` if none exists.
|
||||
int _mixinIndex(String name) {
|
||||
for (var i = _mixins.length - 1; i >= 0; i--) {
|
||||
if (_mixins[i].containsKey(name)) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns whether a mixin named [name] exists.
|
||||
bool mixinExists(String name) => getMixin(name) != null;
|
||||
|
||||
/// Sets the variable named [name] to [value] in the current scope.
|
||||
void setMixin(AsyncCallable callable) {
|
||||
var index = _mixins.length - 1;
|
||||
_mixinIndices[callable.name] = index;
|
||||
_mixins[index][callable.name] = callable;
|
||||
}
|
||||
|
||||
/// Sets [block] and [environment] as [contentBlock] and [contentEnvironment],
|
||||
/// respectively, for the duration of [callback].
|
||||
Future withContent(List<Statement> block, AsyncEnvironment environment,
|
||||
Future callback()) async {
|
||||
var oldBlock = _contentBlock;
|
||||
var oldEnvironment = _contentEnvironment;
|
||||
_contentBlock = block;
|
||||
_contentEnvironment = environment;
|
||||
await callback();
|
||||
_contentBlock = oldBlock;
|
||||
_contentEnvironment = oldEnvironment;
|
||||
}
|
||||
|
||||
/// Sets [inMixin] to `true` for the duration of [callback].
|
||||
Future asMixin(void callback()) async {
|
||||
var oldInMixin = _inMixin;
|
||||
_inMixin = true;
|
||||
await callback();
|
||||
_inMixin = oldInMixin;
|
||||
}
|
||||
|
||||
/// Runs [callback] in a new scope.
|
||||
///
|
||||
/// Variables, functions, and mixins declared in a given scope are
|
||||
/// inaccessible outside of it. If [semiGlobal] is passed, this scope can
|
||||
/// assign to global variables without a `!global` declaration.
|
||||
Future<T> scope<T>(Future<T> callback(), {bool semiGlobal: false}) async {
|
||||
semiGlobal = semiGlobal && (_inSemiGlobalScope || _variables.length == 1);
|
||||
|
||||
// TODO: avoid creating a new scope if no variables are declared.
|
||||
var wasInSemiGlobalScope = _inSemiGlobalScope;
|
||||
_inSemiGlobalScope = semiGlobal;
|
||||
_variables.add(normalizedMap());
|
||||
_functions.add(normalizedMap());
|
||||
_mixins.add(normalizedMap());
|
||||
try {
|
||||
return await callback();
|
||||
} finally {
|
||||
_inSemiGlobalScope = wasInSemiGlobalScope;
|
||||
for (var name in _variables.removeLast().keys) {
|
||||
_variableIndices.remove(name);
|
||||
}
|
||||
for (var name in _functions.removeLast().keys) {
|
||||
_functionIndices.remove(name);
|
||||
}
|
||||
for (var name in _mixins.removeLast().keys) {
|
||||
_mixinIndices.remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,16 +2,22 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'callable/async.dart';
|
||||
|
||||
export 'callable/async.dart';
|
||||
export 'callable/async_built_in.dart';
|
||||
export 'callable/built_in.dart';
|
||||
export 'callable/plain_css.dart';
|
||||
export 'callable/user_defined.dart';
|
||||
|
||||
/// An interface for objects, such as functions and mixins, that can be invoked
|
||||
/// from Sass by passing in arguments.
|
||||
abstract class Callable {
|
||||
/// The callable's name.
|
||||
String get name;
|
||||
|
||||
///
|
||||
/// This extends [AsyncCallable] because all synchronous callables are also
|
||||
/// usable in asynchronous contexts. [Callable]s are usable with both the
|
||||
/// synchronous and asynchronous `compile()` functions, and as such should be
|
||||
/// used in preference to [AsyncCallable]s if possible.
|
||||
abstract class Callable extends AsyncCallable {
|
||||
// TODO(nweiz): I'd like to include the argument declaration on this interface
|
||||
// as well, but supporting overloads for built-in callables makes that more
|
||||
// difficult. Ideally, we'd define overloads as purely an implementation
|
||||
|
14
lib/src/callable/async.dart
Normal file
14
lib/src/callable/async.dart
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright 2017 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.
|
||||
|
||||
/// An interface for objects, such as functions and mixins, that can be invoked
|
||||
/// from Sass by passing in arguments.
|
||||
///
|
||||
/// This class represents callables that *need* to do asynchronous work. It's
|
||||
/// only compatible with the asynchonous `compile()` methods. If a callback can
|
||||
/// work synchronously, it should be a [Callable] instead.
|
||||
abstract class AsyncCallable {
|
||||
/// The callable's name.
|
||||
String get name;
|
||||
}
|
62
lib/src/callable/async_built_in.dart
Normal file
62
lib/src/callable/async_built_in.dart
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright 2017 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 'dart:async';
|
||||
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../ast/sass.dart';
|
||||
import '../value.dart';
|
||||
import 'async.dart';
|
||||
|
||||
/// An [AsyncBuiltInCallable]'s callback.
|
||||
typedef FutureOr<Value> _Callback(List<Value> arguments);
|
||||
|
||||
/// A callable defined in Dart code.
|
||||
///
|
||||
/// Unlike user-defined callables, built-in callables support overloads. They
|
||||
/// may declare multiple different callbacks with multiple different sets of
|
||||
/// arguments. When the callable is invoked, the first callback with matching
|
||||
/// arguments is invoked.
|
||||
class AsyncBuiltInCallable implements AsyncCallable {
|
||||
final String name;
|
||||
|
||||
/// The overloads declared for this callable.
|
||||
final _overloads = <Tuple2<ArgumentDeclaration, _Callback>>[];
|
||||
|
||||
/// Creates a callable with a single [arguments] declaration and a single
|
||||
/// [callback].
|
||||
///
|
||||
/// The argument declaration is parsed from [arguments], which should not
|
||||
/// include parentheses. Throws a [SassFormatException] if parsing fails.
|
||||
AsyncBuiltInCallable(this.name, String arguments,
|
||||
FutureOr<Value> callback(List<Value> arguments)) {
|
||||
_overloads
|
||||
.add(new Tuple2(new ArgumentDeclaration.parse(arguments), callback));
|
||||
}
|
||||
|
||||
/// Creates a callable with multiple implementations.
|
||||
///
|
||||
/// Each key/value pair in [overloads] defines the argument declaration for
|
||||
/// the overload (which should not include parentheses), and the callback to
|
||||
/// execute if that argument declaration matches. Throws a
|
||||
/// [SassFormatException] if parsing fails.
|
||||
AsyncBuiltInCallable.overloaded(this.name, Map<String, _Callback> overloads) {
|
||||
overloads.forEach((arguments, callback) {
|
||||
_overloads
|
||||
.add(new Tuple2(new ArgumentDeclaration.parse(arguments), callback));
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the argument declaration and Dart callback for the given
|
||||
/// positional and named arguments.
|
||||
///
|
||||
/// Note that this doesn't guarantee that [positional] and [names] are valid
|
||||
/// for the returned [ArgumentDeclaration].
|
||||
Tuple2<ArgumentDeclaration, _Callback> callbackFor(
|
||||
int positional, Set<String> names) =>
|
||||
_overloads.take(_overloads.length - 1).firstWhere(
|
||||
(overload) => overload.item1.matches(positional, names),
|
||||
orElse: () => _overloads.last);
|
||||
}
|
@ -7,6 +7,7 @@ import 'package:tuple/tuple.dart';
|
||||
import '../ast/sass.dart';
|
||||
import '../callable.dart';
|
||||
import '../value.dart';
|
||||
import 'async_built_in.dart';
|
||||
|
||||
typedef Value _Callback(List<Value> arguments);
|
||||
|
||||
@ -16,7 +17,7 @@ typedef Value _Callback(List<Value> arguments);
|
||||
/// may declare multiple different callbacks with multiple different sets of
|
||||
/// arguments. When the callable is invoked, the first callback with matching
|
||||
/// arguments is invoked.
|
||||
class BuiltInCallable implements Callable {
|
||||
class BuiltInCallable implements Callable, AsyncBuiltInCallable {
|
||||
final String name;
|
||||
|
||||
/// The overloads declared for this callable.
|
||||
|
@ -4,15 +4,16 @@
|
||||
|
||||
import '../ast/sass.dart';
|
||||
import '../callable.dart';
|
||||
import '../environment.dart';
|
||||
|
||||
/// A callback defined in the user's Sass stylesheet.
|
||||
class UserDefinedCallable implements Callable {
|
||||
///
|
||||
/// The type parameter [E] should either be `Environment` or `AsyncEnvironment`.
|
||||
class UserDefinedCallable<E> implements Callable {
|
||||
/// The declaration.
|
||||
final CallableDeclaration declaration;
|
||||
|
||||
/// The environment in which this callable was declared.
|
||||
final Environment environment;
|
||||
final E environment;
|
||||
|
||||
String get name => declaration.name;
|
||||
|
||||
|
@ -2,12 +2,15 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'ast/sass.dart';
|
||||
import 'importer.dart';
|
||||
import 'importer/node.dart';
|
||||
import 'io.dart';
|
||||
import 'sync_package_resolver.dart';
|
||||
import 'util/path.dart';
|
||||
import 'visitor/async_evaluate.dart';
|
||||
import 'visitor/evaluate.dart';
|
||||
import 'visitor/serialize.dart';
|
||||
|
||||
@ -57,16 +60,9 @@ CompileResult compileString(String source,
|
||||
? new Stylesheet.parseSass(source, url: url, color: color)
|
||||
: new Stylesheet.parseScss(source, url: url, color: color);
|
||||
|
||||
var importerList = (importers?.toList() ?? []);
|
||||
if (loadPaths != null) {
|
||||
importerList.addAll(loadPaths.map((path) => new FilesystemImporter(path)));
|
||||
}
|
||||
if (packageResolver != null) {
|
||||
importerList.add(new PackageImporter(packageResolver));
|
||||
}
|
||||
|
||||
var evaluateResult = evaluate(sassTree,
|
||||
importers: importerList,
|
||||
importers: (importers?.toList() ?? [])
|
||||
..addAll(_toImporters(loadPaths, packageResolver)),
|
||||
nodeImporter: nodeImporter,
|
||||
importer: importer,
|
||||
color: color);
|
||||
@ -79,6 +75,81 @@ CompileResult compileString(String source,
|
||||
return new CompileResult(css, evaluateResult.includedFiles);
|
||||
}
|
||||
|
||||
/// Like [compileAsync] in `lib/sass.dart`, but provides more options to support
|
||||
/// the node-sass compatible API.
|
||||
Future<CompileResult> compileAsync(String path,
|
||||
{bool indented,
|
||||
bool color: false,
|
||||
Iterable<AsyncImporter> importers,
|
||||
NodeImporter nodeImporter,
|
||||
SyncPackageResolver packageResolver,
|
||||
Iterable<String> loadPaths,
|
||||
OutputStyle style,
|
||||
bool useSpaces: true,
|
||||
int indentWidth,
|
||||
LineFeed lineFeed}) =>
|
||||
compileStringAsync(readFile(path),
|
||||
indented: indented ?? p.extension(path) == '.sass',
|
||||
color: color,
|
||||
importers: importers,
|
||||
nodeImporter: nodeImporter,
|
||||
packageResolver: packageResolver,
|
||||
loadPaths: loadPaths,
|
||||
importer: new FilesystemImporter('.'),
|
||||
style: style,
|
||||
useSpaces: useSpaces,
|
||||
indentWidth: indentWidth,
|
||||
lineFeed: lineFeed,
|
||||
url: p.toUri(path));
|
||||
|
||||
/// Like [compileStringAsync] in `lib/sass.dart`, but provides more options to
|
||||
/// support the node-sass compatible API.
|
||||
Future<CompileResult> compileStringAsync(String source,
|
||||
{bool indented: false,
|
||||
bool color: false,
|
||||
Iterable<AsyncImporter> importers,
|
||||
NodeImporter nodeImporter,
|
||||
SyncPackageResolver packageResolver,
|
||||
Iterable<String> loadPaths,
|
||||
AsyncImporter importer,
|
||||
OutputStyle style,
|
||||
bool useSpaces: true,
|
||||
int indentWidth,
|
||||
LineFeed lineFeed,
|
||||
url}) async {
|
||||
var sassTree = indented
|
||||
? new Stylesheet.parseSass(source, url: url, color: color)
|
||||
: new Stylesheet.parseScss(source, url: url, color: color);
|
||||
|
||||
var evaluateResult = await evaluateAsync(sassTree,
|
||||
importers: (importers?.toList() ?? [])
|
||||
..addAll(_toImporters(loadPaths, packageResolver)),
|
||||
nodeImporter: nodeImporter,
|
||||
importer: importer,
|
||||
color: color);
|
||||
var css = serialize(evaluateResult.stylesheet,
|
||||
style: style,
|
||||
useSpaces: useSpaces,
|
||||
indentWidth: indentWidth,
|
||||
lineFeed: lineFeed);
|
||||
|
||||
return new CompileResult(css, evaluateResult.includedFiles);
|
||||
}
|
||||
|
||||
/// Converts the user's [loadPaths] and [packageResolver] options into
|
||||
/// importers.
|
||||
List<Importer> _toImporters(
|
||||
Iterable<String> loadPaths, SyncPackageResolver packageResolver) {
|
||||
var list = <Importer>[];
|
||||
if (loadPaths != null) {
|
||||
list.addAll(loadPaths.map((path) => new FilesystemImporter(path)));
|
||||
}
|
||||
if (packageResolver != null) {
|
||||
list.add(new PackageImporter(packageResolver));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// The result of compiling a Sass document to CSS, along with metadata about
|
||||
/// the compilation process.
|
||||
class CompileResult {
|
||||
|
@ -2,6 +2,11 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
// DO NOT EDIT. This file was generated from async_environment.dart.
|
||||
// See tool/synchronize.dart for details.
|
||||
//
|
||||
// Checksum: 97410abbd78c3bbc9899f3ac460cc0736218bfe3
|
||||
|
||||
import 'ast/sass.dart';
|
||||
import 'callable.dart';
|
||||
import 'functions.dart';
|
||||
|
@ -27,7 +27,11 @@ main(List<String> args) async {
|
||||
..addFlag('help',
|
||||
abbr: 'h', help: 'Print this usage information.', negatable: false)
|
||||
..addFlag('version',
|
||||
help: 'Print the version of Dart Sass.', negatable: false);
|
||||
help: 'Print the version of Dart Sass.', negatable: false)
|
||||
|
||||
// This is used when testing to ensure that the asynchronous evaluator path
|
||||
// works the same as the synchronous one.
|
||||
..addFlag('async', hide: true);
|
||||
|
||||
ArgResults options;
|
||||
try {
|
||||
@ -55,13 +59,20 @@ main(List<String> args) async {
|
||||
|
||||
var color =
|
||||
options.wasParsed('color') ? options['color'] as bool : hasTerminal;
|
||||
var asynchronous = options['async'] as bool;
|
||||
try {
|
||||
String css;
|
||||
if (stdinFlag) {
|
||||
css = await _compileStdin();
|
||||
css = await _compileStdin(asynchronous: asynchronous);
|
||||
} else {
|
||||
var input = options.rest.first;
|
||||
css = input == '-' ? await _compileStdin() : compile(input, color: color);
|
||||
if (input == '-') {
|
||||
css = await _compileStdin(asynchronous: asynchronous);
|
||||
} else if (asynchronous) {
|
||||
css = await compileAsync(input, color: color);
|
||||
} else {
|
||||
css = compile(input, color: color);
|
||||
}
|
||||
}
|
||||
|
||||
if (css.isNotEmpty) print(css);
|
||||
@ -123,9 +134,16 @@ Future<String> _loadVersion() async {
|
||||
}
|
||||
|
||||
/// Compiles Sass from standard input and returns the result.
|
||||
Future<String> _compileStdin({bool color: false}) async =>
|
||||
compileString(await readStdin(),
|
||||
color: color, importer: new FilesystemImporter('.'));
|
||||
Future<String> _compileStdin(
|
||||
{bool asynchronous: false, bool color: false}) async {
|
||||
var text = await readStdin();
|
||||
var importer = new FilesystemImporter('.');
|
||||
if (asynchronous) {
|
||||
return await compileStringAsync(text, color: color, importer: importer);
|
||||
} else {
|
||||
return compileString(text, color: color, importer: importer);
|
||||
}
|
||||
}
|
||||
|
||||
/// Print the usage information for Sass, with [message] as a header.
|
||||
void _printUsage(ArgParser parser, String message) {
|
||||
|
@ -2,9 +2,11 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'importer/async.dart';
|
||||
import 'importer/no_op.dart';
|
||||
import 'importer/result.dart';
|
||||
|
||||
export 'importer/async.dart';
|
||||
export 'importer/filesystem.dart';
|
||||
export 'importer/package.dart';
|
||||
export 'importer/result.dart';
|
||||
@ -16,55 +18,20 @@ export 'importer/result.dart';
|
||||
/// of the importer. For example, the default filesystem importer returns its
|
||||
/// load path.
|
||||
///
|
||||
/// This extends [AsyncImporter] to guarantee that [canonicalize] and [load] are
|
||||
/// synchronous. It's usable with both the synchronous and asynchronous
|
||||
/// `compile()` functions, and as such should be extended in preference to
|
||||
/// [AsyncImporter] if possible.
|
||||
///
|
||||
/// Subclasses should extend [Importer], not implement it.
|
||||
abstract class Importer {
|
||||
abstract class Importer extends AsyncImporter {
|
||||
/// An importer that never imports any stylesheets.
|
||||
///
|
||||
/// This is used for stylesheets which don't support relative imports, such as
|
||||
/// those created from Dart code with plain strings.
|
||||
static final Importer noOp = new NoOpImporter();
|
||||
|
||||
/// If [url] is recognized by this importer, returns its canonical format.
|
||||
///
|
||||
/// If Sass has already loaded a stylesheet with the returned canonical URL,
|
||||
/// it re-uses the existing parse tree. This means that importers **must
|
||||
/// ensure** that the same canonical URL always refers to the same stylesheet,
|
||||
/// *even across different importers*.
|
||||
///
|
||||
/// This may return `null` if [url] isn't recognized by this importer.
|
||||
///
|
||||
/// If this importer's URL format supports file extensions, it should
|
||||
/// canonicalize them the same way as the default filesystem importer:
|
||||
///
|
||||
/// * If the [url] ends in `.sass` or `.scss`, the importer should look for
|
||||
/// a stylesheet with that exact URL and return `null` if it's not found.
|
||||
///
|
||||
/// * Otherwise, the importer should look for a stylesheet at `"$url.sass"` or
|
||||
/// one at `"$url.scss"`, in that order. If neither is found, it should
|
||||
/// return `null`.
|
||||
///
|
||||
/// Sass assumes that calling [canonicalize] multiple times with the same URL
|
||||
/// will return the same result.
|
||||
Uri canonicalize(Uri url);
|
||||
|
||||
/// Loads the Sass text for the given [url], or returns `null` if
|
||||
/// this importer can't find the stylesheet it refers to.
|
||||
///
|
||||
/// The [url] comes from a call to [canonicalize] for this importer.
|
||||
///
|
||||
/// When Sass encounters an `@import` rule in a stylesheet, it first calls
|
||||
/// [canonicalize] and [load] on the importer that first loaded that
|
||||
/// stylesheet with the imported URL resolved relative to the stylesheet's
|
||||
/// original URL. If either of those returns `null`, it then calls
|
||||
/// [canonicalize] and [load] on each importer in order with the URL as it
|
||||
/// appears in the `@import` rule.
|
||||
///
|
||||
/// If the importer finds a stylesheet at [url] but it fails to load for some
|
||||
/// reason, or if [url] is uniquely associated with this importer but doesn't
|
||||
/// refer to a real stylesheet, the importer may throw an exception that will
|
||||
/// be wrapped by Sass. If the exception object has a `message` property, it
|
||||
/// will be used as the wrapped exception's message; otherwise, the exception
|
||||
/// object's `toString()` will be used. This means it's safe for importers to
|
||||
/// throw plain strings.
|
||||
ImporterResult load(Uri url);
|
||||
}
|
||||
|
66
lib/src/importer/async.dart
Normal file
66
lib/src/importer/async.dart
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright 2017 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 'dart:async';
|
||||
|
||||
import 'result.dart';
|
||||
|
||||
/// An interface for importers that resolves URLs in `@import`s to the contents
|
||||
/// of Sass files.
|
||||
///
|
||||
/// Importers should override [toString] to provide a human-readable description
|
||||
/// of the importer. For example, the default filesystem importer returns its
|
||||
/// load path.
|
||||
///
|
||||
/// This class should only be extended by importers that *need* to do
|
||||
/// asynchronous work. It's only compatible with the asynchronous `compile()`
|
||||
/// methods. If an importer can work synchronously, it should extend [Importer]
|
||||
/// instead.
|
||||
///
|
||||
/// Subclasses should extend [AsyncImporter], not implement it.
|
||||
abstract class AsyncImporter {
|
||||
/// If [url] is recognized by this importer, returns its canonical format.
|
||||
///
|
||||
/// If Sass has already loaded a stylesheet with the returned canonical URL,
|
||||
/// it re-uses the existing parse tree. This means that importers **must
|
||||
/// ensure** that the same canonical URL always refers to the same stylesheet,
|
||||
/// *even across different importers*.
|
||||
///
|
||||
/// This may return `null` if [url] isn't recognized by this importer.
|
||||
///
|
||||
/// If this importer's URL format supports file extensions, it should
|
||||
/// canonicalize them the same way as the default filesystem importer:
|
||||
///
|
||||
/// * If the [url] ends in `.sass` or `.scss`, the importer should look for
|
||||
/// a stylesheet with that exact URL and return `null` if it's not found.
|
||||
///
|
||||
/// * Otherwise, the importer should look for a stylesheet at `"$url.sass"` or
|
||||
/// one at `"$url.scss"`, in that order. If neither is found, it should
|
||||
/// return `null`.
|
||||
///
|
||||
/// Sass assumes that calling [canonicalize] multiple times with the same URL
|
||||
/// will return the same result.
|
||||
FutureOr<Uri> canonicalize(Uri url);
|
||||
|
||||
/// Loads the Sass text for the given [url], or returns `null` if
|
||||
/// this importer can't find the stylesheet it refers to.
|
||||
///
|
||||
/// The [url] comes from a call to [canonicalize] for this importer.
|
||||
///
|
||||
/// When Sass encounters an `@import` rule in a stylesheet, it first calls
|
||||
/// [canonicalize] and [load] on the importer that first loaded that
|
||||
/// stylesheet with the imported URL resolved relative to the stylesheet's
|
||||
/// original URL. If either of those returns `null`, it then calls
|
||||
/// [canonicalize] and [load] on each importer in order with the URL as it
|
||||
/// appears in the `@import` rule.
|
||||
///
|
||||
/// If the importer finds a stylesheet at [url] but it fails to load for some
|
||||
/// reason, or if [url] is uniquely associated with this importer but doesn't
|
||||
/// refer to a real stylesheet, the importer may throw an exception that will
|
||||
/// be wrapped by Sass. If the exception object has a `message` property, it
|
||||
/// will be used as the wrapped exception's message; otherwise, the exception
|
||||
/// object's `toString()` will be used. This means it's safe for importers to
|
||||
/// throw plain strings.
|
||||
FutureOr<ImporterResult> load(Uri url);
|
||||
}
|
@ -2,6 +2,9 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:js/js.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../../io.dart';
|
||||
@ -10,7 +13,7 @@ import '../../node/utils.dart';
|
||||
import '../../util/path.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
typedef _Importer(String url, String prev);
|
||||
typedef _Importer(String url, String prev, [void done(result)]);
|
||||
|
||||
/// An importer that encapsulates Node Sass's import logic.
|
||||
///
|
||||
@ -67,26 +70,30 @@ class NodeImporter {
|
||||
previous.scheme == 'file' ? p.fromUri(previous) : previous.toString();
|
||||
for (var importer in _importers) {
|
||||
var value = call2(importer, _context, urlString, previousString);
|
||||
if (value == null) continue;
|
||||
if (isJSError(value)) throw value;
|
||||
if (value != null) return _handleImportResult(url, previous, value);
|
||||
}
|
||||
|
||||
NodeImporterResult result;
|
||||
try {
|
||||
result = value as NodeImporterResult;
|
||||
} on CastError {
|
||||
// is reports a different result than as here. I can't find a minimal
|
||||
// reproduction, but it seems likely to be related to sdk#26838.
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (result.file != null) {
|
||||
var resolved = _resolvePath(result.file, previous);
|
||||
if (resolved != null) return resolved;
|
||||
/// Asynchronously loads the stylesheet at [url].
|
||||
///
|
||||
/// The [previous] URL is the URL of the stylesheet in which the import
|
||||
/// appeared. Returns the contents of the stylesheet and the URL to use as
|
||||
/// [previous] for imports within the loaded stylesheet.
|
||||
Future<Tuple2<String, Uri>> loadAsync(Uri url, Uri previous) async {
|
||||
if (url.scheme == '' || url.scheme == 'file') {
|
||||
var result = _resolvePath(p.fromUri(url), previous);
|
||||
if (result != null) return result;
|
||||
}
|
||||
|
||||
throw "Can't find stylesheet to import.";
|
||||
} else {
|
||||
return new Tuple2(result.contents ?? '', url);
|
||||
}
|
||||
// The previous URL is always an absolute file path for filesystem imports.
|
||||
var urlString = url.toString();
|
||||
var previousString =
|
||||
previous.scheme == 'file' ? p.fromUri(previous) : previous.toString();
|
||||
for (var importer in _importers) {
|
||||
var value = await _callImporterAsync(importer, urlString, previousString);
|
||||
if (value != null) return _handleImportResult(url, previous, value);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -129,4 +136,39 @@ class NodeImporter {
|
||||
? null
|
||||
: new Tuple2(readFile(resolved), p.toUri(resolved));
|
||||
}
|
||||
|
||||
/// Converts an [_Importer]'s return [value] to a tuple that can be returned
|
||||
/// by [load].
|
||||
Tuple2<String, Uri> _handleImportResult(Uri url, Uri previous, Object value) {
|
||||
if (isJSError(value)) throw value;
|
||||
|
||||
NodeImporterResult result;
|
||||
try {
|
||||
result = value as NodeImporterResult;
|
||||
} on CastError {
|
||||
// is reports a different result than as here. I can't find a minimal
|
||||
// reproduction, but it seems likely to be related to sdk#26838.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (result.file != null) {
|
||||
var resolved = _resolvePath(result.file, previous);
|
||||
if (resolved != null) return resolved;
|
||||
|
||||
throw "Can't find stylesheet to import.";
|
||||
} else {
|
||||
return new Tuple2(result.contents ?? '', url);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls an importer that may or may not be asynchronous.
|
||||
Future<Object> _callImporterAsync(
|
||||
_Importer importer, String urlString, String previousString) async {
|
||||
var completer = new Completer();
|
||||
|
||||
var result = call3(importer, _context, urlString, previousString,
|
||||
allowInterop(completer.complete));
|
||||
if (isUndefined(result)) return await completer.future;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,17 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
typedef _Importer(String url, String prev);
|
||||
typedef _Importer(String url, String prev, [void done(result)]);
|
||||
|
||||
class NodeImporter {
|
||||
NodeImporter(Object context, Iterable<String> includePaths,
|
||||
Iterable<_Importer> importers);
|
||||
|
||||
Tuple2<String, Uri> load(Uri url, Uri previous) => null;
|
||||
|
||||
Future<Tuple2<String, Uri>> loadAsync(Uri url, Uri previous) => null;
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:js/js.dart';
|
||||
|
||||
@ -20,7 +22,7 @@ import 'util/path.dart';
|
||||
import 'value/number.dart';
|
||||
import 'visitor/serialize.dart';
|
||||
|
||||
typedef _Importer(String url, String prev);
|
||||
typedef _Importer(String url, String prev, [void done(result)]);
|
||||
|
||||
/// The entrypoint for Node.js.
|
||||
///
|
||||
@ -46,37 +48,29 @@ void main() {
|
||||
/// [render]: https://github.com/sass/node-sass#options
|
||||
void _render(RenderOptions options,
|
||||
void callback(RenderError error, RenderResult result)) {
|
||||
try {
|
||||
callback(null, _doRender(options));
|
||||
} on SassException catch (error) {
|
||||
callback(_wrapException(error), null);
|
||||
} catch (error) {
|
||||
callback(newRenderError(error.toString(), status: 3), null);
|
||||
if (options.fiber != null) {
|
||||
options.fiber.call(allowInterop(() {
|
||||
try {
|
||||
callback(null, _renderSync(options));
|
||||
} catch (error) {
|
||||
callback(error as RenderError, null);
|
||||
}
|
||||
})).run();
|
||||
} else {
|
||||
_renderAsync(options).then((result) {
|
||||
callback(null, result);
|
||||
}, onError: (error, stackTrace) {
|
||||
if (error is SassException) {
|
||||
callback(_wrapException(error), null);
|
||||
} else {
|
||||
callback(newRenderError(error.toString(), status: 3), null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts Sass to CSS.
|
||||
///
|
||||
/// This attempts to match the [node-sass `renderSync()` API][render] as closely
|
||||
/// as possible.
|
||||
///
|
||||
/// [render]: https://github.com/sass/node-sass#options
|
||||
RenderResult _renderSync(RenderOptions options) {
|
||||
try {
|
||||
return _doRender(options);
|
||||
} on SassException catch (error) {
|
||||
jsThrow(_wrapException(error));
|
||||
} catch (error) {
|
||||
jsThrow(newRenderError(error.toString(), status: 3));
|
||||
}
|
||||
throw "unreachable";
|
||||
}
|
||||
|
||||
/// Converts Sass to CSS.
|
||||
///
|
||||
/// Unlike [_render] and [_renderSync], this doesn't do any special handling for
|
||||
/// Dart exceptions.
|
||||
RenderResult _doRender(RenderOptions options) {
|
||||
/// Converts Sass to CSS asynchronously.
|
||||
Future<RenderResult> _renderAsync(RenderOptions options) async {
|
||||
var start = new DateTime.now();
|
||||
CompileResult result;
|
||||
if (options.data != null) {
|
||||
@ -85,7 +79,7 @@ RenderResult _doRender(RenderOptions options) {
|
||||
"options.data and options.file may not both be set.");
|
||||
}
|
||||
|
||||
result = compileString(options.data,
|
||||
result = await compileStringAsync(options.data,
|
||||
nodeImporter: _parseImporter(options, start),
|
||||
indented: options.indentedSyntax ?? false,
|
||||
style: _parseOutputStyle(options.outputStyle),
|
||||
@ -94,7 +88,7 @@ RenderResult _doRender(RenderOptions options) {
|
||||
lineFeed: _parseLineFeed(options.linefeed),
|
||||
url: 'stdin');
|
||||
} else if (options.file != null) {
|
||||
result = compile(options.file,
|
||||
result = await compileAsync(options.file,
|
||||
nodeImporter: _parseImporter(options, start),
|
||||
indented: options.indentedSyntax,
|
||||
style: _parseOutputStyle(options.outputStyle),
|
||||
@ -114,6 +108,58 @@ RenderResult _doRender(RenderOptions options) {
|
||||
includedFiles: result.includedFiles.toList());
|
||||
}
|
||||
|
||||
/// Converts Sass to CSS.
|
||||
///
|
||||
/// This attempts to match the [node-sass `renderSync()` API][render] as closely
|
||||
/// as possible.
|
||||
///
|
||||
/// [render]: https://github.com/sass/node-sass#options
|
||||
RenderResult _renderSync(RenderOptions options) {
|
||||
try {
|
||||
var start = new DateTime.now();
|
||||
CompileResult result;
|
||||
if (options.data != null) {
|
||||
if (options.file != null) {
|
||||
throw new ArgumentError(
|
||||
"options.data and options.file may not both be set.");
|
||||
}
|
||||
|
||||
result = compileString(options.data,
|
||||
nodeImporter: _parseImporter(options, start),
|
||||
indented: options.indentedSyntax ?? false,
|
||||
style: _parseOutputStyle(options.outputStyle),
|
||||
useSpaces: options.indentType != 'tab',
|
||||
indentWidth: _parseIndentWidth(options.indentWidth),
|
||||
lineFeed: _parseLineFeed(options.linefeed),
|
||||
url: 'stdin');
|
||||
} else if (options.file != null) {
|
||||
result = compile(options.file,
|
||||
nodeImporter: _parseImporter(options, start),
|
||||
indented: options.indentedSyntax,
|
||||
style: _parseOutputStyle(options.outputStyle),
|
||||
useSpaces: options.indentType != 'tab',
|
||||
indentWidth: _parseIndentWidth(options.indentWidth),
|
||||
lineFeed: _parseLineFeed(options.linefeed));
|
||||
} else {
|
||||
throw new ArgumentError(
|
||||
"Either options.data or options.file must be set.");
|
||||
}
|
||||
var end = new DateTime.now();
|
||||
|
||||
return newRenderResult(result.css,
|
||||
entry: options.file ?? 'data',
|
||||
start: start.millisecondsSinceEpoch,
|
||||
end: end.millisecondsSinceEpoch,
|
||||
duration: end.difference(start).inMilliseconds,
|
||||
includedFiles: result.includedFiles.toList());
|
||||
} on SassException catch (error) {
|
||||
jsThrow(_wrapException(error));
|
||||
} catch (error) {
|
||||
jsThrow(newRenderError(error.toString(), status: 3));
|
||||
}
|
||||
throw "unreachable";
|
||||
}
|
||||
|
||||
/// Converts a [SassException] to a [RenderError].
|
||||
RenderError _wrapException(SassException exception) {
|
||||
var trace = exception is SassRuntimeException
|
||||
@ -170,6 +216,23 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) {
|
||||
context.options.context = context;
|
||||
}
|
||||
|
||||
if (options.fiber != null) {
|
||||
importers = importers.map((importer) {
|
||||
return allowInteropCaptureThis((thisArg, String url, String previous,
|
||||
[_]) {
|
||||
var fiber = options.fiber.current;
|
||||
var result =
|
||||
call3(importer, thisArg, url, previous, allowInterop((result) {
|
||||
// Schedule a microtask so we don't try to resume the running fiber if
|
||||
// [importer] calls `done()` synchronously.
|
||||
scheduleMicrotask(() => fiber.run(result));
|
||||
}));
|
||||
if (isUndefined(result)) return options.fiber.yield();
|
||||
return result;
|
||||
}) as _Importer;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return new NodeImporter(context, includePaths, importers);
|
||||
}
|
||||
|
||||
|
22
lib/src/node/fiber.dart
Normal file
22
lib/src/node/fiber.dart
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright 2017 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:js/js.dart';
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
class FiberClass {
|
||||
// Work around sdk#31490.
|
||||
external Fiber call(function());
|
||||
|
||||
external Fiber get current;
|
||||
|
||||
external yield([value]);
|
||||
}
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
class Fiber {
|
||||
external run([value]);
|
||||
}
|
@ -4,6 +4,8 @@
|
||||
|
||||
import 'package:js/js.dart';
|
||||
|
||||
import 'fiber.dart';
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
class RenderOptions {
|
||||
@ -16,6 +18,7 @@ class RenderOptions {
|
||||
external String get indentType;
|
||||
external dynamic get indentWidth;
|
||||
external String get linefeed;
|
||||
external FiberClass get fiber;
|
||||
|
||||
external factory RenderOptions(
|
||||
{String file,
|
||||
@ -26,5 +29,6 @@ class RenderOptions {
|
||||
String outputStyle,
|
||||
String indentType,
|
||||
indentWidth,
|
||||
String linefeed});
|
||||
String linefeed,
|
||||
FiberClass fiber});
|
||||
}
|
||||
|
@ -23,6 +23,11 @@ void jsThrow(error) => _jsThrow.call(error);
|
||||
|
||||
final _jsThrow = new JSFunction("error", "throw error;");
|
||||
|
||||
/// Returns whether or not [value] is the JS `undefined` value.
|
||||
bool isUndefined(value) => _isUndefined.call(value) as bool;
|
||||
|
||||
final _isUndefined = new JSFunction("value", "return value === undefined;");
|
||||
|
||||
@JS("Error")
|
||||
external Function get _JSError;
|
||||
|
||||
@ -33,3 +38,8 @@ bool isJSError(value) => instanceof(value, _JSError) as bool;
|
||||
R call2<R, A1, A2>(
|
||||
R function(A1 arg1, A2 arg2), Object thisArg, A1 arg1, A2 arg2) =>
|
||||
(function as JSFunction).apply(thisArg, [arg1, arg2]) as R;
|
||||
|
||||
/// Invokes [function] with [thisArg] as `this`.
|
||||
R call3<R, A1, A2, A3>(R function(A1 arg1, A2 arg2, A3 arg3), Object thisArg,
|
||||
A1 arg1, A2 arg2, A3 arg3) =>
|
||||
(function as JSFunction).apply(thisArg, [arg1, arg2, arg3]) as R;
|
||||
|
@ -2,6 +2,7 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
|
||||
@ -273,6 +274,43 @@ void rotateSlice(List list, int start, int end) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [Iterable.map] but for an asynchronous [callback].
|
||||
Future<Iterable<F>> mapAsync<E, F>(
|
||||
Iterable<E> iterable, Future<F> callback(E value)) async {
|
||||
var result = <F>[];
|
||||
for (var element in iterable) {
|
||||
result.add(await callback(element));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Like [Map.putIfAbsent], but for an asynchronous [ifAbsent].
|
||||
///
|
||||
/// Note that this is *not* safe to call in parallel on the same map with the
|
||||
/// same key.
|
||||
Future<V> putIfAbsentAsync<K, V>(
|
||||
Map<K, V> map, K key, Future<V> ifAbsent()) async {
|
||||
if (map.containsKey(key)) return map[key];
|
||||
var value = await ifAbsent();
|
||||
map[key] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Like [normalizedMapMap], but for asynchronous [key] and [value].
|
||||
Future<Map<String, V2>> normalizedMapMapAsync<K, V1, V2>(Map<K, V1> map,
|
||||
{Future<String> key(K key, V1 value),
|
||||
Future<V2> value(K key, V1 value)}) async {
|
||||
key ??= (mapKey, _) async => mapKey as String;
|
||||
value ??= (_, mapValue) async => mapValue as V2;
|
||||
|
||||
var result = normalizedMap<V2>();
|
||||
for (var mapKey in map.keys) {
|
||||
var mapValue = map[mapKey];
|
||||
result[await key(mapKey, mapValue)] = await value(mapKey, mapValue);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Prints a warning to standard error, associated with [span].
|
||||
///
|
||||
/// If [color] is `true`, this uses terminal colors.
|
||||
|
@ -12,7 +12,11 @@ import '../value.dart';
|
||||
/// it may be passed between modules.
|
||||
class SassFunction extends Value {
|
||||
/// The callable that this function invokes.
|
||||
final Callable callable;
|
||||
///
|
||||
/// Note that this is typed as an [AsyncCallback] so that it will work with
|
||||
/// both synchronous and asynchronous evaluate visitors, but in practice the
|
||||
/// synchronous evaluate visitor will crash if this isn't a [Callback].
|
||||
final AsyncCallable callable;
|
||||
|
||||
SassFunction(this.callable);
|
||||
|
||||
|
1772
lib/src/visitor/async_evaluate.dart
Normal file
1772
lib/src/visitor/async_evaluate.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,11 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
// DO NOT EDIT. This file was generated from async_evaluate.dart.
|
||||
// See tool/synchronize.dart for details.
|
||||
//
|
||||
// Checksum: ae64ba442752642066f0e9e038f5f2c1bbfa866b
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:charcode/charcode.dart';
|
||||
@ -13,9 +18,9 @@ import 'package:tuple/tuple.dart';
|
||||
import '../ast/css.dart';
|
||||
import '../ast/sass.dart';
|
||||
import '../ast/selector.dart';
|
||||
import '../environment.dart';
|
||||
import '../callable.dart';
|
||||
import '../color_names.dart';
|
||||
import '../environment.dart';
|
||||
import '../exception.dart';
|
||||
import '../extend/extender.dart';
|
||||
import '../importer.dart';
|
||||
@ -29,7 +34,7 @@ import 'interface/statement.dart';
|
||||
import 'interface/expression.dart';
|
||||
|
||||
/// A function that takes a callback with no arguments.
|
||||
typedef _ScopeCallback(callback());
|
||||
typedef void _ScopeCallback(void callback());
|
||||
|
||||
/// The URL used in stack traces when no source URL is available.
|
||||
final _noSourceUrl = Uri.parse("-");
|
||||
@ -246,8 +251,14 @@ class _EvaluateVisitor
|
||||
return expression.accept(this);
|
||||
}
|
||||
|
||||
return _runFunctionCallable(invocation,
|
||||
function.assertFunction("function").callable, _callableSpan);
|
||||
var callable = function.assertFunction("function").callable;
|
||||
if (callable is Callable) {
|
||||
return _runFunctionCallable(invocation, callable, _callableSpan);
|
||||
} else {
|
||||
throw new SassScriptException(
|
||||
"The function ${callable.name} is asynchronous.\n"
|
||||
"This is probably caused by a bug in a Sass plugin.");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@ -295,12 +306,12 @@ class _EvaluateVisitor
|
||||
}
|
||||
|
||||
Value visitAtRootRule(AtRootRule node) {
|
||||
var query = node.query == null
|
||||
? AtRootQuery.defaultQuery
|
||||
: _adjustParseError(
|
||||
node.query.span,
|
||||
() => new AtRootQuery.parse(
|
||||
_performInterpolation(node.query, warnForColor: true)));
|
||||
var query = AtRootQuery.defaultQuery;
|
||||
if (node.query != null) {
|
||||
var resolved = _performInterpolation(node.query, warnForColor: true);
|
||||
query = _adjustParseError(
|
||||
node.query.span, () => new AtRootQuery.parse(resolved));
|
||||
}
|
||||
|
||||
var parent = _parent;
|
||||
var included = <CssParentNode>[];
|
||||
@ -378,7 +389,7 @@ class _EvaluateVisitor
|
||||
/// [_parent] to [newParent].
|
||||
_ScopeCallback _scopeForAtRoot(CssParentNode newParent, AtRootQuery query,
|
||||
List<CssParentNode> included) {
|
||||
var scope = (callback()) {
|
||||
var scope = (void callback()) {
|
||||
// We can't use [_withParent] here because it'll add the node to the tree
|
||||
// in the wrong place.
|
||||
var oldParent = _parent;
|
||||
@ -617,11 +628,13 @@ class _EvaluateVisitor
|
||||
}
|
||||
|
||||
Value visitIfRule(IfRule node) {
|
||||
var clause = node.clauses
|
||||
.firstWhere((pair) => pair.item1.accept(this).isTruthy,
|
||||
orElse: () => null)
|
||||
?.item2 ??
|
||||
node.lastClause;
|
||||
var clause = node.lastClause;
|
||||
for (var pair in node.clauses) {
|
||||
if (pair.item1.accept(this).isTruthy) {
|
||||
clause = pair.item2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (clause == null) return null;
|
||||
|
||||
return _environment.scope(
|
||||
@ -786,7 +799,8 @@ class _EvaluateVisitor
|
||||
}
|
||||
|
||||
Value visitIncludeRule(IncludeRule node) {
|
||||
var mixin = _environment.getMixin(node.name) as UserDefinedCallable;
|
||||
var mixin =
|
||||
_environment.getMixin(node.name) as UserDefinedCallable<Environment>;
|
||||
if (mixin == null) {
|
||||
throw _exception("Undefined mixin.", node.span);
|
||||
}
|
||||
@ -795,18 +809,16 @@ class _EvaluateVisitor
|
||||
throw _exception("Mixin doesn't accept a content block.", node.span);
|
||||
}
|
||||
|
||||
Value callback() {
|
||||
_environment.asMixin(() {
|
||||
for (var statement in mixin.declaration.children) {
|
||||
statement.accept(this);
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
var environment = node.children == null ? null : _environment.closure();
|
||||
_runUserDefinedCallable(node.arguments, mixin, node.span, () {
|
||||
_environment.withContent(node.children, environment, callback);
|
||||
_environment.withContent(node.children, environment, () {
|
||||
_environment.asMixin(() {
|
||||
for (var statement in mixin.declaration.children) {
|
||||
statement.accept(this);
|
||||
}
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
return null;
|
||||
@ -869,11 +881,13 @@ class _EvaluateVisitor
|
||||
|
||||
/// Evaluates [interpolation] and parses the result as a list of media
|
||||
/// queries.
|
||||
List<CssMediaQuery> _visitMediaQueries(Interpolation interpolation) =>
|
||||
_adjustParseError(
|
||||
interpolation.span,
|
||||
() => CssMediaQuery.parseList(
|
||||
_performInterpolation(interpolation, warnForColor: true)));
|
||||
List<CssMediaQuery> _visitMediaQueries(Interpolation interpolation) {
|
||||
var resolved = _performInterpolation(interpolation, warnForColor: true);
|
||||
|
||||
// TODO(nweiz): Remove this type argument when sdk#31398 is fixed.
|
||||
return _adjustParseError<List<CssMediaQuery>>(
|
||||
interpolation.span, () => CssMediaQuery.parseList(resolved));
|
||||
}
|
||||
|
||||
/// Returns a list of queries that selects for platforms that match both
|
||||
/// [queries1] and [queries2].
|
||||
@ -1169,7 +1183,7 @@ class _EvaluateVisitor
|
||||
SassColor visitColorExpression(ColorExpression node) => node.value;
|
||||
|
||||
SassList visitListExpression(ListExpression node) => new SassList(
|
||||
node.contents.map((expression) => expression.accept(this)),
|
||||
node.contents.map((Expression expression) => expression.accept(this)),
|
||||
node.separator,
|
||||
brackets: node.hasBrackets);
|
||||
|
||||
@ -1202,7 +1216,7 @@ class _EvaluateVisitor
|
||||
/// Evaluates the arguments in [arguments] as applied to [callable], and
|
||||
/// invokes [run] in a scope with those arguments defined.
|
||||
Value _runUserDefinedCallable(ArgumentInvocation arguments,
|
||||
UserDefinedCallable callable, FileSpan span, Value run()) {
|
||||
UserDefinedCallable<Environment> callable, FileSpan span, Value run()) {
|
||||
var triple = _evaluateArguments(arguments, span);
|
||||
var positional = triple.item1;
|
||||
var named = triple.item2;
|
||||
@ -1267,7 +1281,7 @@ class _EvaluateVisitor
|
||||
ArgumentInvocation arguments, Callable callable, FileSpan span) {
|
||||
if (callable is BuiltInCallable) {
|
||||
return _runBuiltInCallable(arguments, callable, span).withoutSlash();
|
||||
} else if (callable is UserDefinedCallable) {
|
||||
} else if (callable is UserDefinedCallable<Environment>) {
|
||||
return _runUserDefinedCallable(arguments, callable, span, () {
|
||||
for (var statement in callable.declaration.children) {
|
||||
var returnValue = statement.accept(this);
|
||||
@ -1368,7 +1382,7 @@ class _EvaluateVisitor
|
||||
Tuple3<List<Value>, Map<String, Value>, ListSeparator> _evaluateArguments(
|
||||
ArgumentInvocation arguments, FileSpan span) {
|
||||
var positional = arguments.positional
|
||||
.map((expression) => expression.accept(this))
|
||||
.map((Expression expression) => expression.accept(this))
|
||||
.toList();
|
||||
var named = normalizedMapMap<String, Expression, Value>(arguments.named,
|
||||
value: (_, expression) => expression.accept(this));
|
||||
|
9
package.json
Normal file
9
package.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"//": [
|
||||
"This isn't the official package.json for Dart Sass. It's just used to ",
|
||||
"install dependencies used for testing the Node API."
|
||||
],
|
||||
"devDependencies": {
|
||||
"fibers": ">=1.0.0 <3.0.0"
|
||||
}
|
||||
}
|
@ -25,6 +25,8 @@ dependencies:
|
||||
|
||||
dev_dependencies:
|
||||
archive: "^1.0.0"
|
||||
analyzer: "^0.30.0"
|
||||
crypto: ">=0.9.2 <3.0.0"
|
||||
dart_style: "^1.0.0"
|
||||
grinder: "^0.8.0"
|
||||
http: "^0.11.0"
|
||||
@ -34,6 +36,6 @@ dev_dependencies:
|
||||
stream_channel: "^1.0.0"
|
||||
test_descriptor: "^1.0.0"
|
||||
test_process: "^1.0.0-rc.1"
|
||||
test: "^0.12.26"
|
||||
test: "^0.12.29"
|
||||
xml: "^2.4.0"
|
||||
yaml: "^2.0.0"
|
||||
|
@ -13,23 +13,30 @@ hybridMain(StreamChannel channel) async {
|
||||
throw "NPM package is not built. Run pub run grinder npm_package.";
|
||||
}
|
||||
|
||||
var lastModified = new DateTime(0);
|
||||
var entriesToCheck = new Directory("lib").listSync(recursive: true).toList()
|
||||
..add(new File("pubspec.lock"));
|
||||
var lastModified = new File("build/npm/package.json").lastModifiedSync();
|
||||
var entriesToCheck = new Directory("lib").listSync(recursive: true).toList();
|
||||
|
||||
// If we have a dependency override, "pub run" will touch the lockfile to mark
|
||||
// it as newer than the pubspec, which makes it unsuitable to use for
|
||||
// freshness checking.
|
||||
if (new File("pubspec.yaml")
|
||||
.readAsStringSync()
|
||||
.contains("dependency_overrides")) {
|
||||
entriesToCheck.add(new File("pubspec.yaml"));
|
||||
} else {
|
||||
entriesToCheck.add(new File("pubspec.lock"));
|
||||
}
|
||||
|
||||
for (var entry in entriesToCheck) {
|
||||
if (entry is File) {
|
||||
var entryLastModified = entry.lastModifiedSync();
|
||||
if (lastModified.isBefore(entryLastModified)) {
|
||||
lastModified = entryLastModified;
|
||||
throw "${entry.path} was modified after NPM package was generated.\n"
|
||||
"Run pub run grinder before_test.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastModified
|
||||
.isAfter(new File("build/npm/package.json").lastModifiedSync())) {
|
||||
throw "NPM package is out-of-date. Run pub run grinder npm_package.";
|
||||
}
|
||||
|
||||
channel.sink.close();
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
/// to Dart. This is kind of convoluted, but it allows us to test the API as it
|
||||
/// will be used in the real world without having to manually write any JS.
|
||||
|
||||
import 'package:sass/src/node/fiber.dart';
|
||||
import 'package:sass/src/node/render_error.dart';
|
||||
import 'package:sass/src/node/render_options.dart';
|
||||
import 'package:sass/src/node/render_result.dart';
|
||||
@ -20,16 +21,34 @@ export 'package:sass/src/node/render_options.dart';
|
||||
export 'package:sass/src/node/render_result.dart';
|
||||
|
||||
/// The Sass module.
|
||||
final sass = _require(p.absolute("build/npm/sass.dart"));
|
||||
final sass = _requireSass(p.absolute("build/npm/sass.dart"));
|
||||
|
||||
/// The Fiber class.
|
||||
final fiber = _requireFiber("fibers");
|
||||
|
||||
/// A `null` that's guaranteed to be represented by JavaScript's `undefined`
|
||||
/// value, not by `null`.
|
||||
@JS()
|
||||
external Object get undefined;
|
||||
|
||||
/// A `null` that's guaranteed to be represented by JavaScript's `null` value,
|
||||
/// not by `undefined`.
|
||||
///
|
||||
/// We have to use eval here because otherwise dart2js will inline the null
|
||||
/// value and then optimize it away.
|
||||
final Object jsNull = _eval("null");
|
||||
|
||||
@JS("eval")
|
||||
external Object _eval(String js);
|
||||
|
||||
@JS("process.chdir")
|
||||
external void chdir(String directory);
|
||||
|
||||
@JS("require")
|
||||
external Sass _require(String path);
|
||||
external Sass _requireSass(String path);
|
||||
|
||||
@JS("require")
|
||||
external FiberClass _requireFiber(String path);
|
||||
|
||||
@JS()
|
||||
class Sass {
|
||||
|
@ -13,6 +13,7 @@ import 'package:test/test.dart';
|
||||
|
||||
import 'package:sass/src/io.dart';
|
||||
import 'package:sass/src/util/path.dart';
|
||||
import 'package:sass/src/node/utils.dart';
|
||||
import 'package:sass/src/value/number.dart';
|
||||
|
||||
import '../ensure_npm_package.dart';
|
||||
@ -534,4 +535,121 @@ void main() {
|
||||
" stdin 1:9 root stylesheet"));
|
||||
});
|
||||
});
|
||||
|
||||
group("render()", () {
|
||||
test("supports asynchronous importers", () {
|
||||
expect(
|
||||
render(new RenderOptions(
|
||||
data: "@import 'foo'",
|
||||
importer: allowInterop((_, __, done) {
|
||||
new Future.delayed(Duration.ZERO).then((_) {
|
||||
done(new NodeImporterResult(contents: 'a {b: c}'));
|
||||
});
|
||||
}))),
|
||||
completion(equalsIgnoringWhitespace('a { b: c; }')));
|
||||
});
|
||||
|
||||
test("supports asynchronous errors", () {
|
||||
expect(
|
||||
renderError(new RenderOptions(
|
||||
data: "@import 'foo'",
|
||||
importer: allowInterop((_, __, done) {
|
||||
new Future.delayed(Duration.ZERO).then((_) {
|
||||
done(new JSError('oh no'));
|
||||
});
|
||||
}))),
|
||||
completion(toStringAndMessageEqual("oh no\n"
|
||||
" stdin 1:9 root stylesheet")));
|
||||
});
|
||||
|
||||
test("supports synchronous importers", () {
|
||||
expect(
|
||||
render(new RenderOptions(
|
||||
data: "@import 'foo'",
|
||||
importer: allowInterop((_, __, ___) =>
|
||||
new NodeImporterResult(contents: 'a {b: c}')))),
|
||||
completion(equalsIgnoringWhitespace('a { b: c; }')));
|
||||
});
|
||||
|
||||
test("supports synchronous null returns", () {
|
||||
expect(
|
||||
renderError(new RenderOptions(
|
||||
data: "@import 'foo'",
|
||||
importer: allowInterop((_, __, ___) => jsNull))),
|
||||
completion(
|
||||
toStringAndMessageEqual("Can't find stylesheet to import.\n"
|
||||
" stdin 1:9 root stylesheet")));
|
||||
});
|
||||
|
||||
group("with fibers", () {
|
||||
setUpAll(() {
|
||||
try {
|
||||
fiber;
|
||||
} catch (_) {
|
||||
throw "Can't load fibers package.\n"
|
||||
"Run pub run grinder before_test.";
|
||||
}
|
||||
});
|
||||
|
||||
test("supports asynchronous importers", () {
|
||||
expect(
|
||||
render(new RenderOptions(
|
||||
data: "@import 'foo'",
|
||||
importer: allowInterop((_, __, done) {
|
||||
new Future.delayed(Duration.ZERO).then((_) {
|
||||
done(new NodeImporterResult(contents: 'a {b: c}'));
|
||||
});
|
||||
}),
|
||||
fiber: fiber)),
|
||||
completion(equalsIgnoringWhitespace('a { b: c; }')));
|
||||
});
|
||||
|
||||
test("supports synchronous calls to done", () {
|
||||
expect(
|
||||
render(new RenderOptions(
|
||||
data: "@import 'foo'",
|
||||
importer: allowInterop((_, __, done) {
|
||||
done(new NodeImporterResult(contents: 'a {b: c}'));
|
||||
}),
|
||||
fiber: fiber)),
|
||||
completion(equalsIgnoringWhitespace('a { b: c; }')));
|
||||
});
|
||||
|
||||
test("supports synchronous importers", () {
|
||||
expect(
|
||||
render(new RenderOptions(
|
||||
data: "@import 'foo'",
|
||||
importer: allowInterop((_, __, ___) {
|
||||
return new NodeImporterResult(contents: 'a {b: c}');
|
||||
}),
|
||||
fiber: fiber)),
|
||||
completion(equalsIgnoringWhitespace('a { b: c; }')));
|
||||
});
|
||||
|
||||
test("supports asynchronous errors", () {
|
||||
expect(
|
||||
renderError(new RenderOptions(
|
||||
data: "@import 'foo'",
|
||||
importer: allowInterop((_, __, done) {
|
||||
new Future.delayed(Duration.ZERO).then((_) {
|
||||
done(new JSError('oh no'));
|
||||
});
|
||||
}),
|
||||
fiber: fiber)),
|
||||
completion(toStringAndMessageEqual("oh no\n"
|
||||
" stdin 1:9 root stylesheet")));
|
||||
});
|
||||
|
||||
test("supports synchronous null returns", () {
|
||||
expect(
|
||||
renderError(new RenderOptions(
|
||||
data: "@import 'foo'",
|
||||
importer: allowInterop((_, __, ___) => jsNull),
|
||||
fiber: fiber)),
|
||||
completion(
|
||||
toStringAndMessageEqual("Can't find stylesheet to import.\n"
|
||||
" stdin 1:9 root stylesheet")));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -37,10 +37,11 @@ Matcher toStringAndMessageEqual(String text) => predicate((error) {
|
||||
/// Returns the result of rendering via [options] as a string.
|
||||
Future<String> render(RenderOptions options) {
|
||||
var completer = new Completer<String>();
|
||||
sass.render(options, allowInterop((error, result) {
|
||||
sass.render(options,
|
||||
allowInterop(Zone.current.bindBinaryCallback((error, result) {
|
||||
expect(error, isNull);
|
||||
completer.complete(UTF8.decode(result.css));
|
||||
}));
|
||||
})));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@ -48,10 +49,11 @@ Future<String> render(RenderOptions options) {
|
||||
/// error.
|
||||
Future<RenderError> renderError(RenderOptions options) {
|
||||
var completer = new Completer<RenderError>();
|
||||
sass.render(options, allowInterop((error, result) {
|
||||
sass.render(options,
|
||||
allowInterop(Zone.current.bindBinaryCallback((error, result) {
|
||||
expect(result, isNull);
|
||||
completer.complete(error);
|
||||
}));
|
||||
})));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
|
27
test/synchronize_test.dart
Normal file
27
test/synchronize_test.dart
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2017 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 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test("synchronized files are up-to-date", () {
|
||||
({
|
||||
'lib/src/visitor/async_evaluate.dart': 'lib/src/visitor/evaluate.dart',
|
||||
'lib/src/async_environment.dart': 'lib/src/environment.dart'
|
||||
}).forEach((sourcePath, targetPath) {
|
||||
var source = new File(sourcePath).readAsStringSync();
|
||||
var target = new File(targetPath).readAsStringSync();
|
||||
|
||||
var hash = sha1.convert(UTF8.encode(source));
|
||||
if (!target.contains("Checksum: $hash")) {
|
||||
fail("$targetPath is out-of-date.\n"
|
||||
"Run pub run grinder to update it.");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
@ -17,6 +17,10 @@ import 'package:yaml/yaml.dart';
|
||||
|
||||
import 'package:sass/src/util/path.dart';
|
||||
|
||||
import 'synchronize.dart';
|
||||
|
||||
export 'synchronize.dart';
|
||||
|
||||
/// The version of Dart Sass.
|
||||
final String _version =
|
||||
loadYaml(new File('pubspec.yaml').readAsStringSync())['version'] as String;
|
||||
@ -33,7 +37,11 @@ final _sdkDir = p.dirname(p.dirname(Platform.resolvedExecutable));
|
||||
|
||||
main(List<String> args) => grind(args);
|
||||
|
||||
@DefaultTask('Run the Dart formatter.')
|
||||
@DefaultTask('Compile async code and reformat.')
|
||||
@Depends(format, synchronize)
|
||||
all() {}
|
||||
|
||||
@Task('Run the Dart formatter.')
|
||||
format() {
|
||||
Pub.run('dart_style',
|
||||
script: 'format',
|
||||
@ -90,6 +98,13 @@ npm_package() {
|
||||
_writeNpmPackage('build/npm-old', json..addAll({"name": "dart-sass"}));
|
||||
}
|
||||
|
||||
@Task('Installs dependencies from npm.')
|
||||
npm_install() => run("npm", arguments: ["install"]);
|
||||
|
||||
@Task('Runs the tasks that are required for running tests.')
|
||||
@Depends(npm_package, npm_install)
|
||||
before_test() {}
|
||||
|
||||
/// Writes a Dart Sass NPM package to the directory at [destination].
|
||||
///
|
||||
/// The [json] will be used as the package's package.json.
|
||||
|
208
tool/synchronize.dart
Normal file
208
tool/synchronize.dart
Normal file
@ -0,0 +1,208 @@
|
||||
// Copyright 2017 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 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:analyzer/dart/ast/token.dart';
|
||||
import 'package:dart_style/dart_style.dart';
|
||||
import 'package:grinder/grinder.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// The files to compile to synchronous versions.
|
||||
final _sources = {
|
||||
'lib/src/visitor/async_evaluate.dart': 'lib/src/visitor/evaluate.dart',
|
||||
'lib/src/async_environment.dart': 'lib/src/environment.dart'
|
||||
};
|
||||
|
||||
/// This is how we support both synchronous and asynchronous compilation modes.
|
||||
///
|
||||
/// Both modes are necessary. Synchronous mode is faster, works in sync-only
|
||||
/// contexts, and allows us to support Node Sass's renderSync() method.
|
||||
/// Asynchronous mode allows users to write async importers and functions in
|
||||
/// both Dart and JS.
|
||||
///
|
||||
/// The logic for synchronous and asynchronous mode is identical, but the async
|
||||
/// code needs to await every statement and expression evaluation in case they
|
||||
/// do asynchronous work. To avoid duplicating logic, we hand-write asynchronous
|
||||
/// code for the evaluator and the environment and use this task to compile it
|
||||
/// to a synchronous equivalent.
|
||||
@Task('Compile async code to synchronous code.')
|
||||
synchronize() {
|
||||
_sources.forEach((source, target) {
|
||||
var visitor = new _Visitor(new File(source).readAsStringSync(), source);
|
||||
parseDartFile(source).accept(visitor);
|
||||
var formatted = new DartFormatter().format(visitor.result);
|
||||
new File(target).writeAsStringSync(formatted);
|
||||
});
|
||||
}
|
||||
|
||||
/// The visitor that traverses the asynchronous parse tree and converts it to
|
||||
/// synchronous code.
|
||||
///
|
||||
/// To preserve the original whitespace and comments, this copies text from the
|
||||
/// original source where possible. It tracks the [_position] at the end of the
|
||||
/// text that's been written, and writes from that position to the new position
|
||||
/// whenever text needs to be emitted.
|
||||
class _Visitor extends RecursiveAstVisitor {
|
||||
/// The source of the original asynchronous file.
|
||||
final String _source;
|
||||
|
||||
/// The current position in [_source].
|
||||
var _position = 0;
|
||||
|
||||
/// The buffer in which the text of the synchronous file is built up.
|
||||
final _buffer = new StringBuffer();
|
||||
|
||||
/// The synchronous text.
|
||||
String get result {
|
||||
_buffer.write(_source.substring(_position));
|
||||
_position = _source.length;
|
||||
return _buffer.toString();
|
||||
}
|
||||
|
||||
_Visitor(this._source, String path) {
|
||||
var afterHeader = "\n".allMatches(_source).skip(3).first.end;
|
||||
_buffer.writeln(_source.substring(0, afterHeader));
|
||||
_buffer.writeln("""
|
||||
// DO NOT EDIT. This file was generated from ${p.basename(path)}.
|
||||
// See tool/synchronize.dart for details.
|
||||
//
|
||||
// Checksum: ${sha1.convert(UTF8.encode(_source))}
|
||||
""");
|
||||
_position = afterHeader;
|
||||
}
|
||||
|
||||
void visitAwaitExpression(AwaitExpression node) {
|
||||
_skip(node.awaitKeyword);
|
||||
|
||||
// Skip the space after "await" to work around dart-lang/dart_style#226.
|
||||
_position++;
|
||||
node.expression.accept(this);
|
||||
}
|
||||
|
||||
void visitParenthesizedExpression(ParenthesizedExpression node) {
|
||||
if (node.expression is AwaitExpression) {
|
||||
_skip(node.leftParenthesis);
|
||||
node.expression.accept(this);
|
||||
_skip(node.rightParenthesis);
|
||||
} else {
|
||||
node.expression.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
void visitBlockFunctionBody(BlockFunctionBody node) {
|
||||
_skip(node.keyword);
|
||||
node.visitChildren(this);
|
||||
}
|
||||
|
||||
void visitExpressionFunctionBody(ExpressionFunctionBody node) {
|
||||
_skip(node.keyword);
|
||||
node.visitChildren(this);
|
||||
}
|
||||
|
||||
void visitMethodDeclaration(MethodDeclaration node) {
|
||||
if (_synchronizeName(node.name.name) != node.name.name) {
|
||||
// If the file defines any asynchronous versions of synchronous functions,
|
||||
// remove them.
|
||||
_skipNode(node);
|
||||
} else {
|
||||
super.visitMethodDeclaration(node);
|
||||
}
|
||||
}
|
||||
|
||||
void visitImportDirective(ImportDirective node) {
|
||||
_skipNode(node);
|
||||
var text = node.toString();
|
||||
if (!text.contains("dart:async")) {
|
||||
_buffer.write(text.replaceAll("async_", ""));
|
||||
}
|
||||
}
|
||||
|
||||
void visitMethodInvocation(MethodInvocation node) {
|
||||
// Convert async utility methods to their synchronous equivalents.
|
||||
if (node.target == null &&
|
||||
["mapAsync", "putIfAbsentAsync"].contains(node.methodName.name)) {
|
||||
_writeTo(node);
|
||||
var arguments = node.argumentList.arguments;
|
||||
_write(arguments.first);
|
||||
|
||||
_buffer.write(".${_synchronizeName(node.methodName.name)}");
|
||||
if (node.typeArguments != null) _write(node.typeArguments);
|
||||
_buffer.write("(");
|
||||
|
||||
_position = arguments[1].beginToken.offset;
|
||||
for (var argument in arguments.skip(1)) {
|
||||
argument.accept(this);
|
||||
}
|
||||
} else {
|
||||
super.visitMethodInvocation(node);
|
||||
}
|
||||
}
|
||||
|
||||
void visitSimpleIdentifier(SimpleIdentifier node) {
|
||||
_skip(node.token);
|
||||
_buffer.write(_synchronizeName(node.name));
|
||||
}
|
||||
|
||||
void visitTypeName(TypeName node) {
|
||||
if (["Future", "FutureOr"].contains(node.name.name)) {
|
||||
_skip(node.name.beginToken);
|
||||
if (node.typeArguments != null) {
|
||||
_skip(node.typeArguments.leftBracket);
|
||||
node.typeArguments.arguments.first.accept(this);
|
||||
_skip(node.typeArguments.rightBracket);
|
||||
} else {
|
||||
_buffer.write("void");
|
||||
}
|
||||
} else {
|
||||
super.visitTypeName(node);
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes [_source] to [_buffer] up to the beginning of [token], then puts
|
||||
/// [_position] after [token] so it doesn't get written.
|
||||
void _skip(Token token) {
|
||||
if (token == null) return;
|
||||
_buffer.write(_source.substring(_position, token.offset));
|
||||
_position = token.end;
|
||||
}
|
||||
|
||||
/// Writes [_source] to [_buffer] up to the beginning of [node], then puts
|
||||
/// [_position] after [node] so it doesn't get written.
|
||||
void _skipNode(AstNode node) {
|
||||
if (node == null) return;
|
||||
_writeTo(node);
|
||||
_position = node.endToken.end;
|
||||
}
|
||||
|
||||
/// Writes [_source] to [_buffer] up to the beginning of [node].
|
||||
void _writeTo(AstNode node) {
|
||||
_buffer.write(_source.substring(_position, node.beginToken.offset));
|
||||
_position = node.beginToken.offset;
|
||||
}
|
||||
|
||||
/// Writes the contents of [node] to [_buffer].
|
||||
///
|
||||
/// This leaves [_position] at the end of [node].
|
||||
void _write(AstNode node) {
|
||||
_position = node.beginToken.offset;
|
||||
node.accept(this);
|
||||
_buffer.write(_source.substring(_position, node.endToken.end));
|
||||
_position = node.endToken.end;
|
||||
}
|
||||
|
||||
/// Strips an "async" prefix or suffix from [name].
|
||||
String _synchronizeName(String name) {
|
||||
if (name.toLowerCase().startsWith('async')) {
|
||||
return name.substring('async'.length);
|
||||
} else if (name.toLowerCase().endsWith('async')) {
|
||||
return name.substring(0, name.length - 'async'.length);
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user