From 2010016b9b1241ec6451a7f0548844ceb6c9984e Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 20 May 2024 17:00:30 -0700 Subject: [PATCH 1/5] [Color 4] Throw errors for non-legacy colors in deprecated functions --- lib/src/functions/color.dart | 97 ++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 26 deletions(-) diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index 9a0b6af01..afb9fcaba 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -106,6 +106,12 @@ final global = UnmodifiableListView([ var color = arguments[0].assertColor("color"); var degrees = _angleValue(arguments[1], "degrees"); + if (!color.isLegacy) { + throw SassScriptException( + "adjust-hue() is only supported for legacy colors. Please use " + "color.adjust() instead with an explicit \$space argument."); + } + var suggestedValue = SassNumber(degrees, 'deg'); warnForDeprecation( "adjust-hue() is deprecated. Suggestion:\n" @@ -121,6 +127,12 @@ final global = UnmodifiableListView([ _function("lighten", r"$color, $amount", (arguments) { var color = arguments[0].assertColor("color"); var amount = arguments[1].assertNumber("amount"); + if (!color.isLegacy) { + throw SassScriptException( + "lighten() is only supported for legacy colors. Please use " + "color.adjust() instead with an explicit \$space argument."); + } + var result = color.changeHsl( lightness: (color.lightness + amount.valueInRange(0, 100, "amount")) .clamp(0, 100)); @@ -137,6 +149,12 @@ final global = UnmodifiableListView([ _function("darken", r"$color, $amount", (arguments) { var color = arguments[0].assertColor("color"); var amount = arguments[1].assertNumber("amount"); + if (!color.isLegacy) { + throw SassScriptException( + "darken() is only supported for legacy colors. Please use " + "color.adjust() instead with an explicit \$space argument."); + } + var result = color.changeHsl( lightness: (color.lightness - amount.valueInRange(0, 100, "amount")) .clamp(0, 100)); @@ -162,6 +180,12 @@ final global = UnmodifiableListView([ r"$color, $amount": (arguments) { var color = arguments[0].assertColor("color"); var amount = arguments[1].assertNumber("amount"); + if (!color.isLegacy) { + throw SassScriptException( + "saturate() is only supported for legacy colors. Please use " + "color.adjust() instead with an explicit \$space argument."); + } + var result = color.changeHsl( saturation: (color.saturation + amount.valueInRange(0, 100, "amount")) .clamp(0, 100)); @@ -179,6 +203,12 @@ final global = UnmodifiableListView([ _function("desaturate", r"$color, $amount", (arguments) { var color = arguments[0].assertColor("color"); var amount = arguments[1].assertNumber("amount"); + if (!color.isLegacy) { + throw SassScriptException( + "desaturate() is only supported for legacy colors. Please use " + "color.adjust() instead with an explicit \$space argument."); + } + var result = color.changeHsl( saturation: (color.saturation - amount.valueInRange(0, 100, "amount")) .clamp(0, 100)); @@ -203,18 +233,16 @@ final global = UnmodifiableListView([ (arguments) => _transparentize("fade-out", arguments)), BuiltInCallable.overloadedFunction("alpha", { - r"$color": (arguments) { - var argument = arguments[0]; - if (argument is SassString && - !argument.hasQuotes && - argument.text.contains(_microsoftFilterStart)) { - // Support the proprietary Microsoft alpha() function. - return _functionString("alpha", arguments); - } - - var color = argument.assertColor("color"); - return SassNumber(color.alpha); - }, + r"$color": (arguments) => switch (arguments[0]) { + // Support the proprietary Microsoft alpha() function. + SassString(hasQuotes: false, :var text) + when text.contains(_microsoftFilterStart) => + _functionString("alpha", arguments), + SassColor(isLegacy: false) => throw SassScriptException( + "alpha() is only supported for legacy colors. Please use " + "color.channel() instead."), + var argument => SassNumber(argument.assertColor("color").alpha) + }, r"$args...": (arguments) { var argList = arguments[0].asList; if (argList.isNotEmpty && @@ -364,21 +392,26 @@ final module = BuiltInModule("color", functions: [ BuiltInCallable.overloadedFunction("alpha", { r"$color": (arguments) { - var argument = arguments[0]; - if (argument is SassString && - !argument.hasQuotes && - argument.text.contains(_microsoftFilterStart)) { - var result = _functionString("alpha", arguments); - warnForDeprecation( - "Using color.alpha() for a Microsoft filter is deprecated.\n" - "\n" - "Recommendation: $result", - Deprecation.colorModuleCompat); - return result; - } + switch (arguments[0]) { + // Support the proprietary Microsoft alpha() function. + case SassString(hasQuotes: false, :var text) + when text.contains(_microsoftFilterStart): + var result = _functionString("alpha", arguments); + warnForDeprecation( + "Using color.alpha() for a Microsoft filter is deprecated.\n" + "\n" + "Recommendation: $result", + Deprecation.colorModuleCompat); + return result; + + case SassColor(isLegacy: false): + throw SassScriptException( + "color.alpha() is only supported for legacy colors. Please use " + "color.channel() instead."); - var color = argument.assertColor("color"); - return SassNumber(color.alpha); + case var argument: + return SassNumber(argument.assertColor("color").alpha); + } }, r"$args...": (arguments) { if (arguments[0].asList.every((argument) => @@ -1155,6 +1188,12 @@ SassColor _mixLegacy(SassColor color1, SassColor color2, SassNumber weight) { SassColor _opacify(String name, List arguments) { var color = arguments[0].assertColor("color"); var amount = arguments[1].assertNumber("amount"); + if (!color.isLegacy) { + throw SassScriptException( + "$name() is only supported for legacy colors. Please use " + "color.adjust() instead with an explicit \$space argument."); + } + var result = color.changeAlpha( (color.alpha + amount.valueInRangeWithUnit(0, 1, "amount", "")) .clamp(0, 1)); @@ -1172,6 +1211,12 @@ SassColor _opacify(String name, List arguments) { SassColor _transparentize(String name, List arguments) { var color = arguments[0].assertColor("color"); var amount = arguments[1].assertNumber("amount"); + if (!color.isLegacy) { + throw SassScriptException( + "$name() is only supported for legacy colors. Please use " + "color.adjust() instead with an explicit \$space argument."); + } + var result = color.changeAlpha( (color.alpha - amount.valueInRangeWithUnit(0, 1, "amount", "")) .clamp(0, 1)); From 2d01ae8dc8108de9d769fecb5e6680c6caf145bd Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 5 Jun 2024 17:25:37 -0700 Subject: [PATCH 2/5] [Color 4] Update behavior to cover more missing channel edge cases --- CHANGELOG.md | 4 + lib/src/functions/color.dart | 209 ++++++++++++++++++++++------------- lib/src/value/color.dart | 42 ++++--- 3 files changed, 160 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7802eca1..c6d344d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ is now interpreted as a percentage, instead of ignoring the unit. For example, `color.change(red, $alpha: 50%)` now returns `rgb(255 0 0 / 0.5)`. +* **Potentially breaking compatibility fix**: Sass no longer rounds RGB channels + to the nearest integer. This means that, for example, `rgb(0 0 1) != rgb(0 0 + 0.6)`. This matches the latest version of the CSS spec and browser behavior. + * **Potentially breaking compatibility fix**: Passing large positive or negative values to `color.adjust()` can now cause a color's channels to go outside that color's gamut. In most cases this will currently be clipped by the browser and diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index afb9fcaba..9603d819c 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -457,21 +457,14 @@ final module = BuiltInModule("color", functions: [ (arguments) => SassString(arguments.first.assertColor("color").space.name, quotes: false)), - _function("to-space", r"$color, $space", (arguments) { - var converted = _colorInSpace(arguments[0], arguments[1]); - // `color.to-space()` never returns missing channels for legacy color - // spaces because they're less compatible and users are probably using a - // legacy space because they want a highly compatible color. - return converted.isLegacy && - (converted.isChannel0Missing || - converted.isChannel1Missing || - converted.isChannel2Missing || - converted.isAlphaMissing) && - converted.space != (arguments[0] as SassColor).space - ? SassColor.forSpaceInternal(converted.space, converted.channel0, - converted.channel1, converted.channel2, converted.alpha) - : converted; - }), + // `color.to-space()` never returns missing channels for legacy color spaces + // because they're less compatible and users are probably using a legacy space + // because they want a highly compatible color. + _function( + "to-space", + r"$color, $space", + (arguments) => + _colorInSpace(arguments[0], arguments[1], legacyMissing: false)), _function("is-legacy", r"$color", (arguments) => SassBoolean(arguments[0].assertColor("color").isLegacy)), @@ -508,7 +501,10 @@ final module = BuiltInModule("color", functions: [ (arguments[2].assertString("method")..assertUnquoted("method")).text); if (!space.isBounded) return color; - return color.toSpace(space).toGamut(method).toSpace(color.space); + return color + .toSpace(space) + .toGamut(method) + .toSpace(color.space, legacyMissing: false); }), _function("channel", r"$color, $channel, $space: null", (arguments) { @@ -595,7 +591,8 @@ final _mix = _function("mix", r"$color1, $color2, $weight: 50%, $method: null", if (arguments[3] != sassNull) { return color1.interpolate( color2, InterpolationMethod.fromValue(arguments[3], "method"), - weight: weight.valueInRangeWithUnit(0, 100, "weight", "%") / 100); + weight: weight.valueInRangeWithUnit(0, 100, "weight", "%") / 100, + legacyMissing: false); } _checkPercent(weight, "weight"); @@ -630,9 +627,24 @@ final _complement = "Color space $space doesn't have a hue channel.", 'space'); } - var inSpace = color.toSpace(space); - return inSpace.changeChannels({'hue': inSpace.channel('hue') + 180}).toSpace( - color.space); + var colorInSpace = + color.toSpace(space, legacyMissing: arguments[1] != sassNull); + return (space.isLegacy + ? SassColor.forSpaceInternal( + space, + _adjustChannel(colorInSpace, space.channels[0], + colorInSpace.channel0OrNull, SassNumber(180)), + colorInSpace.channel1OrNull, + colorInSpace.channel2OrNull, + colorInSpace.alphaOrNull) + : SassColor.forSpaceInternal( + space, + colorInSpace.channel0OrNull, + colorInSpace.channel1OrNull, + _adjustChannel(colorInSpace, space.channels[2], + colorInSpace.channel2OrNull, SassNumber(180)), + colorInSpace.alphaOrNull)) + .toSpace(color.space, legacyMissing: false); }); /// The implementation of the `invert()` function. @@ -662,9 +674,13 @@ Value _invert(List arguments, {bool global = false}) { _checkPercent(weightNumber, "weight"); var rgb = color.toSpace(ColorSpace.rgb); + var [channel0, channel1, channel2] = ColorSpace.rgb.channels; return _mixLegacy( - SassColor.rgb(255.0 - rgb.channel0, 255.0 - rgb.channel1, - 255.0 - rgb.channel2, color.alphaOrNull), + SassColor.rgb( + _invertChannel(rgb, channel0, rgb.channel0OrNull), + _invertChannel(rgb, channel1, rgb.channel1OrNull), + _invertChannel(rgb, channel2, rgb.channel2OrNull), + color.alphaOrNull), color, weightNumber) .toSpace(color.space); @@ -678,38 +694,46 @@ Value _invert(List arguments, {bool global = false}) { var inSpace = color.toSpace(space); var inverted = switch (space) { - ColorSpace.hwb => SassColor.hwb((inSpace.channel0 + 180) % 360, - inSpace.channel2, inSpace.channel1, inSpace.alpha), - ColorSpace.hsl => SassColor.hsl((inSpace.channel0 + 180) % 360, - inSpace.channel1, 100 - inSpace.channel2, inSpace.alpha), - ColorSpace.lch => SassColor.lch(100 - inSpace.channel0, inSpace.channel1, - (inSpace.channel2 + 180) % 360, inSpace.alpha), - ColorSpace.oklch => SassColor.oklch(1 - inSpace.channel0, inSpace.channel1, - (inSpace.channel2 + 180) % 360, inSpace.alpha), - ColorSpace( - channels: [ - LinearChannel channel0, - LinearChannel channel1, - LinearChannel channel2 - ] - ) => + ColorSpace.hwb => SassColor.hwb( + _invertChannel(inSpace, space.channels[0], inSpace.channel0OrNull), + inSpace.channel2OrNull, + inSpace.channel1OrNull, + inSpace.alpha), + ColorSpace.hsl || + ColorSpace.lch || + ColorSpace.oklch => + SassColor.forSpaceInternal( + space, + _invertChannel(inSpace, space.channels[0], inSpace.channel0OrNull), + inSpace.channel1OrNull, + _invertChannel(inSpace, space.channels[2], inSpace.channel2OrNull), + inSpace.alpha), + ColorSpace(channels: [var channel0, var channel1, var channel2]) => SassColor.forSpaceInternal( space, - _invertChannel(channel0, inSpace.channel0), - _invertChannel(channel1, inSpace.channel1), - _invertChannel(channel2, inSpace.channel2), + _invertChannel(inSpace, channel0, inSpace.channel0OrNull), + _invertChannel(inSpace, channel1, inSpace.channel1OrNull), + _invertChannel(inSpace, channel2, inSpace.channel2OrNull), inSpace.alpha), _ => throw UnsupportedError("Unknown color space $space.") }; - if (fuzzyEquals(weight, 1)) return inverted.toSpace(color.space); - return color.interpolate(inverted, InterpolationMethod(space), - weight: 1 - weight); + return fuzzyEquals(weight, 1) + ? inverted.toSpace(color.space, legacyMissing: false) + : color.interpolate(inverted, InterpolationMethod(space), + weight: 1 - weight, legacyMissing: false); } /// Returns the inverse of the given [value] in a linear color channel. -double _invertChannel(LinearChannel channel, double value) => - channel.min < 0 ? -value : channel.max - value; +double _invertChannel(SassColor color, ColorChannel channel, double? value) { + if (value == null) _missingChannelError(color, channel.name); + return switch (channel) { + LinearChannel(min: < 0) => -value, + LinearChannel(min: 0, :var max) => max - value, + ColorChannel(isPolarAngle: true) => (value + 180) % 360, + _ => throw UnsupportedError("Unknown channel $channel.") + }; +} /// The implementation of the `grayscale()` function, without any logic for the /// plain-CSS `grayscale()` syntax. @@ -718,11 +742,12 @@ Value _grayscale(Value colorArg) { if (color.isLegacy) { var hsl = color.toSpace(ColorSpace.hsl); - return SassColor.hsl(hsl.channel0, 0, hsl.channel2, hsl.alpha) - .toSpace(color.space); + return SassColor.hsl(hsl.channel0OrNull, 0, hsl.channel2OrNull, hsl.alpha) + .toSpace(color.space, legacyMissing: false); } else { var oklch = color.toSpace(ColorSpace.oklch); - return SassColor.oklch(oklch.channel0, 0, oklch.channel2, oklch.alpha) + return SassColor.oklch( + oklch.channel0OrNull, 0, oklch.channel2OrNull, oklch.alpha) .toSpace(color.space); } } @@ -774,12 +799,14 @@ SassColor _updateComponents(List arguments, var alphaArg = keywords.remove('alpha'); // For backwards-compatibility, we allow legacy colors to modify channels in - // any legacy color space. - var color = - spaceKeyword == null && originalColor.isLegacy && keywords.isNotEmpty - ? _sniffLegacyColorSpace(keywords).andThen(originalColor.toSpace) ?? - originalColor - : _colorInSpace(originalColor, spaceKeyword ?? sassNull); + // any legacy color space and we their powerless channels as 0. + var color = spaceKeyword == null && + originalColor.isLegacy && + keywords.isNotEmpty + ? _sniffLegacyColorSpace(keywords).andThen( + (space) => originalColor.toSpace(space, legacyMissing: false)) ?? + originalColor + : _colorInSpace(originalColor, spaceKeyword ?? sassNull); var oldChannels = color.channels; var channelArgs = List.filled(oldChannels.length, null); @@ -809,7 +836,7 @@ SassColor _updateComponents(List arguments, : _adjustColor(color, channelNumbers, alphaNumber); } - return result.toSpace(originalColor.space); + return result.toSpace(originalColor.space, legacyMissing: false); } /// Returns a copy of [color] with its channel values replaced by those in @@ -872,20 +899,25 @@ SassColor _scaleColor( SassColor color, List channelArgs, SassNumber? alphaArg) => SassColor.forSpaceInternal( color.space, - _scaleChannel(color.space.channels[0], color.channel0, channelArgs[0]), - _scaleChannel(color.space.channels[1], color.channel1, channelArgs[1]), - _scaleChannel(color.space.channels[2], color.channel2, channelArgs[2]), - _scaleChannel(ColorChannel.alpha, color.alpha, alphaArg)); + _scaleChannel(color, color.space.channels[0], color.channel0OrNull, + channelArgs[0]), + _scaleChannel(color, color.space.channels[1], color.channel1OrNull, + channelArgs[1]), + _scaleChannel(color, color.space.channels[2], color.channel2OrNull, + channelArgs[2]), + _scaleChannel(color, ColorChannel.alpha, color.alphaOrNull, alphaArg)); /// Returns [oldValue] scaled by [factorArg] according to the definition in /// [channel]. -double _scaleChannel( - ColorChannel channel, double oldValue, SassNumber? factorArg) { +double? _scaleChannel(SassColor color, ColorChannel channel, double? oldValue, + SassNumber? factorArg) { if (factorArg == null) return oldValue; if (channel is! LinearChannel) { throw SassScriptException("Channel isn't scalable.", channel.name); } + if (oldValue == null) _missingChannelError(color, channel.name); + var factor = (factorArg..assertUnit('%', channel.name)) .valueInRangeWithUnit(-100, 100, channel.name, '%') / 100; @@ -906,32 +938,33 @@ SassColor _adjustColor( SassColor color, List channelArgs, SassNumber? alphaArg) => SassColor.forSpaceInternal( color.space, - _adjustChannel(color.space, color.space.channels[0], color.channel0, + _adjustChannel(color, color.space.channels[0], color.channel0OrNull, channelArgs[0]), - _adjustChannel(color.space, color.space.channels[1], color.channel1, + _adjustChannel(color, color.space.channels[1], color.channel1OrNull, channelArgs[1]), - _adjustChannel(color.space, color.space.channels[2], color.channel2, + _adjustChannel(color, color.space.channels[2], color.channel2OrNull, channelArgs[2]), // The color space doesn't matter for alpha, as long as it's not // strictly bounded. - _adjustChannel( - ColorSpace.lab, ColorChannel.alpha, color.alpha, alphaArg) - .clamp(0, 1)); + _adjustChannel(color, ColorChannel.alpha, color.alphaOrNull, alphaArg) + ?.clamp(0, 1)); /// Returns [oldValue] adjusted by [adjustmentArg] according to the definition -/// in [space]'s [channel]. -double _adjustChannel(ColorSpace space, ColorChannel channel, double oldValue, +/// in [color]'s space's [channel]. +double? _adjustChannel(SassColor color, ColorChannel channel, double? oldValue, SassNumber? adjustmentArg) { if (adjustmentArg == null) return oldValue; - switch ((space, channel)) { - case (ColorSpace.hsl || ColorSpace.hwb, _) when channel is! LinearChannel: + if (oldValue == null) _missingChannelError(color, channel.name); + + switch ((color.space, channel)) { + case (ColorSpace.hsl || ColorSpace.hwb, ColorChannel(isPolarAngle: true)): // `_channelFromValue` expects all hue values to be compatible with `deg`, // but we're still in the deprecation period where we allow non-`deg` // values for HSL and HWB so we have to handle that ahead-of-time. adjustmentArg = SassNumber(_angleValue(adjustmentArg, 'hue')); - case (ColorSpace.hsl, LinearChannel()): + case (ColorSpace.hsl, LinearChannel(name: 'saturation' || 'lightness')): // `_channelFromValue` expects lightness/saturation to be `%`, but we're // still in the deprecation period where we allow non-`%` values so we // have to handle that ahead-of-time. @@ -1047,6 +1080,14 @@ Value _rgbTwoArg(String name, List arguments) { } var color = first.assertColor("color"); + if (!color.isLegacy) { + throw SassScriptException( + 'Expected $color to be in the legacy RGB, HSL, or HWB color space.\n' + '\n' + 'Recommendation: color.change($color, \$alpha: $second)', + name); + } + color.assertLegacy("color"); color = color.toSpace(ColorSpace.rgb); if (second.isSpecialNumber) { @@ -1233,17 +1274,22 @@ SassColor _transparentize(String name, List arguments) { /// Returns the [colorUntyped] as a [SassColor] in the color space specified by /// [spaceUntyped]. /// +/// If [legacyMissing] is false, this will convert missing channels in legacy +/// color spaces to zero if a conversion occurs. +/// /// Throws a [SassScriptException] if either argument isn't the expected type or /// if [spaceUntyped] isn't the name of a color space. If [spaceUntyped] is /// `sassNull`, it defaults to the color's existing space. -SassColor _colorInSpace(Value colorUntyped, Value spaceUntyped) { +SassColor _colorInSpace(Value colorUntyped, Value spaceUntyped, + {bool legacyMissing = true}) { var color = colorUntyped.assertColor("color"); if (spaceUntyped == sassNull) return color; - var space = ColorSpace.fromName( - (spaceUntyped.assertString("space")..assertUnquoted("space")).text, - "space"); - return color.space == space ? color : color.toSpace(space); + return color.toSpace( + ColorSpace.fromName( + (spaceUntyped.assertString("space")..assertUnquoted("space")).text, + "space"), + legacyMissing: legacyMissing); } /// Returns the color space named by [space], or throws a [SassScriptException] @@ -1584,6 +1630,15 @@ String _suggestScaleAndAdjust( return suggestion + "color.adjust(\$color, \$$channelName: $difference)"; } +/// Throws an error indicating that a missing channel named [name] can't be +/// modified. +Never _missingChannelError(SassColor color, String channel) => + throw SassScriptException( + "Because the CSS working group is still deciding on the best behavior, " + "Sass doesn't currently support modifying missing channels (color: " + "$color).", + channel); + /// Asserts that `value` is an unquoted string and throws an error if it's not. /// /// Assumes that `value` comes from a parameter named `$channel`. diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index 9a61f91eb..b720a0c2d 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -640,16 +640,24 @@ class SassColor extends Value { /// Converts this color to [space]. /// - /// If this came from a function argument, [name] is the argument name for - /// this color (without the `$`). It's used for error reporting. - /// - /// This currently can't produce an error, but it will likely do so in the - /// future when Sass adds support for color spaces that don't support - /// automatic conversions. - SassColor toSpace(ColorSpace space) => this.space == space - ? this - : this.space.convert( - space, channel0OrNull, channel1OrNull, channel2OrNull, alpha); + /// If [legacyMissing] is false, this will convert missing channels in + /// legacy color spaces to zero if a conversion occurs. + SassColor toSpace(ColorSpace space, {bool legacyMissing = true}) { + if (this.space == space) return this; + + var converted = this + .space + .convert(space, channel0OrNull, channel1OrNull, channel2OrNull, alpha); + return !legacyMissing && + converted.isLegacy && + (converted.isChannel0Missing || + converted.isChannel1Missing || + converted.isChannel2Missing || + converted.isAlphaMissing) + ? SassColor.forSpaceInternal(converted.space, converted.channel0, + converted.channel1, converted.channel2, converted.alpha) + : converted; + } /// Returns a copy of this color that's in-gamut in the current color space. SassColor toGamut(GamutMapMethod method) => @@ -724,12 +732,7 @@ class SassColor extends Value { /// name (without the `$`). This is used for error reporting. SassColor changeChannels(Map newValues, {ColorSpace? space, String? colorName}) { - if (newValues.isEmpty) { - // If space conversion produces an error, we still want to expose that - // error even if there's nothing to change. - if (space != null && space != this.space) toSpace(space); - return this; - } + if (newValues.isEmpty) return this; if (space != null && space != this.space) { return toSpace(space) @@ -807,8 +810,11 @@ class SassColor extends Value { /// /// The [weight] is a number between 0 and 1 that indicates how much of [this] /// should be in the resulting color. It defaults to 0.5. + /// + /// If [legacyMissing] is false, this will convert missing channels in legacy + /// color spaces to zero if a conversion occurs. SassColor interpolate(SassColor other, InterpolationMethod method, - {double? weight}) { + {double? weight, bool legacyMissing = true}) { weight ??= 0.5; if (fuzzyEquals(weight, 0)) return other; @@ -877,7 +883,7 @@ class SassColor extends Value { _ => SassColor.forSpaceInternal( method.space, mixed0, mixed1, mixed2, mixedAlpha) } - .toSpace(space); + .toSpace(space, legacyMissing: legacyMissing); } /// Returns whether [output], which was converted to its color space from From 4476944e8ffad9f7ca018d8d3a931f857079c12c Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 8 Jul 2024 18:27:20 -0700 Subject: [PATCH 3/5] Properly support degenerate channel values --- lib/src/functions/color.dart | 62 ++++++++++--------- lib/src/js/legacy/value/color.dart | 10 +-- lib/src/util/number.dart | 6 ++ lib/src/value/color.dart | 16 ----- .../value/color/gamut_map_method/clip.dart | 3 +- lib/src/visitor/serialize.dart | 36 +++++------ 6 files changed, 61 insertions(+), 72 deletions(-) diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index 9603d819c..9c4467453 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -134,8 +134,8 @@ final global = UnmodifiableListView([ } var result = color.changeHsl( - lightness: (color.lightness + amount.valueInRange(0, 100, "amount")) - .clamp(0, 100)); + lightness: clampLikeCss( + color.lightness + amount.valueInRange(0, 100, "amount"), 0, 100)); warnForDeprecation( "lighten() is deprecated. " @@ -156,8 +156,8 @@ final global = UnmodifiableListView([ } var result = color.changeHsl( - lightness: (color.lightness - amount.valueInRange(0, 100, "amount")) - .clamp(0, 100)); + lightness: clampLikeCss( + color.lightness - amount.valueInRange(0, 100, "amount"), 0, 100)); warnForDeprecation( "darken() is deprecated. " @@ -187,8 +187,10 @@ final global = UnmodifiableListView([ } var result = color.changeHsl( - saturation: (color.saturation + amount.valueInRange(0, 100, "amount")) - .clamp(0, 100)); + saturation: clampLikeCss( + color.saturation + amount.valueInRange(0, 100, "amount"), + 0, + 100)); warnForDeprecation( "saturate() is deprecated. " @@ -210,8 +212,8 @@ final global = UnmodifiableListView([ } var result = color.changeHsl( - saturation: (color.saturation - amount.valueInRange(0, 100, "amount")) - .clamp(0, 100)); + saturation: clampLikeCss( + color.saturation - amount.valueInRange(0, 100, "amount"), 0, 100)); warnForDeprecation( "desaturate() is deprecated. " @@ -947,7 +949,7 @@ SassColor _adjustColor( // The color space doesn't matter for alpha, as long as it's not // strictly bounded. _adjustChannel(color, ColorChannel.alpha, color.alphaOrNull, alphaArg) - ?.clamp(0, 1)); + .andThen((alpha) => clampLikeCss(alpha, 0, 1))); /// Returns [oldValue] adjusted by [adjustmentArg] according to the definition /// in [color]'s space's [channel]. @@ -1062,9 +1064,10 @@ Value _rgb(String name, List arguments) { arguments[0].assertNumber("red"), arguments[1].assertNumber("green"), arguments[2].assertNumber("blue"), - alpha.andThen((alpha) => - _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha") - .clamp(0, 1)) ?? + alpha.andThen((alpha) => clampLikeCss( + _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"), + 0, + 1)) ?? 1, fromRgbFunction: true); } @@ -1100,8 +1103,8 @@ Value _rgbTwoArg(String name, List arguments) { } var alpha = arguments[1].assertNumber("alpha"); - return color - .changeAlpha(_percentageOrUnitless(alpha, 1, "alpha").clamp(0, 1)); + return color.changeAlpha( + clampLikeCss(_percentageOrUnitless(alpha, 1, "alpha"), 0, 1)); } /// The implementation of the three- and four-argument `hsl()` and `hsla()` @@ -1120,9 +1123,10 @@ Value _hsl(String name, List arguments) { arguments[0].assertNumber("hue"), arguments[1].assertNumber("saturation"), arguments[2].assertNumber("lightness"), - alpha.andThen((alpha) => - _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha") - .clamp(0, 1)) ?? + alpha.andThen((alpha) => clampLikeCss( + _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"), + 0, + 1)) ?? 1); } @@ -1235,9 +1239,8 @@ SassColor _opacify(String name, List arguments) { "color.adjust() instead with an explicit \$space argument."); } - var result = color.changeAlpha( - (color.alpha + amount.valueInRangeWithUnit(0, 1, "amount", "")) - .clamp(0, 1)); + var result = color.changeAlpha(clampLikeCss( + (color.alpha + amount.valueInRangeWithUnit(0, 1, "amount", "")), 0, 1)); warnForDeprecation( "$name() is deprecated. " @@ -1258,9 +1261,8 @@ SassColor _transparentize(String name, List arguments) { "color.adjust() instead with an explicit \$space argument."); } - var result = color.changeAlpha( - (color.alpha - amount.valueInRangeWithUnit(0, 1, "amount", "")) - .clamp(0, 1)); + var result = color.changeAlpha(clampLikeCss( + (color.alpha - amount.valueInRangeWithUnit(0, 1, "amount", "")), 0, 1)); warnForDeprecation( "$name() is deprecated. " @@ -1390,8 +1392,10 @@ Value _parseChannels(String functionName, Value input, var alpha = switch (alphaValue) { null => 1.0, SassString(hasQuotes: false, text: 'none') => null, - _ => _percentageOrUnitless(alphaValue.assertNumber(name), 1, 'alpha') - .clamp(0, 1) + _ => clampLikeCss( + _percentageOrUnitless(alphaValue.assertNumber(name), 1, 'alpha'), + 0, + 1) .toDouble() }; @@ -1549,10 +1553,10 @@ double? _channelFromValue(ColorChannel channel, SassNumber? value, _percentageOrUnitless(value, channel.max, channel.name), LinearChannel() when !clamp => _percentageOrUnitless(value, channel.max, channel.name), - LinearChannel(:var lowerClamped, :var upperClamped) => - _percentageOrUnitless(value, channel.max, channel.name).clamp( - lowerClamped ? channel.min : double.negativeInfinity, - upperClamped ? channel.max : double.infinity), + LinearChannel(:var lowerClamped, :var upperClamped) => clampLikeCss( + _percentageOrUnitless(value, channel.max, channel.name), + lowerClamped ? channel.min : double.negativeInfinity, + upperClamped ? channel.max : double.infinity), _ => value.coerceValueToUnit('deg', channel.name) % 360 }); diff --git a/lib/src/js/legacy/value/color.dart b/lib/src/js/legacy/value/color.dart index 6aae64426..0545e761e 100644 --- a/lib/src/js/legacy/value/color.dart +++ b/lib/src/js/legacy/value/color.dart @@ -4,6 +4,7 @@ import 'package:js/js.dart'; +import '../../../util/nullable.dart'; import '../../../util/number.dart'; import '../../../value.dart'; import '../../reflection.dart'; @@ -45,8 +46,8 @@ final JSClass legacyColorClass = createJSClass('sass.types.Color', red = redOrArgb!; } - thisArg.dartValue = SassColor.rgb( - _clamp(red), _clamp(green), _clamp(blue), alpha?.clamp(0, 1) ?? 1); + thisArg.dartValue = SassColor.rgb(_clamp(red), _clamp(green), _clamp(blue), + alpha.andThen((alpha) => clampLikeCss(alpha.toDouble(), 0, 1)) ?? 1); }) ..defineMethods({ 'getR': (_NodeSassColor thisArg) => thisArg.dartValue.red, @@ -63,10 +64,11 @@ final JSClass legacyColorClass = createJSClass('sass.types.Color', thisArg.dartValue = thisArg.dartValue.changeRgb(blue: _clamp(value)); }, 'setA': (_NodeSassColor thisArg, num value) { - thisArg.dartValue = thisArg.dartValue.changeRgb(alpha: value.clamp(0, 1)); + thisArg.dartValue = thisArg.dartValue + .changeRgb(alpha: clampLikeCss(value.toDouble(), 0, 1)); } }); /// Clamps [channel] within the range 0, 255 and rounds it to the nearest /// integer. -int _clamp(num channel) => fuzzyRound(channel.clamp(0, 255)); +int _clamp(num channel) => fuzzyRound(clampLikeCss(channel.toDouble(), 0, 255)); diff --git a/lib/src/util/number.dart b/lib/src/util/number.dart index 9eca12928..8aad45581 100644 --- a/lib/src/util/number.dart +++ b/lib/src/util/number.dart @@ -140,6 +140,12 @@ double moduloLikeSass(double num1, double num2) { return result == 0 ? 0 : result + num2; } +//// Returns [num] clamped between [lowerBound] and [upperBound], with `NaN` +//// preferring the lower bound (unlike Dart for which it prefers the upper +//// bound). +double clampLikeCss(double number, double lowerBound, double upperBound) => + number.isNaN ? lowerBound : number.clamp(lowerBound, upperBound); + /// Returns the square root of [number]. SassNumber sqrt(SassNumber number) { number.assertNoUnits("number"); diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index b720a0c2d..5384725e7 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -523,22 +523,6 @@ class SassColor extends Value { alpha.andThen((alpha) => fuzzyAssertRange(alpha, 0, 1, "alpha")) { assert(format == null || _space == ColorSpace.rgb); assert(space != ColorSpace.lms); - - _checkChannel(channel0OrNull, space.channels[0].name); - _checkChannel(channel1OrNull, space.channels[1].name); - _checkChannel(channel2OrNull, space.channels[2].name); - } - - /// Throws a [RangeError] if [channel] isn't a finite number. - void _checkChannel(double? channel, String name) { - switch (channel) { - case null: - return; - case double(isNaN: true): - throw RangeError.value(channel, name, 'must be a number.'); - case double(isFinite: false): - throw RangeError.value(channel, name, 'must be finite.'); - } } /// If [hue] isn't null, normalizes it to the range `[0, 360)`. diff --git a/lib/src/value/color/gamut_map_method/clip.dart b/lib/src/value/color/gamut_map_method/clip.dart index 363f59374..14420dea3 100644 --- a/lib/src/value/color/gamut_map_method/clip.dart +++ b/lib/src/value/color/gamut_map_method/clip.dart @@ -4,6 +4,7 @@ import 'package:meta/meta.dart'; +import '../../../util/number.dart'; import '../../color.dart'; /// Gamut mapping by clipping individual channels. @@ -24,7 +25,7 @@ final class ClipGamutMap extends GamutMapMethod { double? _clampChannel(double? value, ColorChannel channel) => value == null ? null : switch (channel) { - LinearChannel(:var min, :var max) => value.clamp(min, max), + LinearChannel(:var min, :var max) => clampLikeCss(value, min, max), _ => value }; } diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index ae70415c3..dec68b275 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -597,14 +597,11 @@ final class _SerializeVisitor _buffer ..write(value.space) ..writeCharCode($lparen); - _writeChannel(value.channel0OrNull); - if (!_isCompressed && !value.isChannel0Missing) _buffer.write('deg'); + _writeChannel(value.channel0OrNull, _isCompressed ? null : 'deg'); _buffer.writeCharCode($space); - _writeChannel(value.channel1OrNull); - if (!value.isChannel1Missing) _buffer.writeCharCode($percent); + _writeChannel(value.channel1OrNull, '%'); _buffer.writeCharCode($space); - _writeChannel(value.channel2OrNull); - if (!value.isChannel2Missing) _buffer.writeCharCode($percent); + _writeChannel(value.channel2OrNull, '%'); _maybeWriteSlashAlpha(value); _buffer.writeCharCode($rparen); @@ -672,10 +669,8 @@ final class _SerializeVisitor _buffer.writeCharCode($space); _writeChannel(value.channel1OrNull); _buffer.writeCharCode($space); - _writeChannel(value.channel2OrNull); - if (!_isCompressed && !value.isChannel2Missing && polar) { - _buffer.write('deg'); - } + _writeChannel( + value.channel2OrNull, polar && !_isCompressed ? 'deg' : null); _maybeWriteSlashAlpha(value); _buffer.writeCharCode($rparen); @@ -685,11 +680,14 @@ final class _SerializeVisitor } /// Writes a [channel] which may be missing. - void _writeChannel(double? channel) { + void _writeChannel(double? channel, [String? unit]) { if (channel == null) { _buffer.write('none'); - } else { + } else if (channel.isFinite) { _writeNumber(channel); + if (unit != null) _buffer.write(unit); + } else { + visitNumber(SassNumber(channel, unit)); } } @@ -866,13 +864,11 @@ final class _SerializeVisitor var opaque = fuzzyEquals(color.alpha, 1); var hsl = color.toSpace(ColorSpace.hsl); _buffer.write(opaque ? "hsl(" : "hsla("); - _writeNumber(hsl.channel('hue')); + _writeChannel(hsl.channel('hue')); _buffer.write(_commaSeparator); - _writeNumber(hsl.channel('saturation')); - _buffer.writeCharCode($percent); + _writeChannel(hsl.channel('saturation'), '%'); _buffer.write(_commaSeparator); - _writeNumber(hsl.channel('lightness')); - _buffer.writeCharCode($percent); + _writeChannel(hsl.channel('lightness'), '%'); if (!opaque) { _buffer.write(_commaSeparator); @@ -948,11 +944,7 @@ final class _SerializeVisitor _writeOptionalSpace(); _buffer.writeCharCode($slash); _writeOptionalSpace(); - if (color.isAlphaMissing) { - _buffer.write('none'); - } else { - _writeNumber(color.alpha); - } + _writeChannel(color.alphaOrNull); } void visitFunction(SassFunction function) { From fc065ddd8cfe9585ca9e461256655ff8d771dd79 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 9 Jul 2024 14:13:55 -0700 Subject: [PATCH 4/5] Avoid `[this]` in Dartdoc comments (#2273) Dartdoc broke this in dart-lang/dartdoc#3765. --- lib/src/ast/css/modifiable/node.dart | 8 ++-- lib/src/ast/sass/at_root_query.dart | 4 +- .../ast/sass/expression/binary_operation.dart | 6 +-- lib/src/ast/sass/expression/list.dart | 2 +- lib/src/ast/sass/expression/string.dart | 2 +- .../ast/sass/expression/unary_operation.dart | 4 +- lib/src/ast/selector.dart | 2 +- lib/src/ast/selector/list.dart | 4 +- lib/src/ast/selector/pseudo.dart | 2 +- lib/src/ast/selector/simple.dart | 2 +- lib/src/async_environment.dart | 4 +- lib/src/environment.dart | 6 +-- lib/src/extend/extension_store.dart | 10 ++--- lib/src/stylesheet_graph.dart | 2 +- lib/src/util/map.dart | 2 +- lib/src/util/multi_span.dart | 2 +- lib/src/util/nullable.dart | 2 +- lib/src/util/span.dart | 4 +- lib/src/value.dart | 42 +++++++++---------- lib/src/value/calculation.dart | 6 +-- lib/src/value/number.dart | 32 +++++++------- 21 files changed, 74 insertions(+), 74 deletions(-) diff --git a/lib/src/ast/css/modifiable/node.dart b/lib/src/ast/css/modifiable/node.dart index bef0be821..1b27f8258 100644 --- a/lib/src/ast/css/modifiable/node.dart +++ b/lib/src/ast/css/modifiable/node.dart @@ -17,7 +17,7 @@ abstract base class ModifiableCssNode extends CssNode { ModifiableCssParentNode? get parent => _parent; ModifiableCssParentNode? _parent; - /// The index of [this] in `parent.children`. + /// The index of `this` in `parent.children`. /// /// This makes [remove] more efficient. int? _indexInParent; @@ -33,7 +33,7 @@ abstract base class ModifiableCssNode extends CssNode { T accept(ModifiableCssVisitor visitor); - /// Removes [this] from [parent]'s child list. + /// Removes `this` from [parent]'s child list. /// /// Throws a [StateError] if [parent] is `null`. void remove() { @@ -65,10 +65,10 @@ abstract base class ModifiableCssParentNode extends ModifiableCssNode : _children = children, children = UnmodifiableListView(children); - /// Returns whether [this] is equal to [other], ignoring their child nodes. + /// Returns whether `this` is equal to [other], ignoring their child nodes. bool equalsIgnoringChildren(ModifiableCssNode other); - /// Returns a copy of [this] with an empty [children] list. + /// Returns a copy of `this` with an empty [children] list. /// /// This is *not* a deep copy. If other parts of this node are modifiable, /// they are shared between the new and old nodes. diff --git a/lib/src/ast/sass/at_root_query.dart b/lib/src/ast/sass/at_root_query.dart index 3bad9cf20..58b2e2f94 100644 --- a/lib/src/ast/sass/at_root_query.dart +++ b/lib/src/ast/sass/at_root_query.dart @@ -61,7 +61,7 @@ final class AtRootQuery { {Object? url, Logger? logger, InterpolationMap? interpolationMap}) => AtRootQueryParser(contents, url: url, logger: logger).parse(); - /// Returns whether [this] excludes [node]. + /// Returns whether `this` excludes [node]. /// /// @nodoc @internal @@ -76,6 +76,6 @@ final class AtRootQuery { }; } - /// Returns whether [this] excludes an at-rule with the given [name]. + /// Returns whether `this` excludes an at-rule with the given [name]. bool excludesName(String name) => (_all || names.contains(name)) != include; } diff --git a/lib/src/ast/sass/expression/binary_operation.dart b/lib/src/ast/sass/expression/binary_operation.dart index dc750900a..15ab22bba 100644 --- a/lib/src/ast/sass/expression/binary_operation.dart +++ b/lib/src/ast/sass/expression/binary_operation.dart @@ -153,13 +153,13 @@ enum BinaryOperator { /// The modulo operator, `%`. modulo('modulo', '%', 6); - /// The English name of [this]. + /// The English name of `this`. final String name; - /// The Sass syntax for [this]. + /// The Sass syntax for `this`. final String operator; - /// The precedence of [this]. + /// The precedence of `this`. /// /// An operator with higher precedence binds tighter. final int precedence; diff --git a/lib/src/ast/sass/expression/list.dart b/lib/src/ast/sass/expression/list.dart index 01416afa4..5bf768cac 100644 --- a/lib/src/ast/sass/expression/list.dart +++ b/lib/src/ast/sass/expression/list.dart @@ -58,7 +58,7 @@ final class ListExpression implements Expression { return buffer.toString(); } - /// Returns whether [expression], contained in [this], needs parentheses when + /// Returns whether [expression], contained in `this`, needs parentheses when /// printed as Sass source. bool _elementNeedsParens(Expression expression) => switch (expression) { ListExpression( diff --git a/lib/src/ast/sass/expression/string.dart b/lib/src/ast/sass/expression/string.dart index 2e7824345..a8539146a 100644 --- a/lib/src/ast/sass/expression/string.dart +++ b/lib/src/ast/sass/expression/string.dart @@ -23,7 +23,7 @@ final class StringExpression implements Expression { /// included. final Interpolation text; - /// Whether [this] has quotes. + /// Whether `this` has quotes. final bool hasQuotes; FileSpan get span => text.span; diff --git a/lib/src/ast/sass/expression/unary_operation.dart b/lib/src/ast/sass/expression/unary_operation.dart index d437fafc2..18e5f0c27 100644 --- a/lib/src/ast/sass/expression/unary_operation.dart +++ b/lib/src/ast/sass/expression/unary_operation.dart @@ -63,10 +63,10 @@ enum UnaryOperator { /// The boolean negation operator, `not`. not('not', 'not'); - /// The English name of [this]. + /// The English name of `this`. final String name; - /// The Sass syntax for [this]. + /// The Sass syntax for `this`. final String operator; const UnaryOperator(this.name, this.operator); diff --git a/lib/src/ast/selector.dart b/lib/src/ast/selector.dart index 953ccf7aa..fcabcdc25 100644 --- a/lib/src/ast/selector.dart +++ b/lib/src/ast/selector.dart @@ -83,7 +83,7 @@ abstract base class Selector implements AstNode { Selector(this.span); - /// Prints a warning if [this] is a bogus selector. + /// Prints a warning if `this` is a bogus selector. /// /// This may only be called from within a custom Sass function. This will /// throw a [SassException] in Dart Sass 2.0.0. diff --git a/lib/src/ast/selector/list.dart b/lib/src/ast/selector/list.dart index 8da4598f6..b45fa2a2d 100644 --- a/lib/src/ast/selector/list.dart +++ b/lib/src/ast/selector/list.dart @@ -96,9 +96,9 @@ final class SelectorList extends Selector { return contents.isEmpty ? null : SelectorList(contents, span); } - /// Returns a new selector list that represents [this] nested within [parent]. + /// Returns a new selector list that represents `this` nested within [parent]. /// - /// By default, this replaces [ParentSelector]s in [this] with [parent]. If + /// By default, this replaces [ParentSelector]s in `this` with [parent]. If /// [preserveParentSelectors] is true, this instead preserves those selectors /// as parent selectors. /// diff --git a/lib/src/ast/selector/pseudo.dart b/lib/src/ast/selector/pseudo.dart index 44a263d15..4301d13e1 100644 --- a/lib/src/ast/selector/pseudo.dart +++ b/lib/src/ast/selector/pseudo.dart @@ -170,7 +170,7 @@ final class PseudoSelector extends SimpleSelector { for (var simple in compound) { if (simple case PseudoSelector(isElement: true)) { // A given compound selector may only contain one pseudo element. If - // [compound] has a different one than [this], unification fails. + // [compound] has a different one than `this`, unification fails. if (isElement) return null; // Otherwise, this is a pseudo selector and should come before pseudo diff --git a/lib/src/ast/selector/simple.dart b/lib/src/ast/selector/simple.dart index d8ae7864d..423ad794c 100644 --- a/lib/src/ast/selector/simple.dart +++ b/lib/src/ast/selector/simple.dart @@ -49,7 +49,7 @@ abstract base class SimpleSelector extends Selector { url: url, logger: logger, allowParent: allowParent) .parseSimpleSelector(); - /// Returns a new [SimpleSelector] based on [this], as though it had been + /// Returns a new [SimpleSelector] based on `this`, as though it had been /// written with [suffix] at the end. /// /// Assumes [suffix] is a valid identifier suffix. If this wouldn't produce a diff --git a/lib/src/async_environment.dart b/lib/src/async_environment.dart index 96cbecc18..5313d8c45 100644 --- a/lib/src/async_environment.dart +++ b/lib/src/async_environment.dart @@ -790,7 +790,7 @@ final class AsyncEnvironment { return Configuration.implicit(configuration); } - /// Returns a module that represents the top-level members defined in [this], + /// Returns a module that represents the top-level members defined in `this`, /// that contains [css] and [preModuleComments] as its CSS, which can be /// extended using [extensionStore]. Module toModule( @@ -802,7 +802,7 @@ final class AsyncEnvironment { forwarded: _forwardedModules.andThen((modules) => MapKeySet(modules))); } - /// Returns a module with the same members and upstream modules as [this], but + /// Returns a module with the same members and upstream modules as `this`, but /// an empty stylesheet and extension store. /// /// This is used when resolving imports, since they need to inject forwarded diff --git a/lib/src/environment.dart b/lib/src/environment.dart index 623f67828..3aa0fa45d 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_environment.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: f7172be68e0a19c4dc2d2ad04fc32a843a98a6bd +// Checksum: e1beeae58a4d5b97cd7d4f01c7d46b0586b508b9 // // ignore_for_file: unused_import @@ -796,7 +796,7 @@ final class Environment { return Configuration.implicit(configuration); } - /// Returns a module that represents the top-level members defined in [this], + /// Returns a module that represents the top-level members defined in `this`, /// that contains [css] and [preModuleComments] as its CSS, which can be /// extended using [extensionStore]. Module toModule( @@ -808,7 +808,7 @@ final class Environment { forwarded: _forwardedModules.andThen((modules) => MapKeySet(modules))); } - /// Returns a module with the same members and upstream modules as [this], but + /// Returns a module with the same members and upstream modules as `this`, but /// an empty stylesheet and extension store. /// /// This is used when resolving imports, since they need to inject forwarded diff --git a/lib/src/extend/extension_store.dart b/lib/src/extend/extension_store.dart index 3637b5aac..5f760005d 100644 --- a/lib/src/extend/extension_store.dart +++ b/lib/src/extend/extension_store.dart @@ -386,12 +386,12 @@ class ExtensionStore { } } - /// Extends [this] with all the extensions in [extensions]. + /// Extends `this` with all the extensions in [extensions]. /// - /// These extensions will extend all selectors already in [this], but they + /// These extensions will extend all selectors already in `this`, but they /// will *not* extend other extensions from [extensionStores]. void addExtensions(Iterable extensionStores) { - // Extensions already in [this] whose extenders are extended by + // Extensions already in `this` whose extenders are extended by // [extensions], and thus which need to be updated. List? extensionsToExtend; @@ -973,8 +973,8 @@ class ExtensionStore { return specificity; } - /// Returns a copy of [this] that extends new selectors, as well as a map - /// (with reference equality) from the selectors extended by [this] to the + /// Returns a copy of `this` that extends new selectors, as well as a map + /// (with reference equality) from the selectors extended by `this` to the /// selectors extended by the new [ExtensionStore]. (ExtensionStore, Map>) clone() { var newSelectors = >>{}; diff --git a/lib/src/stylesheet_graph.dart b/lib/src/stylesheet_graph.dart index 3109fc5f0..245d8b146 100644 --- a/lib/src/stylesheet_graph.dart +++ b/lib/src/stylesheet_graph.dart @@ -385,7 +385,7 @@ class StylesheetNode { _upstreamImports = newUpstreamImports; } - /// Removes [this] as an upstream and downstream node from all the nodes that + /// Removes `this` as an upstream and downstream node from all the nodes that /// import it and that it imports. void _remove() { for (var node in {...upstream.values, ...upstreamImports.values}) { diff --git a/lib/src/util/map.dart b/lib/src/util/map.dart index a61c151df..865b213bc 100644 --- a/lib/src/util/map.dart +++ b/lib/src/util/map.dart @@ -5,7 +5,7 @@ import 'option.dart'; extension MapExtensions on Map { - /// If [this] doesn't contain the given [key], sets that key to [value] and + /// If `this` doesn't contain the given [key], sets that key to [value] and /// returns it. /// /// Otherwise, calls [merge] with the existing value and [value] and sets diff --git a/lib/src/util/multi_span.dart b/lib/src/util/multi_span.dart index 24ca42b48..41121042a 100644 --- a/lib/src/util/multi_span.dart +++ b/lib/src/util/multi_span.dart @@ -70,7 +70,7 @@ class MultiSpan implements FileSpan { primaryColor: primaryColor, secondaryColor: secondaryColor); - /// Returns a copy of [this] with [newPrimary] as its primary span. + /// Returns a copy of `this` with [newPrimary] as its primary span. MultiSpan _withPrimary(FileSpan newPrimary) => MultiSpan._(newPrimary, primaryLabel, secondarySpans); } diff --git a/lib/src/util/nullable.dart b/lib/src/util/nullable.dart index ad4a8ba2f..cf24c880e 100644 --- a/lib/src/util/nullable.dart +++ b/lib/src/util/nullable.dart @@ -3,7 +3,7 @@ // https://opensource.org/licenses/MIT. extension NullableExtension on T? { - /// If [this] is `null`, returns `null`. Otherwise, runs [fn] and returns its + /// If `this` is `null`, returns `null`. Otherwise, runs [fn] and returns its /// result. /// /// Based on Rust's `Option.and_then`. diff --git a/lib/src/util/span.dart b/lib/src/util/span.dart index bcb9b6165..6328f4aed 100644 --- a/lib/src/util/span.dart +++ b/lib/src/util/span.dart @@ -84,10 +84,10 @@ extension SpanExtensions on FileSpan { return subspan(scanner.position).trimLeft(); } - /// Whether [this] FileSpan contains the [target] FileSpan. + /// Whether this [FileSpan] contains the [target] FileSpan. /// /// Validates the FileSpans to be in the same file and for the [target] to be - /// within [this] FileSpan inclusive range [start,end]. + /// within this [FileSpan]'s inclusive range `[start,end]`. bool contains(FileSpan target) => file.url == target.file.url && start.offset <= target.start.offset && diff --git a/lib/src/value.dart b/lib/src/value.dart index ae9cb0bf0..1a854afb9 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -100,7 +100,7 @@ abstract class Value { @internal bool get isVar => false; - /// Returns Dart's `null` value if this is [sassNull], and returns [this] + /// Returns Dart's `null` value if this is [sassNull], and returns `this` /// otherwise. Value? get realNull => this; @@ -148,7 +148,7 @@ abstract class Value { return index < 0 ? lengthAsList + index : index - 1; } - /// Throws a [SassScriptException] if [this] isn't a boolean. + /// Throws a [SassScriptException] if `this` isn't a boolean. /// /// Note that generally, functions should use [isTruthy] rather than requiring /// a literal boolean. @@ -158,53 +158,53 @@ abstract class Value { SassBoolean assertBoolean([String? name]) => throw SassScriptException("$this is not a boolean.", name); - /// Throws a [SassScriptException] if [this] isn't a calculation. + /// Throws a [SassScriptException] if `this` isn't a calculation. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. SassCalculation assertCalculation([String? name]) => throw SassScriptException("$this is not a calculation.", name); - /// Throws a [SassScriptException] if [this] isn't a color. + /// Throws a [SassScriptException] if `this` isn't a color. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. SassColor assertColor([String? name]) => throw SassScriptException("$this is not a color.", name); - /// Throws a [SassScriptException] if [this] isn't a function reference. + /// Throws a [SassScriptException] if `this` isn't a function reference. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. SassFunction assertFunction([String? name]) => throw SassScriptException("$this is not a function reference.", name); - /// Throws a [SassScriptException] if [this] isn't a mixin reference. + /// Throws a [SassScriptException] if `this` isn't a mixin reference. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. SassMixin assertMixin([String? name]) => throw SassScriptException("$this is not a mixin reference.", name); - /// Throws a [SassScriptException] if [this] isn't a map. + /// Throws a [SassScriptException] if `this` isn't a map. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. SassMap assertMap([String? name]) => throw SassScriptException("$this is not a map.", name); - /// Returns [this] as a [SassMap] if it is one (including empty lists, which + /// Returns `this` as a [SassMap] if it is one (including empty lists, which /// count as empty maps) or returns `null` if it's not. SassMap? tryMap() => null; - /// Throws a [SassScriptException] if [this] isn't a number. + /// Throws a [SassScriptException] if `this` isn't a number. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. SassNumber assertNumber([String? name]) => throw SassScriptException("$this is not a number.", name); - /// Throws a [SassScriptException] if [this] isn't a string. + /// Throws a [SassScriptException] if `this` isn't a string. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. @@ -241,7 +241,7 @@ abstract class Value { /// Converts a `selector-parse()`-style input into a string that can be /// parsed. /// - /// Throws a [SassScriptException] if [this] isn't a type or a structure that + /// Throws a [SassScriptException] if `this` isn't a type or a structure that /// can be parsed as a selector. String _selectorString([String? name]) { if (_selectorStringOrNull() case var string?) return string; @@ -255,7 +255,7 @@ abstract class Value { /// Converts a `selector-parse()`-style input into a string that can be /// parsed. /// - /// Returns `null` if [this] isn't a type or a structure that can be parsed as + /// Returns `null` if `this` isn't a type or a structure that can be parsed as /// a selector. String? _selectorStringOrNull() { var self = this; @@ -397,7 +397,7 @@ abstract class Value { @internal Value unaryNot() => sassFalse; - /// Returns a copy of [this] without [SassNumber.asSlash] set. + /// Returns a copy of `this` without [SassNumber.asSlash] set. /// /// If this isn't a [SassNumber], returns it as-is. /// @@ -405,9 +405,9 @@ abstract class Value { @internal Value withoutSlash() => this; - /// Returns a valid CSS representation of [this]. + /// Returns a valid CSS representation of `this`. /// - /// Throws a [SassScriptException] if [this] can't be represented in plain + /// Throws a [SassScriptException] if `this` can't be represented in plain /// CSS. Use [toString] instead to get a string representation even if this /// isn't valid CSS. // @@ -416,11 +416,11 @@ abstract class Value { String toCssString({@internal bool quote = true}) => serializeValue(this, quote: quote); - /// Returns a string representation of [this]. + /// Returns a string representation of `this`. /// /// Note that this is equivalent to calling `inspect()` on the value, and thus /// won't reflect the user's output settings. [toCssString] should be used - /// instead to convert [this] to CSS. + /// instead to convert `this` to CSS. String toString() => serializeValue(this, inspect: true); } @@ -431,7 +431,7 @@ abstract class Value { /// /// {@category Value} extension SassApiValue on Value { - /// Parses [this] as a selector list, in the same manner as the + /// Parses `this` as a selector list, in the same manner as the /// `selector-parse()` function. /// /// Throws a [SassScriptException] if this isn't a type that can be parsed as a @@ -455,7 +455,7 @@ extension SassApiValue on Value { } } - /// Parses [this] as a simple selector, in the same manner as the + /// Parses `this` as a simple selector, in the same manner as the /// `selector-parse()` function. /// /// Throws a [SassScriptException] if this isn't a type that can be parsed as a @@ -480,7 +480,7 @@ extension SassApiValue on Value { } } - /// Parses [this] as a compound selector, in the same manner as the + /// Parses `this` as a compound selector, in the same manner as the /// `selector-parse()` function. /// /// Throws a [SassScriptException] if this isn't a type that can be parsed as a @@ -505,7 +505,7 @@ extension SassApiValue on Value { } } - /// Parses [this] as a complex selector, in the same manner as the + /// Parses `this` as a complex selector, in the same manner as the /// `selector-parse()` function. /// /// Throws a [SassScriptException] if this isn't a type that can be parsed as a diff --git a/lib/src/value/calculation.dart b/lib/src/value/calculation.dart index cbb8b92e6..261400dc2 100644 --- a/lib/src/value/calculation.dart +++ b/lib/src/value/calculation.dart @@ -906,13 +906,13 @@ enum CalculationOperator { /// The division operator. dividedBy('divided by', '/', 2); - /// The English name of [this]. + /// The English name of `this`. final String name; - /// The CSS syntax for [this]. + /// The CSS syntax for `this`. final String operator; - /// The precedence of [this]. + /// The precedence of `this`. /// /// An operator with higher precedence binds tighter. /// diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index 154367554..f2527ad0b 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -190,7 +190,7 @@ abstract class SassNumber extends Value { /// The value of this number. /// - /// Note that Sass stores all numbers as [double]s even if if [this] + /// 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. @@ -209,14 +209,14 @@ abstract class SassNumber extends Value { /// This number's denominator units. List get denominatorUnits; - /// Whether [this] has any units. + /// Whether `this` has any units. /// /// If a function expects a number to have no units, it should use /// [assertNoUnits]. If it expects the number to have a particular unit, it /// should use [assertUnit]. bool get hasUnits; - /// Whether [this] has more than one numerator unit, or any denominator units. + /// Whether `this` has more than one numerator unit, or any denominator units. /// /// This is `true` for numbers whose units make them unrepresentable as CSS /// lengths. @@ -229,7 +229,7 @@ abstract class SassNumber extends Value { @internal final (SassNumber, SassNumber)? asSlash; - /// Whether [this] is an integer, according to [fuzzyEquals]. + /// Whether `this` is an integer, according to [fuzzyEquals]. /// /// The [int] value can be accessed using [asInt] or [assertInt]. Note that /// this may return `false` for very large doubles even though they may be @@ -237,7 +237,7 @@ abstract class SassNumber extends Value { /// representation for integers that large. bool get isInt => fuzzyIsInt(value); - /// If [this] is an integer according to [isInt], returns [value] as an [int]. + /// If `this` is an integer according to [isInt], returns [value] as an [int]. /// /// Otherwise, returns `null`. int? get asInt => fuzzyAsInt(value); @@ -304,20 +304,20 @@ abstract class SassNumber extends Value { T accept(ValueVisitor visitor) => visitor.visitNumber(this); - /// Returns a number with the same units as [this] but with [value] as its + /// Returns a number with the same units as `this` but with [value] as its /// value. /// /// @nodoc @protected SassNumber withValue(num value); - /// Returns a copy of [this] without [asSlash] set. + /// Returns a copy of `this` without [asSlash] set. /// /// @nodoc @internal SassNumber withoutSlash() => asSlash == null ? this : withValue(value); - /// Returns a copy of [this] with [asSlash] set to a pair containing + /// Returns a copy of `this` with [asSlash] set to a pair containing /// [numerator] and [denominator]. /// /// @nodoc @@ -365,10 +365,10 @@ abstract class SassNumber extends Value { "Expected $this to be within $min$unit and $max$unit.", name); } - /// Returns whether [this] has [unit] as its only unit (and as a numerator). + /// Returns whether `this` has [unit] as its only unit (and as a numerator). bool hasUnit(String unit); - /// Returns whether [this] has units that are compatible with [other]. + /// Returns whether `this` has units that are compatible with [other]. /// /// Unlike [isComparableTo], unitless numbers are only considered compatible /// with other unitless numbers. @@ -378,17 +378,17 @@ abstract class SassNumber extends Value { return isComparableTo(other); } - /// Returns whether [this] has units that are possibly-compatible with + /// Returns whether `this` has units that are possibly-compatible with /// [other], as defined by the Sass spec. @internal bool hasPossiblyCompatibleUnits(SassNumber other); - /// Returns whether [this] can be coerced to the given [unit]. + /// Returns whether `this` can be coerced to the given [unit]. /// /// This always returns `true` for a unitless number. bool compatibleWithUnit(String unit); - /// Throws a [SassScriptException] unless [this] has [unit] as its only unit + /// Throws a [SassScriptException] unless `this` has [unit] as its only unit /// (and as a numerator). /// /// If this came from a function argument, [name] is the argument name @@ -398,7 +398,7 @@ abstract class SassNumber extends Value { throw SassScriptException('Expected $this to have unit "$unit".', name); } - /// Throws a [SassScriptException] unless [this] has no units. + /// Throws a [SassScriptException] unless `this` has no units. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. @@ -574,7 +574,7 @@ abstract class SassNumber extends Value { /// /// If [other] is passed, it should be the number from which [newNumerators] /// and [newDenominators] are derived. The [name] and [otherName] are the Sass - /// function parameter names of [this] and [other], respectively, used for + /// function parameter names of `this` and [other], respectively, used for /// error reporting. double _coerceOrConvertValue( List newNumerators, List newDenominators, @@ -778,7 +778,7 @@ abstract class SassNumber extends Value { return operation(value, other.coerceValueToMatch(this)); } on SassScriptException { // If the conversion fails, re-run it in the other direction. This will - // generate an error message that prints [this] before [other], which is + // generate an error message that prints `this` before [other], which is // more readable. coerceValueToMatch(other); rethrow; // This should be unreachable. From c681af384fe88037eafa3c9d0889b17064fe8a0a Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 9 Jul 2024 14:15:57 -0700 Subject: [PATCH 5/5] Fix Color-specific `[this]` --- lib/src/value.dart | 2 +- lib/src/value/color.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/value.dart b/lib/src/value.dart index 1a854afb9..81f4df27e 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -211,7 +211,7 @@ abstract class Value { SassString assertString([String? name]) => throw SassScriptException("$this is not a string.", name); - /// Throws a [SassScriptException] if [this] isn't a list of the sort commonly + /// Throws a [SassScriptException] if `this` isn't a list of the sort commonly /// used in plain CSS expression syntax: space-separated and unbracketed. /// /// If [allowSlash] is `true`, this allows slash-separated lists as well. diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index 5384725e7..8843aed27 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -787,12 +787,12 @@ class SassColor extends Value { new1 ?? channel1OrNull, new2 ?? channel2OrNull, alpha ?? alphaOrNull); } - /// Returns a color partway between [this] and [other] according to [method], + /// Returns a color partway between `this` and [other] according to [method], /// as defined by the CSS Color 4 [color interpolation] procedure. /// /// [color interpolation]: https://www.w3.org/TR/css-color-4/#interpolation /// - /// The [weight] is a number between 0 and 1 that indicates how much of [this] + /// The [weight] is a number between 0 and 1 that indicates how much of `this` /// should be in the resulting color. It defaults to 0.5. /// /// If [legacyMissing] is false, this will convert missing channels in legacy