Skip to content

Commit

Permalink
Adds RenderOptions to the context of custom functions. (#1236)
Browse files Browse the repository at this point in the history
  • Loading branch information
Awjin Ahn committed Feb 18, 2021
1 parent 94d1fc4 commit ad4a169
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 30 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

* Update chokidar version for Node API tests.

### JavaScript API

* Allow a custom function to access the `render()` options object within its
local context, as `this.options`.

## 1.32.7

* Allow the null safety release of stream_transform.
Expand Down
64 changes: 35 additions & 29 deletions lib/src/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Future<RenderResult> _renderAsync(RenderOptions options) async {
if (options.data != null) {
result = await compileStringAsync(options.data,
nodeImporter: _parseImporter(options, start),
functions: _parseFunctions(options, asynch: true),
functions: _parseFunctions(options, start, asynch: true),
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
Expand All @@ -107,7 +107,7 @@ Future<RenderResult> _renderAsync(RenderOptions options) async {
} else if (options.file != null) {
result = await compileAsync(file,
nodeImporter: _parseImporter(options, start),
functions: _parseFunctions(options, asynch: true),
functions: _parseFunctions(options, start, asynch: true),
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
Expand Down Expand Up @@ -135,7 +135,7 @@ RenderResult _renderSync(RenderOptions options) {
if (options.data != null) {
result = compileString(options.data,
nodeImporter: _parseImporter(options, start),
functions: _parseFunctions(options).cast(),
functions: _parseFunctions(options, start).cast(),
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
Expand All @@ -146,7 +146,7 @@ RenderResult _renderSync(RenderOptions options) {
} else if (options.file != null) {
result = compile(file,
nodeImporter: _parseImporter(options, start),
functions: _parseFunctions(options).cast(),
functions: _parseFunctions(options, start).cast(),
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
Expand Down Expand Up @@ -186,7 +186,7 @@ JsError _wrapException(Object exception) {
///
/// This is typed to always return [AsyncCallable], but in practice it will
/// return a `List<Callable>` if [asynch] is `false`.
List<AsyncCallable> _parseFunctions(RenderOptions options,
List<AsyncCallable> _parseFunctions(RenderOptions options, DateTime start,
{bool asynch = false}) {
if (options.functions == null) return const [];

Expand All @@ -200,6 +200,8 @@ List<AsyncCallable> _parseFunctions(RenderOptions options,
'Invalid signature "${signature}": ${error.message}', error.span);
}

var context = _contextWithOptions(options, start);

if (options.fiber != null) {
result.add(BuiltInCallable.parsed(tuple.item1, tuple.item2, (arguments) {
var fiber = options.fiber.current;
Expand All @@ -211,7 +213,7 @@ List<AsyncCallable> _parseFunctions(RenderOptions options,
scheduleMicrotask(() => fiber.run(result));
})
];
var result = Function.apply(callback as Function, jsArguments);
var result = (callback as JSFunction).apply(context, jsArguments);
return unwrapValue(isUndefined(result)
// Run `fiber.yield()` in runZoned() so that Dart resets the current
// zone once it's done. Otherwise, interweaving fibers can leave
Expand All @@ -223,8 +225,8 @@ List<AsyncCallable> _parseFunctions(RenderOptions options,
result.add(BuiltInCallable.parsed(
tuple.item1,
tuple.item2,
(arguments) => unwrapValue(Function.apply(
callback as Function, arguments.map(wrapValue).toList()))));
(arguments) => unwrapValue((callback as JSFunction)
.apply(context, arguments.map(wrapValue).toList()))));
} else {
result.add(AsyncBuiltInCallable.parsed(tuple.item1, tuple.item2,
(arguments) async {
Expand All @@ -233,7 +235,7 @@ List<AsyncCallable> _parseFunctions(RenderOptions options,
...arguments.map(wrapValue),
allowInterop(([Object result]) => completer.complete(result))
];
var result = Function.apply(callback as Function, jsArguments);
var result = (callback as JSFunction).apply(context, jsArguments);
return unwrapValue(
isUndefined(result) ? await completer.future : result);
}));
Expand All @@ -254,27 +256,8 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) {
importers = [options.importer as JSFunction];
}

var includePaths = List<String>.from(options.includePaths ?? []);

RenderContext context;
if (importers.isNotEmpty) {
context = RenderContext(
options: RenderContextOptions(
file: options.file,
data: options.data,
includePaths:
([p.current, ...includePaths]).join(isWindows ? ';' : ':'),
precision: SassNumber.precision,
style: 1,
indentType: options.indentType == 'tab' ? 1 : 0,
indentWidth: _parseIndentWidth(options.indentWidth) ?? 2,
linefeed: _parseLineFeed(options.linefeed).text,
result: RenderResult(
stats: RenderResultStats(
start: start.millisecondsSinceEpoch,
entry: options.file ?? 'data'))));
context.options.context = context;
}
if (importers.isNotEmpty) context = _contextWithOptions(options, start);

if (options.fiber != null) {
importers = importers.map((importer) {
Expand All @@ -297,9 +280,32 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) {
}).toList();
}

var includePaths = List<String>.from(options.includePaths ?? []);
return NodeImporter(context, includePaths, importers);
}

/// Creates a `this` context that contains the render options.
RenderContext _contextWithOptions(RenderOptions options, DateTime start) {
var includePaths = List<String>.from(options.includePaths ?? []);
var context = RenderContext(
options: RenderContextOptions(
file: options.file,
data: options.data,
includePaths:
([p.current, ...includePaths]).join(isWindows ? ';' : ':'),
precision: SassNumber.precision,
style: 1,
indentType: options.indentType == 'tab' ? 1 : 0,
indentWidth: _parseIndentWidth(options.indentWidth) ?? 2,
linefeed: _parseLineFeed(options.linefeed).text,
result: RenderResult(
stats: RenderResultStats(
start: start.millisecondsSinceEpoch,
entry: options.file ?? 'data'))));
context.options.context = context;
return context;
}

/// Parse [style] into an [OutputStyle].
OutputStyle _parseOutputStyle(String style) {
if (style == null || style == 'expanded') return OutputStyle.expanded;
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: sass
version: 1.32.8-dev
version: 1.32.8
description: A Sass implementation in Dart.
author: Sass Team
homepage: https://github.com/sass/dart-sass
Expand Down
165 changes: 165 additions & 0 deletions test/node_api/function_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ import 'dart:js_util';
import 'package:js/js.dart';
import 'package:node_interop/js.dart';
import 'package:test/test.dart';
import 'package:path/path.dart' as p;

import 'package:sass/src/io.dart';
import 'package:sass/src/value/number.dart';

import '../ensure_npm_package.dart';
import '../hybrid.dart';
import 'api.dart';
import 'utils.dart';

Expand Down Expand Up @@ -180,6 +185,166 @@ void main() {
});
});

group('this', () {
String sassPath;
setUp(() async {
sassPath = p.join(sandbox, 'test.scss');
});

test('includes default option values', () {
renderSync(RenderOptions(
data: 'a {b: foo()}',
functions: jsify({
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
var options = this_.options;
expect(options.includePaths, equals(p.current));
expect(options.precision, equals(SassNumber.precision));
expect(options.style, equals(1));
expect(options.indentType, equals(0));
expect(options.indentWidth, equals(2));
expect(options.linefeed, equals('\n'));
return callConstructor(sass.types.Number, [12]);
}))
}),
));
});

test('includes the data when rendering via data', () {
renderSync(RenderOptions(
data: 'a {b: foo()}',
functions: jsify({
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
expect(this_.options.data, equals('a {b: foo()}'));
expect(this_.options.file, isNull);
return callConstructor(sass.types.Number, [12]);
}))
}),
));
});

test('includes the filename when rendering via file', () async {
await writeTextFile(sassPath, 'a {b: foo()}');
renderSync(RenderOptions(
file: sassPath,
functions: jsify({
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
expect(this_.options.data, isNull);
expect(this_.options.file, equals(sassPath));
return callConstructor(sass.types.Number, [12]);
}))
}),
));
});

test('includes other include paths', () {
renderSync(RenderOptions(
data: 'a {b: foo()}',
includePaths: [sandbox],
functions: jsify({
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
expect(this_.options.includePaths,
equals('${p.current}${isWindows ? ';' : ':'}$sandbox'));
return callConstructor(sass.types.Number, [12]);
}))
}),
));
});

group('can override', () {
test('indentWidth', () {
renderSync(RenderOptions(
data: 'a {b: foo()}',
indentWidth: 5,
functions: jsify({
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
expect(this_.options.indentWidth, equals(5));
return callConstructor(sass.types.Number, [12]);
}))
}),
));
});

test('indentType', () {
renderSync(RenderOptions(
data: 'a {b: foo()}',
indentType: 'tab',
functions: jsify({
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
expect(this_.options.indentType, equals(1));
return callConstructor(sass.types.Number, [12]);
}))
}),
));
});

test('linefeed', () {
renderSync(RenderOptions(
data: 'a {b: foo()}',
linefeed: 'cr',
functions: jsify({
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
expect(this_.options.linefeed, equals('\r'));
return callConstructor(sass.types.Number, [12]);
}))
}),
));
});
});

test('has a circular reference', () {
renderSync(RenderOptions(
data: 'a {b: foo()}',
functions: jsify({
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
expect(this_.options.context, same(this_));
return callConstructor(sass.types.Number, [12]);
}))
}),
));
});

group('includes render stats with', () {
test('a start time', () {
var start = DateTime.now();
renderSync(RenderOptions(
data: 'a {b: foo()}',
functions: jsify({
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
expect(this_.options.result.stats.start,
greaterThanOrEqualTo(start.millisecondsSinceEpoch));
return callConstructor(sass.types.Number, [12]);
}))
}),
));
});

test('a data entry', () {
renderSync(RenderOptions(
data: 'a {b: foo()}',
functions: jsify({
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
expect(this_.options.result.stats.entry, equals('data'));
return callConstructor(sass.types.Number, [12]);
}))
}),
));
});

test('a file entry', () async {
await writeTextFile(sassPath, 'a {b: foo()}');
renderSync(RenderOptions(
file: sassPath,
functions: jsify({
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
expect(this_.options.result.stats.entry, equals(sassPath));
return callConstructor(sass.types.Number, [12]);
}))
}),
));
});
});
});

test("gracefully handles an error from the function", () {
var error = renderSyncError(RenderOptions(
data: "a {b: foo()}",
Expand Down

0 comments on commit ad4a169

Please sign in to comment.