Skip to content

Commit

Permalink
Merge pull request #525 from sass/color-4-rgb-hsl
Browse files Browse the repository at this point in the history
Add support for CSS Color Level 4 rgb() and hsl() syntax
  • Loading branch information
nex3 authored Nov 14, 2018
2 parents 13006e9 + 50efdab commit 0aca829
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 158 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
* Add support for passing arguments to `@content` blocks. See [the
proposal][content-args] for details.

* Add support for the new `rgb()` and `hsl()` syntax introduced in CSS Colors
Level 4, such as `rgb(0% 100% 0% / 0.5)`. See [the proposal][color-4-rgb-hsl]
for more details.

* Add support for interpolation in at-rule names. See [the
proposal][at-rule-interpolation] for details.

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

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

### JavaScript API
Expand Down
305 changes: 161 additions & 144 deletions lib/src/functions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,100 +44,26 @@ final List<BuiltInCallable> coreFunctions = new UnmodifiableListView([
// ### RGB

new BuiltInCallable.overloaded("rgb", {
r"$red, $green, $blue": (arguments) {
if (arguments[0].isSpecialNumber ||
arguments[1].isSpecialNumber ||
arguments[2].isSpecialNumber) {
return _functionString('rgb', arguments);
}

var red = arguments[0].assertNumber("red");
var green = arguments[1].assertNumber("green");
var blue = arguments[2].assertNumber("blue");

return new SassColor.rgb(
fuzzyRound(_percentageOrUnitless(red, 255, "red")),
fuzzyRound(_percentageOrUnitless(green, 255, "green")),
fuzzyRound(_percentageOrUnitless(blue, 255, "blue")));
},
r"$red, $green": (arguments) {
// rgb(123, var(--foo)) is valid CSS because --foo might be `456, 789` and
// functions are parsed after variable substitution.
if (arguments[0].isVar || arguments[1].isVar) {
return _functionString('rgb', arguments);
} else {
throw new SassScriptException(r"Missing argument $blue.");
}
},
r"$red": (arguments) {
if (arguments.first.isVar) {
return _functionString('rgb', arguments);
} else {
throw new SassScriptException(r"Missing argument $green.");
}
r"$red, $green, $blue, $alpha": (arguments) => _rgb("rgb", arguments),
r"$red, $green, $blue": (arguments) => _rgb("rgb", arguments),
r"$color, $alpha": (arguments) => _rgbTwoArg("rgb", arguments),
r"$channels": (arguments) {
var parsed = _parseChannels(
"rgb", [r"$red", r"$green", r"$blue"], arguments.first);
return parsed is SassString ? parsed : _rgb("rgb", parsed as List<Value>);
}
}),

new BuiltInCallable.overloaded("rgba", {
r"$red, $green, $blue, $alpha": (arguments) {
if (arguments[0].isSpecialNumber ||
arguments[1].isSpecialNumber ||
arguments[2].isSpecialNumber ||
arguments[3].isSpecialNumber) {
return _functionString('rgba', arguments);
}

var red = arguments[0].assertNumber("red");
var green = arguments[1].assertNumber("green");
var blue = arguments[2].assertNumber("blue");
var alpha = arguments[3].assertNumber("alpha");

return new SassColor.rgb(
fuzzyRound(_percentageOrUnitless(red, 255, "red")),
fuzzyRound(_percentageOrUnitless(green, 255, "green")),
fuzzyRound(_percentageOrUnitless(blue, 255, "blue")),
_percentageOrUnitless(alpha, 1, "alpha"));
},
r"$color, $alpha": (arguments) {
// rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456,
// 789` and functions are parsed after variable substitution.
if (arguments[0].isVar) {
return _functionString('rgba', arguments);
} else if (arguments[1].isVar) {
var first = arguments[0];
if (first is SassColor) {
return new SassString(
"rgba(${first.red}, ${first.green}, ${first.blue}, "
"${arguments[1].toCssString()})",
quotes: false);
} else {
return _functionString('rgba', arguments);
}
} else if (arguments[1].isSpecialNumber) {
var color = arguments[0].assertColor("color");
return new SassString(
"rgba(${color.red}, ${color.green}, ${color.blue}, "
"${arguments[1].toCssString()})",
quotes: false);
}

var color = arguments[0].assertColor("color");
var alpha = arguments[1].assertNumber("alpha");
return color.changeAlpha(_percentageOrUnitless(alpha, 1, "alpha"));
},
r"$red, $green, $blue": (arguments) {
if (arguments[0].isVar || arguments[1].isVar || arguments[2].isVar) {
return _functionString('rgba', arguments);
} else {
throw new SassScriptException(r"Missing argument $alpha.");
}
},
r"$red": (arguments) {
if (arguments.first.isVar) {
return _functionString('rgba', arguments);
} else {
throw new SassScriptException(r"Missing argument $green.");
}
r"$red, $green, $blue, $alpha": (arguments) => _rgb("rgba", arguments),
r"$red, $green, $blue": (arguments) => _rgb("rgba", arguments),
r"$color, $alpha": (arguments) => _rgbTwoArg("rgba", arguments),
r"$channels": (arguments) {
var parsed = _parseChannels(
"rgba", [r"$red", r"$green", r"$blue"], arguments.first);
return parsed is SassString
? parsed
: _rgb("rgba", parsed as List<Value>);
}
}),

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

new BuiltInCallable.overloaded("hsl", {
r"$hue, $saturation, $lightness": (arguments) {
if (arguments[0].isSpecialNumber ||
arguments[1].isSpecialNumber ||
arguments[2].isSpecialNumber) {
return _functionString("hsl", arguments);
}

var hue = arguments[0].assertNumber("hue");
var saturation = arguments[1].assertNumber("saturation");
var lightness = arguments[2].assertNumber("lightness");

return new SassColor.hsl(hue.value, saturation.value.clamp(0, 100),
lightness.value.clamp(0, 100));
},
r"$hue, $saturation, $lightness, $alpha": (arguments) =>
_hsl("hsl", arguments),
r"$hue, $saturation, $lightness": (arguments) => _hsl("hsl", arguments),
r"$hue, $saturation": (arguments) {
// hsl(123, var(--foo)) is valid CSS because --foo might be `10%, 20%` and
// functions are parsed after variable substitution.
Expand All @@ -186,57 +101,30 @@ final List<BuiltInCallable> coreFunctions = new UnmodifiableListView([
throw new SassScriptException(r"Missing argument $lightness.");
}
},
r"$hue": (arguments) {
if (arguments.first.isVar) {
return _functionString('hsl', arguments);
} else {
throw new SassScriptException(r"Missing argument $saturation.");
}
r"$channels": (arguments) {
var parsed = _parseChannels(
"hsl", [r"$hue", r"$saturation", r"$lightness"], arguments.first);
return parsed is SassString ? parsed : _hsl("hsl", parsed as List<Value>);
}
}),

new BuiltInCallable.overloaded("hsla", {
r"$hue, $saturation, $lightness, $alpha": (arguments) {
if (arguments[0].isSpecialNumber ||
arguments[1].isSpecialNumber ||
arguments[2].isSpecialNumber ||
arguments[3].isSpecialNumber) {
return _functionString("hsla", arguments);
}

var hue = arguments[0].assertNumber("hue");
var saturation = arguments[1].assertNumber("saturation");
var lightness = arguments[2].assertNumber("lightness");
var alpha = arguments[3].assertNumber("alpha");

return new SassColor.hsl(
hue.value,
saturation.value.clamp(0, 100),
lightness.value.clamp(0, 100),
_percentageOrUnitless(alpha, 1, "alpha"));
},
r"$hue, $saturation, $lightness": (arguments) {
// hsla(123, var(--foo)) is valid CSS because --foo might be `10%, 20%,
// 0.5` and functions are parsed after variable substitution.
if (arguments[0].isVar || arguments[1].isVar || arguments[2].isVar) {
return _functionString('hsla', arguments);
} else {
throw new SassScriptException(r"Missing argument $alpha.");
}
},
r"$hue, $saturation, $lightness, $alpha": (arguments) =>
_hsl("hsla", arguments),
r"$hue, $saturation, $lightness": (arguments) => _hsl("hsla", arguments),
r"$hue, $saturation": (arguments) {
if (arguments[0].isVar || arguments[1].isVar) {
return _functionString('hsla', arguments);
} else {
throw new SassScriptException(r"Missing argument $lightness.");
}
},
r"$hue": (arguments) {
if (arguments.first.isVar) {
return _functionString('hsla', arguments);
} else {
throw new SassScriptException(r"Missing argument $saturation.");
}
r"$channels": (arguments) {
var parsed = _parseChannels(
"hsla", [r"$hue", r"$saturation", r"$lightness"], arguments.first);
return parsed is SassString
? parsed
: _hsl("hsla", parsed as List<Value>);
}
}),

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

Value _rgb(String name, List<Value> arguments) {
var alpha = arguments.length > 3 ? arguments[3] : null;
if (arguments[0].isSpecialNumber ||
arguments[1].isSpecialNumber ||
arguments[2].isSpecialNumber ||
(alpha?.isSpecialNumber ?? false)) {
return _functionString(name, arguments);
}

var red = arguments[0].assertNumber("red");
var green = arguments[1].assertNumber("green");
var blue = arguments[2].assertNumber("blue");

return new SassColor.rgb(
fuzzyRound(_percentageOrUnitless(red, 255, "red")),
fuzzyRound(_percentageOrUnitless(green, 255, "green")),
fuzzyRound(_percentageOrUnitless(blue, 255, "blue")),
alpha == null
? null
: _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"));
}

Value _rgbTwoArg(String name, List<Value> arguments) {
// rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456, 789`
// and functions are parsed after variable substitution.
if (arguments[0].isVar) {
return _functionString(name, arguments);
} else if (arguments[1].isVar) {
var first = arguments[0];
if (first is SassColor) {
return new SassString(
"$name(${first.red}, ${first.green}, ${first.blue}, "
"${arguments[1].toCssString()})",
quotes: false);
} else {
return _functionString(name, arguments);
}
} else if (arguments[1].isSpecialNumber) {
var color = arguments[0].assertColor("color");
return new SassString(
"$name(${color.red}, ${color.green}, ${color.blue}, "
"${arguments[1].toCssString()})",
quotes: false);
}

var color = arguments[0].assertColor("color");
var alpha = arguments[1].assertNumber("alpha");
return color.changeAlpha(_percentageOrUnitless(alpha, 1, "alpha"));
}

Value _hsl(String name, List<Value> arguments) {
var alpha = arguments.length > 3 ? arguments[3] : null;
if (arguments[0].isSpecialNumber ||
arguments[1].isSpecialNumber ||
arguments[2].isSpecialNumber ||
(alpha?.isSpecialNumber ?? false)) {
return _functionString(name, arguments);
}

var hue = arguments[0].assertNumber("hue");
var saturation = arguments[1].assertNumber("saturation");
var lightness = arguments[2].assertNumber("lightness");

return new SassColor.hsl(
hue.value,
saturation.value.clamp(0, 100),
lightness.value.clamp(0, 100),
alpha == null
? null
: _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"));
}

/* SassString | List<Value> */ _parseChannels(
String name, List<String> argumentNames, Value channels) {
if (channels.isVar) return _functionString(name, [channels]);

var isCommaSeparated = channels.separator == ListSeparator.comma;
var isBracketed = channels.hasBrackets;
if (isCommaSeparated || isBracketed) {
var buffer = new StringBuffer(r"$channels must be");
if (isBracketed) buffer.write(" an unbracketed");
if (isCommaSeparated) {
buffer.write(isBracketed ? "," : " a");
buffer.write(" space-separated");
}
buffer.write(" list.");
throw new SassScriptException(buffer.toString());
}

var list = channels.asList;
if (list.length > 3) {
throw new SassScriptException(
"Only 3 elements allowed, but ${list.length} were passed.");
} else if (list.length < 3) {
if (list.any((value) => value.isVar) ||
(list.isNotEmpty && _isVarSlash(list.last))) {
return _functionString(name, [channels]);
} else {
var argument = argumentNames[list.length];
throw new SassScriptException("Missing element $argument.");
}
}

var maybeSlashSeparated = list[2];
if (maybeSlashSeparated is SassNumber &&
maybeSlashSeparated.asSlash != null) {
return [
list[0],
list[1],
maybeSlashSeparated.asSlash.item1,
maybeSlashSeparated.asSlash.item2
];
} else if (maybeSlashSeparated is SassString &&
!maybeSlashSeparated.hasQuotes &&
maybeSlashSeparated.text.contains("/")) {
return _functionString(name, [channels]);
} else {
return list;
}
}

/// Returns whether [value] is an unquoted string that start with `var(` and
/// contains `/`.
bool _isVarSlash(Value value) =>
value is SassString &&
value.hasQuotes &&
startsWithIgnoreCase(value.text, "var(") &&
value.text.contains("/");

/// Asserts that [number] is a percentage or has no units, and normalizes the
/// value.
///
Expand Down
12 changes: 12 additions & 0 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,18 @@ bool equalsIgnoreCase(String string1, String string2) {
return string1.toUpperCase() == string2.toUpperCase();
}

/// Returns whether [string] starts with [prefix], ignoring ASCII case.
bool startsWithIgnoreCase(String string, String prefix) {
if (string.length < prefix.length) return false;
for (var i = 0; i < prefix.length; i++) {
if (!characterEqualsIgnoreCase(
string.codeUnitAt(i), prefix.codeUnitAt(i))) {
return false;
}
}
return true;
}

/// Returns an empty map that uses [equalsIgnoreSeparator] for key equality.
///
/// If [source] is passed, copies it into the map.
Expand Down
Loading

0 comments on commit 0aca829

Please sign in to comment.