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