Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds built-in clamp() and hypot() #906

Merged
merged 4 commits into from Dec 26, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,11 @@
## 1.25.0

* Add functions to the built-in "sass:math" module.
Awjin marked this conversation as resolved.
Show resolved Hide resolved
* `clamp()`: given a `$min`, $number`, and `$max` values, clamps the `$number`
in between `$min` and `$max`.
* `hypot()`: given *n* numbers, outputs the length of the *n*-dimensional
vector that has components equal to each of the inputs.

## 1.24.0

* Add an optional `with` clause to the `@forward` rule. This works like the
Expand Down
145 changes: 105 additions & 40 deletions lib/src/functions/math.dart
Expand Up @@ -13,34 +13,58 @@ import '../module/built_in.dart';
import '../util/number.dart';
import '../value.dart';

/// A random number generator.
final _random = math.Random();

/// The global definitions of Sass math functions.
final global = UnmodifiableListView([
_round, _ceil, _floor, _abs, _max, _min, _randomFunction, _unit, //
_percentage,
_abs, _ceil, _floor, _max, _min, _percentage, _randomFunction, _round,
_unit, //
_compatible.withName("comparable"),
_isUnitless.withName("unitless"),
_compatible.withName("comparable")
]);

/// The Sass math module.
final module = BuiltInModule("math", functions: [
_round, _ceil, _floor, _abs, _max, _min, _randomFunction, _unit,
_isUnitless, //
_percentage, _compatible
_abs, _ceil, _clamp, _compatible, _floor, _hypot, _isUnitless, _max, _min, //
_percentage, _randomFunction, _round, _unit,
]);

final _percentage = BuiltInCallable("percentage", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
number.assertNoUnits("number");
return SassNumber(number.value * 100, '%');
});
/// Returns a [Callable] named [name] that transforms a number's value
/// using [transform] and preserves its units.
BuiltInCallable _numberFunction(String name, num transform(num value)) {
return BuiltInCallable(name, r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassNumber.withUnits(transform(number.value),
numeratorUnits: number.numeratorUnits,
denominatorUnits: number.denominatorUnits);
});
}

///
/// Bounding functions
///

final _round = _numberFunction("round", fuzzyRound);
final _ceil = _numberFunction("ceil", (value) => value.ceil());

final _clamp = BuiltInCallable("clamp", r"$min, $number, $max", (arguments) {
var min = arguments[0].assertNumber("min");
var number = arguments[1].assertNumber("number");
var max = arguments[2].assertNumber("max");

if (min.hasUnits == number.hasUnits && number.hasUnits == max.hasUnits) {
if (min.greaterThanOrEquals(max).isTruthy) return min;
if (min.greaterThanOrEquals(number).isTruthy) return min;
if (number.greaterThanOrEquals(max).isTruthy) return max;
return number;
}

var arg1 = "$min";
var arg2 = "${min.hasUnits != number.hasUnits ? number : max}";
var unit1 = "${min.hasUnits ? "has units" : "is unitless"}";
var unit2 = "${min.hasUnits ? "is unitless" : "has units"}";
throw SassScriptException(
"$arg1 $unit1 but $arg2 $unit2. Arguments must all have units or all be unitless.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also include the names of the arguments in question here—it may not be obvious from context.

Also, this is a long line (the formatter won't break strings for you).

Same below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

});

final _floor = _numberFunction("floor", (value) => value.floor());
final _abs = _numberFunction("abs", (value) => value.abs());

final _max = BuiltInCallable("max", r"$numbers...", (arguments) {
SassNumber max;
Expand All @@ -62,39 +86,80 @@ final _min = BuiltInCallable("min", r"$numbers...", (arguments) {
throw SassScriptException("At least one argument must be passed.");
});

final _randomFunction = BuiltInCallable("random", r"$limit: null", (arguments) {
if (arguments[0] == sassNull) return SassNumber(_random.nextDouble());
var limit = arguments[0].assertNumber("limit").assertInt("limit");
if (limit < 1) {
throw SassScriptException("\$limit: Must be greater than 0, was $limit.");
final _round = _numberFunction("round", fuzzyRound);

///
/// Distance functions
///

final _abs = _numberFunction("abs", (value) => value.abs());

final _hypot = BuiltInCallable("hypot", r"$numbers...", (arguments) {
var numbers =
arguments[0].asList.map((argument) => argument.assertNumber()).toList();

if (numbers.isEmpty) {
throw SassScriptException("At least one argument must be passed.");
}
return SassNumber(_random.nextInt(limit) + 1);
});

final _unit = BuiltInCallable("unit", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassString(number.unitString, quotes: true);
});
var unitRequirement = numbers[0].hasUnits;
var numeratorUnits = numbers[0].numeratorUnits;
var denominatorUnits = numbers[0].denominatorUnits;
var subtotal = 0.0;

final _isUnitless = BuiltInCallable("is-unitless", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassBoolean(!number.hasUnits);
for (var number in numbers) {
if (number.hasUnits != unitRequirement) {
var unit1 = "${unitRequirement ? "has units" : "is unitless"}";
var unit2 = "${unitRequirement ? "is unitless" : "has units"}";
throw SassScriptException(
"${numbers[0]} $unit1 but $number $unit2. Arguments must all have units or all be unitless.");
}
number = number.coerce(numeratorUnits, denominatorUnits);
subtotal += math.pow(number.value, 2);
}

return SassNumber.withUnits(math.sqrt(subtotal),
numeratorUnits: numeratorUnits, denominatorUnits: denominatorUnits);
});

///
/// Unit functions
///

final _compatible =
BuiltInCallable("compatible", r"$number1, $number2", (arguments) {
var number1 = arguments[0].assertNumber("number1");
var number2 = arguments[1].assertNumber("number2");
return SassBoolean(number1.isComparableTo(number2));
});

/// Returns a [Callable] named [name] that transforms a number's value
/// using [transform] and preserves its units.
BuiltInCallable _numberFunction(String name, num transform(num value)) {
return BuiltInCallable(name, r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassNumber.withUnits(transform(number.value),
numeratorUnits: number.numeratorUnits,
denominatorUnits: number.denominatorUnits);
});
}
final _isUnitless = BuiltInCallable("is-unitless", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassBoolean(!number.hasUnits);
});

final _unit = BuiltInCallable("unit", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassString(number.unitString, quotes: true);
});

///
/// Other functions
///

final _percentage = BuiltInCallable("percentage", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
number.assertNoUnits("number");
return SassNumber(number.value * 100, '%');
});

final _random = math.Random();

final _randomFunction = BuiltInCallable("random", r"$limit: null", (arguments) {
if (arguments[0] == sassNull) return SassNumber(_random.nextDouble());
var limit = arguments[0].assertNumber("limit").assertInt("limit");
if (limit < 1) {
throw SassScriptException("\$limit: Must be greater than 0, was $limit.");
}
return SassNumber(_random.nextInt(limit) + 1);
});
5 changes: 4 additions & 1 deletion lib/src/parse/stylesheet.dart
Expand Up @@ -2367,6 +2367,9 @@ relase. For details, see http://bit.ly/moz-document.
number += _tryDecimal(allowTrailingDot: scanner.position != start.position);
number *= _tryExponent();

// Preserve the sign of -0 by representing it as a double.
number = (sign.isNegative && number == 0) ? -0.0 : sign * number;
Awjin marked this conversation as resolved.
Show resolved Hide resolved

String unit;
if (scanner.scanChar($percent)) {
unit = "%";
Expand All @@ -2376,7 +2379,7 @@ relase. For details, see http://bit.ly/moz-document.
unit = identifier(unit: true);
}

return NumberExpression(sign * number, scanner.spanFrom(start), unit: unit);
return NumberExpression(number, scanner.spanFrom(start), unit: unit);
}

/// Consumes the decimal component of a number and returns its value, or 0 if
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
@@ -1,5 +1,5 @@
name: sass
version: 1.24.0
version: 1.25.0-dev
description: A Sass implementation in Dart.
author: Sass Team
homepage: https://github.com/sass/dart-sass
Expand Down