diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dfef18ca..10d6eb63a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.29.0 + +* Support a broader syntax for `@supports` conditions, based on the latest + [Editor's Draft of CSS Conditional Rules 3]. Almost all syntax will be allowed + (with interpolation) in the conditions' parentheses, as well as function + syntax such as `@supports selector(...)`. + +[Editor's Draft of CSS Conditional Rules 3]: https://drafts.csswg.org/css-conditional-3/#at-supports + ## 1.28.0 * Add a [`color.hwb()`] function to `sass:color` that can express colors in [HWB] format. diff --git a/lib/src/ast/sass.dart b/lib/src/ast/sass.dart index 93f373b4e..e463a5da7 100644 --- a/lib/src/ast/sass.dart +++ b/lib/src/ast/sass.dart @@ -60,7 +60,9 @@ export 'sass/statement/variable_declaration.dart'; export 'sass/statement/warn_rule.dart'; export 'sass/statement/while_rule.dart'; export 'sass/supports_condition.dart'; +export 'sass/supports_condition/anything.dart'; export 'sass/supports_condition/declaration.dart'; +export 'sass/supports_condition/function.dart'; export 'sass/supports_condition/interpolation.dart'; export 'sass/supports_condition/negation.dart'; export 'sass/supports_condition/operation.dart'; diff --git a/lib/src/ast/sass/supports_condition/anything.dart b/lib/src/ast/sass/supports_condition/anything.dart new file mode 100644 index 000000000..11e7a4752 --- /dev/null +++ b/lib/src/ast/sass/supports_condition/anything.dart @@ -0,0 +1,21 @@ +// Copyright 2020 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:source_span/source_span.dart'; + +import '../interpolation.dart'; +import '../supports_condition.dart'; + +/// A supports condition that represents the forwards-compatible +/// `` production. +class SupportsAnything implements SupportsCondition { + /// The contents of the condition. + final Interpolation contents; + + final FileSpan span; + + SupportsAnything(this.contents, this.span); + + String toString() => "($contents)"; +} diff --git a/lib/src/ast/sass/supports_condition/function.dart b/lib/src/ast/sass/supports_condition/function.dart new file mode 100644 index 000000000..0ca118108 --- /dev/null +++ b/lib/src/ast/sass/supports_condition/function.dart @@ -0,0 +1,23 @@ +// Copyright 2020 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:source_span/source_span.dart'; + +import '../interpolation.dart'; +import '../supports_condition.dart'; + +/// A function-syntax condition. +class SupportsFunction implements SupportsCondition { + /// The name of the function. + final Interpolation name; + + /// The arguments to the function. + final Interpolation arguments; + + final FileSpan span; + + SupportsFunction(this.name, this.arguments, this.span); + + String toString() => "$name($arguments)"; +} diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 2d609626b..f1739c21f 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -372,7 +372,7 @@ abstract class StylesheetParser extends Parser { // Parse custom properties as declarations no matter what. var name = nameBuffer.interpolation(scanner.spanFrom(start, beforeColon)); if (name.initialPlain.startsWith('--')) { - var value = _interpolatedDeclarationValue(); + var value = StringExpression(_interpolatedDeclarationValue()); expectStatementSeparator("custom property"); return Declaration(name, scanner.spanFrom(start), value: value); } @@ -538,7 +538,7 @@ abstract class StylesheetParser extends Parser { scanner.expectChar($colon); if (parseCustomProperties && name.initialPlain.startsWith('--')) { - var value = _interpolatedDeclarationValue(); + var value = StringExpression(_interpolatedDeclarationValue()); expectStatementSeparator("custom property"); return Declaration(name, scanner.spanFrom(start), value: value); } @@ -2681,8 +2681,7 @@ relase. For details, see http://bit.ly/moz-document. return null; } - buffer - .addInterpolation(_interpolatedDeclarationValue(allowEmpty: true).text); + buffer.addInterpolation(_interpolatedDeclarationValue(allowEmpty: true)); scanner.expectChar($rparen); buffer.writeCharCode($rparen); @@ -2808,8 +2807,7 @@ relase. For details, see http://bit.ly/moz-document. buffer ..write(name) ..writeCharCode($lparen) - ..addInterpolation( - _interpolatedDeclarationValue(allowEmpty: true).asInterpolation()) + ..addInterpolation(_interpolatedDeclarationValue(allowEmpty: true)) ..writeCharCode($rparen); if (!scanner.scanChar($rparen)) return false; return true; @@ -2984,10 +2982,18 @@ relase. For details, see http://bit.ly/moz-document. /// /// If [allowEmpty] is `false` (the default), this requires at least one token. /// + /// If [allowSemicolon] is `true`, this doesn't stop at semicolons and instead + /// includes them in the interpolated output. + /// + /// If [allowColon] is `false`, this stops at top-level colons. + /// /// Unlike [declarationValue], this allows interpolation. - StringExpression _interpolatedDeclarationValue({bool allowEmpty = false}) { - // NOTE: this logic is largely duplicated in Parser.declarationValue and - // isIdentifier in utils.dart. Most changes here should be mirrored there. + Interpolation _interpolatedDeclarationValue( + {bool allowEmpty = false, + bool allowSemicolon = false, + bool allowColon = true}) { + // NOTE: this logic is largely duplicated in Parser.declarationValue. Most + // changes here should be mirrored there. var start = scanner.state; var buffer = InterpolationBuffer(); @@ -3065,8 +3071,15 @@ relase. For details, see http://bit.ly/moz-document. break; case $semicolon: - if (brackets.isEmpty) break loop; + if (!allowSemicolon && brackets.isEmpty) break loop; + buffer.writeCharCode(scanner.readChar()); + wroteNewline = false; + break; + + case $colon: + if (!allowColon && brackets.isEmpty) break loop; buffer.writeCharCode(scanner.readChar()); + wroteNewline = false; break; case $u: @@ -3103,7 +3116,7 @@ relase. For details, see http://bit.ly/moz-document. if (brackets.isNotEmpty) scanner.expectChar(brackets.last); if (!allowEmpty && buffer.isEmpty) scanner.error("Expected token."); - return StringExpression(buffer.interpolation(scanner.spanFrom(start))); + return buffer.interpolation(scanner.spanFrom(start)); } /// Consumes an identifier that may contain interpolation. @@ -3301,10 +3314,7 @@ relase. For details, see http://bit.ly/moz-document. /// Consumes a `@supports` condition. SupportsCondition _supportsCondition() { var start = scanner.state; - var first = scanner.peekChar(); - if (first != $lparen && first != $hash) { - var start = scanner.state; - expectIdentifier("not"); + if (scanIdentifier("not")) { whitespace(); return SupportsNegation( _supportsConditionInParens(), scanner.spanFrom(start)); @@ -3312,9 +3322,11 @@ relase. For details, see http://bit.ly/moz-document. var condition = _supportsConditionInParens(); whitespace(); + String operator; while (lookingAtIdentifier()) { - String operator; - if (scanIdentifier("or")) { + if (operator != null) { + expectIdentifier(operator); + } else if (scanIdentifier("or")) { operator = "or"; } else { expectIdentifier("and"); @@ -3333,56 +3345,126 @@ relase. For details, see http://bit.ly/moz-document. /// Consumes a parenthesized supports condition, or an interpolation. SupportsCondition _supportsConditionInParens() { var start = scanner.state; - if (scanner.peekChar() == $hash) { - return SupportsInterpolation( - singleInterpolation(), scanner.spanFrom(start)); + + if (_lookingAtInterpolatedIdentifier()) { + var identifier = interpolatedIdentifier(); + if (identifier.asPlain?.toLowerCase() == "not") { + error('"not" is not a valid identifier here.', identifier.span); + } + + if (scanner.scanChar($lparen)) { + var arguments = _interpolatedDeclarationValue( + allowEmpty: true, allowSemicolon: true); + scanner.expectChar($rparen); + return SupportsFunction(identifier, arguments, scanner.spanFrom(start)); + } else if (identifier.contents.length != 1 || + identifier.contents.first is! Expression) { + error("Expected @supports condition.", identifier.span); + } else { + return SupportsInterpolation( + identifier.contents.first as Expression, scanner.spanFrom(start)); + } } scanner.expectChar($lparen); whitespace(); - var next = scanner.peekChar(); - if (next == $lparen || next == $hash) { - var condition = _supportsCondition(); + if (scanIdentifier("not")) { whitespace(); + var condition = _supportsConditionInParens(); + scanner.expectChar($rparen); + return SupportsNegation(condition, scanner.spanFrom(start)); + } else if (scanner.peekChar() == $lparen) { + var condition = _supportsCondition(); scanner.expectChar($rparen); return condition; } - if (next == $n || next == $N) { - var negation = _trySupportsNegation(); - if (negation != null) { + // Unfortunately, we may have to backtrack here. The grammar is: + // + // Expression ":" Expression + // | InterpolatedIdentifier InterpolatedAnyValue? + // + // These aren't ambiguous because this `InterpolatedAnyValue` is forbidden + // from containing a top-level colon, but we still have to parse the full + // expression to figure out if there's a colon after it. + // + // We could avoid the overhead of a full expression parse by looking ahead + // for a colon (outside of balanced brackets), but in practice we expect the + // vast majority of real uses to be `Expression ":" Expression`, so it makes + // sense to parse that case faster in exchange for less code complexity and + // a slower backtracking case. + Expression name; + var nameStart = scanner.state; + var wasInParentheses = _inParentheses; + try { + name = expression(); + scanner.expectChar($colon); + } on FormatException catch (_) { + scanner.state = nameStart; + _inParentheses = wasInParentheses; + + var identifier = interpolatedIdentifier(); + var operation = _trySupportsOperation(identifier, nameStart); + if (operation != null) { scanner.expectChar($rparen); - return negation; + return operation; } + + // If parsing an expression fails, try to parse an + // `InterpolatedAnyValue` instead. But if that value runs into a + // top-level colon, then this is probably intended to be a declaration + // after all, so we rethrow the declaration-parsing error. + var contents = (InterpolationBuffer() + ..addInterpolation(identifier) + ..addInterpolation(_interpolatedDeclarationValue( + allowEmpty: true, allowSemicolon: true, allowColon: false))) + .interpolation(scanner.spanFrom(nameStart)); + if (scanner.peekChar() == $colon) rethrow; + + scanner.expectChar($rparen); + return SupportsAnything(contents, scanner.spanFrom(start)); } - var name = expression(); - scanner.expectChar($colon); whitespace(); var value = expression(); scanner.expectChar($rparen); return SupportsDeclaration(name, value, scanner.spanFrom(start)); } - /// Tries to consume a negated supports condition. + /// If [interpolation] is followed by `"and"` or `"or"`, parse it as a supports operation. /// - /// Returns `null` if it fails. - SupportsNegation _trySupportsNegation() { - var start = scanner.state; - if (!scanIdentifier("not") || scanner.isDone) { - scanner.state = start; - return null; - } + /// Otherwise, return `null` without moving the scanner position. + SupportsOperation _trySupportsOperation(Interpolation interpolation, LineScannerState start) { + if (interpolation.contents.length != 1) return null; + var expression = interpolation.contents.first; + if (expression is! Expression) return null; - var next = scanner.peekChar(); - if (!isWhitespace(next) && next != $lparen) { - scanner.state = start; - return null; + var beforeWhitespace = scanner.state; + whitespace(); + + SupportsOperation operation; + String operator; + while (lookingAtIdentifier()) { + if (operator != null) { + expectIdentifier(operator); + } else if (scanIdentifier("and")) { + operator = "and"; + } else if (scanIdentifier("or")) { + operator = "or"; + } else { + scanner.state = beforeWhitespace; + return null; + } + + whitespace(); + var right = _supportsConditionInParens(); + operation = SupportsOperation( + operation ?? SupportsInterpolation( + expression as Expression, interpolation.span), right, operator, scanner.spanFrom(start)); + whitespace(); } - whitespace(); - return SupportsNegation( - _supportsConditionInParens(), scanner.spanFrom(start)); + return operation; } // ## Characters diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index 36261b897..c815954f8 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -98,8 +98,10 @@ class SassColor extends Value implements ext.SassColor { factory SassColor.hwb(num hue, num whiteness, num blackness, [num alpha]) { // From https://www.w3.org/TR/css-color-4/#hwb-to-rgb var scaledHue = hue % 360 / 360; - var scaledWhiteness = fuzzyAssertRange(whiteness, 0, 100, "whiteness") / 100; - var scaledBlackness = fuzzyAssertRange(blackness, 0, 100, "blackness") / 100; + var scaledWhiteness = + fuzzyAssertRange(whiteness, 0, 100, "whiteness") / 100; + var scaledBlackness = + fuzzyAssertRange(blackness, 0, 100, "blackness") / 100; var sum = scaledWhiteness + scaledBlackness; if (sum > 1) { @@ -117,11 +119,8 @@ class SassColor extends Value implements ext.SassColor { // don't cache its values because we expect the memory overhead of doing so // to outweigh the cost of recalculating it on access. Instead, we eagerly // convert it to RGB and then convert back if necessary. - return SassColor.rgb( - toRgb(scaledHue + 1/3), - toRgb(scaledHue), - toRgb(scaledHue - 1/3), - alpha); + return SassColor.rgb(toRgb(scaledHue + 1 / 3), toRgb(scaledHue), + toRgb(scaledHue - 1 / 3), alpha); } SassColor._(this._red, this._green, this._blue, this._hue, this._saturation, diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index b7bbf74b1..69594f024 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -1806,6 +1806,11 @@ class _EvaluateVisitor } else if (condition is SupportsDeclaration) { return "(${await _evaluateToCss(condition.name)}: " "${await _evaluateToCss(condition.value)})"; + } else if (condition is SupportsFunction) { + return "${await _performInterpolation(condition.name)}(" + "${await _performInterpolation(condition.arguments)})"; + } else if (condition is SupportsAnything) { + return "(${await _performInterpolation(condition.contents)})"; } else { return null; } diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 530301c11..4fcbb1de4 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 485fce53ba9f381973c25a69b193a681891be098 +// Checksum: ae80942bc5f7f9f4b92e7e4d46903578ea3b9a58 // // ignore_for_file: unused_import @@ -1797,6 +1797,11 @@ class _EvaluateVisitor } else if (condition is SupportsDeclaration) { return "(${_evaluateToCss(condition.name)}: " "${_evaluateToCss(condition.value)})"; + } else if (condition is SupportsFunction) { + return "${_performInterpolation(condition.name)}(" + "${_performInterpolation(condition.arguments)})"; + } else if (condition is SupportsAnything) { + return "(${_performInterpolation(condition.contents)})"; } else { return null; } diff --git a/pubspec.yaml b/pubspec.yaml index 120a1b2e1..cb1b1d1f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.28.0 +version: 1.29.0 description: A Sass implementation in Dart. author: Sass Team homepage: https://github.com/sass/dart-sass diff --git a/test/dart_api/value/color_test.dart b/test/dart_api/value/color_test.dart index 4f88f60b8..aa5e189d4 100644 --- a/test/dart_api/value/color_test.dart +++ b/test/dart_api/value/color_test.dart @@ -130,22 +130,21 @@ void main() { equals(SassColor.hwb(210, 7.0588235294117645, 42))); expect( value.changeHwb(alpha: 0.5), - equals( - SassColor.hwb(210, 7.0588235294117645, 66.27450980392157, 0.5))); + equals(SassColor.hwb( + 210, 7.0588235294117645, 66.27450980392157, 0.5))); expect( - value.changeHwb( - hue: 120, whiteness: 42, blackness: 42, alpha: 0.5), + value.changeHwb(hue: 120, whiteness: 42, blackness: 42, alpha: 0.5), equals(SassColor.hwb(120, 42, 42, 0.5))); expect( - value.changeHwb(whiteness: 50), - equals(SassColor.hwb(210, 43, 57))); + value.changeHwb(whiteness: 50), equals(SassColor.hwb(210, 43, 57))); }); test("allows valid values", () { expect(value.changeHwb(whiteness: 0).whiteness, equals(0)); expect(value.changeHwb(whiteness: 100).whiteness, equals(60.0)); expect(value.changeHwb(blackness: 0).blackness, equals(0)); - expect(value.changeHwb(blackness: 100).blackness, equals(93.33333333333333)); + expect(value.changeHwb(blackness: 100).blackness, + equals(93.33333333333333)); expect(value.changeHwb(alpha: 0).alpha, equals(0)); expect(value.changeHwb(alpha: 1).alpha, equals(1)); }); @@ -219,7 +218,10 @@ void main() { test("equals the same color", () { expect(value, equalsWithHash(SassColor.rgb(0x3E, 0x98, 0x3E))); expect(value, equalsWithHash(SassColor.hsl(120, 42, 42))); - expect(value, equalsWithHash(SassColor.hwb(120, 24.313725490196077, 40.3921568627451))); + expect( + value, + equalsWithHash( + SassColor.hwb(120, 24.313725490196077, 40.3921568627451))); }); }); @@ -295,9 +297,9 @@ void main() { }); test("allows valid values", () { - expect(SassColor.hwb(0, 0, 0, 0), equals(parseValue("rgba(255, 0, 0, 0)"))); - expect(SassColor.hwb(4320, 100, 100, 1), - equals(parseValue("grey"))); + expect( + SassColor.hwb(0, 0, 0, 0), equals(parseValue("rgba(255, 0, 0, 0)"))); + expect(SassColor.hwb(4320, 100, 100, 1), equals(parseValue("grey"))); }); test("disallows invalid values", () { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy