1use 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, Targets};
12use crate::traits::{FallbackValues, IsCompatible, Parse, ToCss};
13use crate::values::string::CowArcStr;
14use crate::values::url::Url;
15use crate::vendor_prefix::VendorPrefix;
16#[cfg(feature = "visitor")]
17use crate::visitor::Visit;
18use cssparser::*;
19use smallvec::SmallVec;
20
21#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
23#[cfg_attr(feature = "visitor", derive(Visit))]
24#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
25#[cfg_attr(feature = "visitor", visit(visit_image, IMAGES))]
26#[cfg_attr(
27 feature = "serde",
28 derive(serde::Serialize, serde::Deserialize),
29 serde(tag = "type", content = "value", rename_all = "kebab-case")
30)]
31#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
32pub enum Image<'i> {
33 None,
35 #[cfg_attr(feature = "serde", serde(borrow))]
37 Url(Url<'i>),
38 Gradient(Box<Gradient>),
40 ImageSet(ImageSet<'i>),
42}
43
44impl<'i> Default for Image<'i> {
45 fn default() -> Image<'i> {
46 Image::None
47 }
48}
49
50impl<'i> Image<'i> {
51 pub fn has_vendor_prefix(&self) -> bool {
53 let prefix = self.get_vendor_prefix();
54 !prefix.is_empty() && prefix != VendorPrefix::None
55 }
56
57 pub fn get_vendor_prefix(&self) -> VendorPrefix {
59 match self {
60 Image::Gradient(a) => a.get_vendor_prefix(),
61 Image::ImageSet(a) => a.get_vendor_prefix(),
62 _ => VendorPrefix::empty(),
63 }
64 }
65
66 pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
68 match self {
69 Image::Gradient(grad) => grad.get_necessary_prefixes(targets),
70 Image::ImageSet(image_set) => image_set.get_necessary_prefixes(targets),
71 _ => VendorPrefix::None,
72 }
73 }
74
75 pub fn get_prefixed(&self, prefix: VendorPrefix) -> Image<'i> {
77 match self {
78 Image::Gradient(grad) => Image::Gradient(Box::new(grad.get_prefixed(prefix))),
79 Image::ImageSet(image_set) => Image::ImageSet(image_set.get_prefixed(prefix)),
80 _ => self.clone(),
81 }
82 }
83
84 pub fn get_legacy_webkit(&self) -> Result<Image<'i>, ()> {
88 match self {
89 Image::Gradient(grad) => Ok(Image::Gradient(Box::new(grad.get_legacy_webkit()?))),
90 _ => Ok(self.clone()),
91 }
92 }
93
94 pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
96 match self {
97 Image::Gradient(grad) => grad.get_necessary_fallbacks(targets),
98 _ => ColorFallbackKind::empty(),
99 }
100 }
101
102 pub fn get_fallback(&self, kind: ColorFallbackKind) -> Image<'i> {
104 match self {
105 Image::Gradient(grad) => Image::Gradient(Box::new(grad.get_fallback(kind))),
106 _ => self.clone(),
107 }
108 }
109}
110
111impl<'i> IsCompatible for Image<'i> {
112 fn is_compatible(&self, browsers: Browsers) -> bool {
113 match self {
114 Image::Gradient(g) => match &**g {
115 Gradient::Linear(g) => {
116 compat::Feature::LinearGradient.is_compatible(browsers) && g.is_compatible(browsers)
117 }
118 Gradient::RepeatingLinear(g) => {
119 compat::Feature::RepeatingLinearGradient.is_compatible(browsers) && g.is_compatible(browsers)
120 }
121 Gradient::Radial(g) => {
122 compat::Feature::RadialGradient.is_compatible(browsers) && g.is_compatible(browsers)
123 }
124 Gradient::RepeatingRadial(g) => {
125 compat::Feature::RepeatingRadialGradient.is_compatible(browsers) && g.is_compatible(browsers)
126 }
127 Gradient::Conic(g) => compat::Feature::ConicGradient.is_compatible(browsers) && g.is_compatible(browsers),
128 Gradient::RepeatingConic(g) => {
129 compat::Feature::RepeatingConicGradient.is_compatible(browsers) && g.is_compatible(browsers)
130 }
131 Gradient::WebKitGradient(..) => is_webkit_gradient(browsers),
132 },
133 Image::ImageSet(i) => i.is_compatible(browsers),
134 Image::Url(..) | Image::None => true,
135 }
136 }
137}
138
139pub(crate) trait ImageFallback<'i>: Sized {
140 fn get_image(&self) -> &Image<'i>;
141 fn with_image(&self, image: Image<'i>) -> Self;
142
143 #[inline]
144 fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
145 self.get_image().get_necessary_fallbacks(targets)
146 }
147
148 #[inline]
149 fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
150 self.with_image(self.get_image().get_fallback(kind))
151 }
152}
153
154impl<'i> ImageFallback<'i> for Image<'i> {
155 #[inline]
156 fn get_image(&self) -> &Image<'i> {
157 self
158 }
159
160 #[inline]
161 fn with_image(&self, image: Image<'i>) -> Self {
162 image
163 }
164}
165
166impl<'i> FallbackValues for Image<'i> {
167 fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
168 let prefixes = self.get_necessary_prefixes(targets);
170 let fallbacks = self.get_necessary_fallbacks(targets);
171 let mut res = Vec::new();
172
173 let rgb = if fallbacks.contains(ColorFallbackKind::RGB) {
175 Some(self.get_fallback(ColorFallbackKind::RGB))
176 } else {
177 None
178 };
179
180 let prefix_image = rgb.as_ref().unwrap_or(self);
182
183 if prefixes.contains(VendorPrefix::WebKit)
185 && targets.browsers.map(is_webkit_gradient).unwrap_or(false)
186 && matches!(prefix_image, Image::Gradient(_))
187 {
188 if let Ok(legacy) = prefix_image.get_legacy_webkit() {
189 res.push(legacy);
190 }
191 }
192
193 if prefixes.contains(VendorPrefix::WebKit) {
195 res.push(prefix_image.get_prefixed(VendorPrefix::WebKit))
196 }
197
198 if prefixes.contains(VendorPrefix::Moz) {
199 res.push(prefix_image.get_prefixed(VendorPrefix::Moz))
200 }
201
202 if prefixes.contains(VendorPrefix::O) {
203 res.push(prefix_image.get_prefixed(VendorPrefix::O))
204 }
205
206 if prefixes.contains(VendorPrefix::None) {
207 if let Some(rgb) = rgb {
209 res.push(rgb);
210 }
211
212 if fallbacks.contains(ColorFallbackKind::P3) {
214 res.push(self.get_fallback(ColorFallbackKind::P3));
215 }
216
217 if fallbacks.contains(ColorFallbackKind::LAB) {
219 *self = self.get_fallback(ColorFallbackKind::LAB);
220 }
221 } else if let Some(last) = res.pop() {
222 *self = last;
226 }
227
228 res
229 }
230}
231
232impl<'i, T: ImageFallback<'i>> FallbackValues for SmallVec<[T; 1]> {
233 fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
234 let mut prefixes = VendorPrefix::empty();
236 let mut fallbacks = ColorFallbackKind::empty();
237 let mut res = Vec::new();
238 for item in self.iter() {
239 prefixes |= item.get_image().get_necessary_prefixes(targets);
240 fallbacks |= item.get_necessary_fallbacks(targets);
241 }
242
243 let rgb: Option<SmallVec<[T; 1]>> = if fallbacks.contains(ColorFallbackKind::RGB) {
245 Some(self.iter().map(|item| item.get_fallback(ColorFallbackKind::RGB)).collect())
246 } else {
247 None
248 };
249
250 let prefix_images = rgb.as_ref().unwrap_or(&self);
252
253 if prefixes.contains(VendorPrefix::WebKit) && targets.browsers.map(is_webkit_gradient).unwrap_or(false) {
255 let images: SmallVec<[T; 1]> = prefix_images
256 .iter()
257 .map(|item| item.get_image().get_legacy_webkit().map(|image| item.with_image(image)))
258 .flatten()
259 .collect();
260 if !images.is_empty() {
261 res.push(images)
262 }
263 }
264
265 macro_rules! prefix {
267 ($prefix: ident) => {
268 if prefixes.contains(VendorPrefix::$prefix) {
269 let images = prefix_images
270 .iter()
271 .map(|item| {
272 let image = item.get_image().get_prefixed(VendorPrefix::$prefix);
273 item.with_image(image)
274 })
275 .collect();
276 res.push(images)
277 }
278 };
279 }
280
281 prefix!(WebKit);
282 prefix!(Moz);
283 prefix!(O);
284 if prefixes.contains(VendorPrefix::None) {
285 if let Some(rgb) = rgb {
286 res.push(rgb);
287 }
288
289 if fallbacks.contains(ColorFallbackKind::P3) {
290 let p3_images = self.iter().map(|item| item.get_fallback(ColorFallbackKind::P3)).collect();
291
292 res.push(p3_images)
293 }
294
295 if fallbacks.contains(ColorFallbackKind::LAB) {
297 for item in self.iter_mut() {
298 *item = item.get_fallback(ColorFallbackKind::LAB);
299 }
300 }
301 } else if let Some(last) = res.pop() {
302 *self = last;
306 }
307
308 res
309 }
310}
311
312#[derive(Debug, Clone, PartialEq)]
317#[cfg_attr(feature = "visitor", derive(Visit))]
318#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
319#[cfg_attr(
320 feature = "serde",
321 derive(serde::Serialize, serde::Deserialize),
322 serde(rename_all = "camelCase")
323)]
324#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
325pub struct ImageSet<'i> {
326 #[cfg_attr(feature = "serde", serde(borrow))]
328 pub options: Vec<ImageSetOption<'i>>,
329 pub vendor_prefix: VendorPrefix,
331}
332
333impl<'i> ImageSet<'i> {
334 pub fn get_vendor_prefix(&self) -> VendorPrefix {
336 self.vendor_prefix
337 }
338
339 pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
341 targets.prefixes(self.vendor_prefix, Feature::ImageSet)
342 }
343
344 pub fn get_prefixed(&self, prefix: VendorPrefix) -> ImageSet<'i> {
346 ImageSet {
347 options: self.options.clone(),
348 vendor_prefix: prefix,
349 }
350 }
351}
352
353impl<'i> Parse<'i> for ImageSet<'i> {
354 fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
355 let location = input.current_source_location();
356 let f = input.expect_function()?;
357 let vendor_prefix = match_ignore_ascii_case! { &*f,
358 "image-set" => VendorPrefix::None,
359 "-webkit-image-set" => VendorPrefix::WebKit,
360 _ => return Err(location.new_unexpected_token_error(
361 cssparser::Token::Ident(f.clone())
362 ))
363 };
364
365 let options = input.parse_nested_block(|input| input.parse_comma_separated(ImageSetOption::parse))?;
366 Ok(ImageSet { options, vendor_prefix })
367 }
368}
369
370impl<'i> ToCss for ImageSet<'i> {
371 fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
372 where
373 W: std::fmt::Write,
374 {
375 self.vendor_prefix.to_css(dest)?;
376 dest.write_str("image-set(")?;
377 let mut first = true;
378 for option in &self.options {
379 if first {
380 first = false;
381 } else {
382 dest.delim(',', false)?;
383 }
384 option.to_css(dest, self.vendor_prefix != VendorPrefix::None)?;
385 }
386 dest.write_char(')')
387 }
388}
389
390impl<'i> IsCompatible for ImageSet<'i> {
391 fn is_compatible(&self, browsers: Browsers) -> bool {
392 compat::Feature::ImageSet.is_compatible(browsers)
393 && self.options.iter().all(|opt| opt.image.is_compatible(browsers))
394 }
395}
396
397#[derive(Debug, Clone, PartialEq)]
399#[cfg_attr(feature = "visitor", derive(Visit))]
400#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
401#[cfg_attr(
402 feature = "serde",
403 derive(serde::Serialize, serde::Deserialize),
404 serde(rename_all = "camelCase")
405)]
406#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
407pub struct ImageSetOption<'i> {
408 #[cfg_attr(feature = "visitor", skip_type)]
410 pub image: Image<'i>,
411 pub resolution: Resolution,
413 #[cfg_attr(feature = "serde", serde(borrow))]
415 pub file_type: Option<CowArcStr<'i>>,
416}
417
418impl<'i> Parse<'i> for ImageSetOption<'i> {
419 fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
420 let loc = input.current_source_location();
421 let image = if let Ok(url) = input.try_parse(|input| input.expect_url_or_string()) {
422 Image::Url(Url {
423 url: url.into(),
424 loc: loc.into(),
425 })
426 } else {
427 Image::parse(input)?
428 };
429
430 let (resolution, file_type) = if let Ok(res) = input.try_parse(Resolution::parse) {
431 let file_type = input.try_parse(parse_file_type).ok();
432 (res, file_type)
433 } else {
434 let file_type = input.try_parse(parse_file_type).ok();
435 let resolution = input.try_parse(Resolution::parse).unwrap_or(Resolution::Dppx(1.0));
436 (resolution, file_type)
437 };
438
439 Ok(ImageSetOption {
440 image,
441 resolution,
442 file_type: file_type.map(|x| x.into()),
443 })
444 }
445}
446
447impl<'i> ImageSetOption<'i> {
448 fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
449 where
450 W: std::fmt::Write,
451 {
452 match &self.image {
453 Image::Url(url) if !is_prefixed => {
455 let dep = if dest.dependencies.is_some() {
457 Some(UrlDependency::new(url, dest.filename()))
458 } else {
459 None
460 };
461 if let Some(dep) = dep {
462 serialize_string(&dep.placeholder, dest)?;
463 if let Some(dependencies) = &mut dest.dependencies {
464 dependencies.push(Dependency::Url(dep))
465 }
466 } else {
467 serialize_string(&url.url, dest)?;
468 }
469 }
470 _ => self.image.to_css(dest)?,
471 }
472
473 dest.write_char(' ')?;
477
478 let targets = std::mem::take(&mut dest.targets);
482 self.resolution.to_css(dest)?;
483 dest.targets = targets;
484
485 if let Some(file_type) = &self.file_type {
486 dest.write_str(" type(")?;
487 serialize_string(&file_type, dest)?;
488 dest.write_char(')')?;
489 }
490
491 Ok(())
492 }
493}
494
495fn parse_file_type<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CowRcStr<'i>, ParseError<'i, ParserError<'i>>> {
496 input.expect_function_matching("type")?;
497 input.parse_nested_block(|input| Ok(input.expect_string_cloned()?))
498}