diff --git a/CHANGELOG.md b/CHANGELOG.md index 837a3a0f1..dfb61cdf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ +## 1.52.0 + +* Add support for arbitrary modifiers at the end of plain CSS imports, in + addition to the existing `supports()` and media queries. Sass now allows any + sequence of identifiers of functions after the URL of an import for forwards + compatibility with future additions to the CSS spec. + ## 1.51.1 * Fix an issue where source locations tracked through variable references could potentially become incorrect. + * Fix a bug where a loud comment in the source can break the source map when embedding the sources, when using the command-line interface or the legacy JS API. diff --git a/lib/src/ast/css/import.dart b/lib/src/ast/css/import.dart index 33db7c9b9..b8ad9e9e0 100644 --- a/lib/src/ast/css/import.dart +++ b/lib/src/ast/css/import.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import '../../visitor/interface/css.dart'; -import 'media_query.dart'; import 'node.dart'; import 'value.dart'; @@ -14,11 +13,8 @@ abstract class CssImport extends CssNode { /// This includes quotes. CssValue get url; - /// The supports condition attached to this import. - CssValue? get supports; - - /// The media query attached to this import. - List? get media; + /// The modifiers (such as media or supports queries) attached to this import. + CssValue? get modifiers; T accept(CssVisitor visitor) => visitor.visitCssImport(this); } diff --git a/lib/src/ast/css/modifiable/import.dart b/lib/src/ast/css/modifiable/import.dart index 9d705b72e..033424e78 100644 --- a/lib/src/ast/css/modifiable/import.dart +++ b/lib/src/ast/css/modifiable/import.dart @@ -6,7 +6,6 @@ import 'package:source_span/source_span.dart'; import '../../../visitor/interface/modifiable_css.dart'; import '../import.dart'; -import '../media_query.dart'; import '../value.dart'; import 'node.dart'; @@ -17,17 +16,11 @@ class ModifiableCssImport extends ModifiableCssNode implements CssImport { /// This includes quotes. final CssValue url; - /// The supports condition attached to this import. - final CssValue? supports; - - /// The media query attached to this import. - final List? media; + final CssValue? modifiers; final FileSpan span; - ModifiableCssImport(this.url, this.span, - {this.supports, Iterable? media}) - : media = media == null ? null : List.unmodifiable(media); + ModifiableCssImport(this.url, this.span, {this.modifiers}); T accept(ModifiableCssVisitor visitor) => visitor.visitCssImport(this); } diff --git a/lib/src/ast/sass/import/static.dart b/lib/src/ast/sass/import/static.dart index 6388a5cbd..69b1e3bb3 100644 --- a/lib/src/ast/sass/import/static.dart +++ b/lib/src/ast/sass/import/static.dart @@ -7,7 +7,6 @@ import 'package:source_span/source_span.dart'; import '../import.dart'; import '../interpolation.dart'; -import '../supports_condition.dart'; /// An import that produces a plain CSS `@import` rule. /// @@ -19,22 +18,13 @@ class StaticImport implements Import { /// This already contains quotes. final Interpolation url; - /// The supports condition attached to this import, or `null` if no condition - /// is attached. - final SupportsCondition? supports; - - /// The media query attached to this import, or `null` if no condition is - /// attached. - final Interpolation? media; + /// The modifiers (such as media or supports queries) attached to this import, + /// or `null` if none are attached. + final Interpolation? modifiers; final FileSpan span; - StaticImport(this.url, this.span, {this.supports, this.media}); + StaticImport(this.url, this.span, {this.modifiers}); - String toString() { - var buffer = StringBuffer(url); - if (supports != null) buffer.write(" supports($supports)"); - if (media != null) buffer.write(" $media"); - return buffer.toString(); - } + String toString() => "$url${modifiers == null ? '' : ' $modifiers'}"; } diff --git a/lib/src/ast/sass/interpolation.dart b/lib/src/ast/sass/interpolation.dart index dd3698348..17a440666 100644 --- a/lib/src/ast/sass/interpolation.dart +++ b/lib/src/ast/sass/interpolation.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; +import '../../interpolation_buffer.dart'; import 'expression.dart'; import 'node.dart'; @@ -40,6 +41,32 @@ class Interpolation implements SassNode { return first is String ? first : ''; } + /// Creates a new [Interpolation] by concatenating a sequence of [String]s, + /// [Expression]s, or nested [Interpolation]s. + static Interpolation concat( + Iterable contents, + FileSpan span) { + var buffer = InterpolationBuffer(); + for (var element in contents) { + if (element is! String && + element is! Expression && + element is! Interpolation) { + throw ArgumentError.value(contents, "contents", + "May only contains Strings, Expressions, or Interpolations."); + } + + if (element is String) { + buffer.write(element); + } else if (element is Expression) { + buffer.add(element); + } else if (element is Interpolation) { + buffer.addInterpolation(element); + } + } + + return buffer.interpolation(span); + } + Interpolation(Iterable contents, this.span) : contents = List.unmodifiable(contents) { for (var i = 0; i < this.contents.length; i++) { diff --git a/lib/src/ast/sass/supports_condition.dart b/lib/src/ast/sass/supports_condition.dart index 53f96bb38..112ca9a30 100644 --- a/lib/src/ast/sass/supports_condition.dart +++ b/lib/src/ast/sass/supports_condition.dart @@ -4,10 +4,15 @@ import 'package:meta/meta.dart'; +import 'interpolation.dart'; import 'node.dart'; /// An abstract class for defining the condition a `@supports` rule selects. /// /// {@category AST} @sealed -abstract class SupportsCondition extends SassNode {} +abstract class SupportsCondition extends SassNode { + /// Returns an [Interpolation] that evaluates to the same value as this + /// [SupportsCondition]. + Interpolation toInterpolation(); +} diff --git a/lib/src/ast/sass/supports_condition/anything.dart b/lib/src/ast/sass/supports_condition/anything.dart index 4dbece4b2..48208619d 100644 --- a/lib/src/ast/sass/supports_condition/anything.dart +++ b/lib/src/ast/sass/supports_condition/anything.dart @@ -21,5 +21,8 @@ class SupportsAnything implements SupportsCondition { SupportsAnything(this.contents, this.span); + Interpolation toInterpolation() => + Interpolation.concat(['(', contents, ')'], span); + String toString() => "($contents)"; } diff --git a/lib/src/ast/sass/supports_condition/declaration.dart b/lib/src/ast/sass/supports_condition/declaration.dart index d29d717c9..c60a28bad 100644 --- a/lib/src/ast/sass/supports_condition/declaration.dart +++ b/lib/src/ast/sass/supports_condition/declaration.dart @@ -7,6 +7,7 @@ import 'package:source_span/source_span.dart'; import '../expression.dart'; import '../expression/string.dart'; +import '../interpolation.dart'; import '../supports_condition.dart'; /// A condition that selects for browsers where a given declaration is @@ -42,5 +43,8 @@ class SupportsDeclaration implements SupportsCondition { SupportsDeclaration(this.name, this.value, this.span); + Interpolation toInterpolation() => Interpolation.concat( + ['(', name, isCustomProperty ? ':' : ': ', value, ')'], span); + String toString() => "($name: $value)"; } diff --git a/lib/src/ast/sass/supports_condition/function.dart b/lib/src/ast/sass/supports_condition/function.dart index 73bdb9bda..1c239c9dc 100644 --- a/lib/src/ast/sass/supports_condition/function.dart +++ b/lib/src/ast/sass/supports_condition/function.dart @@ -23,5 +23,8 @@ class SupportsFunction implements SupportsCondition { SupportsFunction(this.name, this.arguments, this.span); + Interpolation toInterpolation() => + Interpolation.concat([name, '(', arguments, ')'], span); + String toString() => "$name($arguments)"; } diff --git a/lib/src/ast/sass/supports_condition/interpolation.dart b/lib/src/ast/sass/supports_condition/interpolation.dart index 9fbd85829..9f0e3a5aa 100644 --- a/lib/src/ast/sass/supports_condition/interpolation.dart +++ b/lib/src/ast/sass/supports_condition/interpolation.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; import '../expression.dart'; +import '../interpolation.dart'; import '../supports_condition.dart'; /// An interpolated condition. @@ -20,5 +21,7 @@ class SupportsInterpolation implements SupportsCondition { SupportsInterpolation(this.expression, this.span); + Interpolation toInterpolation() => Interpolation([expression], span); + String toString() => "#{$expression}"; } diff --git a/lib/src/ast/sass/supports_condition/negation.dart b/lib/src/ast/sass/supports_condition/negation.dart index 4187f3793..5550a0446 100644 --- a/lib/src/ast/sass/supports_condition/negation.dart +++ b/lib/src/ast/sass/supports_condition/negation.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; +import '../interpolation.dart'; import '../supports_condition.dart'; import 'operation.dart'; @@ -20,6 +21,17 @@ class SupportsNegation implements SupportsCondition { SupportsNegation(this.condition, this.span); + Interpolation toInterpolation() { + var needsParens = + condition is SupportsNegation || condition is SupportsOperation; + return Interpolation.concat([ + "not ", + if (needsParens) "(", + condition.toInterpolation(), + if (needsParens) ")" + ], span); + } + String toString() { if (condition is SupportsNegation || condition is SupportsOperation) { return "not ($condition)"; diff --git a/lib/src/ast/sass/supports_condition/operation.dart b/lib/src/ast/sass/supports_condition/operation.dart index 3e4fc5113..13249dbad 100644 --- a/lib/src/ast/sass/supports_condition/operation.dart +++ b/lib/src/ast/sass/supports_condition/operation.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; +import '../interpolation.dart'; import '../supports_condition.dart'; import 'negation.dart'; @@ -34,10 +35,25 @@ class SupportsOperation implements SupportsCondition { } } + Interpolation toInterpolation() => Interpolation.concat([ + ..._parenthesizeInterpolation(left), + " $operator ", + ..._parenthesizeInterpolation(right) + ], span); + + /// Returns a list that can be passed to [Interpolation.concat], with + /// parentheses around [condition] if necessary. + List + _parenthesizeInterpolation(SupportsCondition condition) => condition + is SupportsNegation || + (condition is SupportsOperation && condition.operator == operator) + ? ["(", condition.toInterpolation(), ")"] + : [condition.toInterpolation()]; + String toString() => - "${_parenthesize(left)} $operator ${_parenthesize(right)}"; + "${_parenthesizeString(left)} $operator ${_parenthesizeString(right)}"; - String _parenthesize(SupportsCondition condition) => + String _parenthesizeString(SupportsCondition condition) => condition is SupportsNegation || (condition is SupportsOperation && condition.operator == operator) ? "($condition)" diff --git a/lib/src/parse/css.dart b/lib/src/parse/css.dart index ba09278e9..bffdaa7c9 100644 --- a/lib/src/parse/css.dart +++ b/lib/src/parse/css.dart @@ -94,11 +94,11 @@ class CssParser extends ScssParser { var urlSpan = scanner.spanFrom(urlStart); whitespace(); - var queries = tryImportQueries(); + var modifiers = tryImportModifiers(); expectStatementSeparator("@import rule"); return ImportRule([ StaticImport(Interpolation([url], urlSpan), scanner.spanFrom(urlStart), - supports: queries?.item1, media: queries?.item2) + modifiers: modifiers) ], scanner.spanFrom(start)); } diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index f106fc033..fd3a3095b 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -1086,20 +1086,20 @@ abstract class StylesheetParser extends Parser { if (next == $u || next == $U) { var url = dynamicUrl(); whitespace(); - var queries = tryImportQueries(); + var modifiers = tryImportModifiers(); return StaticImport(Interpolation([url], scanner.spanFrom(start)), scanner.spanFrom(start), - supports: queries?.item1, media: queries?.item2); + modifiers: modifiers); } var url = string(); var urlSpan = scanner.spanFrom(start); whitespace(); - var queries = tryImportQueries(); - if (isPlainImportUrl(url) || queries != null) { + var modifiers = tryImportModifiers(); + if (isPlainImportUrl(url) || modifiers != null) { return StaticImport( Interpolation([urlSpan.text], urlSpan), scanner.spanFrom(start), - supports: queries?.item1, media: queries?.item2); + modifiers: modifiers); } else { try { return DynamicImport(parseImportUrl(url), urlSpan); @@ -1135,54 +1135,100 @@ abstract class StylesheetParser extends Parser { return url.startsWith("http://") || url.startsWith("https://"); } - /// Consumes a supports condition and/or a media query after an `@import`. + /// Consumes a sequence of modifiers (such as media or supports queries) + /// after an import argument. /// - /// Returns `null` if neither type of query can be found. - Tuple2? tryImportQueries() { - SupportsCondition? supports; - if (scanIdentifier("supports")) { - scanner.expectChar($lparen); - var start = scanner.state; - if (scanIdentifier("not")) { - whitespace(); - supports = SupportsNegation( - _supportsConditionInParens(), scanner.spanFrom(start)); - } else if (scanner.peekChar() == $lparen) { - supports = _supportsCondition(); - } else { - if (_lookingAtInterpolatedIdentifier()) { - var identifier = interpolatedIdentifier(); - if (identifier.asPlain?.toLowerCase() == "not") { - error('"not" is not a valid identifier here.', identifier.span); - } + /// Returns `null` if there are no modifiers. + Interpolation? tryImportModifiers() { + // Exit before allocating anything if we're not looking at any modifiers, as + // is the most common case. + if (!_lookingAtInterpolatedIdentifier() && scanner.peekChar() != $lparen) { + return null; + } - if (scanner.scanChar($lparen)) { - var arguments = _interpolatedDeclarationValue( - allowEmpty: true, allowSemicolon: true); - scanner.expectChar($rparen); - supports = SupportsFunction( - identifier, arguments, scanner.spanFrom(start)); + var start = scanner.state; + var buffer = InterpolationBuffer(); + while (true) { + if (_lookingAtInterpolatedIdentifier()) { + if (!buffer.isEmpty) buffer.writeCharCode($space); + + var identifier = interpolatedIdentifier(); + buffer.addInterpolation(identifier); + + var name = identifier.asPlain?.toLowerCase(); + if (name != "and" && scanner.scanChar($lparen)) { + if (name == "supports") { + var query = _importSupportsQuery(); + if (query is! SupportsDeclaration) buffer.writeCharCode($lparen); + buffer.addInterpolation(query.toInterpolation()); + if (query is! SupportsDeclaration) buffer.writeCharCode($rparen); } else { - // Backtrack to parse a variable declaration - scanner.state = start; + buffer.writeCharCode($lparen); + buffer.addInterpolation(_interpolatedDeclarationValue( + allowEmpty: true, allowSemicolon: true)); + buffer.writeCharCode($rparen); + } + + scanner.expectChar($rparen); + whitespace(); + } else { + whitespace(); + if (scanner.scanChar($comma)) { + buffer.write(", "); + buffer.addInterpolation(_mediaQueryList()); + return buffer.interpolation(scanner.spanFrom(start)); } } - if (supports == null) { - var name = expression(); - scanner.expectChar($colon); - supports = _supportsDeclarationValue(name, start); - } + } else if (scanner.peekChar() == $lparen) { + if (!buffer.isEmpty) buffer.writeCharCode($space); + buffer.addInterpolation(_mediaQueryList()); + return buffer.interpolation(scanner.spanFrom(start)); + } else { + return buffer.interpolation(scanner.spanFrom(start)); } - scanner.expectChar($rparen); + } + } + + /// Consumes the contents of a `supports()` function after an `@import` rule + /// (but not the function name or parentheses). + SupportsCondition _importSupportsQuery() { + if (scanIdentifier("not")) { whitespace(); + var start = scanner.state; + return SupportsNegation( + _supportsConditionInParens(), scanner.spanFrom(start)); + } else if (scanner.peekChar() == $lparen) { + return _supportsCondition(); + } else { + var function = _tryImportSupportsFunction(); + if (function != null) return function; + + var start = scanner.state; + var name = expression(); + scanner.expectChar($colon); + return _supportsDeclarationValue(name, start); + } + } + + /// Consumes a function call within a `supports()` function after an + /// `@import` if available. + SupportsFunction? _tryImportSupportsFunction() { + if (!_lookingAtInterpolatedIdentifier()) return null; + + var start = scanner.state; + var name = interpolatedIdentifier(); + assert(name.asPlain != "not"); + + if (!scanner.scanChar($lparen)) { + scanner.state = start; + return null; } - var media = - _lookingAtInterpolatedIdentifier() || scanner.peekChar() == $lparen - ? _mediaQueryList() - : null; - if (supports == null && media == null) return null; - return Tuple2(supports, media); + var value = + _interpolatedDeclarationValue(allowEmpty: true, allowSemicolon: true); + scanner.expectChar($rparen); + + return SupportsFunction(name, value, scanner.spanFrom(start)); } /// Consumes an `@include` rule. diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index df184909a..9c0e7aad0 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -1663,20 +1663,10 @@ class _EvaluateVisitor // NOTE: this logic is largely duplicated in [visitCssImport]. Most changes // here should be mirrored there. - var url = await _interpolationToValue(import.url); - var supports = await import.supports.andThen((supports) async { - var arg = supports is SupportsDeclaration - ? "${await _evaluateToCss(supports.name)}:" - "${supports.isCustomProperty ? '' : ' '}" - "${await _evaluateToCss(supports.value)}" - : await supports.andThen(_visitSupportsCondition); - return CssValue("supports($arg)", supports.span); - }); - var rawMedia = import.media; - var mediaQuery = await rawMedia.andThen(_visitMediaQueries); - - var node = ModifiableCssImport(url, import.span, - supports: supports, media: mediaQuery); + var node = ModifiableCssImport( + await _interpolationToValue(import.url), import.span, + modifiers: await import.modifiers + .andThen>?>(_interpolationToValue)); if (_parent != _root) { _parent.addChild(node); @@ -2923,8 +2913,8 @@ class _EvaluateVisitor // NOTE: this logic is largely duplicated in [_visitStaticImport]. Most // changes here should be mirrored there. - var modifiableNode = ModifiableCssImport(node.url, node.span, - supports: node.supports, media: node.media); + var modifiableNode = + ModifiableCssImport(node.url, node.span, modifiers: node.modifiers); if (_parent != _root) { _parent.addChild(modifiableNode); } else if (_endOfImports == _root.children.length) { diff --git a/lib/src/visitor/clone_css.dart b/lib/src/visitor/clone_css.dart index ba4bf7333..73b0f8b76 100644 --- a/lib/src/visitor/clone_css.dart +++ b/lib/src/visitor/clone_css.dart @@ -48,8 +48,7 @@ class _CloneCssVisitor implements CssVisitor { valueSpanForMap: node.valueSpanForMap); ModifiableCssImport visitCssImport(CssImport node) => - ModifiableCssImport(node.url, node.span, - supports: node.supports, media: node.media); + ModifiableCssImport(node.url, node.span, modifiers: node.modifiers); ModifiableCssKeyframeBlock visitCssKeyframeBlock(CssKeyframeBlock node) => _visitChildren( diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 18663b7e4..a89b9a7f1 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: 45277707f5ab21408f3abb8f249ed7115e0a3c0f +// Checksum: 68136a98fc33b94bd58d0658e325cf90bdfba006 // // ignore_for_file: unused_import @@ -1660,20 +1660,10 @@ class _EvaluateVisitor // NOTE: this logic is largely duplicated in [visitCssImport]. Most changes // here should be mirrored there. - var url = _interpolationToValue(import.url); - var supports = import.supports.andThen((supports) { - var arg = supports is SupportsDeclaration - ? "${_evaluateToCss(supports.name)}:" - "${supports.isCustomProperty ? '' : ' '}" - "${_evaluateToCss(supports.value)}" - : supports.andThen(_visitSupportsCondition); - return CssValue("supports($arg)", supports.span); - }); - var rawMedia = import.media; - var mediaQuery = rawMedia.andThen(_visitMediaQueries); - - var node = ModifiableCssImport(url, import.span, - supports: supports, media: mediaQuery); + var node = ModifiableCssImport( + _interpolationToValue(import.url), import.span, + modifiers: + import.modifiers.andThen?>(_interpolationToValue)); if (_parent != _root) { _parent.addChild(node); @@ -2903,8 +2893,8 @@ class _EvaluateVisitor // NOTE: this logic is largely duplicated in [_visitStaticImport]. Most // changes here should be mirrored there. - var modifiableNode = ModifiableCssImport(node.url, node.span, - supports: node.supports, media: node.media); + var modifiableNode = + ModifiableCssImport(node.url, node.span, modifiers: node.modifiers); if (_parent != _root) { _parent.addChild(modifiableNode); } else if (_endOfImports == _root.children.length) { diff --git a/lib/src/visitor/recursive_ast.dart b/lib/src/visitor/recursive_ast.dart index 21e37ee61..fbc8589b5 100644 --- a/lib/src/visitor/recursive_ast.dart +++ b/lib/src/visitor/recursive_ast.dart @@ -93,8 +93,7 @@ abstract class RecursiveAstVisitor extends RecursiveStatementVisitor for (var import in node.imports) { if (import is StaticImport) { visitInterpolation(import.url); - import.supports.andThen(visitSupportsCondition); - import.media.andThen(visitInterpolation); + import.modifiers.andThen(visitInterpolation); } } } diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 669c624b0..1f1c0c9ec 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -237,16 +237,10 @@ class _SerializeVisitor _writeOptionalSpace(); _for(node.url, () => _writeImportUrl(node.url.value)); - var supports = node.supports; - if (supports != null) { + var modifiers = node.modifiers; + if (modifiers != null) { _writeOptionalSpace(); - _write(supports); - } - - var media = node.media; - if (media != null) { - _writeOptionalSpace(); - _writeBetween(media, _commaSeparator, _visitMediaQuery); + _buffer.write(modifiers); } }); } diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index a602c9374..27195ac04 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.0.0-beta.45 + +* **Breaking change:** Replace `StaticImport.supports` and `StaticImport.media` + with a unified `StaticImport.modifiers` field. Same for `CssImport`. + +* Add `SupportsCondition.toInterpolation()`. + ## 1.0.0-beta.44 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 697d971db..e7e03f8d8 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -2,7 +2,7 @@ name: sass_api # Note: Every time we add a new Sass AST node, we need to bump the *major* # version because it's a breaking change for anyone who's implementing the # visitor interface(s). -version: 1.0.0-beta.44 +version: 1.0.0-beta.45 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: '>=2.12.0 <3.0.0' dependencies: - sass: 1.51.1 + sass: 1.52.0 dependency_overrides: sass: {path: ../..} diff --git a/pubspec.yaml b/pubspec.yaml index acd9134be..51c86bf5e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.51.1-dev +version: 1.52.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass