Skip to content

Commit

Permalink
Add asynchronous versions of several evaluation-related libraries
Browse files Browse the repository at this point in the history
This allows us to support asynchronous importers and, eventually,
functions without breaking synchronous support. The copies were made
manually, but the eventual plan is to auto-generate the synchronous
versions by stripping all asynchrony from the async versions.

See #9
  • Loading branch information
nex3 committed Nov 18, 2017
1 parent b68a25a commit 6140af7
Show file tree
Hide file tree
Showing 17 changed files with 2,493 additions and 71 deletions.
6 changes: 5 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## 1.0.0-beta.4

* 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()`.

* Fix a crash when `:not(...)` extends a selector that appears in
`:not(:not(...))`.

Expand Down
44 changes: 44 additions & 0 deletions lib/sass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
306 changes: 306 additions & 0 deletions lib/src/async_environment.dart
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Loading

0 comments on commit 6140af7

Please sign in to comment.