Skip to content

Commit

Permalink
[Indented syntax improvements] Dart implementation (#2467)
Browse files Browse the repository at this point in the history
Co-authored-by: Natalie Weizenbaum <nweiz@google.com>
  • Loading branch information
jamesnw and nex3 authored Feb 5, 2025
1 parent d973e3e commit ae4b757
Show file tree
Hide file tree
Showing 15 changed files with 369 additions and 246 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
## 1.83.5-dev
## 1.84.0

* Allow newlines in whitespace in the indented syntax.

* **Potentially breaking bug fix**: Selectors with unmatched brackets now always
produce a parser error. Previously, some edge cases like `[foo#{"]:is(bar"}) {a:
b}` would compile without error, but this was an unintentional bug.

* Fix a bug in which various Color Level 4 functions weren't allowed in plain
CSS.
Expand Down
13 changes: 9 additions & 4 deletions lib/src/parse/at_root_query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,27 @@ class AtRootQueryParser extends Parser {
AtRootQuery parse() {
return wrapSpanFormatException(() {
scanner.expectChar($lparen);
whitespace();
_whitespace();
var include = scanIdentifier("with");
if (!include) expectIdentifier("without", name: '"with" or "without"');
whitespace();
_whitespace();
scanner.expectChar($colon);
whitespace();
_whitespace();

var atRules = <String>{};
do {
atRules.add(identifier().toLowerCase());
whitespace();
_whitespace();
} while (lookingAtIdentifier());
scanner.expectChar($rparen);
scanner.expectDone();

return AtRootQuery(atRules, include: include);
});
}

/// The value of `consumeNewlines` is not relevant for this class.
void _whitespace() {
whitespace(consumeNewlines: true);
}
}
15 changes: 10 additions & 5 deletions lib/src/parse/css.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class CssParser extends ScssParser {
var start = scanner.state;
scanner.expectChar($at);
var name = interpolatedIdentifier();
whitespace();
_whitespace();

return switch (name.asPlain) {
"at-root" ||
Expand Down Expand Up @@ -119,7 +119,7 @@ class CssParser extends ScssParser {
.text
};

whitespace();
_whitespace();
var modifiers = tryImportModifiers();
expectStatementSeparator("@import rule");
return ImportRule(
Expand All @@ -132,7 +132,7 @@ class CssParser extends ScssParser {
// evaluation time.
var start = scanner.state;
scanner.expectChar($lparen);
whitespace();
_whitespace();
var expression = expressionUntilComma();
scanner.expectChar($rparen);
return ParenthesizedExpression(expression, scanner.spanFrom(start));
Expand All @@ -157,7 +157,7 @@ class CssParser extends ScssParser {
var arguments = <Expression>[];
if (!scanner.scanChar($rparen)) {
do {
whitespace();
_whitespace();
if (allowEmptySecondArg &&
arguments.length == 1 &&
scanner.peekChar() == $rparen) {
Expand All @@ -166,7 +166,7 @@ class CssParser extends ScssParser {
}

arguments.add(expressionUntilComma(singleEquals: true));
whitespace();
_whitespace();
} while (scanner.scanChar($comma));
scanner.expectChar($rparen);
}
Expand All @@ -186,4 +186,9 @@ class CssParser extends ScssParser {
var expression = super.namespacedExpression(namespace, start);
error("Module namespaces aren't allowed in plain CSS.", expression.span);
}

/// The value of `consumeNewlines` is not relevant for this class.
void _whitespace() {
whitespace(consumeNewlines: true);
}
}
9 changes: 7 additions & 2 deletions lib/src/parse/keyframe_selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class KeyframeSelectorParser extends Parser {
return wrapSpanFormatException(() {
var selectors = <String>[];
do {
whitespace();
_whitespace();
if (lookingAtIdentifier()) {
if (scanIdentifier("from")) {
selectors.add("from");
Expand All @@ -26,7 +26,7 @@ class KeyframeSelectorParser extends Parser {
} else {
selectors.add(_percentage());
}
whitespace();
_whitespace();
} while (scanner.scanChar($comma));
scanner.expectDone();

Expand Down Expand Up @@ -71,4 +71,9 @@ class KeyframeSelectorParser extends Parser {
buffer.writeCharCode($percent);
return buffer.toString();
}

/// The value of `consumeNewlines` is not relevant for this class.
void _whitespace() {
whitespace(consumeNewlines: true);
}
}
17 changes: 11 additions & 6 deletions lib/src/parse/media_query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ class MediaQueryParser extends Parser {
return wrapSpanFormatException(() {
var queries = <CssMediaQuery>[];
do {
whitespace();
_whitespace();
queries.add(_mediaQuery());
whitespace();
_whitespace();
} while (scanner.scanChar($comma));
scanner.expectDone();
return queries;
Expand All @@ -30,7 +30,7 @@ class MediaQueryParser extends Parser {
// This is somewhat duplicated in StylesheetParser._mediaQuery.
if (scanner.peekChar() == $lparen) {
var conditions = [_mediaInParens()];
whitespace();
_whitespace();

var conjunction = true;
if (scanIdentifier("and")) {
Expand All @@ -57,7 +57,7 @@ class MediaQueryParser extends Parser {
}
}

whitespace();
_whitespace();
if (!lookingAtIdentifier()) {
// For example, "@media screen {"
return CssMediaQuery.type(identifier1);
Expand All @@ -70,7 +70,7 @@ class MediaQueryParser extends Parser {
// For example, "@media screen and ..."
type = identifier1;
} else {
whitespace();
_whitespace();
modifier = identifier1;
type = identifier2;
if (scanIdentifier("and")) {
Expand Down Expand Up @@ -102,7 +102,7 @@ class MediaQueryParser extends Parser {
var result = <String>[];
while (true) {
result.add(_mediaInParens());
whitespace();
_whitespace();

if (!scanIdentifier(operator)) return result;
expectWhitespace();
Expand All @@ -117,4 +117,9 @@ class MediaQueryParser extends Parser {
scanner.expectChar($rparen);
return result;
}

/// The value of `consumeNewlines` is not relevant for this class.
void _whitespace() {
whitespace(consumeNewlines: true);
}
}
29 changes: 20 additions & 9 deletions lib/src/parse/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,31 @@ class Parser {
if (!scanner.scanChar($dollar)) return false;
if (!lookingAtIdentifier()) return false;
identifier();
whitespace();
whitespace(consumeNewlines: true);
return scanner.scanChar($colon);
}

// ## Tokens

/// Consumes whitespace, including any comments.
///
/// If [consumeNewlines] is `true`, the indented syntax will consume newlines
/// as whitespace. It should only be set to `true` in positions when a
/// statement can't end.
@protected
void whitespace() {
void whitespace({required bool consumeNewlines}) {
do {
whitespaceWithoutComments();
whitespaceWithoutComments(consumeNewlines: consumeNewlines);
} while (scanComment());
}

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

/// Like [whitespace], but throws an error if no whitespace is consumed.
///
/// If [consumeNewlines] is `true`, the indented syntax will consume newlines
/// as whitespace. It should only be set to `true` in positions when a
/// statement can't end.
@protected
void expectWhitespace() {
void expectWhitespace({bool consumeNewlines = false}) {
if (scanner.isDone || !(scanner.peekChar().isWhitespace || scanComment())) {
scanner.error("Expected whitespace.");
}

whitespace();
whitespace(consumeNewlines: consumeNewlines);
}

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

whitespace();
whitespace(consumeNewlines: true);

// Match Ruby Sass's behavior: parse a raw URL() if possible, and if not
// backtrack and re-parse as a function expression.
Expand All @@ -407,7 +418,7 @@ class Parser {
>= 0x0080:
buffer.writeCharCode(scanner.readChar());
case int(isWhitespace: true):
whitespace();
whitespace(consumeNewlines: true);
if (scanner.peekChar() != $rparen) break loop;
case $rparen:
buffer.writeCharCode(scanner.readChar());
Expand Down
55 changes: 28 additions & 27 deletions lib/src/parse/sass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ class SassParser extends StylesheetParser {
}

void expectStatementSeparator([String? name]) {
if (!atEndOfStatement()) _expectNewline();
var trailingSemicolon = _tryTrailingSemicolon();
if (!atEndOfStatement()) {
_expectNewline(trailingSemicolon: trailingSemicolon);
}
if (_peekIndentation() <= currentIndentation) return;
scanner.error(
"Nothing may be indented ${name == null ? 'here' : 'beneath a $name'}.",
Expand Down Expand Up @@ -259,7 +262,7 @@ class SassParser extends StylesheetParser {
buffer.writeCharCode(scanner.readChar());
buffer.writeCharCode(scanner.readChar());
var span = scanner.spanFrom(start);
whitespace();
whitespace(consumeNewlines: false);

// For backwards compatibility, allow additional comments after
// the initial comment is closed.
Expand All @@ -269,7 +272,7 @@ class SassParser extends StylesheetParser {
_expectNewline();
}
_readIndentation();
whitespace();
whitespace(consumeNewlines: false);
}

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

void whitespaceWithoutComments() {
// This overrides whitespace consumption so that it doesn't consume
// newlines.
void whitespaceWithoutComments({required bool consumeNewlines}) {
// This overrides whitespace consumption to only consume newlines when
// `consumeNewlines` is true.
while (!scanner.isDone) {
var next = scanner.peekChar();
if (next != $tab && next != $space) break;
if (consumeNewlines ? !next.isWhitespace : !next.isSpaceOrTab) break;
scanner.readChar();
}
}

void loudComment() {
// This overrides loud comment consumption so that it doesn't consume
// multi-line comments.
scanner.expect("/*");
while (true) {
var next = scanner.readChar();
if (next.isNewline) scanner.error("expected */.");
if (next != $asterisk) continue;

do {
next = scanner.readChar();
} while (next == $asterisk);
if (next == $slash) break;
}
}

/// Expect and consume a single newline character.
void _expectNewline() {
///
/// If [trailingSemicolon] is true, this follows a semicolon, which is used
/// for error reporting.
void _expectNewline({bool trailingSemicolon = false}) {
switch (scanner.peekChar()) {
case $semicolon:
scanner.error("semicolons aren't allowed in the indented syntax.");
case $cr:
scanner.readChar();
if (scanner.peekChar() == $lf) scanner.readChar();
Expand All @@ -348,7 +336,9 @@ class SassParser extends StylesheetParser {
scanner.readChar();
return;
default:
scanner.error("expected newline.");
scanner.error(trailingSemicolon
? "multiple statements on one line are not supported in the indented syntax."
: "expected newline.");
}
}

Expand Down Expand Up @@ -467,4 +457,15 @@ class SassParser extends StylesheetParser {
position: scanner.position - scanner.column, length: scanner.column);
}
}

/// Consumes a semicolon and trailing whitespace, including comments.
///
/// Returns whether a semicolon was consumed.
bool _tryTrailingSemicolon() {
if (scanCharIf((char) => char == $semicolon)) {
whitespace(consumeNewlines: false);
return true;
}
return false;
}
}
Loading

0 comments on commit ae4b757

Please sign in to comment.