Skip to content

Commit

Permalink
Properly adjust source maps when prepending encoding information (#470)
Browse files Browse the repository at this point in the history
Closes #469
  • Loading branch information
nex3 authored Sep 10, 2018
1 parent edf3370 commit ede9c81
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 20 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.13.3

* Properly generate source maps for stylesheets that emit `@charset`
declarations.

## 1.13.2

* Properly parse `:nth-child()` and `:nth-last-child()` selectors with
Expand Down
4 changes: 2 additions & 2 deletions lib/src/util/no_source_map_buffer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ class NoSourceMapBuffer implements SourceMapBuffer {
void clear() =>
throw new UnsupportedError("SourceMapBuffer.clear() is not supported.");

SingleMapping buildSourceMap() =>
throw new UnsupportedError("NoSourceMapBuffer.clear() is not supported.");
SingleMapping buildSourceMap({String prefix}) => throw new UnsupportedError(
"NoSourceMapBuffer.buildSourceMap() is not supported.");
}
31 changes: 30 additions & 1 deletion lib/src/util/source_map_buffer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,35 @@ class SourceMapBuffer implements StringBuffer {

/// Returns the source map for the file being written.
///
/// If [prefix] is passed, all the entries in the source map will be moved
/// forward by the number of characters and lines in [prefix].
///
/// [SingleMapping.targetUrl] will be `null`.
SingleMapping buildSourceMap() => new SingleMapping.fromEntries(_entries);
SingleMapping buildSourceMap({String prefix}) {
if (prefix == null || prefix.isEmpty) {
return new SingleMapping.fromEntries(_entries);
}

var prefixLength = prefix.length;
var prefixLines = 0;
var prefixColumn = 0;
for (var i = 0; i < prefix.length; i++) {
if (prefix.codeUnitAt(i) == $lf) {
prefixLines++;
prefixColumn = 0;
} else {
prefixColumn++;
}
}

return new SingleMapping.fromEntries(_entries.map((entry) => new Entry(
entry.source,
new SourceLocation(entry.target.offset + prefixLength,
line: entry.target.line + prefixLines,
// Only adjust the column for entries that are on the same line as
// the last chunk of the prefix.
column: entry.target.column +
(entry.target.line == 0 ? prefixColumn : 0)),
entry.identifierName)));
}
}
38 changes: 38 additions & 0 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,44 @@ int countOccurrences(String string, int codeUnit) {
return count;
}

/// Like [String.trim], but only trims ASCII whitespace.
String trimAscii(String string) {
var start = _firstNonWhitespace(string);
return start == null
? ""
: string.substring(start, _lastNonWhitespace(string) + 1);
}

/// Like [String.trimLeft], but only trims ASCII whitespace.
String trimAsciiLeft(String string) {
var start = _firstNonWhitespace(string);
return start == null ? "" : string.substring(start);
}

/// Like [String.trimRight], but only trims ASCII whitespace.
String trimAsciiRight(String string) {
var end = _lastNonWhitespace(string);
return end == null ? "" : string.substring(0, end + 1);
}

/// Returns the index of the first character in [string] that's not ASCII
/// whitepsace, or [null] if [string] is entirely spaces.
int _firstNonWhitespace(String string) {
for (var i = 0; i < string.length; i++) {
if (!isWhitespace(string.codeUnitAt(i))) return i;
}
return null;
}

/// Returns the index of the last character in [string] that's not ASCII
/// whitespace, or [null] if [string] is entirely spaces.
int _lastNonWhitespace(String string) {
for (var i = string.length - 1; i >= 0; i--) {
if (!isWhitespace(string.codeUnitAt(i))) return i;
}
return null;
}

/// Flattens the first level of nested arrays in [iterable].
///
/// The return value is ordered first by index in the nested iterable, then by
Expand Down
12 changes: 8 additions & 4 deletions lib/src/visitor/serialize.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,20 @@ SerializeResult serialize(CssNode node,
sourceMap: sourceMap);
node.accept(visitor);
var css = visitor._buffer.toString();
String prefix;
if (css.codeUnits.any((codeUnit) => codeUnit > 0x7F)) {
if (style == OutputStyle.compressed) {
css = '\uFEFF$css';
prefix = '\uFEFF';
} else {
css = '@charset "UTF-8";\n$css';
prefix = '@charset "UTF-8";\n';
}
} else {
prefix = '';
}

return new SerializeResult(css,
sourceMap: sourceMap ? visitor._buffer.buildSourceMap() : null,
return new SerializeResult(prefix + css,
sourceMap:
sourceMap ? visitor._buffer.buildSourceMap(prefix: prefix) : null,
sourceFiles: sourceMap ? visitor._buffer.sourceFiles : null);
}

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.13.2
version: 1.13.3-dev
description: A Sass implementation in Dart.
author: Dart Team <[email protected]>
homepage: https://github.com/sass/dart-sass
Expand Down
57 changes: 45 additions & 12 deletions test/source_map_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:test/test.dart';
import 'package:tuple/tuple.dart';

import 'package:sass/sass.dart';
import 'package:sass/src/utils.dart';

main() {
group("maps source to target for", () {
Expand Down Expand Up @@ -617,6 +618,35 @@ main() {
});
});
});

