Skip to content

Commit ae4b757

Browse files
jamesnwnex3
andauthored
[Indented syntax improvements] Dart implementation (#2467)
Co-authored-by: Natalie Weizenbaum <[email protected]>
1 parent d973e3e commit ae4b757

File tree

15 files changed

+369
-246
lines changed

15 files changed

+369
-246
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
## 1.83.5-dev
1+
## 1.84.0
2+
3+
* Allow newlines in whitespace in the indented syntax.
4+
5+
* **Potentially breaking bug fix**: Selectors with unmatched brackets now always
6+
produce a parser error. Previously, some edge cases like `[foo#{"]:is(bar"}) {a:
7+
b}` would compile without error, but this was an unintentional bug.
28

39
* Fix a bug in which various Color Level 4 functions weren't allowed in plain
410
CSS.

lib/src/parse/at_root_query.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,27 @@ class AtRootQueryParser extends Parser {
1414
AtRootQuery parse() {
1515
return wrapSpanFormatException(() {
1616
scanner.expectChar($lparen);
17-
whitespace();
17+
_whitespace();
1818
var include = scanIdentifier("with");
1919
if (!include) expectIdentifier("without", name: '"with" or "without"');
20-
whitespace();
20+
_whitespace();
2121
scanner.expectChar($colon);
22-
whitespace();
22+
_whitespace();
2323

2424
var atRules = <String>{};
2525
do {
2626
atRules.add(identifier().toLowerCase());
27-
whitespace();
27+
_whitespace();
2828
} while (lookingAtIdentifier());
2929
scanner.expectChar($rparen);
3030
scanner.expectDone();
3131

3232
return AtRootQuery(atRules, include: include);
3333
});
3434
}
35+
36+
/// The value of `consumeNewlines` is not relevant for this class.
37+
void _whitespace() {
38+
whitespace(consumeNewlines: true);
39+
}
3540
}

lib/src/parse/css.dart

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class CssParser extends ScssParser {
5555
var start = scanner.state;
5656
scanner.expectChar($at);
5757
var name = interpolatedIdentifier();
58-
whitespace();
58+
_whitespace();
5959

6060
return switch (name.asPlain) {
6161
"at-root" ||
@@ -119,7 +119,7 @@ class CssParser extends ScssParser {
119119
.text
120120
};
121121

122-
whitespace();
122+
_whitespace();
123123
var modifiers = tryImportModifiers();
124124
expectStatementSeparator("@import rule");
125125
return ImportRule(
@@ -132,7 +132,7 @@ class CssParser extends ScssParser {
132132
// evaluation time.
133133
var start = scanner.state;
134134
scanner.expectChar($lparen);
135-
whitespace();
135+
_whitespace();
136136
var expression = expressionUntilComma();
137137
scanner.expectChar($rparen);
138138
return ParenthesizedExpression(expression, scanner.spanFrom(start));
@@ -157,7 +157,7 @@ class CssParser extends ScssParser {
157157
var arguments = <Expression>[];
158158
if (!scanner.scanChar($rparen)) {
159159
do {
160-
whitespace();
160+
_whitespace();
161161
if (allowEmptySecondArg &&
162162
arguments.length == 1 &&
163163
scanner.peekChar() == $rparen) {
@@ -166,7 +166,7 @@ class CssParser extends ScssParser {
166166
}
167167

168168
arguments.add(expressionUntilComma(singleEquals: true));
169-
whitespace();
169+
_whitespace();
170170
} while (scanner.scanChar($comma));
171171
scanner.expectChar($rparen);
172172
}
@@ -186,4 +186,9 @@ class CssParser extends ScssParser {
186186
var expression = super.namespacedExpression(namespace, start);
187187
error("Module namespaces aren't allowed in plain CSS.", expression.span);
188188
}
189+
190+
/// The value of `consumeNewlines` is not relevant for this class.
191+
void _whitespace() {
192+
whitespace(consumeNewlines: true);
193+
}
189194
}

lib/src/parse/keyframe_selector.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class KeyframeSelectorParser extends Parser {
1515
return wrapSpanFormatException(() {
1616
var selectors = <String>[];
1717
do {
18-
whitespace();
18+
_whitespace();
1919
if (lookingAtIdentifier()) {
2020
if (scanIdentifier("from")) {
2121
selectors.add("from");
@@ -26,7 +26,7 @@ class KeyframeSelectorParser extends Parser {
2626
} else {
2727
selectors.add(_percentage());
2828
}
29-
whitespace();
29+
_whitespace();
3030
} while (scanner.scanChar($comma));
3131
scanner.expectDone();
3232

@@ -71,4 +71,9 @@ class KeyframeSelectorParser extends Parser {
7171
buffer.writeCharCode($percent);
7272
return buffer.toString();
7373
}
74+
75+
/// The value of `consumeNewlines` is not relevant for this class.
76+
void _whitespace() {
77+
whitespace(consumeNewlines: true);
78+
}
7479
}

lib/src/parse/media_query.dart

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ class MediaQueryParser extends Parser {
1616
return wrapSpanFormatException(() {
1717
var queries = <CssMediaQuery>[];
1818
do {
19-
whitespace();
19+
_whitespace();
2020
queries.add(_mediaQuery());
21-
whitespace();
21+
_whitespace();
2222
} while (scanner.scanChar($comma));
2323
scanner.expectDone();
2424
return queries;
@@ -30,7 +30,7 @@ class MediaQueryParser extends Parser {
3030
// This is somewhat duplicated in StylesheetParser._mediaQuery.
3131
if (scanner.peekChar() == $lparen) {
3232
var conditions = [_mediaInParens()];
33-
whitespace();
33+
_whitespace();
3434

3535
var conjunction = true;
3636
if (scanIdentifier("and")) {
@@ -57,7 +57,7 @@ class MediaQueryParser extends Parser {
5757
}
5858
}
5959

60-
whitespace();
60+
_whitespace();
6161
if (!lookingAtIdentifier()) {
6262
// For example, "@media screen {"
6363
return CssMediaQuery.type(identifier1);
@@ -70,7 +70,7 @@ class MediaQueryParser extends Parser {
7070
// For example, "@media screen and ..."
7171
type = identifier1;
7272
} else {
73-
whitespace();
73+
_whitespace();
7474
modifier = identifier1;
7575
type = identifier2;
7676
if (scanIdentifier("and")) {
@@ -102,7 +102,7 @@ class MediaQueryParser extends Parser {
102102
var result = <String>[];
103103
while (true) {
104104
result.add(_mediaInParens());
105-
whitespace();
105+
_whitespace();
106106

107107
if (!scanIdentifier(operator)) return result;
108108
expectWhitespace();
@@ -117,4 +117,9 @@ class MediaQueryParser extends Parser {
117117
scanner.expectChar($rparen);
118118
return result;
119119
}
120+
121+
/// The value of `consumeNewlines` is not relevant for this class.
122+
void _whitespace() {
123+
whitespace(consumeNewlines: true);
124+
}
120125
}

lib/src/parse/parser.dart

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,31 @@ class Parser {
6767
if (!scanner.scanChar($dollar)) return false;
6868
if (!lookingAtIdentifier()) return false;
6969
identifier();
70-
whitespace();
70+
whitespace(consumeNewlines: true);
7171
return scanner.scanChar($colon);
7272
}
7373

7474
// ## Tokens
7575

7676
/// Consumes whitespace, including any comments.
77+
///
78+
/// If [consumeNewlines] is `true`, the indented syntax will consume newlines
79+
/// as whitespace. It should only be set to `true` in positions when a
80+
/// statement can't end.
7781
@protected
78-
void whitespace() {
82+
void whitespace({required bool consumeNewlines}) {
7983
do {
80-
whitespaceWithoutComments();
84+
whitespaceWithoutComments(consumeNewlines: consumeNewlines);
8185
} while (scanComment());
8286
}
8387

8488
/// Consumes whitespace, but not comments.
89+
///
90+
/// If [consumeNewlines] is `true`, the indented syntax will consume newlines
91+
/// as whitespace. It should only be set to `true` in positions when a
92+
/// statement can't end.
8593
@protected
86-
void whitespaceWithoutComments() {
94+
void whitespaceWithoutComments({required bool consumeNewlines}) {
8795
while (!scanner.isDone && scanner.peekChar().isWhitespace) {
8896
scanner.readChar();
8997
}
@@ -116,13 +124,16 @@ class Parser {
116124
}
117125

118126
/// Like [whitespace], but throws an error if no whitespace is consumed.
127+
///
128+
/// If [consumeNewlines] is `true`, the indented syntax will consume newlines
129+
/// as whitespace. It should only be set to `true` in positions when a
130+
/// statement can't end.
119131
@protected
120-
void expectWhitespace() {
132+
void expectWhitespace({bool consumeNewlines = false}) {
121133
if (scanner.isDone || !(scanner.peekChar().isWhitespace || scanComment())) {
122134
scanner.error("Expected whitespace.");
123135
}
124-
125-
whitespace();
136+
whitespace(consumeNewlines: consumeNewlines);
126137
}
127138

128139
/// Consumes and ignores a single silent (Sass-style) comment, not including
@@ -386,7 +397,7 @@ class Parser {
386397
return null;
387398
}
388399

389-
whitespace();
400+
whitespace(consumeNewlines: true);
390401

391402
// Match Ruby Sass's behavior: parse a raw URL() if possible, and if not
392403
// backtrack and re-parse as a function expression.
@@ -407,7 +418,7 @@ class Parser {
407418
>= 0x0080:
408419
buffer.writeCharCode(scanner.readChar());
409420
case int(isWhitespace: true):
410-
whitespace();
421+
whitespace(consumeNewlines: true);
411422
if (scanner.peekChar() != $rparen) break loop;
412423
case $rparen:
413424
buffer.writeCharCode(scanner.readChar());

lib/src/parse/sass.dart

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ class SassParser extends StylesheetParser {
5454
}
5555

5656
void expectStatementSeparator([String? name]) {
57-
if (!atEndOfStatement()) _expectNewline();
57+
var trailingSemicolon = _tryTrailingSemicolon();
58+
if (!atEndOfStatement()) {
59+
_expectNewline(trailingSemicolon: trailingSemicolon);
60+
}
5861
if (_peekIndentation() <= currentIndentation) return;
5962
scanner.error(
6063
"Nothing may be indented ${name == null ? 'here' : 'beneath a $name'}.",
@@ -259,7 +262,7 @@ class SassParser extends StylesheetParser {
259262
buffer.writeCharCode(scanner.readChar());
260263
buffer.writeCharCode(scanner.readChar());
261264
var span = scanner.spanFrom(start);
262-
whitespace();
265+
whitespace(consumeNewlines: false);
263266

264267
// For backwards compatibility, allow additional comments after
265268
// the initial comment is closed.
@@ -269,7 +272,7 @@ class SassParser extends StylesheetParser {
269272
_expectNewline();
270273
}
271274
_readIndentation();
272-
whitespace();
275+
whitespace(consumeNewlines: false);
273276
}
274277

275278
if (!scanner.isDone && !scanner.peekChar().isNewline) {
@@ -309,37 +312,22 @@ class SassParser extends StylesheetParser {
309312
return LoudComment(buffer.interpolation(scanner.spanFrom(start)));
310313
}
311314

312-
void whitespaceWithoutComments() {
313-
// This overrides whitespace consumption so that it doesn't consume
314-
// newlines.
315+
void whitespaceWithoutComments({required bool consumeNewlines}) {
316+
// This overrides whitespace consumption to only consume newlines when
317+
// `consumeNewlines` is true.
315318
while (!scanner.isDone) {
316319
var next = scanner.peekChar();
317-
if (next != $tab && next != $space) break;
320+
if (consumeNewlines ? !next.isWhitespace : !next.isSpaceOrTab) break;
318321
scanner.readChar();
319322
}
320323
}
321324

322-
void loudComment() {
323-
// This overrides loud comment consumption so that it doesn't consume
324-
// multi-line comments.
325-
scanner.expect("/*");
326-
while (true) {
327-
var next = scanner.readChar();
328-
if (next.isNewline) scanner.error("expected */.");
329-
if (next != $asterisk) continue;
330-
331-
do {
332-
next = scanner.readChar();
333-
} while (next == $asterisk);
334-
if (next == $slash) break;
335-
}
336-
}
337-
338325
/// Expect and consume a single newline character.
339-
void _expectNewline() {
326+
///
327+
/// If [trailingSemicolon] is true, this follows a semicolon, which is used
328+
/// for error reporting.
329+
void _expectNewline({bool trailingSemicolon = false}) {
340330
switch (scanner.peekChar()) {
341-
case $semicolon:
342-
scanner.error("semicolons aren't allowed in the indented syntax.");
343331
case $cr:
344332
scanner.readChar();
345333
if (scanner.peekChar() == $lf) scanner.readChar();
@@ -348,7 +336,9 @@ class SassParser extends StylesheetParser {
348336
scanner.readChar();
349337
return;
350338
default:
351-
scanner.error("expected newline.");
339+
scanner.error(trailingSemicolon
340+
? "multiple statements on one line are not supported in the indented syntax."
341+
: "expected newline.");
352342
}
353343
}
354344

@@ -467,4 +457,15 @@ class SassParser extends StylesheetParser {
467457
position: scanner.position - scanner.column, length: scanner.column);
468458
}
469459
}
460+
461+
/// Consumes a semicolon and trailing whitespace, including comments.
462+
///
463+
/// Returns whether a semicolon was consumed.
464+
bool _tryTrailingSemicolon() {
465+
if (scanCharIf((char) => char == $semicolon)) {
466+
whitespace(consumeNewlines: false);
467+
return true;
468+
}
469+
return false;
470+
}
470471
}

0 commit comments

Comments
 (0)