diff --git a/CHANGELOG.md b/CHANGELOG.md index 132a4eba4..f70edd917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ ## 1.55.0 +* **Potentially breaking bug fix:** Sass numbers are now universally stored as + 64-bit floating-point numbers, rather than sometimes being stored as integers. + This will generally make arithmetic with very large numbers more reliable and + more consistent across platforms, but it does mean that numbers between nine + quadrillion and nine quintillion will no longer be represented with full + accuracy when compiling Sass on the Dart VM. + +* **Potentially breaking bug fix:** Sass equality is now properly [transitive]. + Two numbers are now considered equal (after doing unit conversions) if they + round to the same `1e-11`th. Previously, numbers were considered equal if they + were within `1e-11` of one another, which led to some circumstances where `$a + == $b` and `$b == $c` but `$a != $b`. + +[transitive]: https://en.wikipedia.org/wiki/Transitive_property + +* **Potentially breaking bug fix:** Various functions in `sass:math` no longer + treat floating-point numbers that are very close (but not identical) to + integers as integers. Instead, these functions now follow the floating-point + specification exactly. For example, `math.pow(0.000000000001, -1)` now returns + `1000000000000` instead of `Infinity`. + * Emit a deprecation warning for `$a -$b` and `$a +$b`, since these look like they could be unary operations but they're actually parsed as binary operations. Either explicitly write `$a - $b` or `$a (-$b)`. See @@ -10,6 +31,10 @@ * Add an optional `argumentName` parameter to `SassScriptException()` to make it easier to throw exceptions associated with particular argument names. +* Most APIs that previously returned `num` now return `double`. All APIs + continue to _accept_ `num`, although in Dart 2.0.0 these APIs will be changed + to accept only `double`. + ### JS API * Fix a bug in which certain warning spans would not have their properties diff --git a/lib/src/ast/sass/expression/number.dart b/lib/src/ast/sass/expression/number.dart index 3467a4af7..ad1f1ed1e 100644 --- a/lib/src/ast/sass/expression/number.dart +++ b/lib/src/ast/sass/expression/number.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; import '../../../visitor/interface/expression.dart'; +import '../../../value/number.dart'; import '../expression.dart'; /// A number literal. @@ -14,7 +15,7 @@ import '../expression.dart'; @sealed class NumberExpression implements Expression { /// The numeric value. - final num value; + final double value; /// The number's unit, or `null`. final String? unit; @@ -26,5 +27,5 @@ class NumberExpression implements Expression { T accept(ExpressionVisitor visitor) => visitor.visitNumberExpression(this); - String toString() => "$value${unit ?? ''}"; + String toString() => SassNumber(value, unit).toString(); } diff --git a/lib/src/exception.dart b/lib/src/exception.dart index 05f6941a9..0c4c20701 100644 --- a/lib/src/exception.dart +++ b/lib/src/exception.dart @@ -171,7 +171,7 @@ class SassScriptException { /// triggered this exception. If it's not null, it's automatically included in /// [message]. SassScriptException(String message, [String? argumentName]) - : message = argumentName == null ? message : "\$$argumentName: $message"; + : message = argumentName == null ? message : "\$$argumentName: $message"; String toString() => "$message\n\nBUG: This should include a source span!"; } diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index 64e7d35c3..6f0c03a5c 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -10,8 +10,8 @@ import '../callable.dart'; import '../evaluation_context.dart'; import '../exception.dart'; import '../module/built_in.dart'; -import '../util/number.dart'; import '../util/nullable.dart'; +import '../util/number.dart'; import '../utils.dart'; import '../value.dart'; @@ -452,7 +452,7 @@ SassColor _updateComponents(List arguments, /// /// [max] should be 255 for RGB channels, 1 for the alpha channel, and 100 /// for saturation, lightness, whiteness, and blackness. - num? getParam(String name, num max, + double? getParam(String name, num max, {bool checkPercent = false, bool assertPercent = false}) { var number = keywords.remove(name)?.assertNumber(name); if (number == null) return null; @@ -500,15 +500,15 @@ SassColor _updateComponents(List arguments, } /// Updates [current] based on [param], clamped within [max]. - num updateValue(num current, num? param, num max) { + double updateValue(double current, double? param, num max) { if (param == null) return current; if (change) return param; - if (adjust) return (current + param).clamp(0, max); + if (adjust) return (current + param).clamp(0, max).toDouble(); return current + (param > 0 ? max - current : current) * (param / 100); } - int updateRgb(int current, num? param) => - fuzzyRound(updateValue(current, param, 255)); + int updateRgb(int current, double? param) => + fuzzyRound(updateValue(current.toDouble(), param, 255)); if (hasRgb) { return color.changeRgb( @@ -789,8 +789,8 @@ bool _isVarSlash(Value value) => /// within `0` and [max]. Otherwise, this throws a [SassScriptException]. /// /// [name] is used to identify the argument in the error message. -num _percentageOrUnitless(SassNumber number, num max, String name) { - num value; +double _percentageOrUnitless(SassNumber number, num max, String name) { + double value; if (!number.hasUnits) { value = number.value; } else if (number.hasUnit("%")) { @@ -800,7 +800,7 @@ num _percentageOrUnitless(SassNumber number, num max, String name) { '\$$name: Expected $number to have no units or "%".'); } - return value.clamp(0, max); + return value.clamp(0, max).toDouble(); } /// Returns [color1] and [color2], mixed together and weighted by [weight]. diff --git a/lib/src/functions/math.dart b/lib/src/functions/math.dart index 44f1948ab..d0b0f151c 100644 --- a/lib/src/functions/math.dart +++ b/lib/src/functions/math.dart @@ -11,7 +11,6 @@ import '../callable.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. @@ -30,13 +29,18 @@ final module = BuiltInModule("math", functions: [ ], variables: { "e": SassNumber(math.e), "pi": SassNumber(math.pi), + "epsilon": SassNumber(2.220446049250313e-16), + "max-safe-integer": SassNumber(9007199254740991), + "min-safe-integer": SassNumber(-9007199254740991), + "max-number": SassNumber(double.maxFinite), + "min-number": SassNumber(double.minPositive), }); /// /// Bounding functions /// -final _ceil = _numberFunction("ceil", (value) => value.ceil()); +final _ceil = _numberFunction("ceil", (value) => value.ceil().toDouble()); final _clamp = _function("clamp", r"$min, $number, $max", (arguments) { var min = arguments[0].assertNumber("min"); @@ -55,7 +59,7 @@ final _clamp = _function("clamp", r"$min, $number, $max", (arguments) { return number; }); -final _floor = _numberFunction("floor", (value) => value.floor()); +final _floor = _numberFunction("floor", (value) => value.floor().toDouble()); final _max = _function("max", r"$numbers...", (arguments) { SassNumber? max; @@ -77,7 +81,7 @@ final _min = _function("min", r"$numbers...", (arguments) { throw SassScriptException("At least one argument must be passed."); }); -final _round = _numberFunction("round", fuzzyRound); +final _round = _numberFunction("round", (number) => number.round().toDouble()); /// /// Distance functions @@ -112,20 +116,16 @@ final _log = _function("log", r"$number, $base: null", (arguments) { var number = arguments[0].assertNumber("number"); if (number.hasUnits) { throw SassScriptException("\$number: Expected $number to have no units."); + } else if (arguments[1] == sassNull) { + return SassNumber(math.log(number.value)); } - var numberValue = _fuzzyRoundIfZero(number.value); - if (arguments[1] == sassNull) return SassNumber(math.log(numberValue)); - var base = arguments[1].assertNumber("base"); if (base.hasUnits) { throw SassScriptException("\$base: Expected $base to have no units."); + } else { + return SassNumber(math.log(number.value) / math.log(base.value)); } - - var baseValue = fuzzyEquals(base.value, 1) - ? fuzzyRound(base.value) - : _fuzzyRoundIfZero(base.value); - return SassNumber(math.log(numberValue) / math.log(baseValue)); }); final _pow = _function("pow", r"$base, $exponent", (arguments) { @@ -136,45 +136,18 @@ final _pow = _function("pow", r"$base, $exponent", (arguments) { } else if (exponent.hasUnits) { throw SassScriptException( "\$exponent: Expected $exponent to have no units."); + } else { + return SassNumber(math.pow(base.value, exponent.value)); } - - // Exponentiating certain real numbers leads to special behaviors. Ensure that - // these behaviors are consistent for numbers within the precision limit. - var baseValue = _fuzzyRoundIfZero(base.value); - var exponentValue = _fuzzyRoundIfZero(exponent.value); - if (fuzzyEquals(baseValue.abs(), 1) && exponentValue.isInfinite) { - return SassNumber(double.nan); - } else if (fuzzyEquals(baseValue, 0)) { - if (exponentValue.isFinite) { - var intExponent = fuzzyAsInt(exponentValue); - if (intExponent != null && intExponent % 2 == 1) { - exponentValue = fuzzyRound(exponentValue); - } - } - } else if (baseValue.isFinite && - fuzzyLessThan(baseValue, 0) && - exponentValue.isFinite && - fuzzyIsInt(exponentValue)) { - exponentValue = fuzzyRound(exponentValue); - } else if (baseValue.isInfinite && - fuzzyLessThan(baseValue, 0) && - exponentValue.isFinite) { - var intExponent = fuzzyAsInt(exponentValue); - if (intExponent != null && intExponent % 2 == 1) { - exponentValue = fuzzyRound(exponentValue); - } - } - return SassNumber(math.pow(baseValue, exponentValue)); }); 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)); } - - var numberValue = _fuzzyRoundIfZero(number.value); - return SassNumber(math.sqrt(numberValue)); }); /// @@ -185,75 +158,60 @@ 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']); } - - var numberValue = fuzzyEquals(number.value.abs(), 1) - ? fuzzyRound(number.value) - : number.value; - var acos = math.acos(numberValue) * 180 / math.pi; - return SassNumber.withUnits(acos, numeratorUnits: ['deg']); }); 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']); } - - var numberValue = fuzzyEquals(number.value.abs(), 1) - ? fuzzyRound(number.value) - : _fuzzyRoundIfZero(number.value); - var asin = math.asin(numberValue) * 180 / math.pi; - return SassNumber.withUnits(asin, numeratorUnits: ['deg']); }); 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']); } - - var numberValue = _fuzzyRoundIfZero(number.value); - var atan = math.atan(numberValue) * 180 / math.pi; - return SassNumber.withUnits(atan, numeratorUnits: ['deg']); }); final _atan2 = _function("atan2", r"$y, $x", (arguments) { var y = arguments[0].assertNumber("y"); var x = arguments[1].assertNumber("x"); - - var xValue = _fuzzyRoundIfZero(x.convertValueToMatch(y, 'x', 'y')); - var yValue = _fuzzyRoundIfZero(y.value); - var atan2 = math.atan2(yValue, xValue) * 180 / math.pi; - return SassNumber.withUnits(atan2, numeratorUnits: ['deg']); -}); - -final _cos = _function("cos", r"$number", (arguments) { - var value = - arguments[0].assertNumber("number").coerceValueToUnit("rad", "number"); - return SassNumber(math.cos(value)); -}); - -final _sin = _function("sin", r"$number", (arguments) { - var value = _fuzzyRoundIfZero( - arguments[0].assertNumber("number").coerceValueToUnit("rad", "number")); - return SassNumber(math.sin(value)); + return SassNumber.withUnits( + math.atan2(y.value, x.convertValueToMatch(y, 'x', 'y')) * 180 / math.pi, + numeratorUnits: ['deg']); }); -final _tan = _function("tan", r"$number", (arguments) { - var value = - arguments[0].assertNumber("number").coerceValueToUnit("rad", "number"); - var asymptoteInterval = 0.5 * math.pi; - var tanPeriod = 2 * math.pi; - if (fuzzyEquals((value - asymptoteInterval) % tanPeriod, 0)) { - return SassNumber(double.infinity); - } else if (fuzzyEquals((value + asymptoteInterval) % tanPeriod, 0)) { - return SassNumber(double.negativeInfinity); - } else { - var numberValue = _fuzzyRoundIfZero(value); - return SassNumber(math.tan(numberValue)); - } -}); +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 @@ -329,14 +287,9 @@ final _div = _function("div", r"$number1, $number2", (arguments) { /// Helpers /// -num _fuzzyRoundIfZero(num number) { - if (!fuzzyEquals(number, 0)) return number; - return number.isNegative ? -0.0 : 0; -} - /// Returns a [Callable] named [name] that transforms a number's value /// using [transform] and preserves its units. -BuiltInCallable _numberFunction(String name, num transform(num value)) { +BuiltInCallable _numberFunction(String name, double transform(double value)) { return _function(name, r"$number", (arguments) { var number = arguments[0].assertNumber("number"); return SassNumber.withUnits(transform(number.value), diff --git a/lib/src/node/legacy/value/color.dart b/lib/src/node/legacy/value/color.dart index fe5fcbf5c..6aae64426 100644 --- a/lib/src/node/legacy/value/color.dart +++ b/lib/src/node/legacy/value/color.dart @@ -4,6 +4,7 @@ import 'package:js/js.dart'; +import '../../../util/number.dart'; import '../../../value.dart'; import '../../reflection.dart'; @@ -68,4 +69,4 @@ final JSClass legacyColorClass = createJSClass('sass.types.Color', /// Clamps [channel] within the range 0, 255 and rounds it to the nearest /// integer. -int _clamp(num channel) => channel.clamp(0, 255).round(); +int _clamp(num channel) => fuzzyRound(channel.clamp(0, 255)); diff --git a/lib/src/parse/parser.dart b/lib/src/parse/parser.dart index 16f01bd89..58c1e73e1 100644 --- a/lib/src/parse/parser.dart +++ b/lib/src/parse/parser.dart @@ -261,17 +261,18 @@ class Parser { return buffer.toString(); } - /// Consumes and returns a natural number (that is, a non-negative integer). + /// Consumes and returns a natural number (that is, a non-negative integer) as + /// a double. /// /// Doesn't support scientific notation. @protected - int naturalNumber() { + double naturalNumber() { var first = scanner.readChar(); if (!isDigit(first)) { scanner.error("Expected digit.", position: scanner.position - 1); } - var number = asDecimal(first); + var number = asDecimal(first).toDouble(); while (isDigit(scanner.peekChar())) { number *= 10; number += asDecimal(scanner.readChar()); diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index a76bef0e4..c1326af59 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -2,8 +2,6 @@ // 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 'package:path/path.dart' as p; @@ -2412,7 +2410,7 @@ abstract class StylesheetParser extends Parser { int red; int green; int blue; - num? alpha; + double? alpha; if (!isHex(scanner.peekChar())) { // #abc red = (digit1 << 4) + digit1; @@ -2530,16 +2528,19 @@ abstract class StylesheetParser extends Parser { NumberExpression _number() { var start = scanner.state; var first = scanner.peekChar(); - var sign = first == $minus ? -1 : 1; if (first == $plus || first == $minus) scanner.readChar(); - num number = scanner.peekChar() == $dot ? 0 : naturalNumber(); + if (scanner.peekChar() != $dot) _consumeNaturalNumber(); // Don't complain about a dot after a number unless the number starts with a // dot. We don't allow a plain ".", but we need to allow "1." so that // "1..." will work as a rest argument. - number += _tryDecimal(allowTrailingDot: scanner.position != start.position); - number *= _tryExponent(); + _tryDecimal(allowTrailingDot: scanner.position != start.position); + _tryExponent(); + + // Use Dart's built-in double parsing so that we don't accumulate + // floating-point errors for numbers with lots of digits. + var number = double.parse(scanner.substring(start.position)); String? unit; if (scanner.scanChar($percent)) { @@ -2550,21 +2551,32 @@ abstract class StylesheetParser extends Parser { unit = identifier(unit: true); } - return NumberExpression(sign * number, scanner.spanFrom(start), unit: unit); + return NumberExpression(number, scanner.spanFrom(start), unit: unit); } - /// Consumes the decimal component of a number and returns its value, or 0 if - /// there is no decimal component. + /// Consumes a natural number (that is, a non-negative integer). + /// + /// Doesn't support scientific notation. + void _consumeNaturalNumber() { + if (!isDigit(scanner.readChar())) { + scanner.error("Expected digit.", position: scanner.position - 1); + } + + while (isDigit(scanner.peekChar())) { + scanner.readChar(); + } + } + + /// Consumes the decimal component of a number if it exists. /// /// If [allowTrailingDot] is `false`, this will throw an error if there's a /// dot without any numbers following it. Otherwise, it will ignore the dot /// without consuming it. - num _tryDecimal({bool allowTrailingDot = false}) { - var start = scanner.position; - if (scanner.peekChar() != $dot) return 0; + void _tryDecimal({bool allowTrailingDot = false}) { + if (scanner.peekChar() != $dot) return; if (!isDigit(scanner.peekChar(1))) { - if (allowTrailingDot) return 0; + if (allowTrailingDot) return; scanner.error("Expected digit.", position: scanner.position + 1); } @@ -2572,33 +2584,23 @@ abstract class StylesheetParser extends Parser { while (isDigit(scanner.peekChar())) { scanner.readChar(); } - - // Use Dart's built-in double parsing so that we don't accumulate - // floating-point errors for numbers with lots of digits. - return double.parse(scanner.substring(start)); } - /// Consumes the exponent component of a number and returns its value, or 1 if - /// there is no exponent component. - num _tryExponent() { + /// Consumes the exponent component of a number if it exists. + void _tryExponent() { var first = scanner.peekChar(); - if (first != $e && first != $E) return 1; + if (first != $e && first != $E) return; var next = scanner.peekChar(1); - if (!isDigit(next) && next != $minus && next != $plus) return 1; + if (!isDigit(next) && next != $minus && next != $plus) return; scanner.readChar(); - var exponentSign = next == $minus ? -1 : 1; if (next == $plus || next == $minus) scanner.readChar(); if (!isDigit(scanner.peekChar())) scanner.error("Expected digit."); - var exponent = 0.0; while (isDigit(scanner.peekChar())) { - exponent *= 10; - exponent += scanner.readChar() - $0; + scanner.readChar(); } - - return math.pow(10, exponentSign * exponent); } /// Consumes a unicode range expression. diff --git a/lib/src/util/number.dart b/lib/src/util/number.dart index c47ab44da..80fd3aaa2 100644 --- a/lib/src/util/number.dart +++ b/lib/src/util/number.dart @@ -6,22 +6,35 @@ import 'dart:math' as math; import '../value.dart'; -/// The maximum distance two Sass numbers are allowed to be from one another -/// before they're considered different. -final epsilon = math.pow(10, -SassNumber.precision - 1); - -/// Returns whether [number1] and [number2] are equal within [epsilon]. -bool fuzzyEquals(num number1, num number2) => - (number1 - number2).abs() < epsilon; +/// The power of ten to which to round Sass numbers to determine if they're +/// [fuzzy equal] to one another +/// +/// [fuzzy-equal]: https://github.com/sass/sass/blob/main/spec/types/number.md#fuzzy-equality +/// +/// This is also the minimum distance such that `a - b > _epsilon` implies that +/// `a` isn't [fuzzy-equal] to `b`. Note that the inverse implication is not +/// necessarily true! For example, if `a = 5.1e-11` and `b = 4.4e-11`, then +/// `a - b < 1e-11` but `a` fuzzy-equals 5e-11 and b fuzzy-equals 4e-11. +final _epsilon = math.pow(10, -SassNumber.precision - 1); -/// `1 / epsilon`, cached since [math.pow] may not be computed at compile-time +/// `1 / _epsilon`, cached since [math.pow] may not be computed at compile-time /// and thus this probably won't be constant-folded. -final _inverseEpsilon = 1 / epsilon; +final _inverseEpsilon = math.pow(10, SassNumber.precision + 1); + +/// Returns whether [number1] and [number2] are equal up to the 11th decimal +/// digit. +bool fuzzyEquals(num number1, num number2) { + if (number1 == number2) return true; + return (number1 - number2).abs() <= _epsilon && + (number1 * _inverseEpsilon).round() == + (number2 * _inverseEpsilon).round(); +} /// Returns a hash code for [number] that matches [fuzzyEquals]. -int fuzzyHashCode(num number) => number.isInfinite || number.isNaN - ? number.hashCode - : (number * _inverseEpsilon).round().hashCode; +int fuzzyHashCode(double number) { + if (!number.isFinite) return number.hashCode; + return (number * _inverseEpsilon).round().hashCode; +} /// Returns whether [number1] is less than [number2], and not [fuzzyEquals]. bool fuzzyLessThan(num number1, num number2) => @@ -40,21 +53,20 @@ bool fuzzyGreaterThanOrEquals(num number1, num number2) => number1 > number2 || fuzzyEquals(number1, number2); /// Returns whether [number] is [fuzzyEquals] to an integer. -bool fuzzyIsInt(num number) { - // Check this before is int to work around dart-lang/sdk#43325. +bool fuzzyIsInt(double number) { if (number.isInfinite || number.isNaN) return false; - if (number is int) return true; - - // Check against 0.5 rather than 0.0 so that we catch numbers that are both - // very slightly above an integer, and very slightly below. - return fuzzyEquals((number - 0.5).abs() % 1, 0.5); + return fuzzyEquals(number, number.round()); } /// If [number] is an integer according to [fuzzyIsInt], returns it as an /// [int]. /// /// Otherwise, returns `null`. -int? fuzzyAsInt(num number) => fuzzyIsInt(number) ? number.round() : null; +int? fuzzyAsInt(double number) { + if (number.isInfinite || number.isNaN) return null; + var rounded = number.round(); + return fuzzyEquals(number, rounded) ? rounded : null; +} /// Rounds [number] to the nearest integer. /// @@ -75,9 +87,9 @@ int fuzzyRound(num number) { /// /// If [number] is [fuzzyEquals] to [min] or [max], it's clamped to the /// appropriate value. -num? fuzzyCheckRange(num number, num min, num max) { - if (fuzzyEquals(number, min)) return min; - if (fuzzyEquals(number, max)) return max; +double? fuzzyCheckRange(double number, num min, num max) { + if (fuzzyEquals(number, min)) return min.toDouble(); + if (fuzzyEquals(number, max)) return max.toDouble(); if (number > min && number < max) return number; return null; } @@ -86,9 +98,23 @@ num? fuzzyCheckRange(num number, num min, num max) { /// /// If [number] is [fuzzyEquals] to [min] or [max], it's clamped to the /// appropriate value. [name] is used in error reporting. -num fuzzyAssertRange(num number, int min, int max, [String? name]) { +double fuzzyAssertRange(double number, int min, int max, [String? name]) { var result = fuzzyCheckRange(number, min, max); if (result != null) return result; throw RangeError.range( number, min, max, name, "must be between $min and $max"); } + +/// Return [num1] modulo [num2], using Sass's [floored division] modulo +/// semantics, which it inherited from Ruby and which differ from Dart's. +/// +/// [floored division]: https://en.wikipedia.org/wiki/Modulo_operation#Variants_of_the_definition +double moduloLikeSass(double num1, double num2) { + if (num2 > 0) return num1 % num2; + if (num2 == 0) return double.nan; + + // Dart has different mod-negative semantics than Ruby, and thus than + // Sass. + var result = num1 % num2; + return result == 0 ? 0 : result + num2; +} diff --git a/lib/src/value.dart b/lib/src/value.dart index 23197ff43..fef8881c8 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -410,7 +410,8 @@ extension SassApiValue on Value { // TODO(nweiz): colorize this if we're running in an environment where // that works. throwWithTrace( - SassScriptException(error.toString().replaceFirst("Error: ", ""), name), + SassScriptException( + error.toString().replaceFirst("Error: ", ""), name), stackTrace); } } @@ -433,7 +434,8 @@ extension SassApiValue on Value { // TODO(nweiz): colorize this if we're running in an environment where // that works. throwWithTrace( - SassScriptException(error.toString().replaceFirst("Error: ", ""), name), + SassScriptException( + error.toString().replaceFirst("Error: ", ""), name), stackTrace); } } @@ -456,7 +458,8 @@ extension SassApiValue on Value { // TODO(nweiz): colorize this if we're running in an environment where // that works. throwWithTrace( - SassScriptException(error.toString().replaceFirst("Error: ", ""), name), + SassScriptException( + error.toString().replaceFirst("Error: ", ""), name), stackTrace); } } @@ -479,7 +482,8 @@ extension SassApiValue on Value { // TODO(nweiz): colorize this if we're running in an environment where // that works. throwWithTrace( - SassScriptException(error.toString().replaceFirst("Error: ", ""), name), + SassScriptException( + error.toString().replaceFirst("Error: ", ""), name), stackTrace); } } diff --git a/lib/src/value/calculation.dart b/lib/src/value/calculation.dart index 209ad7a8d..d52e5d283 100644 --- a/lib/src/value/calculation.dart +++ b/lib/src/value/calculation.dart @@ -235,7 +235,8 @@ class SassCalculation extends Value { return arg; } else if (arg is SassString) { if (!arg.hasQuotes) return arg; - throw SassScriptException("Quoted string $arg can't be used in a calculation."); + throw SassScriptException( + "Quoted string $arg can't be used in a calculation."); } else if (arg is SassCalculation) { return arg.name == 'calc' ? arg.arguments[0] : arg; } else if (arg is Value) { @@ -255,7 +256,8 @@ class SassCalculation extends Value { for (var arg in args) { if (arg is! SassNumber) continue; if (arg.numeratorUnits.length > 1 || arg.denominatorUnits.isNotEmpty) { - throw SassScriptException("Number $arg isn't compatible with CSS calculations."); + throw SassScriptException( + "Number $arg isn't compatible with CSS calculations."); } } diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index 02e03477c..3d3db20f2 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -42,31 +42,31 @@ class SassColor extends Value { int? _blue; /// This color's hue, between `0` and `360`. - num get hue { + double get hue { if (_hue == null) _rgbToHsl(); return _hue!; } - num? _hue; + double? _hue; /// This color's saturation, a percentage between `0` and `100`. - num get saturation { + double get saturation { if (_saturation == null) _rgbToHsl(); return _saturation!; } - num? _saturation; + double? _saturation; /// This color's lightness, a percentage between `0` and `100`. - num get lightness { + double get lightness { if (_lightness == null) _rgbToHsl(); return _lightness!; } - num? _lightness; + double? _lightness; /// This color's whiteness, a percentage between `0` and `100`. - num get whiteness { + double get whiteness { // Because HWB is (currently) used much less frequently than HSL or RGB, we // don't cache its values because we expect the memory overhead of doing so // to outweigh the cost of recalculating it on access. @@ -74,7 +74,7 @@ class SassColor extends Value { } /// This color's blackness, a percentage between `0` and `100`. - num get blackness { + double get blackness { // Because HWB is (currently) used much less frequently than HSL or RGB, we // don't cache its values because we expect the memory overhead of doing so // to outweigh the cost of recalculating it on access. @@ -85,8 +85,8 @@ class SassColor extends Value { // the same name in the JS API. /// This color's alpha channel, between `0` and `1`. - num get alpha => _alpha; - final num _alpha; + double get alpha => _alpha; + final double _alpha; /// The format in which this color was originally written and should be /// serialized in expanded mode, or `null` if the color wasn't written in a @@ -109,7 +109,9 @@ class SassColor extends Value { @internal SassColor.rgbInternal(this._red, this._green, this._blue, [num? alpha, this.format]) - : _alpha = alpha == null ? 1 : fuzzyAssertRange(alpha, 0, 1, "alpha") { + : _alpha = alpha == null + ? 1 + : fuzzyAssertRange(alpha.toDouble(), 0, 1, "alpha") { RangeError.checkValueInInterval(red, 0, 255, "red"); RangeError.checkValueInInterval(green, 0, 255, "green"); RangeError.checkValueInInterval(blue, 0, 255, "blue"); @@ -129,9 +131,13 @@ class SassColor extends Value { SassColor.hslInternal(num hue, num saturation, num lightness, [num? alpha, this.format]) : _hue = hue % 360, - _saturation = fuzzyAssertRange(saturation, 0, 100, "saturation"), - _lightness = fuzzyAssertRange(lightness, 0, 100, "lightness"), - _alpha = alpha == null ? 1 : fuzzyAssertRange(alpha, 0, 1, "alpha"); + _saturation = + fuzzyAssertRange(saturation.toDouble(), 0, 100, "saturation"), + _lightness = + fuzzyAssertRange(lightness.toDouble(), 0, 100, "lightness"), + _alpha = alpha == null + ? 1 + : fuzzyAssertRange(alpha.toDouble(), 0, 1, "alpha"); /// Creates an HWB color. /// @@ -141,9 +147,9 @@ class SassColor extends Value { // From https://www.w3.org/TR/css-color-4/#hwb-to-rgb var scaledHue = hue % 360 / 360; var scaledWhiteness = - fuzzyAssertRange(whiteness, 0, 100, "whiteness") / 100; + fuzzyAssertRange(whiteness.toDouble(), 0, 100, "whiteness") / 100; var scaledBlackness = - fuzzyAssertRange(blackness, 0, 100, "blackness") / 100; + fuzzyAssertRange(blackness.toDouble(), 0, 100, "blackness") / 100; var sum = scaledWhiteness + scaledBlackness; if (sum > 1) { @@ -152,7 +158,7 @@ class SassColor extends Value { } var factor = 1 - scaledWhiteness - scaledBlackness; - int toRgb(num hue) { + int toRgb(double hue) { var channel = _hueToRgb(0, 1, hue) * factor + scaledWhiteness; return fuzzyRound(channel * 255); } @@ -192,8 +198,14 @@ class SassColor extends Value { blackness ?? this.blackness, alpha ?? this.alpha); /// Returns a new copy of this color with the alpha channel set to [alpha]. - SassColor changeAlpha(num alpha) => SassColor._(_red, _green, _blue, _hue, - _saturation, _lightness, fuzzyAssertRange(alpha, 0, 1, "alpha")); + SassColor changeAlpha(num alpha) => SassColor._( + _red, + _green, + _blue, + _hue, + _saturation, + _lightness, + fuzzyAssertRange(alpha.toDouble(), 0, 1, "alpha")); /// @nodoc @internal @@ -282,7 +294,7 @@ class SassColor extends Value { /// An algorithm from the CSS3 spec: /// http://www.w3.org/TR/css3-color/#hsl-color. - static num _hueToRgb(num m1, num m2, num hue) { + static double _hueToRgb(double m1, double m2, double hue) { if (hue < 0) hue += 1; if (hue > 1) hue -= 1; diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index e4448059e..e57e8e303 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -22,7 +22,7 @@ import 'number/unitless.dart'; const _conversions = { // Length "in": { - "in": 1, + "in": 1.0, "cm": 1 / 2.54, "pc": 1 / 6, "mm": 1 / 25.4, @@ -32,7 +32,7 @@ const _conversions = { }, "cm": { "in": 2.54, - "cm": 1, + "cm": 1.0, "pc": 2.54 / 6, "mm": 1 / 10, "q": 1 / 40, @@ -40,9 +40,9 @@ const _conversions = { "px": 2.54 / 96, }, "pc": { - "in": 6, + "in": 6.0, "cm": 6 / 2.54, - "pc": 1, + "pc": 1.0, "mm": 6 / 25.4, "q": 6 / 101.6, "pt": 1 / 12, @@ -50,96 +50,96 @@ const _conversions = { }, "mm": { "in": 25.4, - "cm": 10, + "cm": 10.0, "pc": 25.4 / 6, - "mm": 1, + "mm": 1.0, "q": 1 / 4, "pt": 25.4 / 72, "px": 25.4 / 96, }, "q": { "in": 101.6, - "cm": 40, + "cm": 40.0, "pc": 101.6 / 6, - "mm": 4, - "q": 1, + "mm": 4.0, + "q": 1.0, "pt": 101.6 / 72, "px": 101.6 / 96, }, "pt": { - "in": 72, + "in": 72.0, "cm": 72 / 2.54, - "pc": 12, + "pc": 12.0, "mm": 72 / 25.4, "q": 72 / 101.6, - "pt": 1, + "pt": 1.0, "px": 3 / 4, }, "px": { - "in": 96, + "in": 96.0, "cm": 96 / 2.54, - "pc": 16, + "pc": 16.0, "mm": 96 / 25.4, "q": 96 / 101.6, "pt": 4 / 3, - "px": 1, + "px": 1.0, }, // Rotation "deg": { - "deg": 1, + "deg": 1.0, "grad": 9 / 10, "rad": 180 / pi, - "turn": 360, + "turn": 360.0, }, "grad": { "deg": 10 / 9, - "grad": 1, + "grad": 1.0, "rad": 200 / pi, - "turn": 400, + "turn": 400.0, }, "rad": { "deg": pi / 180, "grad": pi / 200, - "rad": 1, + "rad": 1.0, "turn": 2 * pi, }, "turn": { "deg": 1 / 360, "grad": 1 / 400, "rad": 1 / (2 * pi), - "turn": 1, + "turn": 1.0, }, // Time "s": { - "s": 1, + "s": 1.0, "ms": 1 / 1000, }, "ms": { - "s": 1000, - "ms": 1, + "s": 1000.0, + "ms": 1.0, }, // Frequency - "Hz": {"Hz": 1, "kHz": 1000}, - "kHz": {"Hz": 1 / 1000, "kHz": 1}, + "Hz": {"Hz": 1.0, "kHz": 1000.0}, + "kHz": {"Hz": 1 / 1000, "kHz": 1.0}, // Pixel density "dpi": { - "dpi": 1, + "dpi": 1.0, "dpcm": 2.54, - "dppx": 96, + "dppx": 96.0, }, "dpcm": { "dpi": 1 / 2.54, - "dpcm": 1, + "dpcm": 1.0, "dppx": 96 / 2.54, }, "dppx": { "dpi": 1 / 96, "dpcm": 2.54 / 96, - "dppx": 1, + "dppx": 1.0, }, }; @@ -165,7 +165,7 @@ final _typesByUnit = { /// /// @nodoc @internal -num? conversionFactor(String unit1, String unit2) { +double? conversionFactor(String unit1, String unit2) { if (unit1 == unit2) return 1; var innerMap = _conversions[unit1]; if (innerMap == null) return null; @@ -191,12 +191,12 @@ abstract class SassNumber extends Value { /// The value of this number. /// - /// Note that due to details of floating-point arithmetic, this may be a - /// [double] even if [this] represents an int from Sass's perspective. Use - /// [isInt] to determine whether this is an integer, [asInt] to get its - /// integer value, or [assertInt] to do both at once. - num get value => _value; - final num _value; + /// Note that Sass stores all numbers as [double]s even if if [this] + /// represents an integer from Sass's perspective. Use [isInt] to determine + /// whether this is an integer, [asInt] to get its integer value, or + /// [assertInt] to do both at once. + double get value => _value; + final double _value; /// The cached hash code for this number, if it's been computed. /// @@ -246,24 +246,25 @@ abstract class SassNumber extends Value { /// This matches the numbers that can be written as literals. /// [SassNumber.withUnits] can be used to construct more complex units. factory SassNumber(num value, [String? unit]) => unit == null - ? UnitlessSassNumber(value) - : SingleUnitSassNumber(value, unit); + ? UnitlessSassNumber(value.toDouble()) + : SingleUnitSassNumber(value.toDouble(), unit); /// Creates a number with full [numeratorUnits] and [denominatorUnits]. factory SassNumber.withUnits(num value, {List? numeratorUnits, List? denominatorUnits}) { + var valueDouble = value.toDouble(); if (denominatorUnits == null || denominatorUnits.isEmpty) { if (numeratorUnits == null || numeratorUnits.isEmpty) { - return UnitlessSassNumber(value); + return UnitlessSassNumber(valueDouble); } else if (numeratorUnits.length == 1) { - return SingleUnitSassNumber(value, numeratorUnits[0]); + return SingleUnitSassNumber(valueDouble, numeratorUnits[0]); } else { return ComplexSassNumber( - value, List.unmodifiable(numeratorUnits), const []); + valueDouble, List.unmodifiable(numeratorUnits), const []); } } else if (numeratorUnits == null || numeratorUnits.isEmpty) { return ComplexSassNumber( - value, const [], List.unmodifiable(denominatorUnits)); + valueDouble, const [], List.unmodifiable(denominatorUnits)); } else { var numerators = numeratorUnits.toList(); var unsimplifiedDenominators = denominatorUnits.toList(); @@ -274,7 +275,7 @@ abstract class SassNumber extends Value { for (var i = 0; i < numerators.length; i++) { var factor = conversionFactor(denominator, numerators[i]); if (factor == null) continue; - value *= factor; + valueDouble *= factor; numerators.removeAt(i); simplifiedAway = true; break; @@ -284,13 +285,13 @@ abstract class SassNumber extends Value { if (denominatorUnits.isEmpty) { if (numeratorUnits.isEmpty) { - return UnitlessSassNumber(value); + return UnitlessSassNumber(valueDouble); } else if (numeratorUnits.length == 1) { - return SingleUnitSassNumber(value, numeratorUnits.single); + return SingleUnitSassNumber(valueDouble, numeratorUnits.single); } } - return ComplexSassNumber(value, List.unmodifiable(numerators), + return ComplexSassNumber(valueDouble, List.unmodifiable(numerators), List.unmodifiable(denominators)); } } @@ -341,7 +342,7 @@ abstract class SassNumber extends Value { /// appropriate value. Otherwise, this throws a [SassScriptException]. If this /// came from a function argument, [name] is the argument name (without the /// `$`). It's used for error reporting. - num valueInRange(num min, num max, [String? name]) { + double valueInRange(num min, num max, [String? name]) { var result = fuzzyCheckRange(value, min, max); if (result != null) return result; throw SassScriptException( @@ -358,7 +359,7 @@ abstract class SassNumber extends Value { /// /// @nodoc @internal - num valueInRangeWithUnit(num min, num max, String name, String unit) { + double valueInRangeWithUnit(num min, num max, String name, String unit) { var result = fuzzyCheckRange(value, min, max); if (result != null) return result; throw SassScriptException( @@ -433,7 +434,7 @@ abstract class SassNumber extends Value { /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - num convertValue(List newNumerators, List newDenominators, + double convertValue(List newNumerators, List newDenominators, [String? name]) => _coerceOrConvertValue(newNumerators, newDenominators, coerceUnitless: false, name: name); @@ -465,7 +466,7 @@ abstract class SassNumber extends Value { /// If this came from a function argument, [name] is the argument name /// (without the `$`) and [otherName] is the argument name for [other]. These /// are used for error reporting. - num convertValueToMatch(SassNumber other, + double convertValueToMatch(SassNumber other, [String? name, String? otherName]) => _coerceOrConvertValue(other.numeratorUnits, other.denominatorUnits, coerceUnitless: false, @@ -507,13 +508,13 @@ abstract class SassNumber extends Value { /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - num coerceValue(List newNumerators, List newDenominators, + double coerceValue(List newNumerators, List newDenominators, [String? name]) => _coerceOrConvertValue(newNumerators, newDenominators, coerceUnitless: true, name: name); /// A shorthand for [coerceValue] with only one numerator unit. - num coerceValueToUnit(String unit, [String? name]) => + double coerceValueToUnit(String unit, [String? name]) => coerceValue([unit], [], name); /// Returns a copy of this number, converted to the same units as [other]. @@ -551,14 +552,15 @@ abstract class SassNumber extends Value { /// If this came from a function argument, [name] is the argument name /// (without the `$`) and [otherName] is the argument name for [other]. These /// are used for error reporting. - num coerceValueToMatch(SassNumber other, [String? name, String? otherName]) => + double coerceValueToMatch(SassNumber other, + [String? name, String? otherName]) => _coerceOrConvertValue(other.numeratorUnits, other.denominatorUnits, coerceUnitless: true, name: name, other: other, otherName: otherName); /// This has been renamed [coerceValue] for consistency with [coerceToMatch], /// [coerceValueToMatch], [convertToMatch], and [convertValueToMatch]. @Deprecated("Use coerceValue instead.") - num valueInUnits(List newNumerators, List newDenominators, + double valueInUnits(List newNumerators, List newDenominators, [String? name]) => coerceValue(newNumerators, newDenominators, name); @@ -572,7 +574,7 @@ abstract class SassNumber extends Value { /// and [newDenominators] are derived. The [name] and [otherName] are the Sass /// function parameter names of [this] and [other], respectively, used for /// error reporting. - num _coerceOrConvertValue( + double _coerceOrConvertValue( List newNumerators, List newDenominators, {required bool coerceUnitless, String? name, @@ -716,21 +718,6 @@ abstract class SassNumber extends Value { throw SassScriptException('Undefined operation "$this % $other".'); } - /// Return [num1] modulo [num2], using Sass's modulo semantics, which it - /// inherited from Ruby and which differ from Dart's. - /// - /// @nodoc - @internal - num moduloLikeSass(num num1, num num2) { - if (num2 > 0) return num1 % num2; - if (num2 == 0) return double.nan; - - // Dart has different mod-negative semantics than Ruby, and thus than - // Sass. - var result = num1 % num2; - return result == 0 ? 0 : result + num2; - } - /// @nodoc @internal Value plus(Value other) { @@ -784,7 +771,7 @@ abstract class SassNumber extends Value { /// /// @nodoc @protected - T _coerceUnits(SassNumber other, T operation(num num1, num num2)) { + T _coerceUnits(SassNumber other, T operation(double num1, double num2)) { try { return operation(value, other.coerceValueToMatch(this)); } on SassScriptException { @@ -802,8 +789,8 @@ abstract class SassNumber extends Value { /// /// @nodoc @protected - SassNumber multiplyUnits( - num value, List otherNumerators, List otherDenominators) { + SassNumber multiplyUnits(double value, List otherNumerators, + List otherDenominators) { // Short-circuit without allocating any new unit lists if possible. if (numeratorUnits.isEmpty) { if (otherDenominators.isEmpty && @@ -932,7 +919,7 @@ abstract class SassNumber extends Value { /// /// That is, if `X units1 == Y units2`, `X * _canonicalMultiplier(units1) == Y /// * _canonicalMultiplier(units2)`. - num _canonicalMultiplier(List units) => units.fold( + double _canonicalMultiplier(List units) => units.fold( 1, (multiplier, unit) => multiplier * canonicalMultiplierForUnit(unit)); /// Returns a multiplier that encapsulates unit equivalence with [unit]. @@ -942,7 +929,7 @@ abstract class SassNumber extends Value { /// /// @nodoc @protected - num canonicalMultiplierForUnit(String unit) { + double canonicalMultiplierForUnit(String unit) { var innerMap = _conversions[unit]; return innerMap == null ? 1 : 1 / innerMap.values.first; } diff --git a/lib/src/value/number/complex.dart b/lib/src/value/number/complex.dart index cf82384c2..adbc362e4 100644 --- a/lib/src/value/number/complex.dart +++ b/lib/src/value/number/complex.dart @@ -26,10 +26,11 @@ class ComplexSassNumber extends SassNumber { bool get hasUnits => true; ComplexSassNumber( - num value, List numeratorUnits, List denominatorUnits) + double value, List numeratorUnits, List denominatorUnits) : this._(value, numeratorUnits, denominatorUnits); - ComplexSassNumber._(num value, this._numeratorUnits, this._denominatorUnits, + ComplexSassNumber._( + double value, this._numeratorUnits, this._denominatorUnits, [Tuple2? asSlash]) : super.protected(value, asSlash) { assert(numeratorUnits.length > 1 || denominatorUnits.isNotEmpty); @@ -48,7 +49,7 @@ class ComplexSassNumber extends SassNumber { } SassNumber withValue(num value) => - ComplexSassNumber._(value, numeratorUnits, denominatorUnits); + ComplexSassNumber._(value.toDouble(), numeratorUnits, denominatorUnits); SassNumber withSlash(SassNumber numerator, SassNumber denominator) => ComplexSassNumber._(value, numeratorUnits, denominatorUnits, diff --git a/lib/src/value/number/single_unit.dart b/lib/src/value/number/single_unit.dart index c9e81cf7b..cb439630d 100644 --- a/lib/src/value/number/single_unit.dart +++ b/lib/src/value/number/single_unit.dart @@ -47,11 +47,12 @@ class SingleUnitSassNumber extends SassNumber { bool get hasUnits => true; - SingleUnitSassNumber(num value, this._unit, + SingleUnitSassNumber(double value, this._unit, [Tuple2? asSlash]) : super.protected(value, asSlash); - SassNumber withValue(num value) => SingleUnitSassNumber(value, _unit); + SassNumber withValue(num value) => + SingleUnitSassNumber(value.toDouble(), _unit); SassNumber withSlash(SassNumber numerator, SassNumber denominator) => SingleUnitSassNumber(value, _unit, Tuple2(numerator, denominator)); @@ -81,7 +82,8 @@ class SingleUnitSassNumber extends SassNumber { // Call this to generate a consistent error message. super.coerceToMatch(other, name, otherName); - num coerceValueToMatch(SassNumber other, [String? name, String? otherName]) => + double coerceValueToMatch(SassNumber other, + [String? name, String? otherName]) => (other is SingleUnitSassNumber ? _coerceValueToUnit(other._unit) : null) ?? @@ -94,7 +96,7 @@ class SingleUnitSassNumber extends SassNumber { // Call this to generate a consistent error message. super.convertToMatch(other, name, otherName); - num convertValueToMatch(SassNumber other, + double convertValueToMatch(SassNumber other, [String? name, String? otherName]) => (other is SingleUnitSassNumber ? _coerceValueToUnit(other._unit) @@ -110,7 +112,7 @@ class SingleUnitSassNumber extends SassNumber { // Call this to generate a consistent error message. super.coerce(newNumerators, newDenominators, name); - num coerceValue(List newNumerators, List newDenominators, + double coerceValue(List newNumerators, List newDenominators, [String? name]) => (newNumerators.length == 1 && newDenominators.isEmpty ? _coerceValueToUnit(newNumerators[0]) @@ -118,7 +120,7 @@ class SingleUnitSassNumber extends SassNumber { // Call this to generate a consistent error message. super.coerceValue(newNumerators, newDenominators, name); - num coerceValueToUnit(String unit, [String? name]) => + double coerceValueToUnit(String unit, [String? name]) => _coerceValueToUnit(unit) ?? // Call this to generate a consistent error message. super.coerceValueToUnit(unit, name); @@ -132,7 +134,7 @@ class SingleUnitSassNumber extends SassNumber { } /// Like [coerceValueToUnit], except that it returns `null` if coercion fails. - num? _coerceValueToUnit(String unit) => + double? _coerceValueToUnit(String unit) => conversionFactor(unit, _unit).andThen((factor) => value * factor); SassNumber multiplyUnits( diff --git a/lib/src/value/number/unitless.dart b/lib/src/value/number/unitless.dart index 38116cccd..506ef6d5b 100644 --- a/lib/src/value/number/unitless.dart +++ b/lib/src/value/number/unitless.dart @@ -19,10 +19,10 @@ class UnitlessSassNumber extends SassNumber { bool get hasUnits => false; - UnitlessSassNumber(num value, [Tuple2? asSlash]) + UnitlessSassNumber(double value, [Tuple2? asSlash]) : super.protected(value, asSlash); - SassNumber withValue(num value) => UnitlessSassNumber(value); + SassNumber withValue(num value) => UnitlessSassNumber(value.toDouble()); SassNumber withSlash(SassNumber numerator, SassNumber denominator) => UnitlessSassNumber(value, Tuple2(numerator, denominator)); @@ -41,7 +41,8 @@ class UnitlessSassNumber extends SassNumber { [String? name, String? otherName]) => other.withValue(value); - num coerceValueToMatch(SassNumber other, [String? name, String? otherName]) => + double coerceValueToMatch(SassNumber other, + [String? name, String? otherName]) => value; SassNumber convertToMatch(SassNumber other, @@ -51,7 +52,7 @@ class UnitlessSassNumber extends SassNumber { ? super.convertToMatch(other, name, otherName) : this; - num convertValueToMatch(SassNumber other, + double convertValueToMatch(SassNumber other, [String? name, String? otherName]) => other.hasUnits // Call this to generate a consistent error message. @@ -63,11 +64,11 @@ class UnitlessSassNumber extends SassNumber { SassNumber.withUnits(value, numeratorUnits: newNumerators, denominatorUnits: newDenominators); - num coerceValue(List newNumerators, List newDenominators, + double coerceValue(List newNumerators, List newDenominators, [String? name]) => value; - num coerceValueToUnit(String unit, [String? name]) => value; + double coerceValueToUnit(String unit, [String? name]) => value; SassBoolean greaterThan(Value other) { if (other is SassNumber) { diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 5bae8be6e..c6c348387 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -776,7 +776,7 @@ class _SerializeVisitor /// Writes [number] without exponent notation and with at most /// [SassNumber.precision] digits after the decimal point. - void _writeNumber(num number) { + void _writeNumber(double number) { // Dart always converts integers to strings in the obvious way, so all we // have to do is clamp doubles that are close to being integers. var integer = fuzzyAsInt(number); diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 7b35160c9..8ef193753 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,10 +1,17 @@ -## 3.1.0 +## 4.0.0 ### Dart API +* **Breaking change:** The first argument to `NumberExpression()` is now a + `double` rather than a `num`. + * Add an optional `argumentName` parameter to `SassScriptException()` to make it easier to throw exceptions associated with particular argument names. +* Most APIs that previously returned `num` now return `double`. All APIs + continue to _accept_ `num`, although in Dart 2.0.0 most of these APIs will be + changed to accept only `double`. + ## 3.0.4 * `UnaryOperationExpression`s with operator `not` now include a correct span, diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index c1f10ffa1..dd22fae72 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: 3.1.0-dev +version: 4.0.0 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass diff --git a/test/dart_api/value/color_test.dart b/test/dart_api/value/color_test.dart index e426bee31..bb7ee5f51 100644 --- a/test/dart_api/value/color_test.dart +++ b/test/dart_api/value/color_test.dart @@ -5,7 +5,6 @@ import 'package:test/test.dart'; import 'package:sass/sass.dart'; -import 'package:sass/src/util/number.dart'; import 'utils.dart'; @@ -225,7 +224,7 @@ void main() { test("an RGBA color has an alpha channel", () { var color = parseValue("rgba(10, 20, 30, 0.7)") as SassColor; - expect(color.alpha, closeTo(0.7, epsilon)); + expect(color.alpha, closeTo(0.7, 1e-11)); }); group("new SassColor.rgb()", () { diff --git a/test/dart_api/value/number_test.dart b/test/dart_api/value/number_test.dart index b2594d2a1..37f36d207 100644 --- a/test/dart_api/value/number_test.dart +++ b/test/dart_api/value/number_test.dart @@ -17,7 +17,7 @@ void main() { test("has the correct value", () { expect(value.value, equals(123)); - expect(value.value, const TypeMatcher()); + expect(value.value, const TypeMatcher()); }); test("has no units", () {