parcel_css/values/
image.rs

1//! CSS image values.
2
3use super::color::ColorFallbackKind;
4use super::gradient::*;
5use super::resolution::Resolution;
6use crate::compat;
7use crate::dependencies::{Dependency, UrlDependency};
8use crate::error::{ParserError, PrinterError};
9use crate::prefixes::{is_webkit_gradient, Feature};
10use crate::printer::Printer;
11use crate::targets::Browsers;
12use crate::traits::{FallbackValues, Parse, ToCss};
13use crate::values::string::CowArcStr;
14use crate::values::url::Url;
15use crate::vendor_prefix::VendorPrefix;
16use cssparser::*;
17use smallvec::SmallVec;
18
19/// A CSS [`<image>`](https://www.w3.org/TR/css-images-3/#image-values) value.
20#[derive(Debug, Clone, PartialEq)]
21#[cfg_attr(
22  feature = "serde",
23  derive(serde::Serialize, serde::Deserialize),
24  serde(tag = "type", content = "value", rename_all = "kebab-case")
25)]
26pub enum Image<'i> {
27  /// The `none` keyword.
28  None,
29  /// A `url()`.
30  #[cfg_attr(feature = "serde", serde(borrow))]
31  Url(Url<'i>),
32  /// A gradient.
33  Gradient(Box<Gradient>),
34  /// An `image-set()`.
35  ImageSet(ImageSet<'i>),
36}
37
38impl<'i> Default for Image<'i> {
39  fn default() -> Image<'i> {
40    Image::None
41  }
42}
43
44impl<'i> Image<'i> {
45  /// Returns whether the image includes any vendor prefixed values.
46  pub fn has_vendor_prefix(&self) -> bool {
47    let prefix = self.get_vendor_prefix();
48    !prefix.is_empty() && prefix != VendorPrefix::None
49  }
50
51  /// Returns the vendor prefix used in the image value.
52  pub fn get_vendor_prefix(&self) -> VendorPrefix {
53    match self {
54      Image::Gradient(a) => a.get_vendor_prefix(),
55      Image::ImageSet(a) => a.get_vendor_prefix(),
56      _ => VendorPrefix::empty(),
57    }
58  }
59
60  /// Returns the vendor prefixes that are needed for the given browser targets.
61  pub fn get_necessary_prefixes(&self, targets: Browsers) -> VendorPrefix {
62    match self {
63      Image::Gradient(grad) => grad.get_necessary_prefixes(targets),
64      Image::ImageSet(image_set) => image_set.get_necessary_prefixes(targets),
65      _ => VendorPrefix::None,
66    }
67  }
68
69  /// Returns a vendor prefixed version of the image for the given vendor prefixes.
70  pub fn get_prefixed(&self, prefix: VendorPrefix) -> Image<'i> {
71    match self {
72      Image::Gradient(grad) => Image::Gradient(Box::new(grad.get_prefixed(prefix))),
73      Image::ImageSet(image_set) => Image::ImageSet(image_set.get_prefixed(prefix)),
74      _ => self.clone(),
75    }
76  }
77
78  /// Returns a legacy `-webkit-gradient()` value for the image.
79  ///
80  /// May return an error in case the gradient cannot be converted.
81  pub fn get_legacy_webkit(&self) -> Result<Image<'i>, ()> {
82    match self {
83      Image::Gradient(grad) => Ok(Image::Gradient(Box::new(grad.get_legacy_webkit()?))),
84      _ => Ok(self.clone()),
85    }
86  }
87
88  /// Returns the color fallbacks that are needed for the given browser targets.
89  pub fn get_necessary_fallbacks(&self, targets: Browsers) -> ColorFallbackKind {
90    match self {
91      Image::Gradient(grad) => grad.get_necessary_fallbacks(targets),
92      _ => ColorFallbackKind::empty(),
93    }
94  }
95
96  /// Returns a fallback version of the image for the given color fallback type.
97  pub fn get_fallback(&self, kind: ColorFallbackKind) -> Image<'i> {
98    match self {
99      Image::Gradient(grad) => Image::Gradient(Box::new(grad.get_fallback(kind))),
100      _ => self.clone(),
101    }
102  }
103
104  pub(crate) fn should_preserve_fallback(&self, fallback: &Option<Image>, targets: Option<Browsers>) -> bool {
105    if let (Some(fallback), Some(targets)) = (&fallback, targets) {
106      return !compat::Feature::ImageSet.is_compatible(targets)
107        && matches!(self, Image::ImageSet(..))
108        && !matches!(fallback, Image::ImageSet(..));
109    }
110
111    false
112  }
113
114  pub(crate) fn should_preserve_fallbacks(
115    images: &SmallVec<[Image; 1]>,
116    fallback: Option<&SmallVec<[Image; 1]>>,
117    targets: Option<Browsers>,
118  ) -> bool {
119    if let (Some(fallback), Some(targets)) = (&fallback, targets) {
120      return !compat::Feature::ImageSet.is_compatible(targets)
121        && images.iter().any(|x| matches!(x, Image::ImageSet(..)))
122        && !fallback.iter().any(|x| matches!(x, Image::ImageSet(..)));
123    }
124
125    false
126  }
127}
128
129pub(crate) trait ImageFallback<'i>: Sized {
130  fn get_image(&self) -> &Image<'i>;
131  fn with_image(&self, image: Image<'i>) -> Self;
132
133  #[inline]
134  fn get_necessary_fallbacks(&self, targets: Browsers) -> ColorFallbackKind {
135    self.get_image().get_necessary_fallbacks(targets)
136  }
137
138  #[inline]
139  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
140    self.with_image(self.get_image().get_fallback(kind))
141  }
142}
143
144impl<'i> ImageFallback<'i> for Image<'i> {
145  #[inline]
146  fn get_image(&self) -> &Image<'i> {
147    self
148  }
149
150  #[inline]
151  fn with_image(&self, image: Image<'i>) -> Self {
152    image
153  }
154}
155
156impl<'i> FallbackValues for Image<'i> {
157  fn get_fallbacks(&mut self, targets: Browsers) -> Vec<Self> {
158    // Determine which prefixes and color fallbacks are needed.
159    let prefixes = self.get_necessary_prefixes(targets);
160    let fallbacks = self.get_necessary_fallbacks(targets);
161    let mut res = Vec::new();
162
163    // Get RGB fallbacks if needed.
164    let rgb = if fallbacks.contains(ColorFallbackKind::RGB) {
165      Some(self.get_fallback(ColorFallbackKind::RGB))
166    } else {
167      None
168    };
169
170    // Prefixed properties only support RGB.
171    let prefix_image = rgb.as_ref().unwrap_or(self);
172
173    // Legacy -webkit-gradient()
174    if prefixes.contains(VendorPrefix::WebKit)
175      && is_webkit_gradient(targets)
176      && matches!(prefix_image, Image::Gradient(_))
177    {
178      if let Ok(legacy) = prefix_image.get_legacy_webkit() {
179        res.push(legacy);
180      }
181    }
182
183    // Standard syntax, with prefixes.
184    if prefixes.contains(VendorPrefix::WebKit) {
185      res.push(prefix_image.get_prefixed(VendorPrefix::WebKit))
186    }
187
188    if prefixes.contains(VendorPrefix::Moz) {
189      res.push(prefix_image.get_prefixed(VendorPrefix::Moz))
190    }
191
192    if prefixes.contains(VendorPrefix::O) {
193      res.push(prefix_image.get_prefixed(VendorPrefix::O))
194    }
195
196    if prefixes.contains(VendorPrefix::None) {
197      // Unprefixed, rgb fallback.
198      if let Some(rgb) = rgb {
199        res.push(rgb);
200      }
201
202      // P3 fallback.
203      if fallbacks.contains(ColorFallbackKind::P3) {
204        res.push(self.get_fallback(ColorFallbackKind::P3));
205      }
206
207      // Convert original to lab if needed (e.g. if oklab is not supported but lab is).
208      if fallbacks.contains(ColorFallbackKind::LAB) {
209        *self = self.get_fallback(ColorFallbackKind::LAB);
210      }
211    } else if let Some(last) = res.pop() {
212      // Prefixed property with no unprefixed version.
213      // Replace self with the last prefixed version so that it doesn't
214      // get duplicated when the caller pushes the original value.
215      *self = last;
216    }
217
218    res
219  }
220}
221
222impl<'i, T: ImageFallback<'i>> FallbackValues for SmallVec<[T; 1]> {
223  fn get_fallbacks(&mut self, targets: Browsers) -> Vec<Self> {
224    // Determine what vendor prefixes and color fallbacks are needed.
225    let mut prefixes = VendorPrefix::empty();
226    let mut fallbacks = ColorFallbackKind::empty();
227    let mut res = Vec::new();
228    for item in self.iter() {
229      prefixes |= item.get_image().get_necessary_prefixes(targets);
230      fallbacks |= item.get_necessary_fallbacks(targets);
231    }
232
233    // Get RGB fallbacks if needed.
234    let rgb: Option<SmallVec<[T; 1]>> = if fallbacks.contains(ColorFallbackKind::RGB) {
235      Some(self.iter().map(|item| item.get_fallback(ColorFallbackKind::RGB)).collect())
236    } else {
237      None
238    };
239
240    // Prefixed properties only support RGB.
241    let prefix_images = rgb.as_ref().unwrap_or(&self);
242
243    // Legacy -webkit-gradient()
244    if prefixes.contains(VendorPrefix::WebKit) && is_webkit_gradient(targets) {
245      let images: SmallVec<[T; 1]> = prefix_images
246        .iter()
247        .map(|item| item.get_image().get_legacy_webkit().map(|image| item.with_image(image)))
248        .flatten()
249        .collect();
250      if !images.is_empty() {
251        res.push(images)
252      }
253    }
254
255    // Standard syntax, with prefixes.
256    macro_rules! prefix {
257      ($prefix: ident) => {
258        if prefixes.contains(VendorPrefix::$prefix) {
259          let images = prefix_images
260            .iter()
261            .map(|item| {
262              let image = item.get_image().get_prefixed(VendorPrefix::$prefix);
263              item.with_image(image)
264            })
265            .collect();
266          res.push(images)
267        }
268      };
269    }
270
271    prefix!(WebKit);
272    prefix!(Moz);
273    prefix!(O);
274    if prefixes.contains(VendorPrefix::None) {
275      if let Some(rgb) = rgb {
276        res.push(rgb);
277      }
278
279      if fallbacks.contains(ColorFallbackKind::P3) {
280        let p3_images = self.iter().map(|item| item.get_fallback(ColorFallbackKind::P3)).collect();
281
282        res.push(p3_images)
283      }
284
285      // Convert to lab if needed (e.g. if oklab is not supported but lab is).
286      if fallbacks.contains(ColorFallbackKind::LAB) {
287        for item in self.iter_mut() {
288          *item = item.get_fallback(ColorFallbackKind::LAB);
289        }
290      }
291    } else if let Some(last) = res.pop() {
292      // Prefixed property with no unprefixed version.
293      // Replace self with the last prefixed version so that it doesn't
294      // get duplicated when the caller pushes the original value.
295      *self = last;
296    }
297
298    res
299  }
300}
301
302impl<'i> Parse<'i> for Image<'i> {
303  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
304    if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
305      return Ok(Image::None);
306    }
307
308    if let Ok(url) = input.try_parse(Url::parse) {
309      return Ok(Image::Url(url));
310    }
311
312    if let Ok(grad) = input.try_parse(Gradient::parse) {
313      return Ok(Image::Gradient(Box::new(grad)));
314    }
315
316    if let Ok(image_set) = input.try_parse(ImageSet::parse) {
317      return Ok(Image::ImageSet(image_set));
318    }
319
320    Err(input.new_error_for_next_token())
321  }
322}
323
324impl<'i> ToCss for Image<'i> {
325  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
326  where
327    W: std::fmt::Write,
328  {
329    match self {
330      Image::None => dest.write_str("none"),
331      Image::Url(url) => url.to_css(dest),
332      Image::Gradient(grad) => grad.to_css(dest),
333      Image::ImageSet(image_set) => image_set.to_css(dest),
334    }
335  }
336}
337
338/// A CSS [`image-set()`](https://drafts.csswg.org/css-images-4/#image-set-notation) value.
339///
340/// `image-set()` allows the user agent to choose between multiple versions of an image to
341/// display the most appropriate resolution or file type that it supports.
342#[derive(Debug, Clone, PartialEq)]
343#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
344pub struct ImageSet<'i> {
345  /// The image options to choose from.
346  #[cfg_attr(feature = "serde", serde(borrow))]
347  pub options: Vec<ImageSetOption<'i>>,
348  /// The vendor prefix for the `image-set()` function.
349  pub vendor_prefix: VendorPrefix,
350}
351
352impl<'i> ImageSet<'i> {
353  /// Returns the vendor prefix for the `image-set()`.
354  pub fn get_vendor_prefix(&self) -> VendorPrefix {
355    self.vendor_prefix
356  }
357
358  /// Returns the vendor prefixes needed for the given browser targets.
359  pub fn get_necessary_prefixes(&self, targets: Browsers) -> VendorPrefix {
360    if self.vendor_prefix.contains(VendorPrefix::None) {
361      Feature::ImageSet.prefixes_for(targets)
362    } else {
363      self.vendor_prefix
364    }
365  }
366
367  /// Returns the `image-set()` value with the given vendor prefix.
368  pub fn get_prefixed(&self, prefix: VendorPrefix) -> ImageSet<'i> {
369    ImageSet {
370      options: self.options.clone(),
371      vendor_prefix: prefix,
372    }
373  }
374}
375
376impl<'i> Parse<'i> for ImageSet<'i> {
377  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
378    let location = input.current_source_location();
379    let f = input.expect_function()?;
380    let vendor_prefix = match_ignore_ascii_case! { &*f,
381      "image-set" => VendorPrefix::None,
382      "-webkit-image-set" => VendorPrefix::WebKit,
383      _ => return Err(location.new_unexpected_token_error(
384        cssparser::Token::Ident(f.clone())
385      ))
386    };
387
388    let options = input.parse_nested_block(|input| input.parse_comma_separated(ImageSetOption::parse))?;
389    Ok(ImageSet { options, vendor_prefix })
390  }
391}
392
393impl<'i> ToCss for ImageSet<'i> {
394  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
395  where
396    W: std::fmt::Write,
397  {
398    self.vendor_prefix.to_css(dest)?;
399    dest.write_str("image-set(")?;
400    let mut first = true;
401    for option in &self.options {
402      if first {
403        first = false;
404      } else {
405        dest.delim(',', false)?;
406      }
407      option.to_css(dest, self.vendor_prefix != VendorPrefix::None)?;
408    }
409    dest.write_char(')')
410  }
411}
412
413/// An image option within the `image-set()` function. See [ImageSet](ImageSet).
414#[derive(Debug, Clone, PartialEq)]
415#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
416pub struct ImageSetOption<'i> {
417  /// The image for this option.
418  pub image: Image<'i>,
419  /// The resolution of the image.
420  pub resolution: Resolution,
421  /// The mime type of the image.
422  #[cfg_attr(feature = "serde", serde(borrow))]
423  pub file_type: Option<CowArcStr<'i>>,
424}
425
426impl<'i> Parse<'i> for ImageSetOption<'i> {
427  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
428    let loc = input.current_source_location();
429    let image = if let Ok(url) = input.try_parse(|input| input.expect_url_or_string()) {
430      Image::Url(Url {
431        url: url.into(),
432        loc: loc.into(),
433      })
434    } else {
435      Image::parse(input)?
436    };
437
438    let (resolution, file_type) = if let Ok(res) = input.try_parse(Resolution::parse) {
439      let file_type = input.try_parse(parse_file_type).ok();
440      (res, file_type)
441    } else {
442      let file_type = input.try_parse(parse_file_type).ok();
443      let resolution = input.try_parse(Resolution::parse).unwrap_or(Resolution::Dppx(1.0));
444      (resolution, file_type)
445    };
446
447    Ok(ImageSetOption {
448      image,
449      resolution,
450      file_type: file_type.map(|x| x.into()),
451    })
452  }
453}
454
455impl<'i> ImageSetOption<'i> {
456  fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
457  where
458    W: std::fmt::Write,
459  {
460    match &self.image {
461      // Prefixed syntax didn't allow strings, only url()
462      Image::Url(url) if !is_prefixed => {
463        // Add dependency if needed. Normally this is handled by the Url type.
464        let dep = if dest.dependencies.is_some() {
465          Some(UrlDependency::new(url, dest.filename()))
466        } else {
467          None
468        };
469        if let Some(dep) = dep {
470          serialize_string(&dep.placeholder, dest)?;
471          if let Some(dependencies) = &mut dest.dependencies {
472            dependencies.push(Dependency::Url(dep))
473          }
474        } else {
475          serialize_string(&url.url, dest)?;
476        }
477      }
478      _ => self.image.to_css(dest)?,
479    }
480
481    // TODO: Throwing an error when `self.resolution = Resolution::Dppx(0.0)`
482    // TODO: -webkit-image-set() does not support `<image()> | <image-set()> |
483    // <cross-fade()> | <element()> | <gradient>` and `type(<string>)`.
484    dest.write_char(' ')?;
485
486    // Safari only supports the x resolution unit in image-set().
487    // In other places, x was added as an alias later.
488    // Temporarily ignore the targets while printing here.
489    let targets = std::mem::take(&mut dest.targets);
490    self.resolution.to_css(dest)?;
491    dest.targets = targets;
492
493    if let Some(file_type) = &self.file_type {
494      dest.write_str(" type(")?;
495      serialize_string(&file_type, dest)?;
496      dest.write_char(')')?;
497    }
498
499    Ok(())
500  }
501}
502
503fn parse_file_type<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CowRcStr<'i>, ParseError<'i, ParserError<'i>>> {
504  input.expect_function_matching("type")?;
505  input.parse_nested_block(|input| Ok(input.expect_string_cloned()?))
506}