lightningcss/rules/
supports.rs

1//! The `@supports` rule.
2
3use std::collections::HashMap;
4
5use super::Location;
6use super::{CssRuleList, MinifyContext};
7use crate::error::{MinifyError, ParserError, PrinterError};
8use crate::parser::DefaultAtRule;
9use crate::printer::Printer;
10use crate::properties::PropertyId;
11use crate::targets::Targets;
12use crate::traits::{Parse, ToCss};
13use crate::values::string::CowArcStr;
14use crate::vendor_prefix::VendorPrefix;
15#[cfg(feature = "visitor")]
16use crate::visitor::Visit;
17use cssparser::*;
18
19#[cfg(feature = "serde")]
20use crate::serialization::ValueWrapper;
21
22/// A [@supports](https://drafts.csswg.org/css-conditional-3/#at-supports) rule.
23#[derive(Debug, PartialEq, Clone)]
24#[cfg_attr(feature = "visitor", derive(Visit))]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
27#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
28pub struct SupportsRule<'i, R = DefaultAtRule> {
29  /// The supports condition.
30  #[cfg_attr(feature = "serde", serde(borrow))]
31  pub condition: SupportsCondition<'i>,
32  /// The rules within the `@supports` rule.
33  pub rules: CssRuleList<'i, R>,
34  /// The location of the rule in the source file.
35  #[cfg_attr(feature = "visitor", skip_visit)]
36  pub loc: Location,
37}
38
39impl<'i, T: Clone> SupportsRule<'i, T> {
40  pub(crate) fn minify(
41    &mut self,
42    context: &mut MinifyContext<'_, 'i>,
43    parent_is_unused: bool,
44  ) -> Result<(), MinifyError> {
45    self.condition.set_prefixes_for_targets(&context.targets);
46    self.rules.minify(context, parent_is_unused)
47  }
48}
49
50impl<'a, 'i, T: ToCss> ToCss for SupportsRule<'i, T> {
51  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
52  where
53    W: std::fmt::Write,
54  {
55    #[cfg(feature = "sourcemap")]
56    dest.add_mapping(self.loc);
57    dest.write_str("@supports ")?;
58    self.condition.to_css(dest)?;
59    dest.whitespace()?;
60    dest.write_char('{')?;
61    dest.indent();
62    dest.newline()?;
63    self.rules.to_css(dest)?;
64    dest.dedent();
65    dest.newline()?;
66    dest.write_char('}')
67  }
68}
69
70/// A [`<supports-condition>`](https://drafts.csswg.org/css-conditional-3/#typedef-supports-condition),
71/// as used in the `@supports` and `@import` rules.
72#[derive(Debug, PartialEq, Clone)]
73#[cfg_attr(feature = "visitor", derive(Visit))]
74#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
75#[cfg_attr(feature = "visitor", visit(visit_supports_condition, SUPPORTS_CONDITIONS))]
76#[cfg_attr(
77  feature = "serde",
78  derive(serde::Serialize, serde::Deserialize),
79  serde(tag = "type", rename_all = "kebab-case")
80)]
81#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
82pub enum SupportsCondition<'i> {
83  /// A `not` expression.
84  #[cfg_attr(feature = "visitor", skip_type)]
85  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<SupportsCondition>>"))]
86  Not(Box<SupportsCondition<'i>>),
87  /// An `and` expression.
88  #[cfg_attr(feature = "visitor", skip_type)]
89  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Vec<SupportsCondition>>"))]
90  And(Vec<SupportsCondition<'i>>),
91  /// An `or` expression.
92  #[cfg_attr(feature = "visitor", skip_type)]
93  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Vec<SupportsCondition>>"))]
94  Or(Vec<SupportsCondition<'i>>),
95  /// A declaration to evaluate.
96  Declaration {
97    /// The property id for the declaration.
98    #[cfg_attr(feature = "serde", serde(borrow, rename = "propertyId"))]
99    property_id: PropertyId<'i>,
100    /// The raw value of the declaration.
101    value: CowArcStr<'i>,
102  },
103  /// A selector to evaluate.
104  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
105  Selector(CowArcStr<'i>),
106  // FontTechnology()
107  /// An unknown condition.
108  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
109  Unknown(CowArcStr<'i>),
110}
111
112impl<'i> SupportsCondition<'i> {
113  /// Combines the given supports condition into this one with an `and` expression.
114  pub fn and(&mut self, b: &SupportsCondition<'i>) {
115    if let SupportsCondition::And(a) = self {
116      if !a.contains(&b) {
117        a.push(b.clone());
118      }
119    } else if self != b {
120      *self = SupportsCondition::And(vec![self.clone(), b.clone()])
121    }
122  }
123
124  /// Combines the given supports condition into this one with an `or` expression.
125  pub fn or(&mut self, b: &SupportsCondition<'i>) {
126    if let SupportsCondition::Or(a) = self {
127      if !a.contains(&b) {
128        a.push(b.clone());
129      }
130    } else if self != b {
131      *self = SupportsCondition::Or(vec![self.clone(), b.clone()])
132    }
133  }
134
135  fn set_prefixes_for_targets(&mut self, targets: &Targets) {
136    match self {
137      SupportsCondition::Not(cond) => cond.set_prefixes_for_targets(targets),
138      SupportsCondition::And(items) | SupportsCondition::Or(items) => {
139        for item in items {
140          item.set_prefixes_for_targets(targets);
141        }
142      }
143      SupportsCondition::Declaration { property_id, .. } => {
144        let prefix = property_id.prefix();
145        if prefix.is_empty() || prefix.contains(VendorPrefix::None) {
146          property_id.set_prefixes_for_targets(*targets);
147        }
148      }
149      _ => {}
150    }
151  }
152}
153
154impl<'i> Parse<'i> for SupportsCondition<'i> {
155  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
156    if input.try_parse(|input| input.expect_ident_matching("not")).is_ok() {
157      let in_parens = Self::parse_in_parens(input)?;
158      return Ok(SupportsCondition::Not(Box::new(in_parens)));
159    }
160
161    let in_parens = Self::parse_in_parens(input)?;
162    let mut expected_type = None;
163    let mut conditions = Vec::new();
164    let mut seen_declarations = HashMap::new();
165
166    loop {
167      let condition = input.try_parse(|input| {
168        let location = input.current_source_location();
169        let s = input.expect_ident()?;
170        let found_type = match_ignore_ascii_case! { &s,
171          "and" => 1,
172          "or" => 2,
173          _ => return Err(location.new_unexpected_token_error(
174            cssparser::Token::Ident(s.clone())
175          ))
176        };
177
178        if let Some(expected) = expected_type {
179          if found_type != expected {
180            return Err(location.new_unexpected_token_error(cssparser::Token::Ident(s.clone())));
181          }
182        } else {
183          expected_type = Some(found_type);
184        }
185
186        Self::parse_in_parens(input)
187      });
188
189      if let Ok(condition) = condition {
190        if conditions.is_empty() {
191          conditions.push(in_parens.clone());
192          if let SupportsCondition::Declaration { property_id, value } = &in_parens {
193            seen_declarations.insert((property_id.with_prefix(VendorPrefix::None), value.clone()), 0);
194          }
195        }
196
197        if let SupportsCondition::Declaration { property_id, value } = condition {
198          // Merge multiple declarations with the same property id (minus prefix) and value together.
199          let property_id = property_id.with_prefix(VendorPrefix::None);
200          let key = (property_id.clone(), value.clone());
201          if let Some(index) = seen_declarations.get(&key) {
202            if let SupportsCondition::Declaration {
203              property_id: cur_property,
204              ..
205            } = &mut conditions[*index]
206            {
207              cur_property.add_prefix(property_id.prefix());
208            }
209          } else {
210            seen_declarations.insert(key, conditions.len());
211            conditions.push(SupportsCondition::Declaration { property_id, value });
212          }
213        } else {
214          conditions.push(condition);
215        }
216      } else {
217        break;
218      }
219    }
220
221    if conditions.len() == 1 {
222      return Ok(conditions.pop().unwrap());
223    }
224
225    match expected_type {
226      Some(1) => Ok(SupportsCondition::And(conditions)),
227      Some(2) => Ok(SupportsCondition::Or(conditions)),
228      _ => Ok(in_parens),
229    }
230  }
231}
232
233impl<'i> SupportsCondition<'i> {
234  fn parse_in_parens<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
235    input.skip_whitespace();
236    let location = input.current_source_location();
237    let pos = input.position();
238    match input.next()? {
239      Token::Function(ref f) => {
240        match_ignore_ascii_case! { &*f,
241          "selector" => {
242            let res = input.try_parse(|input| {
243              input.parse_nested_block(|input| {
244                let pos = input.position();
245                input.expect_no_error_token()?;
246                Ok(SupportsCondition::Selector(input.slice_from(pos).into()))
247              })
248            });
249            if res.is_ok() {
250              return res
251            }
252          },
253          _ => {}
254        }
255      }
256      Token::ParenthesisBlock => {
257        let res = input.try_parse(|input| {
258          input.parse_nested_block(|input| {
259            if let Ok(condition) = input.try_parse(SupportsCondition::parse) {
260              return Ok(condition);
261            }
262
263            Self::parse_declaration(input)
264          })
265        });
266        if res.is_ok() {
267          return res;
268        }
269      }
270      t => return Err(location.new_unexpected_token_error(t.clone())),
271    };
272
273    input.parse_nested_block(|input| input.expect_no_error_token().map_err(|err| err.into()))?;
274    Ok(SupportsCondition::Unknown(input.slice_from(pos).into()))
275  }
276
277  pub(crate) fn parse_declaration<'t>(
278    input: &mut Parser<'i, 't>,
279  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
280    let property_id = PropertyId::parse(input)?;
281    input.expect_colon()?;
282    input.skip_whitespace();
283    let pos = input.position();
284    input.expect_no_error_token()?;
285    Ok(SupportsCondition::Declaration {
286      property_id,
287      value: input.slice_from(pos).into(),
288    })
289  }
290
291  fn needs_parens(&self, parent: &SupportsCondition) -> bool {
292    match self {
293      SupportsCondition::Not(_) => true,
294      SupportsCondition::And(_) => !matches!(parent, SupportsCondition::And(_)),
295      SupportsCondition::Or(_) => !matches!(parent, SupportsCondition::Or(_)),
296      _ => false,
297    }
298  }
299
300  fn to_css_with_parens_if_needed<W>(&self, dest: &mut Printer<W>, needs_parens: bool) -> Result<(), PrinterError>
301  where
302    W: std::fmt::Write,
303  {
304    if needs_parens {
305      dest.write_char('(')?;
306    }
307    self.to_css(dest)?;
308    if needs_parens {
309      dest.write_char(')')?;
310    }
311    Ok(())
312  }
313}
314
315impl<'i> ToCss for SupportsCondition<'i> {
316  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
317  where
318    W: std::fmt::Write,
319  {
320    match self {
321      SupportsCondition::Not(condition) => {
322        dest.write_str("not ")?;
323        condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self))
324      }
325      SupportsCondition::And(conditions) => {
326        let mut first = true;
327        for condition in conditions {
328          if first {
329            first = false;
330          } else {
331            dest.write_str(" and ")?;
332          }
333          condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self))?;
334        }
335        Ok(())
336      }
337      SupportsCondition::Or(conditions) => {
338        let mut first = true;
339        for condition in conditions {
340          if first {
341            first = false;
342          } else {
343            dest.write_str(" or ")?;
344          }
345          condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self))?;
346        }
347        Ok(())
348      }
349      SupportsCondition::Declaration { property_id, value } => {
350        dest.write_char('(')?;
351
352        let prefix = property_id.prefix().or_none();
353        if prefix != VendorPrefix::None {
354          dest.write_char('(')?;
355        }
356
357        let name = property_id.name();
358        let mut first = true;
359        for p in prefix {
360          if first {
361            first = false;
362          } else {
363            dest.write_str(") or (")?;
364          }
365
366          p.to_css(dest)?;
367          serialize_name(name, dest)?;
368          dest.delim(':', false)?;
369          dest.write_str(value)?;
370        }
371
372        if prefix != VendorPrefix::None {
373          dest.write_char(')')?;
374        }
375
376        dest.write_char(')')
377      }
378      SupportsCondition::Selector(sel) => {
379        dest.write_str("selector(")?;
380        dest.write_str(sel)?;
381        dest.write_char(')')
382      }
383      SupportsCondition::Unknown(unknown) => dest.write_str(&unknown),
384    }
385  }
386}