//! The `@supports` rule. use std::collections::HashMap; use super::Location; use super::{CssRuleList, MinifyContext}; use crate::error::{MinifyError, ParserError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; use crate::properties::custom::TokenList; use crate::properties::PropertyId; use crate::targets::{Features, FeaturesIterator, Targets}; use crate::traits::{Parse, ToCss}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] use crate::visitor::Visit; use cssparser::*; #[cfg(feature = "serde")] use crate::serialization::ValueWrapper; /// A [@supports](https://drafts.csswg.org/css-conditional-3/#at-supports) rule. #[derive(Debug, PartialEq, Clone)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub struct SupportsRule<'i, R = DefaultAtRule> { /// The supports condition. #[cfg_attr(feature = "serde", serde(borrow))] pub condition: SupportsCondition<'i>, /// The rules within the `@supports` rule. pub rules: CssRuleList<'i, R>, /// The location of the rule in the source file. #[cfg_attr(feature = "visitor", skip_visit)] pub loc: Location, } impl<'i, T: Clone> SupportsRule<'i, T> { pub(crate) fn minify( &mut self, context: &mut MinifyContext<'_, 'i>, parent_is_unused: bool, ) -> Result<(), MinifyError> { let inserted = context.targets.enter_supports(self.condition.get_supported_features()); if inserted { context.handler_context.targets = context.targets.current; } self.condition.set_prefixes_for_targets(&context.targets.current); let result = self.rules.minify(context, parent_is_unused); if inserted { context.targets.exit_supports(); context.handler_context.targets = context.targets.current; } result } } impl<'a, 'i, T: ToCss> ToCss for SupportsRule<'i, T> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { #[cfg(feature = "sourcemap")] dest.add_mapping(self.loc); dest.write_str("@supports ")?; self.condition.to_css(dest)?; dest.whitespace()?; dest.write_char('{')?; dest.indent(); dest.newline()?; let inserted = dest.targets.enter_supports(self.condition.get_supported_features()); self.rules.to_css(dest)?; if inserted { dest.targets.exit_supports(); } dest.dedent(); dest.newline()?; dest.write_char('}') } } /// A [``](https://drafts.csswg.org/css-conditional-3/#typedef-supports-condition), /// as used in the `@supports` and `@import` rules. #[derive(Debug, PartialEq, Clone)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] #[cfg_attr(feature = "visitor", visit(visit_supports_condition, SUPPORTS_CONDITIONS))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum SupportsCondition<'i> { /// A `not` expression. #[cfg_attr(feature = "visitor", skip_type)] #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::>"))] Not(Box>), /// An `and` expression. #[cfg_attr(feature = "visitor", skip_type)] #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::>"))] And(Vec>), /// An `or` expression. #[cfg_attr(feature = "visitor", skip_type)] #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::>"))] Or(Vec>), /// A declaration to evaluate. Declaration { /// The property id for the declaration. #[cfg_attr(feature = "serde", serde(borrow, rename = "propertyId"))] property_id: PropertyId<'i>, /// The raw value of the declaration. value: CowArcStr<'i>, }, /// A selector to evaluate. #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::"))] Selector(CowArcStr<'i>), // FontTechnology() /// An unknown condition. #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::"))] Unknown(CowArcStr<'i>), } impl<'i> SupportsCondition<'i> { /// Combines the given supports condition into this one with an `and` expression. pub fn and(&mut self, b: &SupportsCondition<'i>) { if let SupportsCondition::And(a) = self { if !a.contains(&b) { a.push(b.clone()); } } else if self != b { *self = SupportsCondition::And(vec![self.clone(), b.clone()]) } } /// Combines the given supports condition into this one with an `or` expression. pub fn or(&mut self, b: &SupportsCondition<'i>) { if let SupportsCondition::Or(a) = self { if !a.contains(&b) { a.push(b.clone()); } } else if self != b { *self = SupportsCondition::Or(vec![self.clone(), b.clone()]) } } fn set_prefixes_for_targets(&mut self, targets: &Targets) { match self { SupportsCondition::Not(cond) => cond.set_prefixes_for_targets(targets), SupportsCondition::And(items) | SupportsCondition::Or(items) => { for item in items { item.set_prefixes_for_targets(targets); } } SupportsCondition::Declaration { property_id, .. } => { let prefix = property_id.prefix(); if prefix.is_empty() || prefix.contains(VendorPrefix::None) { property_id.set_prefixes_for_targets(*targets); } } _ => {} } } fn get_supported_features(&self) -> Features { fn get_supported_features_internal(value: &SupportsCondition) -> Option { match value { SupportsCondition::And(list) => list.iter().map(|c| get_supported_features_internal(c)).try_union_all(), SupportsCondition::Declaration { value, .. } => { let mut input = ParserInput::new(&value); let mut parser = Parser::new(&mut input); if let Ok(tokens) = TokenList::parse(&mut parser, &Default::default(), 0) { Some(tokens.get_features()) } else { Some(Features::empty()) } } // bail out if "not" or "or" exists for now SupportsCondition::Not(_) | SupportsCondition::Or(_) => None, SupportsCondition::Selector(_) | SupportsCondition::Unknown(_) => Some(Features::empty()), } } get_supported_features_internal(self).unwrap_or(Features::empty()) } } impl<'i> Parse<'i> for SupportsCondition<'i> { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { if input.try_parse(|input| input.expect_ident_matching("not")).is_ok() { let in_parens = Self::parse_in_parens(input)?; return Ok(SupportsCondition::Not(Box::new(in_parens))); } let in_parens = Self::parse_in_parens(input)?; let mut expected_type = None; let mut conditions = Vec::new(); let mut seen_declarations = HashMap::new(); loop { let condition = input.try_parse(|input| { let location = input.current_source_location(); let s = input.expect_ident()?; let found_type = match_ignore_ascii_case! { &s, "and" => 1, "or" => 2, _ => return Err(location.new_unexpected_token_error( cssparser::Token::Ident(s.clone()) )) }; if let Some(expected) = expected_type { if found_type != expected { return Err(location.new_unexpected_token_error(cssparser::Token::Ident(s.clone()))); } } else { expected_type = Some(found_type); } Self::parse_in_parens(input) }); if let Ok(condition) = condition { if conditions.is_empty() { conditions.push(in_parens.clone()); if let SupportsCondition::Declaration { property_id, value } = &in_parens { seen_declarations.insert((property_id.with_prefix(VendorPrefix::None), value.clone()), 0); } } if let SupportsCondition::Declaration { property_id, value } = condition { // Merge multiple declarations with the same property id (minus prefix) and value together. let property_id = property_id.with_prefix(VendorPrefix::None); let key = (property_id.clone(), value.clone()); if let Some(index) = seen_declarations.get(&key) { if let SupportsCondition::Declaration { property_id: cur_property, .. } = &mut conditions[*index] { cur_property.add_prefix(property_id.prefix()); } } else { seen_declarations.insert(key, conditions.len()); conditions.push(SupportsCondition::Declaration { property_id, value }); } } else { conditions.push(condition); } } else { break; } } if conditions.len() == 1 { return Ok(conditions.pop().unwrap()); } match expected_type { Some(1) => Ok(SupportsCondition::And(conditions)), Some(2) => Ok(SupportsCondition::Or(conditions)), _ => Ok(in_parens), } } } impl<'i> SupportsCondition<'i> { fn parse_in_parens<'t>(input: &mut Parser<'i, 't>) -> Result>> { input.skip_whitespace(); let location = input.current_source_location(); let pos = input.position(); match input.next()? { Token::Function(ref f) => { match_ignore_ascii_case! { &*f, "selector" => { let res = input.try_parse(|input| { input.parse_nested_block(|input| { let pos = input.position(); input.expect_no_error_token()?; Ok(SupportsCondition::Selector(input.slice_from(pos).into())) }) }); if res.is_ok() { return res } }, _ => {} } } Token::ParenthesisBlock => { let res = input.try_parse(|input| { input.parse_nested_block(|input| { if let Ok(condition) = input.try_parse(SupportsCondition::parse) { return Ok(condition); } Self::parse_declaration(input) }) }); if res.is_ok() { return res; } } t => return Err(location.new_unexpected_token_error(t.clone())), }; input.parse_nested_block(|input| input.expect_no_error_token().map_err(|err| err.into()))?; Ok(SupportsCondition::Unknown(input.slice_from(pos).into())) } pub(crate) fn parse_declaration<'t>( input: &mut Parser<'i, 't>, ) -> Result>> { let property_id = PropertyId::parse(input)?; input.expect_colon()?; input.skip_whitespace(); let pos = input.position(); input.expect_no_error_token()?; Ok(SupportsCondition::Declaration { property_id, value: input.slice_from(pos).into(), }) } fn needs_parens(&self, parent: &SupportsCondition) -> bool { match self { SupportsCondition::Not(_) => true, SupportsCondition::And(_) => !matches!(parent, SupportsCondition::And(_)), SupportsCondition::Or(_) => !matches!(parent, SupportsCondition::Or(_)), _ => false, } } fn to_css_with_parens_if_needed(&self, dest: &mut Printer, needs_parens: bool) -> Result<(), PrinterError> where W: std::fmt::Write, { if needs_parens { dest.write_char('(')?; } self.to_css(dest)?; if needs_parens { dest.write_char(')')?; } Ok(()) } } impl<'i> ToCss for SupportsCondition<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { SupportsCondition::Not(condition) => { dest.write_str("not ")?; condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self)) } SupportsCondition::And(conditions) => { let mut first = true; for condition in conditions { if first { first = false; } else { dest.write_str(" and ")?; } condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self))?; } Ok(()) } SupportsCondition::Or(conditions) => { let mut first = true; for condition in conditions { if first { first = false; } else { dest.write_str(" or ")?; } condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self))?; } Ok(()) } SupportsCondition::Declaration { property_id, value } => { dest.write_char('(')?; let prefix = property_id.prefix().or_none(); if prefix != VendorPrefix::None { dest.write_char('(')?; } let name = property_id.name(); let mut first = true; for p in prefix { if first { first = false; } else { dest.write_str(") or (")?; } p.to_css(dest)?; serialize_name(name, dest)?; dest.delim(':', false)?; dest.write_str(value)?; } if prefix != VendorPrefix::None { dest.write_char(')')?; } dest.write_char(')') } SupportsCondition::Selector(sel) => { dest.write_str("selector(")?; dest.write_str(sel)?; dest.write_char(')') } SupportsCondition::Unknown(unknown) => dest.write_str(&unknown), } } }