group("a stylesheet with Unicode characters", () {
test("in expanded mode", () {
_expectSourceMap("""
{{1}}föö
{{2}}bär: bäz
""", """
{{1}}föö {
{{2}}bär: bäz;
}
""", """
@charset "UTF-8";
{{1}}föö {
{{2}}bär: bäz;
}
""");
});

test("in compressed mode", () {
_expectSourceMap("""
{{1}}föö
{{2}}bär: bäz
""", """
{{1}}föö {
{{2}}bär: bäz;
}
""", "\uFEFF{{1}}föö{{{2}}bär:bäz}", style: OutputStyle.compressed);
});
});
});

test("doesn't use the source map location for variable errors", () {
Expand Down Expand Up @@ -658,13 +688,14 @@ main() {
/// target locations.
///
/// This also re-indents the input strings with [_reindent].
void _expectSourceMap(String sass, String scss, String css) {
_expectSassSourceMap(sass, css);
_expectScssSourceMap(scss, css);
void _expectSourceMap(String sass, String scss, String css,
{OutputStyle style}) {
_expectSassSourceMap(sass, css, style: style);
_expectScssSourceMap(scss, css, style: style);
}

/// Like [_expectSourceMap], but with only SCSS source.
void _expectScssSourceMap(String scss, String css) {
void _expectScssSourceMap(String scss, String css, {OutputStyle style}) {
var scssTuple = _extractLocations(_reindent(scss));
var scssText = scssTuple.item1;
var scssLocations = _tuplesToMap(scssTuple.item2);
Expand All @@ -674,13 +705,14 @@ void _expectScssSourceMap(String scss, String css) {
var cssLocations = cssTuple.item2;

SingleMapping scssMap;
var scssOutput = compileString(scssText, sourceMap: (map) => scssMap = map);
var scssOutput =
compileString(scssText, sourceMap: (map) => scssMap = map, style: style);
expect(scssOutput, equals(cssText));
_expectMapMatches(scssMap, scssText, cssText, scssLocations, cssLocations);
}

/// Like [_expectSourceMap], but with only indented source.
void _expectSassSourceMap(String sass, String css) {
void _expectSassSourceMap(String sass, String css, {OutputStyle style}) {
var sassTuple = _extractLocations(_reindent(sass));
var sassText = sassTuple.item1;
var sassLocations = _tuplesToMap(sassTuple.item2);
Expand All @@ -691,21 +723,21 @@ void _expectSassSourceMap(String sass, String css) {

SingleMapping sassMap;
var sassOutput = compileString(sassText,
indented: true, sourceMap: (map) => sassMap = map);
indented: true, sourceMap: (map) => sassMap = map, style: style);
expect(sassOutput, equals(cssText));
_expectMapMatches(sassMap, sassText, cssText, sassLocations, cssLocations);
}

/// Returns [string] with leading whitepsace stripped from each line so that the
/// least-indented line has zero indentation.
String _reindent(String string) {
var lines = string.trimRight().split("\n");
var lines = trimAsciiRight(string).split("\n");
var minIndent = lines
.where((line) => line.trim().isNotEmpty)
.map((line) => line.length - line.trimLeft().length)
.where((line) => trimAscii(line).isNotEmpty)
.map((line) => line.length - trimAsciiLeft(line).length)
.reduce((length1, length2) => length1 < length2 ? length1 : length2);
return lines
.map((line) => line.trim().isEmpty ? "" : line.substring(minIndent))
.map((line) => trimAscii(line).isEmpty ? "" : line.substring(minIndent))
.join("\n");
}

Expand All @@ -720,7 +752,8 @@ Tuple2<String, List<Tuple2<String, SourceLocation>>> _extractLocations(
var line = 0;
var column = 0;
while (!scanner.isDone) {
if (scanner.scan("{{")) {
if (scanner.matches(new RegExp(r"{{[^{]"))) {
scanner.expect("{{");
var start = scanner.position;
while (!scanner.scan("}}")) {
scanner.readChar();
Expand Down

0 comments on commit ede9c81

Please sign in to comment.