Skip to main content

lightningcss/rules/
container.rs

1//! The `@container` rule.
2
3use cssparser::*;
4
5use super::Location;
6use super::{CssRuleList, MinifyContext};
7use crate::error::{MinifyError, ParserError, PrinterError};
8use crate::media_query::{
9  define_query_features, operation_to_css, parse_query_condition, to_css_with_parens_if_needed, FeatureToCss,
10  MediaFeatureType, Operator, QueryCondition, QueryConditionFlags, QueryFeature, ValueType,
11};
12use crate::parser::{DefaultAtRule, ParserOptions};
13use crate::printer::Printer;
14use crate::properties::custom::TokenList;
15use crate::properties::{Property, PropertyId};
16#[cfg(feature = "serde")]
17use crate::serialization::ValueWrapper;
18use crate::targets::{Features, Targets};
19use crate::traits::{Parse, ParseWithOptions, ToCss};
20use crate::values::ident::CustomIdent;
21#[cfg(feature = "visitor")]
22use crate::visitor::Visit;
23
24/// A [@container](https://drafts.csswg.org/css-contain-3/#container-rule) rule.
25#[derive(Debug, PartialEq, Clone)]
26#[cfg_attr(feature = "visitor", derive(Visit))]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
29#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
30pub struct ContainerRule<'i, R = DefaultAtRule> {
31  /// The name of the container.
32  #[cfg_attr(feature = "serde", serde(borrow))]
33  pub name: Option<ContainerName<'i>>,
34  /// The container condition.
35  pub condition: Option<ContainerCondition<'i>>,
36  /// The rules within the `@container` rule.
37  pub rules: CssRuleList<'i, R>,
38  /// The location of the rule in the source file.
39  #[cfg_attr(feature = "visitor", skip_visit)]
40  pub loc: Location,
41}
42
43/// Represents a container condition.
44#[derive(Clone, Debug, PartialEq)]
45#[cfg_attr(feature = "visitor", derive(Visit))]
46#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
47#[cfg_attr(
48  feature = "serde",
49  derive(serde::Serialize, serde::Deserialize),
50  serde(tag = "type", rename_all = "kebab-case")
51)]
52#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
53pub enum ContainerCondition<'i> {
54  /// A size container feature, implicitly parenthesized.
55  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<ContainerSizeFeature>"))]
56  Feature(ContainerSizeFeature<'i>),
57  /// A negation of a condition.
58  #[cfg_attr(feature = "visitor", skip_type)]
59  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<ContainerCondition>>"))]
60  Not(Box<ContainerCondition<'i>>),
61  /// A set of joint operations.
62  #[cfg_attr(feature = "visitor", skip_type)]
63  Operation {
64    /// The operator for the conditions.
65    operator: Operator,
66    /// The conditions for the operator.
67    conditions: Vec<ContainerCondition<'i>>,
68  },
69  /// A style query.
70  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<StyleQuery>"))]
71  Style(StyleQuery<'i>),
72  /// A scroll state query.
73  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<ScrollStateQuery>"))]
74  ScrollState(ScrollStateQuery<'i>),
75  /// Unknown tokens.
76  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<TokenList>"))]
77  Unknown(TokenList<'i>),
78}
79
80/// A container query size feature.
81pub type ContainerSizeFeature<'i> = QueryFeature<'i, ContainerSizeFeatureId>;
82
83define_query_features! {
84  /// A container query size feature identifier.
85  pub enum ContainerSizeFeatureId {
86    /// The [width](https://w3c.github.io/csswg-drafts/css-contain-3/#width) size container feature.
87    "width": Width = Length,
88    /// The [height](https://w3c.github.io/csswg-drafts/css-contain-3/#height) size container feature.
89    "height": Height = Length,
90    /// The [inline-size](https://w3c.github.io/csswg-drafts/css-contain-3/#inline-size) size container feature.
91    "inline-size": InlineSize = Length,
92    /// The [block-size](https://w3c.github.io/csswg-drafts/css-contain-3/#block-size) size container feature.
93    "block-size": BlockSize = Length,
94    /// The [aspect-ratio](https://w3c.github.io/csswg-drafts/css-contain-3/#aspect-ratio) size container feature.
95    "aspect-ratio": AspectRatio = Ratio,
96    /// The [orientation](https://w3c.github.io/csswg-drafts/css-contain-3/#orientation) size container feature.
97    "orientation": Orientation = Ident,
98  }
99}
100
101impl FeatureToCss for ContainerSizeFeatureId {
102  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
103  where
104    W: std::fmt::Write,
105  {
106    dest.write_str(prefix)?;
107    self.to_css(dest)
108  }
109}
110
111/// Represents a style query within a container condition.
112#[derive(Clone, Debug, PartialEq)]
113#[cfg_attr(feature = "visitor", derive(Visit))]
114#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
115#[cfg_attr(
116  feature = "serde",
117  derive(serde::Serialize, serde::Deserialize),
118  serde(tag = "type", rename_all = "kebab-case")
119)]
120#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
121pub enum StyleQuery<'i> {
122  /// A property declaration.
123  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<Property>"))]
124  Declaration(Property<'i>),
125  /// A property name, without a value.
126  /// This matches if the property value is different from the initial value.
127  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<PropertyId>"))]
128  Property(PropertyId<'i>),
129  /// A negation of a condition.
130  #[cfg_attr(feature = "visitor", skip_type)]
131  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<StyleQuery>>"))]
132  Not(Box<StyleQuery<'i>>),
133  /// A set of joint operations.
134  #[cfg_attr(feature = "visitor", skip_type)]
135  Operation {
136    /// The operator for the conditions.
137    operator: Operator,
138    /// The conditions for the operator.
139    conditions: Vec<StyleQuery<'i>>,
140  },
141}
142
143/// Represents a scroll state query within a container condition.
144#[derive(Clone, Debug, PartialEq)]
145#[cfg_attr(feature = "visitor", derive(Visit))]
146#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
147#[cfg_attr(
148  feature = "serde",
149  derive(serde::Serialize, serde::Deserialize),
150  serde(tag = "type", rename_all = "kebab-case")
151)]
152#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
153pub enum ScrollStateQuery<'i> {
154  /// A size container feature, implicitly parenthesized.
155  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<ScrollStateFeature>"))]
156  Feature(ScrollStateFeature<'i>),
157  /// A negation of a condition.
158  #[cfg_attr(feature = "visitor", skip_type)]
159  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<ScrollStateQuery>>"))]
160  Not(Box<ScrollStateQuery<'i>>),
161  /// A set of joint operations.
162  #[cfg_attr(feature = "visitor", skip_type)]
163  Operation {
164    /// The operator for the conditions.
165    operator: Operator,
166    /// The conditions for the operator.
167    conditions: Vec<ScrollStateQuery<'i>>,
168  },
169}
170
171/// A container query size feature.
172pub type ScrollStateFeature<'i> = QueryFeature<'i, ScrollStateFeatureId>;
173
174define_query_features! {
175  /// A container query scroll state feature identifier.
176  pub enum ScrollStateFeatureId {
177    /// The [stuck](https://drafts.csswg.org/css-conditional-5/#stuck) scroll state feature.
178    "stuck": Stuck = Ident,
179    /// The [snapped](https://drafts.csswg.org/css-conditional-5/#snapped) scroll state feature.
180    "snapped": Snapped = Ident,
181    /// The [scrollable](https://drafts.csswg.org/css-conditional-5/#scrollable) scroll state feature.
182    "scrollable": Scrollable = Ident,
183    /// The [scrolled](https://drafts.csswg.org/css-conditional-5/#scrolled) scroll state feature.
184    "scrolled": Scrolled = Ident,
185  }
186}
187
188impl FeatureToCss for ScrollStateFeatureId {
189  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
190  where
191    W: std::fmt::Write,
192  {
193    dest.write_str(prefix)?;
194    self.to_css(dest)
195  }
196}
197
198impl<'i> QueryCondition<'i> for ContainerCondition<'i> {
199  #[inline]
200  fn parse_feature<'t>(
201    input: &mut Parser<'i, 't>,
202    options: &ParserOptions<'i>,
203  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
204    let feature = QueryFeature::parse_with_options(input, options)?;
205    Ok(Self::Feature(feature))
206  }
207
208  #[inline]
209  fn create_negation(condition: Box<ContainerCondition<'i>>) -> Self {
210    Self::Not(condition)
211  }
212
213  #[inline]
214  fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self {
215    Self::Operation { operator, conditions }
216  }
217
218  fn parse_style_query<'t>(
219    input: &mut Parser<'i, 't>,
220    options: &ParserOptions<'i>,
221  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
222    input.parse_nested_block(|input| {
223      if let Ok(res) =
224        input.try_parse(|input| parse_query_condition(input, QueryConditionFlags::ALLOW_OR, options))
225      {
226        return Ok(Self::Style(res));
227      }
228
229      Ok(Self::Style(StyleQuery::parse_feature(input, options)?))
230    })
231  }
232
233  fn parse_scroll_state_query<'t>(
234    input: &mut Parser<'i, 't>,
235    options: &ParserOptions<'i>,
236  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
237    input.parse_nested_block(|input| {
238      if let Ok(res) =
239        input.try_parse(|input| parse_query_condition(input, QueryConditionFlags::ALLOW_OR, options))
240      {
241        return Ok(Self::ScrollState(res));
242      }
243
244      Ok(Self::ScrollState(ScrollStateQuery::parse_feature(input, options)?))
245    })
246  }
247
248  fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
249    match self {
250      ContainerCondition::Not(_) => true,
251      ContainerCondition::Operation { operator, .. } => Some(*operator) != parent_operator,
252      ContainerCondition::Feature(f) => f.needs_parens(parent_operator, targets),
253      ContainerCondition::Style(_) => false,
254      ContainerCondition::ScrollState(_) => false,
255      ContainerCondition::Unknown(_) => false,
256    }
257  }
258}
259
260impl<'i> QueryCondition<'i> for ScrollStateQuery<'i> {
261  #[inline]
262  fn parse_feature<'t>(
263    input: &mut Parser<'i, 't>,
264    options: &ParserOptions<'i>,
265  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
266    let feature = QueryFeature::parse_with_options(input, options)?;
267    Ok(Self::Feature(feature))
268  }
269
270  #[inline]
271  fn create_negation(condition: Box<Self>) -> Self {
272    Self::Not(condition)
273  }
274
275  #[inline]
276  fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self {
277    Self::Operation { operator, conditions }
278  }
279
280  fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
281    match self {
282      ScrollStateQuery::Not(_) => true,
283      ScrollStateQuery::Operation { operator, .. } => Some(*operator) != parent_operator,
284      ScrollStateQuery::Feature(f) => f.needs_parens(parent_operator, targets),
285    }
286  }
287}
288
289impl<'i> QueryCondition<'i> for StyleQuery<'i> {
290  #[inline]
291  fn parse_feature<'t>(
292    input: &mut Parser<'i, 't>,
293    options: &ParserOptions<'i>,
294  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
295    let property_id = PropertyId::parse(input)?;
296    if input.try_parse(|input| input.expect_colon()).is_ok() {
297      input.skip_whitespace();
298      let feature = Self::Declaration(Property::parse(property_id, input, options)?);
299      let _ = input.try_parse(|input| parse_important(input));
300      Ok(feature)
301    } else {
302      Ok(Self::Property(property_id))
303    }
304  }
305
306  #[inline]
307  fn create_negation(condition: Box<Self>) -> Self {
308    Self::Not(condition)
309  }
310
311  #[inline]
312  fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self {
313    Self::Operation { operator, conditions }
314  }
315
316  fn needs_parens(&self, parent_operator: Option<Operator>, _targets: &Targets) -> bool {
317    match self {
318      StyleQuery::Not(_) => true,
319      StyleQuery::Operation { operator, .. } => Some(*operator) != parent_operator,
320      StyleQuery::Declaration(_) | StyleQuery::Property(_) => true,
321    }
322  }
323}
324
325impl<'i> ParseWithOptions<'i> for ContainerCondition<'i> {
326  fn parse_with_options<'t>(
327    input: &mut Parser<'i, 't>,
328    options: &ParserOptions<'i>,
329  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
330    input
331      .try_parse(|input| {
332        parse_query_condition(
333          input,
334          QueryConditionFlags::ALLOW_OR
335            | QueryConditionFlags::ALLOW_STYLE
336            | QueryConditionFlags::ALLOW_SCROLL_STATE,
337          options,
338        )
339      })
340      .or_else(|e| {
341        if options.error_recovery {
342          options.warn(e);
343          Ok(ContainerCondition::Unknown(TokenList::parse(input, options, 0)?))
344        } else {
345          Err(e)
346        }
347      })
348  }
349}
350
351impl<'i> ToCss for ContainerCondition<'i> {
352  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
353  where
354    W: std::fmt::Write,
355  {
356    match *self {
357      ContainerCondition::Feature(ref f) => f.to_css(dest),
358      ContainerCondition::Not(ref c) => {
359        dest.write_str("not ")?;
360        to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current))
361      }
362      ContainerCondition::Operation {
363        ref conditions,
364        operator,
365      } => operation_to_css(operator, conditions, dest),
366      ContainerCondition::Style(ref query) => {
367        dest.write_str("style(")?;
368        query.to_css(dest)?;
369        dest.write_char(')')
370      }
371      ContainerCondition::ScrollState(ref query) => {
372        let needs_parens = !matches!(query, ScrollStateQuery::Feature(_));
373        dest.write_str("scroll-state")?;
374        if needs_parens {
375          dest.write_char('(')?;
376        }
377        query.to_css(dest)?;
378        if needs_parens {
379          dest.write_char(')')?;
380        }
381        Ok(())
382      }
383      ContainerCondition::Unknown(ref tokens) => tokens.to_css(dest, false),
384    }
385  }
386}
387
388impl<'i> ToCss for StyleQuery<'i> {
389  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
390  where
391    W: std::fmt::Write,
392  {
393    match *self {
394      StyleQuery::Declaration(ref f) => f.to_css(dest, false),
395      StyleQuery::Property(ref f) => f.to_css(dest),
396      StyleQuery::Not(ref c) => {
397        dest.write_str("not ")?;
398        to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current))
399      }
400      StyleQuery::Operation {
401        ref conditions,
402        operator,
403      } => operation_to_css(operator, conditions, dest),
404    }
405  }
406}
407
408impl<'i> ToCss for ScrollStateQuery<'i> {
409  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
410  where
411    W: std::fmt::Write,
412  {
413    match *self {
414      ScrollStateQuery::Feature(ref f) => f.to_css(dest),
415      ScrollStateQuery::Not(ref c) => {
416        dest.write_str("not ")?;
417        to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current))
418      }
419      ScrollStateQuery::Operation {
420        ref conditions,
421        operator,
422      } => operation_to_css(operator, conditions, dest),
423    }
424  }
425}
426
427/// A [`<container-name>`](https://drafts.csswg.org/css-contain-3/#typedef-container-name) in a `@container` rule.
428#[derive(Debug, Clone, PartialEq)]
429#[cfg_attr(feature = "visitor", derive(Visit))]
430#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
431#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
432#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
433pub struct ContainerName<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub CustomIdent<'i>);
434
435impl<'i> Parse<'i> for ContainerName<'i> {
436  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
437    let ident = CustomIdent::parse(input)?;
438    match_ignore_ascii_case! { &*ident.0,
439      "none" | "and" | "not" | "or" => Err(input.new_unexpected_token_error(Token::Ident(ident.0.as_ref().to_owned().into()))),
440      _ => Ok(ContainerName(ident))
441    }
442  }
443}
444
445impl<'i> ToCss for ContainerName<'i> {
446  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
447  where
448    W: std::fmt::Write,
449  {
450    // Container name should not be hashed
451    // https://github.com/vercel/next.js/issues/71233
452    self.0.to_css_with_options(
453      dest,
454      match &dest.css_module {
455        Some(css_module) => css_module.config.container,
456        None => false,
457      },
458    )
459  }
460}
461
462impl<'i, T: Clone> ContainerRule<'i, T> {
463  pub(crate) fn minify(
464    &mut self,
465    context: &mut MinifyContext<'_, 'i>,
466    parent_is_unused: bool,
467  ) -> Result<bool, MinifyError> {
468    self.rules.minify(context, parent_is_unused)?;
469    Ok(self.rules.0.is_empty())
470  }
471}
472
473impl<'a, 'i, T: ToCss> ToCss for ContainerRule<'i, T> {
474  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
475  where
476    W: std::fmt::Write,
477  {
478    #[cfg(feature = "sourcemap")]
479    dest.add_mapping(self.loc);
480    dest.write_str("@container ")?;
481    let has_condition = self.condition.is_some();
482
483    if let Some(name) = &self.name {
484      name.to_css(dest)?;
485      if has_condition {
486        dest.write_char(' ')?;
487      }
488    }
489
490    if let Some(condition) = &self.condition {
491      // Don't downlevel range syntax in container queries.
492      let exclude = dest.targets.current.exclude;
493      dest.targets.current.exclude.insert(Features::MediaQueries);
494      condition.to_css(dest)?;
495      dest.targets.current.exclude = exclude;
496    }
497
498    dest.whitespace()?;
499    dest.write_char('{')?;
500    dest.indent();
501    dest.newline()?;
502    self.rules.to_css(dest)?;
503    dest.dedent();
504    dest.newline()?;
505    dest.write_char('}')
506  }
507}