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)? * 255.0).round() as u8;
209				let g = (channel_unit(c2)? * 255.0).round() as u8;
210				let b = (channel_unit(c3)? * 255.0).round() as u8;
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(),
359			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(red))) => red.value() / 100.0 * 255.0,
360			NoneOr::Some(_) => return None,
361		})
362		.round() as u8;
363		let green = (match green {
364			NoneOr::None(_) => 0.0,
365			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(green))) => green.value(),
366			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(green))) => green.value() / 100.0 * 255.0,
367			NoneOr::Some(_) => return None,
368		})
369		.round() as u8;
370		let blue = (match blue {
371			NoneOr::None(_) => 0.0,
372			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(blue))) => blue.value(),
373			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(blue))) => blue.value() / 100.0 * 255.0,
374			NoneOr::Some(_) => return None,
375		})
376		.round() as u8;
377		Some(chromashift::Color::Srgb(Srgb::new(red, green, blue, alpha)))
378	}
379}
380
381/// <https://drafts.csswg.org/css-color/#funcdef-hsl>
382///
383/// ```text,ignore
384/// hsl() = [ <legacy-hsl-syntax> | <modern-hsl-syntax> ]
385/// hsla() = [ <legacy-hsla-syntax> | <modern-hsla-syntax> ]
386/// <modern-hsl-syntax> = hsl(
387///     [<hue> | none]
388///     [<percentage> | <number> | none]
389///     [<percentage> | <number> | none]
390///     [ / [<alpha-value> | none] ]? )
391/// <modern-hsla-syntax> = hsla(
392///     [<hue> | none]
393///     [<percentage> | <number> | none]
394///     [<percentage> | <number> | none]
395///     [ / [<alpha-value> | none] ]? )
396/// <legacy-hsl-syntax> = hsl( <hue>, <percentage>, <percentage>, <alpha-value>? )
397/// <legacy-hsla-syntax> = hsla( <hue>, <percentage>, <percentage>, <alpha-value>? )
398/// ```
399#[node]
400#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
401#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
402#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
403#[derive(csskit_derives::NodeWithMetadata)]
404pub struct HslFunction<'a> {
405	#[atom(CssAtomSet::Hsl)]
406	pub name: T![Function],
407	pub params: HslFunctionParams<'a>,
408	#[semantic_eq(skip)]
409	pub close: T![')'],
410}
411
412#[cfg(feature = "chromashift")]
413impl crate::ToChromashift for HslFunction<'_> {
414	fn to_chromashift(&self) -> Option<chromashift::Color> {
415		self.params.to_chromashift()
416	}
417}
418
419#[node]
420#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
421#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
422#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
423#[derive(csskit_derives::NodeWithMetadata)]
424pub struct HslaFunction<'a> {
425	#[atom(CssAtomSet::Hsla)]
426	pub name: T![Function],
427	pub params: HslFunctionParams<'a>,
428	#[semantic_eq(skip)]
429	pub close: T![')'],
430}
431
432#[cfg(feature = "chromashift")]
433impl crate::ToChromashift for HslaFunction<'_> {
434	fn to_chromashift(&self) -> Option<chromashift::Color> {
435		self.params.to_chromashift()
436	}
437}
438
439#[node]
440#[derive(Parse, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
441#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
442#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
443#[derive(csskit_derives::NodeWithMetadata)]
444pub struct HslFunctionParams<'a>(
445	pub NoneOr<NumericValue<'a, AngleOrNumber>>,
446	#[semantic_eq(skip)] pub Option<T![,]>,
447	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
448	#[semantic_eq(skip)] pub Option<T![,]>,
449	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
450	pub Option<CommaOrSlash>,
451	pub Option<NoneOr<NumericValue<'a, NumberOrPercentage>>>,
452);
453
454#[cfg(feature = "chromashift")]
455impl crate::ToChromashift for HslFunctionParams<'_> {
456	fn to_chromashift(&self) -> Option<chromashift::Color> {
457		use chromashift::Hsl;
458		let Self(hue, _, saturation, _, lightness, _, alpha) = &self;
459		let hue = match hue {
460			NoneOr::None(_) => 0.0,
461			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Number(hue))) => hue.value(),
462			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Angle(d))) => d.as_degrees(),
463			NoneOr::Some(_) => return None,
464		};
465		let saturation = match saturation {
466			NoneOr::None(_) => 0.0,
467			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
468			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
469			NoneOr::Some(_) => return None,
470		};
471		let lightness = match lightness {
472			NoneOr::None(_) => 0.0,
473			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
474			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
475			NoneOr::Some(_) => return None,
476		};
477		let alpha = match alpha {
478			Some(NoneOr::None(_)) => 0.0,
479			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
480			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
481			Some(NoneOr::Some(_)) => return None,
482			None => 100.0,
483		};
484		Some(chromashift::Color::Hsl(Hsl::new(hue, saturation, lightness, alpha)))
485	}
486}
487
488/// <https://drafts.csswg.org/css-color/#funcdef-hwb>
489///
490/// ```text,ignore
491/// hwb() = hwb(
492///  [<hue> | none]
493///  [<percentage> | <number> | none]
494///  [<percentage> | <number> | none]
495///  [ / [<alpha-value> | none] ]? )
496/// ```
497#[node]
498#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
499#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
500#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
501#[derive(csskit_derives::NodeWithMetadata)]
502pub struct HwbFunction<'a> {
503	#[atom(CssAtomSet::Hwb)]
504	pub name: T![Function],
505	pub params: HwbFunctionParams<'a>,
506	#[semantic_eq(skip)]
507	pub close: T![')'],
508}
509
510#[cfg(feature = "chromashift")]
511impl crate::ToChromashift for HwbFunction<'_> {
512	fn to_chromashift(&self) -> Option<chromashift::Color> {
513		use chromashift::Hwb;
514		let HwbFunctionParams(hue, whiteness, blackness, _, alpha) = &self.params;
515		let hue = match hue {
516			NoneOr::None(_) => 0.0,
517			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Number(hue))) => hue.value(),
518			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Angle(d))) => d.as_degrees(),
519			NoneOr::Some(_) => return None,
520		};
521		let whiteness = match whiteness {
522			NoneOr::None(_) => 0.0,
523			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
524			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
525			NoneOr::Some(_) => return None,
526		};
527		let blackness = match blackness {
528			NoneOr::None(_) => 0.0,
529			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
530			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
531			NoneOr::Some(_) => return None,
532		};
533		let alpha = match alpha {
534			Some(NoneOr::None(_)) => 0.0,
535			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
536			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
537			Some(NoneOr::Some(_)) => return None,
538			None => 100.0,
539		};
540		Some(chromashift::Color::Hwb(Hwb::new(hue, whiteness, blackness, alpha)))
541	}
542}
543
544#[node]
545#[derive(Parse, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
546#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
547#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
548#[derive(csskit_derives::NodeWithMetadata)]
549pub struct HwbFunctionParams<'a>(
550	pub NoneOr<NumericValue<'a, AngleOrNumber>>,
551	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
552	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
553	#[semantic_eq(skip)] pub Option<T![/]>,
554	pub Option<NoneOr<NumericValue<'a, NumberOrPercentage>>>,
555);
556
557/// <https://drafts.csswg.org/css-color/#funcdef-lab>
558///
559/// ```text,ignore
560/// lab() = lab( [<percentage> | <number> | none]
561///  [ <percentage> | <number> | none]
562///  [ <percentage> | <number> | none]
563///  [ / [<alpha-value> | none] ]? )
564/// ```
565#[node]
566#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
567#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
568#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
569#[derive(csskit_derives::NodeWithMetadata)]
570pub struct LabFunction<'a> {
571	#[atom(CssAtomSet::Lab)]
572	pub name: T![Function],
573	pub params: LabFunctionParams<'a>,
574	#[semantic_eq(skip)]
575	pub close: T![')'],
576}
577
578#[cfg(feature = "chromashift")]
579impl crate::ToChromashift for LabFunction<'_> {
580	fn to_chromashift(&self) -> Option<chromashift::Color> {
581		use chromashift::Lab;
582		let LabFunctionParams(l, a, b, _, alpha) = &self.params;
583		let l = match l {
584			NoneOr::None(_) => 0.0,
585			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
586			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
587			NoneOr::Some(_) => return None,
588		} as f64;
589		let a = match a {
590			NoneOr::None(_) => 0.0,
591			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
592			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 125.0,
593			NoneOr::Some(_) => return None,
594		} as f64;
595		let b = match b {
596			NoneOr::None(_) => 0.0,
597			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
598			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 125.0,
599			NoneOr::Some(_) => return None,
600		} as f64;
601		let alpha = match alpha {
602			Some(NoneOr::None(_)) => 0.0,
603			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
604			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
605			Some(NoneOr::Some(_)) => return None,
606			None => 100.0,
607		};
608		Some(chromashift::Color::Lab(Lab::new(l, a, b, alpha)))
609	}
610}
611
612#[node]
613#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
614#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
615#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
616#[derive(csskit_derives::NodeWithMetadata)]
617pub struct LabFunctionParams<'a>(
618	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
619	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
620	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
621	#[semantic_eq(skip)] pub Option<T![/]>,
622	pub Option<NoneOr<NumericValue<'a, NumberOrPercentage>>>,
623);
624
625/// <https://drafts.csswg.org/css-color/#funcdef-lch>
626///
627/// ```text,ignore
628/// lch() = lch( [<percentage> | <number> | none]
629///  [ <percentage> | <number> | none]
630///  [ <hue> | none]
631///  [ / [<alpha-value> | none] ]? )
632/// ```
633#[node]
634#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
635#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
636#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
637#[derive(csskit_derives::NodeWithMetadata)]
638pub struct LchFunction<'a> {
639	#[atom(CssAtomSet::Lch)]
640	pub name: T![Function],
641	pub params: LchFunctionParams<'a>,
642	#[semantic_eq(skip)]
643	pub close: T![')'],
644}
645
646#[cfg(feature = "chromashift")]
647impl crate::ToChromashift for LchFunction<'_> {
648	fn to_chromashift(&self) -> Option<chromashift::Color> {
649		use chromashift::Lch;
650		let LchFunctionParams(lightness, chroma, hue, _, alpha) = &self.params;
651		let lightness = match lightness {
652			NoneOr::None(_) => 0.0,
653			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
654			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
655			NoneOr::Some(_) => return None,
656		} as f64;
657		let chroma = match chroma {
658			NoneOr::None(_) => 0.0,
659			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
660			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 150.0,
661			NoneOr::Some(_) => return None,
662		} as f64;
663		let hue = match hue {
664			NoneOr::None(_) => 0.0,
665			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Number(hue))) => hue.value(),
666			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Angle(d))) => d.as_degrees(),
667			NoneOr::Some(_) => return None,
668		} as f64;
669		let alpha = match alpha {
670			Some(NoneOr::None(_)) => 0.0,
671			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
672			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
673			Some(NoneOr::Some(_)) => return None,
674			None => 100.0,
675		};
676		Some(chromashift::Color::Lch(Lch::new(lightness, chroma, hue, alpha)))
677	}
678}
679
680#[node]
681#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
682#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
683#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
684#[derive(csskit_derives::NodeWithMetadata)]
685pub struct LchFunctionParams<'a>(
686	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
687	pub NoneOr<NumericValue<'a, NumberOrPercentage>>,
688	pub NoneOr<NumericValue<'a, AngleOrNumber>>,
689	#[semantic_eq(skip)] pub Option<T![/]>,
690	pub Option<NoneOr<NumericValue<'a, NumberOrPercentage>>>,
691);
692
693/// <https://drafts.csswg.org/css-color/#funcdef-oklab>
694///
695/// ```text,ignore
696/// oklab() = oklab( [ <percentage> | <number> | none]
697///  [ <percentage> | <number> | none]
698///  [ <percentage> | <number> | none]
699///  [ / [<alpha-value> | none] ]? )
700///  ```
701#[node]
702#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
703#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
704#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
705#[derive(csskit_derives::NodeWithMetadata)]
706pub struct OklabFunction<'a> {
707	#[atom(CssAtomSet::Oklab)]
708	pub name: T![Function],
709	pub params: LabFunctionParams<'a>,
710	#[semantic_eq(skip)]
711	pub close: T![')'],
712}
713
714#[cfg(feature = "chromashift")]
715impl crate::ToChromashift for OklabFunction<'_> {
716	fn to_chromashift(&self) -> Option<chromashift::Color> {
717		use chromashift::Oklab;
718		let LabFunctionParams(l, a, b, _, alpha) = &self.params;
719		let alpha = match alpha {
720			Some(NoneOr::None(_)) => 0.0,
721			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
722			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
723			Some(NoneOr::Some(_)) => return None,
724			None => 100.0,
725		};
726		let l = match l {
727			NoneOr::None(_) => 0.0,
728			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
729			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0,
730			NoneOr::Some(_) => return None,
731		} as f64;
732		let a = match a {
733			NoneOr::None(_) => 0.0,
734			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
735			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 0.4,
736			NoneOr::Some(_) => return None,
737		} as f64;
738		let b = match b {
739			NoneOr::None(_) => 0.0,
740			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
741			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 0.4,
742			NoneOr::Some(_) => return None,
743		} as f64;
744		Some(chromashift::Color::Oklab(Oklab::new(l, a, b, alpha)))
745	}
746}
747
748/// <https://drafts.csswg.org/css-color/#funcdef-oklch>
749///
750/// ```text,ignore
751/// oklab() = oklab( [ <percentage> | <number> | none]
752///  [ <percentage> | <number> | none]
753///  [ <percentage> | <number> | none]
754///  [ / [<alpha-value> | none] ]? )
755///  ```
756#[node]
757#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
758#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
759#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
760#[derive(csskit_derives::NodeWithMetadata)]
761pub struct OklchFunction<'a> {
762	#[atom(CssAtomSet::Oklch)]
763	pub name: T![Function],
764	pub params: LchFunctionParams<'a>,
765	#[semantic_eq(skip)]
766	pub close: T![')'],
767}
768
769#[cfg(feature = "chromashift")]
770impl crate::ToChromashift for OklchFunction<'_> {
771	fn to_chromashift(&self) -> Option<chromashift::Color> {
772		use chromashift::Oklch;
773		let LchFunctionParams(lightness, chroma, hue, _, alpha) = &self.params;
774		let lightness = match lightness {
775			NoneOr::None(_) => 0.0,
776			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
777			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value(),
778			NoneOr::Some(_) => return None,
779		} as f64;
780		let chroma = match chroma {
781			NoneOr::None(_) => 0.0,
782			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(n))) => n.value(),
783			NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(p))) => p.value() / 100.0 * 150.0,
784			NoneOr::Some(_) => return None,
785		} as f64;
786		let hue = match hue {
787			NoneOr::None(_) => 0.0,
788			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Number(hue))) => hue.value(),
789			NoneOr::Some(NumericValue::Literal(AngleOrNumber::Angle(d))) => d.as_degrees(),
790			NoneOr::Some(_) => return None,
791		} as f64;
792		let alpha = match alpha {
793			Some(NoneOr::None(_)) => 0.0,
794			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Number(t)))) => t.value() * 100.0,
795			Some(NoneOr::Some(NumericValue::Literal(NumberOrPercentage::Percentage(t)))) => t.value(),
796			Some(NoneOr::Some(_)) => return None,
797			None => 100.0,
798		};
799		Some(chromashift::Color::Oklch(Oklch::new(lightness, chroma, hue, alpha)))
800	}
801}
802
803#[cfg(test)]
804mod tests {
805	use super::*;
806
807	#[test]
808	fn substitution_in_channels() {
809		use css_parse::assert_parse;
810		// Math functions in channels.
811		assert_parse!(CssAtomSet::ATOMS, RgbFunction, "rgb(calc(255/2) 0 0)");
812		assert_parse!(CssAtomSet::ATOMS, HslFunction, "hsl(calc(120deg) 50% 50%)");
813		// Substitution functions in channels.
814		assert_parse!(CssAtomSet::ATOMS, RgbFunction, "rgb(var(--r) 0 0)");
815		assert_parse!(CssAtomSet::ATOMS, HwbFunction, "hwb(90deg calc(10%) var(--b))");
816		// Substitution in alpha.
817		assert_parse!(CssAtomSet::ATOMS, RgbFunction, "rgb(0 0 0/var(--a))");
818		// color() with math/substitution channels.
819		assert_parse!(CssAtomSet::ATOMS, ColorFunctionColor, "color(srgb calc(0.5) var(--g) 0)");
820	}
821}