Skip to content

Commit 0aca829

Browse files
authored
Merge pull request #525 from sass/color-4-rgb-hsl
Add support for CSS Color Level 4 rgb() and hsl() syntax
2 parents 13006e9 + 50efdab commit 0aca829

File tree

8 files changed

+195
-158
lines changed

8 files changed

+195
-158
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
* Add support for passing arguments to `@content` blocks. See [the
44
proposal][content-args] for details.
55

6+
* Add support for the new `rgb()` and `hsl()` syntax introduced in CSS Colors
7+
Level 4, such as `rgb(0% 100% 0% / 0.5)`. See [the proposal][color-4-rgb-hsl]
8+
for more details.
9+
610
* Add support for interpolation in at-rule names. See [the
711
proposal][at-rule-interpolation] for details.
812

@@ -17,6 +21,7 @@
1721
* Properly compile selectors that end in escaped whitespace.
1822

1923
[content-args]: https://github.com/sass/language/blob/master/accepted/content-args.md
24+
[color-4-rgb-hsl]: https://github.com/sass/language/blob/master/accepted/color-4-rgb-hsl.md
2025
[at-rule-interpolation]: https://github.com/sass/language/blob/master/accepted/at-rule-interpolation.md
2126

2227
### JavaScript API

lib/src/functions.dart

