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