mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 16:00:54 +00:00
212 lines
7.4 KiB
Rust
212 lines
7.4 KiB
Rust
//! CSS color parsing helpers shared by HTML and CSS observable surfaces.
|
|
|
|
use cssparser::{Parser, ParserInput};
|
|
use style::{
|
|
color::AbsoluteColor,
|
|
context::QuirksMode,
|
|
custom_properties::AttrTaint,
|
|
parser::{Parse, ParserContext},
|
|
stylesheets::{CssRuleType, Origin, UrlExtraData},
|
|
values::specified::color::{Color, ColorScheme, ColorSchemeFlags, SystemColor},
|
|
};
|
|
use style_traits::ParsingMode;
|
|
|
|
/// Parse a CSS color and serialize its opaque sRGB channels as lowercase
|
|
/// `#rrggbb`.
|
|
///
|
|
/// This matches the HTML-compatible value mode of `<input type=color>`:
|
|
/// alpha is discarded, out-of-gamut channels are clipped during sRGB
|
|
/// serialization, `currentcolor` resolves to black, and system colors use the
|
|
/// headless Servo light palette.
|
|
pub fn parse_css_color_to_opaque_srgb_hex(value: &str) -> Option<String> {
|
|
let [red, green, blue, _alpha] = parse_css_color_to_srgb_bytes(value)?;
|
|
Some(format!("#{red:02x}{green:02x}{blue:02x}"))
|
|
}
|
|
|
|
/// Parse a CSS color and return clipped, unpremultiplied sRGB bytes.
|
|
///
|
|
/// This is the shared lower-level form used by protocol surfaces that must
|
|
/// preserve alpha instead of applying HTML `<input type=color>`'s opaque
|
|
/// serialization.
|
|
pub fn parse_css_color_to_srgb_bytes(value: &str) -> Option<[u8; 4]> {
|
|
let color = parse_specified_color(value)?;
|
|
let absolute = match color {
|
|
Color::CurrentColor => AbsoluteColor::BLACK,
|
|
Color::System(system) => system_color_absolute(system),
|
|
color => color
|
|
.to_computed_color(None)
|
|
.ok()?
|
|
.resolve_to_absolute(&AbsoluteColor::BLACK),
|
|
};
|
|
Some(absolute.to_nscolor().to_le_bytes())
|
|
}
|
|
|
|
/// Resolve a CSS system-color keyword through the headless Servo light
|
|
/// palette.
|
|
pub fn css_system_color_srgb(value: &str) -> Option<(u8, u8, u8)> {
|
|
let Color::System(system) = parse_specified_color(value)? else {
|
|
return None;
|
|
};
|
|
let [red, green, blue, _alpha] = system_color_absolute(system).to_nscolor().to_le_bytes();
|
|
Some((red, green, blue))
|
|
}
|
|
|
|
/// Parse the CSS `color-scheme` grammar and return its known-scheme flags.
|
|
///
|
|
/// A successful value with no flags is the valid `normal` keyword. Custom
|
|
/// schemes are preserved by Stylo's validation but do not set a known flag.
|
|
pub fn parse_css_color_scheme_flags(value: &str) -> Option<ColorSchemeFlags> {
|
|
let url_data = UrlExtraData::from(url::Url::parse("about:blank").ok()?);
|
|
let context = ParserContext::new(
|
|
Origin::Author,
|
|
&url_data,
|
|
Some(CssRuleType::Style),
|
|
ParsingMode::DEFAULT,
|
|
QuirksMode::NoQuirks,
|
|
Default::default(),
|
|
None,
|
|
None,
|
|
AttrTaint::default(),
|
|
);
|
|
let mut input = ParserInput::new(value);
|
|
Parser::new(&mut input)
|
|
.parse_entirely(|input| ColorScheme::parse(&context, input))
|
|
.ok()
|
|
.map(|scheme| scheme.bits)
|
|
}
|
|
|
|
fn parse_specified_color(value: &str) -> Option<Color> {
|
|
let url_data = UrlExtraData::from(url::Url::parse("about:blank").ok()?);
|
|
let context = ParserContext::new(
|
|
Origin::Author,
|
|
&url_data,
|
|
Some(CssRuleType::Style),
|
|
ParsingMode::DEFAULT,
|
|
QuirksMode::NoQuirks,
|
|
Default::default(),
|
|
None,
|
|
None,
|
|
AttrTaint::default(),
|
|
);
|
|
let mut input = ParserInput::new(value);
|
|
Parser::new(&mut input)
|
|
.parse_entirely(|input| Color::parse(&context, input))
|
|
.ok()
|
|
}
|
|
|
|
fn system_color_absolute(system: SystemColor) -> AbsoluteColor {
|
|
let (red, green, blue) = match system {
|
|
SystemColor::Accentcolor | SystemColor::Selecteditem => (0, 102, 204),
|
|
SystemColor::Accentcolortext | SystemColor::Selecteditemtext => (255, 255, 255),
|
|
SystemColor::Activetext => (238, 0, 0),
|
|
SystemColor::Linktext => (0, 0, 238),
|
|
SystemColor::Visitedtext => (85, 26, 139),
|
|
SystemColor::Buttonborder
|
|
| SystemColor::Activeborder
|
|
| SystemColor::Inactiveborder
|
|
| SystemColor::Threeddarkshadow
|
|
| SystemColor::Threedshadow
|
|
| SystemColor::Windowframe => (169, 169, 169),
|
|
SystemColor::Buttonface
|
|
| SystemColor::Buttonhighlight
|
|
| SystemColor::Buttonshadow
|
|
| SystemColor::Threedface
|
|
| SystemColor::Threedhighlight
|
|
| SystemColor::Threedlightshadow => (220, 220, 220),
|
|
SystemColor::Buttontext
|
|
| SystemColor::Canvastext
|
|
| SystemColor::Captiontext
|
|
| SystemColor::Fieldtext
|
|
| SystemColor::Highlighttext
|
|
| SystemColor::Infotext
|
|
| SystemColor::Marktext
|
|
| SystemColor::Menutext
|
|
| SystemColor::Windowtext => (0, 0, 0),
|
|
SystemColor::Canvas
|
|
| SystemColor::Activecaption
|
|
| SystemColor::Appworkspace
|
|
| SystemColor::Background
|
|
| SystemColor::Field
|
|
| SystemColor::Inactivecaption
|
|
| SystemColor::Infobackground
|
|
| SystemColor::Menu
|
|
| SystemColor::Scrollbar
|
|
| SystemColor::Window => (255, 255, 255),
|
|
SystemColor::Graytext | SystemColor::Inactivecaptiontext => (109, 109, 109),
|
|
SystemColor::Highlight => (0, 65, 198),
|
|
SystemColor::Mark => (255, 235, 59),
|
|
};
|
|
AbsoluteColor::srgb_legacy(red, green, blue, 1.0)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn css_colors_convert_to_opaque_srgb_hex() {
|
|
for (value, expected) in [
|
|
("#fff", "#ffffff"),
|
|
("crimson", "#dc143c"),
|
|
("rgb(1,1,1,0.2)", "#010101"),
|
|
("hsl(150deg 100 53.5)", "#12ff88"),
|
|
("color(display-p3 .5 0 0)", "#8c0000"),
|
|
("color(display-p3 1 0 0)", "#ff0000"),
|
|
("transparent", "#000000"),
|
|
("currentColor", "#000000"),
|
|
("ActiveBorder", "#a9a9a9"),
|
|
] {
|
|
assert_eq!(
|
|
parse_css_color_to_opaque_srgb_hex(value).as_deref(),
|
|
Some(expected),
|
|
"{value}"
|
|
);
|
|
}
|
|
assert_eq!(parse_css_color_to_opaque_srgb_hex("inherit"), None);
|
|
assert_eq!(parse_css_color_to_opaque_srgb_hex("not-a-color"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn css_colors_preserve_alpha_in_srgb_bytes() {
|
|
assert_eq!(
|
|
parse_css_color_to_srgb_bytes("#11223380"),
|
|
Some([0x11, 0x22, 0x33, 0x80])
|
|
);
|
|
assert_eq!(
|
|
parse_css_color_to_srgb_bytes("rgba(255, 0, 0, 0.25)"),
|
|
Some([255, 0, 0, 64])
|
|
);
|
|
assert_eq!(parse_css_color_to_srgb_bytes("not-a-color"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn system_color_lookup_rejects_non_system_colors() {
|
|
assert_eq!(css_system_color_srgb("MenuText"), Some((0, 0, 0)));
|
|
assert_eq!(css_system_color_srgb("ActiveBorder"), Some((169, 169, 169)));
|
|
assert_eq!(css_system_color_srgb("red"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn color_scheme_flags_use_stylo_grammar() {
|
|
assert_eq!(
|
|
parse_css_color_scheme_flags("normal"),
|
|
Some(ColorSchemeFlags::empty())
|
|
);
|
|
assert_eq!(
|
|
parse_css_color_scheme_flags("light dark"),
|
|
Some(ColorSchemeFlags::LIGHT | ColorSchemeFlags::DARK)
|
|
);
|
|
assert_eq!(
|
|
parse_css_color_scheme_flags("BLUE light"),
|
|
Some(ColorSchemeFlags::LIGHT)
|
|
);
|
|
assert_eq!(
|
|
parse_css_color_scheme_flags("only light"),
|
|
Some(ColorSchemeFlags::ONLY | ColorSchemeFlags::LIGHT)
|
|
);
|
|
for invalid in ["", "light,dark", "light, dark", "only"] {
|
|
assert_eq!(parse_css_color_scheme_flags(invalid), None, "{invalid}");
|
|
}
|
|
}
|
|
}
|