lightningcss/properties/
border_image.rs

1//! CSS properties related to border images.
2
3use crate::context::PropertyHandlerContext;
4use crate::declaration::{DeclarationBlock, DeclarationList};
5use crate::error::{ParserError, PrinterError};
6use crate::prefixes::Feature;
7use crate::printer::Printer;
8use crate::properties::{Property, PropertyId, VendorPrefix};
9use crate::targets::{Browsers, Targets};
10use crate::traits::{IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
11use crate::values::image::Image;
12use crate::values::number::CSSNumber;
13use crate::values::rect::Rect;
14#[cfg(feature = "visitor")]
15use crate::visitor::Visit;
16use crate::{compat, macros::*};
17use crate::{
18  traits::FallbackValues,
19  values::{
20    length::*,
21    percentage::{NumberOrPercentage, Percentage},
22  },
23};
24use cssparser::*;
25
26enum_property! {
27  /// A single [border-image-repeat](https://www.w3.org/TR/css-backgrounds-3/#border-image-repeat) keyword.
28  pub enum BorderImageRepeatKeyword {
29    /// The image is stretched to fill the area.
30    Stretch,
31    /// The image is tiled (repeated) to fill the area.
32    Repeat,
33     /// The image is scaled so that it repeats an even number of times.
34    Round,
35    /// The image is repeated so that it fits, and then spaced apart evenly.
36    Space,
37  }
38}
39
40impl IsCompatible for BorderImageRepeatKeyword {
41  fn is_compatible(&self, browsers: Browsers) -> bool {
42    use BorderImageRepeatKeyword::*;
43    match self {
44      Round => compat::Feature::BorderImageRepeatRound.is_compatible(browsers),
45      Space => compat::Feature::BorderImageRepeatSpace.is_compatible(browsers),
46      Stretch | Repeat => true,
47    }
48  }
49}
50
51/// A value for the [border-image-repeat](https://www.w3.org/TR/css-backgrounds-3/#border-image-repeat) property.
52#[derive(Debug, Clone, PartialEq)]
53#[cfg_attr(feature = "visitor", derive(Visit))]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
56#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
57pub struct BorderImageRepeat {
58  /// The horizontal repeat value.
59  pub horizontal: BorderImageRepeatKeyword,
60  /// The vertical repeat value.
61  pub vertical: BorderImageRepeatKeyword,
62}
63
64impl Default for BorderImageRepeat {
65  fn default() -> BorderImageRepeat {
66    BorderImageRepeat {
67      horizontal: BorderImageRepeatKeyword::Stretch,
68      vertical: BorderImageRepeatKeyword::Stretch,
69    }
70  }
71}
72
73impl<'i> Parse<'i> for BorderImageRepeat {
74  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
75    let horizontal = BorderImageRepeatKeyword::parse(input)?;
76    let vertical = input.try_parse(BorderImageRepeatKeyword::parse).ok();
77    Ok(BorderImageRepeat {
78      horizontal,
79      vertical: vertical.unwrap_or(horizontal),
80    })
81  }
82}
83
84impl ToCss for BorderImageRepeat {
85  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
86  where
87    W: std::fmt::Write,
88  {
89    self.horizontal.to_css(dest)?;
90    if self.horizontal != self.vertical {
91      dest.write_str(" ")?;
92      self.vertical.to_css(dest)?;
93    }
94    Ok(())
95  }
96}
97
98impl IsCompatible for BorderImageRepeat {
99  fn is_compatible(&self, browsers: Browsers) -> bool {
100    self.horizontal.is_compatible(browsers) && self.vertical.is_compatible(browsers)
101  }
102}
103
104/// A value for the [border-image-width](https://www.w3.org/TR/css-backgrounds-3/#border-image-width) property.
105#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
106#[cfg_attr(feature = "visitor", derive(Visit))]
107#[cfg_attr(
108  feature = "serde",
109  derive(serde::Serialize, serde::Deserialize),
110  serde(tag = "type", content = "value", rename_all = "kebab-case")
111)]
112#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
113#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
114pub enum BorderImageSideWidth {
115  /// A number representing a multiple of the border width.
116  Number(CSSNumber),
117  /// An explicit length or percentage.
118  LengthPercentage(LengthPercentage),
119  /// The `auto` keyword, representing the natural width of the image slice.
120  Auto,
121}
122
123impl Default for BorderImageSideWidth {
124  fn default() -> BorderImageSideWidth {
125    BorderImageSideWidth::Number(1.0)
126  }
127}
128
129impl IsCompatible for BorderImageSideWidth {
130  fn is_compatible(&self, browsers: Browsers) -> bool {
131    match self {
132      BorderImageSideWidth::LengthPercentage(l) => l.is_compatible(browsers),
133      _ => true,
134    }
135  }
136}
137
138/// A value for the [border-image-slice](https://www.w3.org/TR/css-backgrounds-3/#border-image-slice) property.
139#[derive(Debug, Clone, PartialEq)]
140#[cfg_attr(feature = "visitor", derive(Visit))]
141#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
142#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
143#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
144pub struct BorderImageSlice {
145  /// The offsets from the edges of the image.
146  pub offsets: Rect<NumberOrPercentage>,
147  /// Whether the middle of the border image should be preserved.
148  pub fill: bool,
149}
150
151impl Default for BorderImageSlice {
152  fn default() -> BorderImageSlice {
153    BorderImageSlice {
154      offsets: Rect::all(NumberOrPercentage::Percentage(Percentage(1.0))),
155      fill: false,
156    }
157  }
158}
159
160impl<'i> Parse<'i> for BorderImageSlice {
161  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
162    let mut fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok();
163    let offsets = Rect::parse(input)?;
164    if !fill {
165      fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok();
166    }
167    Ok(BorderImageSlice { offsets, fill })
168  }
169}
170
171impl ToCss for BorderImageSlice {
172  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
173  where
174    W: std::fmt::Write,
175  {
176    self.offsets.to_css(dest)?;
177    if self.fill {
178      dest.write_str(" fill")?;
179    }
180    Ok(())
181  }
182}
183
184impl IsCompatible for BorderImageSlice {
185  fn is_compatible(&self, _browsers: Browsers) -> bool {
186    true
187  }
188}
189
190define_shorthand! {
191  /// A value for the [border-image](https://www.w3.org/TR/css-backgrounds-3/#border-image) shorthand property.
192  #[derive(Default)]
193  pub struct BorderImage<'i>(VendorPrefix) {
194    /// The border image.
195    #[cfg_attr(feature = "serde", serde(borrow))]
196    source: BorderImageSource(Image<'i>),
197    /// The offsets that define where the image is sliced.
198    slice: BorderImageSlice(BorderImageSlice),
199    /// The width of the border image.
200    width: BorderImageWidth(Rect<BorderImageSideWidth>),
201    /// The amount that the image extends beyond the border box.
202    outset: BorderImageOutset(Rect<LengthOrNumber>),
203    /// How the border image is scaled and tiled.
204    repeat: BorderImageRepeat(BorderImageRepeat),
205  }
206}
207
208impl<'i> Parse<'i> for BorderImage<'i> {
209  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
210    BorderImage::parse_with_callback(input, |_| false)
211  }
212}
213
214impl<'i> BorderImage<'i> {
215  pub(crate) fn parse_with_callback<'t, F>(
216    input: &mut Parser<'i, 't>,
217    mut callback: F,
218  ) -> Result<Self, ParseError<'i, ParserError<'i>>>
219  where
220    F: FnMut(&mut Parser<'i, 't>) -> bool,
221  {
222    let mut source: Option<Image> = None;
223    let mut slice: Option<BorderImageSlice> = None;
224    let mut width: Option<Rect<BorderImageSideWidth>> = None;
225    let mut outset: Option<Rect<LengthOrNumber>> = None;
226    let mut repeat: Option<BorderImageRepeat> = None;
227    loop {
228      if slice.is_none() {
229        if let Ok(value) = input.try_parse(|input| BorderImageSlice::parse(input)) {
230          slice = Some(value);
231          // Parse border image width and outset, if applicable.
232          let maybe_width_outset: Result<_, cssparser::ParseError<'_, ParserError<'i>>> =
233            input.try_parse(|input| {
234              input.expect_delim('/')?;
235
236              // Parse border image width, if applicable.
237              let w = input.try_parse(|input| Rect::parse(input)).ok();
238
239              // Parse border image outset if applicable.
240              let o = input
241                .try_parse(|input| {
242                  input.expect_delim('/')?;
243                  Rect::parse(input)
244                })
245                .ok();
246              if w.is_none() && o.is_none() {
247                Err(input.new_custom_error(ParserError::InvalidDeclaration))
248              } else {
249                Ok((w, o))
250              }
251            });
252          if let Ok((w, o)) = maybe_width_outset {
253            width = w;
254            outset = o;
255          }
256          continue;
257        }
258      }
259
260      if source.is_none() {
261        if let Ok(value) = input.try_parse(|input| Image::parse(input)) {
262          source = Some(value);
263          continue;
264        }
265      }
266
267      if repeat.is_none() {
268        if let Ok(value) = input.try_parse(|input| BorderImageRepeat::parse(input)) {
269          repeat = Some(value);
270          continue;
271        }
272      }
273
274      if callback(input) {
275        continue;
276      }
277
278      break;
279    }
280
281    if source.is_some() || slice.is_some() || width.is_some() || outset.is_some() || repeat.is_some() {
282      Ok(BorderImage {
283        source: source.unwrap_or_default(),
284        slice: slice.unwrap_or_default(),
285        width: width.unwrap_or(Rect::all(BorderImageSideWidth::default())),
286        outset: outset.unwrap_or(Rect::all(LengthOrNumber::default())),
287        repeat: repeat.unwrap_or_default(),
288      })
289    } else {
290      Err(input.new_custom_error(ParserError::InvalidDeclaration))
291    }
292  }
293
294  pub(crate) fn to_css_internal<W>(
295    source: &Image<'i>,
296    slice: &BorderImageSlice,
297    width: &Rect<BorderImageSideWidth>,
298    outset: &Rect<LengthOrNumber>,
299    repeat: &BorderImageRepeat,
300    dest: &mut Printer<W>,
301  ) -> Result<(), PrinterError>
302  where
303    W: std::fmt::Write,
304  {
305    if *source != Image::default() {
306      source.to_css(dest)?;
307    }
308    let has_slice = *slice != BorderImageSlice::default();
309    let has_width = *width != Rect::all(BorderImageSideWidth::default());
310    let has_outset = *outset != Rect::all(LengthOrNumber::Number(0.0));
311    if has_slice || has_width || has_outset {
312      dest.write_str(" ")?;
313      slice.to_css(dest)?;
314      if has_width || has_outset {
315        dest.delim('/', true)?;
316      }
317      if has_width {
318        width.to_css(dest)?;
319      }
320
321      if has_outset {
322        dest.delim('/', true)?;
323        outset.to_css(dest)?;
324      }
325    }
326
327    if *repeat != BorderImageRepeat::default() {
328      dest.write_str(" ")?;
329      repeat.to_css(dest)?;
330    }
331
332    Ok(())
333  }
334}
335
336impl<'i> ToCss for BorderImage<'i> {
337  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
338  where
339    W: std::fmt::Write,
340  {
341    BorderImage::to_css_internal(&self.source, &self.slice, &self.width, &self.outset, &self.repeat, dest)
342  }
343}
344
345impl<'i> FallbackValues for BorderImage<'i> {
346  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
347    self
348      .source
349      .get_fallbacks(targets)
350      .into_iter()
351      .map(|source| BorderImage { source, ..self.clone() })
352      .collect()
353  }
354}
355
356property_bitflags! {
357  #[derive(Default, Debug)]
358  struct BorderImageProperty: u16 {
359    const BorderImageSource = 1 << 0;
360    const BorderImageSlice = 1 << 1;
361    const BorderImageWidth = 1 << 2;
362    const BorderImageOutset = 1 << 3;
363    const BorderImageRepeat = 1 << 4;
364    const BorderImage(_vp) = Self::BorderImageSource.bits() | Self::BorderImageSlice.bits() | Self::BorderImageWidth.bits() | Self::BorderImageOutset.bits() | Self::BorderImageRepeat.bits();
365  }
366}
367
368#[derive(Debug)]
369pub(crate) struct BorderImageHandler<'i> {
370  source: Option<Image<'i>>,
371  slice: Option<BorderImageSlice>,
372  width: Option<Rect<BorderImageSideWidth>>,
373  outset: Option<Rect<LengthOrNumber>>,
374  repeat: Option<BorderImageRepeat>,
375  vendor_prefix: VendorPrefix,
376  flushed_properties: BorderImageProperty,
377  has_any: bool,
378}
379
380impl<'i> Default for BorderImageHandler<'i> {
381  fn default() -> Self {
382    BorderImageHandler {
383      vendor_prefix: VendorPrefix::empty(),
384      source: None,
385      slice: None,
386      width: None,
387      outset: None,
388      repeat: None,
389      flushed_properties: BorderImageProperty::empty(),
390      has_any: false,
391    }
392  }
393}
394
395impl<'i> PropertyHandler<'i> for BorderImageHandler<'i> {
396  fn handle_property(
397    &mut self,
398    property: &Property<'i>,
399    dest: &mut DeclarationList<'i>,
400    context: &mut PropertyHandlerContext<'i, '_>,
401  ) -> bool {
402    use Property::*;
403    macro_rules! property {
404      ($name: ident, $val: ident) => {{
405        if self.vendor_prefix != VendorPrefix::None {
406          self.flush(dest, context);
407        }
408        flush!($name, $val);
409        self.vendor_prefix = VendorPrefix::None;
410        self.$name = Some($val.clone());
411        self.has_any = true;
412      }};
413    }
414
415    macro_rules! flush {
416      ($name: ident, $val: expr) => {{
417        if self.$name.is_some() && self.$name.as_ref().unwrap() != $val && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets)) {
418          self.flush(dest, context);
419        }
420      }};
421    }
422
423    match property {
424      BorderImageSource(val) => property!(source, val),
425      BorderImageSlice(val) => property!(slice, val),
426      BorderImageWidth(val) => property!(width, val),
427      BorderImageOutset(val) => property!(outset, val),
428      BorderImageRepeat(val) => property!(repeat, val),
429      BorderImage(val, vp) => {
430        flush!(source, &val.source);
431        flush!(slice, &val.slice);
432        flush!(width, &val.width);
433        flush!(outset, &val.outset);
434        flush!(repeat, &val.repeat);
435        self.source = Some(val.source.clone());
436        self.slice = Some(val.slice.clone());
437        self.width = Some(val.width.clone());
438        self.outset = Some(val.outset.clone());
439        self.repeat = Some(val.repeat.clone());
440        self.vendor_prefix |= *vp;
441        self.has_any = true;
442      }
443      Unparsed(val) if is_border_image_property(&val.property_id) => {
444        self.flush(dest, context);
445
446        // Even if we weren't able to parse the value (e.g. due to var() references),
447        // we can still add vendor prefixes to the property itself.
448        let mut unparsed = if matches!(val.property_id, PropertyId::BorderImage(_)) {
449          val.get_prefixed(context.targets, Feature::BorderImage)
450        } else {
451          val.clone()
452        };
453
454        context.add_unparsed_fallbacks(&mut unparsed);
455        self
456          .flushed_properties
457          .insert(BorderImageProperty::try_from(&unparsed.property_id).unwrap());
458        dest.push(Property::Unparsed(unparsed));
459      }
460      _ => return false,
461    }
462
463    true
464  }
465
466  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
467    self.flush(dest, context);
468    self.flushed_properties = BorderImageProperty::empty();
469  }
470}
471
472impl<'i> BorderImageHandler<'i> {
473  pub fn reset(&mut self) {
474    self.source = None;
475    self.slice = None;
476    self.width = None;
477    self.outset = None;
478    self.repeat = None;
479  }
480
481  pub fn will_flush(&self, property: &Property<'i>) -> bool {
482    use Property::*;
483    match property {
484      BorderImageSource(_) | BorderImageSlice(_) | BorderImageWidth(_) | BorderImageOutset(_)
485      | BorderImageRepeat(_) => self.vendor_prefix != VendorPrefix::None,
486      Unparsed(val) => is_border_image_property(&val.property_id),
487      _ => false,
488    }
489  }
490
491  fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
492    if !self.has_any {
493      return;
494    }
495
496    self.has_any = false;
497
498    macro_rules! push {
499      ($prop: ident, $val: expr) => {
500        dest.push(Property::$prop($val));
501        self.flushed_properties.insert(BorderImageProperty::$prop);
502      };
503    }
504
505    let source = std::mem::take(&mut self.source);
506    let slice = std::mem::take(&mut self.slice);
507    let width = std::mem::take(&mut self.width);
508    let outset = std::mem::take(&mut self.outset);
509    let repeat = std::mem::take(&mut self.repeat);
510
511    if source.is_some() && slice.is_some() && width.is_some() && outset.is_some() && repeat.is_some() {
512      let mut border_image = BorderImage {
513        source: source.unwrap(),
514        slice: slice.unwrap(),
515        width: width.unwrap(),
516        outset: outset.unwrap(),
517        repeat: repeat.unwrap(),
518      };
519
520      let mut prefix = self.vendor_prefix;
521      if prefix.contains(VendorPrefix::None) && !border_image.slice.fill {
522        prefix = context.targets.prefixes(self.vendor_prefix, Feature::BorderImage);
523        if !self.flushed_properties.intersects(BorderImageProperty::BorderImage) {
524          let fallbacks = border_image.get_fallbacks(context.targets);
525          for fallback in fallbacks {
526            // Match prefix of fallback. e.g. -webkit-linear-gradient
527            // can only be used in -webkit-border-image, not -moz-border-image.
528            // However, if border-image is unprefixed, gradients can still be.
529            let mut p = fallback.source.get_vendor_prefix() & prefix;
530            if p.is_empty() {
531              p = prefix;
532            }
533            dest.push(Property::BorderImage(fallback, p));
534          }
535        }
536      }
537
538      let p = border_image.source.get_vendor_prefix() & prefix;
539      if !p.is_empty() {
540        prefix = p;
541      }
542
543      dest.push(Property::BorderImage(border_image, prefix));
544      self.flushed_properties.insert(BorderImageProperty::BorderImage);
545    } else {
546      if let Some(mut source) = source {
547        if !self.flushed_properties.contains(BorderImageProperty::BorderImageSource) {
548          let fallbacks = source.get_fallbacks(context.targets);
549          for fallback in fallbacks {
550            dest.push(Property::BorderImageSource(fallback));
551          }
552        }
553
554        push!(BorderImageSource, source);
555      }
556
557      if let Some(slice) = slice {
558        push!(BorderImageSlice, slice);
559      }
560
561      if let Some(width) = width {
562        push!(BorderImageWidth, width);
563      }
564
565      if let Some(outset) = outset {
566        push!(BorderImageOutset, outset);
567      }
568
569      if let Some(repeat) = repeat {
570        push!(BorderImageRepeat, repeat);
571      }
572    }
573
574    self.vendor_prefix = VendorPrefix::empty();
575  }
576}
577
578#[inline]
579fn is_border_image_property(property_id: &PropertyId) -> bool {
580  match property_id {
581    PropertyId::BorderImageSource
582    | PropertyId::BorderImageSlice
583    | PropertyId::BorderImageWidth
584    | PropertyId::BorderImageOutset
585    | PropertyId::BorderImageRepeat
586    | PropertyId::BorderImage(_) => true,
587    _ => false,
588  }
589}