Merge pull request #525 from sass/color-4-rgb-hsl

Add support for CSS Color Level 4 rgb() and hsl() syntax
This commit is contained in:
Natalie Weizenbaum 2018-11-14 13:37:13 -08:00 committed by GitHub
commit 0aca829515
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 195 additions and 158 deletions

View File

@ -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.
@ -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

View File

@ -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>);
}
}),
@ -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.
@ -186,44 +101,17 @@ 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);
@ -231,12 +119,12 @@ final List<BuiltInCallable> coreFunctions = new UnmodifiableListView([
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>);
}
}),
@ -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.
///

View File

@ -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.

View File

@ -4,6 +4,8 @@
import 'dart:math';
import 'package:tuple/tuple.dart';
import '../exception.dart';
import '../util/number.dart';
import '../utils.dart';
@ -151,8 +153,9 @@ class SassNumber extends Value implements ext.SassNumber {
final List<String> denominatorUnits;
/// The slash-separated representation of this number, if it has one.
final String asSlash;
/// The representation of this number as two slash-separated numbers, if it
/// has one.
final Tuple2<SassNumber, SassNumber> asSlash;
bool get hasUnits => numeratorUnits.isNotEmpty || denominatorUnits.isNotEmpty;
@ -188,9 +191,11 @@ class SassNumber extends Value implements ext.SassNumber {
return new SassNumber._(value, numeratorUnits, denominatorUnits);
}
/// Returns a copy of [this] with [this.asSlash] set to [asSlash].
SassNumber withSlash(String asSlash) =>
new SassNumber._(value, numeratorUnits, denominatorUnits, asSlash);
/// Returns a copy of [this] with [this.asSlash] set to a tuple containing
/// [numerator] and [denominator].
SassNumber withSlash(SassNumber numerator, SassNumber denominator) =>
new SassNumber._(value, numeratorUnits, denominatorUnits,
new Tuple2(numerator, denominator));
SassNumber assertNumber([String name]) => this;

View File

@ -1190,9 +1190,7 @@ class _EvaluateVisitor
var right = await node.right.accept(this);
var result = left.dividedBy(right);
if (node.allowsSlash && left is SassNumber && right is SassNumber) {
var leftSlash = left.asSlash ?? _serialize(left, node.left.span);
var rightSlash = right.asSlash ?? _serialize(right, node.left.span);
return (result as SassNumber).withSlash("$leftSlash/$rightSlash");
return (result as SassNumber).withSlash(left, right);
} else {
return result;
}

View File

@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/synchronize.dart for details.
//
// Checksum: abeba8d186777d0dc1eedf9f9d29e4514bcd7619
// Checksum: 6da6661213e8c929ae91a0a993a6cf2827f033f5
import 'async_evaluate.dart' show EvaluateResult;
export 'async_evaluate.dart' show EvaluateResult;
@ -1183,9 +1183,7 @@ class _EvaluateVisitor
var right = node.right.accept(this);
var result = left.dividedBy(right);
if (node.allowsSlash && left is SassNumber && right is SassNumber) {
var leftSlash = left.asSlash ?? _serialize(left, node.left.span);
var rightSlash = right.asSlash ?? _serialize(right, node.left.span);
return (result as SassNumber).withSlash("$leftSlash/$rightSlash");
return (result as SassNumber).withSlash(left, right);
} else {
return result;
}

View File

@ -644,7 +644,9 @@ class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor {
void visitNumber(SassNumber value) {
if (value.asSlash != null) {
_buffer.write(value.asSlash);
visitNumber(value.asSlash.item1);
_buffer.writeCharCode($slash);
visitNumber(value.asSlash.item2);
return;
}

View File

@ -1,5 +1,5 @@
name: sass
version: 1.15.0-dev
version: 1.15.0
description: A Sass implementation in Dart.
author: Dart Team <misc@dartlang.org>
homepage: https://github.com/sass/dart-sass