Skip to content

Commit 1faf81c

Browse files
nickbehrensGoodwinenex3
authored
Fix #417 preserve the location of trailing loud comments (#849)
See sass/sass-spec#1485 - Update lib/src/visitor/serialize.dart to stop using old-style int-based for loop. - Extend FileSpan with a .contains(targetSpan) method Co-authored-by: Nick Behrens <nbehrens@google.com> Co-authored-by: Carlos Israel Ortiz García <goodwine@google.com> Co-Authored-By: Natalie Weizenbaum <nweiz@google.com>
1 parent cb74cc4 commit 1faf81c

File tree

4 files changed

+329
-37
lines changed

4 files changed

+329
-37
lines changed

lib/src/util/span.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ extension SpanExtensions on FileSpan {
7777
_scanIdentifier(scanner);
7878
return subspan(scanner.position).trimLeft();
7979
}
80+
81+
/// Whether [this] FileSpan contains the [target] FileSpan.
82+
///
83+
/// Validates the FileSpans to be in the same file and for the [target] to be
84+
/// within [this] FileSpan inclusive range [start,end].
85+
bool contains(FileSpan target) =>
86+
file.url == target.file.url &&
87+
start.offset <= target.start.offset &&
88+
end.offset >= target.end.offset;
8089
}
8190

8291
/// Consumes an identifier from [scanner].

lib/src/visitor/serialize.dart

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import '../util/character.dart';
2121
import '../util/no_source_map_buffer.dart';
2222
import '../util/number.dart';
2323
import '../util/source_map_buffer.dart';
24+
import '../util/span.dart';
2425
import '../value.dart';
2526
import 'interface/css.dart';
2627
import 'interface/selector.dart';
@@ -153,14 +154,16 @@ class _SerializeVisitor
153154

154155
void visitCssStylesheet(CssStylesheet node) {
155156
CssNode? previous;
156-
for (var i = 0; i < node.children.length; i++) {
157-
var child = node.children[i];
157+
for (var child in node.children) {
158158
if (_isInvisible(child)) continue;
159-
160159
if (previous != null) {
161160
if (_requiresSemicolon(previous)) _buffer.writeCharCode($semicolon);
162-
_writeLineFeed();
163-
if (previous.isGroupEnd) _writeLineFeed();
161+
if (_isTrailingComment(child, previous)) {
162+
_writeOptionalSpace();
163+
} else {
164+
_writeLineFeed();
165+
if (previous.isGroupEnd) _writeLineFeed();
166+
}
164167
}
165168
previous = child;
166169

@@ -208,7 +211,7 @@ class _SerializeVisitor
208211

209212
if (!node.isChildless) {
210213
_writeOptionalSpace();
211-
_visitChildren(node.children);
214+
_visitChildren(node);
212215
}
213216
}
214217

@@ -226,7 +229,7 @@ class _SerializeVisitor
226229
});
227230

228231
_writeOptionalSpace();
229-
_visitChildren(node.children);
232+
_visitChildren(node);
230233
}
231234

232235
void visitCssImport(CssImport node) {
@@ -273,7 +276,7 @@ class _SerializeVisitor
273276
() =>
274277
_writeBetween(node.selector.value, _commaSeparator, _buffer.write));
275278
_writeOptionalSpace();
276-
_visitChildren(node.children);
279+
_visitChildren(node);
277280
}
278281

279282
void _visitMediaQuery(CssMediaQuery query) {
@@ -298,7 +301,7 @@ class _SerializeVisitor
298301

299302
_for(node.selector, () => node.selector.value.accept(this));
300303
_writeOptionalSpace();
301-
_visitChildren(node.children);
304+
_visitChildren(node);
302305
}
303306

304307
void visitCssSupportsRule(CssSupportsRule node) {
@@ -315,7 +318,7 @@ class _SerializeVisitor
315318
});
316319

317320
_writeOptionalSpace();
318-
_visitChildren(node.children);
321+
_visitChildren(node);
319322
}
320323

321324
void visitCssDeclaration(CssDeclaration node) {
@@ -1286,45 +1289,80 @@ class _SerializeVisitor
12861289
void _write(CssValue<String> value) =>
12871290
_for(value, () => _buffer.write(value.value));
12881291

1289-
/// Emits [children] in a block.
1290-
void _visitChildren(List<CssNode> children) {
1292+
/// Emits [parent.children] in a block.
1293+
void _visitChildren(CssParentNode parent) {
12911294
_buffer.writeCharCode($lbrace);
1292-
if (children.every(_isInvisible)) {
1293-
_buffer.writeCharCode($rbrace);
1294-
return;
1295-
}
12961295

1297-
_writeLineFeed();
1298-
CssNode? previous_;
1299-
_indent(() {
1300-
for (var i = 0; i < children.length; i++) {
1301-
var child = children[i];
1302-
if (_isInvisible(child)) continue;
1296+
CssNode? prePrevious;
1297+
CssNode? previous;
1298+
for (var child in parent.children) {
1299+
if (_isInvisible(child)) continue;
13031300

1304-
var previous = previous_; // dart-lang/sdk#45348
1305-
if (previous != null) {
1306-
if (_requiresSemicolon(previous)) _buffer.writeCharCode($semicolon);
1307-
_writeLineFeed();
1308-
if (previous.isGroupEnd) _writeLineFeed();
1309-
}
1310-
previous_ = child;
1301+
if (previous != null && _requiresSemicolon(previous)) {
1302+
_buffer.writeCharCode($semicolon);
1303+
}
13111304

1312-
child.accept(this);
1305+
if (_isTrailingComment(child, previous ?? parent)) {
1306+
_writeOptionalSpace();
1307+
_withoutIndendation(() => child.accept(this));
1308+
} else {
1309+
_writeLineFeed();
1310+
_indent(() {
1311+
child.accept(this);
1312+
});
13131313
}
1314-
});
13151314

1316-
if (_requiresSemicolon(previous_!) && !_isCompressed) {
1317-
_buffer.writeCharCode($semicolon);
1315+
prePrevious = previous;
1316+
previous = child;
13181317
}
1319-
_writeLineFeed();
1320-
_writeIndentation();
1318+
1319+
if (previous != null) {
1320+
if (_requiresSemicolon(previous) && !_isCompressed) {
1321+
_buffer.writeCharCode($semicolon);
1322+
}
1323+
1324+
if (prePrevious == null && _isTrailingComment(previous, parent)) {
1325+
_writeOptionalSpace();
1326+
} else {
1327+
_writeLineFeed();
1328+
_writeIndentation();
1329+
}
1330+
}
1331+
13211332
_buffer.writeCharCode($rbrace);
13221333
}
13231334

13241335
/// Whether [node] requires a semicolon to be written after it.
1325-
bool _requiresSemicolon(CssNode? node) =>
1336+
bool _requiresSemicolon(CssNode node) =>
13261337
node is CssParentNode ? node.isChildless : node is! CssComment;
13271338

1339+
/// Whether [node] represents a trailing comment when it appears after
1340+
/// [previous] in a sequence of nodes being serialized.
1341+
///
1342+
/// Note [previous] could be either a sibling of [node] or the parent of
1343+
/// [node], with [node] being the first visible child.
1344+
bool _isTrailingComment(CssNode node, CssNode previous) {
1345+
// Short-circuit in compressed mode to avoid expensive span shenanigans
1346+
// (shespanigans?), since we're compressing all whitespace anyway.
1347+
if (_isCompressed) return false;
1348+
if (node is! CssComment) return false;
1349+
1350+
if (!previous.span.contains(node.span)) {
1351+
return node.span.start.line == previous.span.end.line;
1352+
}
1353+
1354+
// Walk back from just before the current node starts looking for the
1355+
// parent's left brace (to open the child block). This is safer than a
1356+
// simple forward search of the previous.span.text as that might contain
1357+
// other left braces.
1358+
var searchFrom = node.span.start.offset - previous.span.start.offset - 1;
1359+
var endOffset = previous.span.text.lastIndexOf("{", searchFrom);
1360+
endOffset = math.max(0, endOffset);
1361+
var span = previous.span.file.span(
1362+
previous.span.start.offset, previous.span.start.offset + endOffset);
1363+
return node.span.start.line == span.end.line;
1364+
}
1365+
13281366
/// Writes a line feed, unless this emitting compressed CSS.
13291367
void _writeLineFeed() {
13301368
if (!_isCompressed) _buffer.write(_lineFeed.text);
@@ -1373,6 +1411,14 @@ class _SerializeVisitor
13731411
_indentation--;
13741412
}
13751413

1414+
/// Runs [callback] without any indentation.
1415+
void _withoutIndendation(void callback()) {
1416+
var savedIndentation = _indentation;
1417+
_indentation = 0;
1418+
callback();
1419+
_indentation = savedIndentation;
1420+
}
1421+
13761422
/// Returns whether [node] is considered invisible.
13771423
bool _isInvisible(CssNode node) {
13781424
if (_inspect) return false;

test/dart_api_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ a {
304304
expect(compileStringAsync("""
305305
@use 'sass:meta';
306306
@include meta.load-css("other.scss");
307-
""", loadPaths: [d.sandbox]), completion(equals("/**/\n/**/")));
307+
""", loadPaths: [d.sandbox]), completion(equals("/**/ /**/")));
308308

309309
// Give the race condition time to appear.
310310
await pumpEventQueue();

0 commit comments

Comments
 (0)