diff --git a/crates/swc_css_compat/src/compiler/color_hex_alpha.rs b/crates/swc_css_compat/src/compiler/color_hex_alpha.rs new file mode 100644 index 000000000000..de85a51c88cd --- /dev/null +++ b/crates/swc_css_compat/src/compiler/color_hex_alpha.rs @@ -0,0 +1,146 @@ +use swc_atoms::js_word; +use swc_common::DUMMY_SP; +use swc_css_ast::{ + AbsoluteColorBase, AlphaValue, Color, ComponentValue, Delimiter, DelimiterValue, Function, + Ident, Number, +}; + +use crate::compiler::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!(); + } + } +} + +#[inline] +fn clamp_unit_f32(val: f64) -> u8 { + (val * 255.).round().max(0.).min(255.) as u8 +} + +fn shorten_hex_color(value: &str) -> Option<&str> { + let length = value.len(); + let chars = value.as_bytes(); + + if length == 8 + && (chars[6] == b'f' || chars[6] == b'F') + && (chars[7] == b'f' || chars[7] == b'F') + { + return Some(&value[0..6]); + } else if length == 4 && chars[3] == b'f' || chars[3] == b'F' { + return Some(&value[0..3]); + } + + 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( + hex_color, + ))) = n + { + if hex_color.value.len() != 4 && hex_color.value.len() != 8 { + return; + } + + if let Some(shortened) = shorten_hex_color(&hex_color.value) { + hex_color.value = shortened.into(); + hex_color.raw = None; + + return; + } + + let rgba = hex_to_rgba(&hex_color.value); + + let r = rgba.0 as f64; + let g = rgba.1 as f64; + let b = rgba.2 as f64; + let a = rgba.3; + + let mut rounded_alpha = (a * 100.).round() / 100.; + + if clamp_unit_f32(rounded_alpha) != clamp_unit_f32(a) { + rounded_alpha = (a * 1000.).round() / 1000.; + } + + *n = ComponentValue::Color(Box::new(Color::AbsoluteColorBase( + AbsoluteColorBase::Function(Function { + span: hex_color.span, + name: Ident { + span: DUMMY_SP, + value: js_word!("rgba"), + raw: None, + }, + value: vec![ + ComponentValue::Number(Box::new(Number { + span: DUMMY_SP, + value: r, + raw: None, + })), + ComponentValue::Delimiter(Box::new(Delimiter { + span: DUMMY_SP, + value: DelimiterValue::Comma, + })), + ComponentValue::Number(Box::new(Number { + span: DUMMY_SP, + value: g, + raw: None, + })), + ComponentValue::Delimiter(Box::new(Delimiter { + span: DUMMY_SP, + value: DelimiterValue::Comma, + })), + ComponentValue::Number(Box::new(Number { + span: DUMMY_SP, + value: b, + raw: None, + })), + ComponentValue::Delimiter(Box::new(Delimiter { + span: DUMMY_SP, + value: DelimiterValue::Comma, + })), + ComponentValue::AlphaValue(Box::new(AlphaValue::Number(Number { + span: DUMMY_SP, + value: rounded_alpha, + raw: None, + }))), + ], + }), + ))); + } + } +} diff --git a/crates/swc_css_compat/src/compiler/mod.rs b/crates/swc_css_compat/src/compiler/mod.rs index d962df16cfa4..f1b1e352ebfd 100644 --- a/crates/swc_css_compat/src/compiler/mod.rs +++ b/crates/swc_css_compat/src/compiler/mod.rs @@ -1,13 +1,14 @@ use swc_common::{Spanned, DUMMY_SP}; use swc_css_ast::{ - AtRule, MediaAnd, MediaCondition, MediaConditionAllType, MediaConditionWithoutOr, - MediaInParens, MediaQuery, Rule, + AtRule, ComponentValue, MediaAnd, MediaCondition, MediaConditionAllType, + MediaConditionWithoutOr, MediaInParens, MediaQuery, Rule, SupportsCondition, }; use swc_css_visit::{VisitMut, VisitMutWith}; use self::custom_media::CustomMediaHandler; use crate::feature::Features; +mod color_hex_alpha; mod custom_media; mod media_query_ranges; @@ -17,6 +18,7 @@ pub struct Compiler { #[allow(unused)] c: Config, custom_media: CustomMediaHandler, + in_supports_condition: bool, } #[derive(Debug)] @@ -30,6 +32,7 @@ impl Compiler { Self { c: config, custom_media: Default::default(), + in_supports_condition: Default::default(), } } } @@ -43,6 +46,16 @@ impl VisitMut for Compiler { } } + fn visit_mut_supports_condition(&mut self, n: &mut SupportsCondition) { + let old_in_support_condition = self.in_supports_condition; + + self.in_supports_condition = true; + + n.visit_mut_children_with(self); + + self.in_supports_condition = old_in_support_condition; + } + fn visit_mut_media_query(&mut self, n: &mut MediaQuery) { n.visit_mut_children_with(self); @@ -105,4 +118,16 @@ impl VisitMut for Compiler { } } } + + fn visit_mut_component_value(&mut self, n: &mut ComponentValue) { + n.visit_mut_children_with(self); + + if self.in_supports_condition { + return; + } + + if self.c.process.contains(Features::COLOR_HEX_ALPHA) { + self.process_color_hex_alpha(n); + } + } } diff --git a/crates/swc_css_compat/src/feature.rs b/crates/swc_css_compat/src/feature.rs index 784f2d957ee3..b94df38e91dd 100644 --- a/crates/swc_css_compat/src/feature.rs +++ b/crates/swc_css_compat/src/feature.rs @@ -5,5 +5,6 @@ bitflags! { const NESTING = 1 << 0; const CUSTOM_MEDIA = 1 << 1; const MEDIA_QUERY_RANGES = 1 << 2; + const COLOR_HEX_ALPHA = 1 << 3; } } diff --git a/crates/swc_css_compat/tests/color-hex-alpha/input.css b/crates/swc_css_compat/tests/color-hex-alpha/input.css new file mode 100644 index 000000000000..9daaa7d268b6 --- /dev/null +++ b/crates/swc_css_compat/tests/color-hex-alpha/input.css @@ -0,0 +1,52 @@ +body { + background: #9d9 linear-gradient(#9823f8a9, #9823f834); + color: red; + color: #f00; + color: #f00f; + color: #FC0F; + color: #0000ff; + color: #0000ff00; + color: #FFFFFF; + content: "#f00"; + content: "#0000ff00"; +} + +body { + background-color: #f3f3f3f3; + color: #0003; +} + +#svg-element { + clip-path: url(#SVGID_1_); + clip-path: url(#aaaa); +} + +.other { + background: #ff0099; + background: #ff0099ff; + background: #ff009900; +} + +.short { + color: #ffff; + color: #ffffffff; + color: #abdabcff; + color: #000000FF; + color: #abcf; + color: #ff0f; + color: #000f; + color: #0000; + color: #001e; +} + +@supports (color: #1111) { + .a { + color: #1111; + } +} + +@supports (color: #11111111) { + .a { + color: #11111111; + } +} diff --git a/crates/swc_css_compat/tests/color-hex-alpha/input.expect.css b/crates/swc_css_compat/tests/color-hex-alpha/input.expect.css new file mode 100644 index 000000000000..777680c05dbb --- /dev/null +++ b/crates/swc_css_compat/tests/color-hex-alpha/input.expect.css @@ -0,0 +1,46 @@ +body { + background: #9d9 linear-gradient(rgba(152, 35, 248, 0.663), rgba(152, 35, 248, 0.204)); + color: red; + color: #f00; + color: #f00; + color: #FC0; + color: #0000ff; + color: rgba(0, 0, 255, 0); + color: #FFFFFF; + content: "#f00"; + content: "#0000ff00"; +} +body { + background-color: rgba(243, 243, 243, 0.953); + color: rgba(0, 0, 0, 0.2); +} +#svg-element { + clip-path: url(#SVGID_1_); + clip-path: url(#aaaa); +} +.other { + background: #ff0099; + background: #ff0099; + background: rgba(255, 0, 153, 0); +} +.short { + color: #fff; + color: #ffffff; + color: #abdabc; + color: #000000; + color: #abc; + color: #ff0; + color: #000; + color: rgba(0, 0, 0, 0); + color: rgba(0, 0, 17, 0.933); +} +@supports (color: #1111) { + .a { + color: rgba(17, 17, 17, 0.067); + } +} +@supports (color: #11111111) { + .a { + color: rgba(17, 17, 17, 0.067); + } +} diff --git a/crates/swc_css_compat/tests/fixture.rs b/crates/swc_css_compat/tests/fixture.rs index 7e5c04697d9a..83b60c8e46a0 100644 --- a/crates/swc_css_compat/tests/fixture.rs +++ b/crates/swc_css_compat/tests/fixture.rs @@ -125,3 +125,25 @@ fn test_media_query_ranges(input: PathBuf) { }) .unwrap(); } + +#[testing::fixture("tests/color-hex-alpha/**/*.css", exclude("expect.css"))] +fn test_color_hex_alpha(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_HEX_ALPHA, + })); + + let s = print_stylesheet(&ss); + + NormalizedOutput::from(s).compare_to_file(&output).unwrap(); + + Ok(()) + }) + .unwrap(); +}