diff --git a/CHANGELOG.md b/CHANGELOG.md index e828616a6..2894b314a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,5 @@ ## 1.67.0 -* All functions defined in CSS Values and Units 4 are now once again parsed as - calculation objects: `round()`, `mod()`, `rem()`, `sin()`, `cos()`, `tan()`, - `asin()`, `acos()`, `atan()`, `atan2()`, `pow()`, `sqrt()`, `hypot()`, - `log()`, `exp()`, `abs()`, and `sign()`. - - Unlike in 1.65.0, function calls are _not_ locked into being parsed as - calculations or plain Sass functions at parse-time. This means that - user-defined functions will take precedence over CSS calculations of the same - name. Although the function names `calc()` and `clamp()` are still forbidden, - users may continue to freely define functions whose names overlap with other - CSS calculations (including `abs()`, `min()`, `max()`, and `round()` whose - names overlap with global Sass functions). - -* As a consequence of the change in calculation parsing described above, - calculation functions containing interpolation are now parsed more strictly - than before. However, all interpolations that would have produced valid CSS - will continue to work, so this is not considered a breaking change. - -* Interpolations in calculation functions that aren't used in a position that - could also have a normal calculation value are now deprecated. For example, - `calc(1px #{"+ 2px"})` is deprecated, but `calc(1px + #{"2px"})` is still - allowed. This deprecation is named `calc-interp`. See [the Sass website] for - more information. - - [the Sass website]: https://sass-lang.com/d/calc-interp - * **Potentially breaking bug fix**: The importer used to load a given file is no longer used to load absolute URLs that appear in that file. This was unintented behavior that contradicted the Sass specification. Absolute URLs diff --git a/lib/src/ast/sass.dart b/lib/src/ast/sass.dart index 149641670..aa5ebbb79 100644 --- a/lib/src/ast/sass.dart +++ b/lib/src/ast/sass.dart @@ -13,6 +13,7 @@ export 'sass/dependency.dart'; export 'sass/expression.dart'; export 'sass/expression/binary_operation.dart'; export 'sass/expression/boolean.dart'; +export 'sass/expression/calculation.dart'; export 'sass/expression/color.dart'; export 'sass/expression/function.dart'; export 'sass/expression/if.dart'; diff --git a/lib/src/ast/sass/expression.dart b/lib/src/ast/sass/expression.dart index d4ee83122..a5682411e 100644 --- a/lib/src/ast/sass/expression.dart +++ b/lib/src/ast/sass/expression.dart @@ -2,16 +2,13 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'package:charcode/charcode.dart'; import 'package:meta/meta.dart'; import '../../exception.dart'; import '../../logger.dart'; import '../../parse/scss.dart'; -import '../../util/nullable.dart'; -import '../../value.dart'; import '../../visitor/interface/expression.dart'; -import '../sass.dart'; +import 'node.dart'; /// A SassScript expression in a Sass syntax tree. /// @@ -30,87 +27,3 @@ abstract interface class Expression implements SassNode { factory Expression.parse(String contents, {Object? url, Logger? logger}) => ScssParser(contents, url: url, logger: logger).parseExpression(); } - -// Use an extension class rather than a method so we don't have to make -// [Expression] a concrete base class for something we'll get rid of anyway once -// we remove the global math functions that make this necessary. -extension ExpressionExtensions on Expression { - /// Whether this expression can be used in a calculation context. - /// - /// @nodoc - @internal - bool get isCalculationSafe => accept(_IsCalculationSafeVisitor()); -} - -// We could use [AstSearchVisitor] to implement this more tersely, but that -// would default to returning `true` if we added a new expression type and -// forgot to update this class. -class _IsCalculationSafeVisitor implements ExpressionVisitor { - const _IsCalculationSafeVisitor(); - - bool visitBinaryOperationExpression(BinaryOperationExpression node) => - (const { - BinaryOperator.times, - BinaryOperator.dividedBy, - BinaryOperator.plus, - BinaryOperator.minus - }).contains(node.operator) && - (node.left.accept(this) || node.right.accept(this)); - - bool visitBooleanExpression(BooleanExpression node) => false; - - bool visitColorExpression(ColorExpression node) => false; - - bool visitFunctionExpression(FunctionExpression node) => true; - - bool visitInterpolatedFunctionExpression( - InterpolatedFunctionExpression node) => - true; - - bool visitIfExpression(IfExpression node) => true; - - bool visitListExpression(ListExpression node) => - node.separator == ListSeparator.space && - !node.hasBrackets && - node.contents.any((expression) => - expression is StringExpression && - !expression.hasQuotes && - !expression.text.isPlain); - - bool visitMapExpression(MapExpression node) => false; - - bool visitNullExpression(NullExpression node) => false; - - bool visitNumberExpression(NumberExpression node) => true; - - bool visitParenthesizedExpression(ParenthesizedExpression node) => - node.expression.accept(this); - - bool visitSelectorExpression(SelectorExpression node) => false; - - bool visitStringExpression(StringExpression node) { - if (node.hasQuotes) return false; - - // Exclude non-identifier constructs that are parsed as [StringExpression]s. - // We could just check if they parse as valid identifiers, but this is - // cheaper. - var text = node.text.initialPlain; - return - // !important - !text.startsWith("!") && - // ID-style identifiers - !text.startsWith("#") && - // Unicode ranges - text.codeUnitAtOrNull(1) != $plus && - // url() - text.codeUnitAtOrNull(3) != $lparen; - } - - bool visitSupportsExpression(SupportsExpression node) => false; - - bool visitUnaryOperationExpression(UnaryOperationExpression node) => false; - - bool visitValueExpression(ValueExpression node) => false; - - bool visitVariableExpression(VariableExpression node) => true; -} diff --git a/lib/src/ast/sass/expression/binary_operation.dart b/lib/src/ast/sass/expression/binary_operation.dart index dc750900a..4e87fe8de 100644 --- a/lib/src/ast/sass/expression/binary_operation.dart +++ b/lib/src/ast/sass/expression/binary_operation.dart @@ -6,7 +6,6 @@ import 'package:charcode/charcode.dart'; import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; -import '../../../util/span.dart'; import '../../../visitor/interface/expression.dart'; import '../expression.dart'; import 'list.dart'; @@ -46,17 +45,6 @@ final class BinaryOperationExpression implements Expression { return left.span.expand(right.span); } - /// Returns the span that covers only [operator]. - /// - /// @nodoc - @internal - FileSpan get operatorSpan => left.span.file == right.span.file && - left.span.end.offset < right.span.start.offset - ? left.span.file - .span(left.span.end.offset, right.span.start.offset) - .trim() - : span; - BinaryOperationExpression(this.operator, this.left, this.right) : allowsSlash = false; diff --git a/lib/src/ast/sass/expression/calculation.dart b/lib/src/ast/sass/expression/calculation.dart new file mode 100644 index 000000000..38c25ed14 --- /dev/null +++ b/lib/src/ast/sass/expression/calculation.dart @@ -0,0 +1,108 @@ +// Copyright 2021 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:meta/meta.dart'; +import 'package:source_span/source_span.dart'; + +import '../../../visitor/interface/expression.dart'; +import '../expression.dart'; +import 'binary_operation.dart'; +import 'function.dart'; +import 'if.dart'; +import 'number.dart'; +import 'parenthesized.dart'; +import 'string.dart'; +import 'variable.dart'; + +/// A calculation literal. +/// +/// {@category AST} +final class CalculationExpression implements Expression { + /// This calculation's name. + final String name; + + /// The arguments for the calculation. + final List arguments; + + final FileSpan span; + + /// Returns a `calc()` calculation expression. + CalculationExpression.calc(Expression argument, FileSpan span) + : this("calc", [argument], span); + + /// Returns a `min()` calculation expression. + CalculationExpression.min(Iterable arguments, this.span) + : name = "min", + arguments = _verifyArguments(arguments) { + if (this.arguments.isEmpty) { + throw ArgumentError("min() requires at least one argument."); + } + } + + /// Returns a `max()` calculation expression. + CalculationExpression.max(Iterable arguments, this.span) + : name = "max", + arguments = _verifyArguments(arguments) { + if (this.arguments.isEmpty) { + throw ArgumentError("max() requires at least one argument."); + } + } + + /// Returns a `clamp()` calculation expression. + CalculationExpression.clamp( + Expression min, Expression value, Expression max, FileSpan span) + : this("clamp", [min, max, value], span); + + /// Returns a calculation expression with the given name and arguments. + /// + /// Unlike the other constructors, this doesn't verify that the arguments are + /// valid for the name. + @internal + CalculationExpression(this.name, Iterable arguments, this.span) + : arguments = _verifyArguments(arguments); + + /// Throws an [ArgumentError] if [arguments] aren't valid calculation + /// arguments, and returns them as an unmodifiable list if they are. + static List _verifyArguments(Iterable arguments) => + List.unmodifiable(arguments.map((arg) { + _verify(arg); + return arg; + })); + + /// Throws an [ArgumentError] if [expression] isn't a valid calculation + /// argument. + static void _verify(Expression expression) { + switch (expression) { + case NumberExpression() || + CalculationExpression() || + VariableExpression() || + FunctionExpression() || + IfExpression() || + StringExpression(hasQuotes: false): + break; + + case ParenthesizedExpression(:var expression): + _verify(expression); + + case BinaryOperationExpression( + :var left, + :var right, + operator: BinaryOperator.plus || + BinaryOperator.minus || + BinaryOperator.times || + BinaryOperator.dividedBy + ): + _verify(left); + _verify(right); + + case _: + throw ArgumentError("Invalid calculation argument $expression."); + } + } + + T accept(ExpressionVisitor visitor) => + visitor.visitCalculationExpression(this); + + String toString() => "$name(${arguments.join(', ')})"; +} diff --git a/lib/src/ast/sass/interpolation.dart b/lib/src/ast/sass/interpolation.dart index 075b3344f..578394e83 100644 --- a/lib/src/ast/sass/interpolation.dart +++ b/lib/src/ast/sass/interpolation.dart @@ -21,9 +21,6 @@ final class Interpolation implements SassNode { final FileSpan span; - /// Returns whether this contains no interpolated expressions. - bool get isPlain => asPlain != null; - /// If this contains no interpolated expressions, returns its text contents. /// /// Otherwise, returns `null`. diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart index 007fbb152..5ea363008 100644 --- a/lib/src/deprecation.dart +++ b/lib/src/deprecation.dart @@ -69,11 +69,6 @@ enum Deprecation { deprecatedIn: '1.62.3', description: 'Passing null as alpha in the ${isJS ? 'JS' : 'Dart'} API.'), - calcInterp('calc-interp', - deprecatedIn: '1.67.0', - description: 'Using interpolation in a calculation outside a value ' - 'position.'), - /// Deprecation for `@import` rules. import.future('import', description: '@import rules.'), diff --git a/lib/src/embedded/protofier.dart b/lib/src/embedded/protofier.dart index 6ad083ca4..3a1a792b0 100644 --- a/lib/src/embedded/protofier.dart +++ b/lib/src/embedded/protofier.dart @@ -134,6 +134,8 @@ final class Protofier { ..operator = _protofyCalculationOperator(value.operator) ..left = _protofyCalculationValue(value.left) ..right = _protofyCalculationValue(value.right); + case CalculationInterpolation(): + result.interpolation = value.value; case _: throw "Unknown calculation value $value"; } @@ -350,7 +352,7 @@ final class Protofier { _deprotofyCalculationValue(value.operation.left), _deprotofyCalculationValue(value.operation.right)), Value_Calculation_CalculationValue_Value.interpolation => - SassString('(${value.interpolation})', quotes: false), + CalculationInterpolation(value.interpolation), Value_Calculation_CalculationValue_Value.notSet => throw mandatoryError("Value.Calculation.value") }; diff --git a/lib/src/functions/math.dart b/lib/src/functions/math.dart index bca609d0d..a85e5b1a4 100644 --- a/lib/src/functions/math.dart +++ b/lib/src/functions/math.dart @@ -12,7 +12,6 @@ import '../deprecation.dart'; import '../evaluation_context.dart'; import '../exception.dart'; import '../module/built_in.dart'; -import '../util/number.dart'; import '../value.dart'; /// The global definitions of Sass math functions. @@ -150,32 +149,87 @@ final _log = _function("log", r"$number, $base: null", (arguments) { final _pow = _function("pow", r"$base, $exponent", (arguments) { var base = arguments[0].assertNumber("base"); var exponent = arguments[1].assertNumber("exponent"); - return pow(base, exponent); + if (base.hasUnits) { + throw SassScriptException("\$base: Expected $base to have no units."); + } else if (exponent.hasUnits) { + throw SassScriptException( + "\$exponent: Expected $exponent to have no units."); + } else { + return SassNumber(math.pow(base.value, exponent.value)); + } }); -final _sqrt = _singleArgumentMathFunc("sqrt", sqrt); +final _sqrt = _function("sqrt", r"$number", (arguments) { + var number = arguments[0].assertNumber("number"); + if (number.hasUnits) { + throw SassScriptException("\$number: Expected $number to have no units."); + } else { + return SassNumber(math.sqrt(number.value)); + } +}); /// /// Trigonometric functions /// -final _acos = _singleArgumentMathFunc("acos", acos); +final _acos = _function("acos", r"$number", (arguments) { + var number = arguments[0].assertNumber("number"); + if (number.hasUnits) { + throw SassScriptException("\$number: Expected $number to have no units."); + } else { + return SassNumber.withUnits(math.acos(number.value) * 180 / math.pi, + numeratorUnits: ['deg']); + } +}); -final _asin = _singleArgumentMathFunc("asin", asin); +final _asin = _function("asin", r"$number", (arguments) { + var number = arguments[0].assertNumber("number"); + if (number.hasUnits) { + throw SassScriptException("\$number: Expected $number to have no units."); + } else { + return SassNumber.withUnits(math.asin(number.value) * 180 / math.pi, + numeratorUnits: ['deg']); + } +}); -final _atan = _singleArgumentMathFunc("atan", atan); +final _atan = _function("atan", r"$number", (arguments) { + var number = arguments[0].assertNumber("number"); + if (number.hasUnits) { + throw SassScriptException("\$number: Expected $number to have no units."); + } else { + return SassNumber.withUnits(math.atan(number.value) * 180 / math.pi, + numeratorUnits: ['deg']); + } +}); final _atan2 = _function("atan2", r"$y, $x", (arguments) { var y = arguments[0].assertNumber("y"); var x = arguments[1].assertNumber("x"); - return atan2(y, x); + return SassNumber.withUnits( + math.atan2(y.value, x.convertValueToMatch(y, 'x', 'y')) * 180 / math.pi, + numeratorUnits: ['deg']); }); -final _cos = _singleArgumentMathFunc("cos", cos); - -final _sin = _singleArgumentMathFunc("sin", sin); - -final _tan = _singleArgumentMathFunc("tan", tan); +final _cos = _function( + "cos", + r"$number", + (arguments) => SassNumber(math.cos(arguments[0] + .assertNumber("number") + .coerceValueToUnit("rad", "number")))); + +final _sin = _function( + "sin", + r"$number", + (arguments) => SassNumber(math.sin(arguments[0] + .assertNumber("number") + .coerceValueToUnit("rad", "number")))); + +final _tan = _function( + "tan", + r"$number", + (arguments) => SassNumber(math.tan(arguments[0] + .assertNumber("number") + .coerceValueToUnit("rad", "number")))); /// /// Unit functions @@ -251,16 +305,6 @@ final _div = _function("div", r"$number1, $number2", (arguments) { /// Helpers /// -/// Returns a [Callable] named [name] that calls a single argument -/// math function. -BuiltInCallable _singleArgumentMathFunc( - String name, SassNumber mathFunc(SassNumber value)) { - return _function(name, r"$number", (arguments) { - var number = arguments[0].assertNumber("number"); - return mathFunc(number); - }); -} - /// Returns a [Callable] named [name] that transforms a number's value /// using [transform] and preserves its units. BuiltInCallable _numberFunction(String name, double transform(double value)) { diff --git a/lib/src/js/value/calculation.dart b/lib/src/js/value/calculation.dart index 51dfadae8..6154de77b 100644 --- a/lib/src/js/value/calculation.dart +++ b/lib/src/js/value/calculation.dart @@ -93,7 +93,7 @@ final JSClass calculationOperationClass = () { _assertCalculationValue(left); _assertCalculationValue(right); return SassCalculation.operateInternal(operator, left, right, - inLegacySassFunction: false, simplify: false); + inMinMax: false, simplify: false); }); jsClass.defineMethods({ @@ -109,7 +109,7 @@ final JSClass calculationOperationClass = () { getJSClass(SassCalculation.operateInternal( CalculationOperator.plus, SassNumber(1), SassNumber(1), - inLegacySassFunction: false, simplify: false)) + inMinMax: false, simplify: false)) .injectSuperclass(jsClass); return jsClass; }(); diff --git a/lib/src/parse/css.dart b/lib/src/parse/css.dart index 6ed9123b7..acbb51fe7 100644 --- a/lib/src/parse/css.dart +++ b/lib/src/parse/css.dart @@ -22,11 +22,7 @@ final _disallowedFunctionNames = ..remove("invert") ..remove("alpha") ..remove("opacity") - ..remove("saturate") - ..remove("min") - ..remove("max") - ..remove("round") - ..remove("abs"); + ..remove("saturate"); class CssParser extends ScssParser { bool get plainCss => true; @@ -100,17 +96,6 @@ class CssParser extends ScssParser { ], scanner.spanFrom(start)); } - ParenthesizedExpression parentheses() { - // Expressions are only allowed within calculations, but we verify this at - // evaluation time. - var start = scanner.state; - scanner.expectChar($lparen); - whitespace(); - var expression = expressionUntilComma(); - scanner.expectChar($rparen); - return ParenthesizedExpression(expression, scanner.spanFrom(start)); - } - Expression identifierLike() { var start = scanner.state; var identifier = interpolatedIdentifier(); @@ -122,8 +107,6 @@ class CssParser extends ScssParser { } var beforeArguments = scanner.state; - // `namespacedExpression()` is just here to throw a clearer error. - if (scanner.scanChar($dot)) return namespacedExpression(plain, start); if (!scanner.scanChar($lparen)) return StringExpression(identifier); var allowEmptySecondArg = lower == 'var'; @@ -149,8 +132,10 @@ class CssParser extends ScssParser { "This function isn't allowed in plain CSS.", scanner.spanFrom(start)); } - return FunctionExpression( - plain, + return InterpolatedFunctionExpression( + // Create a fake interpolation to force the function to be interpreted + // as plain CSS, rather than calling a user-defined function. + Interpolation([StringExpression(identifier)], identifier.span), ArgumentInvocation( arguments, const {}, scanner.spanFrom(beforeArguments)), scanner.spanFrom(start)); diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 587616f14..9e9f7b2ed 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -130,17 +130,6 @@ abstract class StylesheetParser extends Parser { return _useRule(start); }); - Interpolation parseInterpolatedDeclarationValue( - {bool allowEmpty = false, - bool allowSemicolon = false, - bool allowColon = true}) => - // Don't use [_parseSingleProduction] because we want to allow text after - // the value. - wrapSpanFormatException(() => _interpolatedDeclarationValue( - allowEmpty: allowEmpty, - allowSemicolon: allowSemicolon, - allowColon: allowColon)); - /// Parses and returns [production] as the entire contents of [scanner]. T _parseSingleProduction(T production()) { return wrapSpanFormatException(() { @@ -1832,13 +1821,8 @@ abstract class StylesheetParser extends Parser { void addOperator(BinaryOperator operator) { if (plainCss && - operator != BinaryOperator.singleEquals && - // These are allowed in calculations, so we have to check them at - // evaluation time. - operator != BinaryOperator.plus && - operator != BinaryOperator.minus && - operator != BinaryOperator.times && - operator != BinaryOperator.dividedBy) { + operator != BinaryOperator.dividedBy && + operator != BinaryOperator.singleEquals) { scanner.error("Operators aren't allowed in plain CSS.", position: scanner.position - operator.operator.length, length: operator.operator.length); @@ -1892,7 +1876,7 @@ abstract class StylesheetParser extends Parser { case $lparen: // Parenthesized numbers can't be slash-separated. - addSingleExpression(parentheses()); + addSingleExpression(_parentheses()); case $lbracket: addSingleExpression(_expression(bracketList: true)); @@ -2081,7 +2065,7 @@ abstract class StylesheetParser extends Parser { /// produces a potentially slash-separated number. bool _isSlashOperand(Expression expression) => expression is NumberExpression || - expression is FunctionExpression || + expression is CalculationExpression || (expression is BinaryOperationExpression && expression.allowsSlash); /// Consumes an expression that doesn't contain any top-level whitespace. @@ -2089,7 +2073,7 @@ abstract class StylesheetParser extends Parser { // Note: when adding a new case, make sure it's reflected in // [_lookingAtExpression] and [_expression]. null => scanner.error("Expected expression."), - $lparen => parentheses(), + $lparen => _parentheses(), $slash => _unaryOperation(), $dot => _number(), $lbracket => _expression(bracketList: true), @@ -2117,8 +2101,11 @@ abstract class StylesheetParser extends Parser { }; /// Consumes a parenthesized expression. - @protected - Expression parentheses() { + Expression _parentheses() { + if (plainCss) { + scanner.error("Parentheses aren't allowed in plain CSS.", length: 1); + } + var wasInParentheses = _inParentheses; _inParentheses = true; try { @@ -2614,12 +2601,17 @@ abstract class StylesheetParser extends Parser { /// [name]. @protected Expression? trySpecialFunction(String name, LineScannerState start) { + if (scanner.peekChar() == $lparen) { + if (_tryCalculation(name, start) case var calculation?) { + return calculation; + } + } + var normalized = unvendor(name); InterpolationBuffer buffer; switch (normalized) { - case "calc" when normalized != name && scanner.scanChar($lparen): - case "element" || "expression" when scanner.scanChar($lparen): + case "calc" || "element" || "expression" when scanner.scanChar($lparen): buffer = InterpolationBuffer() ..write(name) ..writeCharCode($lparen); @@ -2651,6 +2643,228 @@ abstract class StylesheetParser extends Parser { return StringExpression(buffer.interpolation(scanner.spanFrom(start))); } + /// If [name] is the name of a calculation expression, parses the + /// corresponding calculation and returns it. + /// + /// Assumes the scanner is positioned immediately before the opening + /// parenthesis of the argument list. + CalculationExpression? _tryCalculation(String name, LineScannerState start) { + assert(scanner.peekChar() == $lparen); + switch (name) { + case "calc": + var arguments = _calculationArguments(1); + return CalculationExpression(name, arguments, scanner.spanFrom(start)); + + case "min" || "max": + // min() and max() are parsed as calculations if possible, and otherwise + // are parsed as normal Sass functions. + var beforeArguments = scanner.state; + List arguments; + try { + arguments = _calculationArguments(); + } on FormatException catch (_) { + scanner.state = beforeArguments; + return null; + } + + return CalculationExpression(name, arguments, scanner.spanFrom(start)); + + case "clamp": + var arguments = _calculationArguments(3); + return CalculationExpression(name, arguments, scanner.spanFrom(start)); + + case _: + return null; + } + } + + /// Consumes and returns arguments for a calculation expression, including the + /// opening and closing parentheses. + /// + /// If [maxArgs] is passed, at most that many arguments are consumed. + /// Otherwise, any number greater than zero are consumed. + List _calculationArguments([int? maxArgs]) { + scanner.expectChar($lparen); + if (_tryCalculationInterpolation() case var interpolation?) { + scanner.expectChar($rparen); + return [interpolation]; + } + + whitespace(); + var arguments = [_calculationSum()]; + while ((maxArgs == null || arguments.length < maxArgs) && + scanner.scanChar($comma)) { + whitespace(); + arguments.add(_calculationSum()); + } + + scanner.expectChar($rparen, + name: arguments.length == maxArgs + ? '"+", "-", "*", "/", or ")"' + : '"+", "-", "*", "/", ",", or ")"'); + + return arguments; + } + + /// Parses a calculation operation or value expression. + Expression _calculationSum() { + var sum = _calculationProduct(); + + while (true) { + var next = scanner.peekChar(); + if (next != $plus && next != $minus) return sum; + + if (!scanner.peekChar(-1).isWhitespace || + !scanner.peekChar(1).isWhitespace) { + scanner.error( + '"+" and "-" must be surrounded by whitespace in calculations.'); + } + + scanner.readChar(); + whitespace(); + sum = BinaryOperationExpression( + next == $plus ? BinaryOperator.plus : BinaryOperator.minus, + sum, + _calculationProduct()); + } + } + + /// Parses a calculation product or value expression. + Expression _calculationProduct() { + var product = _calculationValue(); + + while (true) { + whitespace(); + var next = scanner.peekChar(); + if (next != $asterisk && next != $slash) return product; + + scanner.readChar(); + whitespace(); + product = BinaryOperationExpression( + next == $asterisk ? BinaryOperator.times : BinaryOperator.dividedBy, + product, + _calculationValue()); + } + } + + /// Parses a single calculation value. + Expression _calculationValue() { + switch (scanner.peekChar()) { + case $plus || $dot || int(isDigit: true): + return _number(); + case $dollar: + return _variable(); + case $lparen: + var start = scanner.state; + scanner.readChar(); + + Expression? value = _tryCalculationInterpolation(); + if (value == null) { + whitespace(); + value = _calculationSum(); + } + + whitespace(); + scanner.expectChar($rparen); + return ParenthesizedExpression(value, scanner.spanFrom(start)); + case _ when lookingAtIdentifier(): + var start = scanner.state; + var ident = identifier(); + if (scanner.scanChar($dot)) return namespacedExpression(ident, start); + if (scanner.peekChar() != $lparen) { + return StringExpression( + Interpolation([ident], scanner.spanFrom(start)), + quotes: false); + } + + var lowerCase = ident.toLowerCase(); + if (_tryCalculation(lowerCase, start) case var calculation?) { + return calculation; + } else if (lowerCase == "if") { + return IfExpression(_argumentInvocation(), scanner.spanFrom(start)); + } else { + return FunctionExpression( + ident, _argumentInvocation(), scanner.spanFrom(start)); + } + + // This has to go after [lookingAtIdentifier] because a hyphen can start + // an identifier as well as a number. + case $minus: + return _number(); + + case _: + scanner.error("Expected number, variable, function, or calculation."); + } + } + + /// If the following text up to the next unbalanced `")"`, `"]"`, or `"}"` + /// contains interpolation, parses that interpolation as an unquoted + /// [StringExpression] and returns it. + StringExpression? _tryCalculationInterpolation() => + _containsCalculationInterpolation() + ? StringExpression(_interpolatedDeclarationValue()) + : null; + + /// Returns whether the following text up to the next unbalanced `")"`, `"]"`, + /// or `"}"` contains interpolation. + bool _containsCalculationInterpolation() { + var parens = 0; + var brackets = []; + + var start = scanner.state; + while (!scanner.isDone) { + var next = scanner.peekChar(); + switch (next) { + case $backslash: + scanner.readChar(); + scanner.readChar(); + + case $slash: + if (!scanComment()) scanner.readChar(); + + case $single_quote || $double_quote: + interpolatedString(); + + case $hash: + if (parens == 0 && scanner.peekChar(1) == $lbrace) { + scanner.state = start; + return true; + } + scanner.readChar(); + + case $lparen: + parens++; + continue left; + + left: + case $lbrace: + case $lbracket: + // dart-lang/sdk#45357 + brackets.add(opposite(next!)); + scanner.readChar(); + + case $rparen: + parens--; + continue right; + + right: + case $rbrace: + case $rbracket: + if (brackets.isEmpty || brackets.removeLast() != next) { + scanner.state = start; + return false; + } + scanner.readChar(); + + case _: + scanner.readChar(); + } + } + + scanner.state = start; + return false; + } + /// Like [_urlContents], but returns `null` if the URL fails to parse. /// /// [start] is the position before the beginning of the name. [name] is the diff --git a/lib/src/util/nullable.dart b/lib/src/util/nullable.dart index ad4a8ba2f..125f58d46 100644 --- a/lib/src/util/nullable.dart +++ b/lib/src/util/nullable.dart @@ -21,10 +21,3 @@ extension SetExtension on Set { return cast(); } } - -extension StringExtension on String { - /// Like [String.codeUnitAt], but returns `null` instead of throwing an error - /// if [index] is past the end of the string. - int? codeUnitAtOrNull(int index) => - index >= length ? null : codeUnitAt(index); -} diff --git a/lib/src/util/number.dart b/lib/src/util/number.dart index 8df7beb1a..80fd3aaa2 100644 --- a/lib/src/util/number.dart +++ b/lib/src/util/number.dart @@ -110,11 +110,6 @@ double fuzzyAssertRange(double number, int min, int max, [String? name]) { /// /// [floored division]: https://en.wikipedia.org/wiki/Modulo_operation#Variants_of_the_definition double moduloLikeSass(double num1, double num2) { - if (num1.isInfinite) return double.nan; - if (num2.isInfinite) { - return num1.signIncludingZero == num2.sign ? num1 : double.nan; - } - if (num2 > 0) return num1 % num2; if (num2 == 0) return double.nan; @@ -123,76 +118,3 @@ double moduloLikeSass(double num1, double num2) { var result = num1 % num2; return result == 0 ? 0 : result + num2; } - -/// Returns the square root of [number]. -SassNumber sqrt(SassNumber number) { - number.assertNoUnits("number"); - return SassNumber(math.sqrt(number.value)); -} - -/// Returns the sine of [number]. -SassNumber sin(SassNumber number) => - SassNumber(math.sin(number.coerceValueToUnit("rad", "number"))); - -/// Returns the cosine of [number]. -SassNumber cos(SassNumber number) => - SassNumber(math.cos(number.coerceValueToUnit("rad", "number"))); - -/// Returns the tangent of [number]. -SassNumber tan(SassNumber number) => - SassNumber(math.tan(number.coerceValueToUnit("rad", "number"))); - -/// Returns the arctangent of [number]. -SassNumber atan(SassNumber number) { - number.assertNoUnits("number"); - return _radiansToDegrees(math.atan(number.value)); -} - -/// Returns the arcsine of [number]. -SassNumber asin(SassNumber number) { - number.assertNoUnits("number"); - return _radiansToDegrees(math.asin(number.value)); -} - -/// Returns the arccosine of [number] -SassNumber acos(SassNumber number) { - number.assertNoUnits("number"); - return _radiansToDegrees(math.acos(number.value)); -} - -/// Returns the absolute value of [number]. -SassNumber abs(SassNumber number) => - SassNumber(number.value.abs()).coerceToMatch(number); - -/// Returns the logarithm of [number] with respect to [base]. -SassNumber log(SassNumber number, SassNumber? base) { - if (base != null) { - return SassNumber(math.log(number.value) / math.log(base.value)); - } - return SassNumber(math.log(number.value)); -} - -/// Returns the value of [base] raised to the power of [exponent]. -SassNumber pow(SassNumber base, SassNumber exponent) { - base.assertNoUnits("base"); - exponent.assertNoUnits("exponent"); - return SassNumber(math.pow(base.value, exponent.value)); -} - -/// Returns the arctangent for [y] and [x]. -SassNumber atan2(SassNumber y, SassNumber x) => - _radiansToDegrees(math.atan2(y.value, x.convertValueToMatch(y, 'x', 'y'))); - -/// Returns [radians] as a [SassNumber] with unit `deg`. -SassNumber _radiansToDegrees(double radians) => - SassNumber.withUnits(radians * (180 / math.pi), numeratorUnits: ['deg']); - -/// Extension methods to get the sign of the double's numerical value, -/// including positive and negative zero. -extension DoubleWithSignedZero on double { - double get signIncludingZero { - if (identical(this, -0.0)) return -1.0; - if (this == 0) return 1.0; - return sign; - } -} diff --git a/lib/src/value/calculation.dart b/lib/src/value/calculation.dart index cbb8b92e6..b39011bf0 100644 --- a/lib/src/value/calculation.dart +++ b/lib/src/value/calculation.dart @@ -2,18 +2,11 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:math' as math; - -import 'package:charcode/charcode.dart'; import 'package:meta/meta.dart'; -import '../deprecation.dart'; -import '../evaluation_context.dart'; import '../exception.dart'; -import '../callable.dart'; -import '../util/character.dart'; import '../util/nullable.dart'; -import '../util/number.dart' as number_lib; +import '../util/number.dart'; import '../utils.dart'; import '../value.dart'; import '../visitor/interface/value.dart'; @@ -34,7 +27,7 @@ final class SassCalculation extends Value { /// The calculation's arguments. /// /// Each argument is either a [SassNumber], a [SassCalculation], an unquoted - /// [SassString], or a [CalculationOperation]. + /// [SassString], a [CalculationOperation], or a [CalculationInterpolation]. final List arguments; /// @nodoc @@ -51,7 +44,8 @@ final class SassCalculation extends Value { /// Creates a `calc()` calculation with the given [argument]. /// /// The [argument] must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. + /// unquoted [SassString], a [CalculationOperation], or a + /// [CalculationInterpolation]. /// /// This automatically simplifies the calculation, so it may return a /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it @@ -66,8 +60,8 @@ final class SassCalculation extends Value { /// Creates a `min()` calculation with the given [arguments]. /// /// Each argument must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. It must be passed at - /// least one argument. + /// unquoted [SassString], a [CalculationOperation], or a + /// [CalculationInterpolation]. It must be passed at least one argument. /// /// This automatically simplifies the calculation, so it may return a /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it @@ -97,8 +91,8 @@ final class SassCalculation extends Value { /// Creates a `max()` calculation with the given [arguments]. /// /// Each argument must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. It must be passed at - /// least one argument. + /// unquoted [SassString], a [CalculationOperation], or a + /// [CalculationInterpolation]. It must be passed at least one argument. /// /// This automatically simplifies the calculation, so it may return a /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it @@ -125,181 +119,11 @@ final class SassCalculation extends Value { return SassCalculation._("max", args); } - /// Creates a `hypot()` calculation with the given [arguments]. - /// - /// Each argument must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. It must be passed at - /// least one argument. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - static Value hypot(Iterable arguments) { - var args = _simplifyArguments(arguments); - if (args.isEmpty) { - throw ArgumentError("hypot() must have at least one argument."); - } - _verifyCompatibleNumbers(args); - - var subtotal = 0.0; - var first = args.first; - if (first is! SassNumber || first.hasUnit('%')) { - return SassCalculation._("hypot", args); - } - for (var i = 0; i < args.length; i++) { - var number = args.elementAt(i); - if (number is! SassNumber || !number.hasCompatibleUnits(first)) { - return SassCalculation._("hypot", args); - } - var value = - number.convertValueToMatch(first, "numbers[${i + 1}]", "numbers[1]"); - subtotal += value * value; - } - return SassNumber.withUnits(math.sqrt(subtotal), - numeratorUnits: first.numeratorUnits, - denominatorUnits: first.denominatorUnits); - } - - /// Creates a `sqrt()` calculation with the given [argument]. - /// - /// The [argument] must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - static Value sqrt(Object argument) => - _singleArgument("sqrt", argument, number_lib.sqrt, forbidUnits: true); - - /// Creates a `sin()` calculation with the given [argument]. - /// - /// The [argument] must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - static Value sin(Object argument) => - _singleArgument("sin", argument, number_lib.sin); - - /// Creates a `cos()` calculation with the given [argument]. - /// - /// The [argument] must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - static Value cos(Object argument) => - _singleArgument("cos", argument, number_lib.cos); - - /// Creates a `tan()` calculation with the given [argument]. - /// - /// The [argument] must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - static Value tan(Object argument) => - _singleArgument("tan", argument, number_lib.tan); - - /// Creates an `atan()` calculation with the given [argument]. - /// - /// The [argument] must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - static Value atan(Object argument) => - _singleArgument("atan", argument, number_lib.atan, forbidUnits: true); - - /// Creates an `asin()` calculation with the given [argument]. - /// - /// The [argument] must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - static Value asin(Object argument) => - _singleArgument("asin", argument, number_lib.asin, forbidUnits: true); - - /// Creates an `acos()` calculation with the given [argument]. - /// - /// The [argument] must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - static Value acos(Object argument) => - _singleArgument("acos", argument, number_lib.acos, forbidUnits: true); - - /// Creates an `abs()` calculation with the given [argument]. - /// - /// The [argument] must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - static Value abs(Object argument) { - argument = _simplify(argument); - if (argument is! SassNumber) return SassCalculation._("abs", [argument]); - if (argument.hasUnit("%")) { - warnForDeprecation( - "Passing percentage units to the global abs() function is deprecated.\n" - "In the future, this will emit a CSS abs() function to be resolved by the browser.\n" - "To preserve current behavior: math.abs($argument)" - "\n" - "To emit a CSS abs() now: abs(#{$argument})\n" - "More info: https://sass-lang.com/d/abs-percent", - Deprecation.absPercent); - } - return number_lib.abs(argument); - } - - /// Creates an `exp()` calculation with the given [argument]. - /// - /// The [argument] must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - static Value exp(Object argument) { - argument = _simplify(argument); - if (argument is! SassNumber) { - return SassCalculation._("exp", [argument]); - } - argument.assertNoUnits(); - return number_lib.pow(SassNumber(math.e), argument); - } - - /// Creates a `sign()` calculation with the given [argument]. - /// - /// The [argument] must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - static Value sign(Object argument) { - argument = _simplify(argument); - return switch (argument) { - SassNumber(value: double(isNaN: true) || 0) => argument, - SassNumber arg when !arg.hasUnit('%') => - SassNumber(arg.value.sign).coerceToMatch(argument), - _ => SassCalculation._("sign", [argument]), - }; - } - /// Creates a `clamp()` calculation with the given [min], [value], and [max]. /// /// Each argument must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. + /// unquoted [SassString], a [CalculationOperation], or a + /// [CalculationInterpolation]. /// /// This automatically simplifies the calculation, so it may return a /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it @@ -333,246 +157,6 @@ final class SassCalculation extends Value { return SassCalculation._("clamp", args); } - /// Creates a `pow()` calculation with the given [base] and [exponent]. - /// - /// Each argument must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - /// - /// This may be passed fewer than two arguments, but only if one of the - /// arguments is an unquoted `var()` string. - static Value pow(Object base, Object? exponent) { - var args = [base, if (exponent != null) exponent]; - _verifyLength(args, 2); - base = _simplify(base); - exponent = exponent.andThen(_simplify); - if (base is! SassNumber || exponent is! SassNumber) { - return SassCalculation._("pow", args); - } - base.assertNoUnits(); - exponent.assertNoUnits(); - return number_lib.pow(base, exponent); - } - - /// Creates a `log()` calculation with the given [number] and [base]. - /// - /// Each argument must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - /// - /// If arguments contains exactly a single argument, the base is set to - /// `math.e` by default. - static Value log(Object number, Object? base) { - number = _simplify(number); - base = base.andThen(_simplify); - var args = [number, if (base != null) base]; - if (number is! SassNumber || (base != null && base is! SassNumber)) { - return SassCalculation._("log", args); - } - number.assertNoUnits(); - if (base is SassNumber) { - base.assertNoUnits(); - return number_lib.log(number, base); - } - return number_lib.log(number, null); - } - - /// Creates a `atan2()` calculation for [y] and [x]. - /// - /// Each argument must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - /// - /// This may be passed fewer than two arguments, but only if one of the - /// arguments is an unquoted `var()` string. - static Value atan2(Object y, Object? x) { - y = _simplify(y); - x = x.andThen(_simplify); - var args = [y, if (x != null) x]; - _verifyLength(args, 2); - _verifyCompatibleNumbers(args); - if (y is! SassNumber || - x is! SassNumber || - y.hasUnit('%') || - x.hasUnit('%') || - !y.hasCompatibleUnits(x)) { - return SassCalculation._("atan2", args); - } - return number_lib.atan2(y, x); - } - - /// Creates a `rem()` calculation with the given [dividend] and [modulus]. - /// - /// Each argument must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - /// - /// This may be passed fewer than two arguments, but only if one of the - /// arguments is an unquoted `var()` string. - static Value rem(Object dividend, Object? modulus) { - dividend = _simplify(dividend); - modulus = modulus.andThen(_simplify); - var args = [dividend, if (modulus != null) modulus]; - _verifyLength(args, 2); - _verifyCompatibleNumbers(args); - if (dividend is! SassNumber || - modulus is! SassNumber || - !dividend.hasCompatibleUnits(modulus)) { - return SassCalculation._("rem", args); - } - var result = dividend.modulo(modulus); - if (modulus.value.signIncludingZero != dividend.value.signIncludingZero) { - if (modulus.value.isInfinite) return dividend; - if (result.value == 0) { - return result.unaryMinus(); - } - return result.minus(modulus); - } - return result; - } - - /// Creates a `mod()` calculation with the given [dividend] and [modulus]. - /// - /// Each argument must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - /// - /// This may be passed fewer than two arguments, but only if one of the - /// arguments is an unquoted `var()` string. - static Value mod(Object dividend, Object? modulus) { - dividend = _simplify(dividend); - modulus = modulus.andThen(_simplify); - var args = [dividend, if (modulus != null) modulus]; - _verifyLength(args, 2); - _verifyCompatibleNumbers(args); - if (dividend is! SassNumber || - modulus is! SassNumber || - !dividend.hasCompatibleUnits(modulus)) { - return SassCalculation._("mod", args); - } - return dividend.modulo(modulus); - } - - /// Creates a `round()` calculation with the given [strategyOrNumber], - /// [numberOrStep], and [step]. Strategy must be either nearest, up, down or - /// to-zero. - /// - /// Number and step must be either a [SassNumber], a [SassCalculation], an - /// unquoted [SassString], or a [CalculationOperation]. - /// - /// This automatically simplifies the calculation, so it may return a - /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it - /// can determine that the calculation will definitely produce invalid CSS. - /// - /// This may be passed fewer than two arguments, but only if one of the - /// arguments is an unquoted `var()` string. - static Value round(Object strategyOrNumber, - [Object? numberOrStep, Object? step]) { - switch (( - _simplify(strategyOrNumber), - numberOrStep.andThen(_simplify), - step.andThen(_simplify) - )) { - case (SassNumber number, null, null): - return _matchUnits(number.value.round().toDouble(), number); - - case (SassNumber number, SassNumber step, null) - when !number.hasCompatibleUnits(step): - _verifyCompatibleNumbers([number, step]); - return SassCalculation._("round", [number, step]); - - case (SassNumber number, SassNumber step, null): - _verifyCompatibleNumbers([number, step]); - return _roundWithStep('nearest', number, step); - - case ( - SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') && - var strategy, - SassNumber number, - SassNumber step - ) - when !number.hasCompatibleUnits(step): - _verifyCompatibleNumbers([number, step]); - return SassCalculation._("round", [strategy, number, step]); - - case ( - SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') && - var strategy, - SassNumber number, - SassNumber step - ): - _verifyCompatibleNumbers([number, step]); - return _roundWithStep(strategy.text, number, step); - - case ( - SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') && - var strategy, - SassString rest, - null - ): - return SassCalculation._("round", [strategy, rest]); - - case ( - SassString(text: 'nearest' || 'up' || 'down' || 'to-zero'), - _?, - null - ): - throw SassScriptException("If strategy is not null, step is required."); - - case ( - SassString(text: 'nearest' || 'up' || 'down' || 'to-zero'), - null, - null - ): - throw SassScriptException( - "Number to round and step arguments are required."); - - case (SassString rest, null, null): - return SassCalculation._("round", [rest]); - - case (var number, null, null): - throw SassScriptException( - "Single argument $number expected to be simplifiable."); - - case (var number, var step?, null): - return SassCalculation._("round", [number, step]); - - case ( - (SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') || - SassString(isVar: true)) && - var strategy, - var number?, - var step? - ): - return SassCalculation._("round", [strategy, number, step]); - - case (_, _?, _?): - throw SassScriptException( - "$strategyOrNumber must be either nearest, up, down or to-zero."); - - case (_, null, _?): - // TODO(pamelalozano): Get rid of this case once dart-lang/sdk#52908 is solved. - // ignore: unreachable_switch_case - case (_, _, _): - throw SassScriptException("Invalid parameters."); - } - } - /// Creates and simplifies a [CalculationOperation] with the given [operator], /// [left], and [right]. /// @@ -580,15 +164,15 @@ final class SassCalculation extends Value { /// [SassNumber] rather than a [CalculationOperation]. /// /// Each of [left] and [right] must be either a [SassNumber], a - /// [SassCalculation], an unquoted [SassString], or a [CalculationOperation]. + /// [SassCalculation], an unquoted [SassString], a [CalculationOperation], or + /// a [CalculationInterpolation]. static Object operate( CalculationOperator operator, Object left, Object right) => - operateInternal(operator, left, right, - inLegacySassFunction: false, simplify: true); + operateInternal(operator, left, right, inMinMax: false, simplify: true); - /// Like [operate], but with the internal-only [inLegacySassFunction] parameter. + /// Like [operate], but with the internal-only [inMinMax] parameter. /// - /// If [inLegacySassFunction] is `true`, this allows unitless numbers to be added and + /// If [inMinMax] is `true`, this allows unitless numbers to be added and /// subtracted with numbers with units, for backwards-compatibility with the /// old global `min()` and `max()` functions. /// @@ -596,7 +180,7 @@ final class SassCalculation extends Value { @internal static Object operateInternal( CalculationOperator operator, Object left, Object right, - {required bool inLegacySassFunction, required bool simplify}) { + {required bool inMinMax, required bool simplify}) { if (!simplify) return CalculationOperation._(operator, left, right); left = _simplify(left); right = _simplify(right); @@ -604,7 +188,7 @@ final class SassCalculation extends Value { if (operator case CalculationOperator.plus || CalculationOperator.minus) { if (left is SassNumber && right is SassNumber && - (inLegacySassFunction + (inMinMax ? left.isComparableTo(right) : left.hasCompatibleUnits(right))) { return operator == CalculationOperator.plus @@ -614,7 +198,7 @@ final class SassCalculation extends Value { _verifyCompatibleNumbers([left, right]); - if (right is SassNumber && number_lib.fuzzyLessThan(right.value, 0)) { + if (right is SassNumber && fuzzyLessThan(right.value, 0)) { right = right.times(SassNumber(-1)); operator = operator == CalculationOperator.plus ? CalculationOperator.minus @@ -635,88 +219,19 @@ final class SassCalculation extends Value { /// simplification. SassCalculation._(this.name, this.arguments); - // Returns [value] coerced to [number]'s units. - static SassNumber _matchUnits(double value, SassNumber number) => - SassNumber.withUnits(value, - numeratorUnits: number.numeratorUnits, - denominatorUnits: number.denominatorUnits); - - /// Returns a rounded [number] based on a selected rounding [strategy], - /// to the nearest integer multiple of [step]. - static SassNumber _roundWithStep( - String strategy, SassNumber number, SassNumber step) { - if (!{'nearest', 'up', 'down', 'to-zero'}.contains(strategy)) { - throw ArgumentError( - "$strategy must be either nearest, up, down or to-zero."); - } - - if (number.value.isInfinite && step.value.isInfinite || - step.value == 0 || - number.value.isNaN || - step.value.isNaN) { - return _matchUnits(double.nan, number); - } - if (number.value.isInfinite) return number; - - if (step.value.isInfinite) { - return switch ((strategy, number.value)) { - (_, 0) => number, - ('nearest' || 'to-zero', > 0) => _matchUnits(0.0, number), - ('nearest' || 'to-zero', _) => _matchUnits(-0.0, number), - ('up', > 0) => _matchUnits(double.infinity, number), - ('up', _) => _matchUnits(-0.0, number), - ('down', < 0) => _matchUnits(-double.infinity, number), - ('down', _) => _matchUnits(0, number), - (_, _) => throw UnsupportedError("Invalid argument: $strategy.") - }; - } - - var stepWithNumberUnit = step.convertValueToMatch(number); - return switch (strategy) { - 'nearest' => _matchUnits( - (number.value / stepWithNumberUnit).round() * stepWithNumberUnit, - number), - 'up' => _matchUnits( - (step.value < 0 - ? (number.value / stepWithNumberUnit).floor() - : (number.value / stepWithNumberUnit).ceil()) * - stepWithNumberUnit, - number), - 'down' => _matchUnits( - (step.value < 0 - ? (number.value / stepWithNumberUnit).ceil() - : (number.value / stepWithNumberUnit).floor()) * - stepWithNumberUnit, - number), - 'to-zero' => number.value < 0 - ? _matchUnits( - (number.value / stepWithNumberUnit).ceil() * stepWithNumberUnit, - number) - : _matchUnits( - (number.value / stepWithNumberUnit).floor() * stepWithNumberUnit, - number), - _ => _matchUnits(double.nan, number) - }; - } - /// Returns an unmodifiable list of [args], with each argument simplified. static List _simplifyArguments(Iterable args) => List.unmodifiable(args.map(_simplify)); /// Simplifies a calculation argument. static Object _simplify(Object arg) => switch (arg) { - SassNumber() || CalculationOperation() => arg, - CalculationInterpolation() => - SassString('(${arg.value})', quotes: false), + SassNumber() || + CalculationInterpolation() || + CalculationOperation() => + arg, SassString(hasQuotes: false) => arg, SassString() => throw SassScriptException( "Quoted string $arg can't be used in a calculation."), - SassCalculation( - name: 'calc', - arguments: [SassString(hasQuotes: false, :var text)] - ) - when _needsParentheses(text) => - SassString('($text)', quotes: false), SassCalculation(name: 'calc', arguments: [var value]) => value, SassCalculation() => arg, Value() => throw SassScriptException( @@ -724,40 +239,6 @@ final class SassCalculation extends Value { _ => throw ArgumentError("Unexpected calculation argument $arg.") }; - /// Returns whether [text] needs parentheses if it's the contents of a - /// `calc()` being embedded in another calculation. - static bool _needsParentheses(String text) { - var first = text.codeUnitAt(0); - if (_charNeedsParentheses(first)) return true; - var couldBeVar = text.length >= 4 && characterEqualsIgnoreCase(first, $v); - - if (text.length < 2) return false; - var second = text.codeUnitAt(1); - if (_charNeedsParentheses(second)) return true; - couldBeVar = couldBeVar && characterEqualsIgnoreCase(second, $a); - - if (text.length < 3) return false; - var third = text.codeUnitAt(2); - if (_charNeedsParentheses(third)) return true; - couldBeVar = couldBeVar && characterEqualsIgnoreCase(third, $r); - - if (text.length < 4) return false; - var fourth = text.codeUnitAt(3); - if (couldBeVar && fourth == $lparen) return true; - if (_charNeedsParentheses(fourth)) return true; - - for (var i = 4; i < text.length; i++) { - if (_charNeedsParentheses(text.codeUnitAt(i))) return true; - } - return false; - } - - /// Returns whether [character] intrinsically needs parentheses if it appears - /// in the unquoted string argument of a `calc()` being embedded in another - /// calculation. - static bool _charNeedsParentheses(int character) => - character.isWhitespace || character == $slash || character == $asterisk; - /// Verifies that all the numbers in [args] aren't known to be incompatible /// with one another, and that they don't have units that are too complex for /// calculations. @@ -786,10 +267,11 @@ final class SassCalculation extends Value { } /// Throws a [SassScriptException] if [args] isn't [expectedLength] *and* - /// doesn't contain a [SassString]. + /// doesn't contain either a [SassString] or a [CalculationInterpolation]. static void _verifyLength(List args, int expectedLength) { if (args.length == expectedLength) return; - if (args.any((arg) => arg is SassString)) { + if (args + .any((arg) => arg is SassString || arg is CalculationInterpolation)) { return; } throw SassScriptException( @@ -797,21 +279,6 @@ final class SassCalculation extends Value { "${pluralize('was', args.length, plural: 'were')} passed."); } - /// Returns a [Callable] named [name] that calls a single argument - /// math function. - /// - /// If [forbidUnits] is `true` it will throw an error if [argument] has units. - static Value _singleArgument( - String name, Object argument, SassNumber mathFunc(SassNumber value), - {bool forbidUnits = false}) { - argument = _simplify(argument); - if (argument is! SassNumber) { - return SassCalculation._(name, [argument]); - } - if (forbidUnits) argument.assertNoUnits(); - return mathFunc(argument); - } - /// @nodoc @internal T accept(ValueVisitor visitor) => visitor.visitCalculation(this); @@ -862,14 +329,14 @@ final class CalculationOperation { /// The left-hand operand. /// /// This is either a [SassNumber], a [SassCalculation], an unquoted - /// [SassString], or a [CalculationOperation]. + /// [SassString], a [CalculationOperation], or a [CalculationInterpolation]. Object get left => _left; final Object _left; /// The right-hand operand. /// /// This is either a [SassNumber], a [SassCalculation], an unquoted - /// [SassString], or a [CalculationOperation]. + /// [SassString], a [CalculationOperation], or a [CalculationInterpolation]. Object get right => _right; final Object _right; @@ -925,15 +392,12 @@ enum CalculationOperator { String toString() => name; } -/// A deprecated representation of a string injected into a [SassCalculation] -/// using interpolation. +/// A string injected into a [SassCalculation] using interpolation. /// -/// This only exists for backwards-compatibility with an older version of Dart -/// Sass. It's now equivalent to creating a `SassString` whose value is wrapped -/// in parentheses. +/// This is tracked separately from string arguments because it requires +/// additional parentheses when used as an operand of a [CalculationOperation]. /// /// {@category Value} -@Deprecated("Use SassString instead.") @sealed class CalculationInterpolation { /// We use a getters to allow overriding the logic in the JS API diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index a5c90a501..410bc8465 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -710,7 +710,7 @@ abstract class SassNumber extends Value { /// @nodoc @internal - SassNumber modulo(Value other) { + Value modulo(Value other) { if (other is SassNumber) { return withValue(_coerceUnits(other, moduloLikeSass)); } diff --git a/lib/src/value/number/unitless.dart b/lib/src/value/number/unitless.dart index 7272b7c59..06b54d39b 100644 --- a/lib/src/value/number/unitless.dart +++ b/lib/src/value/number/unitless.dart @@ -98,7 +98,7 @@ class UnitlessSassNumber extends SassNumber { return super.lessThanOrEquals(other); } - SassNumber modulo(Value other) { + Value modulo(Value other) { if (other is SassNumber) { return other.withValue(moduloLikeSass(value, other.value)); } diff --git a/lib/src/visitor/ast_search.dart b/lib/src/visitor/ast_search.dart deleted file mode 100644 index d971afd23..000000000 --- a/lib/src/visitor/ast_search.dart +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright 2023 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:meta/meta.dart'; - -import '../ast/sass.dart'; -import '../util/iterable.dart'; -import '../util/nullable.dart'; -import 'interface/expression.dart'; -import 'recursive_statement.dart'; -import 'statement_search.dart'; - -/// A visitor that recursively traverses each statement and expression in a Sass -/// AST whose `visit*` methods default to returning `null`, but which returns -/// the first non-`null` value returned by any method. -/// -/// This extends [RecursiveStatementVisitor] to traverse each expression in -/// addition to each statement. It supports the same additional methods as -/// [RecursiveAstVisitor]. -/// -/// {@category Visitor} -mixin AstSearchVisitor on StatementSearchVisitor - implements ExpressionVisitor { - T? visitAtRootRule(AtRootRule node) => - node.query.andThen(visitInterpolation) ?? super.visitAtRootRule(node); - - T? visitAtRule(AtRule node) => - visitInterpolation(node.name) ?? - node.value.andThen(visitInterpolation) ?? - super.visitAtRule(node); - - T? visitContentRule(ContentRule node) => - visitArgumentInvocation(node.arguments); - - T? visitDebugRule(DebugRule node) => visitExpression(node.expression); - - T? visitDeclaration(Declaration node) => - visitInterpolation(node.name) ?? - node.value.andThen(visitExpression) ?? - super.visitDeclaration(node); - - T? visitEachRule(EachRule node) => - visitExpression(node.list) ?? super.visitEachRule(node); - - T? visitErrorRule(ErrorRule node) => visitExpression(node.expression); - - T? visitExtendRule(ExtendRule node) => visitInterpolation(node.selector); - - T? visitForRule(ForRule node) => - visitExpression(node.from) ?? - visitExpression(node.to) ?? - super.visitForRule(node); - - T? visitForwardRule(ForwardRule node) => node.configuration - .search((variable) => visitExpression(variable.expression)); - - T? visitIfRule(IfRule node) => - node.clauses.search((clause) => - visitExpression(clause.expression) ?? - clause.children.search((child) => child.accept(this))) ?? - node.lastClause.andThen((lastClause) => - lastClause.children.search((child) => child.accept(this))); - - T? visitImportRule(ImportRule node) => - node.imports.search((import) => import is StaticImport - ? visitInterpolation(import.url) ?? - import.modifiers.andThen(visitInterpolation) - : null); - - T? visitIncludeRule(IncludeRule node) => - visitArgumentInvocation(node.arguments) ?? super.visitIncludeRule(node); - - T? visitLoudComment(LoudComment node) => visitInterpolation(node.text); - - T? visitMediaRule(MediaRule node) => - visitInterpolation(node.query) ?? super.visitMediaRule(node); - - T? visitReturnRule(ReturnRule node) => visitExpression(node.expression); - - T? visitStyleRule(StyleRule node) => - visitInterpolation(node.selector) ?? super.visitStyleRule(node); - - T? visitSupportsRule(SupportsRule node) => - visitSupportsCondition(node.condition) ?? super.visitSupportsRule(node); - - T? visitUseRule(UseRule node) => node.configuration - .search((variable) => visitExpression(variable.expression)); - - T? visitVariableDeclaration(VariableDeclaration node) => - visitExpression(node.expression); - - T? visitWarnRule(WarnRule node) => visitExpression(node.expression); - - T? visitWhileRule(WhileRule node) => - visitExpression(node.condition) ?? super.visitWhileRule(node); - - T? visitExpression(Expression expression) => expression.accept(this); - - T? visitBinaryOperationExpression(BinaryOperationExpression node) => - node.left.accept(this) ?? node.right.accept(this); - - T? visitBooleanExpression(BooleanExpression node) => null; - - T? visitColorExpression(ColorExpression node) => null; - - T? visitFunctionExpression(FunctionExpression node) => - visitArgumentInvocation(node.arguments); - - T? visitInterpolatedFunctionExpression(InterpolatedFunctionExpression node) => - visitInterpolation(node.name) ?? visitArgumentInvocation(node.arguments); - - T? visitIfExpression(IfExpression node) => - visitArgumentInvocation(node.arguments); - - T? visitListExpression(ListExpression node) => - node.contents.search((item) => item.accept(this)); - - T? visitMapExpression(MapExpression node) => - node.pairs.search((pair) => pair.$1.accept(this) ?? pair.$2.accept(this)); - - T? visitNullExpression(NullExpression node) => null; - - T? visitNumberExpression(NumberExpression node) => null; - - T? visitParenthesizedExpression(ParenthesizedExpression node) => - node.expression.accept(this); - - T? visitSelectorExpression(SelectorExpression node) => null; - - T? visitStringExpression(StringExpression node) => - visitInterpolation(node.text); - - T? visitSupportsExpression(SupportsExpression node) => - visitSupportsCondition(node.condition); - - T? visitUnaryOperationExpression(UnaryOperationExpression node) => - node.operand.accept(this); - - T? visitValueExpression(ValueExpression node) => null; - - T? visitVariableExpression(VariableExpression node) => null; - - @protected - T? visitCallableDeclaration(CallableDeclaration node) => - node.arguments.arguments.search( - (argument) => argument.defaultValue.andThen(visitExpression)) ?? - super.visitCallableDeclaration(node); - - /// Visits each expression in an [invocation]. - /// - /// The default implementation of the visit methods calls this to visit any - /// argument invocation in a statement. - @protected - T? visitArgumentInvocation(ArgumentInvocation invocation) => - invocation.positional - .search((expression) => visitExpression(expression)) ?? - invocation.named.values - .search((expression) => visitExpression(expression)) ?? - invocation.rest.andThen(visitExpression) ?? - invocation.keywordRest.andThen(visitExpression); - - /// Visits each expression in [condition]. - /// - /// The default implementation of the visit methods call this to visit any - /// [SupportsCondition] they encounter. - @protected - T? visitSupportsCondition(SupportsCondition condition) => switch (condition) { - SupportsOperation() => visitSupportsCondition(condition.left) ?? - visitSupportsCondition(condition.right), - SupportsNegation() => visitSupportsCondition(condition.condition), - SupportsInterpolation() => visitExpression(condition.expression), - SupportsDeclaration() => - visitExpression(condition.name) ?? visitExpression(condition.value), - _ => null - }; - - /// Visits each expression in an [interpolation]. - /// - /// The default implementation of the visit methods call this to visit any - /// interpolation in a statement. - @protected - T? visitInterpolation(Interpolation interpolation) => interpolation.contents - .search((node) => node is Expression ? visitExpression(node) : null); -} diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index e7b9d3f14..3e8dabcd3 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -38,10 +38,8 @@ import '../logger.dart'; import '../module.dart'; import '../module/built_in.dart'; import '../parse/keyframe_selector.dart'; -import '../parse/scss.dart'; import '../syntax.dart'; import '../utils.dart'; -import '../util/character.dart'; import '../util/map.dart'; import '../util/multi_span.dart'; import '../util/nullable.dart'; @@ -432,14 +430,10 @@ final class _EvaluateVisitor return SassFunction(PlainCssCallable(name.text)); } - var callable = _addExceptionSpan(_callableNode!, () { - var normalizedName = name.text.replaceAll("_", "-"); - var namespace = module?.text; - var local = - _environment.getFunction(normalizedName, namespace: namespace); - if (local != null || namespace != null) return local; - return _builtInFunctions[normalizedName]; - }); + var callable = _addExceptionSpan( + _callableNode!, + () => _getFunction(name.text.replaceAll("_", "-"), + namespace: module?.text)); if (callable == null) throw "Function not found: $name"; return SassFunction(callable); @@ -2188,13 +2182,6 @@ final class _EvaluateVisitor // ## Expressions Future visitBinaryOperationExpression(BinaryOperationExpression node) { - if (_stylesheet.plainCss && - node.operator != BinaryOperator.singleEquals && - node.operator != BinaryOperator.dividedBy) { - throw _exception( - "Operators aren't allowed in plain CSS.", node.operatorSpan); - } - return _addExceptionSpanAsync(node, () async { var left = await node.left.accept(this); return switch (node.operator) { @@ -2230,10 +2217,7 @@ final class _EvaluateVisitor Value _slash(Value left, Value right, BinaryOperationExpression node) { var result = left.dividedBy(right); switch ((left, right)) { - case (SassNumber left, SassNumber right) - when node.allowsSlash && - _operandAllowsSlash(node.left) && - _operandAllowsSlash(node.right): + case (SassNumber left, SassNumber right) when node.allowsSlash: return (result as SassNumber).withSlash(left, right); case (SassNumber(), SassNumber()): @@ -2266,20 +2250,6 @@ final class _EvaluateVisitor } } - /// Returns whether [node] can be used as a component of a slash-separated - /// number. - /// - /// Although this logic is mostly resolved at parse-time, we can't tell - /// whether operands will be evaluated as calculations until evaluation-time. - bool _operandAllowsSlash(Expression node) => - node is! FunctionExpression || - (node.namespace == null && - const { - "calc", "clamp", "hypot", "sin", "cos", "tan", "asin", "acos", // - "atan", "sqrt", "exp", "sign", "mod", "rem", "atan2", "pow", "log" - }.contains(node.name.toLowerCase()) && - _environment.getFunction(node.name) == null); - Future visitValueExpression(ValueExpression node) async => node.value; Future visitVariableExpression(VariableExpression node) async { @@ -2324,142 +2294,23 @@ final class _EvaluateVisitor SassNumber(node.value, node.unit); Future visitParenthesizedExpression(ParenthesizedExpression node) => - _stylesheet.plainCss - ? throw _exception( - "Parentheses aren't allowed in plain CSS.", node.span) - : node.expression.accept(this); - - Future visitColorExpression(ColorExpression node) async => - node.value; + node.expression.accept(this); - Future visitListExpression(ListExpression node) async => SassList( - await mapAsync( - node.contents, (Expression expression) => expression.accept(this)), - node.separator, - brackets: node.hasBrackets); - - Future visitMapExpression(MapExpression node) async { - var map = {}; - var keyNodes = {}; - for (var (key, value) in node.pairs) { - var keyValue = await key.accept(this); - var valueValue = await value.accept(this); - - if (map.containsKey(keyValue)) { - var oldValueSpan = keyNodes[keyValue]?.span; - throw MultiSpanSassRuntimeException( - 'Duplicate key.', - key.span, - 'second key', - {if (oldValueSpan != null) oldValueSpan: 'first key'}, - _stackTrace(key.span)); - } - map[keyValue] = valueValue; - keyNodes[keyValue] = key; - } - return SassMap(map); - } - - Future visitFunctionExpression(FunctionExpression node) async { - var function = _stylesheet.plainCss - ? null - : _addExceptionSpan( - node, - () => - _environment.getFunction(node.name, namespace: node.namespace)); - if (function == null) { - if (node.namespace != null) { - throw _exception("Undefined function.", node.span); - } - - switch (node.name.toLowerCase()) { - case "min" || "max" || "round" || "abs" - when node.arguments.named.isEmpty && - node.arguments.rest == null && - node.arguments.positional - .every((argument) => argument.isCalculationSafe): - return await _visitCalculation(node, inLegacySassFunction: true); - - case "calc" || - "clamp" || - "hypot" || - "sin" || - "cos" || - "tan" || - "asin" || - "acos" || - "atan" || - "sqrt" || - "exp" || - "sign" || - "mod" || - "rem" || - "atan2" || - "pow" || - "log": - return await _visitCalculation(node); - } - - function = (_stylesheet.plainCss ? null : _builtInFunctions[node.name]) ?? - PlainCssCallable(node.originalName); - } - - var oldInFunction = _inFunction; - _inFunction = true; - var result = await _addErrorSpan( - node, () => _runFunctionCallable(node.arguments, function, node)); - _inFunction = oldInFunction; - return result; - } - - Future _visitCalculation(FunctionExpression node, - {bool inLegacySassFunction = false}) async { - if (node.arguments.named.isNotEmpty) { - throw _exception( - "Keyword arguments can't be used with calculations.", node.span); - } else if (node.arguments.rest != null) { - throw _exception( - "Rest arguments can't be used with calculations.", node.span); - } - - _checkCalculationArguments(node); + Future visitCalculationExpression(CalculationExpression node) async { var arguments = [ - for (var argument in node.arguments.positional) + for (var argument in node.arguments) await _visitCalculationValue(argument, - inLegacySassFunction: inLegacySassFunction) + inMinMax: node.name == 'min' || node.name == 'max') ]; if (_inSupportsDeclaration) { return SassCalculation.unsimplified(node.name, arguments); } try { - return switch (node.name.toLowerCase()) { + return switch (node.name) { "calc" => SassCalculation.calc(arguments[0]), - "sqrt" => SassCalculation.sqrt(arguments[0]), - "sin" => SassCalculation.sin(arguments[0]), - "cos" => SassCalculation.cos(arguments[0]), - "tan" => SassCalculation.tan(arguments[0]), - "asin" => SassCalculation.asin(arguments[0]), - "acos" => SassCalculation.acos(arguments[0]), - "atan" => SassCalculation.atan(arguments[0]), - "abs" => SassCalculation.abs(arguments[0]), - "exp" => SassCalculation.exp(arguments[0]), - "sign" => SassCalculation.sign(arguments[0]), "min" => SassCalculation.min(arguments), "max" => SassCalculation.max(arguments), - "hypot" => SassCalculation.hypot(arguments), - "pow" => - SassCalculation.pow(arguments[0], arguments.elementAtOrNull(1)), - "atan2" => - SassCalculation.atan2(arguments[0], arguments.elementAtOrNull(1)), - "log" => - SassCalculation.log(arguments[0], arguments.elementAtOrNull(1)), - "mod" => - SassCalculation.mod(arguments[0], arguments.elementAtOrNull(1)), - "rem" => - SassCalculation.rem(arguments[0], arguments.elementAtOrNull(1)), - "round" => SassCalculation.round(arguments[0], - arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)), "clamp" => SassCalculation.clamp(arguments[0], arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)), _ => throw UnsupportedError('Unknown calculation name "${node.name}".') @@ -2468,54 +2319,11 @@ final class _EvaluateVisitor // The simplification logic in the [SassCalculation] static methods will // throw an error if the arguments aren't compatible, but we have access // to the original spans so we can throw a more informative error. - if (error.message.contains("compatible")) { - _verifyCompatibleNumbers(arguments, node.arguments.positional); - } + _verifyCompatibleNumbers(arguments, node.arguments); throwWithTrace(_exception(error.message, node.span), error, stackTrace); } } - /// Verifies that the calculation [node] has the correct number of arguments. - void _checkCalculationArguments(FunctionExpression node) { - void check([int? maxArgs]) { - if (node.arguments.positional.isEmpty) { - throw _exception("Missing argument.", node.span); - } else if (maxArgs != null && - node.arguments.positional.length > maxArgs) { - throw _exception( - "Only $maxArgs ${pluralize('argument', maxArgs)} allowed, but " - "${node.arguments.positional.length} " + - pluralize('was', node.arguments.positional.length, - plural: 'were') + - " passed.", - node.span); - } - } - - switch (node.name.toLowerCase()) { - case "calc" || - "sqrt" || - "sin" || - "cos" || - "tan" || - "asin" || - "acos" || - "atan" || - "abs" || - "exp" || - "sign": - check(1); - case "min" || "max" || "hypot": - check(); - case "pow" || "atan2" || "log" || "mod" || "rem": - check(2); - case "round" || "clamp": - check(3); - case _: - throw UnsupportedError('Unknown calculation name "${node.name}".'); - } - } - /// Verifies that [args] all have compatible units that can be used for CSS /// calculations, and throws a [SassException] if not. /// @@ -2553,51 +2361,55 @@ final class _EvaluateVisitor /// Evaluates [node] as a component of a calculation. /// - /// If [inLegacySassFunction] is `true`, this allows unitless numbers to be added and + /// If [inMinMax] is `true`, this allows unitless numbers to be added and /// subtracted with numbers with units, for backwards-compatibility with the - /// old global `min()`, `max()`, `round()`, and `abs()` functions. + /// old global `min()` and `max()` functions. Future _visitCalculationValue(Expression node, - {required bool inLegacySassFunction}) async { + {required bool inMinMax}) async { switch (node) { case ParenthesizedExpression(expression: var inner): - var result = await _visitCalculationValue(inner, - inLegacySassFunction: inLegacySassFunction); - return result is SassString && - !result.hasQuotes && - (startsWithIgnoreCase(result.text, 'var(') || - (inner is StringExpression && !inner.text.isPlain) || - inner is ListExpression) + var result = await _visitCalculationValue(inner, inMinMax: inMinMax); + return inner is FunctionExpression && + inner.name.toLowerCase() == 'var' && + result is SassString && + !result.hasQuotes ? SassString('(${result.text})', quotes: false) : result; - case StringExpression() when node.isCalculationSafe: + case StringExpression(text: Interpolation(asPlain: var text?)): assert(!node.hasQuotes); - return switch (node.text.asPlain?.toLowerCase()) { + return switch (text.toLowerCase()) { 'pi' => SassNumber(math.pi), 'e' => SassNumber(math.e), 'infinity' => SassNumber(double.infinity), '-infinity' => SassNumber(double.negativeInfinity), 'nan' => SassNumber(double.nan), - _ => SassString(await _performInterpolation(node.text), quotes: false) + _ => SassString(text, quotes: false) }; + // If there's actual interpolation, create a CalculationInterpolation. + // Otherwise, create an UnquotedString. The main difference is that + // UnquotedStrings don't get extra defensive parentheses. + case StringExpression(): + assert(!node.hasQuotes); + return CalculationInterpolation(await _performInterpolation(node.text)); + case BinaryOperationExpression(:var operator, :var left, :var right): - _checkWhitespaceAroundCalculationOperator(node); return await _addExceptionSpanAsync( node, () async => SassCalculation.operateInternal( - _binaryOperatorToCalculationOperator(operator, node), - await _visitCalculationValue(left, - inLegacySassFunction: inLegacySassFunction), - await _visitCalculationValue(right, - inLegacySassFunction: inLegacySassFunction), - inLegacySassFunction: inLegacySassFunction, + _binaryOperatorToCalculationOperator(operator), + await _visitCalculationValue(left, inMinMax: inMinMax), + await _visitCalculationValue(right, inMinMax: inMinMax), + inMinMax: inMinMax, simplify: !_inSupportsDeclaration)); - case NumberExpression() || - VariableExpression() || - FunctionExpression() || - IfExpression(): + case _: + assert(node is NumberExpression || + node is CalculationExpression || + node is VariableExpression || + node is FunctionExpression || + node is IfExpression); return switch (await node.accept(this)) { SassNumber result => result, SassCalculation result => result, @@ -2605,96 +2417,72 @@ final class _EvaluateVisitor var result => throw _exception( "Value $result can't be used in a calculation.", node.span) }; - - case ListExpression() when node.isCalculationSafe: - _warn( - "Interpolation should only be used in calculations where\n" - "values are allowed. This will be an error in Dart Sass 2.0.0.\n" - "\n" - "More info: https://sass-lang.com/d/calc-interp", - node.contents - .firstWhere((element) => - element is StringExpression && - !element.hasQuotes && - !element.text.isPlain) - .span, - Deprecation.calcInterp); - - // This would produce incorrect error locations if it encountered an - // error, but that shouldn't be possible since anything that's valid - // Sass should also be a valid declaration value. - var parser = ScssParser(node.span.file.getText(0), - url: node.span.sourceUrl, logger: _logger); - parser.scanner.position = node.span.start.offset; - var reparsed = parser.parseInterpolatedDeclarationValue(); - return SassString(await _performInterpolation(reparsed), quotes: false); - - case ListExpression( - hasBrackets: false, - separator: ListSeparator.space, - contents: [ - _, - (UnaryOperationExpression( - operator: UnaryOperator.minus || UnaryOperator.plus - ) || - NumberExpression(value: < 0)) && - var right - ] - ): - // `calc(1 -2)` parses as a space-separated list whose second value is a - // unary operator or a negative number, but just saying it's an invalid - // expression doesn't help the user understand what's going wrong. We - // add special case error handling to help clarify the issue. - throw _exception( - '"+" and "-" must be surrounded by whitespace in calculations.', - right.span.subspan(0, 1)); - - case _: - assert(!node.isCalculationSafe); - throw _exception( - "This expression can't be used in a calculation.", node.span); - } - } - - /// Throws an error if [node] requires whitespace around its operator in a - /// calculation but doesn't have it. - void _checkWhitespaceAroundCalculationOperator( - BinaryOperationExpression node) { - if (node.operator != BinaryOperator.plus && - node.operator != BinaryOperator.minus) { - return; - } - - // We _should_ never be able to violate these conditions since we always - // parse binary operations from a single file, but it's better to be safe - // than have this crash bizarrely. - if (node.left.span.file != node.right.span.file) return; - if (node.left.span.end.offset >= node.right.span.start.offset) return; - - var textBetweenOperands = node.left.span.file - .getText(node.left.span.end.offset, node.right.span.start.offset); - var first = textBetweenOperands.codeUnitAt(0); - var last = textBetweenOperands.codeUnitAt(textBetweenOperands.length - 1); - if (!(first.isWhitespace || first == $slash) || - !(last.isWhitespace || last == $slash)) { - throw _exception( - '"+" and "-" must be surrounded by whitespace in calculations.', - node.operatorSpan); } } /// Returns the [CalculationOperator] that corresponds to [operator]. CalculationOperator _binaryOperatorToCalculationOperator( - BinaryOperator operator, BinaryOperationExpression node) => + BinaryOperator operator) => switch (operator) { BinaryOperator.plus => CalculationOperator.plus, BinaryOperator.minus => CalculationOperator.minus, BinaryOperator.times => CalculationOperator.times, BinaryOperator.dividedBy => CalculationOperator.dividedBy, - _ => throw _exception( - "This operation can't be used in a calculation.", node.operatorSpan) + _ => throw UnsupportedError("Invalid calculation operator $operator.") }; + Future visitColorExpression(ColorExpression node) async => + node.value; + + Future visitListExpression(ListExpression node) async => SassList( + await mapAsync( + node.contents, (Expression expression) => expression.accept(this)), + node.separator, + brackets: node.hasBrackets); + + Future visitMapExpression(MapExpression node) async { + var map = {}; + var keyNodes = {}; + for (var (key, value) in node.pairs) { + var keyValue = await key.accept(this); + var valueValue = await value.accept(this); + + var oldValue = map[keyValue]; + if (oldValue != null) { + var oldValueSpan = keyNodes[keyValue]?.span; + throw MultiSpanSassRuntimeException( + 'Duplicate key.', + key.span, + 'second key', + {if (oldValueSpan != null) oldValueSpan: 'first key'}, + _stackTrace(key.span)); + } + map[keyValue] = valueValue; + keyNodes[keyValue] = key; + } + return SassMap(map); + } + + Future visitFunctionExpression(FunctionExpression node) async { + var function = _addExceptionSpan( + node, () => _getFunction(node.name, namespace: node.namespace)); + + if (function == null) { + if (node.namespace != null) { + throw _exception("Undefined function.", node.span); + } + + function = PlainCssCallable(node.originalName); + } + + var oldInFunction = _inFunction; + _inFunction = true; + var result = await _addErrorSpan( + node, () => _runFunctionCallable(node.arguments, function, node)); + _inFunction = oldInFunction; + return result; + } + Future visitInterpolatedFunctionExpression( InterpolatedFunctionExpression node) async { var function = PlainCssCallable(await _performInterpolation(node.name)); @@ -2706,6 +2494,14 @@ final class _EvaluateVisitor return result; } + /// Like `_environment.getFunction`, but also returns built-in + /// globally-available functions. + AsyncCallable? _getFunction(String name, {String? namespace}) { + var local = _environment.getFunction(name, namespace: namespace); + if (local != null || namespace != null) return local; + return _builtInFunctions[name]; + } + /// Evaluates the arguments in [arguments] as applied to [callable], and /// invokes [run] in a scope with those arguments defined. Future _runUserDefinedCallable( diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 2b4a6f999..a8639f4e6 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: 7dbf689b46c5413791593fe03b3b9e0250af71c2 +// Checksum: 6eb7f76735562eba91e9460af796b269b3b0aaf7 // // ignore_for_file: unused_import @@ -47,10 +47,8 @@ import '../logger.dart'; import '../module.dart'; import '../module/built_in.dart'; import '../parse/keyframe_selector.dart'; -import '../parse/scss.dart'; import '../syntax.dart'; import '../utils.dart'; -import '../util/character.dart'; import '../util/map.dart'; import '../util/multi_span.dart'; import '../util/nullable.dart'; @@ -440,14 +438,10 @@ final class _EvaluateVisitor return SassFunction(PlainCssCallable(name.text)); } - var callable = _addExceptionSpan(_callableNode!, () { - var normalizedName = name.text.replaceAll("_", "-"); - var namespace = module?.text; - var local = - _environment.getFunction(normalizedName, namespace: namespace); - if (local != null || namespace != null) return local; - return _builtInFunctions[normalizedName]; - }); + var callable = _addExceptionSpan( + _callableNode!, + () => _getFunction(name.text.replaceAll("_", "-"), + namespace: module?.text)); if (callable == null) throw "Function not found: $name"; return SassFunction(callable); @@ -2176,13 +2170,6 @@ final class _EvaluateVisitor // ## Expressions Value visitBinaryOperationExpression(BinaryOperationExpression node) { - if (_stylesheet.plainCss && - node.operator != BinaryOperator.singleEquals && - node.operator != BinaryOperator.dividedBy) { - throw _exception( - "Operators aren't allowed in plain CSS.", node.operatorSpan); - } - return _addExceptionSpan(node, () { var left = node.left.accept(this); return switch (node.operator) { @@ -2213,10 +2200,7 @@ final class _EvaluateVisitor Value _slash(Value left, Value right, BinaryOperationExpression node) { var result = left.dividedBy(right); switch ((left, right)) { - case (SassNumber left, SassNumber right) - when node.allowsSlash && - _operandAllowsSlash(node.left) && - _operandAllowsSlash(node.right): + case (SassNumber left, SassNumber right) when node.allowsSlash: return (result as SassNumber).withSlash(left, right); case (SassNumber(), SassNumber()): @@ -2249,20 +2233,6 @@ final class _EvaluateVisitor } } - /// Returns whether [node] can be used as a component of a slash-separated - /// number. - /// - /// Although this logic is mostly resolved at parse-time, we can't tell - /// whether operands will be evaluated as calculations until evaluation-time. - bool _operandAllowsSlash(Expression node) => - node is! FunctionExpression || - (node.namespace == null && - const { - "calc", "clamp", "hypot", "sin", "cos", "tan", "asin", "acos", // - "atan", "sqrt", "exp", "sign", "mod", "rem", "atan2", "pow", "log" - }.contains(node.name.toLowerCase()) && - _environment.getFunction(node.name) == null); - Value visitValueExpression(ValueExpression node) => node.value; Value visitVariableExpression(VariableExpression node) { @@ -2306,140 +2276,23 @@ final class _EvaluateVisitor SassNumber(node.value, node.unit); Value visitParenthesizedExpression(ParenthesizedExpression node) => - _stylesheet.plainCss - ? throw _exception( - "Parentheses aren't allowed in plain CSS.", node.span) - : node.expression.accept(this); - - SassColor visitColorExpression(ColorExpression node) => node.value; - - SassList visitListExpression(ListExpression node) => SassList( - node.contents.map((Expression expression) => expression.accept(this)), - node.separator, - brackets: node.hasBrackets); - - SassMap visitMapExpression(MapExpression node) { - var map = {}; - var keyNodes = {}; - for (var (key, value) in node.pairs) { - var keyValue = key.accept(this); - var valueValue = value.accept(this); - - if (map.containsKey(keyValue)) { - var oldValueSpan = keyNodes[keyValue]?.span; - throw MultiSpanSassRuntimeException( - 'Duplicate key.', - key.span, - 'second key', - {if (oldValueSpan != null) oldValueSpan: 'first key'}, - _stackTrace(key.span)); - } - map[keyValue] = valueValue; - keyNodes[keyValue] = key; - } - return SassMap(map); - } + node.expression.accept(this); - Value visitFunctionExpression(FunctionExpression node) { - var function = _stylesheet.plainCss - ? null - : _addExceptionSpan( - node, - () => - _environment.getFunction(node.name, namespace: node.namespace)); - if (function == null) { - if (node.namespace != null) { - throw _exception("Undefined function.", node.span); - } - - switch (node.name.toLowerCase()) { - case "min" || "max" || "round" || "abs" - when node.arguments.named.isEmpty && - node.arguments.rest == null && - node.arguments.positional - .every((argument) => argument.isCalculationSafe): - return _visitCalculation(node, inLegacySassFunction: true); - - case "calc" || - "clamp" || - "hypot" || - "sin" || - "cos" || - "tan" || - "asin" || - "acos" || - "atan" || - "sqrt" || - "exp" || - "sign" || - "mod" || - "rem" || - "atan2" || - "pow" || - "log": - return _visitCalculation(node); - } - - function = (_stylesheet.plainCss ? null : _builtInFunctions[node.name]) ?? - PlainCssCallable(node.originalName); - } - - var oldInFunction = _inFunction; - _inFunction = true; - var result = _addErrorSpan( - node, () => _runFunctionCallable(node.arguments, function, node)); - _inFunction = oldInFunction; - return result; - } - - Value _visitCalculation(FunctionExpression node, - {bool inLegacySassFunction = false}) { - if (node.arguments.named.isNotEmpty) { - throw _exception( - "Keyword arguments can't be used with calculations.", node.span); - } else if (node.arguments.rest != null) { - throw _exception( - "Rest arguments can't be used with calculations.", node.span); - } - - _checkCalculationArguments(node); + Value visitCalculationExpression(CalculationExpression node) { var arguments = [ - for (var argument in node.arguments.positional) + for (var argument in node.arguments) _visitCalculationValue(argument, - inLegacySassFunction: inLegacySassFunction) + inMinMax: node.name == 'min' || node.name == 'max') ]; if (_inSupportsDeclaration) { return SassCalculation.unsimplified(node.name, arguments); } try { - return switch (node.name.toLowerCase()) { + return switch (node.name) { "calc" => SassCalculation.calc(arguments[0]), - "sqrt" => SassCalculation.sqrt(arguments[0]), - "sin" => SassCalculation.sin(arguments[0]), - "cos" => SassCalculation.cos(arguments[0]), - "tan" => SassCalculation.tan(arguments[0]), - "asin" => SassCalculation.asin(arguments[0]), - "acos" => SassCalculation.acos(arguments[0]), - "atan" => SassCalculation.atan(arguments[0]), - "abs" => SassCalculation.abs(arguments[0]), - "exp" => SassCalculation.exp(arguments[0]), - "sign" => SassCalculation.sign(arguments[0]), "min" => SassCalculation.min(arguments), "max" => SassCalculation.max(arguments), - "hypot" => SassCalculation.hypot(arguments), - "pow" => - SassCalculation.pow(arguments[0], arguments.elementAtOrNull(1)), - "atan2" => - SassCalculation.atan2(arguments[0], arguments.elementAtOrNull(1)), - "log" => - SassCalculation.log(arguments[0], arguments.elementAtOrNull(1)), - "mod" => - SassCalculation.mod(arguments[0], arguments.elementAtOrNull(1)), - "rem" => - SassCalculation.rem(arguments[0], arguments.elementAtOrNull(1)), - "round" => SassCalculation.round(arguments[0], - arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)), "clamp" => SassCalculation.clamp(arguments[0], arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)), _ => throw UnsupportedError('Unknown calculation name "${node.name}".') @@ -2448,54 +2301,11 @@ final class _EvaluateVisitor // The simplification logic in the [SassCalculation] static methods will // throw an error if the arguments aren't compatible, but we have access // to the original spans so we can throw a more informative error. - if (error.message.contains("compatible")) { - _verifyCompatibleNumbers(arguments, node.arguments.positional); - } + _verifyCompatibleNumbers(arguments, node.arguments); throwWithTrace(_exception(error.message, node.span), error, stackTrace); } } - /// Verifies that the calculation [node] has the correct number of arguments. - void _checkCalculationArguments(FunctionExpression node) { - void check([int? maxArgs]) { - if (node.arguments.positional.isEmpty) { - throw _exception("Missing argument.", node.span); - } else if (maxArgs != null && - node.arguments.positional.length > maxArgs) { - throw _exception( - "Only $maxArgs ${pluralize('argument', maxArgs)} allowed, but " - "${node.arguments.positional.length} " + - pluralize('was', node.arguments.positional.length, - plural: 'were') + - " passed.", - node.span); - } - } - - switch (node.name.toLowerCase()) { - case "calc" || - "sqrt" || - "sin" || - "cos" || - "tan" || - "asin" || - "acos" || - "atan" || - "abs" || - "exp" || - "sign": - check(1); - case "min" || "max" || "hypot": - check(); - case "pow" || "atan2" || "log" || "mod" || "rem": - check(2); - case "round" || "clamp": - check(3); - case _: - throw UnsupportedError('Unknown calculation name "${node.name}".'); - } - } - /// Verifies that [args] all have compatible units that can be used for CSS /// calculations, and throws a [SassException] if not. /// @@ -2533,51 +2343,54 @@ final class _EvaluateVisitor /// Evaluates [node] as a component of a calculation. /// - /// If [inLegacySassFunction] is `true`, this allows unitless numbers to be added and + /// If [inMinMax] is `true`, this allows unitless numbers to be added and /// subtracted with numbers with units, for backwards-compatibility with the - /// old global `min()`, `max()`, `round()`, and `abs()` functions. - Object _visitCalculationValue(Expression node, - {required bool inLegacySassFunction}) { + /// old global `min()` and `max()` functions. + Object _visitCalculationValue(Expression node, {required bool inMinMax}) { switch (node) { case ParenthesizedExpression(expression: var inner): - var result = _visitCalculationValue(inner, - inLegacySassFunction: inLegacySassFunction); - return result is SassString && - !result.hasQuotes && - (startsWithIgnoreCase(result.text, 'var(') || - (inner is StringExpression && !inner.text.isPlain) || - inner is ListExpression) + var result = _visitCalculationValue(inner, inMinMax: inMinMax); + return inner is FunctionExpression && + inner.name.toLowerCase() == 'var' && + result is SassString && + !result.hasQuotes ? SassString('(${result.text})', quotes: false) : result; - case StringExpression() when node.isCalculationSafe: + case StringExpression(text: Interpolation(asPlain: var text?)): assert(!node.hasQuotes); - return switch (node.text.asPlain?.toLowerCase()) { + return switch (text.toLowerCase()) { 'pi' => SassNumber(math.pi), 'e' => SassNumber(math.e), 'infinity' => SassNumber(double.infinity), '-infinity' => SassNumber(double.negativeInfinity), 'nan' => SassNumber(double.nan), - _ => SassString(_performInterpolation(node.text), quotes: false) + _ => SassString(text, quotes: false) }; + // If there's actual interpolation, create a CalculationInterpolation. + // Otherwise, create an UnquotedString. The main difference is that + // UnquotedStrings don't get extra defensive parentheses. + case StringExpression(): + assert(!node.hasQuotes); + return CalculationInterpolation(_performInterpolation(node.text)); + case BinaryOperationExpression(:var operator, :var left, :var right): - _checkWhitespaceAroundCalculationOperator(node); return _addExceptionSpan( node, () => SassCalculation.operateInternal( - _binaryOperatorToCalculationOperator(operator, node), - _visitCalculationValue(left, - inLegacySassFunction: inLegacySassFunction), - _visitCalculationValue(right, - inLegacySassFunction: inLegacySassFunction), - inLegacySassFunction: inLegacySassFunction, + _binaryOperatorToCalculationOperator(operator), + _visitCalculationValue(left, inMinMax: inMinMax), + _visitCalculationValue(right, inMinMax: inMinMax), + inMinMax: inMinMax, simplify: !_inSupportsDeclaration)); - case NumberExpression() || - VariableExpression() || - FunctionExpression() || - IfExpression(): + case _: + assert(node is NumberExpression || + node is CalculationExpression || + node is VariableExpression || + node is FunctionExpression || + node is IfExpression); return switch (node.accept(this)) { SassNumber result => result, SassCalculation result => result, @@ -2585,96 +2398,70 @@ final class _EvaluateVisitor var result => throw _exception( "Value $result can't be used in a calculation.", node.span) }; - - case ListExpression() when node.isCalculationSafe: - _warn( - "Interpolation should only be used in calculations where\n" - "values are allowed. This will be an error in Dart Sass 2.0.0.\n" - "\n" - "More info: https://sass-lang.com/d/calc-interp", - node.contents - .firstWhere((element) => - element is StringExpression && - !element.hasQuotes && - !element.text.isPlain) - .span, - Deprecation.calcInterp); - - // This would produce incorrect error locations if it encountered an - // error, but that shouldn't be possible since anything that's valid - // Sass should also be a valid declaration value. - var parser = ScssParser(node.span.file.getText(0), - url: node.span.sourceUrl, logger: _logger); - parser.scanner.position = node.span.start.offset; - var reparsed = parser.parseInterpolatedDeclarationValue(); - return SassString(_performInterpolation(reparsed), quotes: false); - - case ListExpression( - hasBrackets: false, - separator: ListSeparator.space, - contents: [ - _, - (UnaryOperationExpression( - operator: UnaryOperator.minus || UnaryOperator.plus - ) || - NumberExpression(value: < 0)) && - var right - ] - ): - // `calc(1 -2)` parses as a space-separated list whose second value is a - // unary operator or a negative number, but just saying it's an invalid - // expression doesn't help the user understand what's going wrong. We - // add special case error handling to help clarify the issue. - throw _exception( - '"+" and "-" must be surrounded by whitespace in calculations.', - right.span.subspan(0, 1)); - - case _: - assert(!node.isCalculationSafe); - throw _exception( - "This expression can't be used in a calculation.", node.span); - } - } - - /// Throws an error if [node] requires whitespace around its operator in a - /// calculation but doesn't have it. - void _checkWhitespaceAroundCalculationOperator( - BinaryOperationExpression node) { - if (node.operator != BinaryOperator.plus && - node.operator != BinaryOperator.minus) { - return; - } - - // We _should_ never be able to violate these conditions since we always - // parse binary operations from a single file, but it's better to be safe - // than have this crash bizarrely. - if (node.left.span.file != node.right.span.file) return; - if (node.left.span.end.offset >= node.right.span.start.offset) return; - - var textBetweenOperands = node.left.span.file - .getText(node.left.span.end.offset, node.right.span.start.offset); - var first = textBetweenOperands.codeUnitAt(0); - var last = textBetweenOperands.codeUnitAt(textBetweenOperands.length - 1); - if (!(first.isWhitespace || first == $slash) || - !(last.isWhitespace || last == $slash)) { - throw _exception( - '"+" and "-" must be surrounded by whitespace in calculations.', - node.operatorSpan); } } /// Returns the [CalculationOperator] that corresponds to [operator]. CalculationOperator _binaryOperatorToCalculationOperator( - BinaryOperator operator, BinaryOperationExpression node) => + BinaryOperator operator) => switch (operator) { BinaryOperator.plus => CalculationOperator.plus, BinaryOperator.minus => CalculationOperator.minus, BinaryOperator.times => CalculationOperator.times, BinaryOperator.dividedBy => CalculationOperator.dividedBy, - _ => throw _exception( - "This operation can't be used in a calculation.", node.operatorSpan) + _ => throw UnsupportedError("Invalid calculation operator $operator.") }; + SassColor visitColorExpression(ColorExpression node) => node.value; + + SassList visitListExpression(ListExpression node) => SassList( + node.contents.map((Expression expression) => expression.accept(this)), + node.separator, + brackets: node.hasBrackets); + + SassMap visitMapExpression(MapExpression node) { + var map = {}; + var keyNodes = {}; + for (var (key, value) in node.pairs) { + var keyValue = key.accept(this); + var valueValue = value.accept(this); + + var oldValue = map[keyValue]; + if (oldValue != null) { + var oldValueSpan = keyNodes[keyValue]?.span; + throw MultiSpanSassRuntimeException( + 'Duplicate key.', + key.span, + 'second key', + {if (oldValueSpan != null) oldValueSpan: 'first key'}, + _stackTrace(key.span)); + } + map[keyValue] = valueValue; + keyNodes[keyValue] = key; + } + return SassMap(map); + } + + Value visitFunctionExpression(FunctionExpression node) { + var function = _addExceptionSpan( + node, () => _getFunction(node.name, namespace: node.namespace)); + + if (function == null) { + if (node.namespace != null) { + throw _exception("Undefined function.", node.span); + } + + function = PlainCssCallable(node.originalName); + } + + var oldInFunction = _inFunction; + _inFunction = true; + var result = _addErrorSpan( + node, () => _runFunctionCallable(node.arguments, function, node)); + _inFunction = oldInFunction; + return result; + } + Value visitInterpolatedFunctionExpression( InterpolatedFunctionExpression node) { var function = PlainCssCallable(_performInterpolation(node.name)); @@ -2686,6 +2473,14 @@ final class _EvaluateVisitor return result; } + /// Like `_environment.getFunction`, but also returns built-in + /// globally-available functions. + Callable? _getFunction(String name, {String? namespace}) { + var local = _environment.getFunction(name, namespace: namespace); + if (local != null || namespace != null) return local; + return _builtInFunctions[name]; + } + /// Evaluates the arguments in [arguments] as applied to [callable], and /// invokes [run] in a scope with those arguments defined. V _runUserDefinedCallable( diff --git a/lib/src/visitor/expression_to_calc.dart b/lib/src/visitor/expression_to_calc.dart index aca554355..961735655 100644 --- a/lib/src/visitor/expression_to_calc.dart +++ b/lib/src/visitor/expression_to_calc.dart @@ -10,13 +10,9 @@ import 'replace_expression.dart'; /// This assumes that [expression] already returns a number. It's intended for /// use in end-user messaging, and may not produce directly evaluable /// expressions. -FunctionExpression expressionToCalc(Expression expression) => - FunctionExpression( - "calc", - ArgumentInvocation( - [expression.accept(const _MakeExpressionCalculationSafe())], - const {}, - expression.span), +CalculationExpression expressionToCalc(Expression expression) => + CalculationExpression.calc( + expression.accept(const _MakeExpressionCalculationSafe()), expression.span); /// A visitor that replaces constructs that can't be used in a calculation with @@ -24,6 +20,8 @@ FunctionExpression expressionToCalc(Expression expression) => class _MakeExpressionCalculationSafe with ReplaceExpressionVisitor { const _MakeExpressionCalculationSafe(); + Expression visitCalculationExpression(CalculationExpression node) => node; + Expression visitBinaryOperationExpression(BinaryOperationExpression node) => node .operator == BinaryOperator.modulo diff --git a/lib/src/visitor/interface/expression.dart b/lib/src/visitor/interface/expression.dart index 7d40c87a2..db5f70f32 100644 --- a/lib/src/visitor/interface/expression.dart +++ b/lib/src/visitor/interface/expression.dart @@ -12,6 +12,7 @@ import '../../ast/sass.dart'; abstract interface class ExpressionVisitor { T visitBinaryOperationExpression(BinaryOperationExpression node); T visitBooleanExpression(BooleanExpression node); + T visitCalculationExpression(CalculationExpression node); T visitColorExpression(ColorExpression node); T visitInterpolatedFunctionExpression(InterpolatedFunctionExpression node); T visitFunctionExpression(FunctionExpression node); diff --git a/lib/src/visitor/recursive_ast.dart b/lib/src/visitor/recursive_ast.dart index 290572697..0b31aafe2 100644 --- a/lib/src/visitor/recursive_ast.dart +++ b/lib/src/visitor/recursive_ast.dart @@ -33,6 +33,12 @@ mixin RecursiveAstVisitor on RecursiveStatementVisitor super.visitAtRule(node); } + void visitCalculationExpression(CalculationExpression node) { + for (var argument in node.arguments) { + argument.accept(this); + } + } + void visitContentRule(ContentRule node) { visitArgumentInvocation(node.arguments); } @@ -104,6 +110,8 @@ mixin RecursiveAstVisitor on RecursiveStatementVisitor super.visitMediaRule(node); } + void visitMixinRule(MixinRule node) => visitCallableDeclaration(node); + void visitReturnRule(ReturnRule node) { visitExpression(node.expression); } @@ -120,7 +128,7 @@ mixin RecursiveAstVisitor on RecursiveStatementVisitor void visitUseRule(UseRule node) { for (var variable in node.configuration) { - visitExpression(variable.expression); + variable.expression.accept(this); } } @@ -152,7 +160,7 @@ mixin RecursiveAstVisitor on RecursiveStatementVisitor void visitForwardRule(ForwardRule node) { for (var variable in node.configuration) { - visitExpression(variable.expression); + variable.expression.accept(this); } } diff --git a/lib/src/visitor/replace_expression.dart b/lib/src/visitor/replace_expression.dart index 43d93eebc..b330cfbbf 100644 --- a/lib/src/visitor/replace_expression.dart +++ b/lib/src/visitor/replace_expression.dart @@ -22,6 +22,10 @@ import 'interface/expression.dart'; /// /// {@category Visitor} mixin ReplaceExpressionVisitor implements ExpressionVisitor { + Expression visitCalculationExpression(CalculationExpression node) => + CalculationExpression(node.name, + node.arguments.map((argument) => argument.accept(this)), node.span); + Expression visitBinaryOperationExpression(BinaryOperationExpression node) => BinaryOperationExpression( node.operator, node.left.accept(this), node.right.accept(this)); diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index f86d6c7fb..c0c071155 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -518,9 +518,13 @@ final class _SerializeVisitor case Value(): value.accept(this); + case CalculationInterpolation(): + _buffer.write(value.value); + case CalculationOperation(:var operator, :var left, :var right): - var parenthesizeLeft = left is CalculationOperation && - left.operator.precedence < operator.precedence; + var parenthesizeLeft = left is CalculationInterpolation || + (left is CalculationOperation && + left.operator.precedence < operator.precedence); if (parenthesizeLeft) _buffer.writeCharCode($lparen); _writeCalculationValue(left); if (parenthesizeLeft) _buffer.writeCharCode($rparen); @@ -530,7 +534,8 @@ final class _SerializeVisitor _buffer.write(operator.operator); if (operatorWhitespace) _buffer.writeCharCode($space); - var parenthesizeRight = (right is CalculationOperation && + var parenthesizeRight = right is CalculationInterpolation || + (right is CalculationOperation && _parenthesizeCalculationRhs(operator, right.operator)) || (operator == CalculationOperator.dividedBy && right is SassNumber && diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index d42f28444..1ba7b431c 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,11 +1,3 @@ -## 9.0.0 - -* Remove the `CalculationExpression` class and the associated visitor methods. - -* Add an `AstSearchVisitor` helper class. - -* Add an `Interpolation.isPlain` getter. - ## 8.2.1 * No user-visible changes. diff --git a/pkg/sass_api/lib/sass_api.dart b/pkg/sass_api/lib/sass_api.dart index b0369f908..1f4b076e3 100644 --- a/pkg/sass_api/lib/sass_api.dart +++ b/pkg/sass_api/lib/sass_api.dart @@ -23,7 +23,6 @@ export 'package:sass/src/visitor/find_dependencies.dart'; export 'package:sass/src/visitor/interface/expression.dart'; export 'package:sass/src/visitor/interface/selector.dart'; export 'package:sass/src/visitor/interface/statement.dart'; -export 'package:sass/src/visitor/ast_search.dart'; export 'package:sass/src/visitor/recursive_ast.dart'; export 'package:sass/src/visitor/recursive_selector.dart'; export 'package:sass/src/visitor/recursive_statement.dart'; diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 0528454f0..21f333de9 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: 9.0.0 +version: 8.2.1 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - sass: 1.67.0 + sass: 1.66.1 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index e9cceabb1..5c2a1698b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.67.0 +version: 1.67.0-dev description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass diff --git a/test/dart_api/value/calculation_test.dart b/test/dart_api/value/calculation_test.dart index ed284db04..594842cee 100644 --- a/test/dart_api/value/calculation_test.dart +++ b/test/dart_api/value/calculation_test.dart @@ -69,12 +69,5 @@ void main() { .assertNumber(), equals(SassNumber(8.5))); }); - - test('interpolation', () { - var result = SassCalculation.calc(CalculationInterpolation('1 + 2')) - .assertCalculation(); - expect(result.name, equals('calc')); - expect(result.arguments[0], equals(SassString('(1 + 2)'))); - }); }); } diff --git a/test/embedded/function_test.dart b/test/embedded/function_test.dart index d927a780f..af4bd62db 100644 --- a/test/embedded/function_test.dart +++ b/test/embedded/function_test.dart @@ -761,7 +761,7 @@ void main() { equals(Value_Calculation() ..name = "calc" ..arguments.add(Value_Calculation_CalculationValue() - ..string = "var(--foo)"))); + ..interpolation = "var(--foo)"))); }); test("with number arguments", () async { @@ -1429,7 +1429,7 @@ void main() { ..name = "calc" ..arguments.add(Value_Calculation_CalculationValue() ..interpolation = "var(--foo)"))), - "calc((var(--foo)))"); + "calc(var(--foo))"); }); test("with number arguments", () async {