Skip to content

Commit ad4a169

Browse files
author
Awjin Ahn
authored
Adds RenderOptions to the context of custom functions. (#1236)
1 parent 94d1fc4 commit ad4a169

File tree

4 files changed

+206
-30
lines changed

4 files changed

+206
-30
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
* Update chokidar version for Node API tests.
44

5+
### JavaScript API
6+
7+
* Allow a custom function to access the `render()` options object within its
8+
local context, as `this.options`.
9+
510
## 1.32.7
611

712
* Allow the null safety release of stream_transform.

lib/src/node.dart

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Future<RenderResult> _renderAsync(RenderOptions options) async {
9696
if (options.data != null) {
9797
result = await compileStringAsync(options.data,
9898
nodeImporter: _parseImporter(options, start),
99-
functions: _parseFunctions(options, asynch: true),
99+
functions: _parseFunctions(options, start, asynch: true),
100100
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
101101
style: _parseOutputStyle(options.outputStyle),
102102
useSpaces: options.indentType != 'tab',
@@ -107,7 +107,7 @@ Future<RenderResult> _renderAsync(RenderOptions options) async {
107107
} else if (options.file != null) {
108108
result = await compileAsync(file,
109109
nodeImporter: _parseImporter(options, start),
110-
functions: _parseFunctions(options, asynch: true),
110+
functions: _parseFunctions(options, start, asynch: true),
111111
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
112112
style: _parseOutputStyle(options.outputStyle),
113113
useSpaces: options.indentType != 'tab',
@@ -135,7 +135,7 @@ RenderResult _renderSync(RenderOptions options) {
135135
if (options.data != null) {
136136
result = compileString(options.data,
137137
nodeImporter: _parseImporter(options, start),
138-
functions: _parseFunctions(options).cast(),
138+
functions: _parseFunctions(options, start).cast(),
139139
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
140140
style: _parseOutputStyle(options.outputStyle),
141141
useSpaces: options.indentType != 'tab',
@@ -146,7 +146,7 @@ RenderResult _renderSync(RenderOptions options) {
146146
} else if (options.file != null) {
147147
result = compile(file,
148148
nodeImporter: _parseImporter(options, start),
149-
functions: _parseFunctions(options).cast(),
149+
functions: _parseFunctions(options, start).cast(),
150150
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
151151
style: _parseOutputStyle(options.outputStyle),
152152
useSpaces: options.indentType != 'tab',
@@ -186,7 +186,7 @@ JsError _wrapException(Object exception) {
186186
///
187187
/// This is typed to always return [AsyncCallable], but in practice it will
188188
/// return a `List<Callable>` if [asynch] is `false`.
189-
List<AsyncCallable> _parseFunctions(RenderOptions options,
189+
List<AsyncCallable> _parseFunctions(RenderOptions options, DateTime start,
190190
{bool asynch = false}) {
191191
if (options.functions == null) return const [];
192192

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

203+
var context = _contextWithOptions(options, start);
204+
203205
if (options.fiber != null) {
204206
result.add(BuiltInCallable.parsed(tuple.item1, tuple.item2, (arguments) {
205207
var fiber = options.fiber.current;
@@ -211,7 +213,7 @@ List<AsyncCallable> _parseFunctions(RenderOptions options,
211213
scheduleMicrotask(() => fiber.run(result));
212214
})
213215
];
214-
var result = Function.apply(callback as Function, jsArguments);
216+
var result = (callback as JSFunction).apply(context, jsArguments);
215217
return unwrapValue(isUndefined(result)
216218
// Run `fiber.yield()` in runZoned() so that Dart resets the current
217219
// zone once it's done. Otherwise, interweaving fibers can leave
@@ -223,8 +225,8 @@ List<AsyncCallable> _parseFunctions(RenderOptions options,
223225
result.add(BuiltInCallable.parsed(
224226
tuple.item1,
225227
tuple.item2,
226-
(arguments) => unwrapValue(Function.apply(
227-
callback as Function, arguments.map(wrapValue).toList()))));
228+
(arguments) => unwrapValue((callback as JSFunction)
229+
.apply(context, arguments.map(wrapValue).toList()))));
228230
} else {
229231
result.add(AsyncBuiltInCallable.parsed(tuple.item1, tuple.item2,
230232
(arguments) async {
@@ -233,7 +235,7 @@ List<AsyncCallable> _parseFunctions(RenderOptions options,
233235
...arguments.map(wrapValue),
234236
allowInterop(([Object result]) => completer.complete(result))
235237
];
236-
var result = Function.apply(callback as Function, jsArguments);
238+
var result = (callback as JSFunction).apply(context, jsArguments);
237239
return unwrapValue(
238240
isUndefined(result) ? await completer.future : result);
239241
}));
@@ -254,27 +256,8 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) {
254256
importers = [options.importer as JSFunction];
255257
}
256258

257-
var includePaths = List<String>.from(options.includePaths ?? []);
258-
259259
RenderContext context;
260-
if (importers.isNotEmpty) {
261-
context = RenderContext(
262-
options: RenderContextOptions(
263-
file: options.file,
264-
data: options.data,
265-
includePaths:
266-
([p.current, ...includePaths]).join(isWindows ? ';' : ':'),
267-
precision: SassNumber.precision,
268-
style: 1,
269-
indentType: options.indentType == 'tab' ? 1 : 0,
270-
indentWidth: _parseIndentWidth(options.indentWidth) ?? 2,
271-
linefeed: _parseLineFeed(options.linefeed).text,
272-
result: RenderResult(
273-
stats: RenderResultStats(
274-
start: start.millisecondsSinceEpoch,
275-
entry: options.file ?? 'data'))));
276-
context.options.context = context;
277-
}
260+
if (importers.isNotEmpty) context = _contextWithOptions(options, start);
278261

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

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

287+
/// Creates a `this` context that contains the render options.
288+
RenderContext _contextWithOptions(RenderOptions options, DateTime start) {
289+
var includePaths = List<String>.from(options.includePaths ?? []);
290+
var context = RenderContext(
291+
options: RenderContextOptions(
292+
file: options.file,
293+
data: options.data,
294+
includePaths:
295+
([p.current, ...includePaths]).join(isWindows ? ';' : ':'),
296+
precision: SassNumber.precision,
297+
style: 1,
298+
indentType: options.indentType == 'tab' ? 1 : 0,
299+
indentWidth: _parseIndentWidth(options.indentWidth) ?? 2,
300+
linefeed: _parseLineFeed(options.linefeed).text,
301+
result: RenderResult(
302+
stats: RenderResultStats(
303+
start: start.millisecondsSinceEpoch,
304+
entry: options.file ?? 'data'))));
305+
context.options.context = context;
306+
return context;
307+
}
308+
303309
/// Parse [style] into an [OutputStyle].
304310
OutputStyle _parseOutputStyle(String style) {
305311
if (style == null || style == 'expanded') return OutputStyle.expanded;

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass
2-
version: 1.32.8-dev
2+
version: 1.32.8
33
description: A Sass implementation in Dart.
44
author: Sass Team
55
homepage: https://github.com/sass/dart-sass

test/node_api/function_test.dart

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@ import 'dart:js_util';
1111
import 'package:js/js.dart';
1212
import 'package:node_interop/js.dart';
1313
import 'package:test/test.dart';
14+
import 'package:path/path.dart' as p;
15+
16+
import 'package:sass/src/io.dart';
17+
import 'package:sass/src/value/number.dart';
1418

1519
import '../ensure_npm_package.dart';
20+
import '../hybrid.dart';
1621
import 'api.dart';
1722
import 'utils.dart';
1823

@@ -180,6 +185,166 @@ void main() {
180185
});
181186
});
182187

188+
group('this', () {
189+
String sassPath;
190+
setUp(() async {
191+
sassPath = p.join(sandbox, 'test.scss');
192+
});
193+
194+
test('includes default option values', () {
195+
renderSync(RenderOptions(
196+
data: 'a {b: foo()}',
197+
functions: jsify({
198+
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
199+
var options = this_.options;
200+
expect(options.includePaths, equals(p.current));
201+
expect(options.precision, equals(SassNumber.precision));
202+
expect(options.style, equals(1));
203+
expect(options.indentType, equals(0));
204+
expect(options.indentWidth, equals(2));
205+
expect(options.linefeed, equals('\n'));
206+
return callConstructor(sass.types.Number, [12]);
207+
}))
208+
}),
209+
));
210+
});
211+
212+
test('includes the data when rendering via data', () {
213+
renderSync(RenderOptions(
214+
data: 'a {b: foo()}',
215+
functions: jsify({
216+
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
217+
expect(this_.options.data, equals('a {b: foo()}'));
218+
expect(this_.options.file, isNull);
219+
return callConstructor(sass.types.Number, [12]);
220+
}))
221+
}),
222+
));
223+
});
224+
225+
test('includes the filename when rendering via file', () async {
226+
await writeTextFile(sassPath, 'a {b: foo()}');
227+
renderSync(RenderOptions(
228+
file: sassPath,
229+
functions: jsify({
230+
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
231+
expect(this_.options.data, isNull);
232+
expect(this_.options.file, equals(sassPath));
233+
return callConstructor(sass.types.Number, [12]);
234+
}))
235+
}),
236+
));
237+
});
238+
239+
test('includes other include paths', () {
240+
renderSync(RenderOptions(
241+
data: 'a {b: foo()}',
242+
includePaths: [sandbox],
243+
functions: jsify({
244+
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
245+
expect(this_.options.includePaths,
246+
equals('${p.current}${isWindows ? ';' : ':'}$sandbox'));
247+
return callConstructor(sass.types.Number, [12]);
248+
}))
249+
}),
250+
));
251+
});
252+
253+
group('can override', () {
254+
test('indentWidth', () {
255+
renderSync(RenderOptions(
256+
data: 'a {b: foo()}',
257+
indentWidth: 5,
258+
functions: jsify({
259+
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
260+
expect(this_.options.indentWidth, equals(5));
261+
return callConstructor(sass.types.Number, [12]);
262+
}))
263+
}),
264+
));
265+
});
266+
267+
test('indentType', () {
268+
renderSync(RenderOptions(
269+
data: 'a {b: foo()}',
270+
indentType: 'tab',
271+
functions: jsify({
272+
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
273+
expect(this_.options.indentType, equals(1));
274+
return callConstructor(sass.types.Number, [12]);
275+
}))
276+
}),
277+
));
278+
});
279+
280+
test('linefeed', () {
281+
renderSync(RenderOptions(
282+
data: 'a {b: foo()}',
283+
linefeed: 'cr',
284+
functions: jsify({
285+
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
286+
expect(this_.options.linefeed, equals('\r'));
287+
return callConstructor(sass.types.Number, [12]);
288+
}))
289+
}),
290+
));
291+
});
292+
});
293+
294+
test('has a circular reference', () {
295+
renderSync(RenderOptions(
296+
data: 'a {b: foo()}',
297+
functions: jsify({
298+
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
299+
expect(this_.options.context, same(this_));
300+
return callConstructor(sass.types.Number, [12]);
301+
}))
302+
}),
303+
));
304+
});
305+
306+
group('includes render stats with', () {
307+
test('a start time', () {
308+
var start = DateTime.now();
309+
renderSync(RenderOptions(
310+
data: 'a {b: foo()}',
311+
functions: jsify({
312+
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
313+
expect(this_.options.result.stats.start,
314+
greaterThanOrEqualTo(start.millisecondsSinceEpoch));
315+
return callConstructor(sass.types.Number, [12]);
316+
}))
317+
}),
318+
));
319+
});
320+
321+
test('a data entry', () {
322+
renderSync(RenderOptions(
323+
data: 'a {b: foo()}',
324+
functions: jsify({
325+
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
326+
expect(this_.options.result.stats.entry, equals('data'));
327+
return callConstructor(sass.types.Number, [12]);
328+
}))
329+
}),
330+
));
331+
});
332+
333+
test('a file entry', () async {
334+
await writeTextFile(sassPath, 'a {b: foo()}');
335+
renderSync(RenderOptions(
336+
file: sassPath,
337+
functions: jsify({
338+
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
339+
expect(this_.options.result.stats.entry, equals(sassPath));
340+
return callConstructor(sass.types.Number, [12]);
341+
}))
342+
}),
343+
));
344+
});
345+
});
346+
});
347+
183348
test("gracefully handles an error from the function", () {
184349
var error = renderSyncError(RenderOptions(
185350
data: "a {b: foo()}",

0 commit comments

Comments
 (0)