blob: a8deccd1b2c5c77579b2259d9c09a888c699a889 [file] [log] [blame] [edit]
//! 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<W>(&self, dest: &mut Printer<W>) -> 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 [`<supports-condition>`](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::<Box<SupportsCondition>>"))]
Not(Box<SupportsCondition<'i>>),
/// An `and` expression.
#[cfg_attr(feature = "visitor", skip_type)]
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Vec<SupportsCondition>>"))]
And(Vec<SupportsCondition<'i>>),
/// An `or` expression.
#[cfg_attr(feature = "visitor", skip_type)]
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Vec<SupportsCondition>>"))]
Or(Vec<SupportsCondition<'i>>),
/// 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::<CowArcStr>"))]
Selector(CowArcStr<'i>),
// FontTechnology()
/// An unknown condition.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
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<Features> {
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<Self, ParseError<'i, ParserError<'i>>> {
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<Self, ParseError<'i, ParserError<'i>>> {
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<Self, ParseError<'i, ParserError<'i>>> {
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<W>(&self, dest: &mut Printer<W>, 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<W>(&self, dest: &mut Printer<W>) -> 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),
}
}
}