Skip to main content

css_ast/functions/
color_function.rs

1use super::prelude::*;
2use crate::functions::color_mix_function::ColorMixFunction;
3use crate::functions::light_dark_function::LightDarkFunction;
4use crate::functions::relative_color::RelativeColorFunction;
5use crate::{AngleOrNumber, NoneOr, NumberOrPercentage, NumericValue};
6use css_parse::Box;
7
8#[node]
9#[derive(
10	Parse, Peek, IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
11)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
13#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
14#[derive(csskit_derives::NodeWithMetadata)]
15pub enum ColorSpace {
16	#[atom(CssAtomSet::Srgb)]
17	Srgb(T![Ident]),
18	#[atom(CssAtomSet::SrgbLinear)]
19	SrgbLinear(T![Ident]),
20	#[atom(CssAtomSet::DisplayP3)]
21	DisplayP3(T![Ident]),
22	#[atom(CssAtomSet::A98Rgb)]
23	A98Rgb(T![Ident]),
24	#[atom(CssAtomSet::ProphotoRgb)]
25	ProphotoRgb(T![Ident]),
26	#[atom(CssAtomSet::Rec2020)]
27	Rec2020(T![Ident]),
28	#[atom(CssAtomSet::Xyz)]
29	Xyz(T![Ident]),
30	#[atom(CssAtomSet::XyzD50)]
31	XyzD50(T![Ident]),
32	#[atom(CssAtomSet::XyzD65)]
33	XyzD65(T![Ident]),
34}
35
36#[node]
37#[derive(IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
39#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
40#[derive(csskit_derives::NodeWithMetadata)]
41pub struct CommaOrSlash(#[metadata(skip)] Cursor);
42
43impl<'a> Peek<'a> for CommaOrSlash {
44	const PEEK_KINDSET: KindSet = KindSet::new(&[Kind::Delim]);
45
46	#[inline(always)]
47	fn peek<I>(_: &Parser<'a, I>, c: Cursor) -> bool
48	where
49		I: Iterator<Item = Cursor> + Clone,
50	{
51		c == ',' || c == '/'
52	}
53}
54
55impl<'a> Parse<'a> for CommaOrSlash {
56	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
57	where
58		I: Iterator<Item = Cursor> + Clone,
59	{
60		if !p.peek::<Self>() {
61			Err(Diagnostic::new(p.next(), Diagnostic::unexpected))?
62		}
63		Ok(Self(p.next()))
64	}
65}
66
67/// <https://drafts.csswg.org/css-color/#typedef-color-function>
68#[node]
69#[derive(Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
71#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(all))]
72#[derive(csskit_derives::NodeWithMetadata)]
73pub enum ColorFunction<'a> {
74	Relative(Box<'a, RelativeColorFunction<'a>>),
75	Color(Box<'a, ColorFunctionColor<'a>>),
76	ColorMix(Box<'a, ColorMixFunction<'a>>),
77	LightDark(Box<'a, LightDarkFunction<'a>>),
78	Rgb(RgbFunction<'a>),
79	Rgba(RgbaFunction<'a>),
80	Hsl(HslFunction<'a>),
81	Hsla(HslaFunction<'a>),
82	Hwb(HwbFunction<'a>),
83	Lab(LabFunction<'a>),
84	Lch(LchFunction<'a>),
85	Oklab(OklabFunction<'a>),
86	Oklch(OklchFunction<'a>),
87}
88
89impl<'a> Parse<'a> for ColorFunction<'a> {
90	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
91	where
92		I: Iterator<Item = Cursor> + Clone,
93	{
94		// Check for relative colour syntax first: any colour function with `from` as 2nd token.
95		if p.peek::<Box<RelativeColorFunction>>() {
96			return Ok(Self::Relative(p.parse()?));
97		}
98		if p.peek::<Box<ColorFunctionColor<'_>>>() {
99			return Ok(Self::Color(p.parse()?));
100		}
101		if p.peek::<Box<ColorMixFunction>>() {
102			return Ok(Self::ColorMix(p.parse()?));
103		}
104		if p.peek::<Box<LightDarkFunction>>() {
105			return Ok(Self::LightDark(p.parse()?));
106		}
107		if p.peek::<RgbFunction>() {
108			return Ok(Self::Rgb(p.parse()?));
109		}
110		if p.peek::<RgbaFunction>() {
111			return Ok(Self::Rgba(p.parse()?));
112		}
113		if p.peek::<HslFunction>() {
114			return Ok(Self::Hsl(p.parse()?));
115		}
116		if p.peek::<HslaFunction>() {
117			return Ok(Self::Hsla(p.parse()?));
118		}
119		if p.peek::<HwbFunction>() {
120			return Ok(Self::Hwb(p.parse()?));
121		}
122		if p.peek::<LabFunction>() {
123			return Ok(Self::Lab(p.parse()?));
124		}
125		if p.peek::<LchFunction>() {
126			return Ok(Self::Lch(p.parse()?));
127		}
128		if p.peek::<OklabFunction>() {
129			return Ok(Self::Oklab(p.parse()?));
130		}
131		Ok(Self::Oklch(p.parse()?))
132	}
133}
134
135#[cfg(feature = "chromashift")]
136impl crate::ToChromashift for ColorFunction<'_> {
137	fn to_chromashift(&self) -> Option<chromashift::Color> {
138		match self {
139			Self::Relative(r) => r.to_chromashift(),
140			Self::Color(c) => c.to_chromashift(),
141			Self::ColorMix(c) => c.to_chromashift(),
142			Self::LightDark(_c) => None,
143			Self::Rgb(c) => c.to_chromashift(),
144			Self::Rgba(c) => c.to_chromashift(),
145			Self::Hsl(c) => c.to_chromashift(),
146			Self::Hsla(c) => c.to_chromashift(),
147			Self::Hwb(c) => c.to_chromashift(),
148			Self::Lab(c) => c.to_chromashift(),
149			Self::Lch(c) => c.to_chromashift(),
150			Self::Oklab(c) => c.to_chromashift(),
151			Self::Oklch(c) => c.to_chromashift(),
152		}
153	}
154}
155
156/// <https://drafts.csswg.org/css-color/#funcdef-color>
157///
158/// ```text,ignore
159/// color() = color( <colorspace-params> [ / [ <alpha-value> | none ] ]? )
160/// <colorspace-params> = [ <predefined-rgb-params> | <xyz-params>]
161/// <predefined-rgb-params> = <predefined-rgb> [ <number> | <percentage> | none ]{3}
162/// <predefined-rgb> = srgb | srgb-linear | display-p3 | a98-rgb | prophoto-rgb | rec2020
163/// <xyz-params> = <xyz-space> [ <number> | <percentage> | none ]{3}
164/// <xyz-space> = xyz | xyz-d50 | xyz-d65
165/// ```
166#[node]
167#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
168#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
169#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
170#[derive(csskit_derives::NodeWithMetadata)]
171pub struct ColorFunctionColor<'a> {
172	#[atom(CssAtomSet::Color)]
173	pub name: T![Function],
174	pub params: ColorFunctionColorParams<'a>,
175	#[semantic_eq(skip)]
176	pub close: T![')'],
177}
178
179#[cfg(feature = "chromashift")]
180impl crate::ToChromashift for ColorFunctionColor<'_> {
181	fn to_chromashift(&self) -> Option<chromashift::Color> {
182		use chromashift::{A98Rgb, DisplayP3, LinearRgb, ProphotoRgb, Rec2020, Srgb, XyzD50, XyzD65};
183
184		let ColorFunctionColorParams(space, c1, c2, c3, _, alpha) = &self.params;
185
186		let alpha = match alpha {
187			Some(NoneOr::None(_)) => 0.0,
188			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
189			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
190			Some(NoneOr::Some(_)) => return None,
191			None => 100.0,
192		};
193
194		// Helper to extract a channel as f64 in 0.0-1.0 range
195		let channel_unit = |c: &NoneOr<NumericValue<'_, NumberOrPercentage>>| -> Option<f64> {
196			match c {
197				NoneOr::None(_) => Some(0.0),
198				NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => Some(n.value() as f64),
199				NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => {
200					Some(p.value() as f64 / 100.0)
201				}
202				NoneOr::Some(_) => None,
203			}
204		};
205
206		match space {
207			ColorSpace::Srgb(_) => {
208				let r = channel_unit(c1)?;
209				let g = channel_unit(c2)?;
210				let b = channel_unit(c3)?;
211				Some(chromashift::Color::Srgb(Srgb::new(r, g, b, alpha)))
212			}
213			ColorSpace::SrgbLinear(_) => {
214				let r = channel_unit(c1)?;
215				let g = channel_unit(c2)?;
216				let b = channel_unit(c3)?;
217				Some(chromashift::Color::LinearRgb(LinearRgb::new(r, g, b, alpha)))
218			}
219			ColorSpace::DisplayP3(_) => {
220				let r = channel_unit(c1)?;
221				let g = channel_unit(c2)?;
222				let b = channel_unit(c3)?;
223				Some(chromashift::Color::DisplayP3(DisplayP3::new(r, g, b, alpha)))
224			}
225			ColorSpace::A98Rgb(_) => {
226				let r = channel_unit(c1)?;
227				let g = channel_unit(c2)?;
228				let b = channel_unit(c3)?;
229				Some(chromashift::Color::A98Rgb(A98Rgb::new(r, g, b, alpha)))
230			}
231			ColorSpace::ProphotoRgb(_) => {
232				let r = channel_unit(c1)?;
233				let g = channel_unit(c2)?;
234				let b = channel_unit(c3)?;
235				Some(chromashift::Color::ProphotoRgb(ProphotoRgb::new(r, g, b, alpha)))
236			}
237			ColorSpace::Rec2020(_) => {
238				let r = channel_unit(c1)?;
239				let g = channel_unit(c2)?;
240				let b = channel_unit(c3)?;
241				Some(chromashift::Color::Rec2020(Rec2020::new(r, g, b, alpha)))
242			}
243			ColorSpace::Xyz(_) | ColorSpace::XyzD65(_) => {
244				let x = channel_unit(c1)? * 100.0;
245				let y = channel_unit(c2)? * 100.0;
246				let z = channel_unit(c3)? * 100.0;
247				Some(chromashift::Color::XyzD65(XyzD65::new(x, y, z, alpha)))
248			}
249			ColorSpace::XyzD50(_) => {
250				let x = channel_unit(c1)? * 100.0;
251				let y = channel_unit(c2)? * 100.0;
252				let z = channel_unit(c3)? * 100.0;
253				Some(chromashift::Color::XyzD50(XyzD50::new(x, y, z, alpha)))
254			}
255		}
256	}
257}
258
259#[node]
260#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
261#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
262#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
263#[derive(csskit_derives::NodeWithMetadata)]
264pub struct ColorFunctionColorParams<'a>(
265	pub ColorSpace,
266	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
267	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
268	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
269	#[semantic_eq(skip)] pub Option<T![/]>,
270	pub Option<NoneOr<NumericValue<'a, NumberOrPercentage>>>,
271);
272
273/// <https://drafts.csswg.org/css-color/#funcdef-rgb>
274///
275/// ```text,ignore
276/// rgb() = [ <legacy-rgb-syntax> | <modern-rgb-syntax> ]
277/// rgba() = [ <legacy-rgba-syntax> | <modern-rgba-syntax> ]
278/// <legacy-rgb-syntax> =   rgb( <percentage>#{3} , <alpha-value>? ) |
279///                   rgb( <number>#{3} , <alpha-value>? )
280/// <legacy-rgba-syntax> = rgba( <percentage>#{3} , <alpha-value>? ) |
281///                   rgba( <number>#{3} , <alpha-value>? )
282/// <modern-rgb-syntax> = rgb(
283///   [ <number> | <percentage> | none]{3}
284///   [ / [<alpha-value> | none] ]?  )
285/// <modern-rgba-syntax> = rgba(
286///   [ <number> | <percentage> | none]{3}
287///   [ / [<alpha-value> | none] ]?  )
288/// ```
289#[node]
290#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
291#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
292#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
293#[derive(csskit_derives::NodeWithMetadata)]
294pub struct RgbFunction<'a> {
295	#[atom(CssAtomSet::Rgb)]
296	pub name: T![Function],
297	pub params: RgbFunctionParams<'a>,
298	#[semantic_eq(skip)]
299	pub close: T![')'],
300}
301
302#[cfg(feature = "chromashift")]
303impl crate::ToChromashift for RgbFunction<'_> {
304	fn to_chromashift(&self) -> Option<chromashift::Color> {
305		self.params.to_chromashift()
306	}
307}
308
309#[node]
310#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
311#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
312#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
313#[derive(csskit_derives::NodeWithMetadata)]
314pub struct RgbaFunction<'a> {
315	#[atom(CssAtomSet::Rgba)]
316	pub name: T![Function],
317	pub params: RgbFunctionParams<'a>,
318	#[semantic_eq(skip)]
319	pub close: T![')'],
320}
321
322#[cfg(feature = "chromashift")]
323impl crate::ToChromashift for RgbaFunction<'_> {
324	fn to_chromashift(&self) -> Option<chromashift::Color> {
325		self.params.to_chromashift()
326	}
327}
328
329#[node]
330#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
331#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
332#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
333#[derive(csskit_derives::NodeWithMetadata)]
334pub struct RgbFunctionParams<'a>(
335	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
336	#[semantic_eq(skip)] pub Option<T![,]>,
337	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
338	#[semantic_eq(skip)] pub Option<T![,]>,
339	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
340	pub Option<CommaOrSlash>,
341	pub Option<NoneOr<NumericValue<'a, NumberOrPercentage>>>,
342);
343
344#[cfg(feature = "chromashift")]
345impl crate::ToChromashift for RgbFunctionParams<'_> {
346	fn to_chromashift(&self) -> Option<chromashift::Color> {
347		use chromashift::Srgb;
348		let Self(red, _, green, _, blue, _, alpha) = &self;
349		let alpha = match alpha {
350			Some(NoneOr::None(_)) => 0.0,
351			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
352			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
353			Some(NoneOr::Some(_)) => return None,
354			None => 100.0,
355		};
356		let red = match red {
357			NoneOr::None(_) => 0.0,
358			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(red))) => red.value() as f64 / 255.0,
359			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(red))) => red.value() as f64 / 100.0,
360			NoneOr::Some(_) => return None,
361		};
362		let green = match green {
363			NoneOr::None(_) => 0.0,
364			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(green))) => green.value() as f64 / 255.0,
365			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(green))) => green.value() as f64 / 100.0,
366			NoneOr::Some(_) => return None,
367		};
368		let blue = match blue {
369			NoneOr::None(_) => 0.0,
370			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(blue))) => blue.value() as f64 / 255.0,
371			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(blue))) => blue.value() as f64 / 100.0,
372			NoneOr::Some(_) => return None,
373		};
374		Some(chromashift::Color::Srgb(Srgb::new(red, green, blue, alpha)))
375	}
376}
377
378/// <https://drafts.csswg.org/css-color/#funcdef-hsl>
379///
380/// ```text,ignore
381/// hsl() = [ <legacy-hsl-syntax> | <modern-hsl-syntax> ]
382/// hsla() = [ <legacy-hsla-syntax> | <modern-hsla-syntax> ]
383/// <modern-hsl-syntax> = hsl(
384///     [<hue> | none]
385///     [<percentage> | <number> | none]
386///     [<percentage> | <number> | none]
387///     [ / [<alpha-value> | none] ]? )
388/// <modern-hsla-syntax> = hsla(
389///     [<hue> | none]
390///     [<percentage> | <number> | none]
391///     [<percentage> | <number> | none]
392///     [ / [<alpha-value> | none] ]? )
393/// <legacy-hsl-syntax> = hsl( <hue>, <percentage>, <percentage>, <alpha-value>? )
394/// <legacy-hsla-syntax> = hsla( <hue>, <percentage>, <percentage>, <alpha-value>? )
395/// ```
396#[node]
397#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
398#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
399#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
400#[derive(csskit_derives::NodeWithMetadata)]
401pub struct HslFunction<'a> {
402	#[atom(CssAtomSet::Hsl)]
403	pub name: T![Function],
404	pub params: HslFunctionParams<'a>,
405	#[semantic_eq(skip)]
406	pub close: T![')'],
407}
408
409#[cfg(feature = "chromashift")]
410impl crate::ToChromashift for HslFunction<'_> {
411	fn to_chromashift(&self) -> Option<chromashift::Color> {
412		self.params.to_chromashift()
413	}
414}
415
416#[node]
417#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
418#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
419#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
420#[derive(csskit_derives::NodeWithMetadata)]
421pub struct HslaFunction<'a> {
422	#[atom(CssAtomSet::Hsla)]
423	pub name: T![Function],
424	pub params: HslFunctionParams<'a>,
425	#[semantic_eq(skip)]
426	pub close: T![')'],
427}
428
429#[cfg(feature = "chromashift")]
430impl crate::ToChromashift for HslaFunction<'_> {
431	fn to_chromashift(&self) -> Option<chromashift::Color> {
432		self.params.to_chromashift()
433	}
434}
435
436#[node]
437#[derive(Parse, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
438#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
439#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
440#[derive(csskit_derives::NodeWithMetadata)]
441pub struct HslFunctionParams<'a>(
442	pub NoneOr<NumericValue<'a, AngleOrNumber>>,
443	#[semantic_eq(skip)] pub Option<T![,]>,
444	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
445	#[semantic_eq(skip)] pub Option<T![,]>,
446	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
447	pub Option<CommaOrSlash>,
448	pub Option<NoneOr<NumericValue<'a, NumberOrPercentage>>>,
449);
450
451#[cfg(feature = "chromashift")]
452impl crate::ToChromashift for HslFunctionParams<'_> {
453	fn to_chromashift(&self) -> Option<chromashift::Color> {
454		use chromashift::Hsl;
455		let Self(hue, _, saturation, _, lightness, _, alpha) = &self;
456		let hue = match hue {
457			NoneOr::None(_) => 0.0,
458			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Number(hue))) => hue.value(),
459			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Angle(d))) => d.as_degrees(),
460			NoneOr::Some(_) => return None,
461		};
462		let saturation = match saturation {
463			NoneOr::None(_) => 0.0,
464			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
465			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
466			NoneOr::Some(_) => return None,
467		};
468		let lightness = match lightness {
469			NoneOr::None(_) => 0.0,
470			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
471			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
472			NoneOr::Some(_) => return None,
473		};
474		let alpha = match alpha {
475			Some(NoneOr::None(_)) => 0.0,
476			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
477			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
478			Some(NoneOr::Some(_)) => return None,
479			None => 100.0,
480		};
481		Some(chromashift::Color::Hsl(Hsl::new(hue.into(), saturation.into(), lightness.into(), alpha)))
482	}
483}
484
485/// <https://drafts.csswg.org/css-color/#funcdef-hwb>
486///
487/// ```text,ignore
488/// hwb() = hwb(
489///  [<hue> | none]
490///  [<percentage> | <number> | none]
491///  [<percentage> | <number> | none]
492///  [ / [<alpha-value> | none] ]? )
493/// ```
494#[node]
495#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
496#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
497#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
498#[derive(csskit_derives::NodeWithMetadata)]
499pub struct HwbFunction<'a> {
500	#[atom(CssAtomSet::Hwb)]
501	pub name: T![Function],
502	pub params: HwbFunctionParams<'a>,
503	#[semantic_eq(skip)]
504	pub close: T![')'],
505}
506
507#[cfg(feature = "chromashift")]
508impl crate::ToChromashift for HwbFunction<'_> {
509	fn to_chromashift(&self) -> Option<chromashift::Color> {
510		use chromashift::Hwb;
511		let HwbFunctionParams(hue, whiteness, blackness, _, alpha) = &self.params;
512		let hue = match hue {
513			NoneOr::None(_) => 0.0,
514			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Number(hue))) => hue.value(),
515			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Angle(d))) => d.as_degrees(),
516			NoneOr::Some(_) => return None,
517		};
518		let whiteness = match whiteness {
519			NoneOr::None(_) => 0.0,
520			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
521			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
522			NoneOr::Some(_) => return None,
523		};
524		let blackness = match blackness {
525			NoneOr::None(_) => 0.0,
526			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
527			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
528			NoneOr::Some(_) => return None,
529		};
530		let alpha = match alpha {
531			Some(NoneOr::None(_)) => 0.0,
532			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
533			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
534			Some(NoneOr::Some(_)) => return None,
535			None => 100.0,
536		};
537		Some(chromashift::Color::Hwb(Hwb::new(hue.into(), whiteness.into(), blackness.into(), alpha)))
538	}
539}
540
541#[node]
542#[derive(Parse, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
543#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
544#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
545#[derive(csskit_derives::NodeWithMetadata)]
546pub struct HwbFunctionParams<'a>(
547	pub NoneOr<NumericValue<'a, AngleOrNumber>>,
548	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
549	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
550	#[semantic_eq(skip)] pub Option<T![/]>,
551	pub Option<NoneOr<NumericValue<'a, NumberOrPercentage>>>,
552);
553
554/// <https://drafts.csswg.org/css-color/#funcdef-lab>
555///
556/// ```text,ignore
557/// lab() = lab( [<percentage> | <number> | none]
558///  [ <percentage> | <number> | none]
559///  [ <percentage> | <number> | none]
560///  [ / [<alpha-value> | none] ]? )
561/// ```
562#[node]
563#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
564#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
565#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
566#[derive(csskit_derives::NodeWithMetadata)]
567pub struct LabFunction<'a> {
568	#[atom(CssAtomSet::Lab)]
569	pub name: T![Function],
570	pub params: LabFunctionParams<'a>,
571	#[semantic_eq(skip)]
572	pub close: T![')'],
573}
574
575#[cfg(feature = "chromashift")]
576impl crate::ToChromashift for LabFunction<'_> {
577	fn to_chromashift(&self) -> Option<chromashift::Color> {
578		use chromashift::Lab;
579		let LabFunctionParams(l, a, b, _, alpha) = &self.params;
580		let l = match l {
581			NoneOr::None(_) => 0.0,
582			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
583			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
584			NoneOr::Some(_) => return None,
585		} as f64;
586		let a = match a {
587			NoneOr::None(_) => 0.0,
588			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
589			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 125.0,
590			NoneOr::Some(_) => return None,
591		} as f64;
592		let b = match b {
593			NoneOr::None(_) => 0.0,
594			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
595			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 125.0,
596			NoneOr::Some(_) => return None,
597		} as f64;
598		let alpha = match alpha {
599			Some(NoneOr::None(_)) => 0.0,
600			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
601			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
602			Some(NoneOr::Some(_)) => return None,
603			None => 100.0,
604		};
605		Some(chromashift::Color::Lab(Lab::new(l, a, b, alpha)))
606	}
607}
608
609#[node]
610#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
611#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
612#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
613#[derive(csskit_derives::NodeWithMetadata)]
614pub struct LabFunctionParams<'a>(
615	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
616	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
617	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
618	#[semantic_eq(skip)] pub Option<T![/]>,
619	pub Option<NoneOr<NumericValue<'a, NumberOrPercentage>>>,
620);
621
622/// <https://drafts.csswg.org/css-color/#funcdef-lch>
623///
624/// ```text,ignore
625/// lch() = lch( [<percentage> | <number> | none]
626///  [ <percentage> | <number> | none]
627///  [ <hue> | none]
628///  [ / [<alpha-value> | none] ]? )
629/// ```
630#[node]
631#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
632#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
633#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
634#[derive(csskit_derives::NodeWithMetadata)]
635pub struct LchFunction<'a> {
636	#[atom(CssAtomSet::Lch)]
637	pub name: T![Function],
638	pub params: LchFunctionParams<'a>,
639	#[semantic_eq(skip)]
640	pub close: T![')'],
641}
642
643#[cfg(feature = "chromashift")]
644impl crate::ToChromashift for LchFunction<'_> {
645	fn to_chromashift(&self) -> Option<chromashift::Color> {
646		use chromashift::Lch;
647		let LchFunctionParams(lightness, chroma, hue, _, alpha) = &self.params;
648		let lightness = match lightness {
649			NoneOr::None(_) => 0.0,
650			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
651			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
652			NoneOr::Some(_) => return None,
653		} as f64;
654		let chroma = match chroma {
655			NoneOr::None(_) => 0.0,
656			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
657			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 150.0,
658			NoneOr::Some(_) => return None,
659		} as f64;
660		let hue = match hue {
661			NoneOr::None(_) => 0.0,
662			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Number(hue))) => hue.value(),
663			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Angle(d))) => d.as_degrees(),
664			NoneOr::Some(_) => return None,
665		} as f64;
666		let alpha = match alpha {
667			Some(NoneOr::None(_)) => 0.0,
668			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
669			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
670			Some(NoneOr::Some(_)) => return None,
671			None => 100.0,
672		};
673		Some(chromashift::Color::Lch(Lch::new(lightness, chroma, hue, alpha)))
674	}
675}
676
677#[node]
678#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
679#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
680#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
681#[derive(csskit_derives::NodeWithMetadata)]
682pub struct LchFunctionParams<'a>(
683	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
684	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
685	pub NoneOr<NumericValue<'a, AngleOrNumber>>,
686	#[semantic_eq(skip)] pub Option<T![/]>,
687	pub Option<NoneOr<NumericValue<'a, NumberOrPercentage>>>,
688);
689
690/// <https://drafts.csswg.org/css-color/#funcdef-oklab>
691///
692/// ```text,ignore
693/// oklab() = oklab( [ <percentage> | <number> | none]
694///  [ <percentage> | <number> | none]
695///  [ <percentage> | <number> | none]
696///  [ / [<alpha-value> | none] ]? )
697///  ```
698#[node]
699#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
700#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
701#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
702#[derive(csskit_derives::NodeWithMetadata)]
703pub struct OklabFunction<'a> {
704	#[atom(CssAtomSet::Oklab)]
705	pub name: T![Function],
706	pub params: LabFunctionParams<'a>,
707	#[semantic_eq(skip)]
708	pub close: T![')'],
709}
710
711#[cfg(feature = "chromashift")]
712impl crate::ToChromashift for OklabFunction<'_> {
713	fn to_chromashift(&self) -> Option<chromashift::Color> {
714		use chromashift::Oklab;
715		let LabFunctionParams(l, a, b, _, alpha) = &self.params;
716		let alpha = match alpha {
717			Some(NoneOr::None(_)) => 0.0,
718			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
719			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
720			Some(NoneOr::Some(_)) => return None,
721			None => 100.0,
722		};
723		let l = match l {
724			NoneOr::None(_) => 0.0,
725			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
726			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0,
727			NoneOr::Some(_) => return None,
728		} as f64;
729		let a = match a {
730			NoneOr::None(_) => 0.0,
731			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
732			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 0.4,
733			NoneOr::Some(_) => return None,
734		} as f64;
735		let b = match b {
736			NoneOr::None(_) => 0.0,
737			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
738			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 0.4,
739			NoneOr::Some(_) => return None,
740		} as f64;
741		Some(chromashift::Color::Oklab(Oklab::new(l, a, b, alpha)))
742	}
743}
744
745/// <https://drafts.csswg.org/css-color/#funcdef-oklch>
746///
747/// ```text,ignore
748/// oklab() = oklab( [ <percentage> | <number> | none]
749///  [ <percentage> | <number> | none]
750///  [ <percentage> | <number> | none]
751///  [ / [<alpha-value> | none] ]? )
752///  ```
753#[node]
754#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
755#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
756#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
757#[derive(csskit_derives::NodeWithMetadata)]
758pub struct OklchFunction<'a> {
759	#[atom(CssAtomSet::Oklch)]
760	pub name: T![Function],
761	pub params: LchFunctionParams<'a>,
762	#[semantic_eq(skip)]
763	pub close: T![')'],
764}
765
766#[cfg(feature = "chromashift")]
767impl crate::ToChromashift for OklchFunction<'_> {
768	fn to_chromashift(&self) -> Option<chromashift::Color> {
769		use chromashift::Oklch;
770		let LchFunctionParams(lightness, chroma, hue, _, alpha) = &self.params;
771		let lightness = match lightness {
772			NoneOr::None(_) => 0.0,
773			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
774			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
775			NoneOr::Some(_) => return None,
776		} as f64;
777		let chroma = match chroma {
778			NoneOr::None(_) => 0.0,
779			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
780			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 150.0,
781			NoneOr::Some(_) => return None,
782		} as f64;
783		let hue = match hue {
784			NoneOr::None(_) => 0.0,
785			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Number(hue))) => hue.value(),
786			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Angle(d))) => d.as_degrees(),
787			NoneOr::Some(_) => return None,
788		} as f64;
789		let alpha = match alpha {
790			Some(NoneOr::None(_)) => 0.0,
791			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
792			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
793			Some(NoneOr::Some(_)) => return None,
794			None => 100.0,
795		};
796		Some(chromashift::Color::Oklch(Oklch::new(lightness, chroma, hue, alpha)))
797	}
798}
799
800#[cfg(test)]
801mod tests {
802	use super::*;
803
804	#[test]
805	fn substitution_in_channels() {
806		use css_parse::assert_parse;
807		// Math functions in channels.
808		assert_parse!(CssAtomSet::ATOMS, RgbFunction, "rgb(calc(255/2) 0 0)");
809		assert_parse!(CssAtomSet::ATOMS, HslFunction, "hsl(calc(120deg) 50% 50%)");
810		// Substitution functions in channels.
811		assert_parse!(CssAtomSet::ATOMS, RgbFunction, "rgb(var(--r) 0 0)");
812		assert_parse!(CssAtomSet::ATOMS, HwbFunction, "hwb(90deg calc(10%) var(--b))");
813		// Substitution in alpha.
814		assert_parse!(CssAtomSet::ATOMS, RgbFunction, "rgb(0 0 0/var(--a))");
815		// color() with math/substitution channels.
816		assert_parse!(CssAtomSet::ATOMS, ColorFunctionColor, "color(srgb calc(0.5) var(--g) 0)");
817	}
818}