parcel_css/properties/
display.rs

1//! CSS properties related to display.
2
3use super::custom::UnparsedProperty;
4use super::{Property, PropertyId};
5use crate::context::PropertyHandlerContext;
6use crate::declaration::DeclarationList;
7use crate::error::{ParserError, PrinterError};
8use crate::macros::enum_property;
9use crate::prefixes::{is_flex_2009, Feature};
10use crate::printer::Printer;
11use crate::targets::Browsers;
12use crate::traits::{Parse, PropertyHandler, ToCss};
13use crate::vendor_prefix::VendorPrefix;
14use cssparser::*;
15
16enum_property! {
17  /// A [`<display-outside>`](https://drafts.csswg.org/css-display-3/#typedef-display-outside) value.
18  #[allow(missing_docs)]
19  pub enum DisplayOutside {
20    "block": Block,
21    "inline": Inline,
22    "run-in": RunIn,
23  }
24}
25
26/// A [`<display-inside>`](https://drafts.csswg.org/css-display-3/#typedef-display-inside) value.
27#[derive(Debug, Clone, PartialEq)]
28#[cfg_attr(
29  feature = "serde",
30  derive(serde::Serialize, serde::Deserialize),
31  serde(rename_all = "kebab-case")
32)]
33#[allow(missing_docs)]
34pub enum DisplayInside {
35  Flow,
36  FlowRoot,
37  Table,
38  Flex(VendorPrefix),
39  Box(VendorPrefix),
40  Grid,
41  Ruby,
42}
43
44impl<'i> Parse<'i> for DisplayInside {
45  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
46    let location = input.current_source_location();
47    let ident = input.expect_ident()?;
48    match_ignore_ascii_case! { &*ident,
49      "flow" => Ok(DisplayInside::Flow),
50      "flow-root" => Ok(DisplayInside::FlowRoot),
51      "table" => Ok(DisplayInside::Table),
52      "flex" => Ok(DisplayInside::Flex(VendorPrefix::None)),
53      "-webkit-flex" => Ok(DisplayInside::Flex(VendorPrefix::WebKit)),
54      "-ms-flexbox" => Ok(DisplayInside::Flex(VendorPrefix::Ms)),
55      "-webkit-box" => Ok(DisplayInside::Box(VendorPrefix::WebKit)),
56      "-moz-box" => Ok(DisplayInside::Box(VendorPrefix::Moz)),
57      "grid" => Ok(DisplayInside::Grid),
58      "ruby" => Ok(DisplayInside::Ruby),
59      _ => Err(location.new_unexpected_token_error(
60        cssparser::Token::Ident(ident.clone())
61      ))
62    }
63  }
64}
65
66impl ToCss for DisplayInside {
67  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
68  where
69    W: std::fmt::Write,
70  {
71    match self {
72      DisplayInside::Flow => dest.write_str("flow"),
73      DisplayInside::FlowRoot => dest.write_str("flow-root"),
74      DisplayInside::Table => dest.write_str("table"),
75      DisplayInside::Flex(prefix) => {
76        prefix.to_css(dest)?;
77        if *prefix == VendorPrefix::Ms {
78          dest.write_str("flexbox")
79        } else {
80          dest.write_str("flex")
81        }
82      }
83      DisplayInside::Box(prefix) => {
84        prefix.to_css(dest)?;
85        dest.write_str("box")
86      }
87      DisplayInside::Grid => dest.write_str("grid"),
88      DisplayInside::Ruby => dest.write_str("ruby"),
89    }
90  }
91}
92
93impl DisplayInside {
94  fn is_equivalent(&self, other: &DisplayInside) -> bool {
95    match (self, other) {
96      (DisplayInside::Flex(_), DisplayInside::Flex(_)) => true,
97      (DisplayInside::Box(_), DisplayInside::Box(_)) => true,
98      (DisplayInside::Flex(_), DisplayInside::Box(_)) => true,
99      (DisplayInside::Box(_), DisplayInside::Flex(_)) => true,
100      _ => self == other,
101    }
102  }
103}
104
105/// A pair of inside and outside display values, as used in the `display` property.
106///
107/// See [Display](Display).
108#[derive(Debug, Clone, PartialEq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub struct DisplayPair {
111  /// The outside display value.
112  pub outside: DisplayOutside,
113  /// The inside display value.
114  pub inside: DisplayInside,
115  /// Whether this is a list item.
116  pub is_list_item: bool,
117}
118
119impl<'i> Parse<'i> for DisplayPair {
120  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
121    let mut list_item = false;
122    let mut outside = None;
123    let mut inside = None;
124
125    loop {
126      if input.try_parse(|input| input.expect_ident_matching("list-item")).is_ok() {
127        list_item = true;
128        continue;
129      }
130
131      if outside.is_none() {
132        if let Ok(o) = input.try_parse(DisplayOutside::parse) {
133          outside = Some(o);
134          continue;
135        }
136      }
137
138      if inside.is_none() {
139        if let Ok(i) = input.try_parse(DisplayInside::parse) {
140          inside = Some(i);
141          continue;
142        }
143      }
144
145      break;
146    }
147
148    if list_item || inside.is_some() || outside.is_some() {
149      let inside = inside.unwrap_or(DisplayInside::Flow);
150      let outside = outside.unwrap_or(match inside {
151        // "If <display-outside> is omitted, the element’s outside display type
152        // defaults to block — except for ruby, which defaults to inline."
153        // https://drafts.csswg.org/css-display/#inside-model
154        DisplayInside::Ruby => DisplayOutside::Inline,
155        _ => DisplayOutside::Block,
156      });
157
158      if list_item && !matches!(inside, DisplayInside::Flow | DisplayInside::FlowRoot) {
159        return Err(input.new_custom_error(ParserError::InvalidDeclaration));
160      }
161
162      return Ok(DisplayPair {
163        outside,
164        inside,
165        is_list_item: list_item,
166      });
167    }
168
169    let location = input.current_source_location();
170    let ident = input.expect_ident()?;
171    match_ignore_ascii_case! { &*ident,
172      "inline-block" => Ok(DisplayPair {
173        outside: DisplayOutside::Inline,
174        inside: DisplayInside::FlowRoot,
175        is_list_item: false
176      }),
177      "inline-table" => Ok(DisplayPair {
178        outside: DisplayOutside::Inline,
179        inside: DisplayInside::Table,
180        is_list_item: false
181      }),
182      "inline-flex" => Ok(DisplayPair {
183        outside: DisplayOutside::Inline,
184        inside: DisplayInside::Flex(VendorPrefix::None),
185        is_list_item: false
186      }),
187      "-webkit-inline-flex" => Ok(DisplayPair {
188        outside: DisplayOutside::Inline,
189        inside: DisplayInside::Flex(VendorPrefix::WebKit),
190        is_list_item: false
191      }),
192      "-ms-inline-flexbox" => Ok(DisplayPair {
193        outside: DisplayOutside::Inline,
194        inside: DisplayInside::Flex(VendorPrefix::Ms),
195        is_list_item: false
196      }),
197      "-webkit-inline-box" => Ok(DisplayPair {
198        outside: DisplayOutside::Inline,
199        inside: DisplayInside::Box(VendorPrefix::WebKit),
200        is_list_item: false
201      }),
202      "-moz-inline-box" => Ok(DisplayPair {
203        outside: DisplayOutside::Inline,
204        inside: DisplayInside::Box(VendorPrefix::Moz),
205        is_list_item: false
206      }),
207      "inline-grid" => Ok(DisplayPair {
208        outside: DisplayOutside::Inline,
209        inside: DisplayInside::Grid,
210        is_list_item: false
211      }),
212      _ => Err(location.new_unexpected_token_error(
213        cssparser::Token::Ident(ident.clone())
214      ))
215    }
216  }
217}
218
219impl ToCss for DisplayPair {
220  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
221  where
222    W: std::fmt::Write,
223  {
224    match self {
225      DisplayPair {
226        outside: DisplayOutside::Inline,
227        inside: DisplayInside::FlowRoot,
228        is_list_item: false,
229      } => dest.write_str("inline-block"),
230      DisplayPair {
231        outside: DisplayOutside::Inline,
232        inside: DisplayInside::Table,
233        is_list_item: false,
234      } => dest.write_str("inline-table"),
235      DisplayPair {
236        outside: DisplayOutside::Inline,
237        inside: DisplayInside::Flex(prefix),
238        is_list_item: false,
239      } => {
240        prefix.to_css(dest)?;
241        if *prefix == VendorPrefix::Ms {
242          dest.write_str("inline-flexbox")
243        } else {
244          dest.write_str("inline-flex")
245        }
246      }
247      DisplayPair {
248        outside: DisplayOutside::Inline,
249        inside: DisplayInside::Box(prefix),
250        is_list_item: false,
251      } => {
252        prefix.to_css(dest)?;
253        dest.write_str("inline-box")
254      }
255      DisplayPair {
256        outside: DisplayOutside::Inline,
257        inside: DisplayInside::Grid,
258        is_list_item: false,
259      } => dest.write_str("inline-grid"),
260      DisplayPair {
261        outside,
262        inside,
263        is_list_item,
264      } => {
265        let default_outside = match inside {
266          DisplayInside::Ruby => DisplayOutside::Inline,
267          _ => DisplayOutside::Block,
268        };
269
270        let mut needs_space = false;
271        if *outside != default_outside || (*inside == DisplayInside::Flow && !*is_list_item) {
272          outside.to_css(dest)?;
273          needs_space = true;
274        }
275
276        if *inside != DisplayInside::Flow {
277          if needs_space {
278            dest.write_char(' ')?;
279          }
280          inside.to_css(dest)?;
281          needs_space = true;
282        }
283
284        if *is_list_item {
285          if needs_space {
286            dest.write_char(' ')?;
287          }
288          dest.write_str("list-item")?;
289        }
290
291        Ok(())
292      }
293    }
294  }
295}
296
297enum_property! {
298  /// A `display` keyword.
299  ///
300  /// See [Display](Display).
301  #[allow(missing_docs)]
302  pub enum DisplayKeyword {
303    "none": None,
304    "contents": Contents,
305    "table-row-group": TableRowGroup,
306    "table-header-group": TableHeaderGroup,
307    "table-footer-group": TableFooterGroup,
308    "table-row": TableRow,
309    "table-cell": TableCell,
310    "table-column-group": TableColumnGroup,
311    "table-column": TableColumn,
312    "table-caption": TableCaption,
313    "ruby-base": RubyBase,
314    "ruby-text": RubyText,
315    "ruby-base-container": RubyBaseContainer,
316    "ruby-text-container": RubyTextContainer,
317  }
318}
319
320/// A value for the [display](https://drafts.csswg.org/css-display-3/#the-display-properties) property.
321#[derive(Debug, Clone, PartialEq)]
322#[cfg_attr(
323  feature = "serde",
324  derive(serde::Serialize, serde::Deserialize),
325  serde(tag = "type", content = "value", rename_all = "kebab-case")
326)]
327pub enum Display {
328  /// A display keyword.
329  Keyword(DisplayKeyword),
330  /// The inside and outside display values.
331  Pair(DisplayPair),
332}
333
334impl<'i> Parse<'i> for Display {
335  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
336    if let Ok(pair) = input.try_parse(DisplayPair::parse) {
337      return Ok(Display::Pair(pair));
338    }
339
340    let keyword = DisplayKeyword::parse(input)?;
341    Ok(Display::Keyword(keyword))
342  }
343}
344
345impl ToCss for Display {
346  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
347  where
348    W: std::fmt::Write,
349  {
350    match self {
351      Display::Keyword(keyword) => keyword.to_css(dest),
352      Display::Pair(pair) => pair.to_css(dest),
353    }
354  }
355}
356
357enum_property! {
358  /// A value for the [visibility](https://drafts.csswg.org/css-display-3/#visibility) property.
359  pub enum Visibility {
360    /// The element is visible.
361    Visible,
362    /// The element is hidden.
363    Hidden,
364    /// The element is collapsed.
365    Collapse,
366  }
367}
368
369#[derive(Default)]
370pub(crate) struct DisplayHandler<'i> {
371  targets: Option<Browsers>,
372  decls: Vec<Property<'i>>,
373  display: Option<Display>,
374}
375
376impl<'i> DisplayHandler<'i> {
377  pub fn new(targets: Option<Browsers>) -> Self {
378    DisplayHandler {
379      targets,
380      ..DisplayHandler::default()
381    }
382  }
383}
384
385impl<'i> PropertyHandler<'i> for DisplayHandler<'i> {
386  fn handle_property(
387    &mut self,
388    property: &Property<'i>,
389    dest: &mut DeclarationList<'i>,
390    context: &mut PropertyHandlerContext<'i, '_>,
391  ) -> bool {
392    if let Property::Display(display) = property {
393      match (&self.display, display) {
394        (Some(Display::Pair(cur)), Display::Pair(new)) => {
395          // If the new value is different but equivalent (e.g. different vendor prefix),
396          // we need to preserve multiple values.
397          if cur.outside == new.outside
398            && cur.is_list_item == new.is_list_item
399            && cur.inside != new.inside
400            && cur.inside.is_equivalent(&new.inside)
401          {
402            // If we have targets, and there is no vendor prefix, clear the existing
403            // declarations. The prefixes will be filled in later. Otherwise, if there
404            // are no targets, or there is a vendor prefix, add a new declaration.
405            if self.targets.is_some() && new.inside == DisplayInside::Flex(VendorPrefix::None) {
406              self.decls.clear();
407            } else if self.targets.is_none() || cur.inside != DisplayInside::Flex(VendorPrefix::None) {
408              self.decls.push(Property::Display(self.display.clone().unwrap()));
409            }
410          }
411        }
412        _ => {}
413      }
414
415      self.display = Some(display.clone());
416      return true;
417    }
418
419    if matches!(
420      property,
421      Property::Unparsed(UnparsedProperty {
422        property_id: PropertyId::Display,
423        ..
424      })
425    ) {
426      self.finalize(dest, context);
427      dest.push(property.clone());
428      return true;
429    }
430
431    false
432  }
433
434  fn finalize(&mut self, dest: &mut DeclarationList<'i>, _: &mut PropertyHandlerContext<'i, '_>) {
435    if self.display.is_none() {
436      return;
437    }
438
439    dest.extend(self.decls.drain(..));
440
441    if let Some(display) = std::mem::take(&mut self.display) {
442      // If we have an unprefixed `flex` value, then add the necessary prefixed values.
443      if let Display::Pair(DisplayPair {
444        inside: DisplayInside::Flex(VendorPrefix::None),
445        outside,
446        ..
447      }) = display
448      {
449        if let Some(targets) = self.targets {
450          let prefixes = Feature::DisplayFlex.prefixes_for(targets);
451
452          // Handle legacy -webkit-box/-moz-box values if needed.
453          if is_flex_2009(targets) {
454            if prefixes.contains(VendorPrefix::WebKit) {
455              dest.push(Property::Display(Display::Pair(DisplayPair {
456                inside: DisplayInside::Box(VendorPrefix::WebKit),
457                outside: outside.clone(),
458                is_list_item: false,
459              })));
460            }
461
462            if prefixes.contains(VendorPrefix::Moz) {
463              dest.push(Property::Display(Display::Pair(DisplayPair {
464                inside: DisplayInside::Box(VendorPrefix::Moz),
465                outside: outside.clone(),
466                is_list_item: false,
467              })));
468            }
469          }
470
471          if prefixes.contains(VendorPrefix::WebKit) {
472            dest.push(Property::Display(Display::Pair(DisplayPair {
473              inside: DisplayInside::Flex(VendorPrefix::WebKit),
474              outside: outside.clone(),
475              is_list_item: false,
476            })));
477          }
478
479          if prefixes.contains(VendorPrefix::Ms) {
480            dest.push(Property::Display(Display::Pair(DisplayPair {
481              inside: DisplayInside::Flex(VendorPrefix::Ms),
482              outside: outside.clone(),
483              is_list_item: false,
484            })));
485          }
486        }
487      }
488
489      dest.push(Property::Display(display))
490    }
491  }
492}