Hwaro::Assets::Sass::Builtins
Constants
ADJUST_KWARGS = ["red", "green", "blue", "hue", "saturation", "lightness", "alpha"]
COLOR_FNS = {"adjust" => Fn.new do |args, kwargs|
compound_color("color.adjust", ComponentMode::Adjust, args, kwargs)
end, "scale" => Fn.new do |args, kwargs|
compound_color("color.scale", ComponentMode::Scale, args, kwargs)
end, "change" => Fn.new do |args, kwargs|
compound_color("color.change", ComponentMode::Change, args, kwargs)
end, "mix" => Fn.new do |args, kwargs|
bound = bind!("mix", args, kwargs, ["color1", "color2", "weight"], required: 2)
weight = (w = bound[2]) ? amount!("mix", w) : 50.0
mix_colors(color!("mix", bound[0].as(Value)), color!("mix", bound[1].as(Value)), weight)
end, "invert" => Fn.new do |args, kwargs|
bound = shadowed_bind!("invert", args, kwargs, ["color", "weight"], required: 1)
color = shadowed_color!(bound[0].as(Value))
weight = (w = bound[1]) ? amount!("invert", w) : 100.0
inverse = ColorV.new(255.0 - color.red, 255.0 - color.green, 255.0 - color.blue, color.alpha)
mix_colors(inverse, color, weight)
end, "grayscale" => Fn.new do |args, kwargs|
bound = shadowed_bind!("grayscale", args, kwargs, ["color"], required: 1)
adjust_hsl(shadowed_color!(bound[0].as(Value)), saturation: -100.0)
end, "complement" => Fn.new do |args, kwargs|
bound = bind!("complement", args, kwargs, ["color"], required: 1)
adjust_hsl(color!("complement", bound[0].as(Value)), hue: 180.0)
end, "adjust-hue" => Fn.new do |args, kwargs|
bound = bind!("adjust-hue", args, kwargs, ["color", "degrees"], required: 2)
color = color!("adjust-hue", bound[0].as(Value))
adjust_hsl(color, hue: finite!("adjust-hue", number!("adjust-hue", bound[1].as(Value))))
end, "darken" => Fn.new do |args, kwargs|
bound = bind!("darken", args, kwargs, ["color", "amount"], required: 2)
color = color!("darken", bound[0].as(Value))
adjust_hsl(color, lightness: -(amount!("darken", bound[1].as(Value))))
end, "lighten" => Fn.new do |args, kwargs|
bound = bind!("lighten", args, kwargs, ["color", "amount"], required: 2)
color = color!("lighten", bound[0].as(Value))
adjust_hsl(color, lightness: amount!("lighten", bound[1].as(Value)))
end, "saturate" => Fn.new do |args, kwargs|
bound = shadowed_bind!("saturate", args, kwargs, ["color", "amount"], required: 2)
color = shadowed_color!(bound[0].as(Value))
adjust_hsl(color, saturation: amount!("saturate", bound[1].as(Value)))
end, "desaturate" => Fn.new do |args, kwargs|
bound = bind!("desaturate", args, kwargs, ["color", "amount"], required: 2)
color = color!("desaturate", bound[0].as(Value))
adjust_hsl(color, saturation: -(amount!("desaturate", bound[1].as(Value))))
end, "opacify" => Fn.new do |args, kwargs|
bound = bind!("opacify", args, kwargs, ["color", "amount"], required: 2)
color = color!("opacify", bound[0].as(Value))
color.with_alpha(color.alpha + (alpha!("opacify", bound[1].as(Value))))
end, "transparentize" => Fn.new do |args, kwargs|
bound = bind!("transparentize", args, kwargs, ["color", "amount"], required: 2)
color = color!("transparentize", bound[0].as(Value))
color.with_alpha(color.alpha - (alpha!("transparentize", bound[1].as(Value))))
end, "red" => Fn.new do |args, kwargs|
bound = bind!("red", args, kwargs, ["color"], required: 1)
Number.new((color!("red", bound[0].as(Value))).red8.to_f)
end, "green" => Fn.new do |args, kwargs|
bound = bind!("green", args, kwargs, ["color"], required: 1)
Number.new((color!("green", bound[0].as(Value))).green8.to_f)
end, "blue" => Fn.new do |args, kwargs|
bound = bind!("blue", args, kwargs, ["color"], required: 1)
Number.new((color!("blue", bound[0].as(Value))).blue8.to_f)
end, "hue" => Fn.new do |args, kwargs|
bound = bind!("hue", args, kwargs, ["color"], required: 1)
Number.new((color!("hue", bound[0].as(Value))).hue, "deg")
end, "saturation" => Fn.new do |args, kwargs|
bound = bind!("saturation", args, kwargs, ["color"], required: 1)
Number.new((color!("saturation", bound[0].as(Value))).saturation, "%")
end, "lightness" => Fn.new do |args, kwargs|
bound = bind!("lightness", args, kwargs, ["color"], required: 1)
Number.new((color!("lightness", bound[0].as(Value))).lightness, "%")
end, "alpha" => Fn.new do |args, kwargs|
bound = bind!("alpha", args, kwargs, ["color"], required: 1)
Number.new((color!("alpha", bound[0].as(Value))).alpha)
end, "opacity" => Fn.new do |args, kwargs|
bound = shadowed_bind!("opacity", args, kwargs, ["color"], required: 1)
Number.new((shadowed_color!(bound[0].as(Value))).alpha)
end}
GLOBAL_FNS = {"if" => IF_FN, "quote" => STRING_FNS["quote"], "unquote" => STRING_FNS["unquote"], "str-length" => STRING_FNS["length"], "str-index" => STRING_FNS["index"], "str-slice" => STRING_FNS["slice"], "str-insert" => STRING_FNS["insert"], "to-upper-case" => STRING_FNS["to-upper-case"], "to-lower-case" => STRING_FNS["to-lower-case"], "length" => LIST_FNS["length"], "nth" => LIST_FNS["nth"], "index" => LIST_FNS["index"], "append" => LIST_FNS["append"], "join" => LIST_FNS["join"], "zip" => LIST_FNS["zip"], "set-nth" => LIST_FNS["set-nth"], "is-bracketed" => LIST_FNS["is-bracketed"], "list-separator" => LIST_FNS["separator"], "map-get" => MAP_FNS["get"], "map-has-key" => MAP_FNS["has-key"], "map-keys" => MAP_FNS["keys"], "map-values" => MAP_FNS["values"], "map-merge" => MAP_FNS["merge"], "map-remove" => MAP_FNS["remove"], "percentage" => MATH_FNS["percentage"], "round" => MATH_FNS["round"], "ceil" => MATH_FNS["ceil"], "floor" => MATH_FNS["floor"], "abs" => MATH_FNS["abs"], "min" => MATH_FNS["min"], "max" => MATH_FNS["max"], "unit" => MATH_FNS["unit"], "unitless" => MATH_FNS["is-unitless"], "comparable" => MATH_FNS["compatible"], "type-of" => META_FNS["type-of"], "inspect" => META_FNS["inspect"], "darken" => COLOR_FNS["darken"], "lighten" => COLOR_FNS["lighten"], "saturate" => COLOR_FNS["saturate"], "desaturate" => COLOR_FNS["desaturate"], "grayscale" => COLOR_FNS["grayscale"], "greyscale" => COLOR_FNS["grayscale"], "complement" => COLOR_FNS["complement"], "adjust-hue" => COLOR_FNS["adjust-hue"], "invert" => COLOR_FNS["invert"], "mix" => COLOR_FNS["mix"], "adjust-color" => COLOR_FNS["adjust"], "scale-color" => COLOR_FNS["scale"], "change-color" => COLOR_FNS["change"], "opacify" => COLOR_FNS["opacify"], "fade-in" => COLOR_FNS["opacify"], "transparentize" => COLOR_FNS["transparentize"], "fade-out" => COLOR_FNS["transparentize"], "red" => COLOR_FNS["red"], "green" => COLOR_FNS["green"], "blue" => COLOR_FNS["blue"], "hue" => COLOR_FNS["hue"], "saturation" => COLOR_FNS["saturation"], "lightness" => COLOR_FNS["lightness"], "alpha" => COLOR_FNS["alpha"], "opacity" => COLOR_FNS["opacity"], "ie-hex-str" => IE_HEX_STR_FN, "rgba" => RGBA_FN, "rgb" => RGBA_FN, "selector-parse" => SELECTOR_FNS["parse"], "selector-nest" => SELECTOR_FNS["nest"], "selector-append" => SELECTOR_FNS["append"], "selector-unify" => SELECTOR_FNS["unify"], "selector-replace" => SELECTOR_FNS["replace"], "is-superselector" => SELECTOR_FNS["is-superselector"], "simple-selectors" => SELECTOR_FNS["simple-selectors"]}
IE_HEX_STR_FN = Fn.new do |args, kwargs|
bound = bind!("ie-hex-str", args, kwargs, ["color"], required: 1)
color = color!("ie-hex-str", bound[0].as(Value))
alpha8 = ColorV.fuzzy_round(color.alpha * 255.0)
Str.new("#%02X%02X%02X%02X" % {alpha8, color.red8, color.green8, color.blue8}, quoted: false)
end
ie-hex-str($color) — the legacy #AARRGGBB spelling IE filters
take.
IF_FN = Fn.new do |args, kwargs|
no_kwargs!("if", kwargs)
arity!("if", args, 3)
args[0].truthy? ? args[1] : args[2]
end
LIST_FNS = {"length" => Fn.new do |args, kwargs|
args = args_with_kwargs("list.length", args, kwargs, ["list"] of ::String)
arity!("length", args, 1)
Number.new((list_of(args[0])).size.to_f, "")
end, "nth" => Fn.new do |args, kwargs|
args = args_with_kwargs("list.nth", args, kwargs, ["list", "n"] of ::String)
arity!("nth", args, 2)
items = list_of(args[0])
n = (number!("nth", args[1])).int_value("nth() index")
if n == 0
raise(SoftEvalError.new("nth() index may not be 0"))
end
idx = n > 0 ? n - 1 : items.size + n
if 0 <= idx && idx < items.size
else
raise(SoftEvalError.new("nth() index #{n} is out of bounds for a #{items.size}-element list"))
end
items[idx]
end, "index" => Fn.new do |args, kwargs|
args = args_with_kwargs("list.index", args, kwargs, ["list", "value"] of ::String)
arity!("index", args, 2)
items = list_of(args[0])
found = items.index(&.eq?(args[1]))
found ? Number.new((found + 1).to_f, "") : NullV.new
end, "append" => Fn.new do |args, kwargs|
args = args_with_kwargs("list.append", args, kwargs, ["list", "val", "separator"] of ::String)
arity!("append", args, 2, 3)
base = args[0]
sep = base.is_a?(ListV) ? base.sep : ListV::Sep::Space
bracketed = base.is_a?(ListV) && base.bracketed
ListV.new((list_of(base)) + [args[1]], sep_from(args[2]?, sep), bracketed)
end, "join" => Fn.new do |args, kwargs|
args = args_with_kwargs("list.join", args, kwargs, ["list1", "list2", "separator", "bracketed"] of ::String, defaults: {2 => (Str.new("auto", quoted: false)).as(Value)})
arity!("join", args, 2, 4)
base = args[0]
other = args[1]
sep = if base.is_a?(ListV) && base.items.size > 1
base.sep
elsif other.is_a?(ListV) && other.items.size > 1
other.sep
else
ListV::Sep::Space
end
bracketed = if (b = args[3]?) && (!(b.is_a?(Str) && (b.text == "auto")))
b.truthy?
else
base.is_a?(ListV) && base.bracketed
end
ListV.new((list_of(base)) + (list_of(args[1])), sep_from(args[2]?, sep), bracketed)
end, "zip" => Fn.new do |args, kwargs|
no_kwargs!("list.zip", kwargs)
arity!("zip", args, 1, Int32::MAX)
lists = args.map do |a| list_of(a) end
size = lists.min_of(&.size)
items = (0...size).map do |i|
(ListV.new(lists.map do |l| l[i] end, ListV::Sep::Space)).as(Value)
end
ListV.new(items, ListV::Sep::Comma)
end, "set-nth" => Fn.new do |args, kwargs|
args = args_with_kwargs("list.set-nth", args, kwargs, ["list", "n", "value"] of ::String)
arity!("set-nth", args, 3)
base = args[0]
items = (list_of(base)).dup
n = (number!("set-nth", args[1])).int_value("set-nth() index")
if n == 0
raise(SoftEvalError.new("set-nth() index may not be 0"))
end
idx = n > 0 ? n - 1 : items.size + n
if 0 <= idx && idx < items.size
else
raise(SoftEvalError.new("set-nth() index #{n} is out of bounds for a #{items.size}-element list"))
end
items[idx] = args[2]
sep = base.is_a?(ListV) ? base.sep : ListV::Sep::Space
bracketed = base.is_a?(ListV) && base.bracketed
ListV.new(items, sep, bracketed)
end, "slash" => Fn.new do |args, kwargs|
no_kwargs!("list.slash", kwargs)
arity!("list.slash", args, 2, Int32::MAX)
ListV.new(args.dup, ListV::Sep::Slash)
end, "is-bracketed" => Fn.new do |args, kwargs|
args = args_with_kwargs("list.is-bracketed", args, kwargs, ["list"] of ::String)
arity!("is-bracketed", args, 1)
value = args[0]
BoolV.new(value.is_a?(ListV) && value.bracketed)
end, "separator" => Fn.new do |args, kwargs|
args = args_with_kwargs("list.separator", args, kwargs, ["list"] of ::String)
arity!("list-separator", args, 1)
sep = case value = args[0]
when ListV
value.sep
else
ListV::Sep::Space
end
name = case sep
in ListV::Sep::Comma
"comma"
in ListV::Sep::Space
"space"
in ListV::Sep::Slash
"slash"
end
Str.new(name, quoted: false)
end}
MAP_FNS = {"get" => Fn.new do |args, kwargs|
args = args_with_kwargs("map.get", args, kwargs, ["map", "key"] of ::String)
arity!("map-get", args, 2, Int32::MAX)
value = args[0].as(Value)
args[1..].each do |key|
if value.is_a?(NullV)
break
end
value = (map!("map-get", value))[key]? || NullV.new
end
value
end, "has-key" => Fn.new do |args, kwargs|
args = args_with_kwargs("map.has-key", args, kwargs, ["map", "key"] of ::String)
arity!("map-has-key", args, 2, Int32::MAX)
value = args[0].as(Value)
found_all = true
args[1..].each do |key|
map = (value.as?(MapV)) ||
(value.is_a?(Raw) ? (Expr.coerce(value.text)).as?(MapV) : nil)
if map && (found = map[key]?)
else
found_all = false
break
end
value = found
end
BoolV.new(found_all)
end, "keys" => Fn.new do |args, kwargs|
args = args_with_kwargs("map.keys", args, kwargs, ["map"] of ::String)
arity!("map-keys", args, 1)
ListV.new((map!("map-keys", args[0])).entries.map(&.key), ListV::Sep::Comma)
end, "values" => Fn.new do |args, kwargs|
args = args_with_kwargs("map.values", args, kwargs, ["map"] of ::String)
arity!("map-values", args, 1)
ListV.new((map!("map-values", args[0])).entries.map(&.value), ListV::Sep::Comma)
end, "merge" => Fn.new do |args, kwargs|
args = args_with_kwargs("map.merge", args, kwargs, ["map1", "map2"] of ::String)
arity!("map-merge", args, 2)
base = map!("map-merge", args[0])
overlay = map!("map-merge", args[1])
entries = base.entries.dup
overlay.entries.each do |entry|
if idx = entries.index() do |__arg6| __arg6.key.eq?(entry.key) end
entries[idx] = entry
else
entries << entry
end
end
MapV.new(entries)
end, "remove" => Fn.new do |args, kwargs|
args = args_with_kwargs("map.remove", args, kwargs, ["map", "key"] of ::String)
arity!("map-remove", args, 1, Int32::MAX)
base = map!("map-remove", args[0])
keys = args[1..]
MapV.new(base.entries.reject do |e| keys.any?(&.eq?(e.key)) end)
end, "set" => Fn.new do |args, kwargs|
no_kwargs!("map.set", kwargs)
arity!("map.set", args, 3, Int32::MAX)
map_set(map!("map.set", args[0]), args[1...-1], args[-1])
end, "deep-merge" => Fn.new do |args, kwargs|
args = args_with_kwargs("map.deep-merge", args, kwargs, ["map1", "map2"] of ::String)
arity!("map.deep-merge", args, 2)
map_deep_merge(map!("map.deep-merge", args[0]), map!("map.deep-merge", args[1]))
end, "deep-remove" => Fn.new do |args, kwargs|
args = args_with_kwargs("map.deep-remove", args, kwargs, ["map", "key"] of ::String)
arity!("map.deep-remove", args, 2, Int32::MAX)
map_deep_remove(map!("map.deep-remove", args[0]), args[1..])
end}
MATH_FNS = {"div" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.div", args, kwargs, ["number1", "number2"] of ::String)
arity!("math.div", args, 2)
a = number!("math.div", args[0])
b = number!("math.div", args[1])
if b.value == 0
raise(SoftEvalError.new("math.div(): division by zero"))
end
value, unit = Expr.divide_numbers(a, b)
Number.new(value, unit)
end, "percentage" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.percentage", args, kwargs, ["number"] of ::String)
arity!("math.percentage", args, 1)
n = number!("math.percentage", args[0])
if n.unit.empty?
else
raise(SoftEvalError.new("math.percentage() expects a unitless number, got #{n.to_css}"))
end
Number.new(n.value * 100, "%")
end, "round" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.round", args, kwargs, ["number"] of ::String)
arity!("round", args, 1)
n = number!("round", args[0])
Number.new(n.value.round(mode: :ties_away).to_f, n.unit)
end, "ceil" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.ceil", args, kwargs, ["number"] of ::String)
arity!("ceil", args, 1)
n = number!("ceil", args[0])
Number.new(n.value.ceil.to_f, n.unit)
end, "floor" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.floor", args, kwargs, ["number"] of ::String)
arity!("floor", args, 1)
n = number!("floor", args[0])
Number.new(n.value.floor.to_f, n.unit)
end, "abs" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.abs", args, kwargs, ["number"] of ::String)
arity!("abs", args, 1)
n = number!("abs", args[0])
Number.new(n.value.abs, n.unit)
end, "min" => Fn.new do |args, kwargs|
no_kwargs!("math.min", kwargs)
arity!("min", args, 1, Int32::MAX)
numbers = args.map do |a| number!("min", a) end
base = same_units!("min", numbers)
winner = numbers.min_by do |n| comparable_value(n, base) end
Number.new(winner.value, winner.unit)
end, "max" => Fn.new do |args, kwargs|
no_kwargs!("math.max", kwargs)
arity!("max", args, 1, Int32::MAX)
numbers = args.map do |a| number!("max", a) end
base = same_units!("max", numbers)
winner = numbers.max_by do |n| comparable_value(n, base) end
Number.new(winner.value, winner.unit)
end, "clamp" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.clamp", args, kwargs, ["min", "number", "max"] of ::String)
arity!("math.clamp", args, 3)
numbers = args.map do |a| number!("math.clamp", a) end
base = same_units!("math.clamp", numbers)
low, mid, high = numbers[0], numbers[1], numbers[2]
lv = comparable_value(low, base)
mv = comparable_value(mid, base)
hv = comparable_value(high, base)
winner = mv < lv ? low : (mv > hv ? high : mid)
Number.new(winner.value, winner.unit)
end, "log" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.log", args, kwargs, ["number", "base"] of ::String)
arity!("math.log", args, 1, 2)
value = unitless!("math.log", args[0])
if value <= 0
raise(SoftEvalError.new("math.log() of a non-positive number"))
end
result = Math.log(value)
if base_arg = args[1]?
base = unitless!("math.log", base_arg)
if base <= 0 || (base == 1)
raise(SoftEvalError.new("math.log(): invalid base #{Number.format(base)}"))
end
result = result / (Math.log(base))
end
Number.new(result, "")
end, "hypot" => Fn.new do |args, kwargs|
no_kwargs!("math.hypot", kwargs)
arity!("math.hypot", args, 1, Int32::MAX)
numbers = args.map do |a| number!("math.hypot", a) end
unit = numbers[0].unit
sum = numbers.reduce(0.0) do |acc, n|
v = if (unit.empty? || n.unit.empty?) || (n.unit == unit)
if unit.empty? != n.unit.empty?
raise(SoftEvalError.new("math.hypot(): mixed unit and unitless arguments"))
end
n.value
else
factor = Number.conversion_factor(n.unit, unit)
if factor
else
raise(SoftEvalError.new("math.hypot(): incompatible units #{unit} and #{n.unit}"))
end
n.value * factor
end
acc + (v * v)
end
Number.new(Math.sqrt(sum), unit)
end, "sin" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.sin", args, kwargs, ["number"] of ::String)
arity!("math.sin", args, 1)
Number.new(Math.sin(radians!("math.sin", number!("math.sin", args[0]))), "")
end, "cos" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.cos", args, kwargs, ["number"] of ::String)
arity!("math.cos", args, 1)
Number.new(Math.cos(radians!("math.cos", number!("math.cos", args[0]))), "")
end, "tan" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.tan", args, kwargs, ["number"] of ::String)
arity!("math.tan", args, 1)
Number.new(Math.tan(radians!("math.tan", number!("math.tan", args[0]))), "")
end, "asin" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.asin", args, kwargs, ["number"] of ::String)
arity!("math.asin", args, 1)
v = unitless!("math.asin", args[0])
if v < -1 || v > 1
raise(SoftEvalError.new("math.asin() argument must be within -1 and 1"))
end
Number.new(((Math.asin(v)) * 180.0) / Math::PI, "deg")
end, "acos" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.acos", args, kwargs, ["number"] of ::String)
arity!("math.acos", args, 1)
v = unitless!("math.acos", args[0])
if v < -1 || v > 1
raise(SoftEvalError.new("math.acos() argument must be within -1 and 1"))
end
Number.new(((Math.acos(v)) * 180.0) / Math::PI, "deg")
end, "atan" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.atan", args, kwargs, ["number"] of ::String)
arity!("math.atan", args, 1)
Number.new(((Math.atan(unitless!("math.atan", args[0]))) * 180.0) / Math::PI, "deg")
end, "atan2" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.atan2", args, kwargs, ["y", "x"] of ::String)
arity!("math.atan2", args, 2)
y = number!("math.atan2", args[0])
x = number!("math.atan2", args[1])
if y.compatible_unit?(x)
else
raise(SoftEvalError.new("math.atan2(): incompatible units #{y.unit} and #{x.unit}"))
end
xv = y.unit.empty? ? x.value : y.coerce_value(x)
Number.new(((Math.atan2(y.value, xv)) * 180.0) / Math::PI, "deg")
end, "pow" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.pow", args, kwargs, ["base", "exponent"] of ::String)
arity!("math.pow", args, 2)
base = number!("math.pow", args[0])
exp = number!("math.pow", args[1])
if base.unit.empty? && exp.unit.empty?
else
raise(SoftEvalError.new("math.pow() expects unitless numbers"))
end
Number.new(base.value ** exp.value, "")
end, "sqrt" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.sqrt", args, kwargs, ["number"] of ::String)
arity!("math.sqrt", args, 1)
n = number!("math.sqrt", args[0])
if n.unit.empty?
else
raise(SoftEvalError.new("math.sqrt() expects a unitless number"))
end
if n.value < 0
raise(SoftEvalError.new("math.sqrt() of a negative number"))
end
Number.new(Math.sqrt(n.value), "")
end, "unit" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.unit", args, kwargs, ["number"] of ::String)
arity!("unit", args, 1)
Str.new((number!("unit", args[0])).unit, quoted: true)
end, "is-unitless" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.is-unitless", args, kwargs, ["number"] of ::String)
arity!("unitless", args, 1)
BoolV.new((number!("unitless", args[0])).unit.empty?)
end, "compatible" => Fn.new do |args, kwargs|
args = args_with_kwargs("math.compatible", args, kwargs, ["number1", "number2"] of ::String)
arity!("comparable", args, 2)
a = number!("comparable", args[0])
b = number!("comparable", args[1])
BoolV.new(a.compatible_unit?(b))
end}
MATH_VARS = {"pi" => "3.1415926536", "e" => "2.7182818285", "epsilon" => "2.220446049250313e-16", "max-safe-integer" => "9007199254740991", "min-safe-integer" => "-9007199254740991", "max-number" => "17976931348623157" + ("0" * 292), "min-number" => "5e-324"}
META_FNS = {"type-of" => Fn.new do |args, kwargs|
args = args_with_kwargs("meta.type-of", args, kwargs, ["value"] of ::String)
arity!("type-of", args, 1)
name = case value = args[0]
when Number
"number"
when ColorV
"color"
when BoolV
"bool"
when NullV
"null"
when ListV
"list"
when MapV
"map"
when Str
!value.quoted && (ColorV.parse?(value.text)) ? "color" : "string"
else
value.is_a?(Raw) ? type_of_raw(value.text) : "string"
end
Str.new(name, quoted: false)
end, "inspect" => Fn.new do |args, kwargs|
args = args_with_kwargs("meta.inspect", args, kwargs, ["value"] of ::String)
arity!("inspect", args, 1)
Str.new(inspect_value(args[0]), quoted: false)
end}
MODULE_TABLES = {"math" => {MATH_FNS, MATH_VARS}, "string" => {STRING_FNS, {} of String => String}, "list" => {LIST_FNS, {} of String => String}, "map" => {MAP_FNS, {} of String => String}, "meta" => {META_FNS, {} of String => String}, "color" => {COLOR_FNS, {} of String => String}, "selector" => {SELECTOR_FNS, {} of String => String}}
sass:<name> module tables: {functions, variables}.
RGBA_FN = Fn.new do |args, kwargs|
bound = shadowed_bind!("rgba", args, kwargs, ["color", "alpha"], required: 2)
color = shadowed_color!(bound[0].as(Value))
color.with_alpha(alpha!("rgba", bound[1].as(Value)))
end
rgba($color, $alpha) / rgb($color, $alpha) — the Sass-only
two-argument spelling. Every other shape (rgb(0, 0, 0),
rgb(0 0 0 / 50%), relative color syntax) is real CSS, so those
decline with ShapeMismatch and reconstruct verbatim instead of
unwinding the whole declaration.
SCALE_KWARGS = ["red", "green", "blue", "saturation", "lightness", "alpha"]
SELECTOR_FNS = {"parse" => Fn.new do |args, kwargs|
args = args_with_kwargs("selector.parse", args, kwargs, ["selector"] of ::String)
arity!("selector-parse", args, 1)
selector_value(selector_complexes("selector.parse()", args[0]))
end, "nest" => Fn.new do |args, kwargs|
no_kwargs!("selector.nest", kwargs)
arity!("selector-nest", args, 1, Int32::MAX)
result = selector_complexes("selector.nest()", args[0])
args[1..].each do |arg|
parts = selector_complexes("selector.nest()", arg)
grown = [] of Array(String)
result.each do |parent|
parent_text = parent.join(' ')
parts.each do |part|
if part.any?(&.includes?('&'))
grown << part.flat_map() do |__arg16| (__arg16.gsub('&', parent_text)).split end
else
grown << (parent + part)
end
end
end
result = grown
end
selector_value(result)
end, "append" => Fn.new do |args, kwargs|
no_kwargs!("selector.append", kwargs)
arity!("selector-append", args, 1, Int32::MAX)
result = selector_complexes("selector.append()", args[0])
args[1..].each do |arg|
parts = selector_complexes("selector.append()", arg)
grown = [] of Array(String)
result.each do |parent|
parts.each do |part|
head = part[0]
if ([">", "+", "~"].includes?(head)) || ([">", "+", "~"].includes?(parent.last))
raise(SoftEvalError.new("selector.append(): can't append #{(part.join(' ')).inspect}"))
end
grown << ((parent[0...-1] + [parent.last + head]) + part[1..])
end
end
result = grown
end
selector_value(result)
end, "unify" => Fn.new do |args, kwargs|
args = args_with_kwargs("selector.unify", args, kwargs, ["selector1", "selector2"] of ::String)
arity!("selector-unify", args, 2)
a_list = selector_complexes("selector.unify()", args[0])
b_list = selector_complexes("selector.unify()", args[1])
unified = [] of Array(String)
a_list.each do |a|
b_list.each do |b|
merged = unify_compounds(a.last, b.last)
if merged
else
next
end
unified << ((a[0...-1] + b[0...-1]) + [merged])
end
end
if unified.empty?
next NullV.new.as(Value)
end
selector_value(unified)
end, "is-superselector" => Fn.new do |args, kwargs|
args = args_with_kwargs("selector.is-superselector", args, kwargs, ["super", "sub"] of ::String)
arity!("is-superselector", args, 2)
supers = selector_complexes("is-superselector()", args[0])
subs = selector_complexes("is-superselector()", args[1])
BoolV.new(subs.all? do |sub|
supers.any? do |sup| complex_superselector?(sup, sub) end
end)
end, "simple-selectors" => Fn.new do |args, kwargs|
args = args_with_kwargs("selector.simple-selectors", args, kwargs, ["selector"] of ::String)
arity!("simple-selectors", args, 1)
complexes = selector_complexes("selector.simple-selectors()", args[0])
if (complexes.size == 1) && (complexes[0].size == 1)
else
raise(SoftEvalError.new("selector.simple-selectors() expects a compound selector, got #{args[0].to_css.inspect}"))
end
element, simples = compound_simples(complexes[0][0])
names = [] of String
if element.empty?
else
names << element
end
names.concat(simples)
ListV.new(names.map do |n| (Str.new(n, quoted: false)).as(Value) end, ListV::Sep::Comma)
end, "replace" => Fn.new do |args, kwargs|
args = args_with_kwargs("selector.replace", args, kwargs, ["selector", "original", "replacement"] of ::String)
arity!("selector-replace", args, 3)
selector_value(selector_replace("selector.replace()", args[0], args[1], args[2]))
end}
STRING_FNS = {"quote" => Fn.new do |args, kwargs|
args = args_with_kwargs("string.quote", args, kwargs, ["string"] of ::String)
arity!("quote", args, 1)
s = string!("quote", args[0])
Str.new(s.text, quoted: true)
end, "unquote" => Fn.new do |args, kwargs|
args = args_with_kwargs("string.unquote", args, kwargs, ["string"] of ::String)
arity!("unquote", args, 1)
s = string!("unquote", args[0])
text = s.quoted ? Str.unquote_text(s.text) : s.text
Str.new(text, quoted: false)
end, "length" => Fn.new do |args, kwargs|
args = args_with_kwargs("string.length", args, kwargs, ["string"] of ::String)
arity!("str-length", args, 1)
Number.new((string!("str-length", args[0])).decoded.size.to_f, "")
end, "index" => Fn.new do |args, kwargs|
args = args_with_kwargs("string.index", args, kwargs, ["string", "substring"] of ::String)
arity!("str-index", args, 2)
haystack = (string!("str-index", args[0])).decoded
needle = (string!("str-index", args[1])).decoded
idx = haystack.index(needle)
idx ? Number.new((idx + 1).to_f, "") : NullV.new
end, "slice" => Fn.new do |args, kwargs|
args = args_with_kwargs("string.slice", args, kwargs, ["string", "start-at", "end-at"] of ::String)
arity!("str-slice", args, 2, 3)
text = (string!("str-slice", args[0])).decoded
quoted = (string!("str-slice", args[0])).quoted
start_at = (number!("str-slice", args[1])).int_value("str-slice() start")
end_at = args[2]? ? (number!("str-slice", args[2])).int_value("str-slice() end") : -1
size = text.size
from = start_at < 0 ? Math.max(size + start_at, 0) : Math.max(start_at - 1, 0)
to = end_at < 0 ? size + end_at : Math.min(end_at - 1, size - 1)
sliced = from > to ? "" : text[from..to]
Str.new(Str.encode(sliced), quoted: quoted)
end, "insert" => Fn.new do |args, kwargs|
args = args_with_kwargs("string.insert", args, kwargs, ["string", "insert", "index"] of ::String)
arity!("str-insert", args, 3)
base = string!("str-insert", args[0])
insert = (string!("str-insert", args[1])).decoded
index = (number!("str-insert", args[2])).int_value("str-insert() index")
base_text = base.decoded
size = base_text.size
if index < 0
index = (size + index) + 2
end
offset = (index - 1).clamp(0, size)
Str.new(Str.encode((base_text[0, offset] + insert) + base_text[offset..]), quoted: base.quoted, quote_char: base.quote_char)
end, "split" => Fn.new do |args, kwargs|
args = args_with_kwargs("string.split", args, kwargs, ["string", "separator", "limit"] of ::String)
arity!("string.split", args, 2, 3)
text = (string!("string.split", args[0])).text
sep = (string!("string.split", args[1])).text
limit = if (arg = args[2]?) && (!arg.is_a?(NullV))
(number!("string.split", arg)).int_value("string.split() limit")
end
parts = if sep.empty?
text.chars.map(&.to_s)
elsif limit
text.split(sep, limit + 1)
else
text.split(sep)
end
ListV.new(parts.map do |p| (Str.new(p, quoted: true)).as(Value) end, ListV::Sep::Comma, bracketed: true)
end, "to-upper-case" => Fn.new do |args, kwargs|
args = args_with_kwargs("string.to-upper-case", args, kwargs, ["string"] of ::String)
arity!("to-upper-case", args, 1)
s = string!("to-upper-case", args[0])
Str.new(s.text.upcase(Unicode::CaseOptions::ASCII), quoted: s.quoted, quote_char: s.quote_char)
end, "to-lower-case" => Fn.new do |args, kwargs|
args = args_with_kwargs("string.to-lower-case", args, kwargs, ["string"] of ::String)
arity!("to-lower-case", args, 1)
s = string!("to-lower-case", args[0])
Str.new(s.text.downcase(Unicode::CaseOptions::ASCII), quoted: s.quoted, quote_char: s.quote_char)
end}
Class methods
HWB → RGB (CSS Color 4): degenerate w+b ≥ 100 is a flat gray, otherwise the pure hue scaled between whiteness and blackness.