1use crate::error::{ParserError, PrinterError};
4use crate::printer::Printer;
5use crate::targets::{Browsers, Targets};
6use crate::traits::{FallbackValues, IsCompatible, Parse, ToCss, Zero};
7use crate::values::color::ColorFallbackKind;
8use crate::values::{angle::Angle, color::CssColor, length::Length, percentage::NumberOrPercentage, url::Url};
9#[cfg(feature = "visitor")]
10use crate::visitor::Visit;
11use cssparser::*;
12use smallvec::SmallVec;
13
14#[derive(Debug, Clone, PartialEq)]
16#[cfg_attr(feature = "visitor", derive(Visit))]
17#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
18#[cfg_attr(
19 feature = "serde",
20 derive(serde::Serialize, serde::Deserialize),
21 serde(tag = "type", content = "value", rename_all = "kebab-case")
22)]
23#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
24pub enum Filter<'i> {
25 Blur(Length),
27 Brightness(NumberOrPercentage),
29 Contrast(NumberOrPercentage),
31 Grayscale(NumberOrPercentage),
33 HueRotate(Angle),
35 Invert(NumberOrPercentage),
37 Opacity(NumberOrPercentage),
39 Saturate(NumberOrPercentage),
41 Sepia(NumberOrPercentage),
43 DropShadow(DropShadow),
45 #[cfg_attr(feature = "serde", serde(borrow))]
47 Url(Url<'i>),
48}
49
50impl<'i> Parse<'i> for Filter<'i> {
51 fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
52 if let Ok(url) = input.try_parse(Url::parse) {
53 return Ok(Filter::Url(url));
54 }
55
56 let location = input.current_source_location();
57 let function = input.expect_function()?;
58 match_ignore_ascii_case! { &function,
59 "blur" => {
60 input.parse_nested_block(|input| {
61 Ok(Filter::Blur(input.try_parse(Length::parse).unwrap_or(Length::zero())))
62 })
63 },
64 "brightness" => {
65 input.parse_nested_block(|input| {
66 Ok(Filter::Brightness(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
67 })
68 },
69 "contrast" => {
70 input.parse_nested_block(|input| {
71 Ok(Filter::Contrast(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
72 })
73 },
74 "grayscale" => {
75 input.parse_nested_block(|input| {
76 Ok(Filter::Grayscale(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
77 })
78 },
79 "hue-rotate" => {
80 input.parse_nested_block(|input| {
81 Ok(Filter::HueRotate(input.try_parse(Angle::parse_with_unitless_zero).unwrap_or(Angle::zero())))
83 })
84 },
85 "invert" => {
86 input.parse_nested_block(|input| {
87 Ok(Filter::Invert(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
88 })
89 },
90 "opacity" => {
91 input.parse_nested_block(|input| {
92 Ok(Filter::Opacity(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
93 })
94 },
95 "saturate" => {
96 input.parse_nested_block(|input| {
97 Ok(Filter::Saturate(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
98 })
99 },
100 "sepia" => {
101 input.parse_nested_block(|input| {
102 Ok(Filter::Sepia(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
103 })
104 },
105 "drop-shadow" => {
106 input.parse_nested_block(|input| {
107 Ok(Filter::DropShadow(DropShadow::parse(input)?))
108 })
109 },
110 _ => Err(location.new_unexpected_token_error(
111 cssparser::Token::Ident(function.clone())
112 ))
113 }
114 }
115}
116
117impl<'i> ToCss for Filter<'i> {
118 fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
119 where
120 W: std::fmt::Write,
121 {
122 match self {
123 Filter::Blur(val) => {
124 dest.write_str("blur(")?;
125 if *val != Length::zero() {
126 val.to_css(dest)?;
127 }
128 dest.write_char(')')
129 }
130 Filter::Brightness(val) => {
131 dest.write_str("brightness(")?;
132 let v: f32 = val.into();
133 if v != 1.0 {
134 val.to_css(dest)?;
135 }
136 dest.write_char(')')
137 }
138 Filter::Contrast(val) => {
139 dest.write_str("contrast(")?;
140 let v: f32 = val.into();
141 if v != 1.0 {
142 val.to_css(dest)?;
143 }
144 dest.write_char(')')
145 }
146 Filter::Grayscale(val) => {
147 dest.write_str("grayscale(")?;
148 let v: f32 = val.into();
149 if v != 1.0 {
150 val.to_css(dest)?;
151 }
152 dest.write_char(')')
153 }
154 Filter::HueRotate(val) => {
155 dest.write_str("hue-rotate(")?;
156 if !val.is_zero() {
157 val.to_css(dest)?;
158 }
159 dest.write_char(')')
160 }
161 Filter::Invert(val) => {
162 dest.write_str("invert(")?;
163 let v: f32 = val.into();
164 if v != 1.0 {
165 val.to_css(dest)?;
166 }
167 dest.write_char(')')
168 }
169 Filter::Opacity(val) => {
170 dest.write_str("opacity(")?;
171 let v: f32 = val.into();
172 if v != 1.0 {
173 val.to_css(dest)?;
174 }
175 dest.write_char(')')
176 }
177 Filter::Saturate(val) => {
178 dest.write_str("saturate(")?;
179 let v: f32 = val.into();
180 if v != 1.0 {
181 val.to_css(dest)?;
182 }
183 dest.write_char(')')
184 }
185 Filter::Sepia(val) => {
186 dest.write_str("sepia(")?;
187 let v: f32 = val.into();
188 if v != 1.0 {
189 val.to_css(dest)?;
190 }
191 dest.write_char(')')
192 }
193 Filter::DropShadow(val) => {
194 dest.write_str("drop-shadow(")?;
195 val.to_css(dest)?;
196 dest.write_char(')')
197 }
198 Filter::Url(url) => url.to_css(dest),
199 }
200 }
201}
202
203impl<'i> Filter<'i> {
204 fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
205 match self {
206 Filter::DropShadow(shadow) => Filter::DropShadow(shadow.get_fallback(kind)),
207 _ => self.clone(),
208 }
209 }
210}
211
212impl IsCompatible for Filter<'_> {
213 fn is_compatible(&self, _browsers: Browsers) -> bool {
214 true
215 }
216}
217
218#[derive(Debug, Clone, PartialEq)]
220#[cfg_attr(feature = "visitor", derive(Visit))]
221#[cfg_attr(
222 feature = "serde",
223 derive(serde::Serialize, serde::Deserialize),
224 serde(rename_all = "camelCase")
225)]
226#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
227#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
228pub struct DropShadow {
229 pub color: CssColor,
231 pub x_offset: Length,
233 pub y_offset: Length,
235 pub blur: Length,
237}
238
239impl<'i> Parse<'i> for DropShadow {
240 fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
241 let mut color = None;
242 let mut lengths = None;
243
244 loop {
245 if lengths.is_none() {
246 let value = input.try_parse::<_, _, ParseError<ParserError<'i>>>(|input| {
247 let horizontal = Length::parse(input)?;
248 let vertical = Length::parse(input)?;
249 let blur = input.try_parse(Length::parse).unwrap_or(Length::zero());
250 Ok((horizontal, vertical, blur))
251 });
252
253 if let Ok(value) = value {
254 lengths = Some(value);
255 continue;
256 }
257 }
258
259 if color.is_none() {
260 if let Ok(value) = input.try_parse(CssColor::parse) {
261 color = Some(value);
262 continue;
263 }
264 }
265
266 break;
267 }
268
269 let lengths = lengths.ok_or(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?;
270 Ok(DropShadow {
271 color: color.unwrap_or(CssColor::current_color()),
272 x_offset: lengths.0,
273 y_offset: lengths.1,
274 blur: lengths.2,
275 })
276 }
277}
278
279impl ToCss for DropShadow {
280 fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
281 where
282 W: std::fmt::Write,
283 {
284 self.x_offset.to_css(dest)?;
285 dest.write_char(' ')?;
286 self.y_offset.to_css(dest)?;
287
288 if self.blur != Length::zero() {
289 dest.write_char(' ')?;
290 self.blur.to_css(dest)?;
291 }
292
293 if self.color != CssColor::current_color() {
294 dest.write_char(' ')?;
295 self.color.to_css(dest)?;
296 }
297
298 Ok(())
299 }
300}
301
302impl DropShadow {
303 fn get_fallback(&self, kind: ColorFallbackKind) -> DropShadow {
304 DropShadow {
305 color: self.color.get_fallback(kind),
306 ..self.clone()
307 }
308 }
309}
310
311#[derive(Debug, Clone, PartialEq)]
314#[cfg_attr(feature = "visitor", derive(Visit))]
315#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
316#[cfg_attr(
317 feature = "serde",
318 derive(serde::Serialize, serde::Deserialize),
319 serde(tag = "type", content = "value", rename_all = "kebab-case")
320)]
321#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
322pub enum FilterList<'i> {
323 None,
325 #[cfg_attr(feature = "serde", serde(borrow))]
327 Filters(SmallVec<[Filter<'i>; 1]>),
328}
329
330impl<'i> Parse<'i> for FilterList<'i> {
331 fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
332 if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
333 return Ok(FilterList::None);
334 }
335
336 let mut filters = SmallVec::new();
337 while let Ok(filter) = input.try_parse(Filter::parse) {
338 filters.push(filter);
339 }
340
341 Ok(FilterList::Filters(filters))
342 }
343}
344
345impl<'i> ToCss for FilterList<'i> {
346 fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
347 where
348 W: std::fmt::Write,
349 {
350 match self {
351 FilterList::None => dest.write_str("none"),
352 FilterList::Filters(filters) => {
353 let mut first = true;
354 for filter in filters {
355 if first {
356 first = false;
357 } else {
358 dest.whitespace()?;
359 }
360 filter.to_css(dest)?;
361 }
362 Ok(())
363 }
364 }
365 }
366}
367
368impl<'i> FallbackValues for FilterList<'i> {
369 fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
370 let mut res = Vec::new();
371 let mut fallbacks = ColorFallbackKind::empty();
372 if let FilterList::Filters(filters) = self {
373 for shadow in filters.iter() {
374 if let Filter::DropShadow(shadow) = &shadow {
375 fallbacks |= shadow.color.get_necessary_fallbacks(targets);
376 }
377 }
378
379 if fallbacks.contains(ColorFallbackKind::RGB) {
380 res.push(FilterList::Filters(
381 filters
382 .iter()
383 .map(|filter| filter.get_fallback(ColorFallbackKind::RGB))
384 .collect(),
385 ));
386 }
387
388 if fallbacks.contains(ColorFallbackKind::P3) {
389 res.push(FilterList::Filters(
390 filters
391 .iter()
392 .map(|filter| filter.get_fallback(ColorFallbackKind::P3))
393 .collect(),
394 ));
395 }
396
397 if fallbacks.contains(ColorFallbackKind::LAB) {
398 for filter in filters.iter_mut() {
399 *filter = filter.get_fallback(ColorFallbackKind::LAB);
400 }
401 }
402 }
403
404 res
405 }
406}
407
408impl IsCompatible for FilterList<'_> {
409 fn is_compatible(&self, _browsers: Browsers) -> bool {
410 true
411 }
412}