Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ An extremely fast CSS parser, transformer, and minifier written in Rust. Use it
- Logical properties
* [Color Level 5](https://drafts.csswg.org/css-color-5/)
- `color-mix()` function
- `light-dark()` function
- `contrast-color()` function
- Relative color syntax, e.g. `lab(from purple calc(l * .8) a b)`
- [Color Level 4](https://drafts.csswg.org/css-color-4/)
- `lab()`, `lch()`, `oklab()`, and `oklch()` colors
Expand Down
3 changes: 3 additions & 0 deletions src/bundler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,9 @@ fn visit_vars<'a, 'b>(
stack.push(light.0.iter_mut());
stack.push(dark.0.iter_mut());
}
UnresolvedColor::ContrastColor { value } => {
stack.push(value.0.iter_mut());
}
},
None => {
stack.pop();
Expand Down
90 changes: 90 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30757,6 +30757,96 @@ mod tests {
);
}

#[test]
fn test_contrast_color() {
// WPT reference:
// https://github.com/web-platform-tests/wpt/blob/e4a69111d7a6e24b80b71e6c6ef4536f84754f22/css/css-color/parsing/color-valid-contrast-color-function.html
minify_test(".foo { color: contrast-color(green); }", ".foo{color:contrast-color(green)}");
minify_test(
".foo { color: contrast-color(hwb(120deg 0 49.8%)); }",
".foo{color:contrast-color(green)}",
);
minify_test(
".foo { color: contrast-color(rgb(70 130 180)); }",
".foo{color:contrast-color(#4682b4)}",
);
minify_test(
".foo { color: contrast-color(hsl(0deg 100% 0%)); }",
".foo{color:contrast-color(#000)}",
);
// relative colors
minify_test(
".foo { color: contrast-color(rgb(from red r g b)); }",
".foo{color:contrast-color(red)}",
);
minify_test(
".foo { color: rgb(from contrast-color(black) r g b); }",
".foo{color:rgb(from contrast-color(#000) r g b)}",
);
// color-mix()
minify_test(
".foo { color: color-mix(in srgb, contrast-color(blue) 100%, purple); }",
".foo{color:color-mix(in srgb, contrast-color(#00f) 100%, purple)}",
);
// light-dark()
minify_test(
".foo { color: contrast-color(light-dark(red, white)); }",
".foo{color:contrast-color(light-dark(red,#fff))}",
);
minify_test(
".foo { color: light-dark(contrast-color(red), white); }",
".foo{color:light-dark(contrast-color(red),#fff)}",
);
minify_test(
".foo { color: contrast-color(currentcolor); }",
".foo{color:contrast-color(currentColor)}",
);
minify_test(
".foo { color: contrast-color(transparent); }",
".foo{color:contrast-color(#0000)}",
);

// Nested contrast-color() should be flattened.
minify_test(
".foo { color: contrast-color(contrast-color(blue)); }",
".foo{color:contrast-color(#00f)}",
);
minify_test(
".foo { color: contrast-color(contrast-color(contrast-color(blue))); }",
".foo{color:contrast-color(#00f)}",
);

// var() and unresolved token list paths should be preserved and flattened safely.
minify_test(
".foo { color: contrast-color(var(--bg)); }",
".foo{color:contrast-color(var(--bg))}",
);
minify_test(
".foo { color: contrast-color(contrast-color(var(--bg))); }",
".foo{color:contrast-color(var(--bg))}",
);
minify_test(
".foo { --x: contrast-color(contrast-color(var(--bg))); }",
".foo{--x:contrast-color(var(--bg))}",
);

// Should not panic for unsupported interpolation inputs.
minify_test(
".foo { color: color-mix(in srgb, contrast-color(blue), red); }",
".foo{color:color-mix(in srgb, contrast-color(#00f), red)}",
);

minify_test(
".foo { background: linear-gradient( contrast-color(black) ); }",
".foo{background:linear-gradient(contrast-color(#000))}",
);
minify_test(
".foo { background: contrast-color(color(srgb calc(0.5) calc(-1 + 1 / 1) 1 / .99)); }",
".foo{background:contrast-color(color(srgb .5 0 1/.99))}",
);

}

#[test]
fn test_print_color_adjust() {
prefix_test(
Expand Down
30 changes: 29 additions & 1 deletion src/properties/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ fn try_parse_color_token<'i, 't>(
input: &mut Parser<'i, 't>,
) -> Option<CssColor> {
match_ignore_ascii_case! { &*f,
"rgb" | "rgba" | "hsl" | "hsla" | "hwb" | "lab" | "lch" | "oklab" | "oklch" | "color" | "color-mix" | "light-dark" => {
"rgb" | "rgba" | "hsl" | "hsla" | "hwb" | "lab" | "lch" | "oklab" | "oklch" | "color" | "color-mix" | "light-dark" | "contrast-color" => {
let s = input.state();
input.reset(&state);
if let Ok(color) = CssColor::parse(input) {
Expand Down Expand Up @@ -1103,6 +1103,9 @@ impl<'i> TokenList<'i> {
features |= light.get_features();
features |= dark.get_features();
}
UnresolvedColor::ContrastColor { value } => {
features |= value.get_features();
}
_ => {}
}
}
Expand Down Expand Up @@ -1490,6 +1493,13 @@ pub enum UnresolvedColor<'i> {
/// The dark value.
dark: TokenList<'i>,
},
/// The contrast-color() function.
/// https://drafts.csswg.org/css-color-5/#contrast-color
#[cfg_attr(feature = "serde", serde(rename = "contrast-color"))]
ContrastColor {
/// contrast-color( <color> )
value: TokenList<'i>,
},
}

impl<'i> LightDarkColor for UnresolvedColor<'i> {
Expand Down Expand Up @@ -1546,6 +1556,19 @@ impl<'i> UnresolvedColor<'i> {
Ok(UnresolvedColor::LightDark { light, dark })
})
},
"contrast-color" => {
input.parse_nested_block(|input| {
let value = TokenList::parse(input, options, 0)?;
let value = match value.0.as_slice() {
[TokenOrValue::Color(CssColor::ContrastColor(inner))] => {
TokenList(vec![TokenOrValue::Color((**inner).clone())])
}
[TokenOrValue::UnresolvedColor(UnresolvedColor::ContrastColor { value })] => value.clone(),
_ => value,
};
Ok(UnresolvedColor::ContrastColor { value })
})
},
_ => Err(input.new_custom_error(ParserError::InvalidValue))
}
}
Expand Down Expand Up @@ -1622,6 +1645,11 @@ impl<'i> UnresolvedColor<'i> {
dark.to_css(dest, is_custom_property)?;
dest.write_char(')')
}
UnresolvedColor::ContrastColor { value } => {
dest.write_str("contrast-color(")?;
value.to_css(dest, is_custom_property)?;
dest.write_char(')')
}
}
}
}
66 changes: 64 additions & 2 deletions src/values/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ pub enum CssColor {
#[cfg_attr(feature = "visitor", skip_type)]
#[cfg_attr(feature = "serde", serde(with = "LightDark"))]
LightDark(Box<CssColor>, Box<CssColor>),
/// The [`contrast-color()`](https://drafts.csswg.org/css-color-5/#contrast-color) function.
#[cfg_attr(feature = "visitor", skip_type)]
#[cfg_attr(feature = "serde", serde(with = "ContrastColor"))]
ContrastColor(Box<CssColor>),
/// A [system color](https://drafts.csswg.org/css-color/#css-system-colors) keyword.
System(SystemColor),
}
Expand Down Expand Up @@ -157,6 +161,38 @@ impl<'de> LightDark {
}
}

