module Sass::Script
# Methods in this module are accessible from the SassScript context.
# For example, you can write
#
# $color = hsl(120deg, 100%, 50%)
#
# and it will call {Sass::Script::Functions#hsl}.
#
# The following functions are provided:
#
# ## RGB Functions
#
# \{#rgb}
# : Converts an `rgb(red, green, blue)` triplet into a color.
#
# \{#rgba}
# : Converts an `rgba(red, green, blue, alpha)` quadruplet into a color.
#
# \{#red}
# : Gets the red component of a color.
#
# \{#green}
# : Gets the green component of a color.
#
# \{#blue}
# : Gets the blue component of a color.
#
# \{#mix}
# : Mixes two colors together.
#
# ## HSL Functions
#
# \{#hsl}
# : Converts an `hsl(hue, saturation, lightness)` triplet into a color.
#
# \{#hsla}
# : Converts an `hsla(hue, saturation, lightness, alpha)` quadruplet into a color.
#
# \{#hue}
# : Gets the hue component of a color.
#
# \{#saturation}
# : Gets the saturation component of a color.
#
# \{#lightness}
# : Gets the lightness component of a color.
#
# \{#adjust_hue #adjust-hue}
# : Changes the hue of a color.
#
# \{#lighten}
# : Makes a color lighter.
#
# \{#darken}
# : Makes a color darker.
#
# \{#saturate}
# : Makes a color more saturated.
#
# \{#desaturate}
# : Makes a color less saturated.
#
# \{#grayscale}
# : Converts a color to grayscale.
#
# \{#complement}
# : Returns the complement of a color.
#
# ## Opacity Functions
#
# \{#alpha} / \{#opacity}
# : Gets the alpha component (opacity) of a color.
#
# \{#rgba}
# : Sets the alpha component of a color.
#
# \{#opacify} / \{#fade_in #fade-in}
# : Makes a color more opaque.
#
# \{#transparentize} / \{#fade_out #fade-out}
# : Makes a color more transparent.
#
# ## String Functions
#
# \{#unquote}
# : Removes the quotes from a string.
#
# \{#quote}
# : Adds quotes to a string.
#
# ## Number Functions
#
# \{#percentage}
# : Converts a unitless number to a percentage.
#
# \{#round}
# : Rounds a number to the nearest whole number.
#
# \{#ceil}
# : Rounds a number up to the nearest whole number.
#
# \{#floor}
# : Rounds a number down to the nearest whole number.
#
# \{#abs}
# : Returns the absolute value of a number.
#
# ## Introspection Functions
#
# \{#type_of}
# : Returns the type of a value.
#
# \{#unit}
# : Returns the units associated with a number.
#
# \{#unitless}
# : Returns whether a number has units or not.
#
# \{#comparable}
# : Returns whether two numbers can be added or compared.
#
# These functions are described in more detail below.
#
# ## Adding Custom Functions
#
# New Sass functions can be added by adding Ruby methods to this module.
# For example:
#
# module Sass::Script::Functions
# def reverse(string)
# assert_type string, :String
# Sass::Script::String.new(string.value.reverse)
# end
# end
#
# There are a few things to keep in mind when modifying this module.
# First of all, the arguments passed are {Sass::Script::Literal} objects.
# Literal objects are also expected to be returned.
# This means that Ruby values must be unwrapped and wrapped.
#
# Most Literal objects support the {Sass::Script::Literal#value value} accessor
# for getting their Ruby values.
# Color objects, though, must be accessed using {Sass::Script::Color#rgb rgb},
# {Sass::Script::Color#red red}, {Sass::Script::Color#blue green}, or {Sass::Script::Color#blue blue}.
#
# Second, making Ruby functions accessible from Sass introduces the temptation
# to do things like database access within stylesheets.
# This is generally a bad idea;
# since Sass files are by default only compiled once,
# dynamic code is not a great fit.
#
# If you really, really need to compile Sass on each request,
# first make sure you have adequate caching set up.
# Then you can use {Sass::Engine} to render the code,
# using the {file:SASS_REFERENCE.md#custom-option `options` parameter}
# to pass in data that {EvaluationContext#options can be accessed}
# from your Sass functions.
#
# Within one of the functions in this module,
# methods of {EvaluationContext} can be used.
#
# ### Caveats
#
# When creating new {Literal} objects within functions,
# be aware that it's not safe to call {Literal#to_s #to_s}
# (or other methods that use the string representation)
# on those objects without first setting {Node#options= the #options attribute}.
module Functions
# The context in which methods in {Script::Functions} are evaluated.
# That means that all instance methods of {EvaluationContext}
# are available to use in functions.
class EvaluationContext
# The options hash for the {Sass::Engine} that is processing the function call
#
# @return [{Symbol => Object}]
attr_reader :options
# @param options [{Symbol => Object}] See \{#options}
def initialize(options)
@options = options
# We need to include this individually in each instance
# because of an icky Ruby restriction
class << self; include Sass::Script::Functions; end
end
# Asserts that the type of a given SassScript value
# is the expected type (designated by a symbol).
# For example:
#
# assert_type value, :String
# assert_type value, :Number
#
# Valid types are `:Bool`, `:Color`, `:Number`, and `:String`.
# Note that `:String` will match both double-quoted strings
# and unquoted identifiers.
#
# @param value [Sass::Script::Literal] A SassScript value
# @param type [Symbol] The name of the type the value is expected to be
def assert_type(value, type)
return if value.is_a?(Sass::Script.const_get(type))
raise ArgumentError.new("#{value.inspect} is not a #{type.to_s.downcase}")
end
end
instance_methods.each { |m| undef_method m unless m.to_s =~ /^__/ }
# Creates a {Color} object from red, green, and blue values.
#
# @param red [Number]
# A number between 0 and 255 inclusive,
# or between 0% and 100% inclusive
# @param green [Number]
# A number between 0 and 255 inclusive,
# or between 0% and 100% inclusive
# @param blue [Number]
# A number between 0 and 255 inclusive,
# or between 0% and 100% inclusive
# @see #rgba
# @return [Color]
def rgb(red, green, blue)
assert_type red, :Number
assert_type green, :Number
assert_type blue, :Number
Color.new([red, green, blue].map do |c|
v = c.value
if c.numerator_units == ["%"] && c.denominator_units.empty?
next v * 255 / 100.0 if (0..100).include?(v)
raise ArgumentError.new("Color value #{c} must be between 0% and 100% inclusive")
else
next v if (0..255).include?(v)
raise ArgumentError.new("Color value #{v} must be between 0 and 255 inclusive")
end
end)
end
# @see #rgb
# @overload rgba(red, green, blue, alpha)
# Creates a {Color} object from red, green, and blue values,
# as well as an alpha channel indicating opacity.
#
# @param red [Number]
# A number between 0 and 255 inclusive
# @param green [Number]
# A number between 0 and 255 inclusive
# @param blue [Number]
# A number between 0 and 255 inclusive
# @param alpha [Number]
# A number between 0 and 1
# @return [Color]
#
# @overload rgba(color, alpha)
# Sets the opacity of a color.
#
# @example
# rgba(#102030, 0.5) => rgba(16, 32, 48, 0.5)
# rgba(blue, 0.2) => rgba(0, 0, 255, 0.2)
#
# @param color [Color]
# @param alpha [Number]
# A number between 0 and 1
# @return [Color]
def rgba(*args)
case args.size
when 2
color, alpha = args
assert_type color, :Color
assert_type alpha, :Number
unless (0..1).include?(alpha.value)
raise ArgumentError.new("Alpha channel #{alpha.value} must be between 0 and 1 inclusive")
end
color.with(:alpha => alpha.value)
when 4
red, green, blue, alpha = args
rgba(rgb(red, green, blue), alpha)
else
raise ArgumentError.new("wrong number of arguments (#{args.size} for 4)")
end
end
# Creates a {Color} object from hue, saturation, and lightness.
# Uses the algorithm from the [CSS3 spec](http://www.w3.org/TR/css3-color/#hsl-color).
#
# @param hue [Number] The hue of the color.
# Should be between 0 and 360 degrees, inclusive
# @param saturation [Number] The saturation of the color.
# Must be between `0%` and `100%`, inclusive
# @param lightness [Number] The lightness of the color.
# Must be between `0%` and `100%`, inclusive
# @return [Color] The resulting color
# @see #hsla
# @raise [ArgumentError] if `saturation` or `lightness` are out of bounds
def hsl(hue, saturation, lightness)
hsla(hue, saturation, lightness, Number.new(1))
end
# Creates a {Color} object from hue, saturation, and lightness,
# as well as an alpha channel indicating opacity.
# Uses the algorithm from the [CSS3 spec](http://www.w3.org/TR/css3-color/#hsl-color).
#
# @param hue [Number] The hue of the color.
# Should be between 0 and 360 degrees, inclusive
# @param saturation [Number] The saturation of the color.
# Must be between `0%` and `100%`, inclusive
# @param lightness [Number] The lightness of the color.
# Must be between `0%` and `100%`, inclusive
# @param alpha [Number] The opacity of the color.
# Must be between 0 and 1, inclusive
# @return [Color] The resulting color
# @see #hsl
# @raise [ArgumentError] if `saturation`, `lightness`, or `alpha` are out of bounds
def hsla(hue, saturation, lightness, alpha)
assert_type hue, :Number
assert_type saturation, :Number
assert_type lightness, :Number
assert_type alpha, :Number
unless (0..1).include?(alpha.value)
raise ArgumentError.new("Alpha channel #{alpha.value} must be between 0 and 1")
end
original_s = saturation
original_l = lightness
# This algorithm is from http://www.w3.org/TR/css3-color#hsl-color
h, s, l = [hue, saturation, lightness].map { |a| a.value }
raise ArgumentError.new("Saturation #{s} must be between 0% and 100%") unless (0..100).include?(s)
raise ArgumentError.new("Lightness #{l} must be between 0% and 100%") unless (0..100).include?(l)
Color.new(:hue => h, :saturation => s, :lightness => l, :alpha => alpha.value)
end
# Returns the red component of a color.
#
# @param color [Color]
# @return [Number]
# @raise [ArgumentError] If `color` isn't a color
def red(color)
assert_type color, :Color
Sass::Script::Number.new(color.red)
end
# Returns the green component of a color.
#
# @param color [Color]
# @return [Number]
# @raise [ArgumentError] If `color` isn't a color
def green(color)
assert_type color, :Color
Sass::Script::Number.new(color.green)
end
# Returns the blue component of a color.
#
# @param color [Color]
# @return [Number]
# @raise [ArgumentError] If `color` isn't a color
def blue(color)
assert_type color, :Color
Sass::Script::Number.new(color.blue)
end
# Returns the hue component of a color.
#
# See [the CSS3 HSL specification](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
#
# Calculated from RGB where necessary via [this algorithm](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
#
# @param color [Color]
# @return [Number] between 0deg and 360deg
# @see #adjust_hue
# @raise [ArgumentError] if `color` isn't a color
def hue(color)
assert_type color, :Color
Sass::Script::Number.new(color.hue, ["deg"])
end
# Returns the saturation component of a color.
#
# See [the CSS3 HSL specification](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
#
# Calculated from RGB where necessary via [this algorithm](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
#
# @param color [Color]
# @return [Number] between 0% and 100%
# @see #saturate
# @see #desaturate
# @raise [ArgumentError] if `color` isn't a color
def saturation(color)
assert_type color, :Color
Sass::Script::Number.new(color.saturation, ["%"])
end
# Returns the hue component of a color.
#
# See [the CSS3 HSL specification](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
#
# Calculated from RGB where necessary via [this algorithm](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
#
# @param color [Color]
# @return [Number] between 0% and 100%
# @see #lighten
# @see #darken
# @raise [ArgumentError] if `color` isn't a color
def lightness(color)
assert_type color, :Color
Sass::Script::Number.new(color.lightness, ["%"])
end
# Returns the alpha component (opacity) of a color.
# This is 1 unless otherwise specified.
#
# This function also supports the proprietary Microsoft
# `alpha(opacity=20)` syntax.
#
# @overload def alpha(color)
# @param color [Color]
# @return [Number]
# @see #opacify
# @see #transparentize
# @raise [ArgumentError] If `color` isn't a color
def alpha(*args)
if args.all? do |a|
a.is_a?(Sass::Script::String) && a.type == :identifier &&
a.value =~ /^[a-zA-Z]+\s*=/
end
# Support the proprietary MS alpha() function
return Sass::Script::String.new("alpha(#{args.map {|a| a.to_s}.join(", ")})")
end
opacity(*args)
end
# Returns the alpha component (opacity) of a color.
# This is 1 unless otherwise specified.
#
# @param color [Color]
# @return [Number]
# @see #opacify
# @see #transparentize
# @raise [ArgumentError] If `color` isn't a color
def opacity(color)
assert_type color, :Color
Sass::Script::Number.new(color.alpha)
end
# Makes a color more opaque.
# Takes a color and an amount between 0 and 1,
# and returns a color with the opacity increased by that value.
#
# For example:
#
# opacify(rgba(0, 0, 0, 0.5), 0.1) => rgba(0, 0, 0, 0.6)
# opacify(rgba(0, 0, 17, 0.8), 0.2) => #001
#
# @param color [Color]
# @param amount [Number]
# @return [Color]
# @see #transparentize
# @raise [ArgumentError] If `color` isn't a color,
# or `number` isn't a number between 0 and 1
def opacify(color, amount)
adjust(color, amount, :alpha, 0..1, :+)
end
alias_method :fade_in, :opacify
# Makes a color more transparent.
# Takes a color and an amount between 0 and 1,
# and returns a color with the opacity decreased by that value.
#
# For example:
#
# transparentize(rgba(0, 0, 0, 0.5), 0.1) => rgba(0, 0, 0, 0.4)
# transparentize(rgba(0, 0, 0, 0.8), 0.2) => rgba(0, 0, 0, 0.6)
#
# @param color [Color]
# @param amount [Number]
# @return [Color]
# @see #opacify
# @raise [ArgumentError] If `color` isn't a color,
# or `number` isn't a number between 0 and 1
def transparentize(color, amount)
adjust(color, amount, :alpha, 0..1, :-)
end
alias_method :fade_out, :transparentize
# Makes a color lighter.
# Takes a color and an amount between 0% and 100%,
# and returns a color with the lightness increased by that value.
#
# For example:
#
# lighten(hsl(0, 0%, 0%), 30%) => hsl(0, 0, 30)
# lighten(#800, 20%) => #e00
#
# @param color [Color]
# @param amount [Number]
# @return [Color]
# @see #darken
# @raise [ArgumentError] If `color` isn't a color,
# or `number` isn't a number between 0% and 100%
def lighten(color, amount)
adjust(color, amount, :lightness, 0..100, :+, "%")
end
# Makes a color darker.
# Takes a color and an amount between 0% and 100%,
# and returns a color with the lightness decreased by that value.
#
# For example:
#
# darken(hsl(25, 100%, 80%), 30%) => hsl(25, 100%, 50%)
# darken(#800, 20%) => #200
#
# @param color [Color]
# @param amount [Number]
# @return [Color]
# @see #lighten
# @raise [ArgumentError] If `color` isn't a color,
# or `number` isn't a number between 0% and 100%
def darken(color, amount)
adjust(color, amount, :lightness, 0..100, :-, "%")
end
# Makes a color more saturated.
# Takes a color and an amount between 0% and 100%,
# and returns a color with the saturation increased by that value.
#
# For example:
#
# saturate(hsl(120, 30%, 90%), 20%) => hsl(120, 50%, 90%)
# saturate(#855, 20%) => #9e3f3f
#
# @param color [Color]
# @param amount [Number]
# @return [Color]
# @see #desaturate
# @raise [ArgumentError] If `color` isn't a color,
# or `number` isn't a number between 0% and 100%
def saturate(color, amount)
adjust(color, amount, :saturation, 0..100, :+, "%")
end
# Makes a color less saturated.
# Takes a color and an amount between 0% and 100%,
# and returns a color with the saturation decreased by that value.
#
# For example:
#
# desaturate(hsl(120, 30%, 90%), 20%) => hsl(120, 10%, 90%)
# desaturate(#855, 20%) => #726b6b
#
# @param color [Color]
# @param amount [Number]
# @return [Color]
# @see #saturate
# @raise [ArgumentError] If `color` isn't a color,
# or `number` isn't a number between 0% and 100%
def desaturate(color, amount)
adjust(color, amount, :saturation, 0..100, :-, "%")
end
# Changes the hue of a color while retaining the lightness and saturation.
# Takes a color and a number of degrees (usually between -360deg and 360deg),
# and returns a color with the hue rotated by that value.
#
# For example:
#
# adjust-hue(hsl(120, 30%, 90%), 60deg) => hsl(180, 30%, 90%)
# adjust-hue(hsl(120, 30%, 90%), 060deg) => hsl(60, 30%, 90%)
# adjust-hue(#811, 45deg) => #886a11
#
# @param color [Color]
# @param amount [Number]
# @return [Color]
# @raise [ArgumentError] If `color` isn't a color, or `number` isn't a number
def adjust_hue(color, degrees)
assert_type color, :Color
assert_type degrees, :Number
color.with(:hue => color.hue + degrees.value)
end
# Mixes together two colors.
# Specifically, takes the average of each of the RGB components,
# optionally weighted by the given percentage.
# The opacity of the colors is also considered when weighting the components.
#
# The weight specifies the amount of the first color that should be included
# in the returned color.
# The default, 50%, means that half the first color
# and half the second color should be used.
# 25% means that a quarter of the first color
# and three quarters of the second color should be used.
#
# For example:
#
# mix(#f00, #00f) => #7f007f
# mix(#f00, #00f, 25%) => #3f00bf
# mix(rgba(255, 0, 0, 0.5), #00f) => rgba(63, 0, 191, 0.75)
#
# @overload mix(color1, color2, weight = 50%)
# @param color1 [Color]
# @param color2 [Color]
# @param weight [Number] between 0% and 100%
# @return [Color]
# @raise [ArgumentError] if `color1` or `color2` aren't colors,
# or `weight` isn't a number between 0% and 100%
def mix(color1, color2, weight = Number.new(50))
assert_type color1, :Color
assert_type color2, :Color
assert_type weight, :Number
unless (0..100).include?(weight.value)
raise ArgumentError.new("Weight #{weight} must be between 0% and 100%")
end
# This algorithm factors in both the user-provided weight
# and the difference between the alpha values of the two colors
# to decide how to perform the weighted average of the two RGB values.
#
# It works by first normalizing both parameters to be within [-1, 1],
# where 1 indicates "only use color1", -1 indicates "only use color 0",
# and all values in between indicated a proportionately weighted average.
#
# Once we have the normalized variables w and a,
# we apply the formula (w + a)/(1 + w*a)
# to get the combined weight (in [-1, 1]) of color1.
# This formula has two especially nice properties:
#
# * When either w or a are -1 or 1, the combined weight is also that number
# (cases where w * a == -1 are undefined, and handled as a special case).
#
# * When a is 0, the combined weight is w, and vice versa
#
# Finally, the weight of color1 is renormalized to be within [0, 1]
# and the weight of color2 is given by 1 minus the weight of color1.
p = weight.value/100.0
w = p*2 - 1
a = color1.alpha - color2.alpha
w1 = (((w * a == -1) ? w : (w + a)/(1 + w*a)) + 1)/2.0
w2 = 1 - w1
rgb = color1.rgb.zip(color2.rgb).map {|v1, v2| v1*w1 + v2*w2}
alpha = color1.alpha*p + color2.alpha*(1-p)
Color.new(rgb + [alpha])
end
# Converts a color to grayscale.
# This is identical to `desaturate(color, 100%)`.
#
# @param color [Color]
# @return [Color]
# @raise [ArgumentError] if `color` isn't a color
# @see #desaturate
def grayscale(color)
desaturate color, Number.new(100)
end
# Returns the complement of a color.
# This is identical to `adjust-hue(color, 180deg)`.
#
# @param color [Color]
# @return [Color]
# @raise [ArgumentError] if `color` isn't a color
# @see #adjust_hue #adjust-hue
def complement(color)
adjust_hue color, Number.new(180)
end
# Removes quotes from a string if the string is quoted,
# or returns the same string if it's not.
#
# @param str [String]
# @return [String]
# @raise [ArgumentError] if `str` isn't a string
# @see #quote
# @example
# unquote("foo") => foo
# unquote(foo) => foo
def unquote(str)
assert_type str, :String
Sass::Script::String.new(str.value, :identifier)
end
# Add quotes to a string if the string isn't quoted,
# or returns the same string if it is.
#
# @param str [String]
# @return [String]
# @raise [ArgumentError] if `str` isn't a string
# @see #unquote
# @example
# quote("foo") => "foo"
# quote(foo) => "foo"
def quote(str)
assert_type str, :String
Sass::Script::String.new(str.value, :string)
end
# Inspects the type of the argument, returning it as an unquoted string.
# For example:
#
# type-of(100px) => number
# type-of(asdf) => string
# type-of("asdf") => string
# type-of(true) => bool
# type-of(#fff) => color
# type-of(blue) => color
#
# @param obj [Literal] The object to inspect
# @return [String] The unquoted string name of the literal's type
def type_of(obj)
Sass::Script::String.new(obj.class.name.gsub(/Sass::Script::/,'').downcase)
end
# Inspects the unit of the number, returning it as a quoted string.
# Complex units are sorted in alphabetical order by numerator and denominator.
# For example:
#
# unit(100) => ""
# unit(100px) => "px"
# unit(3em) => "em"
# unit(10px * 5em) => "em*px"
# unit(10px * 5em / 30cm / 1rem) => "em*px/cm*rem"
#
# @param number [Literal] The number to inspect
# @return [String] The unit(s) of the number
# @raise [ArgumentError] if `number` isn't a number
def unit(number)
assert_type number, :Number
Sass::Script::String.new(number.unit_str, :string)
end
# Inspects the unit of the number, returning a boolean indicating if it is unitless.
# For example:
#
# unitless(100) => true
# unitless(100px) => false
#
# @param number [Literal] The number to inspect
# @return [Bool] Whether or not the number is unitless
# @raise [ArgumentError] if `number` isn't a number
def unitless(number)
assert_type number, :Number
Sass::Script::Bool.new(number.unitless?)
end
# Returns true if two numbers are similar enough to be added, subtracted, or compared.
# For example:
#
# comparable(2px, 1px) => true
# comparable(100px, 3em) => false
# comparable(10cm, 3mm) => true
#
# @param number1 [Number]
# @param number2 [Number]
# @return [Bool] indicating if the numbers can be compared.
# @raise [ArgumentError] if `number1` or `number2` aren't numbers
def comparable(number1, number2)
assert_type number1, :Number
assert_type number2, :Number
Sass::Script::Bool.new(number1.comparable_to?(number2))
end
# Converts a decimal number to a percentage.
# For example:
#
# percentage(100px / 50px) => 200%
#
# @param value [Number] The decimal number to convert to a percentage
# @return [Number] The percentage
# @raise [ArgumentError] If `value` isn't a unitless number
def percentage(value)
unless value.is_a?(Sass::Script::Number) && value.unitless?
raise ArgumentError.new("#{value.inspect} is not a unitless number")
end
Sass::Script::Number.new(value.value * 100, ['%'])
end
# Rounds a number to the nearest whole number.
# For example:
#
# round(10.4px) => 10px
# round(10.6px) => 11px
#
# @param value [Number] The number
# @return [Number] The rounded number
# @raise [Sass::SyntaxError] if `value` isn't a number
def round(value)
numeric_transformation(value) {|n| n.round}
end
# Rounds a number up to the nearest whole number.
# For example:
#
# ciel(10.4px) => 11px
# ciel(10.6px) => 11px
#
# @param value [Number] The number
# @return [Number] The rounded number
# @raise [Sass::SyntaxError] if `value` isn't a number
def ceil(value)
numeric_transformation(value) {|n| n.ceil}
end
# Rounds down to the nearest whole number.
# For example:
#
# floor(10.4px) => 10px
# floor(10.6px) => 10px
#
# @param value [Number] The number
# @return [Number] The rounded number
# @raise [Sass::SyntaxError] if `value` isn't a number
def floor(value)
numeric_transformation(value) {|n| n.floor}
end
# Finds the absolute value of a number.
# For example:
#
# abs(10px) => 10px
# abs(-10px) => 10px
#
# @param value [Number] The number
# @return [Number] The absolute value
# @raise [Sass::SyntaxError] if `value` isn't a number
def abs(value)
numeric_transformation(value) {|n| n.abs}
end
private
# This method implements the pattern of transforming a numeric value into
# another numeric value with the same units.
# It yields a number to a block to perform the operation and return a number
def numeric_transformation(value)
assert_type value, :Number
Sass::Script::Number.new(yield(value.value), value.numerator_units, value.denominator_units)
end
def adjust(color, amount, attr, range, op, units = "")
assert_type color, :Color
assert_type amount, :Number
unless range.include?(amount.value)
raise ArgumentError.new("Amount #{amount} must be between #{range.first}#{units} and #{range.last}#{units}")
end
# TODO: is it worth restricting here,
# or should we do so in the Color constructor itself,
# and allow clipping in rgb() et al?
color.with(attr => Haml::Util.restrict(
color.send(attr).send(op, amount.value), range))
end
end
end