Skip to main content

lightningcss/properties/
size.rs

1//! CSS properties related to box sizing.
2
3use crate::compat::Feature;
4use crate::context::PropertyHandlerContext;
5use crate::declaration::DeclarationList;
6use crate::error::{ParserError, PrinterError};
7use crate::logical::PropertyCategory;
8use crate::macros::{enum_property, property_bitflags};
9use crate::printer::Printer;
10use crate::properties::{Property, PropertyId};
11use crate::traits::{IsCompatible, Parse, PropertyHandler, ToCss};
12use crate::values::length::LengthPercentage;
13use crate::values::ratio::Ratio;
14use crate::vendor_prefix::VendorPrefix;
15#[cfg(feature = "visitor")]
16use crate::visitor::Visit;
17use cssparser::*;
18
19#[cfg(feature = "serde")]
20use crate::serialization::*;
21
22// https://drafts.csswg.org/css-sizing-3/#specifying-sizes
23// https://www.w3.org/TR/css-sizing-4/#sizing-values
24
25/// A value for the [preferred size properties](https://drafts.csswg.org/css-sizing-3/#preferred-size-properties),
26/// i.e. `width` and `height.
27#[derive(Debug, Clone, PartialEq)]
28#[cfg_attr(feature = "visitor", derive(Visit))]
29#[cfg_attr(
30  feature = "serde",
31  derive(serde::Serialize, serde::Deserialize),
32  serde(tag = "type", rename_all = "kebab-case")
33)]
34#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
35#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
36pub enum Size {
37  /// The `auto` keyword.
38  Auto,
39  /// An explicit length or percentage.
40  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
41  LengthPercentage(LengthPercentage),
42  /// The `min-content` keyword.
43  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
44  MinContent(VendorPrefix),
45  /// The `max-content` keyword.
46  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
47  MaxContent(VendorPrefix),
48  /// The `fit-content` keyword.
49  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
50  FitContent(VendorPrefix),
51  /// The `fit-content()` function.
52  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
53  FitContentFunction(LengthPercentage),
54  /// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords.
55  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
56  Stretch(VendorPrefix),
57  /// The `contain` keyword.
58  Contain,
59}
60
61impl<'i> Parse<'i> for Size {
62  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
63    let res = input.try_parse(|input| {
64      let ident = input.expect_ident()?;
65      Ok(match_ignore_ascii_case! { &*ident,
66        "auto" => Size::Auto,
67        "min-content" => Size::MinContent(VendorPrefix::None),
68        "-webkit-min-content" => Size::MinContent(VendorPrefix::WebKit),
69        "-moz-min-content" => Size::MinContent(VendorPrefix::Moz),
70        "max-content" => Size::MaxContent(VendorPrefix::None),
71        "-webkit-max-content" => Size::MaxContent(VendorPrefix::WebKit),
72        "-moz-max-content" => Size::MaxContent(VendorPrefix::Moz),
73        "stretch" => Size::Stretch(VendorPrefix::None),
74        "-webkit-fill-available" => Size::Stretch(VendorPrefix::WebKit),
75        "-moz-available" => Size::Stretch(VendorPrefix::Moz),
76        "fit-content" => Size::FitContent(VendorPrefix::None),
77        "-webkit-fit-content" => Size::FitContent(VendorPrefix::WebKit),
78        "-moz-fit-content" => Size::FitContent(VendorPrefix::Moz),
79        "contain" => Size::Contain,
80        _ => return Err(input.new_custom_error(ParserError::InvalidValue))
81      })
82    });
83
84    if res.is_ok() {
85      return res;
86    }
87
88    if let Ok(res) = input.try_parse(parse_fit_content) {
89      return Ok(Size::FitContentFunction(res));
90    }
91
92    let lp = input.try_parse(LengthPercentage::parse)?;
93    Ok(Size::LengthPercentage(lp))
94  }
95}
96
97impl ToCss for Size {
98  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
99  where
100    W: std::fmt::Write,
101  {
102    use Size::*;
103    match self {
104      Auto => dest.write_str("auto"),
105      Contain => dest.write_str("contain"),
106      MinContent(vp) => {
107        vp.to_css(dest)?;
108        dest.write_str("min-content")
109      }
110      MaxContent(vp) => {
111        vp.to_css(dest)?;
112        dest.write_str("max-content")
113      }
114      FitContent(vp) => {
115        vp.to_css(dest)?;
116        dest.write_str("fit-content")
117      }
118      Stretch(vp) => match *vp {
119        VendorPrefix::None => dest.write_str("stretch"),
120        VendorPrefix::WebKit => dest.write_str("-webkit-fill-available"),
121        VendorPrefix::Moz => dest.write_str("-moz-available"),
122        _ => unreachable!(),
123      },
124      FitContentFunction(l) => {
125        dest.write_str("fit-content(")?;
126        l.to_css(dest)?;
127        dest.write_str(")")
128      }
129      LengthPercentage(l) => l.to_css(dest),
130    }
131  }
132}
133
134impl IsCompatible for Size {
135  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
136    use Size::*;
137    match self {
138      LengthPercentage(l) => l.is_compatible(browsers),
139      MinContent(..) => Feature::MinContentSize.is_compatible(browsers),
140      MaxContent(..) => Feature::MaxContentSize.is_compatible(browsers),
141      FitContent(..) => Feature::FitContentSize.is_compatible(browsers),
142      FitContentFunction(l) => {
143        Feature::FitContentFunctionSize.is_compatible(browsers) && l.is_compatible(browsers)
144      }
145      Stretch(vp) => match *vp {
146        VendorPrefix::None => Feature::StretchSize,
147        VendorPrefix::WebKit | VendorPrefix::Moz => Feature::WebkitFillAvailableSize,
148        _ => return false,
149      }
150      .is_compatible(browsers),
151      Contain => false, // ??? no data in mdn
152      Auto => true,
153    }
154  }
155}
156
157/// A value for the [minimum](https://drafts.csswg.org/css-sizing-3/#min-size-properties)
158/// and [maximum](https://drafts.csswg.org/css-sizing-3/#max-size-properties) size properties,
159/// e.g. `min-width` and `max-height`.
160#[derive(Debug, Clone, PartialEq)]
161#[cfg_attr(feature = "visitor", derive(Visit))]
162#[cfg_attr(
163  feature = "serde",
164  derive(serde::Serialize, serde::Deserialize),
165  serde(tag = "type", rename_all = "kebab-case")
166)]
167#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
168#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
169pub enum MaxSize {
170  /// The `none` keyword.
171  None,
172  /// An explicit length or percentage.
173  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
174  LengthPercentage(LengthPercentage),
175  /// The `min-content` keyword.
176  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
177  MinContent(VendorPrefix),
178  /// The `max-content` keyword.
179  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
180  MaxContent(VendorPrefix),
181  /// The `fit-content` keyword.
182  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
183  FitContent(VendorPrefix),
184  /// The `fit-content()` function.
185  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
186  FitContentFunction(LengthPercentage),
187  /// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords.
188  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
189  Stretch(VendorPrefix),
190  /// The `contain` keyword.
191  Contain,
192}
193
194impl<'i> Parse<'i> for MaxSize {
195  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
196    let res = input.try_parse(|input| {
197      let ident = input.expect_ident()?;
198      Ok(match_ignore_ascii_case! { &*ident,
199        "none" => MaxSize::None,
200        "min-content" => MaxSize::MinContent(VendorPrefix::None),
201        "-webkit-min-content" => MaxSize::MinContent(VendorPrefix::WebKit),
202        "-moz-min-content" => MaxSize::MinContent(VendorPrefix::Moz),
203        "max-content" => MaxSize::MaxContent(VendorPrefix::None),
204        "-webkit-max-content" => MaxSize::MaxContent(VendorPrefix::WebKit),
205        "-moz-max-content" => MaxSize::MaxContent(VendorPrefix::Moz),
206        "stretch" => MaxSize::Stretch(VendorPrefix::None),
207        "-webkit-fill-available" => MaxSize::Stretch(VendorPrefix::WebKit),
208        "-moz-available" => MaxSize::Stretch(VendorPrefix::Moz),
209        "fit-content" => MaxSize::FitContent(VendorPrefix::None),
210        "-webkit-fit-content" => MaxSize::FitContent(VendorPrefix::WebKit),
211        "-moz-fit-content" => MaxSize::FitContent(VendorPrefix::Moz),
212        "contain" => MaxSize::Contain,
213        _ => return Err(input.new_custom_error(ParserError::InvalidValue))
214      })
215    });
216
217    if res.is_ok() {
218      return res;
219    }
220
221    if let Ok(res) = input.try_parse(parse_fit_content) {
222      return Ok(MaxSize::FitContentFunction(res));
223    }
224
225    let lp = input.try_parse(LengthPercentage::parse)?;
226    Ok(MaxSize::LengthPercentage(lp))
227  }
228}
229
230impl ToCss for MaxSize {
231  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
232  where
233    W: std::fmt::Write,
234  {
235    use MaxSize::*;
236    match self {
237      None => dest.write_str("none"),
238      Contain => dest.write_str("contain"),
239      MinContent(vp) => {
240        vp.to_css(dest)?;
241        dest.write_str("min-content")
242      }
243      MaxContent(vp) => {
244        vp.to_css(dest)?;
245        dest.write_str("max-content")
246      }
247      FitContent(vp) => {
248        vp.to_css(dest)?;
249        dest.write_str("fit-content")
250      }
251      Stretch(vp) => match *vp {
252        VendorPrefix::None => dest.write_str("stretch"),
253        VendorPrefix::WebKit => dest.write_str("-webkit-fill-available"),
254        VendorPrefix::Moz => dest.write_str("-moz-available"),
255        _ => unreachable!(),
256      },
257      FitContentFunction(l) => {
258        dest.write_str("fit-content(")?;
259        l.to_css(dest)?;
260        dest.write_str(")")
261      }
262      LengthPercentage(l) => l.to_css(dest),
263    }
264  }
265}
266
267impl IsCompatible for MaxSize {
268  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
269    use MaxSize::*;
270    match self {
271      LengthPercentage(l) => l.is_compatible(browsers),
272      MinContent(..) => Feature::MinContentSize.is_compatible(browsers),
273      MaxContent(..) => Feature::MaxContentSize.is_compatible(browsers),
274      FitContent(..) => Feature::FitContentSize.is_compatible(browsers),
275      FitContentFunction(l) => {
276        Feature::FitContentFunctionSize.is_compatible(browsers) && l.is_compatible(browsers)
277      }
278      Stretch(vp) => match *vp {
279        VendorPrefix::None => Feature::StretchSize,
280        VendorPrefix::WebKit | VendorPrefix::Moz => Feature::WebkitFillAvailableSize,
281        _ => return false,
282      }
283      .is_compatible(browsers),
284      Contain => false, // ??? no data in mdn
285      None => true,
286    }
287  }
288}
289
290fn parse_fit_content<'i, 't>(
291  input: &mut Parser<'i, 't>,
292) -> Result<LengthPercentage, ParseError<'i, ParserError<'i>>> {
293  input.expect_function_matching("fit-content")?;
294  input.parse_nested_block(|input| LengthPercentage::parse(input))
295}
296
297enum_property! {
298  /// A value for the [box-sizing](https://drafts.csswg.org/css-sizing-3/#box-sizing) property.
299  pub enum BoxSizing {
300    /// Exclude the margin/border/padding from the width and height.
301    ContentBox,
302    /// Include the padding and border (but not the margin) in the width and height.
303    BorderBox,
304  }
305}
306
307/// A value for the [aspect-ratio](https://drafts.csswg.org/css-sizing-4/#aspect-ratio) property.
308#[derive(Debug, Clone, PartialEq)]
309#[cfg_attr(feature = "visitor", derive(Visit))]
310#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
311#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
312#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
313pub struct AspectRatio {
314  /// The `auto` keyword.
315  pub auto: bool,
316  /// A preferred aspect ratio for the box, specified as width / height.
317  pub ratio: Option<Ratio>,
318}
319
320impl<'i> Parse<'i> for AspectRatio {
321  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
322    let location = input.current_source_location();
323    let mut auto = input.try_parse(|i| i.expect_ident_matching("auto"));
324    let ratio = input.try_parse(Ratio::parse);
325    if auto.is_err() {
326      auto = input.try_parse(|i| i.expect_ident_matching("auto"));
327    }
328    if auto.is_err() && ratio.is_err() {
329      return Err(location.new_custom_error(ParserError::InvalidValue));
330    }
331
332    Ok(AspectRatio {
333      auto: auto.is_ok(),
334      ratio: ratio.ok(),
335    })
336  }
337}
338
339impl ToCss for AspectRatio {
340  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
341  where
342    W: std::fmt::Write,
343  {
344    if self.auto {
345      dest.write_str("auto")?;
346    }
347
348    if let Some(ratio) = &self.ratio {
349      if self.auto {
350        dest.write_char(' ')?;
351      }
352      ratio.to_css(dest)?;
353    }
354
355    Ok(())
356  }
357}
358
359property_bitflags! {
360  #[derive(Default)]
361  struct SizeProperty: u16 {
362    const Width = 1 << 0;
363    const Height = 1 << 1;
364    const MinWidth = 1 << 2;
365    const MinHeight = 1 << 3;
366    const MaxWidth = 1 << 4;
367    const MaxHeight = 1 << 5;
368    const BlockSize = 1 << 6;
369    const InlineSize = 1 << 7;
370    const MinBlockSize  = 1 << 8;
371    const MinInlineSize = 1 << 9;
372    const MaxBlockSize = 1 << 10;
373    const MaxInlineSize = 1 << 11;
374  }
375}
376
377#[derive(Default)]
378pub(crate) struct SizeHandler {
379  width: Option<Size>,
380  height: Option<Size>,
381  min_width: Option<Size>,
382  min_height: Option<Size>,
383  max_width: Option<MaxSize>,
384  max_height: Option<MaxSize>,
385  block_size: Option<Size>,
386  inline_size: Option<Size>,
387  min_block_size: Option<Size>,
388  min_inline_size: Option<Size>,
389  max_block_size: Option<MaxSize>,
390  max_inline_size: Option<MaxSize>,
391  has_any: bool,
392  flushed_properties: SizeProperty,
393  category: PropertyCategory,
394}
395
396impl<'i> PropertyHandler<'i> for SizeHandler {
397  fn handle_property(
398    &mut self,
399    property: &Property<'i>,
400    dest: &mut DeclarationList<'i>,
401    context: &mut PropertyHandlerContext<'i, '_>,
402  ) -> bool {
403    let logical_supported = !context.should_compile_logical(Feature::LogicalSize);
404
405    macro_rules! property {
406      ($prop: ident, $val: ident, $category: ident) => {{
407        // If the category changes betweet logical and physical,
408        // or if the value contains syntax that isn't supported across all targets,
409        // preserve the previous value as a fallback.
410        if PropertyCategory::$category != self.category || (self.$prop.is_some() && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets))) {
411          self.flush(dest, context);
412        }
413
414        self.$prop = Some($val.clone());
415        self.category = PropertyCategory::$category;
416        self.has_any = true;
417      }};
418    }
419
420    match property {
421      Property::Width(v) => property!(width, v, Physical),
422      Property::Height(v) => property!(height, v, Physical),
423      Property::MinWidth(v) => property!(min_width, v, Physical),
424      Property::MinHeight(v) => property!(min_height, v, Physical),
425      Property::MaxWidth(v) => property!(max_width, v, Physical),
426      Property::MaxHeight(v) => property!(max_height, v, Physical),
427      Property::BlockSize(size) => property!(block_size, size, Logical),
428      Property::MinBlockSize(size) => property!(min_block_size, size, Logical),
429      Property::MaxBlockSize(size) => property!(max_block_size, size, Logical),
430      Property::InlineSize(size) => property!(inline_size, size, Logical),
431      Property::MinInlineSize(size) => property!(min_inline_size, size, Logical),
432      Property::MaxInlineSize(size) => property!(max_inline_size, size, Logical),
433      Property::Unparsed(unparsed) => {
434        self.flush(dest, context);
435        macro_rules! logical_unparsed {
436          ($physical: ident) => {
437            if logical_supported {
438              self
439                .flushed_properties
440                .insert(SizeProperty::try_from(&unparsed.property_id).unwrap());
441              dest.push(property.clone());
442            } else {
443              dest.push(Property::Unparsed(
444                unparsed.with_property_id(PropertyId::$physical),
445              ));
446              self.flushed_properties.insert(SizeProperty::$physical);
447            }
448          };
449        }
450
451        match &unparsed.property_id {
452          PropertyId::Width
453          | PropertyId::Height
454          | PropertyId::MinWidth
455          | PropertyId::MaxWidth
456          | PropertyId::MinHeight
457          | PropertyId::MaxHeight => {
458            self
459              .flushed_properties
460              .insert(SizeProperty::try_from(&unparsed.property_id).unwrap());
461            dest.push(property.clone());
462          }
463          PropertyId::BlockSize => logical_unparsed!(Height),
464          PropertyId::MinBlockSize => logical_unparsed!(MinHeight),
465          PropertyId::MaxBlockSize => logical_unparsed!(MaxHeight),
466          PropertyId::InlineSize => logical_unparsed!(Width),
467          PropertyId::MinInlineSize => logical_unparsed!(MinWidth),
468          PropertyId::MaxInlineSize => logical_unparsed!(MaxWidth),
469          _ => return false,
470        }
471      }
472      _ => return false,
473    }
474
475    true
476  }
477
478  fn finalize(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) {
479    self.flush(dest, context);
480    self.flushed_properties = SizeProperty::empty();
481  }
482}
483
484impl SizeHandler {
485  fn flush<'i>(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) {
486    if !self.has_any {
487      return;
488    }
489
490    self.has_any = false;
491    let logical_supported = !context.should_compile_logical(Feature::LogicalSize);
492
493    macro_rules! prefix {
494      ($prop: ident, $size: ident, $feature: ident) => {
495        if !self.flushed_properties.contains(SizeProperty::$prop) {
496          let prefixes =
497            context.targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::$feature) - VendorPrefix::None;
498          for prefix in prefixes {
499            dest.push(Property::$prop($size::$feature(prefix)));
500          }
501        }
502      };
503    }
504
505    macro_rules! property {
506      ($prop: ident, $val: ident, $size: ident) => {{
507        if let Some(val) = std::mem::take(&mut self.$val) {
508          match val {
509            $size::Stretch(VendorPrefix::None) => prefix!($prop, $size, Stretch),
510            $size::MinContent(VendorPrefix::None) => prefix!($prop, $size, MinContent),
511            $size::MaxContent(VendorPrefix::None) => prefix!($prop, $size, MaxContent),
512            $size::FitContent(VendorPrefix::None) => prefix!($prop, $size, FitContent),
513            _ => {}
514          }
515          dest.push(Property::$prop(val.clone()));
516          self.flushed_properties.insert(SizeProperty::$prop);
517        }
518      }};
519    }
520
521    macro_rules! logical {
522      ($prop: ident, $val: ident, $physical: ident, $size: ident) => {
523        if logical_supported {
524          property!($prop, $val, $size);
525        } else {
526          property!($physical, $val, $size);
527        }
528      };
529    }
530
531    property!(Width, width, Size);
532    property!(MinWidth, min_width, Size);
533    property!(MaxWidth, max_width, MaxSize);
534    property!(Height, height, Size);
535    property!(MinHeight, min_height, Size);
536    property!(MaxHeight, max_height, MaxSize);
537    logical!(BlockSize, block_size, Height, Size);
538    logical!(MinBlockSize, min_block_size, MinHeight, Size);
539    logical!(MaxBlockSize, max_block_size, MaxHeight, MaxSize);
540    logical!(InlineSize, inline_size, Width, Size);
541    logical!(MinInlineSize, min_inline_size, MinWidth, Size);
542    logical!(MaxInlineSize, max_inline_size, MaxWidth, MaxSize);
543  }
544}