diff --git a/crates/swc_css_compat/src/compiler/color_hex_alpha.rs b/crates/swc_css_compat/src/compiler/color_hex_alpha.rs index 244afb4c9a3f..b6887e4e09c7 100644 --- a/crates/swc_css_compat/src/compiler/color_hex_alpha.rs +++ b/crates/swc_css_compat/src/compiler/color_hex_alpha.rs @@ -4,20 +4,9 @@ use swc_css_ast::{ AbsoluteColorBase, AlphaValue, Color, ComponentValue, Delimiter, DelimiterValue, Function, Ident, Number, }; +use swc_css_utils::{hex_to_rgba, round_alpha}; -use crate::compiler::{utils::round_alpha, Compiler}; - -#[inline] -fn from_hex(c: u8) -> u8 { - match c { - b'0'..=b'9' => c - b'0', - b'a'..=b'f' => c - b'a' + 10, - b'A'..=b'F' => c - b'A' + 10, - _ => { - unreachable!(); - } - } -} +use crate::compiler::Compiler; fn shorten_hex_color(value: &str) -> Option<&str> { let length = value.len(); @@ -32,33 +21,6 @@ fn shorten_hex_color(value: &str) -> Option<&str> { None } -fn hex_to_rgba(hex: &str) -> (u8, u8, u8, f64) { - let hex = hex.as_bytes(); - - match hex.len() { - 8 => { - let r = from_hex(hex[0]) * 16 + from_hex(hex[1]); - let g = from_hex(hex[2]) * 16 + from_hex(hex[3]); - let b = from_hex(hex[4]) * 16 + from_hex(hex[5]); - let a = (from_hex(hex[6]) * 16 + from_hex(hex[7])) as f64 / 255.0; - - (r, g, b, a) - } - 4 => { - let r = from_hex(hex[0]) * 17; - let g = from_hex(hex[1]) * 17; - let b = from_hex(hex[2]) * 17; - let a = (from_hex(hex[3]) * 17) as f64 / 255.0; - - (r, g, b, a) - } - - _ => { - unreachable!() - } - } -} - impl Compiler { pub(crate) fn process_color_hex_alpha(&mut self, n: &mut ComponentValue) { if let ComponentValue::Color(box Color::AbsoluteColorBase(AbsoluteColorBase::HexColor( diff --git a/crates/swc_css_compat/src/compiler/color_hwb.rs b/crates/swc_css_compat/src/compiler/color_hwb.rs new file mode 100644 index 000000000000..645e58e00421 --- /dev/null +++ b/crates/swc_css_compat/src/compiler/color_hwb.rs @@ -0,0 +1,201 @@ +use swc_atoms::js_word; +use swc_css_ast::{ + AbsoluteColorBase, AlphaValue, Angle, ComponentValue, Delimiter, DelimiterValue, Hue, Ident, + Number, Percentage, +}; +use swc_css_utils::{angle_to_deg, hwb_to_rgb, to_rgb255}; + +use crate::compiler::Compiler; + +impl Compiler { + fn get_hue(&self, hue: Option<&ComponentValue>) -> Option { + match hue { + Some(ComponentValue::Hue(box hue)) => { + let mut value = match hue { + Hue::Number(Number { value, .. }) => *value, + Hue::Angle(Angle { + value: Number { value, .. }, + unit: Ident { value: unit, .. }, + .. + }) => angle_to_deg(*value, unit), + }; + + value %= 360.0; + + if value < 0.0 { + value += 360.0; + } + + Some(value) + } + Some(ComponentValue::Ident(box Ident { value, .. })) + if value.eq_ignore_ascii_case(&js_word!("none")) => + { + Some(0.0) + } + _ => None, + } + } + + fn get_percentage(&self, percentage: Option<&ComponentValue>) -> Option { + match percentage { + Some(ComponentValue::Percentage(box Percentage { + value: Number { value, .. }, + .. + })) => { + if *value > 100.0 { + return Some(1.0); + } else if *value < 0.0 { + return Some(0.0); + } + + Some(*value / 100.0) + } + Some(ComponentValue::Ident(box Ident { value, .. })) + if value.eq_ignore_ascii_case(&js_word!("none")) => + { + Some(0.0) + } + _ => None, + } + } + + fn get_alpha_value(&self, alpha_value: Option<&ComponentValue>) -> Option { + match alpha_value { + Some(ComponentValue::AlphaValue(box AlphaValue::Number(Number { value, .. }))) => { + if *value > 1.0 { + return Some(1.0); + } else if *value < 0.0 { + return Some(0.0); + } + + Some(*value) + } + Some(ComponentValue::AlphaValue(box AlphaValue::Percentage(Percentage { + value: Number { value, .. }, + .. + }))) => { + if *value > 100.0 { + return Some(1.0); + } else if *value < 0.0 { + return Some(0.0); + } + + Some(*value / 100.0) + } + Some(ComponentValue::Ident(box Ident { value, .. })) + if value.eq_ignore_ascii_case(&js_word!("none")) => + { + Some(0.0) + } + None => Some(1.0), + _ => None, + } + } + + pub(crate) fn process_color_hwb(&mut self, n: &mut AbsoluteColorBase) { + if let AbsoluteColorBase::Function(function) = n { + if function.name.value != js_word!("hwb") { + return; + } + + let h = match self.get_hue(function.value.get(0)) { + Some(value) => value, + _ => return, + }; + let w = match self.get_percentage(function.value.get(1)) { + Some(value) => value, + _ => return, + }; + let b = match self.get_percentage(function.value.get(2)) { + Some(value) => value, + _ => return, + }; + let a = match self.get_alpha_value(function.value.get(4)) { + Some(value) => value, + _ => return, + }; + + let rgb = to_rgb255(hwb_to_rgb([h, w, b])); + + if a == 1.0 { + *n = AbsoluteColorBase::Function(swc_css_ast::Function { + name: Ident { + value: js_word!("rgb"), + span: Default::default(), + raw: None, + }, + value: vec![ + ComponentValue::Number(Box::new(Number { + value: rgb[0].round(), + span: Default::default(), + raw: None, + })), + ComponentValue::Delimiter(box Delimiter { + value: DelimiterValue::Comma, + span: Default::default(), + }), + ComponentValue::Number(Box::new(Number { + value: rgb[1].round(), + span: Default::default(), + raw: None, + })), + ComponentValue::Delimiter(box Delimiter { + value: DelimiterValue::Comma, + span: Default::default(), + }), + ComponentValue::Number(Box::new(Number { + value: rgb[2].round(), + span: Default::default(), + raw: None, + })), + ], + span: Default::default(), + }); + } else { + *n = AbsoluteColorBase::Function(swc_css_ast::Function { + name: Ident { + value: js_word!("rgba"), + span: Default::default(), + raw: None, + }, + value: vec![ + ComponentValue::Number(Box::new(Number { + value: rgb[0].round(), + span: Default::default(), + raw: None, + })), + ComponentValue::Delimiter(box Delimiter { + value: DelimiterValue::Comma, + span: Default::default(), + }), + ComponentValue::Number(Box::new(Number { + value: rgb[1].round(), + span: Default::default(), + raw: None, + })), + ComponentValue::Delimiter(box Delimiter { + value: DelimiterValue::Comma, + span: Default::default(), + }), + ComponentValue::Number(Box::new(Number { + value: rgb[2].round(), + span: Default::default(), + raw: None, + })), + ComponentValue::Delimiter(box Delimiter { + value: DelimiterValue::Comma, + span: Default::default(), + }), + ComponentValue::AlphaValue(box AlphaValue::Number(Number { + value: a, + span: Default::default(), + raw: None, + })), + ], + span: Default::default(), + }); + } + } + } +} diff --git a/crates/swc_css_compat/src/compiler/legacy_rgb_and_hsl.rs b/crates/swc_css_compat/src/compiler/legacy_rgb_and_hsl.rs index 19771d84b943..a3eeffaf71d9 100644 --- a/crates/swc_css_compat/src/compiler/legacy_rgb_and_hsl.rs +++ b/crates/swc_css_compat/src/compiler/legacy_rgb_and_hsl.rs @@ -2,11 +2,9 @@ use std::f64::consts::PI; use swc_atoms::js_word; use swc_css_ast::{AbsoluteColorBase, AlphaValue, Angle, ComponentValue, Hue, Number, Percentage}; +use swc_css_utils::{clamp_unit_f64, round_alpha}; -use crate::compiler::{ - utils::{clamp_unit_f32, round_alpha}, - Compiler, -}; +use crate::compiler::Compiler; impl Compiler { pub(crate) fn process_rgb_and_hsl(&mut self, n: &mut AbsoluteColorBase) { @@ -28,7 +26,7 @@ impl Compiler { .. }) => ComponentValue::Number(Box::new(Number { span, - value: clamp_unit_f32(value / 100.0) as f64, + value: clamp_unit_f64(value / 100.0) as f64, raw: None, })), _ => n, diff --git a/crates/swc_css_compat/src/compiler/mod.rs b/crates/swc_css_compat/src/compiler/mod.rs index edd95695e8fe..bfa24584ca3b 100644 --- a/crates/swc_css_compat/src/compiler/mod.rs +++ b/crates/swc_css_compat/src/compiler/mod.rs @@ -11,6 +11,7 @@ use crate::feature::Features; mod color_alpha_parameter; mod color_hex_alpha; +mod color_hwb; mod color_space_separated_parameters; mod custom_media; mod legacy_rgb_and_hsl; @@ -172,5 +173,9 @@ impl VisitMut for Compiler { if process.contains(Features::COLOR_LEGACY_RGB_AND_HSL) { self.process_rgb_and_hsl(n); } + + if process.contains(Features::COLOR_HWB) { + self.process_color_hwb(n); + } } } diff --git a/crates/swc_css_compat/src/compiler/utils.rs b/crates/swc_css_compat/src/compiler/utils.rs index 355163f9bb5c..8b137891791f 100644 --- a/crates/swc_css_compat/src/compiler/utils.rs +++ b/crates/swc_css_compat/src/compiler/utils.rs @@ -1,15 +1 @@ -#[inline] -pub(crate) fn clamp_unit_f32(val: f64) -> u8 { - (val * 255.).round().max(0.).min(255.) as u8 -} -#[inline] -pub(crate) fn round_alpha(alpha: f64) -> f64 { - let mut rounded_alpha = (alpha * 100.).round() / 100.; - - if clamp_unit_f32(rounded_alpha) != clamp_unit_f32(alpha) { - rounded_alpha = (alpha * 1000.).round() / 1000.; - } - - rounded_alpha -} diff --git a/crates/swc_css_compat/src/feature.rs b/crates/swc_css_compat/src/feature.rs index 84d0d55865d5..0ac164c8a6f8 100644 --- a/crates/swc_css_compat/src/feature.rs +++ b/crates/swc_css_compat/src/feature.rs @@ -9,6 +9,7 @@ bitflags! { const COLOR_ALPHA_PARAMETER = 1 << 4; const COLOR_SPACE_SEPARATED_PARAMETERS = 1 << 5; const COLOR_LEGACY_RGB_AND_HSL = 1 << 6; - const SELECTOR_NOT = 1 << 7; + const COLOR_HWB = 1 << 7; + const SELECTOR_NOT = 1 << 8; } } diff --git a/crates/swc_css_compat/tests/color-hwb/input.css b/crates/swc_css_compat/tests/color-hwb/input.css new file mode 100644 index 000000000000..24cc64d975da --- /dev/null +++ b/crates/swc_css_compat/tests/color-hwb/input.css @@ -0,0 +1,14 @@ +.test-hwb { + color: hwb(194 0% 0%); /* #00c3ff */ + color: hwb(194 0% 0% / .5); /* #00c3ff with 50% opacity */ + color: hwb(0 10% 90%); + color: hwb(0 20% 80%); + color: hwb(0 30% 70%); + color: hwb(0 40% 60%); + color: hwb(0 50% 50%); + color: hwb(0 60% 40%); +} + +.test-ignore { + color: hwb(194, 0%, 0%, .5); /* with comma-separated values */ +} diff --git a/crates/swc_css_compat/tests/color-hwb/input.expect.css b/crates/swc_css_compat/tests/color-hwb/input.expect.css new file mode 100644 index 000000000000..ffb5daf3ce0e --- /dev/null +++ b/crates/swc_css_compat/tests/color-hwb/input.expect.css @@ -0,0 +1,13 @@ +.test-hwb { + color: rgb(0, 195, 255); + color: rgba(0, 195, 255, 0.5); + color: rgb(26, 26, 26); + color: rgb(51, 51, 51); + color: rgb(77, 77, 77); + color: rgb(102, 102, 102); + color: rgb(128, 128, 128); + color: rgb(153, 153, 153); +} +.test-ignore { + color: hwb(194, 0%, 0%, .5); +} diff --git a/crates/swc_css_compat/tests/fixture.rs b/crates/swc_css_compat/tests/fixture.rs index f9f56845c4ca..80d5335df196 100644 --- a/crates/swc_css_compat/tests/fixture.rs +++ b/crates/swc_css_compat/tests/fixture.rs @@ -187,3 +187,24 @@ fn test_selector_not(input: PathBuf) { }) .unwrap(); } + +#[testing::fixture("tests/color-hwb/**/*.css", exclude("expect.css"))] +fn test_color_hwb(input: PathBuf) { + let output = input.with_extension("expect.css"); + + testing::run_test(false, |cm, _| { + let fm = cm.load_file(&input).unwrap(); + let mut ss = parse_stylesheet(&fm); + + ss.visit_mut_with(&mut Compiler::new(Config { + process: Features::COLOR_HWB, + })); + + let s = print_stylesheet(&ss); + + NormalizedOutput::from(s).compare_to_file(&output).unwrap(); + + Ok(()) + }) + .unwrap(); +} diff --git a/crates/swc_css_minifier/src/compressor/angle.rs b/crates/swc_css_minifier/src/compressor/angle.rs index ec282d9cd748..db32240a6529 100644 --- a/crates/swc_css_minifier/src/compressor/angle.rs +++ b/crates/swc_css_minifier/src/compressor/angle.rs @@ -1,7 +1,6 @@ -use core::f64::consts::PI; - -use swc_atoms::{js_word, JsWord}; +use swc_atoms::js_word; use swc_css_ast::*; +use swc_css_utils::angle_to_deg; use super::Compressor; @@ -37,12 +36,7 @@ impl Compressor { return; } - let from = match get_angle_type(&angle.unit.value) { - Some(angel_type) => angel_type, - None => return, - }; - - let deg = to_deg(angle.value.value, from); + let deg = angle_to_deg(angle.value.value, &angle.unit.value); if deg.fract() != 0.0 { return; @@ -64,32 +58,6 @@ impl Compressor { } } -pub(crate) enum AngleType { - Deg, - Grad, - Rad, - Turn, -} - -pub(crate) fn to_deg(value: f64, from: AngleType) -> f64 { - match from { - AngleType::Deg => value, - AngleType::Grad => value * 180.0 / 200.0, - AngleType::Turn => value * 360.0, - AngleType::Rad => value * 180.0 / PI, - } -} - -pub(crate) fn get_angle_type(unit: &JsWord) -> Option { - match *unit { - js_word!("deg") => Some(AngleType::Deg), - js_word!("grad") => Some(AngleType::Grad), - js_word!("rad") => Some(AngleType::Rad), - js_word!("turn") => Some(AngleType::Turn), - _ => None, - } -} - fn normalize_deg(mut value: f64) -> f64 { value = (value % 360.0 + 360.0) % 360.0; diff --git a/crates/swc_css_minifier/src/compressor/color.rs b/crates/swc_css_minifier/src/compressor/color.rs index 6d1edbbfe1bf..5da0a2b55fad 100644 --- a/crates/swc_css_minifier/src/compressor/color.rs +++ b/crates/swc_css_minifier/src/compressor/color.rs @@ -1,12 +1,9 @@ use swc_atoms::{js_word, JsWord}; use swc_common::DUMMY_SP; use swc_css_ast::*; -use swc_css_utils::NAMED_COLORS; +use swc_css_utils::{angle_to_deg, hsl_to_rgb, hwb_to_rgb, to_rgb255, NAMED_COLORS}; -use super::{ - angle::{get_angle_type, to_deg}, - Compressor, -}; +use super::Compressor; use crate::compressor::alpha_value::compress_alpha_value; fn compress_alpha_in_hex(value: &JsWord) -> Option<&str> { @@ -82,62 +79,6 @@ fn get_named_color_by_hex(v: u32) -> Option<&'static str> { Some(s) } -fn hsl_to_rgb(hsl: [f64; 3]) -> [f64; 3] { - let [h, s, l] = hsl; - - let r; - let g; - let b; - - if s == 0.0 { - r = l; - g = l; - b = l; - } else { - let f = |n: f64| -> f64 { - let k = (n + h / 30.0) % 12.0; - let a = s * f64::min(l, 1.0 - l); - - l - a * f64::max(-1.0, f64::min(f64::min(k - 3.0, 9.0 - k), 1.0)) - }; - - r = f(0.0); - g = f(8.0); - b = f(4.0); - } - - [r, g, b] -} - -fn hwb_to_rgb(hwb: [f64; 3]) -> [f64; 3] { - let [h, w, b] = hwb; - - if w + b >= 1.0 { - let gray = w / (w + b); - - return [gray, gray, gray]; - } - - let mut rgb = hsl_to_rgb([h, 1.0, 0.5]); - - for item in &mut rgb { - *item *= 1.0 - w - b; - *item += w; - } - - [rgb[0], rgb[1], rgb[2]] -} - -fn to_rgb255(abc: [f64; 3]) -> [f64; 3] { - let mut abc255 = abc; - - for item in &mut abc255 { - *item *= 255.0; - } - - abc255 -} - macro_rules! make_color { ($span:expr,$r:expr,$g:expr,$b:expr, $a:expr) => {{ let need_alpha_value = $a != 1.0; @@ -324,14 +265,7 @@ impl Compressor { value: Number { value, .. }, unit: Ident { value: unit, .. }, .. - }) => { - let angel_type = match get_angle_type(unit) { - Some(angel_type) => angel_type, - _ => return None, - }; - - to_deg(*value, angel_type) - } + }) => angle_to_deg(*value, unit), }; value %= 360.0; @@ -532,32 +466,19 @@ impl Compressor { value, .. })) if matches!(&*name.value, "hwb") => { - let hsla: Vec<_> = value - .iter() - .filter(|n| { - !matches!( - n, - ComponentValue::Delimiter(box Delimiter { - value: DelimiterValue::Comma | DelimiterValue::Solidus, - .. - }) - ) - }) - .collect(); - - let h = match self.get_hue(hsla.get(0)) { + let h = match self.get_hue(value.get(0).as_ref()) { Some(value) => value, _ => return, }; - let w = match self.get_percentage(hsla.get(1)) { + let w = match self.get_percentage(value.get(1).as_ref()) { Some(value) => value, _ => return, }; - let b = match self.get_percentage(hsla.get(2)) { + let b = match self.get_percentage(value.get(2).as_ref()) { Some(value) => value, _ => return, }; - let a = match self.get_alpha_value(hsla.get(3)) { + let a = match self.get_alpha_value(value.get(4).as_ref()) { Some(value) => value, _ => return, }; diff --git a/crates/swc_css_utils/src/lib.rs b/crates/swc_css_utils/src/lib.rs index aa1636bf1d1d..d20b60848efb 100644 --- a/crates/swc_css_utils/src/lib.rs +++ b/crates/swc_css_utils/src/lib.rs @@ -1,10 +1,10 @@ #![deny(clippy::all)] -use std::{borrow::Cow, char::REPLACEMENT_CHARACTER, str}; +use std::{borrow::Cow, char::REPLACEMENT_CHARACTER, f64::consts::PI, str}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use swc_atoms::JsWord; +use swc_atoms::{js_word, JsWord}; use swc_common::collections::AHashMap; use swc_css_ast::*; use swc_css_visit::{VisitMut, VisitMutWith}; @@ -295,3 +295,124 @@ fn hex_escape(ascii_byte: u8, minify: bool) -> String { .to_string() } } + +pub fn hwb_to_rgb(hwb: [f64; 3]) -> [f64; 3] { + let [h, w, b] = hwb; + + if w + b >= 1.0 { + let gray = w / (w + b); + + return [gray, gray, gray]; + } + + let mut rgb = hsl_to_rgb([h, 1.0, 0.5]); + + for item in &mut rgb { + *item *= 1.0 - w - b; + *item += w; + } + + [rgb[0], rgb[1], rgb[2]] +} + +pub fn hsl_to_rgb(hsl: [f64; 3]) -> [f64; 3] { + let [h, s, l] = hsl; + + let r; + let g; + let b; + + if s == 0.0 { + r = l; + g = l; + b = l; + } else { + let f = |n: f64| -> f64 { + let k = (n + h / 30.0) % 12.0; + let a = s * f64::min(l, 1.0 - l); + + l - a * f64::max(-1.0, f64::min(f64::min(k - 3.0, 9.0 - k), 1.0)) + }; + + r = f(0.0); + g = f(8.0); + b = f(4.0); + } + + [r, g, b] +} + +pub fn to_rgb255(abc: [f64; 3]) -> [f64; 3] { + let mut abc255 = abc; + + for item in &mut abc255 { + *item *= 255.0; + } + + abc255 +} + +pub fn clamp_unit_f64(val: f64) -> u8 { + (val * 255.).round().max(0.).min(255.) as u8 +} + +pub fn round_alpha(alpha: f64) -> f64 { + let mut rounded_alpha = (alpha * 100.).round() / 100.; + + if clamp_unit_f64(rounded_alpha) != clamp_unit_f64(alpha) { + rounded_alpha = (alpha * 1000.).round() / 1000.; + } + + rounded_alpha +} + +#[inline] +fn from_hex(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + b'A'..=b'F' => c - b'A' + 10, + _ => { + unreachable!(); + } + } +} + +pub fn hex_to_rgba(hex: &str) -> (u8, u8, u8, f64) { + let hex = hex.as_bytes(); + + match hex.len() { + 8 => { + let r = from_hex(hex[0]) * 16 + from_hex(hex[1]); + let g = from_hex(hex[2]) * 16 + from_hex(hex[3]); + let b = from_hex(hex[4]) * 16 + from_hex(hex[5]); + let a = (from_hex(hex[6]) * 16 + from_hex(hex[7])) as f64 / 255.0; + + (r, g, b, a) + } + 4 => { + let r = from_hex(hex[0]) * 17; + let g = from_hex(hex[1]) * 17; + let b = from_hex(hex[2]) * 17; + let a = (from_hex(hex[3]) * 17) as f64 / 255.0; + + (r, g, b, a) + } + + _ => { + unreachable!() + } + } +} + +pub fn angle_to_deg(value: f64, from: &JsWord) -> f64 { + match *from { + js_word!("deg") => value, + js_word!("grad") => value * 180.0 / 200.0, + js_word!("turn") => value * 360.0, + js_word!("rad") => value * 180.0 / PI, + _ => { + unreachable!("Unknown angle type: {:?}", from); + } + } +}