Skip to content

Commit a16f014

Browse files
authored
Emit floating-point rgb() values as percentages (#2800)
Closes #2799
1 parent 4ed2c88 commit a16f014

7 files changed

Lines changed: 94 additions & 62 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
## 1.101.4-dev
1+
## 1.101.4
2+
3+
* Avoid emitting `rgb()` or `rgba()` functions with non-percent decimal
4+
channels. Older browsers only support integer values or (potentially decimal)
5+
percentages for these functions, so in order to preserve
6+
backwards-compatibility while retaining full precision for modern browsers,
7+
legacy colors that contain at least one non-integer channel will now use
8+
percentages for their channels (for example, `rgb(0%, 100%, 50%)` rather than
9+
`rgb(0, 255, 127.5)`).
210

311
* Fix a bug where the values of plain-CSS `if()` expressions were emitted using
412
their `meta.inspect()` format rather than their CSS serialization format.

lib/src/visitor/serialize.dart

Lines changed: 80 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,10 @@ String serializeSelector(Selector selector, {bool inspect = false}) {
114114
final class _SerializeVisitor
115115
implements CssVisitor<void>, ValueVisitor<void>, SelectorVisitor<void> {
116116
/// A buffer that contains the CSS produced so far.
117-
final SourceMapBuffer _buffer;
117+
///
118+
/// This can be temporarily replaced to capture a particular chunk of
119+
/// serialization to a string.
120+
SourceMapBuffer _buffer;
118121

119122
/// The current indentation of the CSS output.
120123
var _indentation = 0;
@@ -763,42 +766,17 @@ final class _SerializeVisitor
763766
// In compressed mode, emit colors in the shortest representation possible.
764767
if (_isCompressed) {
765768
var rgb = color.toSpace(ColorSpace.rgb);
766-
if (opaque && _tryIntegerRgb(rgb)) return;
767-
768-
var red = _writeNumberToString(rgb.channel0);
769-
var green = _writeNumberToString(rgb.channel1);
770-
var blue = _writeNumberToString(rgb.channel2);
769+
if (opaque && _tryHexOrNamedRgb(rgb)) return;
771770

772-
var hsl = color.toSpace(ColorSpace.hsl);
773-
var hue = _writeNumberToString(hsl.channel0);
774-
var saturation = _writeNumberToString(hsl.channel1);
775-
var lightness = _writeNumberToString(hsl.channel2);
771+
var rgbString = _capture(() => _writeRgb(rgb));
772+
var hslString = _capture(() => _writeHsl(rgb.toSpace(ColorSpace.hsl)));
776773

777774
// Add two characters for HSL for the %s on saturation and lightness.
778-
if (red.length + green.length + blue.length <=
779-
hue.length + saturation.length + lightness.length + 2) {
780-
_buffer
781-
..write(opaque ? 'rgb(' : 'rgba(')
782-
..write(red)
783-
..writeCharCode($comma)
784-
..write(green)
785-
..writeCharCode($comma)
786-
..write(blue);
775+
if (rgbString.length <= hslString.length + 2) {
776+
_buffer.write(rgbString);
787777
} else {
788-
_buffer
789-
..write(opaque ? 'hsl(' : 'hsla(')
790-
..write(hue)
791-
..writeCharCode($comma)
792-
..write(saturation)
793-
..write('%,')
794-
..write(lightness)
795-
..writeCharCode($percent);
796-
}
797-
if (!opaque) {
798-
_buffer.writeCharCode($comma);
799-
_writeNumber(color.alpha);
778+
_buffer.write(hslString);
800779
}
801-
_buffer.writeCharCode($rparen);
802780
return;
803781
}
804782

@@ -852,7 +830,7 @@ final class _SerializeVisitor
852830
///
853831
/// Otherwise, writes nothing and returns `false`. Assumes [value] is in the
854832
/// RGB space.
855-
bool _tryIntegerRgb(SassColor rgb) {
833+
bool _tryHexOrNamedRgb(SassColor rgb) {
856834
assert(rgb.space == ColorSpace.rgb);
857835
if (!_canUseHex(rgb)) return false;
858836

@@ -898,11 +876,14 @@ final class _SerializeVisitor
898876
var opaque = fuzzyEquals(color.alpha, 1);
899877
var rgb = color.toSpace(ColorSpace.rgb);
900878
_buffer.write(opaque ? "rgb(" : "rgba(");
901-
_writeNumber(rgb.channel('red'));
902-
_buffer.write(_commaSeparator);
903-
_writeNumber(rgb.channel('green'));
904-
_buffer.write(_commaSeparator);
905-
_writeNumber(rgb.channel('blue'));
879+
880+
if (!_tryIntegerRgbChannels(rgb)) {
881+
_writeChannel(color.channel0 * 100 / 255, '%');
882+
_buffer.write(_commaSeparator);
883+
_writeChannel(color.channel1 * 100 / 255, '%');
884+
_buffer.write(_commaSeparator);
885+
_writeChannel(color.channel2 * 100 / 255, '%');
886+
}
906887

907888
if (!opaque) {
908889
_buffer.write(_commaSeparator);
@@ -912,6 +893,32 @@ final class _SerializeVisitor
912893
_buffer.writeCharCode($rparen);
913894
}
914895

896+
/// If [value]'s channels are all integers, writes them as such and returns
897+
/// `true`.
898+
///
899+
/// Otherwise, writes nothing and returns `false`. Assumes [value] is in the
900+
/// RGB space.
901+
bool _tryIntegerRgbChannels(SassColor rgb) {
902+
assert(rgb.space == ColorSpace.rgb);
903+
904+
var red = _asInt(rgb.channel0);
905+
if (red == null) return false;
906+
907+
var green = _asInt(rgb.channel1);
908+
if (green == null) return false;
909+
910+
var blue = _asInt(rgb.channel2);
911+
if (blue == null) return false;
912+
913+
_buffer.write(_removeExponent(red.toString()));
914+
_buffer.write(_commaSeparator);
915+
_buffer.write(_removeExponent(green.toString()));
916+
_buffer.write(_commaSeparator);
917+
_buffer.write(_removeExponent(blue.toString()));
918+
919+
return true;
920+
}
921+
915922
/// Writes [value] as an `hsl()` or `hsla()` function.
916923
void _writeHsl(SassColor color) {
917924
var opaque = fuzzyEquals(color.alpha, 1);
@@ -1143,38 +1150,31 @@ final class _SerializeVisitor
11431150
}
11441151
}
11451152

1146-
/// Like [_writeNumber], but returns a string rather than writing to
1147-
/// [_buffer].
1148-
String _writeNumberToString(double number) {
1149-
var buffer = NoSourceMapBuffer();
1150-
_writeNumber(number, buffer);
1151-
return buffer.toString();
1152-
}
1153-
11541153
/// Writes [number] without exponent notation and with at most
11551154
/// [SassNumber.precision] digits after the decimal point.
11561155
///
11571156
/// The number is written to [buffer], which defaults to [_buffer].
1158-
void _writeNumber(double number, [SourceMapBuffer? buffer]) {
1159-
buffer ??= _buffer;
1157+
void _writeNumber(double number) {
1158+
if (!number.isFinite) {
1159+
visitCalculation(
1160+
SassCalculation.unsimplified('calc', [SassNumber(number)]));
1161+
return;
1162+
}
11601163

11611164
// Dart always converts integers to strings in the obvious way, so all we
11621165
// have to do is clamp doubles that are close to being integers.
1163-
if (fuzzyAsInt(number) case var integer?
1164-
// In inspect mode, we want to show the full precision of every number,
1165-
// so we only write them as integers when they're precisely equal.
1166-
when !_inspect || number == integer) {
1166+
if (_asInt(number) case var integer?) {
11671167
// JS still uses exponential notation for integers, so we have to handle
11681168
// it here.
1169-
buffer.write(_removeExponent(integer.toString()));
1169+
_buffer.write(_removeExponent(integer.toString()));
11701170
return;
11711171
}
11721172

11731173
var text = _removeExponent(number.toString());
11741174

11751175
// Write the number at full precision in inspect mode.
11761176
if (_inspect) {
1177-
buffer.write(text);
1177+
_buffer.write(text);
11781178
return;
11791179
}
11801180

@@ -1185,11 +1185,22 @@ final class _SerializeVisitor
11851185

11861186
if (canWriteDirectly) {
11871187
if (_isCompressed && text.codeUnitAt(0) == $0) text = text.substring(1);
1188-
buffer.write(text);
1188+
_buffer.write(text);
11891189
return;
11901190
}
11911191

1192-
_writeRounded(text, buffer);
1192+
_writeRounded(text, _buffer);
1193+
}
1194+
1195+
/// If [number] is close enough to an integer, returns it as one.
1196+
///
1197+
/// Normally, "close enough" includes fuzzy matching, but in inspect mode we
1198+
/// want to show the full precision of every number so "close enough" requires
1199+
/// literally being an integer.
1200+
int? _asInt(double number) {
1201+
if (_inspect) return fuzzyAsInt(number);
1202+
var rounded = number.round();
1203+
return rounded == number ? rounded : null;
11931204
}
11941205

11951206
/// If [text] is written in exponent notation, returns a string representation
@@ -1854,6 +1865,19 @@ final class _SerializeVisitor
18541865
bool _isInvisible(CssNode node) =>
18551866
!_inspect &&
18561867
(_isCompressed ? node.isInvisibleHidingComments : node.isInvisible);
1868+
1869+
/// Runs [callback] without adding to [_buffer] and returns the text it would
1870+
/// have emitted.
1871+
String _capture(void Function() callback) {
1872+
var oldBuffer = _buffer;
1873+
_buffer = NoSourceMapBuffer();
1874+
try {
1875+
callback();
1876+
return _buffer.toString();
1877+
} finally {
1878+
_buffer = oldBuffer;
1879+
}
1880+
}
18571881
}
18581882

18591883
/// An enum of generated CSS styles.

pkg/sass-parser/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## 0.4.50-dev
1+
## 0.4.50
22

33
* No user-visible changes.
44

pkg/sass-parser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sass-parser",
3-
"version": "0.4.50-dev",
3+
"version": "0.4.50",
44
"description": "A PostCSS-compatible wrapper of the official Sass parser",
55
"repository": "sass/dart-sass",
66
"author": "Google Inc.",

pkg/sass_api/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## 17.7.4-dev
1+
## 17.7.4
22

33
* No user-visible changes.
44

pkg/sass_api/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: sass_api
22
# Note: Every time we add a new Sass AST node, we need to bump the *major*
33
# version because it's a breaking change for anyone who's implementing the
44
# visitor interface(s).
5-
version: 17.7.4-dev
5+
version: 17.7.4
66
description: Additional APIs for Dart Sass.
77
homepage: https://github.com/sass/dart-sass
88

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass
2-
version: 1.101.4-dev
2+
version: 1.101.4
33
description: A Sass implementation in Dart.
44
homepage: https://github.com/sass/dart-sass
55

0 commit comments

Comments
 (0)