// For AST serialization.
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
enum ContrastColor {
ContrastColor { value: CssColor },
}

#[cfg(feature = "serde")]
impl<'de> ContrastColor {
pub fn serialize<S>(value: &Box<CssColor>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let wrapper = ContrastColor::ContrastColor {
value: (**value).clone(),
};
serde::Serialize::serialize(&wrapper, serializer)
}

pub fn deserialize<D>(deserializer: D) -> Result<Box<CssColor>, D::Error>
where
D: serde::Deserializer<'de>,
{
let v: ContrastColor = serde::Deserialize::deserialize(deserializer)?;
match v {
ContrastColor::ContrastColor { value } => Ok(Box::new(value)),
}
}
}

/// A color in a LAB color space, including the `lab()`, `lch()`, `oklab()`, and `oklch()` functions.
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
Expand Down Expand Up @@ -334,6 +370,7 @@ impl CssColor {
CssColor::LightDark(light, dark) => {
Ok(CssColor::LightDark(Box::new(light.to_rgb()?), Box::new(dark.to_rgb()?)))
}
CssColor::ContrastColor(value) => Ok(CssColor::ContrastColor(Box::new(value.to_rgb()?))),
_ => Ok(RGBA::try_from(self)?.into()),
}
}
Expand All @@ -344,6 +381,7 @@ impl CssColor {
CssColor::LightDark(light, dark) => {
Ok(CssColor::LightDark(Box::new(light.to_lab()?), Box::new(dark.to_lab()?)))
}
CssColor::ContrastColor(value) => Ok(CssColor::ContrastColor(Box::new(value.to_lab()?))),
_ => Ok(LAB::try_from(self)?.into()),
}
}
Expand All @@ -354,6 +392,7 @@ impl CssColor {
CssColor::LightDark(light, dark) => {
Ok(CssColor::LightDark(Box::new(light.to_p3()?), Box::new(dark.to_p3()?)))
}
CssColor::ContrastColor(value) => Ok(CssColor::ContrastColor(Box::new(value.to_p3()?))),
_ => Ok(P3::try_from(self)?.into()),
}
}
Expand Down Expand Up @@ -383,6 +422,9 @@ impl CssColor {
CssColor::LightDark(light, dark) => {
return light.get_possible_fallbacks(targets) | dark.get_possible_fallbacks(targets);
}
CssColor::ContrastColor(value) => {
return value.get_possible_fallbacks(targets);
}
};

