diff --git a/CHANGELOG.md b/CHANGELOG.md index 59365084..000ca8b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/src/functions.dart b/lib/src/functions.dart index 0fc1e547..924ae9ba 100644 --- a/lib/src/functions.dart +++ b/lib/src/functions.dart @@ -44,100 +44,26 @@ final List 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); } }), 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); } }), @@ -163,20 +89,9 @@ final List 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 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); } }), 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 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); } }), @@ -1009,6 +897,135 @@ SassString _functionString(String name, Iterable arguments) => ")", quotes: false); +Value _rgb(String name, List 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 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 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 */ _parseChannels( + String name, List 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. /// diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 99ef99c3..829a5a98 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -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. diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index c314a231..b9ea1c8a 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -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 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 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; diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index cfb559ae..f696adc8 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -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; } diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 8ce9559e..77aeb3ff 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -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; } diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 5731ad72..cb4e42b1 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -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; } diff --git a/pubspec.yaml b/pubspec.yaml index 9fdb64ad..0e011578 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.15.0-dev +version: 1.15.0 description: A Sass implementation in Dart. author: Dart Team homepage: https://github.com/sass/dart-sass