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