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::{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#[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 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#[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#[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)] #[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
157impl 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
169impl 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#[cfg(feature = "parser")]
236pub mod parser {
237 #[allow(clippy::wildcard_imports)] use super::*;
239 use crate::props::basic::parse_percentage_value;
240 use crate::corety::AzString;
241
242 #[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 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 #[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)] #[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 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 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 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 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, };
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 #[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 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 fn float_err() -> ParseFloatError {
936 "x".parse::<f32>().unwrap_err()
937 }
938
939 #[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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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)", "blur(5px\u{1F600})", "blur(\u{FF15}px)", "blur(5\u{0301}px)", "blur(\u{2212}5px)", "\u{FC}ber(5px)", "flood(\u{1F600})",
1357 "composite(\u{1F600})",
1358 "hue-rotate(90\u{00B0})", "blur(\u{200B}5px)", ] {
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 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 let long_junk = format!("blur({})", "a".repeat(200_000));
1379 assert!(parse_style_filter(&long_junk).is_err());
1380
1381 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 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 #[test]
1405 fn parse_style_filter_vec_empty_and_whitespace_only_yield_an_empty_vec() {
1406 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 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 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 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 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 #[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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 #![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 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 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 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 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 let flood = parse_style_filter("flood(red)").unwrap();
2527 assert_eq!(flood, StyleFilter::Flood(ColorU::RED));
2528
2529 let composite = parse_style_filter("composite(in)").unwrap();
2531 assert_eq!(composite, StyleFilter::Composite(StyleCompositeFilter::In));
2532
2533 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 assert!(parse_style_filter_vec("blurry(5px)").is_err());
2610 assert!(parse_style_filter_vec("blur(5px 10px 15px)").is_err());
2612 assert!(parse_style_filter_vec("opacity(2)").is_err()); assert!(parse_style_filter_vec("blur(5px").is_err());
2615 }
2616}