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