///|
fn global_color_builtin(name : String) -> Bool {
[
"rgb", "rgba", "hsl", "hsla", "red", "green", "blue", "alpha", "opacity", "hue",
"saturation", "lightness", "mix", "lighten", "darken", "saturate", "desaturate",
"adjust-hue", "complement", "grayscale", "invert", "opacify", "fade-in", "transparentize",
"fade-out", "adjust-color", "change-color", "scale-color",
].contains(name)
}
///|
fn color_builtin(
name : String,
positional : Array[SassValue],
keywords : Map[String, SassValue],
) -> SassValue? raise ParseError {
let operation = if name.has_prefix("color.") {
name[6:].to_owned()
} else {
name
}
let rgb = operation == "rgb" || operation == "rgba"
let hsl = operation == "hsl" || operation == "hsla"
let qualified = name.has_prefix("color.")
if qualified &&
[
"rgb", "rgba", "hsl", "hsla", "lighten", "darken", "saturate", "desaturate",
"adjust-hue", "opacify", "transparentize", "fade-in", "fade-out",
].contains(operation) {
return None
}
if (["adjust", "change", "scale"].contains(operation) && qualified) ||
["adjust-color", "change-color", "scale-color"].contains(operation) {
return Some(Color(color_modify(operation, positional, keywords)))
}
if !global_color_builtin(operation) {
return None
}
if (rgb || hsl) &&
positional.length() == 1 &&
keywords.is_empty() &&
positional[0] is Text(_, _) {
return None
}
// CSS filter forms remain plain CSS functions.
if ["opacity", "saturate", "grayscale", "invert"].contains(operation) &&
!qualified &&
positional.length() == 1 &&
positional[0] is Number(_) &&
keywords.is_empty() {
return None
}
let spec = if rgb && (positional.length() == 2 || keywords.contains("color")) {
["color", "alpha"]
} else if rgb {
["red", "green", "blue", "alpha"]
} else if hsl {
["hue", "saturation", "lightness", "alpha"]
} else if operation == "mix" {
["color1", "color2", "weight"]
} else if [
"lighten", "darken", "saturate", "desaturate", "opacify", "fade-in", "transparentize",
"fade-out",
].contains(operation) {
["color", "amount"]
} else if operation == "adjust-hue" {
["color", "degrees"]
} else if operation == "invert" {
["color", "weight"]
} else {
["color"]
}
let minimum = if rgb && spec[0] == "color" {
2
} else if rgb || hsl {
3
} else if operation == "mix" {
2
} else if operation == "invert" {
1
} else {
spec.length()
}
let args = positional.copy()
if args.length() > spec.length() {
raise Invalid("too many color arguments")
}
for i in 0.. v is Null) {
raise Invalid("missing color argument")
}
let optional = fn(i : Int, fallback : SassValue) -> SassValue {
args.get(i).unwrap_or(fallback)
}
if rgb || hsl {
if args.iter().any(v => v is Text(_, _)) {
// var()/calc() in channel positions must survive for browser evaluation.
if args
.iter()
.any(v => {
match v {
Text(s, false) => s.has_prefix("var(") || s.has_prefix("calc(")
_ => false
}
}) {
return Some(
Text(operation + "(" + args.map(v => v.css()).join(", ") + ")", false),
)
}
}
if rgb && spec[0] == "color" {
let c = require_color(args[0])
return Some(
Color({
..c,
alpha: color_component(args[1], 1.0).max(0.0).min(1.0),
original: None,
format: "rgb",
}),
)
}
let alpha = color_component(optional(3, numeric(1.0)), 1.0)
.max(0.0)
.min(1.0)
if rgb {
let r = color_component(args[0], 255.0).max(0.0).min(255.0)
let g = color_component(args[1], 255.0).max(0.0).min(255.0)
let b = color_component(args[2], 255.0).max(0.0).min(255.0)
let percent = args[:3]
.iter()
.all(v => {
match v {
Number(n) => n.numerator == ["%"]
_ => false
}
})
return Some(
Color(
color(r, g, b, alpha~, format=if percent { "percent" } else { "rgb" }),
),
)
}
let angle = require_number(args[0])
let hue = angle
.convert(require_number(numeric(0.0, unit="deg")), unitless=true)
.unwrap_or_else(() => raise Invalid("expected angle"))
return Some(
Color(
color_hsl(
hue,
color_component(args[1], 100.0),
color_component(args[2], 100.0),
alpha,
"hsl",
),
),
)
}
let c = require_color(args[0])
match operation {
"red" => Some(numeric(c.red.round()))
"green" => Some(numeric(c.green.round()))
"blue" => Some(numeric(c.blue.round()))
"alpha" | "opacity" => Some(numeric(c.alpha))
"hue" => Some(numeric(c.hsl().0, unit="deg"))
"saturation" => Some(numeric(c.hsl().1, unit="%"))
"lightness" => Some(numeric(c.hsl().2, unit="%"))
"mix" | "invert" => {
let weight = color_component(
optional(
if operation == "mix" {
2
} else {
1
},
numeric(if operation == "mix" { 50.0 } else { 100.0 }, unit="%"),
),
100.0,
percent_only=true,
)
if weight < 0.0 || weight > 100.0 {
raise Invalid("color weight out of range")
}
if operation == "mix" {
Some(Color(color_mix(c, require_color(args[1]), weight)))
} else {
Some(
Color(
color_mix(
color(
255.0 - c.red,
255.0 - c.green,
255.0 - c.blue,
alpha=c.alpha,
),
c,
weight,
),
),
)
}
}
"opacify" | "fade-in" | "transparentize" | "fade-out" => {
let amount = color_component(args[1], 1.0)
if amount < 0.0 || amount > 1.0 {
raise Invalid("alpha amount out of range")
}
let delta = if operation == "opacify" || operation == "fade-in" {
amount
} else {
-amount
}
Some(
Color({
..c,
alpha: (c.alpha + delta).max(0.0).min(1.0),
original: None,
}),
)
}
_ => {
let (h, s, l) = c.hsl()
let mut h = h
let mut s = s
let mut l = l
if operation == "complement" {
h += 180.0
} else if operation == "grayscale" {
s = 0.0
} else if operation == "adjust-hue" {
let angle = require_number(args[1])
h += angle
.convert(require_number(numeric(0.0, unit="deg")), unitless=true)
.unwrap_or_else(() => raise Invalid("expected angle"))
} else {
let amount = color_component(args[1], 100.0)
if amount < 0.0 || amount > 100.0 {
raise Invalid("color amount out of range")
}
if operation == "lighten" {
l += amount
} else if operation == "darken" {
l -= amount
} else if operation == "saturate" {
s += amount
} else if operation == "desaturate" {
s -= amount
} else {
return None
}
}
Some(
Color(
color_hsl(
h,
s.max(0.0).min(100.0),
l.max(0.0).min(100.0),
c.alpha,
if c.format == "hsl" {
"hsl"
} else {
"auto"
},
),
),
)
}
}
}
///|
fn color_modify(
operation : String,
positional : Array[SassValue],
keywords : Map[String, SassValue],
) -> SassColor raise ParseError {
let c = if positional.length() == 1 {
if keywords.contains("color") {
raise Invalid("color supplied twice")
}
require_color(positional[0])
} else if positional.is_empty() && keywords.get("color") is Some(v) {
keywords.remove("color")
require_color(v)
} else {
raise Invalid("expected one color")
}
let rgb = ["red", "green", "blue"].iter().any(k => keywords.contains(k))
let hsl = ["hue", "saturation", "lightness"]
.iter()
.any(k => keywords.contains(k))
if rgb && hsl {
raise Invalid("cannot mix RGB and HSL adjustments")
}
let modify = fn(
key : String,
value : Double,
maximum : Double,
) -> Double raise ParseError {
match keywords.get(key) {
None => value
Some(v) => {
keywords.remove(key)
if operation.has_prefix("scale") {
if key == "hue" {
raise Invalid("cannot scale hue")
}
let amount = color_component(v, 1.0, percent_only=true)
if amount < -1.0 || amount > 1.0 {
raise Invalid("scale amount out of range")
}
value + amount * (if amount > 0.0 { maximum - value } else { value })
} else {
let amount = if key == "hue" {
require_number(v)
.convert(require_number(numeric(0.0, unit="deg")), unitless=true)
.unwrap_or_else(() => raise Invalid("expected angle"))
} else {
color_component(v, maximum)
}
let out = if operation.has_prefix("change") {
amount
} else {
value + amount
}
if key != "hue" {
out.max(0.0).min(maximum)
} else {
out
}
}
}
}
}
let alpha = modify("alpha", c.alpha, 1.0)
let result = if hsl {
let (h, s, l) = c.hsl()
color_hsl(
modify("hue", h, 360.0),
modify("saturation", s, 100.0),
modify("lightness", l, 100.0),
alpha,
if c.format == "hsl" {
"hsl"
} else {
"auto"
},
)
} else {
color(
modify("red", c.red, 255.0),
modify("green", c.green, 255.0),
modify("blue", c.blue, 255.0),
alpha~,
format=if c.format == "hsl" { "hsl" } else { "auto" },
)
}
if !keywords.is_empty() {
raise Invalid("unsupported color adjustment keyword or color space")
}
result
}