Skip to main content

takumi_core/style/properties/
traits.rs

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
10/// Parser result type alias for CSS property parsers.
11pub(crate) type ParseResult<'i, T> = Result<T, CssParseError<ValueError<'i>>>;
12
13/// Why a CSS value failed to parse, put into words only when a caller shows it: parsers try
14/// alternatives and drop most failures.
15#[derive(Debug, Clone, PartialEq)]
16pub(crate) enum ValueError<'i> {
17  Message(Cow<'i, str>),
18  /// `token` where `expect` asked for one of `valid_tokens`.
19  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/// Owned error returned by [`FromCssStr::from_css_str`]. Carries a
49/// human-readable message and borrows nothing from the parser, keeping
50/// `cssparser` out of the public API.
51#[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  /// Stringifies an internal `cssparser` error into an owned [`ParseError`].
66  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
76/// Runs the internal `FromCss` parser over `source`, mapping the borrowed
77/// `cssparser` error into the owned public [`ParseError`].
78pub(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
87/// Parses a CSS value type from a string.
88pub trait FromCssStr: Sized {
89  /// Parses `source` into this value type.
90  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
102/// Compact identifiers for frequently reused CSS syntax tokens.
103/// Total length of `lists` flattened, for sizing the [`merge_token_lists`] array.
104pub(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
113/// Concatenates token lists into a single array, e.g. to build a shorthand's `VALID_TOKENS` from
114/// its longhands'. `N` must be [`merged_token_len`].
115pub(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/// The index of the keyword `ident` names in `tokens`, skipping `<syntax>` entries, which an
134/// escaped ident could otherwise spell.
135#[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/// Defines reusable message templates for CSS parse errors.
171#[derive(Debug, Clone, Copy, PartialEq)]
172#[non_exhaustive]
173pub(crate) enum CssExpectedMessage {
174  /// Expects a value or the `none` keyword.
175  ValueOrNone,
176  /// Expects exactly one value.
177  OneValue,
178  /// Expects one or two values.
179  OneOrTwoValues,
180  /// Expects one to four values.
181  OneToFourValues,
182  /// Expects the border-radius shorthand grammar.
183  BorderRadius,
184}
185
186impl CssExpectedMessage {
187  /// Builds the parse-error message for an unexpected token.
188  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
207/// Trait for types that can be parsed from CSS.
208pub(crate) trait FromCss<'i> {
209  /// Parses the type from a [`Parser`] instance.
210  fn from_css(input: &mut Parser<'i>) -> ParseResult<'i, Self>
211  where
212    Self: Sized;
213
214  /// The keywords this type accepts, and the `<syntax>` of each other value it takes, for
215  /// parse errors.
216  const VALID_TOKENS: &'static [&'static str];
217
218  /// Message template used when building parse errors for this type.
219  const EXPECT_MESSAGE: CssExpectedMessage = CssExpectedMessage::OneValue;
220}
221
222impl<'i, T: FromCss<'i>> FromCss<'i> for Option<T> {
223  // 'none' is intentionally omitted and applied in `expect_message`
224  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      // `none, ...` starts a list whose first item is `none`, as `background-image: none, none`.
239      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
262/// Converts a parsed/inherited value into a computed value for the current node context.
263pub(crate) trait MakeComputed {
264  /// Default no-op for types that do not need computed-value normalization.
265  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  /// The value `progress` of the way from `from` to `to`.
281  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
307/// The discrete animation step: `from` before the midpoint, `to` from it on.
308pub(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
416// Matches Blink's `kRepeatableListMaxLength` (list_interpolation_functions.cc);
417// transitions restarted on an already-animating value otherwise compound the
418// LCM expansion until it exhausts memory. See crbug.com/739197.
419const 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
522/// Serialize a style value to its CSS string representation.
523pub trait ToCss {
524  /// Separator used between items when this type is serialized in a `Vec`/`[T]`
525  /// list. Defaults to `", "`; space-separated grammars (e.g. `filter`, grid
526  /// track lists) override it.
527  const LIST_SEPARATOR: &'static str = ", ";
528
529  /// Keyword an empty `Vec`/`[T]` list of this type serializes to instead of an
530  /// empty string (`none` for `filter`/`grid-template-*`, `normal` for
531  /// `font-feature-settings`/`font-variation-settings`). `None` keeps the empty
532  /// string.
533  const EMPTY_LIST_KEYWORD: Option<&'static str> = None;
534
535  /// Write the CSS representation of this value into `dest`.
536  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result;
537
538  /// The CSS representation of this value as an owned `String`.
539  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
561/// Writes `keywords` space-separated, or `empty` when there are none.
562pub(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
666/// Macro to implement a `pub(crate)` conversion method into a foreign (taffy) enum.
667macro_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
681/// Implements `FromCss` for a `Box<[$elem]>` list as a comma-separated list of `$elem`.
682macro_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
701/// Treats the first keyword in each group as canonical and the rest as aliases.
702macro_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
808/// Declares a box-alignment enum parser that accepts the optional `safe`/`unsafe`
809/// overflow-position prefix on its positional keywords.
810macro_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}