Lines changed: 161 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -44,100 +44,26 @@ final List<BuiltInCallable> coreFunctions = new UnmodifiableListView([
4444
// ### RGB
4545

4646
new BuiltInCallable.overloaded("rgb", {
47-
r"$red, $green, $blue": (arguments) {
48-
if (arguments[0].isSpecialNumber ||
49-
arguments[1].isSpecialNumber ||
50-
arguments[2].isSpecialNumber) {
51-
return _functionString('rgb', arguments);
52-
}
53-
54-
var red = arguments[0].assertNumber("red");
55-
var green = arguments[1].assertNumber("green");
56-
var blue = arguments[2].assertNumber("blue");
57-
58-
return new SassColor.rgb(
59-
fuzzyRound(_percentageOrUnitless(red, 255, "red")),
60-
fuzzyRound(_percentageOrUnitless(green, 255, "green")),
61-
fuzzyRound(_percentageOrUnitless(blue, 255, "blue")));
62-
},
63-
r"$red, $green": (arguments) {
64-
// rgb(123, var(--foo)) is valid CSS because --foo might be `456, 789` and
65-
// functions are parsed after variable substitution.
66-
if (arguments[0].isVar || arguments[1].isVar) {
67-
return _functionString('rgb', arguments);
68-
} else {
69-
throw new SassScriptException(r"Missing argument $blue.");
70-
}
71-
},
72-
r"$red": (arguments) {
73-
if (arguments.first.isVar) {
74-
return _functionString('rgb', arguments);
75-
} else {
76-
throw new SassScriptException(r"Missing argument $green.");
77-
}
47+
r"$red, $green, $blue, $alpha": (arguments) => _rgb("rgb", arguments),
48+
r"$red, $green, $blue": (arguments) => _rgb("rgb", arguments),
49+
r"$color, $alpha": (arguments) => _rgbTwoArg("rgb", arguments),
50+
r"$channels": (arguments) {
51+
var parsed = _parseChannels(
52+
"rgb", [r"$red", r"$green", r"$blue"], arguments.first);
53+
return parsed is SassString ? parsed : _rgb("rgb", parsed as List<Value>);
7854
}
7955
}),
8056

8157
new BuiltInCallable.overloaded("rgba", {
82-
r"$red, $green, $blue, $alpha": (arguments) {
83-
if (arguments[0].isSpecialNumber ||
84-
arguments[1].isSpecialNumber ||
85-
arguments[2].isSpecialNumber ||
86-
arguments[3].isSpecialNumber) {
87-
return _functionString('rgba', arguments);
88-
}
89-
90-
var red = arguments[0].assertNumber("red");
91-
var green = arguments[1].assertNumber("green");
92-
var blue = arguments[2].assertNumber("blue");
93-
var alpha = arguments[3].assertNumber("alpha");
94-
95-
return new SassColor.rgb(
96-
fuzzyRound(_percentageOrUnitless(red, 255, "red")),
97-
fuzzyRound(_percentageOrUnitless(green, 255, "green")),
98-
fuzzyRound(_percentageOrUnitless(blue, 255, "blue")),
99-
_percentageOrUnitless(alpha, 1, "alpha"));
100-
},
101-
r"$color, $alpha": (arguments) {
102-
// rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456,
103-
// 789` and functions are parsed after variable substitution.
104-
if (arguments[0].isVar) {
105-
return _functionString('rgba', arguments);
106-
} else if (arguments[1].isVar) {
107-
var first = arguments[0];
108-
if (first is SassColor) {
109-
return new SassString(
110-
"rgba(${first.red}, ${first.green}, ${first.blue}, "
111-
"${arguments[1].toCssString()})",
112-
quotes: false);
113-
} else {
114-
return _functionString('rgba', arguments);
115-
}
116-
} else if (arguments[1].isSpecialNumber) {
117-
var color = arguments[0].assertColor("color");
118-
return new SassString(
119-
"rgba(${color.red}, ${color.green}, ${color.blue}, "
120-
"${arguments[1].toCssString()})",
121-
quotes: false);
122-
}
123-
124-
var color = arguments[0].assertColor("color");
125-
var alpha = arguments[1].assertNumber("alpha");
126-
return color.changeAlpha(_percentageOrUnitless(alpha, 1, "alpha"));
127-
},
128-
r"$red, $green, $blue": (arguments) {
129-
if (arguments[0].isVar || arguments[1].isVar || arguments[2].isVar) {
130-
return _functionString('rgba', arguments);
131-
} else {
132-
throw new SassScriptException(r"Missing argument $alpha.");
133-
}
134-
},
135-
r"$red": (arguments) {
136-
if (arguments.first.isVar) {
137-
return _functionString('rgba', arguments);
138-
} else {
139-
throw new SassScriptException(r"Missing argument $green.");
140-
}
58+
r"$red, $green, $blue, $alpha": (arguments) => _rgb("rgba", arguments),
59+
r"$red, $green, $blue": (arguments) => _rgb("rgba", arguments),
60+
r"$color, $alpha": (arguments) => _rgbTwoArg("rgba", arguments),
61+
r"$channels": (arguments) {
62+
var parsed = _parseChannels(
63+
"rgba", [r"$red", r"$green", r"$blue"], arguments.first);
64+
return parsed is SassString
65+
? parsed
66+
: _rgb("rgba", parsed as List<Value>);
14167
}
14268
}),
14369

@@ -163,20 +89,9 @@ final List<BuiltInCallable> coreFunctions = new UnmodifiableListView([
16389
// ### HSL
16490

16591
new BuiltInCallable.overloaded("hsl", {
166-
r"$hue, $saturation, $lightness": (arguments) {
167-
if (arguments[0].isSpecialNumber ||
168-
arguments[1].isSpecialNumber ||
169-
arguments[2].isSpecialNumber) {
170-
return _functionString("hsl", arguments);
171-
}
172-
173-
var hue = arguments[0].assertNumber("hue");
174-
var saturation = arguments[1].assertNumber("saturation");
175-
var lightness = arguments[2].assertNumber("lightness");
176-
177-
return new SassColor.hsl(hue.value, saturation.value.clamp(0, 100),
178-
lightness.value.clamp(0, 100));
179-
},
92+
r"$hue, $saturation, $lightness, $alpha": (arguments) =>
93+
_hsl("hsl", arguments),
94+
r"$hue, $saturation, $lightness": (arguments) => _hsl("hsl", arguments),
18095
r"$hue, $saturation": (arguments) {
18196
// hsl(123, var(--foo)) is valid CSS because --foo might be `10%, 20%` and
18297
// functions are parsed after variable substitution.
@@ -186,57 +101,30 @@ final List<BuiltInCallable> coreFunctions = new UnmodifiableListView([
186101
throw new SassScriptException(r"Missing argument $lightness.");
187102
}
188103
},
189-
r"$hue": (arguments) {
190-
if (arguments.first.isVar) {
191-
return _functionString('hsl', arguments);
192-
} else {
193-
throw new SassScriptException(r"Missing argument $saturation.");
194-
}
104+
r"$channels": (arguments) {
105+
var parsed = _parseChannels(
106+
"hsl", [r"$hue", r"$saturation", r"$lightness"], arguments.first);
107+
return parsed is SassString ? parsed : _hsl("hsl", parsed as List<Value>);
195108
}
196109
}),
197110

198111
new BuiltInCallable.overloaded("hsla", {
199-
r"$hue, $saturation, $lightness, $alpha": (arguments) {
200-
if (arguments[0].isSpecialNumber ||
201-
arguments[1].isSpecialNumber ||
202-
arguments[2].isSpecialNumber ||
203-
arguments[3].isSpecialNumber) {
204-
return _functionString("hsla", arguments);
205-
}
206-
207-
var hue = arguments[0].assertNumber("hue");
208-
var saturation = arguments[1].assertNumber("saturation");
209-
var lightness = arguments[2].assertNumber("lightness");
210-
var alpha = arguments[3].assertNumber("alpha");
211-
212-
return new SassColor.hsl(
213-
hue.value,
214-
saturation.value.clamp(0, 100),
215-
lightness.value.clamp(0, 100),
216-
_percentageOrUnitless(alpha, 1, "alpha"));
217-
},
218-
r"$hue, $saturation, $lightness": (arguments) {
219-
// hsla(123, var(--foo)) is valid CSS because --foo might be `10%, 20%,
220-
// 0.5` and functions are parsed after variable substitution.
221-
if (arguments[0].isVar || arguments[1].isVar || arguments[2].isVar) {
222-
return _functionString('hsla', arguments);
223-
} else {
224-
throw new SassScriptException(r"Missing argument $alpha.");
225-
}
226-
},
112+
r"$hue, $saturation, $lightness, $alpha": (arguments) =>
113+
_hsl("hsla", arguments),
114+
r"$hue, $saturation, $lightness": (arguments) => _hsl("hsla", arguments),
227115
r"$hue, $saturation": (arguments) {
228116
if (arguments[0].isVar || arguments[1].isVar) {
229117
return _functionString('hsla', arguments);
230118
} else {
231119
throw new SassScriptException(r"Missing argument $lightness.");
232120
}
233121
},
234-
r"$hue": (arguments) {
235-
if (arguments.first.isVar) {
236-
return _functionString('hsla', arguments);
237-
} else {
238-
throw new SassScriptException(r"Missing argument $saturation.");
239-
}
122+
r"$channels": (arguments) {
123+
var parsed = _parseChannels(
124+
"hsla", [r"$hue", r"$saturation", r"$lightness"], arguments.first);
125+
return parsed is SassString
126+
? parsed
127+
: _hsl("hsla", parsed as List<Value>);
240128
}
241129
}),
242130

@@ -1009,6 +897,135 @@ SassString _functionString(String name, Iterable<Value> arguments) =>
1009897
")",
1010898
quotes: false);
1011899

900+
Value _rgb(String name, List<Value> arguments) {
901+
var alpha = arguments.length > 3 ? arguments[3] : null;
902+
if (arguments[0].isSpecialNumber ||
903+
arguments[1].isSpecialNumber ||
904+
arguments[2].isSpecialNumber ||
905+
(alpha?.isSpecialNumber ?? false)) {
906+
return _functionString(name, arguments);
907+
}
908+
909+
var red = arguments[0].assertNumber("red");
910+
var green = arguments[1].assertNumber("green");
911+
var blue = arguments[2].assertNumber("blue");
912+
913+
return new SassColor.rgb(
914+
fuzzyRound(_percentageOrUnitless(red, 255, "red")),
915+
fuzzyRound(_percentageOrUnitless(green, 255, "green")),
916+
fuzzyRound(_percentageOrUnitless(blue, 255, "blue")),
917+
alpha == null
918+
? null
919+
: _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"));
920+
}
921+
922+
Value _rgbTwoArg(String name, List<Value> arguments) {
923+
// rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456, 789`
924+
// and functions are parsed after variable substitution.
925+
if (arguments[0].isVar) {
926+
return _functionString(name, arguments);
927+
} else if (arguments[1].isVar) {
928+
var first = arguments[0];
929+
if (first is SassColor) {
930+
return new SassString(
931+
"$name(${first.red}, ${first.green}, ${first.blue}, "
932+
"${arguments[1].toCssString()})",
933+
quotes: false);
934+
} else {
935+
return _functionString(name, arguments);
936+
}
937+
} else if (arguments[1].isSpecialNumber) {
938+
var color = arguments[0].assertColor("color");
939+
return new SassString(
940+
"$name(${color.red}, ${color.green}, ${color.blue}, "
941+
"${arguments[1].toCssString()})",
942+
quotes: false);
943+
}
944+
945+
var color = arguments[0].assertColor("color");
946+
var alpha = arguments[1].assertNumber("alpha");
947+
return color.changeAlpha(_percentageOrUnitless(alpha, 1, "alpha"));
948+
}
949+
950+
Value _hsl(String name, List<Value> arguments) {
951+
var alpha = arguments.length > 3 ? arguments[3] : null;
952+
if (arguments[0].isSpecialNumber ||
953+
arguments[1].isSpecialNumber ||
954+
arguments[2].isSpecialNumber ||
955+
(alpha?.isSpecialNumber ?? false)) {
956+
return _functionString(name, arguments);
957+
}
958+
959+
var hue = arguments[0].assertNumber("hue");
960+
var saturation = arguments[1].assertNumber("saturation");
961+
var lightness = arguments[2].assertNumber("lightness");
962+
963+
return new SassColor.hsl(
964+
hue.value,
965+
saturation.value.clamp(0, 100),
966+
lightness.value.clamp(0, 100),
967+
alpha == null
968+
? null
969+
: _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"));
970+
}
971+
972+
/* SassString | List<Value> */ _parseChannels(
973+
String name, List<String> argumentNames, Value channels) {
974+
if (channels.isVar) return _functionString(name, [channels]);
975+
976+
var isCommaSeparated = channels.separator == ListSeparator.comma;
977+
var isBracketed = channels.hasBrackets;
978+
if (isCommaSeparated || isBracketed) {
979+
var buffer = new StringBuffer(r"$channels must be");
980+
if (isBracketed) buffer.write(" an unbracketed");
981+
if (isCommaSeparated) {
982+
buffer.write(isBracketed ? "," : " a");
983+
buffer.write(" space-separated");
984+
}
985+
buffer.write(" list.");
986+
throw new SassScriptException(buffer.toString());
987+
}
988+
989+
var list = channels.asList;
990+
if (list.length > 3) {
991+
throw new SassScriptException(
992+
"Only 3 elements allowed, but ${list.length} were passed.");
993+
} else if (list.length < 3) {
994+
if (list.any((value) => value.isVar) ||
995+
(list.isNotEmpty && _isVarSlash(list.last))) {
996+
return _functionString(name, [channels]);
997+
} else {
998+
var argument = argumentNames[list.length];
999+
throw new SassScriptException("Missing element $argument.");
1000+
}
1001+
}
1002+
1003+
var maybeSlashSeparated = list[2];
1004+
if (maybeSlashSeparated is SassNumber &&
1005+
maybeSlashSeparated.asSlash != null) {
1006+
return [
1007+
list[0],
1008+
list[1],
1009+
maybeSlashSeparated.asSlash.item1,
1010+
maybeSlashSeparated.asSlash.item2
1011+
];
1012+
} else if (maybeSlashSeparated is SassString &&
1013+
!maybeSlashSeparated.hasQuotes &&
1014+
maybeSlashSeparated.text.contains("/")) {
1015+
return _functionString(name, [channels]);
1016+
} else {
1017+
return list;
1018+
}
1019+
}
1020+
1021+
/// Returns whether [value] is an unquoted string that start with `var(` and
1022+
/// contains `/`.
1023+
bool _isVarSlash(Value value) =>
1024+
value is SassString &&
1025+
value.hasQuotes &&
1026+
startsWithIgnoreCase(value.text, "var(") &&
1027+
value.text.contains("/");
1028+
10121029
/// Asserts that [number] is a percentage or has no units, and normalizes the
10131030
/// value.
10141031
///

lib/src/utils.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,18 @@ bool equalsIgnoreCase(String string1, String string2) {
236236
return string1.toUpperCase() == string2.toUpperCase();
237237
}
238238

239+
/// Returns whether [string] starts with [prefix], ignoring ASCII case.
240+
bool startsWithIgnoreCase(String string, String prefix) {
241+
if (string.length < prefix.length) return false;
242+
for (var i = 0; i < prefix.length; i++) {
243+
if (!characterEqualsIgnoreCase(
244+
string.codeUnitAt(i), prefix.codeUnitAt(i))) {
245+
return false;
246+
}
247+
}
248+
return true;
249+
}
250+
239251
/// Returns an empty map that uses [equalsIgnoreSeparator] for key equality.
240252
///
241253
/// If [source] is passed, copies it into the map.

0 commit comments

Comments
 (0)