blob: 4964c98c94aad4a6878ca4b2b5cd52499167828c [file] [log] [blame]
//! CSS image values.
use super::color::ColorFallbackKind;
use super::gradient::*;
use super::resolution::Resolution;
use crate::compat;
use crate::dependencies::{Dependency, UrlDependency};
use crate::error::{ParserError, PrinterError};
use crate::prefixes::{is_webkit_gradient, Feature};
use crate::printer::Printer;
use crate::targets::{Browsers, Targets};
use crate::traits::{FallbackValues, IsCompatible, Parse, ToCss};
use crate::values::string::CowArcStr;
use crate::values::url::Url;
use crate::vendor_prefix::VendorPrefix;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;
use smallvec::SmallVec;
/// A CSS [`<image>`](https://www.w3.org/TR/css-images-3/#image-values) value.
#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "visitor", visit(visit_image, IMAGES))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum Image<'i> {
/// The `none` keyword.
None,
/// A `url()`.
#[cfg_attr(feature = "serde", serde(borrow))]
Url(Url<'i>),
/// A gradient.
Gradient(Box<Gradient>),
/// An `image-set()`.
ImageSet(ImageSet<'i>),
}
impl<'i> Default for Image<'i> {
fn default() -> Image<'i> {
Image::None
}
}
impl<'i> Image<'i> {
/// Returns whether the image includes any vendor prefixed values.
pub fn has_vendor_prefix(&self) -> bool {
let prefix = self.get_vendor_prefix();
!prefix.is_empty() && prefix != VendorPrefix::None
}
/// Returns the vendor prefix used in the image value.
pub fn get_vendor_prefix(&self) -> VendorPrefix {
match self {
Image::Gradient(a) => a.get_vendor_prefix(),
Image::ImageSet(a) => a.get_vendor_prefix(),
_ => VendorPrefix::empty(),
}
}
/// Returns the vendor prefixes that are needed for the given browser targets.
pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
match self {
Image::Gradient(grad) => grad.get_necessary_prefixes(targets),
Image::ImageSet(image_set) => image_set.get_necessary_prefixes(targets),
_ => VendorPrefix::None,
}
}
/// Returns a vendor prefixed version of the image for the given vendor prefixes.
pub fn get_prefixed(&self, prefix: VendorPrefix) -> Image<'i> {
match self {
Image::Gradient(grad) => Image::Gradient(Box::new(grad.get_prefixed(prefix))),
Image::ImageSet(image_set) => Image::ImageSet(image_set.get_prefixed(prefix)),
_ => self.clone(),
}
}
/// Returns a legacy `-webkit-gradient()` value for the image.
///
/// May return an error in case the gradient cannot be converted.
pub fn get_legacy_webkit(&self) -> Result<Image<'i>, ()> {
match self {
Image::Gradient(grad) => Ok(Image::Gradient(Box::new(grad.get_legacy_webkit()?))),
_ => Ok(self.clone()),
}
}
/// Returns the color fallbacks that are needed for the given browser targets.
pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
match self {
Image::Gradient(grad) => grad.get_necessary_fallbacks(targets),
_ => ColorFallbackKind::empty(),
}
}
/// Returns a fallback version of the image for the given color fallback type.
pub fn get_fallback(&self, kind: ColorFallbackKind) -> Image<'i> {
match self {
Image::Gradient(grad) => Image::Gradient(Box::new(grad.get_fallback(kind))),
_ => self.clone(),
}
}
}
impl<'i> IsCompatible for Image<'i> {
fn is_compatible(&self, browsers: Browsers) -> bool {
match self {
Image::Gradient(g) => match &**g {
Gradient::Linear(g) => {
compat::Feature::LinearGradient.is_compatible(browsers) && g.is_compatible(browsers)
}
Gradient::RepeatingLinear(g) => {
compat::Feature::RepeatingLinearGradient.is_compatible(browsers) && g.is_compatible(browsers)
}
Gradient::Radial(g) => {
compat::Feature::RadialGradient.is_compatible(browsers) && g.is_compatible(browsers)
}
Gradient::RepeatingRadial(g) => {
compat::Feature::RepeatingRadialGradient.is_compatible(browsers) && g.is_compatible(browsers)
}
Gradient::Conic(g) => compat::Feature::ConicGradient.is_compatible(browsers) && g.is_compatible(browsers),
Gradient::RepeatingConic(g) => {
compat::Feature::RepeatingConicGradient.is_compatible(browsers) && g.is_compatible(browsers)
}
Gradient::WebKitGradient(..) => is_webkit_gradient(browsers),
},
Image::ImageSet(i) => i.is_compatible(browsers),
Image::Url(..) | Image::None => true,
}
}
}
pub(crate) trait ImageFallback<'i>: Sized {
fn get_image(&self) -> &Image<'i>;
fn with_image(&self, image: Image<'i>) -> Self;
#[inline]
fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
self.get_image().get_necessary_fallbacks(targets)
}
#[inline]
fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
self.with_image(self.get_image().get_fallback(kind))
}
}
impl<'i> ImageFallback<'i> for Image<'i> {
#[inline]
fn get_image(&self) -> &Image<'i> {
self
}
#[inline]
fn with_image(&self, image: Image<'i>) -> Self {
image
}
}
impl<'i> FallbackValues for Image<'i> {
fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
// Determine which prefixes and color fallbacks are needed.
let prefixes = self.get_necessary_prefixes(targets);
let fallbacks = self.get_necessary_fallbacks(targets);
let mut res = Vec::new();
// Get RGB fallbacks if needed.
let rgb = if fallbacks.contains(ColorFallbackKind::RGB) {
Some(self.get_fallback(ColorFallbackKind::RGB))
} else {
None
};
// Prefixed properties only support RGB.
let prefix_image = rgb.as_ref().unwrap_or(self);
// Legacy -webkit-gradient()
if prefixes.contains(VendorPrefix::WebKit)
&& targets.browsers.map(is_webkit_gradient).unwrap_or(false)
&& matches!(prefix_image, Image::Gradient(_))
{
if let Ok(legacy) = prefix_image.get_legacy_webkit() {
res.push(legacy);
}
}
// Standard syntax, with prefixes.
if prefixes.contains(VendorPrefix::WebKit) {
res.push(prefix_image.get_prefixed(VendorPrefix::WebKit))
}
if prefixes.contains(VendorPrefix::Moz) {
res.push(prefix_image.get_prefixed(VendorPrefix::Moz))
}
if prefixes.contains(VendorPrefix::O) {
res.push(prefix_image.get_prefixed(VendorPrefix::O))
}
if prefixes.contains(VendorPrefix::None) {
// Unprefixed, rgb fallback.
if let Some(rgb) = rgb {
res.push(rgb);
}
// P3 fallback.
if fallbacks.contains(ColorFallbackKind::P3) {
res.push(self.get_fallback(ColorFallbackKind::P3));
}
// Convert original to lab if needed (e.g. if oklab is not supported but lab is).
if fallbacks.contains(ColorFallbackKind::LAB) {
*self = self.get_fallback(ColorFallbackKind::LAB);
}
} else if let Some(last) = res.pop() {
// Prefixed property with no unprefixed version.
// Replace self with the last prefixed version so that it doesn't
// get duplicated when the caller pushes the original value.
*self = last;
}
res
}
}
impl<'i, T: ImageFallback<'i>> FallbackValues for SmallVec<[T; 1]> {
fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
// Determine what vendor prefixes and color fallbacks are needed.
let mut prefixes = VendorPrefix::empty();
let mut fallbacks = ColorFallbackKind::empty();
let mut res = Vec::new();
for item in self.iter() {
prefixes |= item.get_image().get_necessary_prefixes(targets);
fallbacks |= item.get_necessary_fallbacks(targets);
}
// Get RGB fallbacks if needed.
let rgb: Option<SmallVec<[T; 1]>> = if fallbacks.contains(ColorFallbackKind::RGB) {
Some(self.iter().map(|item| item.get_fallback(ColorFallbackKind::RGB)).collect())
} else {
None
};
// Prefixed properties only support RGB.
let prefix_images = rgb.as_ref().unwrap_or(&self);
// Legacy -webkit-gradient()
if prefixes.contains(VendorPrefix::WebKit) && targets.browsers.map(is_webkit_gradient).unwrap_or(false) {
let images: SmallVec<[T; 1]> = prefix_images
.iter()
.map(|item| item.get_image().get_legacy_webkit().map(|image| item.with_image(image)))
.flatten()
.collect();
if !images.is_empty() {
res.push(images)
}
}
// Standard syntax, with prefixes.
macro_rules! prefix {
($prefix: ident) => {
if prefixes.contains(VendorPrefix::$prefix) {
let images = prefix_images
.iter()
.map(|item| {
let image = item.get_image().get_prefixed(VendorPrefix::$prefix);
item.with_image(image)
})
.collect();
res.push(images)
}
};
}
prefix!(WebKit);
prefix!(Moz);
prefix!(O);
if prefixes.contains(VendorPrefix::None) {
if let Some(rgb) = rgb {
res.push(rgb);
}
if fallbacks.contains(ColorFallbackKind::P3) {
let p3_images = self.iter().map(|item| item.get_fallback(ColorFallbackKind::P3)).collect();
res.push(p3_images)
}
// Convert to lab if needed (e.g. if oklab is not supported but lab is).
if fallbacks.contains(ColorFallbackKind::LAB) {
for item in self.iter_mut() {
*item = item.get_fallback(ColorFallbackKind::LAB);
}
}
} else if let Some(last) = res.pop() {
// Prefixed property with no unprefixed version.
// Replace self with the last prefixed version so that it doesn't
// get duplicated when the caller pushes the original value.
*self = last;
}
res
}
}
/// A CSS [`image-set()`](https://drafts.csswg.org/css-images-4/#image-set-notation) value.
///
/// `image-set()` allows the user agent to choose between multiple versions of an image to
/// display the most appropriate resolution or file type that it supports.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct ImageSet<'i> {
/// The image options to choose from.
#[cfg_attr(feature = "serde", serde(borrow))]
pub options: Vec<ImageSetOption<'i>>,
/// The vendor prefix for the `image-set()` function.
pub vendor_prefix: VendorPrefix,
}
impl<'i> ImageSet<'i> {
/// Returns the vendor prefix for the `image-set()`.
pub fn get_vendor_prefix(&self) -> VendorPrefix {
self.vendor_prefix
}
/// Returns the vendor prefixes needed for the given browser targets.
pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
targets.prefixes(self.vendor_prefix, Feature::ImageSet)
}
/// Returns the `image-set()` value with the given vendor prefix.
pub fn get_prefixed(&self, prefix: VendorPrefix) -> ImageSet<'i> {
ImageSet {
options: self.options.clone(),
vendor_prefix: prefix,
}
}
}
impl<'i> Parse<'i> for ImageSet<'i> {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let location = input.current_source_location();
let f = input.expect_function()?;
let vendor_prefix = match_ignore_ascii_case! { &*f,
"image-set" => VendorPrefix::None,
"-webkit-image-set" => VendorPrefix::WebKit,
_ => return Err(location.new_unexpected_token_error(
cssparser::Token::Ident(f.clone())
))
};
let options = input.parse_nested_block(|input| input.parse_comma_separated(ImageSetOption::parse))?;
Ok(ImageSet { options, vendor_prefix })
}
}
impl<'i> ToCss for ImageSet<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
self.vendor_prefix.to_css(dest)?;
dest.write_str("image-set(")?;
let mut first = true;
for option in &self.options {
if first {
first = false;
} else {
dest.delim(',', false)?;
}
option.to_css(dest, self.vendor_prefix != VendorPrefix::None)?;
}
dest.write_char(')')
}
}
impl<'i> IsCompatible for ImageSet<'i> {
fn is_compatible(&self, browsers: Browsers) -> bool {
compat::Feature::ImageSet.is_compatible(browsers)
&& self.options.iter().all(|opt| opt.image.is_compatible(browsers))
}
}
/// An image option within the `image-set()` function. See [ImageSet](ImageSet).
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct ImageSetOption<'i> {
/// The image for this option.
#[cfg_attr(feature = "visitor", skip_type)]
pub image: Image<'i>,
/// The resolution of the image.
pub resolution: Resolution,
/// The mime type of the image.
#[cfg_attr(feature = "serde", serde(borrow))]
pub file_type: Option<CowArcStr<'i>>,
}
impl<'i> Parse<'i> for ImageSetOption<'i> {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let loc = input.current_source_location();
let image = if let Ok(url) = input.try_parse(|input| input.expect_url_or_string()) {
Image::Url(Url {
url: url.into(),
loc: loc.into(),
})
} else {
Image::parse(input)?
};
let (resolution, file_type) = if let Ok(res) = input.try_parse(Resolution::parse) {
let file_type = input.try_parse(parse_file_type).ok();
(res, file_type)
} else {
let file_type = input.try_parse(parse_file_type).ok();
let resolution = input.try_parse(Resolution::parse).unwrap_or(Resolution::Dppx(1.0));
(resolution, file_type)
};
Ok(ImageSetOption {
image,
resolution,
file_type: file_type.map(|x| x.into()),
})
}
}
impl<'i> ImageSetOption<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match &self.image {
// Prefixed syntax didn't allow strings, only url()
Image::Url(url) if !is_prefixed => {
// Add dependency if needed. Normally this is handled by the Url type.
let dep = if dest.dependencies.is_some() {
Some(UrlDependency::new(url, dest.filename()))
} else {
None
};
if let Some(dep) = dep {
serialize_string(&dep.placeholder, dest)?;
if let Some(dependencies) = &mut dest.dependencies {
dependencies.push(Dependency::Url(dep))
}
} else {
serialize_string(&url.url, dest)?;
}
}
_ => self.image.to_css(dest)?,
}
// TODO: Throwing an error when `self.resolution = Resolution::Dppx(0.0)`
// TODO: -webkit-image-set() does not support `<image()> | <image-set()> |
// <cross-fade()> | <element()> | <gradient>` and `type(<string>)`.
dest.write_char(' ')?;
// Safari only supports the x resolution unit in image-set().
// In other places, x was added as an alias later.
// Temporarily ignore the targets while printing here.
let targets = std::mem::take(&mut dest.targets.current);
self.resolution.to_css(dest)?;
dest.targets.current = targets;
if let Some(file_type) = &self.file_type {
dest.write_str(" type(")?;
serialize_string(&file_type, dest)?;
dest.write_char(')')?;
}
Ok(())
}
}
fn parse_file_type<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CowRcStr<'i>, ParseError<'i, ParserError<'i>>> {
input.expect_function_matching("type")?;
input.parse_nested_block(|input| Ok(input.expect_string_cloned()?))
}