diff --git a/CHANGELOG.md b/CHANGELOG.md index bcbc8d601..e4f16b81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 1.17.4 + +* Consistently parse U+000C FORM FEED, U+000D CARRIAGE RETURN, and sequences of + U+000D CARRIAGE RETURN followed by U+000A LINE FEED as individual newlines. + +### JavaScript API + +* Add a `sass.types.Error` constructor as an alias for `Error`. This makes our + custom function API compatible with Node Sass's. + ## 1.17.3 * Fix an edge case where slash-separated numbers were written to the stylesheet diff --git a/lib/src/node.dart b/lib/src/node.dart index 482c87206..ff8245cc7 100644 --- a/lib/src/node.dart +++ b/lib/src/node.dart @@ -56,7 +56,8 @@ void main() { Map: mapConstructor, Null: nullConstructor, Number: numberConstructor, - String: stringConstructor); + String: stringConstructor, + Error: jsErrorConstructor); } /// Converts Sass to CSS. diff --git a/lib/src/node/types.dart b/lib/src/node/types.dart index 143bd40b6..07f05b6e0 100644 --- a/lib/src/node/types.dart +++ b/lib/src/node/types.dart @@ -14,6 +14,8 @@ class Types { external set Null(function); external set Number(function); external set String(function); + external set Error(function); - external factory Types({Boolean, Color, List, Map, Null, Number, String}); + external factory Types( + {Boolean, Color, List, Map, Null, Number, String, Error}); } diff --git a/lib/src/node/utils.dart b/lib/src/node/utils.dart index e89751d47..38074ed6b 100644 --- a/lib/src/node/utils.dart +++ b/lib/src/node/utils.dart @@ -34,10 +34,10 @@ bool isUndefined(value) => _isUndefined.call(value) as bool; final _isUndefined = JSFunction("value", "return value === undefined;"); @JS("Error") -external Function get _JSError; +external Function get jsErrorConstructor; /// Returns whether [value] is a JS Error object. -bool isJSError(value) => instanceof(value, _JSError); +bool isJSError(value) => instanceof(value, jsErrorConstructor); /// Invokes [function] with [thisArg] as `this`. Object call2(JSFunction function, Object thisArg, Object arg1, Object arg2) => diff --git a/lib/src/node/value.dart b/lib/src/node/value.dart index c3b5da41e..156aa419a 100644 --- a/lib/src/node/value.dart +++ b/lib/src/node/value.dart @@ -5,6 +5,7 @@ import 'dart:js_util'; import '../value.dart'; +import 'utils.dart'; import 'value/color.dart'; import 'value/list.dart'; import 'value/map.dart'; @@ -20,11 +21,14 @@ export 'value/number.dart'; export 'value/string.dart'; /// Unwraps a value wrapped with [wrapValue]. +/// +/// If [object] is a JS error, throws it. Value unwrapValue(object) { if (object != null) { if (object is Value) return object; var value = getProperty(object, 'dartValue'); if (value != null && value is Value) return value; + if (isJSError(object)) throw object; } throw "$object must be a Sass value type."; } diff --git a/lib/src/parse/sass.dart b/lib/src/parse/sass.dart index 6df58fc48..fe5aebbea 100644 --- a/lib/src/parse/sass.dart +++ b/lib/src/parse/sass.dart @@ -155,6 +155,7 @@ class SassParser extends StylesheetParser { // Ignore empty lines. case $cr: case $lf: + case $ff: return null; case $dollar: @@ -282,8 +283,8 @@ class SassParser extends StylesheetParser { if (_peekIndentation() <= parentIndentation) break; // Preserve empty lines. - while (isNewline(scanner.peekChar(1))) { - scanner.readChar(); + while (_lookingAtDoubleNewline()) { + _expectNewline(); buffer.writeln(); buffer.write(" *"); } @@ -315,7 +316,12 @@ class SassParser extends StylesheetParser { case $semicolon: scanner.error("semicolons aren't allowed in the indented syntax."); return; + case $cr: + scanner.readChar(); + if (scanner.peekChar() == $lf) scanner.readChar(); + return; case $lf: + case $ff: scanner.readChar(); return; default: @@ -323,6 +329,21 @@ class SassParser extends StylesheetParser { } } + /// Returns whether the scanner is immediately before *two* newlines. + bool _lookingAtDoubleNewline() { + switch (scanner.peekChar()) { + case $cr: + var nextChar = scanner.peekChar(1); + if (nextChar == $lf) return isNewline(scanner.peekChar(2)); + return nextChar == $cr || nextChar == $ff; + case $lf: + case $ff: + return isNewline(scanner.peekChar(1)); + default: + return false; + } + } + /// As long as the scanner's position is indented beneath the starting line, /// runs [body] to consume the next statement. void _whileIndentedLower(void body()) { diff --git a/lib/src/parse/scss.dart b/lib/src/parse/scss.dart index cd7cb6c64..37cbd7fe7 100644 --- a/lib/src/parse/scss.dart +++ b/lib/src/parse/scss.dart @@ -184,6 +184,16 @@ class ScssParser extends StylesheetParser { buffer.writeCharCode(scanner.readChar()); return LoudComment(buffer.interpolation(scanner.spanFrom(start))); + case $cr: + scanner.readChar(); + if (scanner.peekChar() != $lf) buffer.writeCharCode($lf); + break; + + case $ff: + scanner.readChar(); + buffer.writeCharCode($lf); + break; + default: buffer.writeCharCode(scanner.readChar()); break; diff --git a/package.json b/package.json index b13e24241..4732234fe 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "install dependencies used for testing the Node API." ], "devDependencies": { + "node-sass": "^4.11.0", "chokidar": "^2.0.0", "fibers": ">=1.0.0 <4.0.0", "intercept-stdout": "^0.1.2" diff --git a/pubspec.yaml b/pubspec.yaml index 4b1391823..38d4f6df0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.17.3 +version: 1.17.4 description: A Sass implementation in Dart. author: Dart Team homepage: https://github.com/sass/dart-sass diff --git a/test/node_api/api.dart b/test/node_api/api.dart index a75bb41ca..35d2889d1 100644 --- a/test/node_api/api.dart +++ b/test/node_api/api.dart @@ -75,6 +75,7 @@ class SassTypes { external NodeSassNullClass get Null; external Function get Number; external Function get String; + external Function get Error; } @JS() diff --git a/test/node_api/function_test.dart b/test/node_api/function_test.dart index f84bf9739..1d4a3eebb 100644 --- a/test/node_api/function_test.dart +++ b/test/node_api/function_test.dart @@ -195,6 +195,16 @@ void main() { expect(error.toString(), contains('aw beans')); }); + test("reports a synchronous sass.types.Error", () async { + var error = await renderError(RenderOptions( + data: "a {b: foo()}", + functions: jsify({ + "foo": allowInterop( + (_) => callConstructor(sass.types.Error, ["aw beans"])) + }))); + expect(error.toString(), contains('aw beans')); + }); + test("reports an asynchronous error", () async { var error = await renderError(RenderOptions( data: "a {b: foo()}", @@ -208,6 +218,19 @@ void main() { expect(error.toString(), contains('aw beans')); }); + test("reports an asynchronous sass.types.Error", () async { + var error = await renderError(RenderOptions( + data: "a {b: foo()}", + functions: jsify({ + "foo": allowInterop((done) { + Future.delayed(Duration.zero).then((_) { + done(callConstructor(sass.types.Error, ["aw beans"])); + }); + }) + }))); + expect(error.toString(), contains('aw beans')); + }); + test("reports a null return", () async { var error = await renderError(RenderOptions( data: "a {b: foo()}", diff --git a/test/output_test.dart b/test/output_test.dart new file mode 100644 index 000000000..be513451a --- /dev/null +++ b/test/output_test.dart @@ -0,0 +1,26 @@ +// Copyright 2019 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. + +// Almost all CSS output tests should go in sass-spec rather than here. This +// just covers tests that explicitly validate out that's considered too +// implementation-specific to verify in sass-spec. + +import 'package:test/test.dart'; + +import 'package:sass/sass.dart'; + +void main() { + // Regression test for sass/dart-sass#623. This needs to be tested here + // because sass-spec normalizes CR LF newlines. + group("normalizes newlines in a loud comment", () { + test("in SCSS", () { + expect(compileString("/* foo\r\n * bar */"), equals("/* foo\n * bar */")); + }); + + test("in Sass", () { + expect(compileString("/*\r\n foo\r\n bar", syntax: Syntax.sass), + equals("/* foo\n * bar */")); + }); + }); +}