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