diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c2186b25..6cf85eb40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.84.0-dev + +* 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. + ## 1.83.1 * Fix a bug where `--quiet-deps` would get deactivated for `@content` blocks, diff --git a/lib/src/parse/at_root_query.dart b/lib/src/parse/at_root_query.dart index f69501ea6..008ac1b26 100644 --- a/lib/src/parse/at_root_query.dart +++ b/lib/src/parse/at_root_query.dart @@ -14,17 +14,17 @@ 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 = {}; do { atRules.add(identifier().toLowerCase()); - whitespace(); + _whitespace(); } while (lookingAtIdentifier()); scanner.expectChar($rparen); scanner.expectDone(); @@ -32,4 +32,9 @@ class AtRootQueryParser extends Parser { return AtRootQuery(atRules, include: include); }); } + + /// The value of `consumeNewlines` is not relevant for this class. + void _whitespace() { + whitespace(consumeNewlines: true); + } } diff --git a/lib/src/parse/css.dart b/lib/src/parse/css.dart index 4b28dd3e1..93baaa19e 100644 --- a/lib/src/parse/css.dart +++ b/lib/src/parse/css.dart @@ -49,7 +49,7 @@ class CssParser extends ScssParser { var start = scanner.state; scanner.expectChar($at); var name = interpolatedIdentifier(); - whitespace(); + _whitespace(); return switch (name.asPlain) { "at-root" || @@ -113,7 +113,7 @@ class CssParser extends ScssParser { .text }; - whitespace(); + _whitespace(); var modifiers = tryImportModifiers(); expectStatementSeparator("@import rule"); return ImportRule( @@ -126,7 +126,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)); @@ -151,7 +151,7 @@ class CssParser extends ScssParser { var arguments = []; if (!scanner.scanChar($rparen)) { do { - whitespace(); + _whitespace(); if (allowEmptySecondArg && arguments.length == 1 && scanner.peekChar() == $rparen) { @@ -160,7 +160,7 @@ class CssParser extends ScssParser { } arguments.add(expressionUntilComma(singleEquals: true)); - whitespace(); + _whitespace(); } while (scanner.scanChar($comma)); scanner.expectChar($rparen); } @@ -180,4 +180,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); + } } diff --git a/lib/src/parse/keyframe_selector.dart b/lib/src/parse/keyframe_selector.dart index fb69ee638..227d3a4fd 100644 --- a/lib/src/parse/keyframe_selector.dart +++ b/lib/src/parse/keyframe_selector.dart @@ -15,7 +15,7 @@ class KeyframeSelectorParser extends Parser { return wrapSpanFormatException(() { var selectors = []; do { - whitespace(); + _whitespace(); if (lookingAtIdentifier()) { if (scanIdentifier("from")) { selectors.add("from"); @@ -26,7 +26,7 @@ class KeyframeSelectorParser extends Parser { } else { selectors.add(_percentage()); } - whitespace(); + _whitespace(); } while (scanner.scanChar($comma)); scanner.expectDone(); @@ -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); + } } diff --git a/lib/src/parse/media_query.dart b/lib/src/parse/media_query.dart index 89d854161..5101d4393 100644 --- a/lib/src/parse/media_query.dart +++ b/lib/src/parse/media_query.dart @@ -16,9 +16,9 @@ class MediaQueryParser extends Parser { return wrapSpanFormatException(() { var queries = []; do { - whitespace(); + _whitespace(); queries.add(_mediaQuery()); - whitespace(); + _whitespace(); } while (scanner.scanChar($comma)); scanner.expectDone(); return queries; @@ -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")) { @@ -57,7 +57,7 @@ class MediaQueryParser extends Parser { } } - whitespace(); + _whitespace(); if (!lookingAtIdentifier()) { // For example, "@media screen {" return CssMediaQuery.type(identifier1); @@ -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")) { @@ -102,7 +102,7 @@ class MediaQueryParser extends Parser { var result = []; while (true) { result.add(_mediaInParens()); - whitespace(); + _whitespace(); if (!scanIdentifier(operator)) return result; expectWhitespace(); @@ -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); + } } diff --git a/lib/src/parse/parser.dart b/lib/src/parse/parser.dart index 19ef3971c..e573187f6 100644 --- a/lib/src/parse/parser.dart +++ b/lib/src/parse/parser.dart @@ -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(); } @@ -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 @@ -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. @@ -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()); diff --git a/lib/src/parse/sass.dart b/lib/src/parse/sass.dart index 55b2086c9..406011445 100644 --- a/lib/src/parse/sass.dart +++ b/lib/src/parse/sass.dart @@ -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'}.", @@ -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. @@ -269,7 +272,7 @@ class SassParser extends StylesheetParser { _expectNewline(); } _readIndentation(); - whitespace(); + whitespace(consumeNewlines: false); } if (!scanner.isDone && !scanner.peekChar().isNewline) { @@ -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(); @@ -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."); } } @@ -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; + } } diff --git a/lib/src/parse/scss.dart b/lib/src/parse/scss.dart index d8b923104..0c238d292 100644 --- a/lib/src/parse/scss.dart +++ b/lib/src/parse/scss.dart @@ -20,7 +20,7 @@ class ScssParser extends StylesheetParser { Interpolation styleRuleSelector() => almostAnyValue(); void expectStatementSeparator([String? name]) { - whitespaceWithoutComments(); + _whitespaceWithoutComments(); if (scanner.isDone) return; if (scanner.peekChar() case $semicolon || $rbrace) return; scanner.expectChar($semicolon); @@ -38,7 +38,7 @@ class ScssParser extends StylesheetParser { bool scanElse(int ifIndentation) { var start = scanner.state; - whitespace(); + _whitespace(); var beforeAt = scanner.state; if (scanner.scanChar($at)) { if (scanIdentifier('else', caseSensitive: true)) return true; @@ -62,7 +62,7 @@ class ScssParser extends StylesheetParser { List children(Statement child()) { scanner.expectChar($lbrace); - whitespaceWithoutComments(); + _whitespaceWithoutComments(); var children = []; while (true) { switch (scanner.peekChar()) { @@ -73,17 +73,17 @@ class ScssParser extends StylesheetParser { switch (scanner.peekChar(1)) { case $slash: children.add(_silentComment()); - whitespaceWithoutComments(); + _whitespaceWithoutComments(); case $asterisk: children.add(_loudComment()); - whitespaceWithoutComments(); + _whitespaceWithoutComments(); default: children.add(child()); } case $semicolon: scanner.readChar(); - whitespaceWithoutComments(); + _whitespaceWithoutComments(); case $rbrace: scanner.expectChar($rbrace); @@ -97,7 +97,7 @@ class ScssParser extends StylesheetParser { List statements(Statement? statement()) { var statements = []; - whitespaceWithoutComments(); + _whitespaceWithoutComments(); while (!scanner.isDone) { switch (scanner.peekChar()) { case $dollar: @@ -107,17 +107,17 @@ class ScssParser extends StylesheetParser { switch (scanner.peekChar(1)) { case $slash: statements.add(_silentComment()); - whitespaceWithoutComments(); + _whitespaceWithoutComments(); case $asterisk: statements.add(_loudComment()); - whitespaceWithoutComments(); + _whitespaceWithoutComments(); default: if (statement() case var child?) statements.add(child); } case $semicolon: scanner.readChar(); - whitespaceWithoutComments(); + _whitespaceWithoutComments(); default: if (statement() case var child?) statements.add(child); @@ -183,4 +183,14 @@ class ScssParser extends StylesheetParser { } } } + + /// The value of `consumeNewlines` is not relevant for this class. + void _whitespace() { + whitespace(consumeNewlines: true); + } + + /// The value of `consumeNewlines` is not relevant for this class. + void _whitespaceWithoutComments() { + whitespaceWithoutComments(consumeNewlines: true); + } } diff --git a/lib/src/parse/selector.dart b/lib/src/parse/selector.dart index 595c0bba7..3d1e5e671 100644 --- a/lib/src/parse/selector.dart +++ b/lib/src/parse/selector.dart @@ -87,9 +87,9 @@ class SelectorParser extends Parser { var previousLine = scanner.line; var components = [_complexSelector()]; - whitespace(); + _whitespace(); while (scanner.scanChar($comma)) { - whitespace(); + _whitespace(); if (scanner.peekChar() == $comma) continue; if (scanner.isDone) break; @@ -117,7 +117,7 @@ class SelectorParser extends Parser { loop: while (true) { - whitespace(); + _whitespace(); switch (scanner.peekChar()) { case $plus: @@ -239,22 +239,23 @@ class SelectorParser extends Parser { AttributeSelector _attributeSelector() { var start = scanner.state; scanner.expectChar($lbracket); - whitespace(); + _whitespace(); var name = _attributeName(); - whitespace(); + + _whitespace(); if (scanner.scanChar($rbracket)) { return AttributeSelector(name, spanFrom(start)); } var operator = _attributeOperator(); - whitespace(); + _whitespace(); var next = scanner.peekChar(); var value = next == $single_quote || next == $double_quote ? string() : identifier(); - whitespace(); + _whitespace(); next = scanner.peekChar(); var modifier = next != null && next.isAlphabetic @@ -366,7 +367,7 @@ class SelectorParser extends Parser { if (!scanner.scanChar($lparen)) { return PseudoSelector(name, spanFrom(start), element: element); } - whitespace(); + _whitespace(); var unvendored = unvendor(name); String? argument; @@ -381,11 +382,11 @@ class SelectorParser extends Parser { selector = _selectorList(); } else if (unvendored == "nth-child" || unvendored == "nth-last-child") { argument = _aNPlusB(); - whitespace(); + _whitespace(); if (scanner.peekChar(-1).isWhitespace && scanner.peekChar() != $rparen) { expectIdentifier("of"); argument += " of"; - whitespace(); + _whitespace(); selector = _selectorList(); } @@ -421,18 +422,18 @@ class SelectorParser extends Parser { do { buffer.writeCharCode(scanner.readChar()); } while (scanner.peekChar().isDigit); - whitespace(); + _whitespace(); if (!scanIdentChar($n)) return buffer.toString(); } else { expectIdentChar($n); } buffer.writeCharCode($n); - whitespace(); + _whitespace(); var next = scanner.peekChar(); if (next != $plus && next != $minus) return buffer.toString(); buffer.writeCharCode(scanner.readChar()); - whitespace(); + _whitespace(); if (!scanner.peekChar().isDigit) scanner.error("Expected a number."); do { @@ -478,4 +479,9 @@ class SelectorParser extends Parser { $ampersand => _plainCss, _ => false }; + + /// The value of `consumeNewlines` is not relevant for this class. + void _whitespace() { + whitespace(consumeNewlines: true); + } } diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 1bfbca99e..8f3b6c96f 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -91,7 +91,7 @@ abstract class StylesheetParser extends Parser { // Handle this specially so that [atRule] always returns a non-nullable // Statement. if (scanner.scan('@charset')) { - whitespace(); + whitespace(consumeNewlines: false); string(); return null; } @@ -108,10 +108,10 @@ abstract class StylesheetParser extends Parser { ParameterList parseParameterList() => _parseSingleProduction(() { scanner.expectChar($at, name: "@-rule"); identifier(); - whitespace(); + whitespace(consumeNewlines: true); identifier(); var parameters = _parameterList(); - whitespace(); + whitespace(consumeNewlines: true); scanner.expectChar($lbrace); return parameters; }); @@ -136,7 +136,7 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; scanner.expectChar($at, name: "@-rule"); expectIdentifier("use"); - whitespace(); + whitespace(consumeNewlines: true); return _useRule(start); }), warnings @@ -188,7 +188,7 @@ abstract class StylesheetParser extends Parser { _isUseAllowed = false; var start = scanner.state; scanner.readChar(); - whitespace(); + whitespace(consumeNewlines: true); return _mixinRule(start); case $rbrace: @@ -228,9 +228,9 @@ abstract class StylesheetParser extends Parser { scanner.spanFrom(start)); } - whitespace(); + whitespace(consumeNewlines: true); scanner.expectChar($colon); - whitespace(); + whitespace(consumeNewlines: true); var value = _expression(); @@ -270,7 +270,7 @@ abstract class StylesheetParser extends Parser { error("Invalid flag name.", scanner.spanFrom(flagStart)); } - whitespace(); + whitespace(consumeNewlines: false); flagStart = scanner.state; } @@ -362,7 +362,7 @@ abstract class StylesheetParser extends Parser { if (_lookingAtPotentialPropertyHack()) { startsWithPunctuation = true; nameBuffer.writeCharCode(scanner.readChar()); - nameBuffer.write(rawText(whitespace)); + nameBuffer.write(rawText(() => whitespace(consumeNewlines: false))); } if (!_lookingAtInterpolatedIdentifier()) return nameBuffer; @@ -380,7 +380,7 @@ abstract class StylesheetParser extends Parser { if (scanner.matches("/*")) nameBuffer.write(rawText(loudComment)); var midBuffer = StringBuffer(); - midBuffer.write(rawText(whitespace)); + midBuffer.write(rawText(() => whitespace(consumeNewlines: false))); var beforeColon = scanner.state; if (!scanner.scanChar($colon)) { if (midBuffer.isNotEmpty) nameBuffer.writeCharCode($space); @@ -407,7 +407,7 @@ abstract class StylesheetParser extends Parser { return nameBuffer..write(midBuffer); } - var postColonWhitespace = rawText(whitespace); + var postColonWhitespace = rawText(() => whitespace(consumeNewlines: false)); if (_tryDeclarationChildren(name, start) case var nested?) return nested; midBuffer.write(postColonWhitespace); @@ -530,7 +530,7 @@ abstract class StylesheetParser extends Parser { if (_lookingAtPotentialPropertyHack()) { var nameBuffer = InterpolationBuffer(); nameBuffer.writeCharCode(scanner.readChar()); - nameBuffer.write(rawText(whitespace)); + nameBuffer.write(rawText(() => whitespace(consumeNewlines: false))); nameBuffer.addInterpolation(interpolatedIdentifier()); name = nameBuffer.interpolation(scanner.spanFrom(start)); } else if (!plainCss) { @@ -544,7 +544,7 @@ abstract class StylesheetParser extends Parser { name = interpolatedIdentifier(); } - whitespace(); + whitespace(consumeNewlines: false); scanner.expectChar($colon); if (parseCustomProperties && name.initialPlain.startsWith('--')) { @@ -554,7 +554,7 @@ abstract class StylesheetParser extends Parser { return Declaration(name, value, scanner.spanFrom(start)); } - whitespace(); + whitespace(consumeNewlines: false); if (_tryDeclarationChildren(name, start) case var nested?) return nested; var value = _expression(); @@ -608,7 +608,6 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; scanner.expectChar($at, name: "@-rule"); var name = interpolatedIdentifier(); - whitespace(); // We want to set [_isUseAllowed] to `false` *unless* we're parsing // `@charset`, `@forward`, or `@use`. To avoid double-comparing the rule @@ -733,7 +732,6 @@ abstract class StylesheetParser extends Parser { String _plainAtRuleName() { scanner.expectChar($at, name: "@-rule"); var name = identifier(); - whitespace(); return name; } @@ -741,9 +739,9 @@ abstract class StylesheetParser extends Parser { /// /// [start] should point before the `@`. AtRootRule _atRootRule(LineScannerState start) { + whitespace(consumeNewlines: false); if (scanner.peekChar() == $lparen) { var query = _atRootQuery(); - whitespace(); return _withChildren(_statement, start, (children, span) => AtRootRule(children, span, query: query)); } else if (lookingAtChildren() || (indented && atEndOfStatement())) { @@ -761,18 +759,18 @@ abstract class StylesheetParser extends Parser { var buffer = InterpolationBuffer(); scanner.expectChar($lparen); buffer.writeCharCode($lparen); - whitespace(); + whitespace(consumeNewlines: true); - _addOrInject(buffer, _expression()); + _addOrInject(buffer, _expression(consumeNewlines: true)); if (scanner.scanChar($colon)) { - whitespace(); + whitespace(consumeNewlines: true); buffer.writeCharCode($colon); buffer.writeCharCode($space); - _addOrInject(buffer, _expression()); + _addOrInject(buffer, _expression(consumeNewlines: true)); } scanner.expectChar($rparen); - whitespace(); + whitespace(consumeNewlines: false); buffer.writeCharCode($rparen); return buffer.interpolation(scanner.spanFrom(start)); @@ -788,11 +786,11 @@ abstract class StylesheetParser extends Parser { } var beforeWhitespace = scanner.location; - whitespace(); + whitespace(consumeNewlines: false); ArgumentList arguments; if (scanner.peekChar() == $lparen) { arguments = _argumentInvocation(mixin: true); - whitespace(); + whitespace(consumeNewlines: false); } else { arguments = ArgumentList.empty(beforeWhitespace.pointSpan()); } @@ -805,9 +803,11 @@ abstract class StylesheetParser extends Parser { /// /// [start] should point before the `@`. DebugRule _debugRule(LineScannerState start) { + whitespace(consumeNewlines: true); var value = _expression(); + var expressionEnd = scanner.state; expectStatementSeparator("@debug rule"); - return DebugRule(value, scanner.spanFrom(start)); + return DebugRule(value, scanner.spanFrom(start, expressionEnd)); } /// Consumes an `@each` rule. @@ -815,19 +815,20 @@ abstract class StylesheetParser extends Parser { /// [start] should point before the `@`. [child] is called to consume any /// children that are specifically allowed in the caller's context. EachRule _eachRule(LineScannerState start, Statement child()) { + whitespace(consumeNewlines: true); var wasInControlDirective = _inControlDirective; _inControlDirective = true; var variables = [variableName()]; - whitespace(); + whitespace(consumeNewlines: true); while (scanner.scanChar($comma)) { - whitespace(); + whitespace(consumeNewlines: true); variables.add(variableName()); - whitespace(); + whitespace(consumeNewlines: true); } - + whitespace(consumeNewlines: true); expectIdentifier("in"); - whitespace(); + whitespace(consumeNewlines: true); var list = _expression(); @@ -841,15 +842,18 @@ abstract class StylesheetParser extends Parser { /// /// [start] should point before the `@`. ErrorRule _errorRule(LineScannerState start) { + whitespace(consumeNewlines: true); var value = _expression(); + var expressionEnd = scanner.state; expectStatementSeparator("@error rule"); - return ErrorRule(value, scanner.spanFrom(start)); + return ErrorRule(value, scanner.spanFrom(start, expressionEnd)); } /// Consumes an `@extend` rule. /// /// [start] should point before the `@`. ExtendRule _extendRule(LineScannerState start) { + whitespace(consumeNewlines: true); if (!_inStyleRule && !_inMixin && !_inContentBlock) { error("@extend may only be used within style rules.", scanner.spanFrom(start)); @@ -859,7 +863,7 @@ abstract class StylesheetParser extends Parser { var optional = scanner.scanChar($exclamation); if (optional) { expectIdentifier("optional"); - whitespace(); + whitespace(consumeNewlines: false); } expectStatementSeparator("@extend rule"); return ExtendRule(value, scanner.spanFrom(start), optional: optional); @@ -869,6 +873,7 @@ abstract class StylesheetParser extends Parser { /// /// [start] should point before the `@`. FunctionRule _functionRule(LineScannerState start) { + whitespace(consumeNewlines: true); var precedingComment = lastSilentComment; lastSilentComment = null; var beforeName = scanner.state; @@ -886,7 +891,7 @@ abstract class StylesheetParser extends Parser { )); } - whitespace(); + whitespace(consumeNewlines: true); var parameters = _parameterList(); if (_inMixin || _inContentBlock) { @@ -909,7 +914,7 @@ abstract class StylesheetParser extends Parser { error("Invalid function name.", scanner.spanFrom(start)); } - whitespace(); + whitespace(consumeNewlines: false); return _withChildren( _functionChild, start, @@ -922,30 +927,33 @@ abstract class StylesheetParser extends Parser { /// [start] should point before the `@`. [child] is called to consume any /// children that are specifically allowed in the caller's context. ForRule _forRule(LineScannerState start, Statement child()) { + whitespace(consumeNewlines: true); var wasInControlDirective = _inControlDirective; _inControlDirective = true; var variable = variableName(); - whitespace(); + whitespace(consumeNewlines: true); expectIdentifier("from"); - whitespace(); + whitespace(consumeNewlines: true); bool? exclusive; - var from = _expression(until: () { - if (!lookingAtIdentifier()) return false; - if (scanIdentifier("to")) { - exclusive = true; - return true; - } else if (scanIdentifier("through")) { - exclusive = false; - return true; - } else { - return false; - } - }); + var from = _expression( + consumeNewlines: true, + until: () { + if (!lookingAtIdentifier()) return false; + if (scanIdentifier("to")) { + exclusive = true; + return true; + } else if (scanIdentifier("through")) { + exclusive = false; + return true; + } else { + return false; + } + }); if (exclusive == null) scanner.error('Expected "to" or "through".'); - whitespace(); + whitespace(consumeNewlines: true); var to = _expression(); return _withChildren(child, start, (children, span) { @@ -959,15 +967,16 @@ abstract class StylesheetParser extends Parser { /// /// [start] should point before the `@`. ForwardRule _forwardRule(LineScannerState start) { + whitespace(consumeNewlines: true); var url = _urlString(); - whitespace(); + whitespace(consumeNewlines: false); String? prefix; if (scanIdentifier("as")) { - whitespace(); + whitespace(consumeNewlines: true); prefix = identifier(normalize: true); scanner.expectChar($asterisk); - whitespace(); + whitespace(consumeNewlines: false); } Set? shownMixinsAndFunctions; @@ -975,13 +984,15 @@ abstract class StylesheetParser extends Parser { Set? hiddenMixinsAndFunctions; Set? hiddenVariables; if (scanIdentifier("show")) { + whitespace(consumeNewlines: true); (shownMixinsAndFunctions, shownVariables) = _memberList(); } else if (scanIdentifier("hide")) { + whitespace(consumeNewlines: true); (hiddenMixinsAndFunctions, hiddenVariables) = _memberList(); } var configuration = _configuration(allowGuarded: true); - whitespace(); + whitespace(consumeNewlines: false); expectStatementSeparator("@forward rule"); var span = scanner.spanFrom(start); @@ -1012,7 +1023,7 @@ abstract class StylesheetParser extends Parser { var identifiers = {}; var variables = {}; do { - whitespace(); + whitespace(consumeNewlines: true); withErrorMessage("Expected variable, mixin, or function name", () { if (scanner.peekChar() == $dollar) { variables.add(variableName()); @@ -1020,7 +1031,7 @@ abstract class StylesheetParser extends Parser { identifiers.add(identifier(normalize: true)); } }); - whitespace(); + whitespace(consumeNewlines: false); } while (scanner.scanChar($comma)); return (identifiers, variables); @@ -1031,20 +1042,21 @@ abstract class StylesheetParser extends Parser { /// [start] should point before the `@`. [child] is called to consume any /// children that are specifically allowed in the caller's context. IfRule _ifRule(LineScannerState start, Statement child()) { + whitespace(consumeNewlines: true); var ifIndentation = currentIndentation; var wasInControlDirective = _inControlDirective; _inControlDirective = true; var condition = _expression(); var children = this.children(child); - whitespaceWithoutComments(); + whitespaceWithoutComments(consumeNewlines: false); var clauses = [IfClause(condition, children)]; ElseClause? lastClause; while (scanElse(ifIndentation)) { - whitespace(); + whitespace(consumeNewlines: false); if (scanIdentifier("if")) { - whitespace(); + whitespace(consumeNewlines: true); clauses.add(IfClause(_expression(), this.children(child))); } else { lastClause = ElseClause(this.children(child)); @@ -1054,7 +1066,7 @@ abstract class StylesheetParser extends Parser { _inControlDirective = wasInControlDirective; var span = scanner.spanFrom(start); - whitespaceWithoutComments(); + whitespaceWithoutComments(consumeNewlines: false); return IfRule(clauses, span, lastClause: lastClause); } @@ -1064,7 +1076,7 @@ abstract class StylesheetParser extends Parser { ImportRule _importRule(LineScannerState start) { var imports = []; do { - whitespace(); + whitespace(consumeNewlines: false); var argument = importArgument(); if (argument is DynamicImport) { warnings.add(( @@ -1082,7 +1094,7 @@ abstract class StylesheetParser extends Parser { } imports.add(argument); - whitespace(); + whitespace(consumeNewlines: false); } while (scanner.scanChar($comma)); expectStatementSeparator("@import rule"); @@ -1097,7 +1109,7 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; if (scanner.peekChar() case $u || $U) { var url = dynamicUrl(); - whitespace(); + whitespace(consumeNewlines: false); var modifiers = tryImportModifiers(); return StaticImport( url is StringExpression @@ -1109,7 +1121,7 @@ abstract class StylesheetParser extends Parser { var url = string(); var urlSpan = scanner.spanFrom(start); - whitespace(); + whitespace(consumeNewlines: false); var modifiers = tryImportModifiers(); if (isPlainImportUrl(url) || modifiers != null) { return StaticImport( @@ -1181,14 +1193,13 @@ abstract class StylesheetParser extends Parser { } else { buffer.writeCharCode($lparen); buffer.addInterpolation(_interpolatedDeclarationValue( - allowEmpty: true, allowSemicolon: true)); + allowEmpty: true, allowSemicolon: true, consumeNewlines: true)); buffer.writeCharCode($rparen); } - scanner.expectChar($rparen); - whitespace(); + whitespace(consumeNewlines: false); } else { - whitespace(); + whitespace(consumeNewlines: false); if (scanner.scanChar($comma)) { buffer.write(", "); buffer.addInterpolation(_mediaQueryList()); @@ -1208,18 +1219,19 @@ abstract class StylesheetParser extends Parser { /// Consumes the contents of a `supports()` function after an `@import` rule /// (but not the function name or parentheses). SupportsCondition _importSupportsQuery() { + whitespace(consumeNewlines: true); if (scanIdentifier("not")) { - whitespace(); + whitespace(consumeNewlines: true); var start = scanner.state; return SupportsNegation( _supportsConditionInParens(), scanner.spanFrom(start)); } else if (scanner.peekChar() == $lparen) { - return _supportsCondition(); + return _supportsCondition(inParentheses: true); } else { if (_tryImportSupportsFunction() case var function?) return function; var start = scanner.state; - var name = _expression(); + var name = _expression(consumeNewlines: true); scanner.expectChar($colon); return SupportsDeclaration( name, _supportsDeclarationValue(name), scanner.spanFrom(start)); @@ -1240,8 +1252,8 @@ abstract class StylesheetParser extends Parser { return null; } - var value = - _interpolatedDeclarationValue(allowEmpty: true, allowSemicolon: true); + var value = _interpolatedDeclarationValue( + allowEmpty: true, allowSemicolon: true, consumeNewlines: true); scanner.expectChar($rparen); return SupportsFunction(name, value, scanner.spanFrom(start)); @@ -1251,6 +1263,7 @@ abstract class StylesheetParser extends Parser { /// /// [start] should point before the `@`. IncludeRule _includeRule(LineScannerState start) { + whitespace(consumeNewlines: true); String? namespace; var name = identifier(); if (scanner.scanChar($dot)) { @@ -1258,17 +1271,17 @@ abstract class StylesheetParser extends Parser { name = _publicIdentifier(); } - whitespace(); + whitespace(consumeNewlines: false); var arguments = scanner.peekChar() == $lparen ? _argumentInvocation(mixin: true) : ArgumentList.empty(scanner.emptySpan); - whitespace(); + whitespace(consumeNewlines: false); ParameterList? contentParameters; if (scanIdentifier("using")) { - whitespace(); + whitespace(consumeNewlines: true); contentParameters = _parameterList(); - whitespace(); + whitespace(consumeNewlines: false); } ContentBlock? content; @@ -1295,6 +1308,7 @@ abstract class StylesheetParser extends Parser { /// [start] should point before the `@`. @protected MediaRule mediaRule(LineScannerState start) { + whitespace(consumeNewlines: false); var query = _mediaQueryList(); return _withChildren(_statement, start, (children, span) => MediaRule(query, children, span)); @@ -1304,6 +1318,7 @@ abstract class StylesheetParser extends Parser { /// /// [start] should point before the `@`. MixinRule _mixinRule(LineScannerState start) { + whitespace(consumeNewlines: true); var precedingComment = lastSilentComment; lastSilentComment = null; var beforeName = scanner.state; @@ -1321,7 +1336,7 @@ abstract class StylesheetParser extends Parser { )); } - whitespace(); + whitespace(consumeNewlines: false); var parameters = scanner.peekChar() == $lparen ? _parameterList() : ParameterList.empty(scanner.emptySpan); @@ -1334,7 +1349,7 @@ abstract class StylesheetParser extends Parser { scanner.spanFrom(start)); } - whitespace(); + whitespace(consumeNewlines: false); _inMixin = true; return _withChildren(_statement, start, (children, span) { @@ -1353,6 +1368,7 @@ abstract class StylesheetParser extends Parser { /// [the specification]: http://www.w3.org/TR/css3-conditional/ @protected AtRule mozDocumentRule(LineScannerState start, Interpolation name) { + whitespace(consumeNewlines: false); var valueStart = scanner.state; var buffer = InterpolationBuffer(); var needsDeprecationWarning = false; @@ -1371,7 +1387,7 @@ abstract class StylesheetParser extends Parser { buffer.addInterpolation(contents); } else { scanner.expectChar($lparen); - whitespace(); + whitespace(consumeNewlines: false); var argument = interpolatedString(); scanner.expectChar($rparen); @@ -1404,11 +1420,11 @@ abstract class StylesheetParser extends Parser { } } - whitespace(); + whitespace(consumeNewlines: false); if (!scanner.scanChar($comma)) break; buffer.writeCharCode($comma); - buffer.write(rawText(whitespace)); + buffer.write(rawText(() => whitespace(consumeNewlines: false))); } var value = buffer.interpolation(scanner.spanFrom(valueStart)); @@ -1433,6 +1449,7 @@ abstract class StylesheetParser extends Parser { /// /// [start] should point before the `@`. ReturnRule _returnRule(LineScannerState start) { + whitespace(consumeNewlines: true); var value = _expression(); expectStatementSeparator("@return rule"); return ReturnRule(value, scanner.spanFrom(start)); @@ -1443,8 +1460,9 @@ abstract class StylesheetParser extends Parser { /// [start] should point before the `@`. @protected SupportsRule supportsRule(LineScannerState start) { + whitespace(consumeNewlines: false); var condition = _supportsCondition(); - whitespace(); + whitespace(consumeNewlines: false); return _withChildren(_statement, start, (children, span) => SupportsRule(condition, children, span)); } @@ -1453,13 +1471,14 @@ abstract class StylesheetParser extends Parser { /// /// [start] should point before the `@`. UseRule _useRule(LineScannerState start) { + whitespace(consumeNewlines: true); var url = _urlString(); - whitespace(); + whitespace(consumeNewlines: false); var namespace = _useNamespace(url, start); - whitespace(); + whitespace(consumeNewlines: false); var configuration = _configuration(); - whitespace(); + whitespace(consumeNewlines: false); var span = scanner.spanFrom(start); if (!_isUseAllowed) { @@ -1476,7 +1495,7 @@ abstract class StylesheetParser extends Parser { /// Returns `null` to indicate a `@use` rule without a URL. String? _useNamespace(Uri url, LineScannerState start) { if (scanIdentifier("as")) { - whitespace(); + whitespace(consumeNewlines: true); return scanner.scanChar($asterisk) ? null : identifier(); } @@ -1507,25 +1526,26 @@ abstract class StylesheetParser extends Parser { var variableNames = {}; var configuration = []; - whitespace(); + whitespace(consumeNewlines: true); scanner.expectChar($lparen); while (true) { - whitespace(); + whitespace(consumeNewlines: true); var variableStart = scanner.state; var name = variableName(); - whitespace(); + whitespace(consumeNewlines: true); scanner.expectChar($colon); - whitespace(); - var expression = expressionUntilComma(); + whitespace(consumeNewlines: true); + + var expression = expressionUntilComma(consumeNewlines: true); var guarded = false; var flagStart = scanner.state; if (allowGuarded && scanner.scanChar($exclamation)) { if (identifier() == 'default') { guarded = true; - whitespace(); + whitespace(consumeNewlines: true); } else { error("Invalid flag name.", scanner.spanFrom(flagStart)); } @@ -1540,7 +1560,7 @@ abstract class StylesheetParser extends Parser { .add(ConfiguredVariable(name, expression, span, guarded: guarded)); if (!scanner.scanChar($comma)) break; - whitespace(); + whitespace(consumeNewlines: true); if (!_lookingAtExpression()) break; } @@ -1552,9 +1572,11 @@ abstract class StylesheetParser extends Parser { /// /// [start] should point before the `@`. WarnRule _warnRule(LineScannerState start) { + whitespace(consumeNewlines: true); var value = _expression(); + var expressionEnd = scanner.state; expectStatementSeparator("@warn rule"); - return WarnRule(value, scanner.spanFrom(start)); + return WarnRule(value, scanner.spanFrom(start, expressionEnd)); } /// Consumes a `@while` rule. @@ -1562,6 +1584,7 @@ abstract class StylesheetParser extends Parser { /// [start] should point before the `@`. [child] is called to consume any /// children that are specifically allowed in the caller's context. WhileRule _whileRule(LineScannerState start, Statement child()) { + whitespace(consumeNewlines: true); var wasInControlDirective = _inControlDirective; _inControlDirective = true; var condition = _expression(); @@ -1579,6 +1602,8 @@ abstract class StylesheetParser extends Parser { var wasInUnknownAtRule = _inUnknownAtRule; _inUnknownAtRule = true; + whitespace(consumeNewlines: false); + Interpolation? value; if (scanner.peekChar() != $exclamation && !atEndOfStatement()) { value = _interpolatedDeclarationValue(allowOpenBrace: false); @@ -1606,6 +1631,7 @@ abstract class StylesheetParser extends Parser { /// This declares a return type of [Statement] so that it can be returned /// within case statements. Statement _disallowedAtRule(LineScannerState start) { + whitespace(consumeNewlines: false); _interpolatedDeclarationValue(allowEmpty: true, allowOpenBrace: false); error("This at-rule is not allowed here.", scanner.spanFrom(start)); } @@ -1614,24 +1640,24 @@ abstract class StylesheetParser extends Parser { ParameterList _parameterList() { var start = scanner.state; scanner.expectChar($lparen); - whitespace(); + whitespace(consumeNewlines: true); var parameters = []; var named = {}; String? restParameter; while (scanner.peekChar() == $dollar) { var variableStart = scanner.state; var name = variableName(); - whitespace(); + whitespace(consumeNewlines: true); Expression? defaultValue; if (scanner.scanChar($colon)) { - whitespace(); + whitespace(consumeNewlines: true); defaultValue = expressionUntilComma(); } else if (scanner.scanChar($dot)) { scanner.expectChar($dot); scanner.expectChar($dot); - whitespace(); - if (scanner.scanChar($comma)) whitespace(); + whitespace(consumeNewlines: true); + if (scanner.scanChar($comma)) whitespace(consumeNewlines: true); restParameter = name; break; } @@ -1643,7 +1669,7 @@ abstract class StylesheetParser extends Parser { } if (!scanner.scanChar($comma)) break; - whitespace(); + whitespace(consumeNewlines: true); } scanner.expectChar($rparen); return ParameterList(parameters, scanner.spanFrom(start), @@ -1665,7 +1691,7 @@ abstract class StylesheetParser extends Parser { {bool mixin = false, bool allowEmptySecondArg = false}) { var start = scanner.state; scanner.expectChar($lparen); - whitespace(); + whitespace(consumeNewlines: true); var positional = []; var named = {}; @@ -1673,10 +1699,10 @@ abstract class StylesheetParser extends Parser { Expression? keywordRest; while (_lookingAtExpression()) { var expression = expressionUntilComma(singleEquals: !mixin); - whitespace(); + whitespace(consumeNewlines: true); if (expression is VariableExpression && scanner.scanChar($colon)) { - whitespace(); + whitespace(consumeNewlines: true); if (named.containsKey(expression.name)) { error("Duplicate argument.", expression.span); } @@ -1688,8 +1714,8 @@ abstract class StylesheetParser extends Parser { rest = expression; } else { keywordRest = expression; - whitespace(); - if (scanner.scanChar($comma)) whitespace(); + whitespace(consumeNewlines: true); + if (scanner.scanChar($comma)) whitespace(consumeNewlines: true); break; } } else if (named.isNotEmpty) { @@ -1699,9 +1725,9 @@ abstract class StylesheetParser extends Parser { positional.add(expression); } - whitespace(); + whitespace(consumeNewlines: true); if (!scanner.scanChar($comma)) break; - whitespace(); + whitespace(consumeNewlines: true); if (allowEmptySecondArg && positional.length == 1 && @@ -1729,16 +1755,23 @@ abstract class StylesheetParser extends Parser { /// If [until] is passed, it's called each time the expression could end and /// still be a valid expression. When it returns `true`, this returns the /// expression. + /// + /// 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 Expression _expression( - {bool bracketList = false, bool singleEquals = false, bool until()?}) { + {bool bracketList = false, + bool singleEquals = false, + bool consumeNewlines = false, + bool until()?}) { if (until != null && until()) scanner.error("Expected expression."); LineScannerState? beforeBracket; if (bracketList) { beforeBracket = scanner.state; scanner.expectChar($lbracket); - whitespace(); + whitespace(consumeNewlines: true); if (scanner.scanChar($rbracket)) { return ListExpression( @@ -1914,8 +1947,7 @@ abstract class StylesheetParser extends Parser { length: operator.operator.length); } operands.add(singleExpression); - - whitespace(); + whitespace(consumeNewlines: true); singleExpression_ = _singleExpression(); } @@ -1936,7 +1968,7 @@ abstract class StylesheetParser extends Parser { loop: while (true) { - whitespace(); + whitespace(consumeNewlines: consumeNewlines || bracketList); if (until != null && until()) break; switch (scanner.peekChar()) { @@ -2130,8 +2162,16 @@ abstract class StylesheetParser extends Parser { /// /// If [singleEquals] is true, this will allow the Microsoft-style `=` /// operator at the top level. - Expression expressionUntilComma({bool singleEquals = false}) => _expression( - singleEquals: singleEquals, until: () => scanner.peekChar() == $comma); + /// + /// 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. + Expression expressionUntilComma( + {bool singleEquals = false, bool consumeNewlines = false}) => + _expression( + singleEquals: singleEquals, + consumeNewlines: consumeNewlines, + until: () => scanner.peekChar() == $comma); /// Whether [expression] is allowed as an operand of a `/` expression that /// produces a potentially slash-separated number. @@ -2180,16 +2220,16 @@ abstract class StylesheetParser extends Parser { try { var start = scanner.state; scanner.expectChar($lparen); - whitespace(); + whitespace(consumeNewlines: true); if (!_lookingAtExpression()) { scanner.expectChar($rparen); return ListExpression( [], ListSeparator.undecided, scanner.spanFrom(start)); } - var first = expressionUntilComma(); + var first = expressionUntilComma(consumeNewlines: true); if (scanner.scanChar($colon)) { - whitespace(); + whitespace(consumeNewlines: true); return _map(first, start); } @@ -2197,14 +2237,14 @@ abstract class StylesheetParser extends Parser { scanner.expectChar($rparen); return ParenthesizedExpression(first, scanner.spanFrom(start)); } - whitespace(); + whitespace(consumeNewlines: true); var expressions = [first]; while (true) { if (!_lookingAtExpression()) break; - expressions.add(expressionUntilComma()); + expressions.add(expressionUntilComma(consumeNewlines: true)); if (!scanner.scanChar($comma)) break; - whitespace(); + whitespace(consumeNewlines: true); } scanner.expectChar($rparen); @@ -2221,16 +2261,16 @@ abstract class StylesheetParser extends Parser { /// as the expression before the colon and [start] the point before the /// opening parenthesis. MapExpression _map(Expression first, LineScannerState start) { - var pairs = [(first, expressionUntilComma())]; + var pairs = [(first, expressionUntilComma(consumeNewlines: true))]; while (scanner.scanChar($comma)) { - whitespace(); + whitespace(consumeNewlines: true); if (!_lookingAtExpression()) break; - var key = expressionUntilComma(); + var key = expressionUntilComma(consumeNewlines: true); scanner.expectChar($colon); - whitespace(); - var value = expressionUntilComma(); + whitespace(consumeNewlines: true); + var value = expressionUntilComma(consumeNewlines: true); pairs.add((key, value)); } @@ -2344,7 +2384,7 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; scanner.readChar(); - whitespace(); + whitespace(consumeNewlines: true); expectIdentifier("important"); return StringExpression.plain("!important", scanner.spanFrom(start)); } @@ -2360,7 +2400,7 @@ abstract class StylesheetParser extends Parser { position: scanner.position - 1, length: 1); } - whitespace(); + whitespace(consumeNewlines: true); var operand = _singleExpression(); return UnaryOperationExpression(operator, operand, scanner.spanFrom(start)); } @@ -2595,7 +2635,7 @@ abstract class StylesheetParser extends Parser { return IfExpression( invocation, identifier.span.expand(invocation.span)); } else if (plain == "not") { - whitespace(); + whitespace(consumeNewlines: true); var expression = _singleExpression(); return UnaryOperationExpression(UnaryOperator.not, expression, identifier.span.expand(expression.span)); @@ -2720,7 +2760,7 @@ abstract class StylesheetParser extends Parser { var beginningOfContents = scanner.state; if (!scanner.scanChar($lparen)) return null; - whitespaceWithoutComments(); + whitespaceWithoutComments(consumeNewlines: true); // Match Ruby Sass's behavior: parse a raw URL() if possible, and if not // backtrack and re-parse as a function expression. @@ -2747,7 +2787,7 @@ abstract class StylesheetParser extends Parser { >= 0x80: buffer.writeCharCode(scanner.readChar()); case int(isWhitespace: true): - whitespaceWithoutComments(); + whitespaceWithoutComments(consumeNewlines: true); if (scanner.peekChar() != $rparen) break loop; case $rparen: buffer.writeCharCode(scanner.readChar()); @@ -2797,6 +2837,8 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; var buffer = InterpolationBuffer(); + var brackets = []; + loop: while (true) { switch (scanner.peekChar()) { @@ -2832,7 +2874,7 @@ abstract class StylesheetParser extends Parser { buffer.addInterpolation(interpolatedIdentifier()); case $cr || $lf || $ff: - if (indented) break loop; + if (indented && brackets.isEmpty) break loop; buffer.writeCharCode(scanner.readChar()); case $exclamation || $semicolon || $lbrace || $rbrace: @@ -2857,6 +2899,19 @@ abstract class StylesheetParser extends Parser { buffer.writeCharCode(scanner.readChar()); } + case $lparen || $lbracket: + var bracket = scanner.readChar(); + buffer.writeCharCode(bracket); + brackets.add(opposite(bracket)); + + case ($rparen || $rbracket) && var char?: + if (brackets.isEmpty) { + scanner.error('Unexpected "${String.fromCharCode(char)}".'); + } + var bracket = brackets.removeLast(); + scanner.expectChar(bracket); + buffer.writeCharCode(bracket); + case null: break loop; @@ -2887,13 +2942,18 @@ abstract class StylesheetParser extends Parser { /// comments. Otherwise, it will preserve two adjacent slashes and emit them /// to CSS. /// + /// 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. + /// /// Unlike [declarationValue], this allows interpolation. Interpolation _interpolatedDeclarationValue( {bool allowEmpty = false, bool allowSemicolon = false, bool allowColon = true, bool allowOpenBrace = true, - bool silentComments = true}) { + bool silentComments = true, + bool consumeNewlines = false}) { // NOTE: this logic is largely duplicated in Parser.declarationValue. Most // changes here should be mirrored there. @@ -2943,7 +3003,8 @@ abstract class StylesheetParser extends Parser { case $space || $tab: buffer.writeCharCode(scanner.readChar()); - case $lf || $cr || $ff when indented: + case $lf || $cr || $ff + when indented && !consumeNewlines && brackets.isEmpty: break loop; case $lf || $cr || $ff: @@ -3078,8 +3139,8 @@ abstract class StylesheetParser extends Parser { (Expression, FileSpan span) singleInterpolation() { var start = scanner.state; scanner.expect('#{'); - whitespace(); - var contents = _expression(); + whitespace(consumeNewlines: true); + var contents = _expression(consumeNewlines: true); scanner.expectChar($rbrace); var span = scanner.spanFrom(start); @@ -3097,9 +3158,9 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; var buffer = InterpolationBuffer(); while (true) { - whitespace(); + whitespace(consumeNewlines: false); _mediaQuery(buffer); - whitespace(); + whitespace(consumeNewlines: false); if (!scanner.scanChar($comma)) break; buffer.writeCharCode($comma); buffer.writeCharCode($space); @@ -3112,7 +3173,7 @@ abstract class StylesheetParser extends Parser { // This is somewhat duplicated in MediaQueryParser._mediaQuery. if (scanner.peekChar() == $lparen) { _mediaInParens(buffer); - whitespace(); + whitespace(consumeNewlines: false); if (scanIdentifier("and")) { buffer.write(" and "); expectWhitespace(); @@ -3138,7 +3199,7 @@ abstract class StylesheetParser extends Parser { } } - whitespace(); + whitespace(consumeNewlines: false); buffer.addInterpolation(identifier1); if (!_lookingAtInterpolatedIdentifier()) { // For example, "@media screen {". @@ -3153,7 +3214,7 @@ abstract class StylesheetParser extends Parser { // For example, "@media screen and ..." buffer.write(" and "); } else { - whitespace(); + whitespace(consumeNewlines: false); buffer.addInterpolation(identifier2); if (scanIdentifier("and")) { // For example, "@media only screen and ..." @@ -3185,10 +3246,10 @@ abstract class StylesheetParser extends Parser { void _mediaLogicSequence(InterpolationBuffer buffer, String operator) { while (true) { _mediaOrInterp(buffer); - whitespace(); + whitespace(consumeNewlines: false); if (!scanIdentifier(operator)) return; - expectWhitespace(); + expectWhitespace(consumeNewlines: false); buffer.writeCharCode($space); buffer.write(operator); @@ -3210,32 +3271,32 @@ abstract class StylesheetParser extends Parser { void _mediaInParens(InterpolationBuffer buffer) { scanner.expectChar($lparen, name: "media condition in parentheses"); buffer.writeCharCode($lparen); - whitespace(); + whitespace(consumeNewlines: true); if (scanner.peekChar() == $lparen) { _mediaInParens(buffer); - whitespace(); + whitespace(consumeNewlines: true); if (scanIdentifier("and")) { buffer.write(" and "); - expectWhitespace(); + expectWhitespace(consumeNewlines: true); _mediaLogicSequence(buffer, "and"); } else if (scanIdentifier("or")) { buffer.write(" or "); - expectWhitespace(); + expectWhitespace(consumeNewlines: true); _mediaLogicSequence(buffer, "or"); } } else if (scanIdentifier("not")) { buffer.write("not "); - expectWhitespace(); + expectWhitespace(consumeNewlines: true); _mediaOrInterp(buffer); } else { var expressionBefore = _expressionUntilComparison(); buffer.add(expressionBefore, expressionBefore.span); if (scanner.scanChar($colon)) { - whitespace(); + whitespace(consumeNewlines: true); buffer.writeCharCode($colon); buffer.writeCharCode($space); - var expressionAfter = _expression(); + var expressionAfter = _expression(consumeNewlines: true); buffer.add(expressionAfter, expressionAfter.span); } else { var next = scanner.peekChar(); @@ -3247,7 +3308,7 @@ abstract class StylesheetParser extends Parser { } buffer.writeCharCode($space); - whitespace(); + whitespace(consumeNewlines: true); var expressionMiddle = _expressionUntilComparison(); buffer.add(expressionMiddle, expressionMiddle.span); @@ -3258,7 +3319,7 @@ abstract class StylesheetParser extends Parser { if (scanner.scanChar($equal)) buffer.writeCharCode($equal); buffer.writeCharCode($space); - whitespace(); + whitespace(consumeNewlines: true); var expressionAfter = _expressionUntilComparison(); buffer.add(expressionAfter, expressionAfter.span); } @@ -3267,13 +3328,14 @@ abstract class StylesheetParser extends Parser { } scanner.expectChar($rparen); - whitespace(); + whitespace(consumeNewlines: false); buffer.writeCharCode($rparen); } /// Consumes an expression until it reaches a top-level `<`, `>`, or a `=` /// that's not `==`. Expression _expressionUntilComparison() => _expression( + consumeNewlines: true, until: () => switch (scanner.peekChar()) { $equal => scanner.peekChar(1) != $equal, $langle || $rangle => true, @@ -3283,16 +3345,19 @@ abstract class StylesheetParser extends Parser { // ## Supports Conditions /// Consumes a `@supports` condition. - SupportsCondition _supportsCondition() { + /// + /// If [inParentheses] is true, the indented syntax will consume newlines + /// where a statement otherwise would end. + SupportsCondition _supportsCondition({bool inParentheses = false}) { var start = scanner.state; if (scanIdentifier("not")) { - whitespace(); + whitespace(consumeNewlines: inParentheses); return SupportsNegation( _supportsConditionInParens(), scanner.spanFrom(start)); } var condition = _supportsConditionInParens(); - whitespace(); + whitespace(consumeNewlines: inParentheses); String? operator; while (lookingAtIdentifier()) { if (operator != null) { @@ -3304,11 +3369,11 @@ abstract class StylesheetParser extends Parser { operator = "and"; } - whitespace(); + whitespace(consumeNewlines: inParentheses); var right = _supportsConditionInParens(); condition = SupportsOperation( condition, right, operator, scanner.spanFrom(start)); - whitespace(); + whitespace(consumeNewlines: inParentheses); } return condition; } @@ -3325,7 +3390,7 @@ abstract class StylesheetParser extends Parser { if (scanner.scanChar($lparen)) { var arguments = _interpolatedDeclarationValue( - allowEmpty: true, allowSemicolon: true); + allowEmpty: true, allowSemicolon: true, consumeNewlines: true); scanner.expectChar($rparen); return SupportsFunction(identifier, arguments, scanner.spanFrom(start)); } else if (identifier.contents case [Expression expression]) { @@ -3336,14 +3401,14 @@ abstract class StylesheetParser extends Parser { } scanner.expectChar($lparen); - whitespace(); + whitespace(consumeNewlines: true); if (scanIdentifier("not")) { - whitespace(); + whitespace(consumeNewlines: true); var condition = _supportsConditionInParens(); scanner.expectChar($rparen); return SupportsNegation(condition, scanner.spanFrom(start)); } else if (scanner.peekChar() == $lparen) { - var condition = _supportsCondition(); + var condition = _supportsCondition(inParentheses: true); scanner.expectChar($rparen); return condition.withSpan(scanner.spanFrom(start)); } @@ -3366,7 +3431,7 @@ abstract class StylesheetParser extends Parser { var nameStart = scanner.state; var wasInParentheses = _inParentheses; try { - name = _expression(); + name = _expression(consumeNewlines: true); scanner.expectChar($colon); } on FormatException catch (_) { scanner.state = nameStart; @@ -3385,7 +3450,10 @@ abstract class StylesheetParser extends Parser { var contents = (InterpolationBuffer() ..addInterpolation(identifier) ..addInterpolation(_interpolatedDeclarationValue( - allowEmpty: true, allowSemicolon: true, allowColon: false))) + allowEmpty: true, + allowSemicolon: true, + allowColon: false, + consumeNewlines: true))) .interpolation(scanner.spanFrom(nameStart)); if (scanner.peekChar() == $colon) rethrow; @@ -3405,8 +3473,8 @@ abstract class StylesheetParser extends Parser { when text.initialPlain.startsWith("--")) { return StringExpression(_interpolatedDeclarationValue()); } else { - whitespace(); - return _expression(); + whitespace(consumeNewlines: true); + return _expression(consumeNewlines: true); } } @@ -3420,7 +3488,7 @@ abstract class StylesheetParser extends Parser { if (expression is! Expression) return null; var beforeWhitespace = scanner.state; - whitespace(); + whitespace(consumeNewlines: true); SupportsOperation? operation; String? operator; @@ -3436,14 +3504,14 @@ abstract class StylesheetParser extends Parser { return null; } - whitespace(); + whitespace(consumeNewlines: true); var right = _supportsConditionInParens(); operation = SupportsOperation( operation ?? SupportsInterpolation(expression, interpolation.span), right, operator, scanner.spanFrom(start)); - whitespace(); + whitespace(consumeNewlines: true); } return operation; @@ -3525,7 +3593,7 @@ abstract class StylesheetParser extends Parser { T _withChildren(Statement child(), LineScannerState start, T create(List children, FileSpan span)) { var result = create(children(child), scanner.spanFrom(start)); - whitespaceWithoutComments(); + whitespaceWithoutComments(consumeNewlines: false); return result; } diff --git a/pubspec.yaml b/pubspec.yaml index 9af9300f6..8bbf566bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.83.1 +version: 1.84.0-dev description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass