Skip to main content

azul_css/props/style/
filter.rs

1//! CSS properties for graphical effects like blur, drop-shadow, etc.
2//!
3//! Defines [`StyleFilter`] and [`StyleFilterVec`] for CSS filter functions
4//! (blur, opacity, drop-shadow, color-matrix, brightness, contrast, etc.).
5//! Filters are applied via the `WebRender` compositor (`compositor2`) or the
6//! software CPU renderer (`cpurender`).
7
8use alloc::{
9    string::{String, ToString},
10    vec::Vec,
11};
12use core::{fmt, num::ParseFloatError};
13
14#[cfg(feature = "parser")]
15use crate::props::basic::{
16    error::{InvalidValueErr, InvalidValueErrOwned, WrongComponentCountError},
17    length::parse_float_value,
18    parse::{parse_parentheses, ParenthesisParseError, ParenthesisParseErrorOwned},
19};
20use crate::{
21    codegen::format::GetHash,
22    props::{
23        basic::{
24            angle::{
25                parse_angle_value, AngleValue, CssAngleValueParseError,
26                CssAngleValueParseErrorOwned,
27            },
28            color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
29            length::{FloatValue, PercentageParseError, PercentageValue},
30            pixel::{
31                parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
32                PixelValue,
33            },
34        },
35        formatter::PrintAsCssValue,
36        style::{
37            box_shadow::{
38                parse_style_box_shadow, CssShadowParseError, CssShadowParseErrorOwned,
39                StyleBoxShadow,
40            },
41            effects::{parse_style_mix_blend_mode, MixBlendModeParseError, StyleMixBlendMode},
42        },
43    },
44};
45
46// --- TYPE DEFINITIONS ---
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
49#[repr(C, u8)]
50pub enum StyleFilter {
51    Blend(StyleMixBlendMode),
52    Flood(ColorU),
53    Blur(StyleBlur),
54    Opacity(PercentageValue),
55    ColorMatrix(StyleColorMatrix),
56    DropShadow(StyleBoxShadow),
57    ComponentTransfer,
58    Offset(StyleFilterOffset),
59    Composite(StyleCompositeFilter),
60    // Standard CSS filter functions
61    Brightness(PercentageValue),
62    Contrast(PercentageValue),
63    Grayscale(PercentageValue),
64    HueRotate(AngleValue),
65    Invert(PercentageValue),
66    Saturate(PercentageValue),
67    Sepia(PercentageValue),
68}
69
70impl_option!(
71    StyleFilter,
72    OptionStyleFilter,
73    copy = false,
74    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
75);
76
77impl_vec!(
78    StyleFilter,
79    StyleFilterVec,
80    StyleFilterVecDestructor,
81    StyleFilterVecDestructorType,
82    StyleFilterVecSlice,
83    OptionStyleFilter
84);
85impl_vec_clone!(StyleFilter, StyleFilterVec, StyleFilterVecDestructor);
86impl_vec_debug!(StyleFilter, StyleFilterVec);
87impl_vec_eq!(StyleFilter, StyleFilterVec);
88impl_vec_ord!(StyleFilter, StyleFilterVec);
89impl_vec_hash!(StyleFilter, StyleFilterVec);
90impl_vec_partialeq!(StyleFilter, StyleFilterVec);
91impl_vec_partialord!(StyleFilter, StyleFilterVec);
92
93#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
94#[repr(C)]
95pub struct StyleBlur {
96    pub width: PixelValue,
97    pub height: PixelValue,
98}
99
100/// Color matrix with 20 float values for color transformation.
101/// Layout: 4 rows × 5 columns (RGBA + offset for each channel)
102#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
103#[repr(C)]
104pub struct StyleColorMatrix {
105    pub m0: FloatValue,
106    pub m1: FloatValue,
107    pub m2: FloatValue,
108    pub m3: FloatValue,
109    pub m4: FloatValue,
110    pub m5: FloatValue,
111    pub m6: FloatValue,
112    pub m7: FloatValue,
113    pub m8: FloatValue,
114    pub m9: FloatValue,
115    pub m10: FloatValue,
116    pub m11: FloatValue,
117    pub m12: FloatValue,
118    pub m13: FloatValue,
119    pub m14: FloatValue,
120    pub m15: FloatValue,
121    pub m16: FloatValue,
122    pub m17: FloatValue,
123    pub m18: FloatValue,
124    pub m19: FloatValue,
125}
126
127impl StyleColorMatrix {
128    #[must_use]
129    pub const fn to_array(&self) -> [FloatValue; 20] {
130        [
131            self.m0, self.m1, self.m2, self.m3, self.m4, self.m5, self.m6, self.m7, self.m8,
132            self.m9, self.m10, self.m11, self.m12, self.m13, self.m14, self.m15, self.m16,
133            self.m17, self.m18, self.m19,
134        ]
135    }
136}
137
138#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
139#[repr(C)]
140pub struct StyleFilterOffset {
141    pub x: PixelValue,
142    pub y: PixelValue,
143}
144
145/// Arithmetic coefficients for composite filter (k1, k2, k3, k4).
146/// Result = k1*i1*i2 + k2*i1 + k3*i2 + k4
147#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
148#[repr(C)]
149pub struct ArithmeticCoefficients {
150    pub k1: FloatValue,
151    pub k2: FloatValue,
152    pub k3: FloatValue,
153    pub k4: FloatValue,
154}
155#[allow(variant_size_differences)]
156// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
157#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
158#[repr(C, u8)]
159pub enum StyleCompositeFilter {
160    Over,
161    In,
162    Atop,
163    Out,
164    Xor,
165    Lighter,
166    Arithmetic(ArithmeticCoefficients),
167}
168
169// --- PRINTING IMPLEMENTATIONS ---
170
171impl PrintAsCssValue for StyleFilterVec {
172    fn print_as_css_value(&self) -> String {
173        self.as_ref()
174            .iter()
175            .map(PrintAsCssValue::print_as_css_value)
176            .collect::<Vec<_>>()
177            .join(" ")
178    }
179}
180
181// Formatting to Rust code for StyleFilterVec
182impl crate::codegen::format::FormatAsRustCode for StyleFilterVec {
183    fn format_as_rust_code(&self, _tabs: usize) -> String {
184        format!(
185            "StyleFilterVec::from_const_slice(STYLE_FILTER_{}_ITEMS)",
186            self.get_hash()
187        )
188    }
189}
190
191impl PrintAsCssValue for StyleFilter {
192    fn print_as_css_value(&self) -> String {
193        match self {
194            Self::Blend(mode) => format!("blend({})", mode.print_as_css_value()),
195            Self::Flood(c) => format!("flood({})", c.to_hash()),
196            Self::Blur(c) => {
197                if c.width == c.height {
198                    format!("blur({})", c.width)
199                } else {
200                    format!("blur({} {})", c.width, c.height)
201                }
202            }
203            Self::Opacity(c) => format!("opacity({c})"),
204            Self::ColorMatrix(c) => format!(
205                "color-matrix({})",
206                c.to_array()
207                    .iter()
208                    .map(|s| format!("{s}"))
209                    .collect::<Vec<_>>()
210                    .join(" ")
211            ),
212            Self::DropShadow(shadow) => {
213                format!("drop-shadow({})", shadow.print_as_css_value())
214            }
215            Self::ComponentTransfer => "component-transfer".to_string(),
216            Self::Offset(o) => format!("offset({} {})", o.x, o.y),
217            Self::Composite(c) => format!("composite({})", c.print_as_css_value()),
218            Self::Brightness(v) => format!("brightness({v})"),
219            Self::Contrast(v) => format!("contrast({v})"),
220            Self::Grayscale(v) => format!("grayscale({v})"),
221            Self::HueRotate(a) => format!("hue-rotate({a})"),
222            Self::Invert(v) => format!("invert({v})"),
223            Self::Saturate(v) => format!("saturate({v})"),
224            Self::Sepia(v) => format!("sepia({v})"),
225        }
226    }
227}
228
229impl PrintAsCssValue for StyleCompositeFilter {
230    fn print_as_css_value(&self) -> String {
231        match self {
232            Self::Over => "over".to_string(),
233            Self::In => "in".to_string(),
234            Self::Atop => "atop".to_string(),
235            Self::Out => "out".to_string(),
236            Self::Xor => "xor".to_string(),
237            Self::Lighter => "lighter".to_string(),
238            Self::Arithmetic(fv) => {
239                format!("arithmetic {} {} {} {}", fv.k1, fv.k2, fv.k3, fv.k4)
240            }
241        }
242    }
243}
244
245// --- PARSER ---
246
247#[cfg(feature = "parser")]
248pub mod parser {
249    #[allow(clippy::wildcard_imports)]
250    // parser submodule reuses the parent module's value types
251    use super::*;
252    use crate::corety::AzString;
253    use crate::props::basic::parse_percentage_value;
254
255    // -- Top-level Filter Error --
256
257    #[derive(Clone, PartialEq)]
258    pub enum CssStyleFilterParseError<'a> {
259        InvalidFilter(&'a str),
260        InvalidParenthesis(ParenthesisParseError<'a>),
261        Shadow(CssShadowParseError<'a>),
262        BlendMode(InvalidValueErr<'a>),
263        Color(CssColorParseError<'a>),
264        Opacity(PercentageParseError),
265        Brightness(PercentageParseError),
266        Contrast(PercentageParseError),
267        Saturate(PercentageParseError),
268        Blur(CssStyleBlurParseError<'a>),
269        ColorMatrix(CssStyleColorMatrixParseError<'a>),
270        Offset(CssStyleFilterOffsetParseError<'a>),
271        Composite(CssStyleCompositeFilterParseError<'a>),
272        Angle(CssAngleValueParseError<'a>),
273    }
274
275    impl_debug_as_display!(CssStyleFilterParseError<'a>);
276    impl_display! { CssStyleFilterParseError<'a>, {
277        InvalidFilter(e) => format!("Invalid filter function: \"{}\"", e),
278        InvalidParenthesis(e) => format!("Invalid filter syntax - parenthesis error: {}", e),
279        Shadow(e) => format!("Error parsing drop-shadow(): {}", e),
280        BlendMode(e) => format!("Error parsing blend(): invalid value \"{}\"", e.0),
281        Color(e) => format!("Error parsing flood(): {}", e),
282        Opacity(e) => format!("Error parsing opacity(): {}", e),
283        Brightness(e) => format!("Error parsing brightness(): {}", e),
284        Contrast(e) => format!("Error parsing contrast(): {}", e),
285        Saturate(e) => format!("Error parsing saturate(): {}", e),
286        Blur(e) => format!("Error parsing blur(): {}", e),
287        ColorMatrix(e) => format!("Error parsing color-matrix(): {}", e),
288        Offset(e) => format!("Error parsing offset(): {}", e),
289        Composite(e) => format!("Error parsing composite(): {}", e),
290        Angle(e) => format!("Error parsing hue-rotate(): {}", e),
291    }}
292
293    impl_from!(
294        ParenthesisParseError<'a>,
295        CssStyleFilterParseError::InvalidParenthesis
296    );
297    impl_from!(InvalidValueErr<'a>, CssStyleFilterParseError::BlendMode);
298    impl_from!(CssStyleBlurParseError<'a>, CssStyleFilterParseError::Blur);
299    impl_from!(CssColorParseError<'a>, CssStyleFilterParseError::Color);
300    impl_from!(
301        CssStyleColorMatrixParseError<'a>,
302        CssStyleFilterParseError::ColorMatrix
303    );
304    impl_from!(
305        CssStyleFilterOffsetParseError<'a>,
306        CssStyleFilterParseError::Offset
307    );
308    impl_from!(
309        CssStyleCompositeFilterParseError<'a>,
310        CssStyleFilterParseError::Composite
311    );
312    impl_from!(CssShadowParseError<'a>, CssStyleFilterParseError::Shadow);
313    impl_from!(CssAngleValueParseError<'a>, CssStyleFilterParseError::Angle);
314
315    impl From<PercentageParseError> for CssStyleFilterParseError<'_> {
316        fn from(p: PercentageParseError) -> Self {
317            Self::Opacity(p)
318        }
319    }
320
321    impl<'a> From<MixBlendModeParseError<'a>> for CssStyleFilterParseError<'a> {
322        fn from(e: MixBlendModeParseError<'a>) -> Self {
323            // Extract the InvalidValueErr from the MixBlendModeParseError
324            match e {
325                MixBlendModeParseError::InvalidValue(err) => Self::BlendMode(err),
326            }
327        }
328    }
329
330    #[derive(Debug, Clone, PartialEq)]
331    #[repr(C, u8)]
332    pub enum CssStyleFilterParseErrorOwned {
333        InvalidFilter(AzString),
334        InvalidParenthesis(ParenthesisParseErrorOwned),
335        Shadow(CssShadowParseErrorOwned),
336        BlendMode(InvalidValueErrOwned),
337        Color(CssColorParseErrorOwned),
338        Opacity(PercentageParseError),
339        Brightness(PercentageParseError),
340        Contrast(PercentageParseError),
341        Saturate(PercentageParseError),
342        Blur(CssStyleBlurParseErrorOwned),
343        ColorMatrix(CssStyleColorMatrixParseErrorOwned),
344        Offset(CssStyleFilterOffsetParseErrorOwned),
345        Composite(CssStyleCompositeFilterParseErrorOwned),
346        Angle(CssAngleValueParseErrorOwned),
347    }
348
349    impl CssStyleFilterParseError<'_> {
350        #[must_use]
351        pub fn to_contained(&self) -> CssStyleFilterParseErrorOwned {
352            match self {
353                Self::InvalidFilter(s) => {
354                    CssStyleFilterParseErrorOwned::InvalidFilter((*s).to_string().into())
355                }
356                Self::InvalidParenthesis(e) => {
357                    CssStyleFilterParseErrorOwned::InvalidParenthesis(e.to_contained())
358                }
359                Self::Shadow(e) => CssStyleFilterParseErrorOwned::Shadow(e.to_contained()),
360                Self::BlendMode(e) => CssStyleFilterParseErrorOwned::BlendMode(e.to_contained()),
361                Self::Color(e) => CssStyleFilterParseErrorOwned::Color(e.to_contained()),
362                Self::Opacity(e) => CssStyleFilterParseErrorOwned::Opacity(e.clone()),
363                Self::Brightness(e) => CssStyleFilterParseErrorOwned::Brightness(e.clone()),
364                Self::Contrast(e) => CssStyleFilterParseErrorOwned::Contrast(e.clone()),
365                Self::Saturate(e) => CssStyleFilterParseErrorOwned::Saturate(e.clone()),
366                Self::Blur(e) => CssStyleFilterParseErrorOwned::Blur(e.to_contained()),
367                Self::ColorMatrix(e) => {
368                    CssStyleFilterParseErrorOwned::ColorMatrix(e.to_contained())
369                }
370                Self::Offset(e) => CssStyleFilterParseErrorOwned::Offset(e.to_contained()),
371                Self::Composite(e) => CssStyleFilterParseErrorOwned::Composite(e.to_contained()),
372                Self::Angle(e) => CssStyleFilterParseErrorOwned::Angle(e.to_contained()),
373            }
374        }
375    }
376
377    impl CssStyleFilterParseErrorOwned {
378        #[must_use]
379        pub fn to_shared(&self) -> CssStyleFilterParseError<'_> {
380            match self {
381                Self::InvalidFilter(s) => CssStyleFilterParseError::InvalidFilter(s),
382                Self::InvalidParenthesis(e) => {
383                    CssStyleFilterParseError::InvalidParenthesis(e.to_shared())
384                }
385                Self::Shadow(e) => CssStyleFilterParseError::Shadow(e.to_shared()),
386                Self::BlendMode(e) => CssStyleFilterParseError::BlendMode(e.to_shared()),
387                Self::Color(e) => CssStyleFilterParseError::Color(e.to_shared()),
388                Self::Opacity(e) => CssStyleFilterParseError::Opacity(e.clone()),
389                Self::Brightness(e) => CssStyleFilterParseError::Brightness(e.clone()),
390                Self::Contrast(e) => CssStyleFilterParseError::Contrast(e.clone()),
391                Self::Saturate(e) => CssStyleFilterParseError::Saturate(e.clone()),
392                Self::Blur(e) => CssStyleFilterParseError::Blur(e.to_shared()),
393                Self::ColorMatrix(e) => CssStyleFilterParseError::ColorMatrix(e.to_shared()),
394                Self::Offset(e) => CssStyleFilterParseError::Offset(e.to_shared()),
395                Self::Composite(e) => CssStyleFilterParseError::Composite(e.to_shared()),
396                Self::Angle(e) => CssStyleFilterParseError::Angle(e.to_shared()),
397            }
398        }
399    }
400
401    // -- Sub-Errors for each filter function --
402
403    #[derive(Clone, PartialEq, Eq)]
404    pub enum CssStyleBlurParseError<'a> {
405        Pixel(CssPixelValueParseError<'a>),
406        TooManyComponents(&'a str),
407    }
408
409    impl_debug_as_display!(CssStyleBlurParseError<'a>);
410    impl_display! { CssStyleBlurParseError<'a>, {
411        Pixel(e) => format!("Invalid pixel value: {}", e),
412        TooManyComponents(input) => format!("Expected 1 or 2 components, got more: \"{}\"", input),
413    }}
414    impl_from!(CssPixelValueParseError<'a>, CssStyleBlurParseError::Pixel);
415
416    #[derive(Debug, Clone, PartialEq, Eq)]
417    #[repr(C, u8)]
418    pub enum CssStyleBlurParseErrorOwned {
419        Pixel(CssPixelValueParseErrorOwned),
420        TooManyComponents(AzString),
421    }
422
423    impl CssStyleBlurParseError<'_> {
424        #[must_use]
425        pub fn to_contained(&self) -> CssStyleBlurParseErrorOwned {
426            match self {
427                Self::Pixel(e) => CssStyleBlurParseErrorOwned::Pixel(e.to_contained()),
428                Self::TooManyComponents(s) => {
429                    CssStyleBlurParseErrorOwned::TooManyComponents((*s).to_string().into())
430                }
431            }
432        }
433    }
434
435    impl CssStyleBlurParseErrorOwned {
436        #[must_use]
437        pub fn to_shared(&self) -> CssStyleBlurParseError<'_> {
438            match self {
439                Self::Pixel(e) => CssStyleBlurParseError::Pixel(e.to_shared()),
440                Self::TooManyComponents(s) => CssStyleBlurParseError::TooManyComponents(s),
441            }
442        }
443    }
444
445    #[derive(Clone, PartialEq, Eq)]
446    pub enum CssStyleColorMatrixParseError<'a> {
447        Float(ParseFloatError),
448        WrongNumberOfComponents {
449            expected: usize,
450            got: usize,
451            input: &'a str,
452        },
453    }
454
455    impl_debug_as_display!(CssStyleColorMatrixParseError<'a>);
456    impl_display! { CssStyleColorMatrixParseError<'a>, {
457        Float(e) => format!("Error parsing floating-point value: {}", e),
458        WrongNumberOfComponents { expected, got, input } => format!("Expected {} components, got {}: \"{}\"", expected, got, input),
459    }}
460    impl From<ParseFloatError> for CssStyleColorMatrixParseError<'_> {
461        fn from(p: ParseFloatError) -> Self {
462            Self::Float(p)
463        }
464    }
465    #[allow(variant_size_differences)]
466    // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
467    #[derive(Debug, Clone, PartialEq, Eq)]
468    #[repr(C, u8)]
469    pub enum CssStyleColorMatrixParseErrorOwned {
470        Float(crate::props::basic::error::ParseFloatError),
471        WrongNumberOfComponents(WrongComponentCountError),
472    }
473
474    impl CssStyleColorMatrixParseError<'_> {
475        #[must_use]
476        pub fn to_contained(&self) -> CssStyleColorMatrixParseErrorOwned {
477            match self {
478                Self::Float(e) => CssStyleColorMatrixParseErrorOwned::Float(e.clone().into()),
479                Self::WrongNumberOfComponents {
480                    expected,
481                    got,
482                    input,
483                } => CssStyleColorMatrixParseErrorOwned::WrongNumberOfComponents(
484                    WrongComponentCountError {
485                        expected: *expected,
486                        got: *got,
487                        input: (*input).to_string().into(),
488                    },
489                ),
490            }
491        }
492    }
493
494    impl CssStyleColorMatrixParseErrorOwned {
495        #[must_use]
496        pub fn to_shared(&self) -> CssStyleColorMatrixParseError<'_> {
497            match self {
498                Self::Float(e) => CssStyleColorMatrixParseError::Float(e.to_std()),
499                Self::WrongNumberOfComponents(e) => {
500                    CssStyleColorMatrixParseError::WrongNumberOfComponents {
501                        expected: e.expected,
502                        got: e.got,
503                        input: e.input.as_str(),
504                    }
505                }
506            }
507        }
508    }
509
510    #[derive(Clone, PartialEq, Eq)]
511    pub enum CssStyleFilterOffsetParseError<'a> {
512        Pixel(CssPixelValueParseError<'a>),
513        WrongNumberOfComponents {
514            expected: usize,
515            got: usize,
516            input: &'a str,
517        },
518    }
519
520    impl_debug_as_display!(CssStyleFilterOffsetParseError<'a>);
521    impl_display! { CssStyleFilterOffsetParseError<'a>, {
522        Pixel(e) => format!("Invalid pixel value: {}", e),
523        WrongNumberOfComponents { expected, got, input } => format!("Expected {} components, got {}: \"{}\"", expected, got, input),
524    }}
525    impl_from!(
526        CssPixelValueParseError<'a>,
527        CssStyleFilterOffsetParseError::Pixel
528    );
529
530    #[derive(Debug, Clone, PartialEq, Eq)]
531    #[repr(C, u8)]
532    pub enum CssStyleFilterOffsetParseErrorOwned {
533        Pixel(CssPixelValueParseErrorOwned),
534        WrongNumberOfComponents(WrongComponentCountError),
535    }
536
537    impl CssStyleFilterOffsetParseError<'_> {
538        #[must_use]
539        pub fn to_contained(&self) -> CssStyleFilterOffsetParseErrorOwned {
540            match self {
541                Self::Pixel(e) => CssStyleFilterOffsetParseErrorOwned::Pixel(e.to_contained()),
542                Self::WrongNumberOfComponents {
543                    expected,
544                    got,
545                    input,
546                } => CssStyleFilterOffsetParseErrorOwned::WrongNumberOfComponents(
547                    WrongComponentCountError {
548                        expected: *expected,
549                        got: *got,
550                        input: (*input).to_string().into(),
551                    },
552                ),
553            }
554        }
555    }
556
557    impl CssStyleFilterOffsetParseErrorOwned {
558        #[must_use]
559        pub fn to_shared(&self) -> CssStyleFilterOffsetParseError<'_> {
560            match self {
561                Self::Pixel(e) => CssStyleFilterOffsetParseError::Pixel(e.to_shared()),
562                Self::WrongNumberOfComponents(e) => {
563                    CssStyleFilterOffsetParseError::WrongNumberOfComponents {
564                        expected: e.expected,
565                        got: e.got,
566                        input: e.input.as_str(),
567                    }
568                }
569            }
570        }
571    }
572
573    #[derive(Clone, PartialEq, Eq)]
574    pub enum CssStyleCompositeFilterParseError<'a> {
575        Invalid(InvalidValueErr<'a>),
576        Float(ParseFloatError),
577        WrongNumberOfComponents {
578            expected: usize,
579            got: usize,
580            input: &'a str,
581        },
582    }
583
584    impl_debug_as_display!(CssStyleCompositeFilterParseError<'a>);
585    impl_display! { CssStyleCompositeFilterParseError<'a>, {
586        Invalid(s) => format!("Invalid composite operator: {}", s.0),
587        Float(e) => format!("Error parsing floating-point value for arithmetic(): {}", e),
588        WrongNumberOfComponents { expected, got, input } => format!("Expected {} components for arithmetic(), got {}: \"{}\"", expected, got, input),
589    }}
590    impl_from!(
591        InvalidValueErr<'a>,
592        CssStyleCompositeFilterParseError::Invalid
593    );
594    impl From<ParseFloatError> for CssStyleCompositeFilterParseError<'_> {
595        fn from(p: ParseFloatError) -> Self {
596            Self::Float(p)
597        }
598    }
599
600    #[derive(Debug, Clone, PartialEq, Eq)]
601    #[repr(C, u8)]
602    pub enum CssStyleCompositeFilterParseErrorOwned {
603        Invalid(InvalidValueErrOwned),
604        Float(crate::props::basic::error::ParseFloatError),
605        WrongNumberOfComponents(WrongComponentCountError),
606    }
607
608    impl CssStyleCompositeFilterParseError<'_> {
609        #[must_use]
610        pub fn to_contained(&self) -> CssStyleCompositeFilterParseErrorOwned {
611            match self {
612                Self::Invalid(e) => {
613                    CssStyleCompositeFilterParseErrorOwned::Invalid(e.to_contained())
614                }
615                Self::Float(e) => CssStyleCompositeFilterParseErrorOwned::Float(e.clone().into()),
616                Self::WrongNumberOfComponents {
617                    expected,
618                    got,
619                    input,
620                } => CssStyleCompositeFilterParseErrorOwned::WrongNumberOfComponents(
621                    WrongComponentCountError {
622                        expected: *expected,
623                        got: *got,
624                        input: (*input).to_string().into(),
625                    },
626                ),
627            }
628        }
629    }
630
631    impl CssStyleCompositeFilterParseErrorOwned {
632        #[must_use]
633        pub fn to_shared(&self) -> CssStyleCompositeFilterParseError<'_> {
634            match self {
635                Self::Invalid(e) => CssStyleCompositeFilterParseError::Invalid(e.to_shared()),
636                Self::Float(e) => CssStyleCompositeFilterParseError::Float(e.to_std()),
637                Self::WrongNumberOfComponents(e) => {
638                    CssStyleCompositeFilterParseError::WrongNumberOfComponents {
639                        expected: e.expected,
640                        got: e.got,
641                        input: e.input.as_str(),
642                    }
643                }
644            }
645        }
646    }
647
648    // -- Parser Implementation --
649
650    /// Parses a space-separated list of filter functions.
651    /// # Errors
652    ///
653    /// Returns an error if `input` is not a valid CSS `filter-vec` value.
654    pub fn parse_style_filter_vec(
655        input: &str,
656    ) -> Result<StyleFilterVec, CssStyleFilterParseError<'_>> {
657        let mut filters = Vec::new();
658        let mut remaining = input.trim();
659        while !remaining.is_empty() {
660            let (filter, rest) = parse_one_filter_function(remaining)?;
661            filters.push(filter);
662            remaining = rest.trim_start();
663        }
664        Ok(filters.into())
665    }
666
667    /// Parses one `function(...)` from the beginning of a string and returns the parsed
668    /// filter and the rest of the string.
669    fn parse_one_filter_function(
670        input: &str,
671    ) -> Result<(StyleFilter, &str), CssStyleFilterParseError<'_>> {
672        let open_paren = input
673            .find('(')
674            .ok_or(CssStyleFilterParseError::InvalidFilter(input))?;
675        let func_name = &input[..open_paren];
676
677        let mut balance = 1;
678        let mut close_paren = 0;
679        for (i, c) in input.char_indices().skip(open_paren + 1) {
680            if c == '(' {
681                balance += 1;
682            } else if c == ')' {
683                balance -= 1;
684                if balance == 0 {
685                    close_paren = i;
686                    break;
687                }
688            }
689        }
690
691        if balance != 0 {
692            return Err(ParenthesisParseError::UnclosedBraces.into());
693        }
694
695        let full_function = &input[..=close_paren];
696        let rest = &input[(close_paren + 1)..];
697
698        let filter = parse_style_filter(full_function)?;
699        Ok((filter, rest))
700    }
701
702    /// Parses a single filter function string, like `blur(5px)`.
703    /// # Errors
704    ///
705    /// Returns an error if `input` is not a valid CSS `filter` value.
706    pub fn parse_style_filter(input: &str) -> Result<StyleFilter, CssStyleFilterParseError<'_>> {
707        let (filter_type, filter_values) = parse_parentheses(
708            input,
709            &[
710                "blend",
711                "flood",
712                "blur",
713                "opacity",
714                "color-matrix",
715                "drop-shadow",
716                "component-transfer",
717                "offset",
718                "composite",
719                "brightness",
720                "contrast",
721                "grayscale",
722                "hue-rotate",
723                "invert",
724                "saturate",
725                "sepia",
726            ],
727        )?;
728
729        match filter_type {
730            "blend" => Ok(StyleFilter::Blend(parse_style_mix_blend_mode(
731                filter_values,
732            )?)),
733            "flood" => Ok(StyleFilter::Flood(parse_css_color(filter_values)?)),
734            "blur" => Ok(StyleFilter::Blur(parse_style_blur(filter_values)?)),
735            "opacity" => {
736                let val = parse_percentage_value(filter_values)?;
737                // CSS filter opacity must be between 0 and 1 (or 0% to 100%)
738                let normalized = val.normalized();
739                if !(0.0..=1.0).contains(&normalized) {
740                    return Err(CssStyleFilterParseError::Opacity(
741                        PercentageParseError::InvalidUnit(filter_values.to_string().into()),
742                    ));
743                }
744                Ok(StyleFilter::Opacity(val))
745            }
746            "color-matrix" => Ok(StyleFilter::ColorMatrix(parse_color_matrix(filter_values)?)),
747            "drop-shadow" => Ok(StyleFilter::DropShadow(parse_style_box_shadow(
748                filter_values,
749            )?)),
750            "component-transfer" => Ok(StyleFilter::ComponentTransfer),
751            "offset" => Ok(StyleFilter::Offset(parse_filter_offset(filter_values)?)),
752            "composite" => Ok(StyleFilter::Composite(parse_filter_composite(
753                filter_values,
754            )?)),
755            "brightness" => {
756                let val = parse_percentage_value(filter_values)?;
757                if val.normalized() < 0.0 {
758                    return Err(CssStyleFilterParseError::Brightness(
759                        PercentageParseError::InvalidUnit(filter_values.to_string().into()),
760                    ));
761                }
762                Ok(StyleFilter::Brightness(val))
763            }
764            "contrast" => {
765                let val = parse_percentage_value(filter_values)?;
766                if val.normalized() < 0.0 {
767                    return Err(CssStyleFilterParseError::Contrast(
768                        PercentageParseError::InvalidUnit(filter_values.to_string().into()),
769                    ));
770                }
771                Ok(StyleFilter::Contrast(val))
772            }
773            "grayscale" => Ok(StyleFilter::Grayscale(parse_percentage_value(
774                filter_values,
775            )?)),
776            "hue-rotate" => Ok(StyleFilter::HueRotate(parse_angle_value(filter_values)?)),
777            "invert" => Ok(StyleFilter::Invert(parse_percentage_value(filter_values)?)),
778            "saturate" => {
779                let val = parse_percentage_value(filter_values)?;
780                if val.normalized() < 0.0 {
781                    return Err(CssStyleFilterParseError::Saturate(
782                        PercentageParseError::InvalidUnit(filter_values.to_string().into()),
783                    ));
784                }
785                Ok(StyleFilter::Saturate(val))
786            }
787            "sepia" => Ok(StyleFilter::Sepia(parse_percentage_value(filter_values)?)),
788            _ => unreachable!(),
789        }
790    }
791
792    fn parse_style_blur(input: &str) -> Result<StyleBlur, CssStyleBlurParseError<'_>> {
793        let mut iter = input.split_whitespace();
794        let width_str = iter.next().unwrap_or("");
795        let height_str = iter.next();
796
797        if iter.next().is_some() {
798            return Err(CssStyleBlurParseError::TooManyComponents(input));
799        }
800
801        let width = parse_pixel_value(width_str)?;
802        let height = match height_str {
803            Some(s) => parse_pixel_value(s)?,
804            None => width, // If only one value is given, use it for both
805        };
806
807        Ok(StyleBlur { width, height })
808    }
809
810    fn parse_color_matrix(
811        input: &str,
812    ) -> Result<StyleColorMatrix, CssStyleColorMatrixParseError<'_>> {
813        let components: Vec<_> = input.split_whitespace().collect();
814        if components.len() != 20 {
815            return Err(CssStyleColorMatrixParseError::WrongNumberOfComponents {
816                expected: 20,
817                got: components.len(),
818                input,
819            });
820        }
821
822        let mut values = [FloatValue::const_new(0); 20];
823        for (i, comp) in components.iter().enumerate() {
824            values[i] = parse_float_value(comp)?;
825        }
826
827        Ok(StyleColorMatrix {
828            m0: values[0],
829            m1: values[1],
830            m2: values[2],
831            m3: values[3],
832            m4: values[4],
833            m5: values[5],
834            m6: values[6],
835            m7: values[7],
836            m8: values[8],
837            m9: values[9],
838            m10: values[10],
839            m11: values[11],
840            m12: values[12],
841            m13: values[13],
842            m14: values[14],
843            m15: values[15],
844            m16: values[16],
845            m17: values[17],
846            m18: values[18],
847            m19: values[19],
848        })
849    }
850
851    fn parse_filter_offset(
852        input: &str,
853    ) -> Result<StyleFilterOffset, CssStyleFilterOffsetParseError<'_>> {
854        let components: Vec<_> = input.split_whitespace().collect();
855        if components.len() != 2 {
856            return Err(CssStyleFilterOffsetParseError::WrongNumberOfComponents {
857                expected: 2,
858                got: components.len(),
859                input,
860            });
861        }
862
863        let x = parse_pixel_value(components[0])?;
864        let y = parse_pixel_value(components[1])?;
865
866        Ok(StyleFilterOffset { x, y })
867    }
868
869    fn parse_filter_composite(
870        input: &str,
871    ) -> Result<StyleCompositeFilter, CssStyleCompositeFilterParseError<'_>> {
872        let mut iter = input.split_whitespace();
873        let operator = iter.next().unwrap_or("");
874
875        match operator {
876            "over" => Ok(StyleCompositeFilter::Over),
877            "in" => Ok(StyleCompositeFilter::In),
878            "atop" => Ok(StyleCompositeFilter::Atop),
879            "out" => Ok(StyleCompositeFilter::Out),
880            "xor" => Ok(StyleCompositeFilter::Xor),
881            "lighter" => Ok(StyleCompositeFilter::Lighter),
882            "arithmetic" => {
883                let mut values = [FloatValue::const_new(0); 4];
884                for (i, val) in values.iter_mut().enumerate() {
885                    let s = iter.next().ok_or(
886                        CssStyleCompositeFilterParseError::WrongNumberOfComponents {
887                            expected: 4,
888                            got: i,
889                            input,
890                        },
891                    )?;
892                    *val = parse_float_value(s)?;
893                }
894                Ok(StyleCompositeFilter::Arithmetic(ArithmeticCoefficients {
895                    k1: values[0],
896                    k2: values[1],
897                    k3: values[2],
898                    k4: values[3],
899                }))
900            }
901            _ => Err(InvalidValueErr(operator).into()),
902        }
903    }
904
905    #[cfg(test)]
906    #[allow(clippy::too_many_lines, clippy::float_cmp)]
907    mod autotest_generated {
908        // Parsed values are compared against the exact source literals.
909
910        #[allow(clippy::wildcard_imports)]
911        use super::*;
912        use alloc::{
913            string::{String, ToString},
914            vec,
915            vec::Vec,
916        };
917
918        use crate::props::{
919            basic::{
920                angle::{AngleMetric, AngleValue, CssAngleValueParseError},
921                color::{ColorU, CssColorParseError},
922                error::InvalidValueErr,
923                length::{FloatValue, PercentageParseError, PercentageValue, SizeMetric},
924                parse::ParenthesisParseError,
925                pixel::{CssPixelValueParseError, PixelValue},
926            },
927            formatter::PrintAsCssValue,
928            style::{
929                box_shadow::CssShadowParseError,
930                effects::StyleMixBlendMode,
931                filter::{
932                    ArithmeticCoefficients, StyleBlur, StyleColorMatrix, StyleCompositeFilter,
933                    StyleFilter, StyleFilterOffset,
934                },
935            },
936        };
937
938        // ------------------------------------------------------------------
939        // helpers
940        // ------------------------------------------------------------------
941
942        /// The 20 components of the identity color matrix, as CSS source.
943        const IDENTITY_MATRIX_SRC: &str = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0";
944
945        fn matrix_of(v: [FloatValue; 20]) -> StyleColorMatrix {
946            StyleColorMatrix {
947                m0: v[0],
948                m1: v[1],
949                m2: v[2],
950                m3: v[3],
951                m4: v[4],
952                m5: v[5],
953                m6: v[6],
954                m7: v[7],
955                m8: v[8],
956                m9: v[9],
957                m10: v[10],
958                m11: v[11],
959                m12: v[12],
960                m13: v[13],
961                m14: v[14],
962                m15: v[15],
963                m16: v[16],
964                m17: v[17],
965                m18: v[18],
966                m19: v[19],
967            }
968        }
969
970        /// A `core::num::ParseFloatError` (the `Invalid` kind).
971        fn float_err() -> ParseFloatError {
972            "x".parse::<f32>().unwrap_err()
973        }
974
975        // ==================================================================
976        // StyleColorMatrix::to_array  (getter)
977        // ==================================================================
978
979        #[test]
980        fn color_matrix_to_array_preserves_field_order() {
981            let mut fields = [FloatValue::const_new(0); 20];
982            for (i, f) in fields.iter_mut().enumerate() {
983                *f = FloatValue::const_new(i as isize);
984            }
985            let arr = matrix_of(fields).to_array();
986            assert_eq!(arr.len(), 20);
987            for (i, v) in arr.iter().enumerate() {
988                assert_eq!(
989                    *v,
990                    FloatValue::const_new(i as isize),
991                    "to_array() scrambled index {i}"
992                );
993            }
994        }
995
996        #[test]
997        fn color_matrix_to_array_on_extreme_values_stays_finite() {
998            // Every one of these is a value the *encoding* has to defuse: the
999            // getter itself must not panic and must not leak NaN/inf back out.
1000            let extremes = [
1001                f32::NAN,
1002                f32::INFINITY,
1003                f32::NEG_INFINITY,
1004                f32::MAX,
1005                f32::MIN,
1006                f32::MIN_POSITIVE,
1007                f32::EPSILON,
1008                -0.0,
1009                0.0,
1010                1e30,
1011                -1e30,
1012                1e-30,
1013                -1e-30,
1014                1.0,
1015                -1.0,
1016                0.5,
1017                -0.5,
1018                255.0,
1019                -255.0,
1020                12345.678,
1021            ];
1022            let mut fields = [FloatValue::const_new(0); 20];
1023            for (f, e) in fields.iter_mut().zip(extremes) {
1024                *f = FloatValue::new(e);
1025            }
1026            let m = matrix_of(fields);
1027            for (i, v) in m.to_array().iter().enumerate() {
1028                assert!(
1029                    v.get().is_finite(),
1030                    "m{i} decoded to a non-finite {}",
1031                    v.get()
1032                );
1033            }
1034            // NaN is encoded as zero, +/-inf saturate to the isize extremes.
1035            assert_eq!(m.m0.number(), 0, "NaN did not encode to zero");
1036            assert_eq!(m.m1.number(), isize::MAX);
1037            assert_eq!(m.m2.number(), isize::MIN);
1038            // to_array() is a pure copy: it must agree with the fields.
1039            assert_eq!(m.to_array()[19], m.m19);
1040        }
1041
1042        #[test]
1043        fn color_matrix_to_array_matches_the_parsed_identity_matrix() {
1044            let m = parse_color_matrix(IDENTITY_MATRIX_SRC).unwrap();
1045            let arr = m.to_array();
1046            let one = FloatValue::const_new(1);
1047            let zero = FloatValue::const_new(0);
1048            for (i, v) in arr.iter().enumerate() {
1049                // Diagonal of the 4x5 matrix: indices 0, 6, 12, 18.
1050                let expected = if i % 6 == 0 && i <= 18 { one } else { zero };
1051                assert_eq!(*v, expected, "identity matrix wrong at index {i}");
1052            }
1053        }
1054
1055        // ==================================================================
1056        // parse_style_filter  (parser, public)
1057        // ==================================================================
1058
1059        #[test]
1060        fn parse_style_filter_empty_and_whitespace_only_input() {
1061            for input in ["", "   ", "\t\n", "\r\n\t "] {
1062                assert!(
1063                    matches!(
1064                        parse_style_filter(input),
1065                        Err(CssStyleFilterParseError::InvalidParenthesis(
1066                            ParenthesisParseError::EmptyInput
1067                        ))
1068                    ),
1069                    "{input:?} should be EmptyInput"
1070                );
1071            }
1072        }
1073
1074        #[test]
1075        fn parse_style_filter_garbage_returns_err_and_never_panics() {
1076            for garbage in [
1077                "!!!",
1078                ";",
1079                "\0",
1080                "()",
1081                ")(",
1082                "((((",
1083                "))))",
1084                ")",
1085                "(",
1086                "blur",
1087                "blur(",
1088                "blur)5px(",
1089                "()()",
1090                "5px",
1091                "-",
1092                "#",
1093                "blur[5px]",
1094                "blur{5px}",
1095                "blur(5px))(",
1096                ",,,,",
1097                "\u{FFFD}",
1098            ] {
1099                assert!(
1100                    parse_style_filter(garbage).is_err(),
1101                    "garbage {garbage:?} was accepted"
1102                );
1103            }
1104        }
1105
1106        #[test]
1107        fn parse_style_filter_valid_minimal_positive_control() {
1108            // One known-good minimal input per stopword. This also proves the
1109            // `_ => unreachable!()` arm cannot be reached: every stopword
1110            // parse_parentheses() can hand back is matched.
1111            assert_eq!(
1112                parse_style_filter("blend(normal)").unwrap(),
1113                StyleFilter::Blend(StyleMixBlendMode::Normal)
1114            );
1115            assert_eq!(
1116                parse_style_filter("flood(red)").unwrap(),
1117                StyleFilter::Flood(ColorU::RED)
1118            );
1119            assert_eq!(
1120                parse_style_filter("blur(5px)").unwrap(),
1121                StyleFilter::Blur(StyleBlur {
1122                    width: PixelValue::px(5.0),
1123                    height: PixelValue::px(5.0),
1124                })
1125            );
1126            assert_eq!(
1127                parse_style_filter("opacity(50%)").unwrap(),
1128                StyleFilter::Opacity(PercentageValue::new(50.0))
1129            );
1130            assert_eq!(
1131                parse_style_filter("color-matrix(1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0)")
1132                    .unwrap(),
1133                StyleFilter::ColorMatrix(parse_color_matrix(IDENTITY_MATRIX_SRC).unwrap())
1134            );
1135            assert!(matches!(
1136                parse_style_filter("drop-shadow(1px 2px)").unwrap(),
1137                StyleFilter::DropShadow(_)
1138            ));
1139            assert_eq!(
1140                parse_style_filter("component-transfer()").unwrap(),
1141                StyleFilter::ComponentTransfer
1142            );
1143            assert_eq!(
1144                parse_style_filter("offset(1px 2px)").unwrap(),
1145                StyleFilter::Offset(StyleFilterOffset {
1146                    x: PixelValue::px(1.0),
1147                    y: PixelValue::px(2.0),
1148                })
1149            );
1150            assert_eq!(
1151                parse_style_filter("composite(over)").unwrap(),
1152                StyleFilter::Composite(StyleCompositeFilter::Over)
1153            );
1154            assert_eq!(
1155                parse_style_filter("brightness(100%)").unwrap(),
1156                StyleFilter::Brightness(PercentageValue::new(100.0))
1157            );
1158            assert_eq!(
1159                parse_style_filter("contrast(100%)").unwrap(),
1160                StyleFilter::Contrast(PercentageValue::new(100.0))
1161            );
1162            assert_eq!(
1163                parse_style_filter("grayscale(0%)").unwrap(),
1164                StyleFilter::Grayscale(PercentageValue::new(0.0))
1165            );
1166            assert_eq!(
1167                parse_style_filter("hue-rotate(90deg)").unwrap(),
1168                StyleFilter::HueRotate(AngleValue::deg(90.0))
1169            );
1170            assert_eq!(
1171                parse_style_filter("invert(0%)").unwrap(),
1172                StyleFilter::Invert(PercentageValue::new(0.0))
1173            );
1174            assert_eq!(
1175                parse_style_filter("saturate(100%)").unwrap(),
1176                StyleFilter::Saturate(PercentageValue::new(100.0))
1177            );
1178            assert_eq!(
1179                parse_style_filter("sepia(0%)").unwrap(),
1180                StyleFilter::Sepia(PercentageValue::new(0.0))
1181            );
1182        }
1183
1184        #[test]
1185        fn parse_style_filter_function_names_are_case_sensitive_and_space_sensitive() {
1186            // NOTE (spec deviation): CSS function names are ASCII
1187            // case-insensitive and `filter: BLUR(5px)` is legal. This parser
1188            // compares the stopword byte-for-byte, so the uppercase spelling is
1189            // rejected. Pinned as-is so the behaviour cannot change silently.
1190            for input in ["BLUR(5px)", "Blur(5px)", "Hue-Rotate(90deg)"] {
1191                assert!(
1192                    matches!(
1193                        parse_style_filter(input),
1194                        Err(CssStyleFilterParseError::InvalidParenthesis(
1195                            ParenthesisParseError::StopWordNotFound(_)
1196                        ))
1197                    ),
1198                    "{input:?} unexpectedly parsed"
1199                );
1200            }
1201            // Whitespace between the name and '(' is correctly rejected (CSS
1202            // forbids it for functional notation).
1203            assert!(parse_style_filter("blur (5px)").is_err());
1204        }
1205
1206        #[test]
1207        fn parse_style_filter_leading_and_trailing_whitespace_is_trimmed() {
1208            assert_eq!(
1209                parse_style_filter("   blur(5px)  \n").unwrap(),
1210                parse_style_filter("blur(5px)").unwrap()
1211            );
1212        }
1213
1214        #[test]
1215        fn parse_style_filter_swallows_trailing_junk_but_the_vec_parser_rejects_it() {
1216            // parse_parentheses() locates the payload with find('(') + rfind(')'),
1217            // so anything *after* the last ')' is silently discarded here.
1218            assert_eq!(
1219                parse_style_filter("blur(5px)garbage").unwrap(),
1220                parse_style_filter("blur(5px)").unwrap()
1221            );
1222            assert_eq!(
1223                parse_style_filter("blur(5px);\u{1F600}").unwrap(),
1224                parse_style_filter("blur(5px)").unwrap()
1225            );
1226            // The vec parser cuts at the *balanced* ')' and then has to parse the
1227            // remainder, so the same junk is rejected there. Deterministic, but
1228            // the two entry points disagree.
1229            assert!(parse_style_filter_vec("blur(5px)garbage").is_err());
1230            assert!(parse_style_filter_vec("blur(5px);garbage").is_err());
1231        }
1232
1233        #[test]
1234        fn parse_style_filter_rejects_two_functions_at_once() {
1235            // rfind(')') would run the payload of the *last* function into the
1236            // first one; the inner parse must catch that.
1237            assert!(parse_style_filter("blur(5px) blur(2px)").is_err());
1238            assert!(parse_style_filter("blur(5px) drop-shadow(1px 1px)").is_err());
1239        }
1240
1241        #[test]
1242        fn parse_style_filter_component_transfer_ignores_its_arguments() {
1243            // Any payload at all is accepted and dropped on the floor.
1244            for input in [
1245                "component-transfer()",
1246                "component-transfer(anything at all)",
1247                "component-transfer(\u{1F600})",
1248                "component-transfer(   )",
1249            ] {
1250                assert_eq!(
1251                    parse_style_filter(input).unwrap(),
1252                    StyleFilter::ComponentTransfer,
1253                    "{input:?}"
1254                );
1255            }
1256        }
1257
1258        #[test]
1259        fn parse_style_filter_component_transfer_does_not_round_trip() {
1260            // BUG (pinned): print_as_css_value() emits the bare keyword
1261            // "component-transfer" (no parens), but the parser requires a '(' —
1262            // so printing a StyleFilter and re-parsing it loses this variant.
1263            let printed = StyleFilter::ComponentTransfer.print_as_css_value();
1264            assert_eq!(printed, "component-transfer");
1265            assert!(matches!(
1266                parse_style_filter(&printed),
1267                Err(CssStyleFilterParseError::InvalidParenthesis(
1268                    ParenthesisParseError::NoOpeningBraceFound
1269                ))
1270            ));
1271            assert!(parse_style_filter_vec(&printed).is_err());
1272        }
1273
1274        #[test]
1275        fn parse_style_filter_opacity_range_is_enforced_at_both_ends() {
1276            // In range.
1277            for input in [
1278                "opacity(0)",
1279                "opacity(0%)",
1280                "opacity(1)",
1281                "opacity(100%)",
1282                "opacity(-0%)",
1283            ] {
1284                let f = parse_style_filter(input).unwrap_or_else(|e| panic!("{input:?}: {e}"));
1285                let StyleFilter::Opacity(v) = f else {
1286                    panic!("{input:?} did not parse as Opacity")
1287                };
1288                assert!(
1289                    (0.0..=1.0).contains(&v.normalized()),
1290                    "{input:?} -> {}",
1291                    v.normalized()
1292                );
1293            }
1294            // Out of range / not a number at all.
1295            for input in [
1296                "opacity(2)",
1297                "opacity(101%)",
1298                "opacity(-1%)",
1299                "opacity(-0.5)",
1300                "opacity(1e400%)",
1301                "opacity(NaN)",
1302                "opacity(inf)",
1303                "opacity(-inf)",
1304                "opacity()",
1305                "opacity(   )",
1306                "opacity(abc)",
1307            ] {
1308                assert!(
1309                    parse_style_filter(input).is_err(),
1310                    "{input:?} was accepted as an opacity"
1311                );
1312            }
1313        }
1314
1315        #[test]
1316        fn parse_style_filter_negative_brightness_contrast_saturate_are_rejected() {
1317            for input in [
1318                "brightness(-50%)",
1319                "brightness(-0.5)",
1320                "contrast(-10%)",
1321                "contrast(-0.0001)",
1322                "saturate(-20%)",
1323                "saturate(-1)",
1324            ] {
1325                assert!(parse_style_filter(input).is_err(), "{input:?} was accepted");
1326            }
1327            // ...but zero and negative-zero are fine, and there is no *upper*
1328            // bound (brightness(500%) is legal CSS).
1329            assert!(parse_style_filter("brightness(0%)").is_ok());
1330            assert!(parse_style_filter("brightness(-0%)").is_ok());
1331            assert!(parse_style_filter("contrast(0)").is_ok());
1332            assert!(parse_style_filter("saturate(500%)").is_ok());
1333        }
1334
1335        #[test]
1336        fn parse_style_filter_grayscale_invert_sepia_are_range_unchecked() {
1337            // NOTE (spec deviation): CSS clamps grayscale/invert/sepia to
1338            // [0%, 100%] and rejects negatives. This parser range-checks only
1339            // opacity/brightness/contrast/saturate, so these pass through
1340            // unclamped. Pinned so the leniency is visible.
1341            assert_eq!(
1342                parse_style_filter("grayscale(-50%)").unwrap(),
1343                StyleFilter::Grayscale(PercentageValue::new(-50.0))
1344            );
1345            assert_eq!(
1346                parse_style_filter("invert(500%)").unwrap(),
1347                StyleFilter::Invert(PercentageValue::new(500.0))
1348            );
1349            assert_eq!(
1350                parse_style_filter("sepia(-1)").unwrap(),
1351                StyleFilter::Sepia(PercentageValue::new(-100.0))
1352            );
1353        }
1354
1355        #[test]
1356        fn parse_style_filter_boundary_numbers_saturate_instead_of_leaking_inf() {
1357            // f32 overflow parses as inf in Rust; the FloatValue encoding must
1358            // defuse it into a finite (saturated) value.
1359            let StyleFilter::Blur(b) = parse_style_filter("blur(1e400px)").unwrap() else {
1360                panic!("not a blur")
1361            };
1362            assert!(b.width.number.get().is_finite(), "blur width leaked inf");
1363            assert_eq!(b.width.number.number(), isize::MAX);
1364            assert_eq!(b.width, b.height);
1365
1366            let StyleFilter::Brightness(v) = parse_style_filter("brightness(1e400%)").unwrap()
1367            else {
1368                panic!("not a brightness")
1369            };
1370            assert!(v.normalized().is_finite(), "brightness leaked inf");
1371
1372            // Underflow collapses to zero rather than erroring.
1373            let StyleFilter::Blur(tiny) = parse_style_filter("blur(1e-400px)").unwrap() else {
1374                panic!("not a blur")
1375            };
1376            assert_eq!(tiny.width.number.number(), 0);
1377
1378            // i64::MAX / i64::MIN sized literals.
1379            for input in [
1380                "blur(9223372036854775807px)",
1381                "blur(-9223372036854775808px)",
1382                "offset(9223372036854775807px 0px)",
1383            ] {
1384                let parsed = parse_style_filter(input);
1385                assert!(parsed.is_ok(), "{input:?} rejected: {parsed:?}");
1386            }
1387        }
1388
1389        #[test]
1390        fn parse_style_filter_unicode_input_does_not_panic() {
1391            for input in [
1392                "\u{1F600}(5px)",     // emoji function name
1393                "blur(5px\u{1F600})", // emoji in the payload
1394                "blur(\u{FF15}px)",   // FULLWIDTH DIGIT FIVE
1395                "blur(5\u{0301}px)",  // digit + combining acute
1396                "blur(\u{2212}5px)",  // U+2212 MINUS SIGN, not ASCII '-'
1397                "\u{FC}ber(5px)",     // multi-byte name
1398                "flood(\u{1F600})",
1399                "composite(\u{1F600})",
1400                "hue-rotate(90\u{00B0})", // DEGREE SIGN instead of "deg"
1401                "blur(\u{200B}5px)",      // zero-width space
1402            ] {
1403                assert!(
1404                    parse_style_filter(input).is_err(),
1405                    "unicode input {input:?} was accepted"
1406                );
1407            }
1408        }
1409
1410        #[test]
1411        fn parse_style_filter_extremely_long_input_terminates() {
1412            // 200k digits: Rust's f32 parser yields inf, which then saturates.
1413            let long_number = format!("blur({}px)", "9".repeat(200_000));
1414            let StyleFilter::Blur(b) = parse_style_filter(&long_number).unwrap() else {
1415                panic!("not a blur")
1416            };
1417            assert!(b.width.number.get().is_finite());
1418
1419            // 200k of pure junk must be rejected, not scanned quadratically.
1420            let long_junk = format!("blur({})", "a".repeat(200_000));
1421            assert!(parse_style_filter(&long_junk).is_err());
1422
1423            // A 200k-char function *name* is just a stopword miss.
1424            let long_name = format!("{}(5px)", "b".repeat(200_000));
1425            assert!(parse_style_filter(&long_name).is_err());
1426        }
1427
1428        #[test]
1429        fn parse_style_filter_deeply_nested_input_does_not_stack_overflow() {
1430            // The paren scanners are iterative (find/rfind + a balance counter),
1431            // so 10k levels must terminate with an error, not blow the stack.
1432            let open_only = "(".repeat(10_000);
1433            assert!(parse_style_filter(&open_only).is_err());
1434
1435            let nested = format!("blur({}{})", "(".repeat(10_000), ")".repeat(10_000));
1436            assert!(parse_style_filter(&nested).is_err());
1437
1438            let nested_vec = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1439            assert!(parse_style_filter_vec(&nested_vec).is_err());
1440        }
1441
1442        // ==================================================================
1443        // parse_style_filter_vec  (parser, public)
1444        // ==================================================================
1445
1446        #[test]
1447        fn parse_style_filter_vec_empty_and_whitespace_only_yield_an_empty_vec() {
1448            // NOTE: unlike parse_style_filter(), the vec parser treats "nothing
1449            // at all" as a successful empty list rather than an error.
1450            for input in ["", "   ", "\t\n", "\r\n \t"] {
1451                let v = parse_style_filter_vec(input)
1452                    .unwrap_or_else(|e| panic!("{input:?} should be an empty vec, got {e}"));
1453                assert_eq!(v.len(), 0, "{input:?}");
1454            }
1455        }
1456
1457        #[test]
1458        fn parse_style_filter_vec_valid_minimal_positive_control() {
1459            let v = parse_style_filter_vec("blur(5px)").unwrap();
1460            assert_eq!(v.len(), 1);
1461            assert_eq!(
1462                v.as_slice()[0],
1463                StyleFilter::Blur(StyleBlur {
1464                    width: PixelValue::px(5.0),
1465                    height: PixelValue::px(5.0),
1466                })
1467            );
1468
1469            // Separators are optional: the parser resumes right after ')'.
1470            let packed = parse_style_filter_vec("blur(5px)opacity(0.5)").unwrap();
1471            assert_eq!(packed.len(), 2);
1472            assert_eq!(
1473                packed.as_slice()[1],
1474                StyleFilter::Opacity(PercentageValue::new(50.0))
1475            );
1476
1477            // Interior whitespace of any flavour is tolerated.
1478            let spaced = parse_style_filter_vec("  blur(5px) \n\t opacity(0.5)  ").unwrap();
1479            assert_eq!(spaced.len(), 2);
1480        }
1481
1482        #[test]
1483        fn parse_style_filter_vec_rejects_junk_between_and_around_functions() {
1484            for input in [
1485                "blur(5px);opacity(0.5)",
1486                "blur(5px),opacity(0.5)",
1487                "blur(5px) opacity(0.5) garbage",
1488                "garbage blur(5px)",
1489                "blur(5px) )",
1490                "component-transfer",
1491                "\u{1F600}",
1492            ] {
1493                assert!(
1494                    parse_style_filter_vec(input).is_err(),
1495                    "{input:?} was accepted"
1496                );
1497            }
1498        }
1499
1500        #[test]
1501        fn parse_style_filter_vec_unclosed_paren_is_an_unclosed_braces_error() {
1502            assert!(matches!(
1503                parse_style_filter_vec("blur(5px"),
1504                Err(CssStyleFilterParseError::InvalidParenthesis(
1505                    ParenthesisParseError::UnclosedBraces
1506                ))
1507            ));
1508            assert!(matches!(
1509                parse_style_filter_vec("blur(5px) opacity(0.5"),
1510                Err(CssStyleFilterParseError::InvalidParenthesis(
1511                    ParenthesisParseError::UnclosedBraces
1512                ))
1513            ));
1514        }
1515
1516        #[test]
1517        fn parse_style_filter_vec_many_filters_terminates_and_keeps_order() {
1518            // Every iteration must consume at least one byte, otherwise the
1519            // `while !remaining.is_empty()` loop would spin forever.
1520            let n = 5_000;
1521            let src = "blur(1px)opacity(0.5)".repeat(n);
1522            let v = parse_style_filter_vec(&src).unwrap();
1523            assert_eq!(v.len(), n * 2);
1524            assert_eq!(
1525                v.as_slice()[0],
1526                StyleFilter::Blur(StyleBlur {
1527                    width: PixelValue::px(1.0),
1528                    height: PixelValue::px(1.0),
1529                })
1530            );
1531            assert_eq!(
1532                v.as_slice()[v.len() - 1],
1533                StyleFilter::Opacity(PercentageValue::new(50.0))
1534            );
1535        }
1536
1537        #[test]
1538        fn parse_style_filter_vec_round_trips_through_print_as_css_value() {
1539            // encode(decode(x)) == x for every variant that has a printable,
1540            // re-parsable form (i.e. everything except ComponentTransfer, see
1541            // parse_style_filter_component_transfer_does_not_round_trip).
1542            let src = "blur(5px) blur(2px 4px) flood(#ff0000ff) opacity(50%) blend(multiply) \
1543                       offset(10px 20px) hue-rotate(90deg) grayscale(100%) invert(25%) \
1544                       sepia(60%) brightness(150%) contrast(200%) saturate(50%) \
1545                       composite(over) composite(arithmetic 1 2 3 4) \
1546                       drop-shadow(10px 5px 5px #888888ff) \
1547                       color-matrix(1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0)";
1548
1549            let parsed = parse_style_filter_vec(src).unwrap();
1550            assert_eq!(parsed.len(), 17);
1551
1552            let printed = parsed.print_as_css_value();
1553            let reparsed = parse_style_filter_vec(&printed)
1554                .unwrap_or_else(|e| panic!("re-parsing {printed:?} failed: {e}"));
1555
1556            assert_eq!(parsed, reparsed, "round-trip changed the filter list");
1557            assert_eq!(
1558                printed,
1559                reparsed.print_as_css_value(),
1560                "printing is unstable"
1561            );
1562        }
1563
1564        #[test]
1565        fn parse_style_filter_single_round_trips_for_every_printable_variant() {
1566            for src in [
1567                "blend(color-dodge)",
1568                "flood(#01020304)",
1569                "blur(0px)",
1570                "blur(1px 2px)",
1571                "opacity(0%)",
1572                "opacity(100%)",
1573                "color-matrix(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)",
1574                "drop-shadow(1px 2px)",
1575                "offset(-3px 4px)",
1576                "composite(xor)",
1577                "composite(arithmetic 0 -1 2.5 3)",
1578                "brightness(0%)",
1579                "contrast(300%)",
1580                "grayscale(50%)",
1581                "hue-rotate(-45deg)",
1582                "invert(100%)",
1583                "saturate(0%)",
1584                "sepia(10%)",
1585            ] {
1586                let parsed = parse_style_filter(src).unwrap_or_else(|e| panic!("{src:?}: {e}"));
1587                let printed = parsed.print_as_css_value();
1588                let reparsed = parse_style_filter(&printed)
1589                    .unwrap_or_else(|e| panic!("{src:?} printed as {printed:?}, reparse: {e}"));
1590                assert_eq!(
1591                    parsed, reparsed,
1592                    "{src:?} -> {printed:?} did not round-trip"
1593                );
1594            }
1595        }
1596
1597        // ==================================================================
1598        // parse_one_filter_function  (parser, private)
1599        // ==================================================================
1600
1601        #[test]
1602        fn parse_one_filter_function_empty_and_whitespace_only_input() {
1603            assert!(matches!(
1604                parse_one_filter_function(""),
1605                Err(CssStyleFilterParseError::InvalidFilter(""))
1606            ));
1607            assert!(matches!(
1608                parse_one_filter_function("   "),
1609                Err(CssStyleFilterParseError::InvalidFilter("   "))
1610            ));
1611            assert!(matches!(
1612                parse_one_filter_function("\t\n"),
1613                Err(CssStyleFilterParseError::InvalidFilter("\t\n"))
1614            ));
1615        }
1616
1617        #[test]
1618        fn parse_one_filter_function_returns_the_unconsumed_rest_verbatim() {
1619            let (filter, rest) = parse_one_filter_function("blur(5px) rest here").unwrap();
1620            assert_eq!(
1621                filter,
1622                StyleFilter::Blur(StyleBlur {
1623                    width: PixelValue::px(5.0),
1624                    height: PixelValue::px(5.0),
1625                })
1626            );
1627            assert_eq!(rest, " rest here");
1628
1629            let (_, empty_rest) = parse_one_filter_function("blur(5px)").unwrap();
1630            assert_eq!(empty_rest, "");
1631        }
1632
1633        #[test]
1634        fn parse_one_filter_function_balances_nested_parens_before_splitting() {
1635            // The first ')' is *not* the terminator here: the balance counter has
1636            // to walk past the one closing rgb().
1637            let (filter, rest) = parse_one_filter_function("flood(rgb(255, 0, 0))rest").unwrap();
1638            assert_eq!(filter, StyleFilter::Flood(ColorU::RED));
1639            assert_eq!(rest, "rest");
1640        }
1641
1642        #[test]
1643        fn parse_one_filter_function_unbalanced_parens_are_rejected() {
1644            assert!(matches!(
1645                parse_one_filter_function("blur(5px"),
1646                Err(CssStyleFilterParseError::InvalidParenthesis(
1647                    ParenthesisParseError::UnclosedBraces
1648                ))
1649            ));
1650            assert!(matches!(
1651                parse_one_filter_function("flood(rgb(255,0,0)"),
1652                Err(CssStyleFilterParseError::InvalidParenthesis(
1653                    ParenthesisParseError::UnclosedBraces
1654                ))
1655            ));
1656            // No '(' at all -> InvalidFilter, carrying the whole input.
1657            assert!(matches!(
1658                parse_one_filter_function("component-transfer"),
1659                Err(CssStyleFilterParseError::InvalidFilter(
1660                    "component-transfer"
1661                ))
1662            ));
1663            assert!(matches!(
1664                parse_one_filter_function(")"),
1665                Err(CssStyleFilterParseError::InvalidFilter(")"))
1666            ));
1667        }
1668
1669        #[test]
1670        fn parse_one_filter_function_splits_on_char_boundaries_with_unicode() {
1671            // close_paren+1 must land on a char boundary or this slice panics.
1672            let (filter, rest) = parse_one_filter_function("blur(5px)\u{1F600}\u{00E9}").unwrap();
1673            assert!(matches!(filter, StyleFilter::Blur(_)));
1674            assert_eq!(rest, "\u{1F600}\u{00E9}");
1675
1676            // Multi-byte characters *before* the '(' are equally safe.
1677            assert!(parse_one_filter_function("bl\u{FC}r(5px)").is_err());
1678            assert!(parse_one_filter_function("\u{1F600}(5px)").is_err());
1679        }
1680
1681        #[test]
1682        fn parse_one_filter_function_long_and_nested_input_terminates() {
1683            let long = format!("blur({}px)trailing", "9".repeat(100_000));
1684            let (filter, rest) = parse_one_filter_function(&long).unwrap();
1685            assert!(matches!(filter, StyleFilter::Blur(_)));
1686            assert_eq!(rest, "trailing");
1687
1688            // 10k unclosed levels: iterative balance counter, no recursion.
1689            let nested = format!("blur({}", "(".repeat(10_000));
1690            assert!(matches!(
1691                parse_one_filter_function(&nested),
1692                Err(CssStyleFilterParseError::InvalidParenthesis(
1693                    ParenthesisParseError::UnclosedBraces
1694                ))
1695            ));
1696        }
1697
1698        // ==================================================================
1699        // parse_style_blur  (parser, private)
1700        // ==================================================================
1701
1702        #[test]
1703        fn parse_style_blur_one_value_is_used_for_both_axes() {
1704            let b = parse_style_blur("5px").unwrap();
1705            assert_eq!(b.width, PixelValue::px(5.0));
1706            assert_eq!(b.height, PixelValue::px(5.0));
1707
1708            let b2 = parse_style_blur("2px 4px").unwrap();
1709            assert_eq!(b2.width, PixelValue::px(2.0));
1710            assert_eq!(b2.height, PixelValue::px(4.0));
1711
1712            // split_whitespace(), so any run of whitespace separates.
1713            let b3 = parse_style_blur("  2px \t\n 4px  ").unwrap();
1714            assert_eq!(b3, b2);
1715        }
1716
1717        #[test]
1718        fn parse_style_blur_empty_or_whitespace_only_is_an_empty_pixel_value() {
1719            assert!(matches!(
1720                parse_style_blur(""),
1721                Err(CssStyleBlurParseError::Pixel(
1722                    CssPixelValueParseError::EmptyString
1723                ))
1724            ));
1725            assert!(matches!(
1726                parse_style_blur("   \t\n"),
1727                Err(CssStyleBlurParseError::Pixel(
1728                    CssPixelValueParseError::EmptyString
1729                ))
1730            ));
1731        }
1732
1733        #[test]
1734        fn parse_style_blur_three_or_more_components_is_too_many() {
1735            assert!(matches!(
1736                parse_style_blur("1px 2px 3px"),
1737                Err(CssStyleBlurParseError::TooManyComponents("1px 2px 3px"))
1738            ));
1739            let four = "1px 2px 3px 4px";
1740            assert!(matches!(
1741                parse_style_blur(four),
1742                Err(CssStyleBlurParseError::TooManyComponents(f)) if f == four
1743            ));
1744            // The check runs before the pixel parse, so garbage in slot 3 still
1745            // reports TooManyComponents rather than a pixel error.
1746            assert!(matches!(
1747                parse_style_blur("1px 2px garbage"),
1748                Err(CssStyleBlurParseError::TooManyComponents(_))
1749            ));
1750        }
1751
1752        #[test]
1753        fn parse_style_blur_garbage_returns_err_and_never_panics() {
1754            for garbage in [
1755                "abc", "px", "5xx", "-", ".", "5..5px", "1,5px", ";", "\0", "5px;", "()",
1756            ] {
1757                assert!(
1758                    parse_style_blur(garbage).is_err(),
1759                    "garbage {garbage:?} was accepted"
1760                );
1761            }
1762        }
1763
1764        #[test]
1765        fn parse_style_blur_accepts_negative_and_unitless_radii() {
1766            // NOTE (spec deviation): CSS requires a non-negative <length> for
1767            // blur() and forbids unitless non-zero numbers. Both are accepted
1768            // here; a negative blur radius reaches the compositor unchecked.
1769            let neg = parse_style_blur("-5px").unwrap();
1770            assert_eq!(neg.width, PixelValue::px(-5.0));
1771            let unitless = parse_style_blur("5").unwrap();
1772            assert_eq!(unitless.width, PixelValue::px(5.0));
1773            assert_eq!(unitless.width.metric, SizeMetric::Px);
1774        }
1775
1776        #[test]
1777        fn parse_style_blur_nan_and_inf_are_encoded_not_propagated() {
1778            // Rust's f32 parser accepts "NaN"/"inf"; parse_pixel_value() has no
1779            // unit suffix to strip, so they reach FloatValue, which defuses them.
1780            let nan = parse_style_blur("NaN").unwrap();
1781            assert_eq!(nan.width.number.number(), 0, "NaN leaked into the encoding");
1782            assert!(nan.width.number.get().is_finite());
1783
1784            let inf = parse_style_blur("inf").unwrap();
1785            assert_eq!(inf.width.number.number(), isize::MAX);
1786            assert!(inf.width.number.get().is_finite());
1787
1788            let neg_inf = parse_style_blur("-inf").unwrap();
1789            assert_eq!(neg_inf.width.number.number(), isize::MIN);
1790            assert!(neg_inf.width.number.get().is_finite());
1791
1792            // Both axes independently.
1793            let mixed = parse_style_blur("NaN inf").unwrap();
1794            assert_eq!(mixed.width.number.number(), 0);
1795            assert_eq!(mixed.height.number.number(), isize::MAX);
1796        }
1797
1798        #[test]
1799        fn parse_style_blur_boundary_numbers_stay_finite() {
1800            for input in [
1801                "0px",
1802                "-0px",
1803                "1e400px",
1804                "-1e400px",
1805                "1e-400px",
1806                "9223372036854775807px",
1807                "-9223372036854775808px",
1808                "0.0000000000000000000001px",
1809            ] {
1810                let b = parse_style_blur(input).unwrap_or_else(|e| panic!("{input:?}: {e}"));
1811                assert!(
1812                    b.width.number.get().is_finite() && b.height.number.get().is_finite(),
1813                    "{input:?} leaked a non-finite value"
1814                );
1815            }
1816            // -0 must not surface as a negative zero.
1817            assert!(parse_style_blur("-0px")
1818                .unwrap()
1819                .width
1820                .number
1821                .get()
1822                .is_sign_positive());
1823        }
1824
1825        #[test]
1826        fn parse_style_blur_unicode_does_not_panic() {
1827            for input in [
1828                "\u{1F600}",
1829                "5\u{1F600}",
1830                "\u{FF15}px",
1831                "5\u{0301}px",
1832                "\u{2212}5px",
1833                "5px \u{1F600}",
1834            ] {
1835                assert!(
1836                    parse_style_blur(input).is_err(),
1837                    "unicode {input:?} was accepted"
1838                );
1839            }
1840        }
1841
1842        #[test]
1843        fn parse_style_blur_long_and_nested_input_terminates() {
1844            let long = format!("{}px", "9".repeat(200_000));
1845            assert!(parse_style_blur(&long)
1846                .unwrap()
1847                .width
1848                .number
1849                .get()
1850                .is_finite());
1851
1852            let long_junk = "a".repeat(200_000);
1853            assert!(parse_style_blur(&long_junk).is_err());
1854
1855            // 10k tokens -> TooManyComponents, found without any recursion.
1856            let many = "1px ".repeat(10_000);
1857            assert!(matches!(
1858                parse_style_blur(&many),
1859                Err(CssStyleBlurParseError::TooManyComponents(_))
1860            ));
1861
1862            let nested = format!("{}5px{}", "(".repeat(10_000), ")".repeat(10_000));
1863            assert!(parse_style_blur(&nested).is_err());
1864        }
1865
1866        // ==================================================================
1867        // parse_color_matrix  (parser, private)
1868        // ==================================================================
1869
1870        #[test]
1871        fn parse_color_matrix_valid_minimal_positive_control() {
1872            let m = parse_color_matrix(IDENTITY_MATRIX_SRC).unwrap();
1873            assert_eq!(m.m0, FloatValue::const_new(1));
1874            assert_eq!(m.m1, FloatValue::const_new(0));
1875            assert_eq!(m.m6, FloatValue::const_new(1));
1876            assert_eq!(m.m12, FloatValue::const_new(1));
1877            assert_eq!(m.m18, FloatValue::const_new(1));
1878            assert_eq!(m.m19, FloatValue::const_new(0));
1879
1880            // Any whitespace works as a separator, and the 20 values land in order.
1881            let ordered = (0..20)
1882                .map(|i| i.to_string())
1883                .collect::<Vec<_>>()
1884                .join("\n\t ");
1885            let m2 = parse_color_matrix(&ordered).unwrap();
1886            for (i, v) in m2.to_array().iter().enumerate() {
1887                assert_eq!(*v, FloatValue::const_new(i as isize), "index {i}");
1888            }
1889        }
1890
1891        #[test]
1892        fn parse_color_matrix_wrong_component_count_is_reported_exactly() {
1893            // Empty / whitespace-only -> zero components.
1894            for input in ["", "   ", "\t\n"] {
1895                match parse_color_matrix(input) {
1896                    Err(CssStyleColorMatrixParseError::WrongNumberOfComponents {
1897                        expected,
1898                        got,
1899                        input: reported,
1900                    }) => {
1901                        assert_eq!(expected, 20);
1902                        assert_eq!(got, 0);
1903                        assert_eq!(reported, input);
1904                    }
1905                    other => panic!("{input:?} should be 0-of-20, got {other:?}"),
1906                }
1907            }
1908
1909            // 19 and 21 components.
1910            let nineteen = "1 ".repeat(19);
1911            assert!(matches!(
1912                parse_color_matrix(nineteen.trim()),
1913                Err(CssStyleColorMatrixParseError::WrongNumberOfComponents {
1914                    expected: 20,
1915                    got: 19,
1916                    ..
1917                })
1918            ));
1919            let twentyone = "1 ".repeat(21);
1920            assert!(matches!(
1921                parse_color_matrix(twentyone.trim()),
1922                Err(CssStyleColorMatrixParseError::WrongNumberOfComponents {
1923                    expected: 20,
1924                    got: 21,
1925                    ..
1926                })
1927            ));
1928        }
1929
1930        #[test]
1931        fn parse_color_matrix_garbage_component_is_a_float_error() {
1932            let with_junk = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 abc";
1933            assert!(matches!(
1934                parse_color_matrix(with_junk),
1935                Err(CssStyleColorMatrixParseError::Float(_))
1936            ));
1937            // Units are not floats.
1938            let with_px = "1px 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0";
1939            assert!(matches!(
1940                parse_color_matrix(with_px),
1941                Err(CssStyleColorMatrixParseError::Float(_))
1942            ));
1943            // A percentage is not a float either.
1944            let with_pct = "50% 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0";
1945            assert!(parse_color_matrix(with_pct).is_err());
1946        }
1947
1948        #[test]
1949        fn parse_color_matrix_boundary_numbers_saturate() {
1950            let src = "NaN inf -inf 1e400 -1e400 1e-400 -0 0 1 -1 \
1951                       9223372036854775807 -9223372036854775808 0.5 -0.5 \
1952                       3.4028235e38 -3.4028235e38 1.1754944e-38 0 0 0";
1953            let m = parse_color_matrix(src).unwrap();
1954            for (i, v) in m.to_array().iter().enumerate() {
1955                assert!(
1956                    v.get().is_finite(),
1957                    "component {i} decoded to a non-finite {}",
1958                    v.get()
1959                );
1960            }
1961            assert_eq!(m.m0.number(), 0, "NaN did not encode to zero");
1962            assert_eq!(m.m1.number(), isize::MAX, "inf did not saturate");
1963            assert_eq!(m.m2.number(), isize::MIN, "-inf did not saturate");
1964            assert_eq!(m.m3.number(), isize::MAX, "1e400 did not saturate");
1965            assert_eq!(m.m4.number(), isize::MIN);
1966            assert_eq!(m.m5.number(), 0, "1e-400 did not underflow to zero");
1967            assert!(m.m6.get().is_sign_positive(), "-0 leaked a negative zero");
1968        }
1969
1970        #[test]
1971        fn parse_color_matrix_unicode_does_not_panic() {
1972            let emoji = "\u{1F600} 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0";
1973            assert!(parse_color_matrix(emoji).is_err());
1974            // ARABIC-INDIC DIGIT FIVE is `char::is_numeric()` but not an f32.
1975            let arabic = "\u{0665} 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0";
1976            assert!(parse_color_matrix(arabic).is_err());
1977            // 20 emoji: right count, wrong type.
1978            let all_emoji = "\u{1F600} ".repeat(20);
1979            assert!(matches!(
1980                parse_color_matrix(all_emoji.trim()),
1981                Err(CssStyleColorMatrixParseError::Float(_))
1982            ));
1983        }
1984
1985        #[test]
1986        fn parse_color_matrix_long_and_nested_input_terminates() {
1987            // One 100k-digit component: parses to inf, then saturates.
1988            let huge = format!(
1989                "{} 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0",
1990                "9".repeat(100_000)
1991            );
1992            let m = parse_color_matrix(&huge).unwrap();
1993            assert_eq!(m.m0.number(), isize::MAX);
1994            assert!(m.m0.get().is_finite());
1995
1996            // 100k components: counted, rejected, no allocation blow-up.
1997            let many = "1 ".repeat(100_000);
1998            assert!(matches!(
1999                parse_color_matrix(many.trim()),
2000                Err(CssStyleColorMatrixParseError::WrongNumberOfComponents {
2001                    expected: 20,
2002                    got: 100_000,
2003                    ..
2004                })
2005            ));
2006
2007            // 10k nested brackets in one component: rejected, no stack overflow.
2008            let nested = format!(
2009                "{}1{} 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0",
2010                "(".repeat(10_000),
2011                ")".repeat(10_000)
2012            );
2013            assert!(parse_color_matrix(&nested).is_err());
2014        }
2015
2016        // ==================================================================
2017        // parse_filter_offset  (parser, private)
2018        // ==================================================================
2019
2020        #[test]
2021        fn parse_filter_offset_valid_minimal_positive_control() {
2022            assert_eq!(
2023                parse_filter_offset("10px 20px").unwrap(),
2024                StyleFilterOffset {
2025                    x: PixelValue::px(10.0),
2026                    y: PixelValue::px(20.0),
2027                }
2028            );
2029            // Mixed units and negatives are fine.
2030            assert_eq!(
2031                parse_filter_offset("-1.5em \t 20%").unwrap(),
2032                StyleFilterOffset {
2033                    x: PixelValue::em(-1.5),
2034                    y: PixelValue::percent(20.0),
2035                }
2036            );
2037        }
2038
2039        #[test]
2040        fn parse_filter_offset_requires_exactly_two_components() {
2041            for (input, got) in [("", 0), ("   ", 0), ("1px", 1), ("1px 2px 3px", 3)] {
2042                match parse_filter_offset(input) {
2043                    Err(CssStyleFilterOffsetParseError::WrongNumberOfComponents {
2044                        expected,
2045                        got: g,
2046                        input: reported,
2047                    }) => {
2048                        assert_eq!(expected, 2, "{input:?}");
2049                        assert_eq!(g, got, "{input:?}");
2050                        assert_eq!(reported, input);
2051                    }
2052                    other => panic!("{input:?} should be {got}-of-2, got {other:?}"),
2053                }
2054            }
2055        }
2056
2057        #[test]
2058        fn parse_filter_offset_garbage_and_unicode_return_pixel_errors() {
2059            for input in [
2060                "abc def",
2061                "1px abc",
2062                "abc 1px",
2063                "\u{1F600} \u{1F600}",
2064                "1px \u{FF15}px",
2065                "; ;",
2066                "\0 \0",
2067            ] {
2068                assert!(
2069                    matches!(
2070                        parse_filter_offset(input),
2071                        Err(CssStyleFilterOffsetParseError::Pixel(_))
2072                    ),
2073                    "{input:?} should be a pixel error"
2074                );
2075            }
2076        }
2077
2078        #[test]
2079        fn parse_filter_offset_boundary_numbers_stay_finite() {
2080            for input in [
2081                "0px 0px",
2082                "-0px -0px",
2083                "1e400px -1e400px",
2084                "1e-400px 1e-400px",
2085                "9223372036854775807px -9223372036854775808px",
2086                "NaN NaN",
2087                "inf -inf",
2088            ] {
2089                let o = parse_filter_offset(input).unwrap_or_else(|e| panic!("{input:?}: {e}"));
2090                assert!(
2091                    o.x.number.get().is_finite() && o.y.number.get().is_finite(),
2092                    "{input:?} leaked a non-finite offset"
2093                );
2094            }
2095            // NaN is silently accepted as a zero offset (no unit to reject it).
2096            let nan = parse_filter_offset("NaN NaN").unwrap();
2097            assert_eq!(nan.x.number.number(), 0);
2098            assert_eq!(nan.y.number.number(), 0);
2099        }
2100
2101        #[test]
2102        fn parse_filter_offset_long_input_terminates() {
2103            let long = format!("{}px 0px", "9".repeat(200_000));
2104            assert!(parse_filter_offset(&long)
2105                .unwrap()
2106                .x
2107                .number
2108                .get()
2109                .is_finite());
2110
2111            let many = "1px ".repeat(100_000);
2112            assert!(matches!(
2113                parse_filter_offset(many.trim()),
2114                Err(CssStyleFilterOffsetParseError::WrongNumberOfComponents {
2115                    expected: 2,
2116                    got: 100_000,
2117                    ..
2118                })
2119            ));
2120
2121            let nested = format!("{}{} 0px", "(".repeat(10_000), ")".repeat(10_000));
2122            assert!(parse_filter_offset(&nested).is_err());
2123        }
2124
2125        // ==================================================================
2126        // parse_filter_composite  (parser, private)
2127        // ==================================================================
2128
2129        #[test]
2130        fn parse_filter_composite_valid_minimal_positive_control() {
2131            for (input, expected) in [
2132                ("over", StyleCompositeFilter::Over),
2133                ("in", StyleCompositeFilter::In),
2134                ("atop", StyleCompositeFilter::Atop),
2135                ("out", StyleCompositeFilter::Out),
2136                ("xor", StyleCompositeFilter::Xor),
2137                ("lighter", StyleCompositeFilter::Lighter),
2138            ] {
2139                assert_eq!(
2140                    parse_filter_composite(input).unwrap(),
2141                    expected,
2142                    "{input:?}"
2143                );
2144                // Surrounding whitespace is eaten by split_whitespace().
2145                let padded = format!("  \t{input}\n ");
2146                assert_eq!(parse_filter_composite(&padded).unwrap(), expected);
2147            }
2148
2149            assert_eq!(
2150                parse_filter_composite("arithmetic 1 2 3 4").unwrap(),
2151                StyleCompositeFilter::Arithmetic(ArithmeticCoefficients {
2152                    k1: FloatValue::const_new(1),
2153                    k2: FloatValue::const_new(2),
2154                    k3: FloatValue::const_new(3),
2155                    k4: FloatValue::const_new(4),
2156                })
2157            );
2158        }
2159
2160        #[test]
2161        fn parse_filter_composite_empty_whitespace_and_garbage_are_invalid_operators() {
2162            for input in [
2163                "",
2164                "   ",
2165                "\t\n",
2166                "OVER",
2167                "Over",
2168                "arithmetics",
2169                ";",
2170                "\0",
2171                "\u{1F600}",
2172            ] {
2173                assert!(
2174                    matches!(
2175                        parse_filter_composite(input),
2176                        Err(CssStyleCompositeFilterParseError::Invalid(_))
2177                    ),
2178                    "{input:?} should be an invalid operator"
2179                );
2180            }
2181            // The offending operator is echoed back verbatim.
2182            assert!(matches!(
2183                parse_filter_composite(""),
2184                Err(CssStyleCompositeFilterParseError::Invalid(InvalidValueErr(
2185                    ""
2186                )))
2187            ));
2188            assert!(matches!(
2189                parse_filter_composite("nope 1 2 3 4"),
2190                Err(CssStyleCompositeFilterParseError::Invalid(InvalidValueErr(
2191                    "nope"
2192                )))
2193            ));
2194        }
2195
2196        #[test]
2197        fn parse_filter_composite_arithmetic_missing_coefficients_report_how_many_were_found() {
2198            for (input, got) in [
2199                ("arithmetic", 0),
2200                ("arithmetic 1", 1),
2201                ("arithmetic 1 2", 2),
2202                ("arithmetic 1 2 3", 3),
2203            ] {
2204                match parse_filter_composite(input) {
2205                    Err(CssStyleCompositeFilterParseError::WrongNumberOfComponents {
2206                        expected,
2207                        got: g,
2208                        input: reported,
2209                    }) => {
2210                        assert_eq!(expected, 4, "{input:?}");
2211                        assert_eq!(g, got, "{input:?}");
2212                        assert_eq!(reported, input);
2213                    }
2214                    other => panic!("{input:?} should be {got}-of-4, got {other:?}"),
2215                }
2216            }
2217        }
2218
2219        #[test]
2220        fn parse_filter_composite_arithmetic_ignores_extra_coefficients() {
2221            // NOTE: there is no TooManyComponents check here (unlike blur()), so
2222            // trailing junk after the 4th coefficient is silently dropped —
2223            // including junk that is not even a number.
2224            let coeffs = parse_filter_composite("arithmetic 1 2 3 4 5 6 garbage").unwrap();
2225            assert_eq!(
2226                coeffs,
2227                StyleCompositeFilter::Arithmetic(ArithmeticCoefficients {
2228                    k1: FloatValue::const_new(1),
2229                    k2: FloatValue::const_new(2),
2230                    k3: FloatValue::const_new(3),
2231                    k4: FloatValue::const_new(4),
2232                })
2233            );
2234        }
2235
2236        #[test]
2237        fn parse_filter_composite_arithmetic_garbage_coefficient_is_a_float_error() {
2238            for input in [
2239                "arithmetic a 2 3 4",
2240                "arithmetic 1 2 3 zzz",
2241                "arithmetic 1px 2 3 4",
2242                "arithmetic 1 2 3 \u{1F600}",
2243                "arithmetic 1,2 3 4 5",
2244            ] {
2245                assert!(
2246                    matches!(
2247                        parse_filter_composite(input),
2248                        Err(CssStyleCompositeFilterParseError::Float(_))
2249                    ),
2250                    "{input:?} should be a float error"
2251                );
2252            }
2253        }
2254
2255        #[test]
2256        fn parse_filter_composite_arithmetic_boundary_numbers_saturate() {
2257            let c = parse_filter_composite("arithmetic NaN inf -inf 1e400").unwrap();
2258            let StyleCompositeFilter::Arithmetic(k) = c else {
2259                panic!("not arithmetic")
2260            };
2261            assert_eq!(k.k1.number(), 0, "NaN did not encode to zero");
2262            assert_eq!(k.k2.number(), isize::MAX, "inf did not saturate");
2263            assert_eq!(k.k3.number(), isize::MIN, "-inf did not saturate");
2264            assert_eq!(k.k4.number(), isize::MAX, "1e400 did not saturate");
2265            for v in [k.k1, k.k2, k.k3, k.k4] {
2266                assert!(v.get().is_finite(), "coefficient leaked {}", v.get());
2267            }
2268
2269            // -0 must not survive as a negative zero.
2270            let zeros = parse_filter_composite("arithmetic -0 0 -0.0 1e-400").unwrap();
2271            let StyleCompositeFilter::Arithmetic(z) = zeros else {
2272                panic!("not arithmetic")
2273            };
2274            for v in [z.k1, z.k2, z.k3, z.k4] {
2275                assert_eq!(v.number(), 0);
2276                assert!(v.get().is_sign_positive());
2277            }
2278        }
2279
2280        #[test]
2281        fn parse_filter_composite_long_and_nested_input_terminates() {
2282            let long_operator = "a".repeat(200_000);
2283            assert!(matches!(
2284                parse_filter_composite(&long_operator),
2285                Err(CssStyleCompositeFilterParseError::Invalid(_))
2286            ));
2287
2288            // A 100k-digit coefficient saturates rather than hanging.
2289            let long_coeff = format!("arithmetic {} 0 0 0", "9".repeat(100_000));
2290            let StyleCompositeFilter::Arithmetic(k) = parse_filter_composite(&long_coeff).unwrap()
2291            else {
2292                panic!("not arithmetic")
2293            };
2294            assert_eq!(k.k1.number(), isize::MAX);
2295
2296            // 100k coefficients: the first 4 win, the rest are ignored.
2297            let many = format!("arithmetic {}", "1 ".repeat(100_000));
2298            assert!(matches!(
2299                parse_filter_composite(&many),
2300                Ok(StyleCompositeFilter::Arithmetic(_))
2301            ));
2302
2303            let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
2304            assert!(parse_filter_composite(&nested).is_err());
2305        }
2306
2307        // ==================================================================
2308        // Error getters: to_contained() / to_shared() round-trips
2309        // ==================================================================
2310
2311        fn all_filter_errors() -> Vec<CssStyleFilterParseError<'static>> {
2312            vec![
2313                CssStyleFilterParseError::InvalidFilter(""),
2314                CssStyleFilterParseError::InvalidFilter("blurry(5px)"),
2315                CssStyleFilterParseError::InvalidFilter("\u{1F600}"),
2316                CssStyleFilterParseError::InvalidParenthesis(ParenthesisParseError::UnclosedBraces),
2317                CssStyleFilterParseError::InvalidParenthesis(
2318                    ParenthesisParseError::StopWordNotFound("nope"),
2319                ),
2320                CssStyleFilterParseError::Shadow(CssShadowParseError::TooManyOrTooFewComponents(
2321                    "1px",
2322                )),
2323                CssStyleFilterParseError::BlendMode(InvalidValueErr("bogus")),
2324                CssStyleFilterParseError::Color(CssColorParseError::InvalidColor("nope")),
2325                CssStyleFilterParseError::Color(CssColorParseError::EmptyInput),
2326                CssStyleFilterParseError::Opacity(PercentageParseError::NoPercentSign),
2327                CssStyleFilterParseError::Brightness(PercentageParseError::InvalidUnit(
2328                    "px".to_string().into(),
2329                )),
2330                CssStyleFilterParseError::Contrast(PercentageParseError::InvalidUnit(
2331                    String::new().into(),
2332                )),
2333                CssStyleFilterParseError::Saturate(PercentageParseError::InvalidUnit(
2334                    "\u{1F600}".to_string().into(),
2335                )),
2336                CssStyleFilterParseError::Blur(CssStyleBlurParseError::TooManyComponents(
2337                    "1px 2px 3px",
2338                )),
2339                CssStyleFilterParseError::Blur(CssStyleBlurParseError::Pixel(
2340                    CssPixelValueParseError::EmptyString,
2341                )),
2342                CssStyleFilterParseError::ColorMatrix(
2343                    CssStyleColorMatrixParseError::WrongNumberOfComponents {
2344                        expected: 20,
2345                        got: 0,
2346                        input: "",
2347                    },
2348                ),
2349                CssStyleFilterParseError::ColorMatrix(CssStyleColorMatrixParseError::Float(
2350                    float_err(),
2351                )),
2352                CssStyleFilterParseError::Offset(CssStyleFilterOffsetParseError::Pixel(
2353                    CssPixelValueParseError::InvalidPixelValue("abc"),
2354                )),
2355                CssStyleFilterParseError::Offset(
2356                    CssStyleFilterOffsetParseError::WrongNumberOfComponents {
2357                        expected: 2,
2358                        got: 3,
2359                        input: "1px 2px 3px",
2360                    },
2361                ),
2362                CssStyleFilterParseError::Composite(CssStyleCompositeFilterParseError::Invalid(
2363                    InvalidValueErr(""),
2364                )),
2365                CssStyleFilterParseError::Composite(CssStyleCompositeFilterParseError::Float(
2366                    float_err(),
2367                )),
2368                CssStyleFilterParseError::Composite(
2369                    CssStyleCompositeFilterParseError::WrongNumberOfComponents {
2370                        expected: 4,
2371                        got: 2,
2372                        input: "arithmetic 1 2",
2373                    },
2374                ),
2375                CssStyleFilterParseError::Angle(CssAngleValueParseError::EmptyString),
2376                CssStyleFilterParseError::Angle(CssAngleValueParseError::InvalidAngle(
2377                    "90\u{00B0}",
2378                )),
2379                CssStyleFilterParseError::Angle(CssAngleValueParseError::NoValueGiven(
2380                    "deg",
2381                    AngleMetric::Degree,
2382                )),
2383            ]
2384        }
2385
2386        #[test]
2387        fn filter_parse_error_round_trips_through_owned() {
2388            for e in all_filter_errors() {
2389                let owned = e.to_contained();
2390                assert_eq!(
2391                    e,
2392                    owned.to_shared(),
2393                    "to_contained()/to_shared() is not the identity for {e:?}"
2394                );
2395            }
2396        }
2397
2398        #[test]
2399        fn filter_parse_error_to_contained_keeps_extreme_payloads_intact() {
2400            let huge = "x".repeat(100_000);
2401            let e = CssStyleFilterParseError::InvalidFilter(&huge);
2402            let owned = e.to_contained();
2403            let CssStyleFilterParseErrorOwned::InvalidFilter(ref s) = owned else {
2404                panic!("wrong variant")
2405            };
2406            assert_eq!(s.as_str().len(), 100_000);
2407            assert_eq!(owned.to_shared(), e);
2408
2409            // Empty and multi-byte payloads survive the AzString hop unchanged.
2410            for payload in ["", "\u{1F600}\u{0301}", "\0", "  "] {
2411                let e = CssStyleFilterParseError::InvalidFilter(payload);
2412                assert_eq!(e.to_contained().to_shared(), e, "{payload:?}");
2413            }
2414        }
2415
2416        #[test]
2417        fn filter_parse_error_display_never_panics() {
2418            for e in all_filter_errors() {
2419                // impl_debug_as_display!: both formats must render.
2420                let via_display = format!("{e}");
2421                let via_debug = format!("{e:?}");
2422                assert!(!via_display.is_empty());
2423                assert_eq!(via_display, via_debug);
2424                // ...and so must the owned form.
2425                assert!(!format!("{:?}", e.to_contained()).is_empty());
2426            }
2427        }
2428
2429        #[test]
2430        fn blur_parse_error_round_trips_through_owned() {
2431            let huge = "1px ".repeat(50_000);
2432            let cases = [
2433                CssStyleBlurParseError::Pixel(CssPixelValueParseError::EmptyString),
2434                CssStyleBlurParseError::Pixel(CssPixelValueParseError::NoValueGiven(
2435                    "px",
2436                    SizeMetric::Px,
2437                )),
2438                CssStyleBlurParseError::Pixel(CssPixelValueParseError::InvalidPixelValue("abc")),
2439                CssStyleBlurParseError::TooManyComponents(""),
2440                CssStyleBlurParseError::TooManyComponents("1px 2px 3px"),
2441                CssStyleBlurParseError::TooManyComponents("\u{1F600}"),
2442                CssStyleBlurParseError::TooManyComponents(&huge),
2443            ];
2444            for e in cases {
2445                assert_eq!(e.to_contained().to_shared(), e, "{e:?}");
2446                assert!(!format!("{e}").is_empty());
2447            }
2448        }
2449
2450        #[test]
2451        fn color_matrix_parse_error_round_trips_through_owned() {
2452            let huge = "1 ".repeat(50_000);
2453            let cases = [
2454                CssStyleColorMatrixParseError::Float(float_err()),
2455                CssStyleColorMatrixParseError::Float("".parse::<f32>().unwrap_err()),
2456                CssStyleColorMatrixParseError::WrongNumberOfComponents {
2457                    expected: 20,
2458                    got: 0,
2459                    input: "",
2460                },
2461                CssStyleColorMatrixParseError::WrongNumberOfComponents {
2462                    expected: 20,
2463                    got: usize::MAX,
2464                    input: "\u{1F600}",
2465                },
2466                CssStyleColorMatrixParseError::WrongNumberOfComponents {
2467                    expected: usize::MAX,
2468                    got: 50_000,
2469                    input: &huge,
2470                },
2471            ];
2472            for e in cases {
2473                assert_eq!(e.to_contained().to_shared(), e, "{e:?}");
2474                assert!(!format!("{e}").is_empty());
2475            }
2476        }
2477
2478        #[test]
2479        fn filter_offset_parse_error_round_trips_through_owned() {
2480            let cases = [
2481                CssStyleFilterOffsetParseError::Pixel(CssPixelValueParseError::EmptyString),
2482                CssStyleFilterOffsetParseError::Pixel(CssPixelValueParseError::InvalidPixelValue(
2483                    "\u{1F600}",
2484                )),
2485                CssStyleFilterOffsetParseError::WrongNumberOfComponents {
2486                    expected: 2,
2487                    got: 0,
2488                    input: "",
2489                },
2490                CssStyleFilterOffsetParseError::WrongNumberOfComponents {
2491                    expected: 2,
2492                    got: usize::MAX,
2493                    input: "1px 2px 3px",
2494                },
2495            ];
2496            for e in cases {
2497                assert_eq!(e.to_contained().to_shared(), e, "{e:?}");
2498                assert!(!format!("{e}").is_empty());
2499            }
2500        }
2501
2502        #[test]
2503        fn composite_parse_error_round_trips_through_owned() {
2504            let cases = [
2505                CssStyleCompositeFilterParseError::Invalid(InvalidValueErr("")),
2506                CssStyleCompositeFilterParseError::Invalid(InvalidValueErr("\u{1F600}")),
2507                CssStyleCompositeFilterParseError::Float(float_err()),
2508                CssStyleCompositeFilterParseError::Float("".parse::<f32>().unwrap_err()),
2509                CssStyleCompositeFilterParseError::WrongNumberOfComponents {
2510                    expected: 4,
2511                    got: 0,
2512                    input: "arithmetic",
2513                },
2514                CssStyleCompositeFilterParseError::WrongNumberOfComponents {
2515                    expected: 4,
2516                    got: usize::MAX,
2517                    input: "",
2518                },
2519            ];
2520            for e in cases {
2521                assert_eq!(e.to_contained().to_shared(), e, "{e:?}");
2522                assert!(!format!("{e}").is_empty());
2523            }
2524        }
2525
2526        #[test]
2527        fn parse_errors_from_real_inputs_round_trip_through_owned() {
2528            // The same identity, but on errors the parsers actually produce.
2529            for input in [
2530                "",
2531                "blurry(5px)",
2532                "blur(5px",
2533                "blur(5px 10px 15px)",
2534                "opacity(2)",
2535                "brightness(-1)",
2536                "color-matrix(1 2 3)",
2537                "offset(1px)",
2538                "composite(nope)",
2539                "composite(arithmetic 1 2)",
2540                "hue-rotate(abc)",
2541                "flood(notacolor)",
2542                "drop-shadow(1px)",
2543                "blend(nope)",
2544                "\u{1F600}(5px)",
2545            ] {
2546                let Err(e) = parse_style_filter(input) else {
2547                    continue;
2548                };
2549                assert_eq!(
2550                    e.to_contained().to_shared(),
2551                    e,
2552                    "round-trip failed for the error of {input:?}"
2553                );
2554                assert!(!format!("{e}").is_empty(), "{input:?} has an empty Display");
2555            }
2556        }
2557    }
2558}
2559#[cfg(feature = "parser")]
2560pub use parser::{
2561    parse_style_filter_vec, CssStyleBlurParseError, CssStyleBlurParseErrorOwned,
2562    CssStyleColorMatrixParseError, CssStyleColorMatrixParseErrorOwned,
2563    CssStyleCompositeFilterParseError, CssStyleCompositeFilterParseErrorOwned,
2564    CssStyleFilterOffsetParseError, CssStyleFilterOffsetParseErrorOwned, CssStyleFilterParseError,
2565    CssStyleFilterParseErrorOwned,
2566};
2567
2568#[cfg(all(test, feature = "parser"))]
2569mod tests {
2570    // Tests assert that parsed values equal the exact source literals.
2571    #![allow(clippy::float_cmp)]
2572    use super::*;
2573    use crate::props::style::filter::parser::parse_style_filter;
2574
2575    #[test]
2576    fn test_parse_single_filter_functions() {
2577        // Blur
2578        let blur = parse_style_filter("blur(5px)").unwrap();
2579        assert!(matches!(blur, StyleFilter::Blur(_)));
2580        if let StyleFilter::Blur(b) = blur {
2581            assert_eq!(b.width, PixelValue::px(5.0));
2582            assert_eq!(b.height, PixelValue::px(5.0));
2583        }
2584
2585        // Blur with two values
2586        let blur2 = parse_style_filter("blur(2px 4px)").unwrap();
2587        if let StyleFilter::Blur(b) = blur2 {
2588            assert_eq!(b.width, PixelValue::px(2.0));
2589            assert_eq!(b.height, PixelValue::px(4.0));
2590        }
2591
2592        // Drop Shadow
2593        let shadow = parse_style_filter("drop-shadow(10px 5px 5px #888)").unwrap();
2594        assert!(matches!(shadow, StyleFilter::DropShadow(_)));
2595        if let StyleFilter::DropShadow(s) = shadow {
2596            assert_eq!(s.offset_x.inner, PixelValue::px(10.0));
2597            assert_eq!(s.blur_radius.inner, PixelValue::px(5.0));
2598            assert_eq!(s.color, ColorU::new_rgb(0x88, 0x88, 0x88));
2599        }
2600
2601        // Opacity
2602        let opacity = parse_style_filter("opacity(50%)").unwrap();
2603        assert!(matches!(opacity, StyleFilter::Opacity(_)));
2604        if let StyleFilter::Opacity(p) = opacity {
2605            assert_eq!(p.normalized(), 0.5);
2606        }
2607
2608        // Flood
2609        let flood = parse_style_filter("flood(red)").unwrap();
2610        assert_eq!(flood, StyleFilter::Flood(ColorU::RED));
2611
2612        // Composite
2613        let composite = parse_style_filter("composite(in)").unwrap();
2614        assert_eq!(composite, StyleFilter::Composite(StyleCompositeFilter::In));
2615
2616        // Offset
2617        let offset = parse_style_filter("offset(10px 20%)").unwrap();
2618        if let StyleFilter::Offset(o) = offset {
2619            assert_eq!(o.x, PixelValue::px(10.0));
2620            assert_eq!(o.y, PixelValue::percent(20.0));
2621        }
2622    }
2623
2624    #[test]
2625    fn test_parse_filter_vec() {
2626        let filters =
2627            parse_style_filter_vec("blur(5px) drop-shadow(10px 5px #888) opacity(0.8)").unwrap();
2628        assert_eq!(filters.len(), 3);
2629        assert!(matches!(filters.as_slice()[0], StyleFilter::Blur(_)));
2630        assert!(matches!(filters.as_slice()[1], StyleFilter::DropShadow(_)));
2631        assert!(matches!(filters.as_slice()[2], StyleFilter::Opacity(_)));
2632    }
2633
2634    #[test]
2635    fn test_parse_standard_css_filters() {
2636        let brightness = parse_style_filter("brightness(150%)").unwrap();
2637        if let StyleFilter::Brightness(v) = brightness {
2638            assert!((v.normalized() - 1.5).abs() < 0.001);
2639        } else {
2640            panic!("expected Brightness");
2641        }
2642
2643        let contrast = parse_style_filter("contrast(200%)").unwrap();
2644        if let StyleFilter::Contrast(v) = contrast {
2645            assert!((v.normalized() - 2.0).abs() < 0.001);
2646        } else {
2647            panic!("expected Contrast");
2648        }
2649
2650        let grayscale = parse_style_filter("grayscale(100%)").unwrap();
2651        if let StyleFilter::Grayscale(v) = grayscale {
2652            assert!((v.normalized() - 1.0).abs() < 0.001);
2653        } else {
2654            panic!("expected Grayscale");
2655        }
2656
2657        let hue = parse_style_filter("hue-rotate(90deg)").unwrap();
2658        assert!(matches!(hue, StyleFilter::HueRotate(_)));
2659
2660        let invert = parse_style_filter("invert(75%)").unwrap();
2661        if let StyleFilter::Invert(v) = invert {
2662            assert!((v.normalized() - 0.75).abs() < 0.001);
2663        } else {
2664            panic!("expected Invert");
2665        }
2666
2667        let saturate = parse_style_filter("saturate(50%)").unwrap();
2668        if let StyleFilter::Saturate(v) = saturate {
2669            assert!((v.normalized() - 0.5).abs() < 0.001);
2670        } else {
2671            panic!("expected Saturate");
2672        }
2673
2674        let sepia = parse_style_filter("sepia(60%)").unwrap();
2675        if let StyleFilter::Sepia(v) = sepia {
2676            assert!((v.normalized() - 0.6).abs() < 0.001);
2677        } else {
2678            panic!("expected Sepia");
2679        }
2680    }
2681
2682    #[test]
2683    fn test_negative_values_rejected() {
2684        assert!(parse_style_filter("brightness(-50%)").is_err());
2685        assert!(parse_style_filter("contrast(-10%)").is_err());
2686        assert!(parse_style_filter("saturate(-20%)").is_err());
2687    }
2688
2689    #[test]
2690    fn test_parse_filter_errors() {
2691        // Invalid function name
2692        assert!(parse_style_filter_vec("blurry(5px)").is_err());
2693        // Incorrect arguments
2694        assert!(parse_style_filter_vec("blur(5px 10px 15px)").is_err());
2695        assert!(parse_style_filter_vec("opacity(2)").is_err()); // opacity must be % or 0-1
2696                                                                // Unclosed parenthesis
2697        assert!(parse_style_filter_vec("blur(5px").is_err());
2698    }
2699}