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;
13use crate::printer::Printer;
14use crate::properties::{Property, PropertyId};
15#[cfg(feature = "serde")]
16use crate::serialization::ValueWrapper;
17use crate::targets::{Features, Targets};
18use crate::traits::{Parse, ToCss};
19use crate::values::ident::CustomIdent;
20#[cfg(feature = "visitor")]
21use crate::visitor::Visit;
22
23/// A [@container](https://drafts.csswg.org/css-contain-3/#container-rule) 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 ContainerRule<'i, R = DefaultAtRule> {
30  /// The name of the container.
31  #[cfg_attr(feature = "serde", serde(borrow))]
32  pub name: Option<ContainerName<'i>>,
33  /// The container condition.
34  pub condition: ContainerCondition<'i>,
35  /// The rules within the `@container` rule.
36  pub rules: CssRuleList<'i, R>,
37  /// The location of the rule in the source file.
38  #[cfg_attr(feature = "visitor", skip_visit)]
39  pub loc: Location,
40}
41
42/// Represents a container condition.
43#[derive(Clone, Debug, PartialEq)]
44#[cfg_attr(feature = "visitor", derive(Visit))]
45#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
46#[cfg_attr(
47  feature = "serde",
48  derive(serde::Serialize, serde::Deserialize),
49  serde(tag = "type", rename_all = "kebab-case")
50)]
51#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
52pub enum ContainerCondition<'i> {
53  /// A size container feature, implicitly parenthesized.
54  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<ContainerSizeFeature>"))]
55  Feature(ContainerSizeFeature<'i>),
56  /// A negation of a condition.
57  #[cfg_attr(feature = "visitor", skip_type)]
58  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<ContainerCondition>>"))]
59  Not(Box<ContainerCondition<'i>>),
60  /// A set of joint operations.
61  #[cfg_attr(feature = "visitor", skip_type)]
62  Operation {
63    /// The operator for the conditions.
64    operator: Operator,
65    /// The conditions for the operator.
66    conditions: Vec<ContainerCondition<'i>>,
67  },
68  /// A style query.
69  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<StyleQuery>"))]
70  Style(StyleQuery<'i>),
71}
72
73/// A container query size feature.
74pub type ContainerSizeFeature<'i> = QueryFeature<'i, ContainerSizeFeatureId>;
75
76define_query_features! {
77  /// A container query size feature identifier.
78  pub enum ContainerSizeFeatureId {
79    /// The [width](https://w3c.github.io/csswg-drafts/css-contain-3/#width) size container feature.
80    "width": Width = Length,
81    /// The [height](https://w3c.github.io/csswg-drafts/css-contain-3/#height) size container feature.
82    "height": Height = Length,
83    /// The [inline-size](https://w3c.github.io/csswg-drafts/css-contain-3/#inline-size) size container feature.
84    "inline-size": InlineSize = Length,
85    /// The [block-size](https://w3c.github.io/csswg-drafts/css-contain-3/#block-size) size container feature.
86    "block-size": BlockSize = Length,
87    /// The [aspect-ratio](https://w3c.github.io/csswg-drafts/css-contain-3/#aspect-ratio) size container feature.
88    "aspect-ratio": AspectRatio = Ratio,
89    /// The [orientation](https://w3c.github.io/csswg-drafts/css-contain-3/#orientation) size container feature.
90    "orientation": Orientation = Ident,
91  }
92}
93
94impl FeatureToCss for ContainerSizeFeatureId {
95  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
96  where
97    W: std::fmt::Write,
98  {
99    dest.write_str(prefix)?;
100    self.to_css(dest)
101  }
102}
103
104/// Represents a style query within a container condition.
105#[derive(Clone, Debug, PartialEq)]
106#[cfg_attr(feature = "visitor", derive(Visit))]
107#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
108#[cfg_attr(
109  feature = "serde",
110  derive(serde::Serialize, serde::Deserialize),
111  serde(tag = "type", rename_all = "kebab-case")
112)]
113#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
114pub enum StyleQuery<'i> {
115  /// A property declaration.
116  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<Property>"))]
117  Declaration(Property<'i>),
118  /// A property name, without a value.
119  /// This matches if the property value is different from the initial value.
120  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<PropertyId>"))]
121  Property(PropertyId<'i>),
122  /// A negation of a condition.
123  #[cfg_attr(feature = "visitor", skip_type)]
124  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<StyleQuery>>"))]
125  Not(Box<StyleQuery<'i>>),
126  /// A set of joint operations.
127  #[cfg_attr(feature = "visitor", skip_type)]
128  Operation {
129    /// The operator for the conditions.
130    operator: Operator,
131    /// The conditions for the operator.
132    conditions: Vec<StyleQuery<'i>>,
133  },
134}
135
136impl<'i> QueryCondition<'i> for ContainerCondition<'i> {
137  #[inline]
138  fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
139    let feature = QueryFeature::parse(input)?;
140    Ok(Self::Feature(feature))
141  }
142
143  #[inline]
144  fn create_negation(condition: Box<ContainerCondition<'i>>) -> Self {
145    Self::Not(condition)
146  }
147
148  #[inline]
149  fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self {
150    Self::Operation { operator, conditions }
151  }
152
153  fn parse_style_query<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
154    input.parse_nested_block(|input| {
155      if let Ok(res) = input.try_parse(|input| parse_query_condition(input, QueryConditionFlags::ALLOW_OR)) {
156        return Ok(Self::Style(res));
157      }
158
159      Ok(Self::Style(StyleQuery::parse_feature(input)?))
160    })
161  }
162
163  fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
164    match self {
165      ContainerCondition::Not(_) => true,
166      ContainerCondition::Operation { operator, .. } => Some(*operator) != parent_operator,
167      ContainerCondition::Feature(f) => f.needs_parens(parent_operator, targets),
168      ContainerCondition::Style(_) => false,
169    }
170  }
171}
172
173impl<'i> QueryCondition<'i> for StyleQuery<'i> {
174  #[inline]
175  fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
176    let property_id = PropertyId::parse(input)?;
177    if input.try_parse(|input| input.expect_colon()).is_ok() {
178      input.skip_whitespace();
179      let feature = Self::Declaration(Property::parse(property_id, input, &Default::default())?);
180      let _ = input.try_parse(|input| parse_important(input));
181      Ok(feature)
182    } else {
183      Ok(Self::Property(property_id))
184    }
185  }
186
187  #[inline]
188  fn create_negation(condition: Box<Self>) -> Self {
189    Self::Not(condition)
190  }
191
192  #[inline]
193  fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self {
194    Self::Operation { operator, conditions }
195  }
196
197  fn needs_parens(&self, parent_operator: Option<Operator>, _targets: &Targets) -> bool {
198    match self {
199      StyleQuery::Not(_) => true,
200      StyleQuery::Operation { operator, .. } => Some(*operator) != parent_operator,
201      StyleQuery::Declaration(_) | StyleQuery::Property(_) => true,
202    }
203  }
204}
205
206impl<'i> Parse<'i> for ContainerCondition<'i> {
207  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
208    parse_query_condition(input, QueryConditionFlags::ALLOW_OR | QueryConditionFlags::ALLOW_STYLE)
209  }
210}
211
212impl<'i> ToCss for ContainerCondition<'i> {
213  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
214  where
215    W: std::fmt::Write,
216  {
217    match *self {
218      ContainerCondition::Feature(ref f) => f.to_css(dest),
219      ContainerCondition::Not(ref c) => {
220        dest.write_str("not ")?;
221        to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets))
222      }
223      ContainerCondition::Operation {
224        ref conditions,
225        operator,
226      } => operation_to_css(operator, conditions, dest),
227      ContainerCondition::Style(ref query) => {
228        dest.write_str("style(")?;
229        query.to_css(dest)?;
230        dest.write_char(')')
231      }
232    }
233  }
234}
235
236impl<'i> ToCss for StyleQuery<'i> {
237  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
238  where
239    W: std::fmt::Write,
240  {
241    match *self {
242      StyleQuery::Declaration(ref f) => f.to_css(dest, false),
243      StyleQuery::Property(ref f) => f.to_css(dest),
244      StyleQuery::Not(ref c) => {
245        dest.write_str("not ")?;
246        to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets))
247      }
248      StyleQuery::Operation {
249        ref conditions,
250        operator,
251      } => operation_to_css(operator, conditions, dest),
252    }
253  }
254}
255
256/// A [`<container-name>`](https://drafts.csswg.org/css-contain-3/#typedef-container-name) in a `@container` rule.
257#[derive(Debug, Clone, PartialEq)]
258#[cfg_attr(feature = "visitor", derive(Visit))]
259#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
260#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
261#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
262pub struct ContainerName<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub CustomIdent<'i>);
263
264impl<'i> Parse<'i> for ContainerName<'i> {
265  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
266    let ident = CustomIdent::parse(input)?;
267    match_ignore_ascii_case! { &*ident.0,
268      "none" | "and" | "not" | "or" => Err(input.new_unexpected_token_error(Token::Ident(ident.0.as_ref().to_owned().into()))),
269      _ => Ok(ContainerName(ident))
270    }
271  }
272}
273
274impl<'i> ToCss for ContainerName<'i> {
275  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
276  where
277    W: std::fmt::Write,
278  {
279    // Container name should not be hashed
280    // https://github.com/vercel/next.js/issues/71233
281    self.0.to_css_with_options(
282      dest,
283      match &dest.css_module {
284        Some(css_module) => css_module.config.container,
285        None => false,
286      },
287    )
288  }
289}
290
291impl<'i, T: Clone> ContainerRule<'i, T> {
292  pub(crate) fn minify(
293    &mut self,
294    context: &mut MinifyContext<'_, 'i>,
295    parent_is_unused: bool,
296  ) -> Result<bool, MinifyError> {
297    self.rules.minify(context, parent_is_unused)?;
298    Ok(self.rules.0.is_empty())
299  }
300}
301
302impl<'a, 'i, T: ToCss> ToCss for ContainerRule<'i, T> {
303  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
304  where
305    W: std::fmt::Write,
306  {
307    #[cfg(feature = "sourcemap")]
308    dest.add_mapping(self.loc);
309    dest.write_str("@container ")?;
310    if let Some(name) = &self.name {
311      name.to_css(dest)?;
312      dest.write_char(' ')?;
313    }
314
315    // Don't downlevel range syntax in container queries.
316    let exclude = dest.targets.exclude;
317    dest.targets.exclude.insert(Features::MediaQueries);
318    self.condition.to_css(dest)?;
319    dest.targets.exclude = exclude;
320
321    dest.whitespace()?;
322    dest.write_char('{')?;
323    dest.indent();
324    dest.newline()?;
325    self.rules.to_css(dest)?;
326    dest.dedent();
327    dest.newline()?;
328    dest.write_char('}')
329  }
330}