if fallbacks.contains(ColorFallbackKind::OKLAB) {
Expand Down Expand Up @@ -473,6 +515,9 @@ impl CssColor {
features |= light.get_features();
features |= dark.get_features();
}
CssColor::ContrastColor(value) => {
features |= value.get_features();
}
_ => {}
}

Expand All @@ -495,6 +540,7 @@ impl IsCompatible for CssColor {
CssColor::LightDark(light, dark) => {
Feature::LightDark.is_compatible(browsers) && light.is_compatible(browsers) && dark.is_compatible(browsers)
}
CssColor::ContrastColor(value) => value.is_compatible(browsers),
CssColor::System(system) => system.is_compatible(browsers),
}
}
Expand Down Expand Up @@ -647,6 +693,11 @@ impl ToCss for CssColor {
dark.to_css(dest)?;
dest.write_char(')')
}
CssColor::ContrastColor(value) => {
dest.write_str("contrast-color(")?;
value.to_css(dest)?;
dest.write_char(')')
}
CssColor::System(system) => system.to_css(dest),
}
}
Expand Down Expand Up @@ -1091,6 +1142,15 @@ fn parse_color_function<'i, 't>(
Ok(CssColor::LightDark(light, dark))
})
},
"contrast-color" => {
input.parse_nested_block(|input| {
let value = match CssColor::parse(input)? {
CssColor::ContrastColor(value) => value,
value => Box::new(value),
};
Ok(CssColor::ContrastColor(value))
})
},
_ => Err(location.new_unexpected_token_error(
cssparser::Token::Ident(function.clone())
))
Expand Down Expand Up @@ -2971,6 +3031,7 @@ macro_rules! color_space {
CssColor::Float(float) => (**float).into(),
CssColor::CurrentColor => return Err(()),
CssColor::LightDark(..) => return Err(()),
CssColor::ContrastColor(..) => return Err(()),
CssColor::System(..) => return Err(()),
})
}
Expand All @@ -2986,6 +3047,7 @@ macro_rules! color_space {
CssColor::Float(float) => (*float).into(),
CssColor::CurrentColor => return Err(()),
CssColor::LightDark(..) => return Err(()),
CssColor::ContrastColor(..) => return Err(()),
CssColor::System(..) => return Err(()),
})
}
Expand Down Expand Up @@ -3363,8 +3425,8 @@ impl CssColor {
+ From<OKLCH>
+ Copy,
{
if matches!(self, CssColor::CurrentColor | CssColor::System(..))
|| matches!(other, CssColor::CurrentColor | CssColor::System(..))
if matches!(self, CssColor::CurrentColor | CssColor::System(..) | CssColor::ContrastColor(..))
|| matches!(other, CssColor::CurrentColor | CssColor::System(..) | CssColor::ContrastColor(..))
{
return Err(());
}
Expand Down
Loading