1use std::{borrow::Cow, error::Error, fmt, sync::Arc};
2
3use cssparser::{
4 BasicParseErrorKind, ParseError as CssParseError, ParseErrorKind, Parser,
5 ToCss as CssParserToCss, Token,
6};
7
8use crate::style::{Color, SizingContext, build_unexpected_token, math::lcm, merge_enum_values};
9
10pub(crate) type ParseResult<'i, T> = Result<T, CssParseError<ValueError<'i>>>;
12
13#[derive(Debug, Clone, PartialEq)]
16pub(crate) enum ValueError<'i> {
17 Message(Cow<'i, str>),
18 UnexpectedToken {
20 token: Token<'i>,
21 expect: CssExpectedMessage,
22 valid_tokens: &'static [&'static str],
23 },
24}
25
26impl fmt::Display for ValueError<'_> {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::Message(message) => f.write_str(message),
30 Self::UnexpectedToken {
31 token,
32 expect,
33 valid_tokens,
34 } => f.write_str(&expect.build_message(
35 &CssParserToCss::to_css_string(token),
36 merge_enum_values(valid_tokens),
37 )),
38 }
39 }
40}
41
42impl<'i> From<Cow<'i, str>> for ValueError<'i> {
43 fn from(message: Cow<'i, str>) -> Self {
44 Self::Message(message)
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct ParseError {
53 message: String,
54}
55
56impl fmt::Display for ParseError {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 f.write_str(&self.message)
59 }
60}
61
62impl Error for ParseError {}
63
64impl ParseError {
65 pub(crate) fn from_css_error(error: &CssParseError<ValueError<'_>>) -> Self {
67 let message = match &error.kind {
68 ParseErrorKind::Custom(message) => message.to_string(),
69 basic => format!("{basic:?}"),
70 };
71
72 Self { message }
73 }
74}
75
76pub(crate) fn parse_css_str<T>(source: &str) -> Result<T, ParseError>
79where
80 T: for<'i> FromCss<'i>,
81{
82 let mut parser = Parser::new(source);
83
84 T::from_css(&mut parser).map_err(|error| ParseError::from_css_error(&error))
85}
86
87pub trait FromCssStr: Sized {
89 fn from_css_str(source: &str) -> Result<Self, ParseError>;
91}
92
93impl<T> FromCssStr for T
94where
95 T: for<'i> FromCss<'i>,
96{
97 fn from_css_str(source: &str) -> Result<Self, ParseError> {
98 parse_css_str::<Self>(source)
99 }
100}
101
102pub(crate) const fn merged_token_len(lists: &[&[&'static str]]) -> usize {
105 let (mut len, mut list) = (0, 0);
106 while list < lists.len() {
107 len += lists[list].len();
108 list += 1;
109 }
110 len
111}
112
113pub(crate) const fn merge_token_lists<const N: usize>(
116 lists: &[&[&'static str]],
117) -> [&'static str; N] {
118 let mut merged = [""; N];
119 let (mut index, mut list) = (0, 0);
120 while list < lists.len() {
121 let tokens = lists[list];
122 let mut token = 0;
123 while token < tokens.len() {
124 merged[index] = tokens[token];
125 index += 1;
126 token += 1;
127 }
128 list += 1;
129 }
130 merged
131}
132
133#[inline(never)]
136pub(crate) fn keyword_index(ident: &str, tokens: &[&'static str]) -> Option<usize> {
137 tokens
138 .iter()
139 .position(|token| !token.starts_with('<') && ident.eq_ignore_ascii_case(token))
140}
141
142#[inline(never)]
143pub(crate) fn parse_enum_keyword<'i>(
144 input: &mut Parser<'i>,
145 valid_tokens: &'static [&'static str],
146 expect: CssExpectedMessage,
147) -> ParseResult<'i, usize> {
148 let token = input.next()?;
149
150 let Token::Ident(ident) = token else {
151 return Err(build_unexpected_token(token, expect, valid_tokens));
152 };
153
154 keyword_index(ident, valid_tokens)
155 .ok_or_else(|| build_unexpected_token(token, expect, valid_tokens))
156}
157
158#[inline(never)]
159pub(crate) fn parse_ident_enum_keyword<'i>(
160 input: &mut Parser<'i>,
161 valid_tokens: &'static [&'static str],
162 expect: CssExpectedMessage,
163) -> ParseResult<'i, usize> {
164 let ident = input.expect_ident()?;
165
166 keyword_index(ident, valid_tokens)
167 .ok_or_else(|| build_unexpected_token(&Token::Ident(ident.to_owned()), expect, valid_tokens))
168}
169
170#[derive(Debug, Clone, Copy, PartialEq)]
172#[non_exhaustive]
173pub(crate) enum CssExpectedMessage {
174 ValueOrNone,
176 OneValue,
178 OneOrTwoValues,
180 OneToFourValues,
182 BorderRadius,
184}
185
186impl CssExpectedMessage {
187 pub(crate) fn build_message(&self, token: &str, valid_tokens: String) -> String {
189 match self {
190 Self::ValueOrNone => {
191 format!("Unexpected token: {token}, expected a value of {valid_tokens} or 'none'")
192 }
193 Self::OneValue => format!("Unexpected token: {token}, expected a value of {valid_tokens}"),
194 Self::OneOrTwoValues => {
195 format!("Unexpected token: {token}, expected 1 ~ 2 values of {valid_tokens}")
196 }
197 Self::OneToFourValues => {
198 format!("Unexpected token: {token}, expected 1 ~ 4 values of {valid_tokens}")
199 }
200 Self::BorderRadius => format!(
201 "Unexpected token: {token}, expected 1 to 4 length values for width, optionally followed by '/' and 1 to 4 length values for height"
202 ),
203 }
204 }
205}
206
207pub(crate) trait FromCss<'i> {
209 fn from_css(input: &mut Parser<'i>) -> ParseResult<'i, Self>
211 where
212 Self: Sized;
213
214 const VALID_TOKENS: &'static [&'static str];
217
218 const EXPECT_MESSAGE: CssExpectedMessage = CssExpectedMessage::OneValue;
220}
221
222impl<'i, T: FromCss<'i>> FromCss<'i> for Option<T> {
223 const VALID_TOKENS: &'static [&'static str] = T::VALID_TOKENS;
225
226 const EXPECT_MESSAGE: CssExpectedMessage = CssExpectedMessage::ValueOrNone;
227
228 fn from_css(input: &mut Parser<'i>) -> ParseResult<'i, Self> {
229 let start = input.state();
230
231 if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
232 let after_none = input.state();
233
234 if input.try_parse(Parser::expect_comma).is_err() {
235 return Ok(None);
236 }
237
238 input.reset(&start);
240
241 if let Ok(value) = input.try_parse(T::from_css) {
242 return Ok(Some(value));
243 }
244
245 input.reset(&after_none);
246
247 return Ok(None);
248 }
249
250 T::from_css(input).map(Some)
251 }
252}
253
254impl<'i> FromCss<'i> for String {
255 const VALID_TOKENS: &'static [&'static str] = &["<string>", "<custom-ident>"];
256
257 fn from_css(input: &mut Parser<'i>) -> ParseResult<'i, Self> {
258 Ok(input.expect_ident_or_string()?.to_string())
259 }
260}
261
262pub(crate) trait MakeComputed {
264 fn make_computed(&mut self, _sizing: &SizingContext) {}
266}
267
268pub(crate) trait Animatable: Sized + Clone {
269 fn interpolate(
270 &mut self,
271 from: &Self,
272 to: &Self,
273 progress: f32,
274 _sizing: &SizingContext,
275 _current_color: Color,
276 ) {
277 *self = discrete(from, to, progress);
278 }
279
280 fn interpolated(
282 from: &Self,
283 to: &Self,
284 progress: f32,
285 sizing: &SizingContext,
286 current_color: Color,
287 ) -> Self {
288 let mut value = from.clone();
289
290 value.interpolate(from, to, progress, sizing, current_color);
291 value
292 }
293
294 fn list_interpolation_strategy() -> ListInterpolationStrategy {
295 ListInterpolationStrategy::Discrete
296 }
297
298 fn neutral_value_like(_other: &Self) -> Option<Self> {
299 None
300 }
301
302 fn missing_value() -> Option<Self> {
303 None
304 }
305}
306
307pub(crate) fn discrete<T: Clone>(from: &T, to: &T, progress: f32) -> T {
309 if progress >= 0.5 {
310 to.clone()
311 } else {
312 from.clone()
313 }
314}
315
316pub(crate) enum ListInterpolationStrategy {
317 Discrete,
318 RepeatToLcm,
319 PadToLongestWithNeutral,
320}
321
322impl<T: MakeComputed> MakeComputed for Option<T> {
323 fn make_computed(&mut self, sizing: &SizingContext) {
324 if let Some(value) = self.as_mut() {
325 value.make_computed(sizing);
326 }
327 }
328}
329
330impl<T: MakeComputed> MakeComputed for Box<[T]> {
331 fn make_computed(&mut self, sizing: &SizingContext) {
332 for value in self.iter_mut() {
333 value.make_computed(sizing);
334 }
335 }
336}
337
338impl<T: MakeComputed> MakeComputed for Vec<T> {
339 fn make_computed(&mut self, sizing: &SizingContext) {
340 for value in self.iter_mut() {
341 value.make_computed(sizing);
342 }
343 }
344}
345
346impl<T: Animatable + Clone> Animatable for Option<T> {
347 fn interpolate(
348 &mut self,
349 from: &Self,
350 to: &Self,
351 progress: f32,
352 sizing: &SizingContext,
353 current_color: Color,
354 ) {
355 *self = match (from, to) {
356 (Some(start), Some(end)) => {
357 Some(T::interpolated(start, end, progress, sizing, current_color))
358 }
359 (Some(start), None) => T::missing_value().map_or_else(
360 || discrete(from, to, progress),
361 |missing| {
362 Some(T::interpolated(
363 start,
364 &missing,
365 progress,
366 sizing,
367 current_color,
368 ))
369 },
370 ),
371 (None, Some(end)) => T::missing_value().map_or_else(
372 || discrete(from, to, progress),
373 |missing| {
374 Some(T::interpolated(
375 &missing,
376 end,
377 progress,
378 sizing,
379 current_color,
380 ))
381 },
382 ),
383 (None, None) => None,
384 };
385 }
386}
387
388macro_rules! impl_list_animatable {
389 ($($list:ty),+) => {$(
390 impl<T: Animatable + Clone> Animatable for $list {
391 fn missing_value() -> Option<Self> {
392 match T::list_interpolation_strategy() {
393 ListInterpolationStrategy::Discrete => None,
394 ListInterpolationStrategy::RepeatToLcm
395 | ListInterpolationStrategy::PadToLongestWithNeutral => Some(Self::default()),
396 }
397 }
398
399 fn interpolate(
400 &mut self,
401 from: &Self,
402 to: &Self,
403 progress: f32,
404 sizing: &SizingContext,
405 current_color: Color,
406 ) {
407 *self = interpolate_list(from, to, progress, sizing, current_color)
408 .map_or_else(|| discrete(from, to, progress), Into::into);
409 }
410 }
411 )+};
412}
413
414impl_list_animatable!(Box<[T]>, Arc<[T]>, Vec<T>);
415
416const MAX_INTERPOLATED_LIST_LEN: usize = 1000;
420
421fn interpolate_list<T: Animatable + Clone, C: AsRef<[T]>>(
422 from: &C,
423 to: &C,
424 progress: f32,
425 sizing: &SizingContext,
426 current_color: Color,
427) -> Option<Vec<T>> {
428 let from = from.as_ref();
429 let to = to.as_ref();
430
431 match T::list_interpolation_strategy() {
432 ListInterpolationStrategy::Discrete => {
433 if from.len() != to.len() {
434 return None;
435 }
436
437 Some(interpolate_pairwise_list(
438 from,
439 to,
440 from.len(),
441 progress,
442 sizing,
443 current_color,
444 ))
445 }
446 ListInterpolationStrategy::RepeatToLcm => {
447 if from.is_empty() || to.is_empty() {
448 return None;
449 }
450
451 let output_len = lcm(from.len(), to.len()).min(MAX_INTERPOLATED_LIST_LEN);
452
453 Some(interpolate_pairwise_list(
454 from,
455 to,
456 output_len,
457 progress,
458 sizing,
459 current_color,
460 ))
461 }
462 ListInterpolationStrategy::PadToLongestWithNeutral => {
463 interpolate_neutral_padded_list(from, to, progress, sizing, current_color)
464 }
465 }
466}
467
468fn interpolate_pairwise_list<T: Animatable + Clone>(
469 from: &[T],
470 to: &[T],
471 output_len: usize,
472 progress: f32,
473 sizing: &SizingContext,
474 current_color: Color,
475) -> Vec<T> {
476 (0..output_len)
477 .map(|index| {
478 T::interpolated(
479 &from[index % from.len()],
480 &to[index % to.len()],
481 progress,
482 sizing,
483 current_color,
484 )
485 })
486 .collect()
487}
488
489fn interpolate_neutral_padded_list<T: Animatable + Clone>(
490 from: &[T],
491 to: &[T],
492 progress: f32,
493 sizing: &SizingContext,
494 current_color: Color,
495) -> Option<Vec<T>> {
496 let output_len = from.len().max(to.len());
497
498 (0..output_len)
499 .map(|index| {
500 let from_value = if index < from.len() {
501 from.get(index).cloned()
502 } else {
503 to.get(index).and_then(T::neutral_value_like)
504 }?;
505 let to_value = if index < to.len() {
506 to.get(index).cloned()
507 } else {
508 from.get(index).and_then(T::neutral_value_like)
509 }?;
510
511 Some(T::interpolated(
512 &from_value,
513 &to_value,
514 progress,
515 sizing,
516 current_color,
517 ))
518 })
519 .collect()
520}
521
522pub trait ToCss {
524 const LIST_SEPARATOR: &'static str = ", ";
528
529 const EMPTY_LIST_KEYWORD: Option<&'static str> = None;
534
535 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result;
537
538 fn to_css_string(&self) -> String {
540 let mut css = String::new();
541 let _ = self.to_css(&mut css);
542 css
543 }
544}
545
546impl<T: ToCss + ?Sized> ToCss for &T {
547 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
548 (*self).to_css(dest)
549 }
550}
551
552impl<T: ToCss> ToCss for Option<T> {
553 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
554 match self {
555 Some(v) => v.to_css(dest),
556 None => dest.write_str("none"),
557 }
558 }
559}
560
561pub(crate) fn write_keywords<'k, W: fmt::Write>(
563 dest: &mut W,
564 keywords: impl IntoIterator<Item = &'k str>,
565 empty: &str,
566) -> fmt::Result {
567 let mut keywords = keywords.into_iter().peekable();
568
569 if keywords.peek().is_none() {
570 return dest.write_str(empty);
571 }
572
573 for (index, keyword) in keywords.enumerate() {
574 if index > 0 {
575 dest.write_char(' ')?;
576 }
577
578 dest.write_str(keyword)?;
579 }
580
581 Ok(())
582}
583
584fn write_css_list<W: fmt::Write, T: ToCss>(items: &[T], dest: &mut W) -> fmt::Result {
585 if items.is_empty() {
586 return match T::EMPTY_LIST_KEYWORD {
587 Some(keyword) => dest.write_str(keyword),
588 None => Ok(()),
589 };
590 }
591
592 for (i, item) in items.iter().enumerate() {
593 if i > 0 {
594 dest.write_str(T::LIST_SEPARATOR)?;
595 }
596 item.to_css(dest)?;
597 }
598 Ok(())
599}
600
601impl<T: ToCss> ToCss for Box<[T]> {
602 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
603 write_css_list(self, dest)
604 }
605}
606
607impl<T: ToCss> ToCss for Arc<[T]> {
608 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
609 write_css_list(self, dest)
610 }
611}
612
613impl<T: ToCss> ToCss for Vec<T> {
614 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
615 write_css_list(self, dest)
616 }
617}
618
619impl ToCss for f32 {
620 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
621 write!(dest, "{self}")
622 }
623}
624
625impl ToCss for u32 {
626 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
627 write!(dest, "{self}")
628 }
629}
630
631impl MakeComputed for u32 {}
632impl Animatable for u32 {}
633
634impl<'i> FromCss<'i> for u32 {
635 const VALID_TOKENS: &'static [&'static str] = &["<integer>"];
636
637 fn from_css(input: &mut Parser<'i>) -> ParseResult<'i, Self> {
638 let value = input.expect_integer()?;
639 if value < 0 {
640 return Err(CssParseError::from_basic_kind(
641 BasicParseErrorKind::QualifiedRuleInvalid,
642 ));
643 }
644 Ok(value as u32)
645 }
646}
647
648impl ToCss for i32 {
649 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
650 write!(dest, "{self}")
651 }
652}
653
654impl ToCss for String {
655 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
656 dest.write_str(self)
657 }
658}
659
660impl ToCss for Arc<str> {
661 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
662 dest.write_str(self)
663 }
664}
665
666macro_rules! impl_from_taffy_enum {
668 ($from_ty:ty, $method:ident -> $to_ty:ty, $($variant:ident),*) => {
669 impl $from_ty {
670 pub(crate) fn $method(self) -> $to_ty {
671 match self {
672 $(<$from_ty>::$variant => <$to_ty>::$variant,)*
673 }
674 }
675 }
676 };
677}
678
679pub(crate) use impl_from_taffy_enum;
680
681macro_rules! impl_comma_list_from_css {
683 ($list:ty, $elem:ty) => {
684 $crate::style::properties::impl_comma_list_from_css!($list, $elem, <$elem>::VALID_TOKENS);
685 };
686 ($list:ty, $elem:ty, $valid:expr) => {
687 impl<'i> $crate::style::FromCss<'i> for $list {
688 fn from_css(input: &mut cssparser::Parser<'i>) -> $crate::style::ParseResult<'i, Self> {
689 input
690 .parse_comma_separated(<$elem as $crate::style::FromCss>::from_css)
691 .map(Vec::into_boxed_slice)
692 }
693
694 const VALID_TOKENS: &'static [&'static str] = $valid;
695 }
696 };
697}
698
699pub(crate) use impl_comma_list_from_css;
700
701macro_rules! impl_css_enum {
703 (
704 $enum_type:ty,
705 $($canonical:literal $(| $alias:literal)* => $variant:path),* $(,)?
706 ) => {
707 $crate::style::properties::impl_css_enum!(@impl
708 $crate::style::properties::parse_enum_keyword,
709 default,
710 $enum_type,
711 $($canonical $(| $alias)* => $variant),*
712 );
713 };
714
715 (
716 custom_animatable $enum_type:ty,
717 $($canonical:literal $(| $alias:literal)* => $variant:path),* $(,)?
718 ) => {
719 $crate::style::properties::impl_css_enum!(@impl
720 $crate::style::properties::parse_enum_keyword,
721 custom,
722 $enum_type,
723 $($canonical $(| $alias)* => $variant),*
724 );
725 };
726
727 (ident $enum_type:ty, $($canonical:literal $(| $alias:literal)* => $variant:path),* $(,)?) => {
728 $crate::style::properties::impl_css_enum!(@impl
729 $crate::style::properties::parse_ident_enum_keyword,
730 default,
731 $enum_type,
732 $($canonical $(| $alias)* => $variant),*
733 );
734 };
735
736 (ident keyword $enum_type:ty, $($canonical:literal $(| $alias:literal)* => $variant:path),* $(,)?) => {
737 $crate::style::properties::impl_css_enum!(
738 ident $enum_type,
739 $($canonical $(| $alias)* => $variant),*
740 );
741
742 impl $enum_type {
743 fn from_keyword(ident: &str) -> Option<Self> {
744 $crate::style::keyword_index(
745 ident,
746 <Self as $crate::style::properties::FromCss>::VALID_TOKENS,
747 )
748 .map(|index| Self::KEYWORD_VALUES[index])
749 }
750 }
751 };
752
753 (@impl $parser:path, $animatable:ident, $enum_type:ty, $($canonical:literal $(| $alias:literal)* => $variant:path),* $(,)?) => {
754 impl $enum_type {
755 const KEYWORD_VALUES: &'static [Self] = &[
756 $($variant $(, $crate::style::properties::impl_css_enum!(@value $variant, $alias))*),*
757 ];
758 }
759
760 impl crate::style::MakeComputed for $enum_type {}
761
762 $crate::style::properties::impl_css_enum!(@animatable $animatable $enum_type);
763
764 impl<'i> crate::style::FromCss<'i> for $enum_type {
765 const VALID_TOKENS: &'static [&'static str] = &[
766 $(
767 $canonical
768 $(, $alias)*
769 ),*
770 ];
771
772 fn from_css(input: &mut cssparser::Parser<'i>) -> crate::style::ParseResult<'i, Self> {
773 let index = $parser(
774 input,
775 Self::VALID_TOKENS,
776 <Self as crate::style::FromCss>::EXPECT_MESSAGE,
777 )?;
778
779 Ok(Self::KEYWORD_VALUES[index])
780 }
781 }
782
783 impl crate::style::properties::ToCss for $enum_type {
784 fn to_css<W: std::fmt::Write>(&self, dest: &mut W) -> std::fmt::Result {
785 match self {
786 $(
787 $variant => dest.write_str($canonical),
788 )*
789 }
790 }
791 }
792
793 };
794
795 (@animatable default $enum_type:ty) => {
796 impl $crate::style::Animatable for $enum_type {}
797 };
798
799 (@animatable custom $enum_type:ty) => {};
800
801 (@value $variant:path, $alias:literal) => {
802 $variant
803 };
804}
805
806pub(crate) use impl_css_enum;
807
808macro_rules! declare_box_alignment_enum_impl {
811 (
812 $enum_type:ty,
813 safe { $($safe_css:literal => $base_variant:ident / $safe_variant:ident),+ $(,)? },
814 plain { $($plain_css:literal => $plain_variant:ident),* $(,)? }
815 ) => {
816 impl crate::style::MakeComputed for $enum_type {}
817
818 impl<'i> crate::style::FromCss<'i> for $enum_type {
819 const VALID_TOKENS: &'static [&'static str] = &[
820 $($plain_css,)*
821 $($safe_css,)*
822 "safe",
823 "unsafe",
824 ];
825
826 fn from_css(input: &mut cssparser::Parser<'i>) -> crate::style::ParseResult<'i, Self> {
827 let mut overflow_position = None;
828
829 loop {
830 let token = input.next()?;
831
832 let cssparser::Token::Ident(ident) = token else {
833 return Err($crate::style::unexpected_token!(token));
834 };
835
836 cssparser::match_ignore_ascii_case! {&ident,
837 "safe" | "unsafe" => {
838 if overflow_position.replace(ident.eq_ignore_ascii_case("safe")).is_some() {
839 return Err($crate::style::unexpected_token!(token));
840 }
841 },
842 $($safe_css => return Ok(if overflow_position.unwrap_or(false) { Self::$safe_variant } else { Self::$base_variant }),)*
843 $($plain_css => return if overflow_position.unwrap_or(false) {
844 Err($crate::style::unexpected_token!(token))
845 } else {
846 Ok(Self::$plain_variant)
847 },)*
848 _ => return Err($crate::style::unexpected_token!(token)),
849 }
850 }
851 }
852 }
853
854 impl crate::style::properties::ToCss for $enum_type {
855 fn to_css<W: std::fmt::Write>(&self, dest: &mut W) -> std::fmt::Result {
856 match self {
857 $(Self::$plain_variant => dest.write_str($plain_css),)*
858 $(Self::$base_variant => dest.write_str($safe_css),)*
859 $(Self::$safe_variant => {
860 dest.write_str("safe ")?;
861 dest.write_str($safe_css)
862 })*
863 }
864 }
865 }
866
867 };
868}
869
870pub(crate) use declare_box_alignment_enum_impl;
871
872#[cfg(test)]
873mod tests {
874 use super::*;
875 use crate::{
876 style::{BackgroundRepeat, BackgroundRepeatStyle},
877 viewport::Viewport,
878 };
879
880 fn repeats(len: usize, style: BackgroundRepeatStyle) -> Vec<BackgroundRepeat> {
881 vec![BackgroundRepeat(style, style); len]
882 }
883
884 fn interpolated(from_len: usize, to_len: usize) -> Option<Vec<BackgroundRepeat>> {
885 let sizing = SizingContext::builder()
886 .viewport(Viewport::default())
887 .build();
888
889 interpolate_list(
890 &repeats(from_len, BackgroundRepeatStyle::Repeat),
891 &repeats(to_len, BackgroundRepeatStyle::NoRepeat),
892 0.75,
893 &sizing,
894 Color([0, 0, 0, 255]),
895 )
896 }
897
898 #[test]
899 fn repeat_to_lcm_interpolates_under_the_cap() {
900 assert_eq!(interpolated(2, 3).map(|values| values.len()), Some(6));
901 }
902
903 #[test]
904 fn repeat_to_lcm_clamps_past_the_cap() {
905 assert_eq!(
906 interpolated(61, 67).map(|values| values.len()),
907 Some(MAX_INTERPOLATED_LIST_LEN)
908 );
909 }
910}