1use alloc::{
4 string::{String, ToString},
5 vec::Vec,
6};
7use core::fmt;
8
9#[cfg(feature = "parser")]
10use crate::props::basic::{
11 error::{InvalidValueErr, InvalidValueErrOwned},
12 parse::{
13 parse_parentheses, parse_image, split_string_respect_comma,
14 CssImageParseError, CssImageParseErrorOwned,
15 ParenthesisParseError, ParenthesisParseErrorOwned,
16 },
17 color::parse_color_or_system,
18};
19use crate::{
20 corety::AzString,
21 codegen::format::GetHash,
22 props::{
23 basic::{
24 angle::{
25 parse_angle_value, AngleValue, CssAngleValueParseError,
26 CssAngleValueParseErrorOwned, OptionAngleValue,
27 },
28 color::{ColorU, ColorOrSystem, SystemColorRef, CssColorParseError, CssColorParseErrorOwned},
29 direction::{
30 parse_direction, CssDirectionParseError, CssDirectionParseErrorOwned, Direction,
31 },
32 length::{
33 parse_percentage_value, OptionPercentageValue, PercentageParseError,
34 PercentageParseErrorOwned, PercentageValue,
35 },
36 pixel::{
37 parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
38 PixelValue,
39 },
40 },
41 formatter::PrintAsCssValue,
42 },
43};
44
45#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
49#[repr(C)]
50#[derive(Default)]
51pub enum ExtendMode {
52 #[default]
53 Clamp,
54 Repeat,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C, u8)]
62pub enum StyleBackgroundContent {
63 LinearGradient(LinearGradient),
64 RadialGradient(RadialGradient),
65 ConicGradient(ConicGradient),
66 Image(AzString),
67 Color(ColorU),
68 SystemColor(SystemColorRef),
72}
73
74impl_option!(
75 StyleBackgroundContent,
76 OptionStyleBackgroundContent,
77 copy = false,
78 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
79);
80
81impl_vec!(StyleBackgroundContent, StyleBackgroundContentVec, StyleBackgroundContentVecDestructor, StyleBackgroundContentVecDestructorType, StyleBackgroundContentVecSlice, OptionStyleBackgroundContent);
82impl_vec_debug!(StyleBackgroundContent, StyleBackgroundContentVec);
83impl_vec_partialord!(StyleBackgroundContent, StyleBackgroundContentVec);
84impl_vec_ord!(StyleBackgroundContent, StyleBackgroundContentVec);
85impl_vec_clone!(
86 StyleBackgroundContent,
87 StyleBackgroundContentVec,
88 StyleBackgroundContentVecDestructor
89);
90impl_vec_partialeq!(StyleBackgroundContent, StyleBackgroundContentVec);
91impl_vec_eq!(StyleBackgroundContent, StyleBackgroundContentVec);
92impl_vec_hash!(StyleBackgroundContent, StyleBackgroundContentVec);
93
94impl Default for StyleBackgroundContent {
95 fn default() -> Self {
96 Self::Color(ColorU::TRANSPARENT)
97 }
98}
99
100impl PrintAsCssValue for StyleBackgroundContent {
101 fn print_as_css_value(&self) -> String {
102 match self {
103 Self::LinearGradient(lg) => {
104 let prefix = if lg.extend_mode == ExtendMode::Repeat {
105 "repeating-linear-gradient"
106 } else {
107 "linear-gradient"
108 };
109 format!("{}({})", prefix, lg.print_as_css_value())
110 }
111 Self::RadialGradient(rg) => {
112 let prefix = if rg.extend_mode == ExtendMode::Repeat {
113 "repeating-radial-gradient"
114 } else {
115 "radial-gradient"
116 };
117 format!("{}({})", prefix, rg.print_as_css_value())
118 }
119 Self::ConicGradient(cg) => {
120 let prefix = if cg.extend_mode == ExtendMode::Repeat {
121 "repeating-conic-gradient"
122 } else {
123 "conic-gradient"
124 };
125 format!("{}({})", prefix, cg.print_as_css_value())
126 }
127 Self::Image(id) => format!("url(\"{}\")", id.as_str()),
128 Self::Color(c) => c.to_hash(),
129 Self::SystemColor(s) => s.as_css_str().to_string(),
130 }
131 }
132}
133
134impl crate::codegen::format::FormatAsRustCode for StyleBackgroundContent {
137 fn format_as_rust_code(&self, _tabs: usize) -> String {
138 format!("StyleBackgroundContent::from_css(\"{}\")", self.print_as_css_value())
140 }
141}
142
143impl crate::codegen::format::FormatAsRustCode for StyleBackgroundSizeVec {
144 fn format_as_rust_code(&self, _tabs: usize) -> String {
145 format!(
146 "StyleBackgroundSizeVec::from_const_slice(STYLE_BACKGROUND_SIZE_{}_ITEMS)",
147 self.get_hash()
148 )
149 }
150}
151
152impl crate::codegen::format::FormatAsRustCode for StyleBackgroundRepeatVec {
153 fn format_as_rust_code(&self, _tabs: usize) -> String {
154 format!(
155 "StyleBackgroundRepeatVec::from_const_slice(STYLE_BACKGROUND_REPEAT_{}_ITEMS)",
156 self.get_hash()
157 )
158 }
159}
160
161impl crate::codegen::format::FormatAsRustCode for StyleBackgroundContentVec {
162 fn format_as_rust_code(&self, _tabs: usize) -> String {
163 format!(
164 "StyleBackgroundContentVec::from_const_slice(STYLE_BACKGROUND_CONTENT_{}_ITEMS)",
165 self.get_hash()
166 )
167 }
168}
169
170impl PrintAsCssValue for StyleBackgroundContentVec {
171 fn print_as_css_value(&self) -> String {
172 self.as_ref()
173 .iter()
174 .map(PrintAsCssValue::print_as_css_value)
175 .collect::<Vec<_>>()
176 .join(", ")
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
184#[repr(C)]
185pub struct LinearGradient {
186 pub direction: Direction,
187 pub extend_mode: ExtendMode,
188 pub stops: NormalizedLinearColorStopVec,
189}
190impl Default for LinearGradient {
191 fn default() -> Self {
192 Self {
193 direction: Direction::default(),
194 extend_mode: ExtendMode::default(),
195 stops: Vec::new().into(),
196 }
197 }
198}
199impl PrintAsCssValue for LinearGradient {
200 fn print_as_css_value(&self) -> String {
201 let dir_str = self.direction.print_as_css_value();
202 let stops_str = self
203 .stops
204 .iter()
205 .map(PrintAsCssValue::print_as_css_value)
206 .collect::<Vec<_>>()
207 .join(", ");
208 if stops_str.is_empty() {
209 dir_str
210 } else {
211 format!("{dir_str}, {stops_str}")
212 }
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
218#[repr(C)]
219pub struct RadialGradient {
220 pub shape: Shape,
221 pub size: RadialGradientSize,
222 pub position: StyleBackgroundPosition,
223 pub extend_mode: ExtendMode,
224 pub stops: NormalizedLinearColorStopVec,
225}
226impl Default for RadialGradient {
227 fn default() -> Self {
228 Self {
229 shape: Shape::default(),
230 size: RadialGradientSize::default(),
231 position: StyleBackgroundPosition::default(),
232 extend_mode: ExtendMode::default(),
233 stops: Vec::new().into(),
234 }
235 }
236}
237impl PrintAsCssValue for RadialGradient {
238 fn print_as_css_value(&self) -> String {
239 let stops_str = self
240 .stops
241 .iter()
242 .map(PrintAsCssValue::print_as_css_value)
243 .collect::<Vec<_>>()
244 .join(", ");
245 format!(
246 "{} {} at {}, {}",
247 self.shape,
248 self.size,
249 self.position.print_as_css_value(),
250 stops_str
251 )
252 }
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
257#[repr(C)]
258pub struct ConicGradient {
259 pub extend_mode: ExtendMode,
260 pub center: StyleBackgroundPosition,
261 pub angle: AngleValue,
262 pub stops: NormalizedRadialColorStopVec,
263}
264impl Default for ConicGradient {
265 fn default() -> Self {
266 Self {
267 extend_mode: ExtendMode::default(),
268 center: StyleBackgroundPosition {
272 horizontal: BackgroundPositionHorizontal::Center,
273 vertical: BackgroundPositionVertical::Center,
274 },
275 angle: AngleValue::default(),
276 stops: Vec::new().into(),
277 }
278 }
279}
280impl PrintAsCssValue for ConicGradient {
281 fn print_as_css_value(&self) -> String {
282 let stops_str = self
283 .stops
284 .iter()
285 .map(PrintAsCssValue::print_as_css_value)
286 .collect::<Vec<_>>()
287 .join(", ");
288 format!(
289 "from {} at {}, {}",
290 self.angle,
291 self.center.print_as_css_value(),
292 stops_str
293 )
294 }
295}
296
297#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
301#[repr(C)]
302#[derive(Default)]
303pub enum Shape {
304 #[default]
305 Ellipse,
306 Circle,
307}
308impl fmt::Display for Shape {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 write!(
311 f,
312 "{}",
313 match self {
314 Self::Ellipse => "ellipse",
315 Self::Circle => "circle",
316 }
317 )
318 }
319}
320
321#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
323#[repr(C)]
324#[derive(Default)]
325pub enum RadialGradientSize {
326 ClosestSide,
327 ClosestCorner,
328 FarthestSide,
329 #[default]
330 FarthestCorner,
331}
332impl fmt::Display for RadialGradientSize {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 write!(
335 f,
336 "{}",
337 match self {
338 Self::ClosestSide => "closest-side",
339 Self::ClosestCorner => "closest-corner",
340 Self::FarthestSide => "farthest-side",
341 Self::FarthestCorner => "farthest-corner",
342 }
343 )
344 }
345}
346
347#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
348#[repr(C)]
349pub struct NormalizedLinearColorStop {
350 pub offset: PercentageValue,
351 pub color: ColorOrSystem,
353}
354
355impl NormalizedLinearColorStop {
356 #[must_use] pub const fn new(offset: PercentageValue, color: ColorU) -> Self {
358 Self { offset, color: ColorOrSystem::color(color) }
359 }
360
361 #[must_use] pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
363 self.color.resolve(system_colors, fallback)
364 }
365}
366
367impl_option!(
368 NormalizedLinearColorStop,
369 OptionNormalizedLinearColorStop,
370 copy = false,
371 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
372);
373impl_vec!(NormalizedLinearColorStop, NormalizedLinearColorStopVec, NormalizedLinearColorStopVecDestructor, NormalizedLinearColorStopVecDestructorType, NormalizedLinearColorStopVecSlice, OptionNormalizedLinearColorStop);
374impl_vec_debug!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
375impl_vec_partialord!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
376impl_vec_ord!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
377impl_vec_clone!(
378 NormalizedLinearColorStop,
379 NormalizedLinearColorStopVec,
380 NormalizedLinearColorStopVecDestructor
381);
382impl_vec_partialeq!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
383impl_vec_eq!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
384impl_vec_hash!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
385impl PrintAsCssValue for NormalizedLinearColorStop {
386 fn print_as_css_value(&self) -> String {
387 match &self.color {
388 ColorOrSystem::Color(c) => format!("{} {}", c.to_hash(), self.offset),
389 ColorOrSystem::System(s) => format!("{} {}", s.as_css_str(), self.offset),
390 }
391 }
392}
393
394#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
395#[repr(C)]
396pub struct NormalizedRadialColorStop {
397 pub angle: AngleValue,
398 pub color: ColorOrSystem,
400}
401
402impl NormalizedRadialColorStop {
403 #[must_use] pub const fn new(angle: AngleValue, color: ColorU) -> Self {
405 Self { angle, color: ColorOrSystem::color(color) }
406 }
407
408 #[must_use] pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
410 self.color.resolve(system_colors, fallback)
411 }
412}
413
414impl_option!(
415 NormalizedRadialColorStop,
416 OptionNormalizedRadialColorStop,
417 copy = false,
418 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
419);
420impl_vec!(NormalizedRadialColorStop, NormalizedRadialColorStopVec, NormalizedRadialColorStopVecDestructor, NormalizedRadialColorStopVecDestructorType, NormalizedRadialColorStopVecSlice, OptionNormalizedRadialColorStop);
421impl_vec_debug!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
422impl_vec_partialord!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
423impl_vec_ord!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
424impl_vec_clone!(
425 NormalizedRadialColorStop,
426 NormalizedRadialColorStopVec,
427 NormalizedRadialColorStopVecDestructor
428);
429impl_vec_partialeq!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
430impl_vec_eq!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
431impl_vec_hash!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
432impl PrintAsCssValue for NormalizedRadialColorStop {
433 fn print_as_css_value(&self) -> String {
434 match &self.color {
435 ColorOrSystem::Color(c) => format!("{} {}", c.to_hash(), self.angle),
436 ColorOrSystem::System(s) => format!("{} {}", s.as_css_str(), self.angle),
437 }
438 }
439}
440
441#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
450pub struct LinearColorStop {
451 pub color: ColorOrSystem,
452 pub offset1: OptionPercentageValue,
454 pub offset2: OptionPercentageValue,
457}
458
459#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
468pub struct RadialColorStop {
469 pub color: ColorOrSystem,
470 pub offset1: OptionAngleValue,
472 pub offset2: OptionAngleValue,
475}
476
477#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
481#[repr(C)]
482pub struct StyleBackgroundPosition {
483 pub horizontal: BackgroundPositionHorizontal,
484 pub vertical: BackgroundPositionVertical,
485}
486
487impl_option!(
488 StyleBackgroundPosition,
489 OptionStyleBackgroundPosition,
490 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
491);
492impl_vec!(StyleBackgroundPosition, StyleBackgroundPositionVec, StyleBackgroundPositionVecDestructor, StyleBackgroundPositionVecDestructorType, StyleBackgroundPositionVecSlice, OptionStyleBackgroundPosition);
493impl_vec_debug!(StyleBackgroundPosition, StyleBackgroundPositionVec);
494impl_vec_partialord!(StyleBackgroundPosition, StyleBackgroundPositionVec);
495impl_vec_ord!(StyleBackgroundPosition, StyleBackgroundPositionVec);
496impl_vec_clone!(
497 StyleBackgroundPosition,
498 StyleBackgroundPositionVec,
499 StyleBackgroundPositionVecDestructor
500);
501impl_vec_partialeq!(StyleBackgroundPosition, StyleBackgroundPositionVec);
502impl_vec_eq!(StyleBackgroundPosition, StyleBackgroundPositionVec);
503impl_vec_hash!(StyleBackgroundPosition, StyleBackgroundPositionVec);
504impl Default for StyleBackgroundPosition {
505 fn default() -> Self {
506 Self {
507 horizontal: BackgroundPositionHorizontal::Left,
508 vertical: BackgroundPositionVertical::Top,
509 }
510 }
511}
512
513impl StyleBackgroundPosition {
514 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
515 self.horizontal.scale_for_dpi(scale_factor);
516 self.vertical.scale_for_dpi(scale_factor);
517 }
518}
519
520impl PrintAsCssValue for StyleBackgroundPosition {
521 fn print_as_css_value(&self) -> String {
522 format!(
523 "{} {}",
524 self.horizontal.print_as_css_value(),
525 self.vertical.print_as_css_value()
526 )
527 }
528}
529impl PrintAsCssValue for StyleBackgroundPositionVec {
530 fn print_as_css_value(&self) -> String {
531 self.iter()
532 .map(PrintAsCssValue::print_as_css_value)
533 .collect::<Vec<_>>()
534 .join(", ")
535 }
536}
537
538impl crate::codegen::format::FormatAsRustCode for StyleBackgroundPositionVec {
540 fn format_as_rust_code(&self, _tabs: usize) -> String {
541 format!(
542 "StyleBackgroundPositionVec::from_const_slice(STYLE_BACKGROUND_POSITION_{}_ITEMS)",
543 self.get_hash()
544 )
545 }
546}
547#[allow(variant_size_differences)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
550#[repr(C, u8)]
551pub enum BackgroundPositionHorizontal {
552 Left,
553 Center,
554 Right,
555 Exact(PixelValue),
556}
557
558impl BackgroundPositionHorizontal {
559 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
560 if let Self::Exact(s) = self {
561 s.scale_for_dpi(scale_factor);
562 }
563 }
564}
565
566impl PrintAsCssValue for BackgroundPositionHorizontal {
567 fn print_as_css_value(&self) -> String {
568 match self {
569 Self::Left => "left".to_string(),
570 Self::Center => "center".to_string(),
571 Self::Right => "right".to_string(),
572 Self::Exact(px) => px.print_as_css_value(),
573 }
574 }
575}
576#[allow(variant_size_differences)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
579#[repr(C, u8)]
580pub enum BackgroundPositionVertical {
581 Top,
582 Center,
583 Bottom,
584 Exact(PixelValue),
585}
586
587impl BackgroundPositionVertical {
588 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
589 if let Self::Exact(s) = self {
590 s.scale_for_dpi(scale_factor);
591 }
592 }
593}
594
595impl PrintAsCssValue for BackgroundPositionVertical {
596 fn print_as_css_value(&self) -> String {
597 match self {
598 Self::Top => "top".to_string(),
599 Self::Center => "center".to_string(),
600 Self::Bottom => "bottom".to_string(),
601 Self::Exact(px) => px.print_as_css_value(),
602 }
603 }
604}
605#[allow(variant_size_differences)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
608#[repr(C, u8)]
609#[derive(Default)]
610pub enum StyleBackgroundSize {
611 ExactSize(PixelValueSize),
612 #[default]
613 Contain,
614 Cover,
615}
616
617impl_option!(
618 StyleBackgroundSize,
619 OptionStyleBackgroundSize,
620 copy = false,
621 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
622);
623
624#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
627#[repr(C)]
628pub struct PixelValueSize {
629 pub width: PixelValue,
630 pub height: PixelValue,
631}
632
633impl_vec!(StyleBackgroundSize, StyleBackgroundSizeVec, StyleBackgroundSizeVecDestructor, StyleBackgroundSizeVecDestructorType, StyleBackgroundSizeVecSlice, OptionStyleBackgroundSize);
634impl_vec_debug!(StyleBackgroundSize, StyleBackgroundSizeVec);
635impl_vec_partialord!(StyleBackgroundSize, StyleBackgroundSizeVec);
636impl_vec_ord!(StyleBackgroundSize, StyleBackgroundSizeVec);
637impl_vec_clone!(
638 StyleBackgroundSize,
639 StyleBackgroundSizeVec,
640 StyleBackgroundSizeVecDestructor
641);
642impl_vec_partialeq!(StyleBackgroundSize, StyleBackgroundSizeVec);
643impl_vec_eq!(StyleBackgroundSize, StyleBackgroundSizeVec);
644impl_vec_hash!(StyleBackgroundSize, StyleBackgroundSizeVec);
645
646impl StyleBackgroundSize {
647 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
648 if let Self::ExactSize(size) = self {
649 size.width.scale_for_dpi(scale_factor);
650 size.height.scale_for_dpi(scale_factor);
651 }
652 }
653}
654
655impl PrintAsCssValue for StyleBackgroundSize {
656 fn print_as_css_value(&self) -> String {
657 match self {
658 Self::Contain => "contain".to_string(),
659 Self::Cover => "cover".to_string(),
660 Self::ExactSize(size) => {
661 format!(
662 "{} {}",
663 size.width.print_as_css_value(),
664 size.height.print_as_css_value()
665 )
666 }
667 }
668 }
669}
670impl PrintAsCssValue for StyleBackgroundSizeVec {
671 fn print_as_css_value(&self) -> String {
672 self.iter()
673 .map(PrintAsCssValue::print_as_css_value)
674 .collect::<Vec<_>>()
675 .join(", ")
676 }
677}
678
679#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
681#[repr(C)]
682#[derive(Default)]
683pub enum StyleBackgroundRepeat {
684 NoRepeat,
685 #[default]
686 PatternRepeat,
687 RepeatX,
688 RepeatY,
689}
690
691impl_option!(
692 StyleBackgroundRepeat,
693 OptionStyleBackgroundRepeat,
694 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
695);
696impl_vec!(StyleBackgroundRepeat, StyleBackgroundRepeatVec, StyleBackgroundRepeatVecDestructor, StyleBackgroundRepeatVecDestructorType, StyleBackgroundRepeatVecSlice, OptionStyleBackgroundRepeat);
697impl_vec_debug!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
698impl_vec_partialord!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
699impl_vec_ord!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
700impl_vec_clone!(
701 StyleBackgroundRepeat,
702 StyleBackgroundRepeatVec,
703 StyleBackgroundRepeatVecDestructor
704);
705impl_vec_partialeq!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
706impl_vec_eq!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
707impl_vec_hash!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
708impl PrintAsCssValue for StyleBackgroundRepeat {
709 fn print_as_css_value(&self) -> String {
710 match self {
711 Self::NoRepeat => "no-repeat".to_string(),
712 Self::PatternRepeat => "repeat".to_string(),
713 Self::RepeatX => "repeat-x".to_string(),
714 Self::RepeatY => "repeat-y".to_string(),
715 }
716 }
717}
718impl PrintAsCssValue for StyleBackgroundRepeatVec {
719 fn print_as_css_value(&self) -> String {
720 self.iter()
721 .map(PrintAsCssValue::print_as_css_value)
722 .collect::<Vec<_>>()
723 .join(", ")
724 }
725}
726
727#[derive(Clone, PartialEq)]
730pub enum CssBackgroundParseError<'a> {
731 Error(&'a str),
732 InvalidBackground(ParenthesisParseError<'a>),
733 UnclosedGradient(&'a str),
734 NoDirection(&'a str),
735 TooFewGradientStops(&'a str),
736 DirectionParseError(CssDirectionParseError<'a>),
737 GradientParseError(CssGradientStopParseError<'a>),
738 ConicGradient(CssConicGradientParseError<'a>),
739 ShapeParseError(CssShapeParseError<'a>),
740 ImageParseError(CssImageParseError<'a>),
741 ColorParseError(CssColorParseError<'a>),
742}
743
744impl_debug_as_display!(CssBackgroundParseError<'a>);
745impl_display! { CssBackgroundParseError<'a>, {
746 Error(e) => e,
747 InvalidBackground(val) => format!("Invalid background value: \"{}\"", val),
748 UnclosedGradient(val) => format!("Unclosed gradient: \"{}\"", val),
749 NoDirection(val) => format!("Gradient has no direction: \"{}\"", val),
750 TooFewGradientStops(val) => format!("Failed to parse gradient due to too few gradient steps: \"{}\"", val),
751 DirectionParseError(e) => format!("Failed to parse gradient direction: \"{}\"", e),
752 GradientParseError(e) => format!("Failed to parse gradient: {}", e),
753 ConicGradient(e) => format!("Failed to parse conic gradient: {}", e),
754 ShapeParseError(e) => format!("Failed to parse shape of radial gradient: {}", e),
755 ImageParseError(e) => format!("Failed to parse image() value: {}", e),
756 ColorParseError(e) => format!("Failed to parse color value: {}", e),
757}}
758
759#[cfg(feature = "parser")]
760impl_from!(
761 ParenthesisParseError<'a>,
762 CssBackgroundParseError::InvalidBackground
763);
764#[cfg(feature = "parser")]
765impl_from!(
766 CssDirectionParseError<'a>,
767 CssBackgroundParseError::DirectionParseError
768);
769#[cfg(feature = "parser")]
770impl_from!(
771 CssGradientStopParseError<'a>,
772 CssBackgroundParseError::GradientParseError
773);
774#[cfg(feature = "parser")]
775impl_from!(
776 CssShapeParseError<'a>,
777 CssBackgroundParseError::ShapeParseError
778);
779#[cfg(feature = "parser")]
780impl_from!(
781 CssImageParseError<'a>,
782 CssBackgroundParseError::ImageParseError
783);
784#[cfg(feature = "parser")]
785impl_from!(
786 CssColorParseError<'a>,
787 CssBackgroundParseError::ColorParseError
788);
789#[cfg(feature = "parser")]
790impl_from!(
791 CssConicGradientParseError<'a>,
792 CssBackgroundParseError::ConicGradient
793);
794
795#[derive(Debug, Clone, PartialEq)]
796#[repr(C, u8)]
797pub enum CssBackgroundParseErrorOwned {
798 Error(AzString),
799 InvalidBackground(ParenthesisParseErrorOwned),
800 UnclosedGradient(AzString),
801 NoDirection(AzString),
802 TooFewGradientStops(AzString),
803 DirectionParseError(CssDirectionParseErrorOwned),
804 GradientParseError(CssGradientStopParseErrorOwned),
805 ConicGradient(CssConicGradientParseErrorOwned),
806 ShapeParseError(CssShapeParseErrorOwned),
807 ImageParseError(CssImageParseErrorOwned),
808 ColorParseError(CssColorParseErrorOwned),
809}
810
811impl CssBackgroundParseError<'_> {
812 #[must_use] pub fn to_contained(&self) -> CssBackgroundParseErrorOwned {
813 match self {
814 Self::Error(s) => CssBackgroundParseErrorOwned::Error((*s).to_string().into()),
815 Self::InvalidBackground(e) => {
816 CssBackgroundParseErrorOwned::InvalidBackground(e.to_contained())
817 }
818 Self::UnclosedGradient(s) => {
819 CssBackgroundParseErrorOwned::UnclosedGradient((*s).to_string().into())
820 }
821 Self::NoDirection(s) => CssBackgroundParseErrorOwned::NoDirection((*s).to_string().into()),
822 Self::TooFewGradientStops(s) => {
823 CssBackgroundParseErrorOwned::TooFewGradientStops((*s).to_string().into())
824 }
825 Self::DirectionParseError(e) => {
826 CssBackgroundParseErrorOwned::DirectionParseError(e.to_contained())
827 }
828 Self::GradientParseError(e) => {
829 CssBackgroundParseErrorOwned::GradientParseError(e.to_contained())
830 }
831 Self::ConicGradient(e) => CssBackgroundParseErrorOwned::ConicGradient(e.to_contained()),
832 Self::ShapeParseError(e) => {
833 CssBackgroundParseErrorOwned::ShapeParseError(e.to_contained())
834 }
835 Self::ImageParseError(e) => {
836 CssBackgroundParseErrorOwned::ImageParseError(e.to_contained())
837 }
838 Self::ColorParseError(e) => {
839 CssBackgroundParseErrorOwned::ColorParseError(e.to_contained())
840 }
841 }
842 }
843}
844
845impl CssBackgroundParseErrorOwned {
846 #[must_use] pub fn to_shared(&self) -> CssBackgroundParseError<'_> {
847 match self {
848 Self::Error(s) => CssBackgroundParseError::Error(s),
849 Self::InvalidBackground(e) => CssBackgroundParseError::InvalidBackground(e.to_shared()),
850 Self::UnclosedGradient(s) => CssBackgroundParseError::UnclosedGradient(s),
851 Self::NoDirection(s) => CssBackgroundParseError::NoDirection(s),
852 Self::TooFewGradientStops(s) => CssBackgroundParseError::TooFewGradientStops(s),
853 Self::DirectionParseError(e) => {
854 CssBackgroundParseError::DirectionParseError(e.to_shared())
855 }
856 Self::GradientParseError(e) => {
857 CssBackgroundParseError::GradientParseError(e.to_shared())
858 }
859 Self::ConicGradient(e) => CssBackgroundParseError::ConicGradient(e.to_shared()),
860 Self::ShapeParseError(e) => CssBackgroundParseError::ShapeParseError(e.to_shared()),
861 Self::ImageParseError(e) => CssBackgroundParseError::ImageParseError(e.to_shared()),
862 Self::ColorParseError(e) => CssBackgroundParseError::ColorParseError(e.to_shared()),
863 }
864 }
865}
866
867#[derive(Clone, PartialEq)]
868pub enum CssGradientStopParseError<'a> {
869 Error(&'a str),
870 Percentage(PercentageParseError),
871 Angle(CssAngleValueParseError<'a>),
872 ColorParseError(CssColorParseError<'a>),
873}
874
875impl_debug_as_display!(CssGradientStopParseError<'a>);
876impl_display! { CssGradientStopParseError<'a>, {
877 Error(e) => e,
878 Percentage(e) => format!("Failed to parse offset percentage: {}", e),
879 Angle(e) => format!("Failed to parse angle: {}", e),
880 ColorParseError(e) => format!("{}", e),
881}}
882#[cfg(feature = "parser")]
883impl_from!(
884 CssColorParseError<'a>,
885 CssGradientStopParseError::ColorParseError
886);
887
888#[derive(Debug, Clone, PartialEq)]
889#[repr(C, u8)]
890pub enum CssGradientStopParseErrorOwned {
891 Error(AzString),
892 Percentage(PercentageParseErrorOwned),
893 Angle(CssAngleValueParseErrorOwned),
894 ColorParseError(CssColorParseErrorOwned),
895}
896
897impl CssGradientStopParseError<'_> {
898 #[must_use] pub fn to_contained(&self) -> CssGradientStopParseErrorOwned {
899 match self {
900 Self::Error(s) => CssGradientStopParseErrorOwned::Error((*s).to_string().into()),
901 Self::Percentage(e) => CssGradientStopParseErrorOwned::Percentage(e.to_contained()),
902 Self::Angle(e) => CssGradientStopParseErrorOwned::Angle(e.to_contained()),
903 Self::ColorParseError(e) => {
904 CssGradientStopParseErrorOwned::ColorParseError(e.to_contained())
905 }
906 }
907 }
908}
909
910impl CssGradientStopParseErrorOwned {
911 #[must_use] pub fn to_shared(&self) -> CssGradientStopParseError<'_> {
912 match self {
913 Self::Error(s) => CssGradientStopParseError::Error(s),
914 Self::Percentage(e) => CssGradientStopParseError::Percentage(e.to_shared()),
915 Self::Angle(e) => CssGradientStopParseError::Angle(e.to_shared()),
916 Self::ColorParseError(e) => CssGradientStopParseError::ColorParseError(e.to_shared()),
917 }
918 }
919}
920
921#[derive(Clone, PartialEq, Eq)]
922pub enum CssConicGradientParseError<'a> {
923 Position(CssBackgroundPositionParseError<'a>),
924 Angle(CssAngleValueParseError<'a>),
925 NoAngle(&'a str),
926}
927impl_debug_as_display!(CssConicGradientParseError<'a>);
928impl_display! { CssConicGradientParseError<'a>, {
929 Position(val) => format!("Invalid position attribute: \"{}\"", val),
930 Angle(val) => format!("Invalid angle value: \"{}\"", val),
931 NoAngle(val) => format!("Expected angle: \"{}\"", val),
932}}
933#[cfg(feature = "parser")]
934impl_from!(
935 CssAngleValueParseError<'a>,
936 CssConicGradientParseError::Angle
937);
938#[cfg(feature = "parser")]
939impl_from!(
940 CssBackgroundPositionParseError<'a>,
941 CssConicGradientParseError::Position
942);
943
944#[derive(Debug, Clone, PartialEq, Eq)]
945#[repr(C, u8)]
946pub enum CssConicGradientParseErrorOwned {
947 Position(CssBackgroundPositionParseErrorOwned),
948 Angle(CssAngleValueParseErrorOwned),
949 NoAngle(AzString),
950}
951impl CssConicGradientParseError<'_> {
952 #[must_use] pub fn to_contained(&self) -> CssConicGradientParseErrorOwned {
953 match self {
954 Self::Position(e) => CssConicGradientParseErrorOwned::Position(e.to_contained()),
955 Self::Angle(e) => CssConicGradientParseErrorOwned::Angle(e.to_contained()),
956 Self::NoAngle(s) => CssConicGradientParseErrorOwned::NoAngle((*s).to_string().into()),
957 }
958 }
959}
960impl CssConicGradientParseErrorOwned {
961 #[must_use] pub fn to_shared(&self) -> CssConicGradientParseError<'_> {
962 match self {
963 Self::Position(e) => CssConicGradientParseError::Position(e.to_shared()),
964 Self::Angle(e) => CssConicGradientParseError::Angle(e.to_shared()),
965 Self::NoAngle(s) => CssConicGradientParseError::NoAngle(s),
966 }
967 }
968}
969
970#[derive(Debug, Copy, Clone, PartialEq, Eq)]
971pub enum CssShapeParseError<'a> {
972 ShapeErr(InvalidValueErr<'a>),
973}
974impl_display! {CssShapeParseError<'a>, {
975 ShapeErr(e) => format!("\"{}\"", e.0),
976}}
977#[derive(Debug, Clone, PartialEq, Eq)]
978#[repr(C, u8)]
979pub enum CssShapeParseErrorOwned {
980 ShapeErr(InvalidValueErrOwned),
981}
982impl CssShapeParseError<'_> {
983 #[must_use] pub fn to_contained(&self) -> CssShapeParseErrorOwned {
984 match self {
985 Self::ShapeErr(err) => CssShapeParseErrorOwned::ShapeErr(err.to_contained()),
986 }
987 }
988}
989impl CssShapeParseErrorOwned {
990 #[must_use] pub fn to_shared(&self) -> CssShapeParseError<'_> {
991 match self {
992 Self::ShapeErr(err) => CssShapeParseError::ShapeErr(err.to_shared()),
993 }
994 }
995}
996
997#[derive(Debug, Clone, PartialEq, Eq)]
998pub enum CssBackgroundPositionParseError<'a> {
999 NoPosition(&'a str),
1000 TooManyComponents(&'a str),
1001 FirstComponentWrong(CssPixelValueParseError<'a>),
1002 SecondComponentWrong(CssPixelValueParseError<'a>),
1003}
1004
1005impl_display! {CssBackgroundPositionParseError<'a>, {
1006 NoPosition(e) => format!("First background position missing: \"{}\"", e),
1007 TooManyComponents(e) => format!("background-position can only have one or two components, not more: \"{}\"", e),
1008 FirstComponentWrong(e) => format!("Failed to parse first component: \"{}\"", e),
1009 SecondComponentWrong(e) => format!("Failed to parse second component: \"{}\"", e),
1010}}
1011#[derive(Debug, Clone, PartialEq, Eq)]
1012#[repr(C, u8)]
1013pub enum CssBackgroundPositionParseErrorOwned {
1014 NoPosition(AzString),
1015 TooManyComponents(AzString),
1016 FirstComponentWrong(CssPixelValueParseErrorOwned),
1017 SecondComponentWrong(CssPixelValueParseErrorOwned),
1018}
1019impl CssBackgroundPositionParseError<'_> {
1020 #[must_use] pub fn to_contained(&self) -> CssBackgroundPositionParseErrorOwned {
1021 match self {
1022 Self::NoPosition(s) => CssBackgroundPositionParseErrorOwned::NoPosition((*s).to_string().into()),
1023 Self::TooManyComponents(s) => {
1024 CssBackgroundPositionParseErrorOwned::TooManyComponents((*s).to_string().into())
1025 }
1026 Self::FirstComponentWrong(e) => {
1027 CssBackgroundPositionParseErrorOwned::FirstComponentWrong(e.to_contained())
1028 }
1029 Self::SecondComponentWrong(e) => {
1030 CssBackgroundPositionParseErrorOwned::SecondComponentWrong(e.to_contained())
1031 }
1032 }
1033 }
1034}
1035impl CssBackgroundPositionParseErrorOwned {
1036 #[must_use] pub fn to_shared(&self) -> CssBackgroundPositionParseError<'_> {
1037 match self {
1038 Self::NoPosition(s) => CssBackgroundPositionParseError::NoPosition(s),
1039 Self::TooManyComponents(s) => CssBackgroundPositionParseError::TooManyComponents(s),
1040 Self::FirstComponentWrong(e) => {
1041 CssBackgroundPositionParseError::FirstComponentWrong(e.to_shared())
1042 }
1043 Self::SecondComponentWrong(e) => {
1044 CssBackgroundPositionParseError::SecondComponentWrong(e.to_shared())
1045 }
1046 }
1047 }
1048}
1049
1050#[cfg(feature = "parser")]
1053pub mod parser {
1054 #[allow(clippy::wildcard_imports)] use super::*;
1056
1057 #[allow(clippy::enum_variant_names)]
1060 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
1061 enum GradientType {
1062 LinearGradient,
1063 RepeatingLinearGradient,
1064 RadialGradient,
1065 RepeatingRadialGradient,
1066 ConicGradient,
1067 RepeatingConicGradient,
1068 }
1069
1070 impl GradientType {
1071 pub(crate) const fn get_extend_mode(self) -> ExtendMode {
1072 match self {
1073 Self::LinearGradient | Self::RadialGradient | Self::ConicGradient => {
1074 ExtendMode::Clamp
1075 }
1076 Self::RepeatingLinearGradient
1077 | Self::RepeatingRadialGradient
1078 | Self::RepeatingConicGradient => ExtendMode::Repeat,
1079 }
1080 }
1081 }
1082
1083 pub fn parse_style_background_content_multiple(
1090 input: &str,
1091 ) -> Result<StyleBackgroundContentVec, CssBackgroundParseError<'_>> {
1092 Ok(split_string_respect_comma(input)
1093 .iter()
1094 .map(|i| parse_style_background_content(i))
1095 .collect::<Result<Vec<_>, _>>()?
1096 .into())
1097 }
1098
1099 pub fn parse_style_background_content(
1104 input: &str,
1105 ) -> Result<StyleBackgroundContent, CssBackgroundParseError<'_>> {
1106 match parse_parentheses(
1107 input,
1108 &[
1109 "linear-gradient",
1110 "repeating-linear-gradient",
1111 "radial-gradient",
1112 "repeating-radial-gradient",
1113 "conic-gradient",
1114 "repeating-conic-gradient",
1115 "image",
1116 "url",
1117 ],
1118 ) {
1119 Ok((background_type, brace_contents)) => {
1120 let gradient_type = match background_type {
1121 "linear-gradient" => GradientType::LinearGradient,
1122 "repeating-linear-gradient" => GradientType::RepeatingLinearGradient,
1123 "radial-gradient" => GradientType::RadialGradient,
1124 "repeating-radial-gradient" => GradientType::RepeatingRadialGradient,
1125 "conic-gradient" => GradientType::ConicGradient,
1126 "repeating-conic-gradient" => GradientType::RepeatingConicGradient,
1127 "image" | "url" => {
1128 return Ok(StyleBackgroundContent::Image(
1129 parse_image(brace_contents)?,
1130 ))
1131 }
1132 _ => unreachable!(),
1133 };
1134 parse_gradient(brace_contents, gradient_type)
1135 }
1136 Err(_) => Ok(match parse_color_or_system(input)? {
1142 ColorOrSystem::Color(c) => StyleBackgroundContent::Color(c),
1143 ColorOrSystem::System(s) => StyleBackgroundContent::SystemColor(s),
1144 }),
1145 }
1146 }
1147
1148 pub fn parse_style_background_position_multiple(
1153 input: &str,
1154 ) -> Result<StyleBackgroundPositionVec, CssBackgroundPositionParseError<'_>> {
1155 Ok(split_string_respect_comma(input)
1156 .iter()
1157 .map(|i| parse_style_background_position(i))
1158 .collect::<Result<Vec<_>, _>>()?
1159 .into())
1160 }
1161
1162 pub fn parse_style_background_position(
1167 input: &str,
1168 ) -> Result<StyleBackgroundPosition, CssBackgroundPositionParseError<'_>> {
1169 let input = input.trim();
1170 let mut whitespace_iter = input.split_whitespace();
1171
1172 let first = whitespace_iter
1173 .next()
1174 .ok_or(CssBackgroundPositionParseError::NoPosition(input))?;
1175 let second = whitespace_iter.next();
1176
1177 if whitespace_iter.next().is_some() {
1178 return Err(CssBackgroundPositionParseError::TooManyComponents(input));
1179 }
1180
1181 if let Ok(horizontal) = parse_background_position_horizontal(first) {
1183 let vertical = match second {
1184 Some(s) => parse_background_position_vertical(s)
1185 .map_err(CssBackgroundPositionParseError::SecondComponentWrong)?,
1186 None => BackgroundPositionVertical::Center,
1187 };
1188 return Ok(StyleBackgroundPosition {
1189 horizontal,
1190 vertical,
1191 });
1192 }
1193
1194 if let Ok(vertical) = parse_background_position_vertical(first) {
1196 let horizontal = match second {
1197 Some(s) => parse_background_position_horizontal(s)
1198 .map_err(CssBackgroundPositionParseError::FirstComponentWrong)?,
1199 None => BackgroundPositionHorizontal::Center,
1200 };
1201 return Ok(StyleBackgroundPosition {
1202 horizontal,
1203 vertical,
1204 });
1205 }
1206
1207 Err(CssBackgroundPositionParseError::FirstComponentWrong(
1208 CssPixelValueParseError::InvalidPixelValue(first),
1209 ))
1210 }
1211
1212 pub fn parse_style_background_size_multiple(
1217 input: &str,
1218 ) -> Result<StyleBackgroundSizeVec, InvalidValueErr<'_>> {
1219 Ok(split_string_respect_comma(input)
1220 .iter()
1221 .map(|i| parse_style_background_size(i))
1222 .collect::<Result<Vec<_>, _>>()?
1223 .into())
1224 }
1225
1226 pub fn parse_style_background_size(
1231 input: &str,
1232 ) -> Result<StyleBackgroundSize, InvalidValueErr<'_>> {
1233 let input = input.trim();
1234 match input {
1235 "contain" => Ok(StyleBackgroundSize::Contain),
1236 "cover" => Ok(StyleBackgroundSize::Cover),
1237 other => {
1238 let mut iter = other.split_whitespace();
1239 let x_val = iter.next().ok_or(InvalidValueErr(input))?;
1240 let x_pos = parse_pixel_value(x_val).map_err(|_| InvalidValueErr(input))?;
1241 let y_pos = match iter.next() {
1242 Some(y_val) => parse_pixel_value(y_val).map_err(|_| InvalidValueErr(input))?,
1243 None => x_pos, };
1245 Ok(StyleBackgroundSize::ExactSize(PixelValueSize {
1246 width: x_pos,
1247 height: y_pos,
1248 }))
1249 }
1250 }
1251 }
1252
1253 pub fn parse_style_background_repeat_multiple(
1258 input: &str,
1259 ) -> Result<StyleBackgroundRepeatVec, InvalidValueErr<'_>> {
1260 Ok(split_string_respect_comma(input)
1261 .iter()
1262 .map(|i| parse_style_background_repeat(i))
1263 .collect::<Result<Vec<_>, _>>()?
1264 .into())
1265 }
1266
1267 pub fn parse_style_background_repeat(
1272 input: &str,
1273 ) -> Result<StyleBackgroundRepeat, InvalidValueErr<'_>> {
1274 match input.trim() {
1275 "no-repeat" => Ok(StyleBackgroundRepeat::NoRepeat),
1276 "repeat" => Ok(StyleBackgroundRepeat::PatternRepeat),
1277 "repeat-x" => Ok(StyleBackgroundRepeat::RepeatX),
1278 "repeat-y" => Ok(StyleBackgroundRepeat::RepeatY),
1279 _ => Err(InvalidValueErr(input)),
1280 }
1281 }
1282
1283 fn parse_gradient(
1287 input: &str,
1288 gradient_type: GradientType,
1289 ) -> Result<StyleBackgroundContent, CssBackgroundParseError<'_>> {
1290 let input = input.trim();
1291 let comma_separated_items = split_string_respect_comma(input);
1292 let mut brace_iterator = comma_separated_items.iter();
1293 let first_brace_item = brace_iterator
1294 .next()
1295 .ok_or(CssBackgroundParseError::NoDirection(input))?;
1296
1297 match gradient_type {
1298 GradientType::LinearGradient | GradientType::RepeatingLinearGradient => {
1299 let mut linear_gradient = LinearGradient {
1300 extend_mode: gradient_type.get_extend_mode(),
1301 ..Default::default()
1302 };
1303 let mut linear_stops = Vec::new();
1304
1305 if let Ok(dir) = parse_direction(first_brace_item) {
1306 linear_gradient.direction = dir;
1307 } else {
1308 linear_stops.push(parse_linear_color_stop(first_brace_item)?);
1309 }
1310
1311 for item in brace_iterator {
1312 linear_stops.push(parse_linear_color_stop(item)?);
1313 }
1314
1315 linear_gradient.stops = get_normalized_linear_stops(&linear_stops).into();
1316 Ok(StyleBackgroundContent::LinearGradient(linear_gradient))
1317 }
1318 GradientType::RadialGradient | GradientType::RepeatingRadialGradient => {
1319 let mut radial_gradient = RadialGradient {
1322 extend_mode: gradient_type.get_extend_mode(),
1323 ..Default::default()
1324 };
1325 let mut radial_stops = Vec::new();
1326 let mut current_item = *first_brace_item;
1327 let mut items_consumed = false;
1328
1329 loop {
1331 let mut consumed_in_iteration = false;
1332 let mut temp_iter = current_item.split_whitespace();
1333 for word in temp_iter {
1334 if let Ok(shape) = parse_shape(word) {
1335 radial_gradient.shape = shape;
1336 consumed_in_iteration = true;
1337 } else if let Ok(size) = parse_radial_gradient_size(word) {
1338 radial_gradient.size = size;
1339 consumed_in_iteration = true;
1340 } else if let Ok(pos) = parse_style_background_position(current_item) {
1341 radial_gradient.position = pos;
1342 consumed_in_iteration = true;
1343 break; }
1346 }
1347 if consumed_in_iteration {
1348 if let Some(next_item) = brace_iterator.next() {
1349 current_item = next_item;
1350 items_consumed = true;
1351 } else {
1352 break;
1353 }
1354 } else {
1355 break;
1356 }
1357 }
1358
1359 if items_consumed || parse_linear_color_stop(current_item).is_ok() {
1360 radial_stops.push(parse_linear_color_stop(current_item)?);
1361 }
1362
1363 for item in brace_iterator {
1364 radial_stops.push(parse_linear_color_stop(item)?);
1365 }
1366
1367 radial_gradient.stops = get_normalized_linear_stops(&radial_stops).into();
1368 Ok(StyleBackgroundContent::RadialGradient(radial_gradient))
1369 }
1370 GradientType::ConicGradient | GradientType::RepeatingConicGradient => {
1371 let mut conic_gradient = ConicGradient {
1372 extend_mode: gradient_type.get_extend_mode(),
1373 ..Default::default()
1374 };
1375 let mut conic_stops = Vec::new();
1376
1377 if let Some((angle, center)) = parse_conic_first_item(first_brace_item)? {
1378 conic_gradient.angle = angle;
1379 conic_gradient.center = center;
1380 } else {
1381 conic_stops.push(parse_radial_color_stop(first_brace_item)?);
1382 }
1383
1384 for item in brace_iterator {
1385 conic_stops.push(parse_radial_color_stop(item)?);
1386 }
1387
1388 conic_gradient.stops = get_normalized_radial_stops(&conic_stops).into();
1389 Ok(StyleBackgroundContent::ConicGradient(conic_gradient))
1390 }
1391 }
1392 }
1393
1394 fn parse_linear_color_stop(
1403 input: &str,
1404 ) -> Result<LinearColorStop, CssGradientStopParseError<'_>> {
1405 let input = input.trim();
1406 let (color_str, offset1_str, offset2_str) = split_color_and_offsets(input);
1407
1408 let color = parse_color_or_system(color_str)?;
1409 let offset1 = match offset1_str {
1410 None => OptionPercentageValue::None,
1411 Some(s) => OptionPercentageValue::Some(
1412 parse_percentage_value(s).map_err(CssGradientStopParseError::Percentage)?,
1413 ),
1414 };
1415 let offset2 = match offset2_str {
1416 None => OptionPercentageValue::None,
1417 Some(s) => OptionPercentageValue::Some(
1418 parse_percentage_value(s).map_err(CssGradientStopParseError::Percentage)?,
1419 ),
1420 };
1421
1422 Ok(LinearColorStop {
1423 color,
1424 offset1,
1425 offset2,
1426 })
1427 }
1428
1429 fn parse_radial_color_stop(
1436 input: &str,
1437 ) -> Result<RadialColorStop, CssGradientStopParseError<'_>> {
1438 let input = input.trim();
1439 let (color_str, offset1_str, offset2_str) = split_color_and_offsets(input);
1440
1441 let color = parse_color_or_system(color_str)?;
1442 let offset1 = match offset1_str {
1443 None => OptionAngleValue::None,
1444 Some(s) => OptionAngleValue::Some(
1445 parse_angle_value(s).map_err(CssGradientStopParseError::Angle)?,
1446 ),
1447 };
1448 let offset2 = match offset2_str {
1449 None => OptionAngleValue::None,
1450 Some(s) => OptionAngleValue::Some(
1451 parse_angle_value(s).map_err(CssGradientStopParseError::Angle)?,
1452 ),
1453 };
1454
1455 Ok(RadialColorStop {
1456 color,
1457 offset1,
1458 offset2,
1459 })
1460 }
1461
1462 fn split_color_and_offsets(input: &str) -> (&str, Option<&str>, Option<&str>) {
1470 let input = input.trim();
1475
1476 if let Some((remaining, last_offset)) = try_split_last_offset(input) {
1478 if let Some((color_part, first_offset)) = try_split_last_offset(remaining) {
1480 return (color_part.trim(), Some(first_offset), Some(last_offset));
1481 }
1482 return (remaining.trim(), Some(last_offset), None);
1483 }
1484
1485 (input, None, None)
1486 }
1487
1488 fn try_split_last_offset(input: &str) -> Option<(&str, &str)> {
1491 let input = input.trim();
1492 if let Some(last_ws_idx) = input.rfind(char::is_whitespace) {
1493 let (potential_color, potential_offset) = input.split_at(last_ws_idx);
1494 let potential_offset = potential_offset.trim();
1495
1496 if is_likely_offset(potential_offset) {
1499 return Some((potential_color, potential_offset));
1500 }
1501 }
1502 None
1503 }
1504
1505 fn is_likely_offset(s: &str) -> bool {
1508 if !s.contains(|c: char| c.is_ascii_digit()) {
1509 return false;
1510 }
1511 let units = [
1513 "%", "px", "em", "rem", "ex", "ch", "vw", "vh", "vmin", "vmax", "cm", "mm", "in", "pt",
1514 "pc", "deg", "rad", "grad", "turn",
1515 ];
1516 units.iter().any(|u| s.ends_with(u))
1517 }
1518
1519 fn parse_conic_first_item(
1521 input: &str,
1522 ) -> Result<Option<(AngleValue, StyleBackgroundPosition)>, CssConicGradientParseError<'_>> {
1523 let input = input.trim();
1524 if !input.starts_with("from") {
1525 return Ok(None);
1526 }
1527
1528 let mut parts = input["from".len()..].trim().split("at");
1529 let angle_part = parts
1530 .next()
1531 .ok_or(CssConicGradientParseError::NoAngle(input))?
1532 .trim();
1533 let angle = parse_angle_value(angle_part)?;
1534
1535 let position = match parts.next() {
1536 Some(pos_part) => parse_style_background_position(pos_part.trim())?,
1537 None => StyleBackgroundPosition::default(),
1538 };
1539
1540 Ok(Some((angle, position)))
1541 }
1542
1543 macro_rules! impl_get_normalized_stops {
1546 (
1547 fn $fn_name:ident($input_stop:ty) -> Vec<$output_stop:ident>,
1548 pos_type = $pos_ty:ty,
1549 default_start = $default_start:expr,
1550 default_end = $default_end:expr,
1551 pos_ctor = $pos_ctor:expr,
1552 pos_to_f32 = $pos_to_f32:expr,
1553 output_field = $out_field:ident,
1554 ) => {
1555 #[allow(clippy::suboptimal_flops)] fn $fn_name(stops: &[$input_stop]) -> Vec<$output_stop> {
1557 if stops.is_empty() {
1558 return Vec::new();
1559 }
1560
1561 let mut expanded: Vec<(ColorOrSystem, Option<$pos_ty>)> = Vec::new();
1562
1563 for stop in stops {
1564 match (stop.offset1.into_option(), stop.offset2.into_option()) {
1565 (None, _) => {
1566 expanded.push((stop.color, None));
1567 }
1568 (Some(pos1), None) => {
1569 expanded.push((stop.color, Some(pos1)));
1570 }
1571 (Some(pos1), Some(pos2)) => {
1572 expanded.push((stop.color, Some(pos1)));
1573 expanded.push((stop.color, Some(pos2)));
1574 }
1575 }
1576 }
1577
1578 if expanded.is_empty() {
1579 return Vec::new();
1580 }
1581
1582 let pos_ctor: fn(f32) -> $pos_ty = $pos_ctor;
1583 let pos_to_f32: fn(&$pos_ty) -> f32 = $pos_to_f32;
1584
1585 if expanded[0].1.is_none() {
1586 expanded[0].1 = Some(pos_ctor($default_start));
1587 }
1588 let last_idx = expanded.len() - 1;
1589 if expanded[last_idx].1.is_none() {
1590 expanded[last_idx].1 = Some(pos_ctor($default_end));
1591 }
1592
1593 let mut max_so_far: f32 = 0.0;
1594 for (_, pos) in expanded.iter_mut() {
1595 if let Some(p) = pos {
1596 let val = pos_to_f32(p);
1597 if val < max_so_far {
1598 *p = pos_ctor(max_so_far);
1599 } else {
1600 max_so_far = val;
1601 }
1602 }
1603 }
1604
1605 let mut i = 0;
1606 while i < expanded.len() {
1607 if expanded[i].1.is_none() {
1608 let run_start = i;
1609 let mut run_end = i;
1610 while run_end < expanded.len() && expanded[run_end].1.is_none() {
1611 run_end += 1;
1612 }
1613
1614 let prev_pos = if run_start > 0 {
1615 pos_to_f32(&expanded[run_start - 1].1.unwrap())
1616 } else {
1617 $default_start
1618 };
1619
1620 let next_pos = if run_end < expanded.len() {
1621 pos_to_f32(&expanded[run_end].1.unwrap())
1622 } else {
1623 $default_end
1624 };
1625
1626 let run_len = run_end - run_start;
1627 let step = (next_pos - prev_pos) / crate::cast::usize_to_f32(run_len + 1);
1628
1629 for j in 0..run_len {
1630 expanded[run_start + j].1 =
1631 Some(pos_ctor(prev_pos + step * crate::cast::usize_to_f32(j + 1)));
1632 }
1633
1634 i = run_end;
1635 } else {
1636 i += 1;
1637 }
1638 }
1639
1640 expanded
1641 .into_iter()
1642 .map(|(color, pos)| {
1643 $output_stop {
1644 $out_field: pos.unwrap_or(pos_ctor($default_start)),
1645 color,
1646 }
1647 })
1648 .collect()
1649 }
1650 };
1651 }
1652
1653 impl_get_normalized_stops! {
1654 fn get_normalized_linear_stops(LinearColorStop) -> Vec<NormalizedLinearColorStop>,
1655 pos_type = PercentageValue,
1656 default_start = 0.0,
1657 default_end = 100.0,
1658 pos_ctor = (|v| PercentageValue::new(v)),
1659 pos_to_f32 = (|p: &PercentageValue| p.normalized() * 100.0),
1660 output_field = offset,
1661 }
1662
1663 impl_get_normalized_stops! {
1664 fn get_normalized_radial_stops(RadialColorStop) -> Vec<NormalizedRadialColorStop>,
1665 pos_type = AngleValue,
1666 default_start = 0.0,
1667 default_end = 360.0,
1668 pos_ctor = (|v| AngleValue::deg(v)),
1669 pos_to_f32 = (|p: &AngleValue| p.to_degrees_raw()),
1670 output_field = angle,
1671 }
1672
1673 fn parse_background_position_horizontal(
1676 input: &str,
1677 ) -> Result<BackgroundPositionHorizontal, CssPixelValueParseError<'_>> {
1678 Ok(match input {
1679 "left" => BackgroundPositionHorizontal::Left,
1680 "center" => BackgroundPositionHorizontal::Center,
1681 "right" => BackgroundPositionHorizontal::Right,
1682 other => BackgroundPositionHorizontal::Exact(parse_pixel_value(other)?),
1683 })
1684 }
1685
1686 fn parse_background_position_vertical(
1687 input: &str,
1688 ) -> Result<BackgroundPositionVertical, CssPixelValueParseError<'_>> {
1689 Ok(match input {
1690 "top" => BackgroundPositionVertical::Top,
1691 "center" => BackgroundPositionVertical::Center,
1692 "bottom" => BackgroundPositionVertical::Bottom,
1693 other => BackgroundPositionVertical::Exact(parse_pixel_value(other)?),
1694 })
1695 }
1696
1697 fn parse_shape(input: &str) -> Result<Shape, CssShapeParseError<'_>> {
1698 match input.trim() {
1699 "circle" => Ok(Shape::Circle),
1700 "ellipse" => Ok(Shape::Ellipse),
1701 _ => Err(CssShapeParseError::ShapeErr(InvalidValueErr(input))),
1702 }
1703 }
1704
1705 fn parse_radial_gradient_size(
1706 input: &str,
1707 ) -> Result<RadialGradientSize, InvalidValueErr<'_>> {
1708 match input.trim() {
1709 "closest-side" => Ok(RadialGradientSize::ClosestSide),
1710 "closest-corner" => Ok(RadialGradientSize::ClosestCorner),
1711 "farthest-side" => Ok(RadialGradientSize::FarthestSide),
1712 "farthest-corner" => Ok(RadialGradientSize::FarthestCorner),
1713 _ => Err(InvalidValueErr(input)),
1714 }
1715 }
1716
1717 #[cfg(test)]
1722 #[allow(
1723 clippy::float_cmp,
1724 clippy::too_many_lines,
1725 clippy::unreadable_literal,
1726 clippy::cognitive_complexity,
1727 clippy::wildcard_imports
1728 )]
1729 mod autotest_generated {
1730 use super::*;
1733 use crate::props::style::background::*;
1734 use crate::{
1735 props::basic::{
1736 angle::CssAngleValueParseError,
1737 color::{CssColorParseError, OptionColorU, SystemColorRef},
1738 direction::CssDirectionParseError,
1739 error::InvalidValueErr,
1740 length::PercentageParseError,
1741 parse::{CssImageParseError, ParenthesisParseError},
1742 pixel::CssPixelValueParseError,
1743 },
1744 system::SystemColors,
1745 };
1746 use alloc::{string::ToString, vec::Vec};
1747
1748 const ADVERSARIAL: &[&str] = &[
1755 "",
1756 " ",
1757 " ",
1758 "\t\n\r",
1759 "\u{0}",
1760 "!!!",
1761 ";",
1762 ",",
1763 ",,",
1764 "(",
1765 ")",
1766 "()",
1767 "((((",
1768 "0",
1769 "-0",
1770 "+0",
1771 "NaN",
1772 "nan",
1773 "inf",
1774 "-inf",
1775 "1e40",
1776 "-1e40",
1777 "1e-45",
1778 "3.4028235e38",
1779 "9223372036854775807",
1780 "-9223372036854775808",
1781 "\u{1F600}",
1782 "e\u{0301}\u{0301}\u{0301}",
1783 "\u{00a0}",
1784 "red\u{00a0}50%",
1785 " valid ",
1786 "valid;garbage",
1787 "red;blue",
1788 "linear-gradient",
1789 "linear-gradient(",
1790 "linear-gradient()",
1791 "url(",
1792 "url()",
1793 "rgba(",
1794 "rgb(0,0,0",
1795 "to right",
1796 "circle",
1797 "from",
1798 ];
1799
1800 const ALL_SYSTEM_REFS: [SystemColorRef; 9] = [
1801 SystemColorRef::Text,
1802 SystemColorRef::Background,
1803 SystemColorRef::Accent,
1804 SystemColorRef::AccentText,
1805 SystemColorRef::ButtonFace,
1806 SystemColorRef::ButtonText,
1807 SystemColorRef::WindowBackground,
1808 SystemColorRef::SelectionBackground,
1809 SystemColorRef::SelectionText,
1810 ];
1811
1812 const ALL_GRADIENT_TYPES: [GradientType; 6] = [
1813 GradientType::LinearGradient,
1814 GradientType::RepeatingLinearGradient,
1815 GradientType::RadialGradient,
1816 GradientType::RepeatingRadialGradient,
1817 GradientType::ConicGradient,
1818 GradientType::RepeatingConicGradient,
1819 ];
1820
1821 fn blue() -> ColorU {
1822 ColorU::new_rgb(0, 0, 255)
1823 }
1824
1825 fn linear(input: &str) -> LinearGradient {
1826 match parse_style_background_content(input) {
1827 Ok(StyleBackgroundContent::LinearGradient(g)) => g,
1828 other => panic!("expected a linear gradient for {input:?}, got {other:?}"),
1829 }
1830 }
1831
1832 fn radial(input: &str) -> RadialGradient {
1833 match parse_style_background_content(input) {
1834 Ok(StyleBackgroundContent::RadialGradient(g)) => g,
1835 other => panic!("expected a radial gradient for {input:?}, got {other:?}"),
1836 }
1837 }
1838
1839 fn conic(input: &str) -> ConicGradient {
1840 match parse_style_background_content(input) {
1841 Ok(StyleBackgroundContent::ConicGradient(g)) => g,
1842 other => panic!("expected a conic gradient for {input:?}, got {other:?}"),
1843 }
1844 }
1845
1846 fn offsets(stops: &NormalizedLinearColorStopVec) -> Vec<f32> {
1848 stops
1849 .iter()
1850 .map(|s| s.offset.normalized() * 100.0)
1851 .collect()
1852 }
1853
1854 #[test]
1859 fn autotest_shape_display_is_exact_and_never_empty() {
1860 assert_eq!(Shape::Ellipse.to_string(), "ellipse");
1861 assert_eq!(Shape::Circle.to_string(), "circle");
1862 assert_eq!(Shape::default(), Shape::Ellipse);
1863 assert_eq!(Shape::default().to_string(), "ellipse");
1864 for s in [Shape::Ellipse, Shape::Circle] {
1865 assert!(!s.to_string().is_empty());
1866 assert_eq!(parse_shape(&s.to_string()).unwrap(), s);
1868 }
1869 }
1870
1871 #[test]
1872 fn autotest_radial_gradient_size_display_is_exact_and_never_empty() {
1873 assert_eq!(RadialGradientSize::ClosestSide.to_string(), "closest-side");
1874 assert_eq!(
1875 RadialGradientSize::ClosestCorner.to_string(),
1876 "closest-corner"
1877 );
1878 assert_eq!(
1879 RadialGradientSize::FarthestSide.to_string(),
1880 "farthest-side"
1881 );
1882 assert_eq!(
1883 RadialGradientSize::FarthestCorner.to_string(),
1884 "farthest-corner"
1885 );
1886 assert_eq!(
1887 RadialGradientSize::default(),
1888 RadialGradientSize::FarthestCorner
1889 );
1890 for s in [
1891 RadialGradientSize::ClosestSide,
1892 RadialGradientSize::ClosestCorner,
1893 RadialGradientSize::FarthestSide,
1894 RadialGradientSize::FarthestCorner,
1895 ] {
1896 assert!(!s.to_string().is_empty());
1897 assert_eq!(parse_radial_gradient_size(&s.to_string()).unwrap(), s);
1898 }
1899 }
1900
1901 #[test]
1906 fn autotest_normalized_linear_stop_new_keeps_its_arguments() {
1907 let stop = NormalizedLinearColorStop::new(PercentageValue::new(42.5), ColorU::RED);
1908 assert_eq!(stop.offset.normalized() * 100.0, 42.5);
1909 assert_eq!(stop.color, ColorOrSystem::Color(ColorU::RED));
1910
1911 for f in [
1914 0.0_f32,
1915 -0.0,
1916 f32::NAN,
1917 f32::INFINITY,
1918 f32::NEG_INFINITY,
1919 f32::MAX,
1920 f32::MIN,
1921 f32::MIN_POSITIVE,
1922 -100.0,
1923 1e30,
1924 ] {
1925 let stop = NormalizedLinearColorStop::new(
1926 PercentageValue::new(f),
1927 ColorU::TRANSPARENT,
1928 );
1929 assert!(
1930 stop.offset.normalized().is_finite(),
1931 "offset went non-finite for {f}"
1932 );
1933 assert_eq!(stop.color, ColorOrSystem::Color(ColorU::TRANSPARENT));
1934 }
1935 assert_eq!(
1937 NormalizedLinearColorStop::new(PercentageValue::new(f32::NAN), ColorU::RED).offset,
1938 PercentageValue::new(0.0)
1939 );
1940 }
1941
1942 #[test]
1943 fn autotest_normalized_radial_stop_new_keeps_its_arguments() {
1944 let stop = NormalizedRadialColorStop::new(AngleValue::deg(90.0), ColorU::RED);
1945 assert_eq!(stop.angle, AngleValue::deg(90.0));
1946 assert_eq!(stop.angle.to_degrees_raw(), 90.0);
1947 assert_eq!(stop.color, ColorOrSystem::Color(ColorU::RED));
1948
1949 for f in [
1950 0.0_f32,
1951 -0.0,
1952 f32::NAN,
1953 f32::INFINITY,
1954 f32::NEG_INFINITY,
1955 f32::MAX,
1956 f32::MIN,
1957 720.0,
1958 -360.0,
1959 ] {
1960 let stop = NormalizedRadialColorStop::new(AngleValue::deg(f), ColorU::WHITE);
1961 assert!(
1962 stop.angle.to_degrees_raw().is_finite(),
1963 "angle went non-finite for {f}"
1964 );
1965 assert_eq!(stop.color, ColorOrSystem::Color(ColorU::WHITE));
1966 }
1967 assert_eq!(
1968 NormalizedRadialColorStop::new(AngleValue::deg(f32::NAN), ColorU::RED).angle,
1969 AngleValue::deg(0.0)
1970 );
1971 }
1972
1973 #[test]
1978 fn autotest_resolve_concrete_color_ignores_system_colors() {
1979 let stop = NormalizedLinearColorStop::new(PercentageValue::new(0.0), ColorU::RED);
1980 assert_eq!(stop.resolve(&SystemColors::default(), ColorU::WHITE), ColorU::RED);
1981
1982 let populated = SystemColors {
1983 accent: OptionColorU::Some(ColorU::new_rgb(1, 2, 3)),
1984 ..SystemColors::default()
1985 };
1986 assert_eq!(stop.resolve(&populated, ColorU::WHITE), ColorU::RED);
1987
1988 let rstop = NormalizedRadialColorStop::new(AngleValue::deg(0.0), ColorU::RED);
1989 assert_eq!(rstop.resolve(&populated, ColorU::WHITE), ColorU::RED);
1990 }
1991
1992 #[test]
1993 fn autotest_resolve_system_stop_falls_back_for_every_variant() {
1994 let fallback = ColorU::rgba(9, 8, 7, 6);
1995 for r in ALL_SYSTEM_REFS {
1996 let lin = NormalizedLinearColorStop {
1997 offset: PercentageValue::new(50.0),
1998 color: ColorOrSystem::System(r),
1999 };
2000 let rad = NormalizedRadialColorStop {
2001 angle: AngleValue::deg(180.0),
2002 color: ColorOrSystem::System(r),
2003 };
2004 assert_eq!(lin.resolve(&SystemColors::default(), fallback), fallback);
2006 assert_eq!(rad.resolve(&SystemColors::default(), fallback), fallback);
2007 }
2008
2009 let accent = ColorU::new_rgb(0, 122, 255);
2011 let populated = SystemColors {
2012 accent: OptionColorU::Some(accent),
2013 ..SystemColors::default()
2014 };
2015 let stop = NormalizedLinearColorStop {
2016 offset: PercentageValue::new(0.0),
2017 color: ColorOrSystem::System(SystemColorRef::Accent),
2018 };
2019 assert_eq!(stop.resolve(&populated, fallback), accent);
2020 let other = NormalizedLinearColorStop {
2021 offset: PercentageValue::new(0.0),
2022 color: ColorOrSystem::System(SystemColorRef::ButtonText),
2023 };
2024 assert_eq!(other.resolve(&populated, fallback), fallback);
2025 }
2026
2027 #[test]
2032 fn autotest_background_position_horizontal_scale_for_dpi() {
2033 for f in [0.0_f32, 1.0, -1.0, f32::NAN, f32::INFINITY, f32::MIN, f32::MAX] {
2035 for keyword in [
2036 BackgroundPositionHorizontal::Left,
2037 BackgroundPositionHorizontal::Center,
2038 BackgroundPositionHorizontal::Right,
2039 ] {
2040 let mut k = keyword;
2041 k.scale_for_dpi(f);
2042 assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
2043 }
2044 }
2045
2046 let mut exact = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2047 exact.scale_for_dpi(2.0);
2048 assert_eq!(exact, BackgroundPositionHorizontal::Exact(PixelValue::px(20.0)));
2049
2050 let mut zeroed = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2052 zeroed.scale_for_dpi(0.0);
2053 assert_eq!(zeroed, BackgroundPositionHorizontal::Exact(PixelValue::px(0.0)));
2054
2055 let mut negated = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2056 negated.scale_for_dpi(-1.0);
2057 assert_eq!(
2058 negated,
2059 BackgroundPositionHorizontal::Exact(PixelValue::px(-10.0))
2060 );
2061
2062 let mut nan = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2064 nan.scale_for_dpi(f32::NAN);
2065 assert_eq!(nan, BackgroundPositionHorizontal::Exact(PixelValue::px(0.0)));
2066
2067 for f in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
2069 let mut v = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2070 v.scale_for_dpi(f);
2071 let BackgroundPositionHorizontal::Exact(px) = v else {
2072 panic!("variant changed under scaling");
2073 };
2074 assert!(px.number.get().is_finite(), "non-finite result for {f}");
2075 assert_eq!(px.number.get().is_sign_negative(), f.is_sign_negative());
2076 }
2077 }
2078
2079 #[test]
2080 fn autotest_background_position_vertical_scale_for_dpi() {
2081 for f in [0.0_f32, 1.0, -1.0, f32::NAN, f32::INFINITY, f32::MIN, f32::MAX] {
2082 for keyword in [
2083 BackgroundPositionVertical::Top,
2084 BackgroundPositionVertical::Center,
2085 BackgroundPositionVertical::Bottom,
2086 ] {
2087 let mut k = keyword;
2088 k.scale_for_dpi(f);
2089 assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
2090 }
2091 }
2092
2093 let mut exact = BackgroundPositionVertical::Exact(PixelValue::em(4.0));
2094 exact.scale_for_dpi(0.5);
2095 assert_eq!(exact, BackgroundPositionVertical::Exact(PixelValue::em(2.0)));
2096
2097 let mut nan = BackgroundPositionVertical::Exact(PixelValue::px(10.0));
2098 nan.scale_for_dpi(f32::NAN);
2099 assert_eq!(nan, BackgroundPositionVertical::Exact(PixelValue::px(0.0)));
2100
2101 let mut saturated = BackgroundPositionVertical::Exact(PixelValue::px(f32::MAX));
2104 saturated.scale_for_dpi(f32::MAX);
2105 let once = saturated;
2106 saturated.scale_for_dpi(f32::MAX);
2107 assert_eq!(saturated, once);
2108 let BackgroundPositionVertical::Exact(px) = saturated else {
2109 panic!("variant changed under scaling");
2110 };
2111 assert!(px.number.get() > 0.0);
2112 assert!(px.number.get().is_finite());
2113 }
2114
2115 #[test]
2116 fn autotest_style_background_position_scale_for_dpi_scales_both_axes() {
2117 let mut pos = StyleBackgroundPosition {
2118 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::px(10.0)),
2119 vertical: BackgroundPositionVertical::Exact(PixelValue::px(20.0)),
2120 };
2121 pos.scale_for_dpi(3.0);
2122 assert_eq!(
2123 pos.horizontal,
2124 BackgroundPositionHorizontal::Exact(PixelValue::px(30.0))
2125 );
2126 assert_eq!(
2127 pos.vertical,
2128 BackgroundPositionVertical::Exact(PixelValue::px(60.0))
2129 );
2130
2131 pos.scale_for_dpi(2.0);
2134 assert_eq!(
2135 pos.horizontal,
2136 BackgroundPositionHorizontal::Exact(PixelValue::px(60.0))
2137 );
2138
2139 for f in [0.0_f32, 1.0, -2.5, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2141 let mut default = StyleBackgroundPosition::default();
2142 default.scale_for_dpi(f);
2143 assert_eq!(default, StyleBackgroundPosition::default());
2144 }
2145 }
2146
2147 #[test]
2148 fn autotest_style_background_size_scale_for_dpi() {
2149 for f in [0.0_f32, 2.0, -1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2151 for keyword in [StyleBackgroundSize::Contain, StyleBackgroundSize::Cover] {
2152 let mut k = keyword;
2153 k.scale_for_dpi(f);
2154 assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
2155 }
2156 }
2157
2158 let mut size = StyleBackgroundSize::ExactSize(PixelValueSize {
2159 width: PixelValue::px(10.0),
2160 height: PixelValue::percent(50.0),
2161 });
2162 size.scale_for_dpi(2.0);
2163 assert_eq!(
2164 size,
2165 StyleBackgroundSize::ExactSize(PixelValueSize {
2166 width: PixelValue::px(20.0),
2167 height: PixelValue::percent(100.0),
2170 })
2171 );
2172
2173 let mut nan = StyleBackgroundSize::ExactSize(PixelValueSize {
2174 width: PixelValue::px(10.0),
2175 height: PixelValue::px(20.0),
2176 });
2177 nan.scale_for_dpi(f32::NAN);
2178 assert_eq!(
2179 nan,
2180 StyleBackgroundSize::ExactSize(PixelValueSize {
2181 width: PixelValue::px(0.0),
2182 height: PixelValue::px(0.0),
2183 })
2184 );
2185
2186 let mut inf = StyleBackgroundSize::ExactSize(PixelValueSize {
2187 width: PixelValue::px(1.0),
2188 height: PixelValue::px(-1.0),
2189 });
2190 inf.scale_for_dpi(f32::INFINITY);
2191 let StyleBackgroundSize::ExactSize(s) = inf else {
2192 panic!("variant changed under scaling");
2193 };
2194 assert!(s.width.number.get().is_finite() && s.width.number.get() > 0.0);
2195 assert!(s.height.number.get().is_finite() && s.height.number.get() < 0.0);
2196 }
2197
2198 #[test]
2203 fn autotest_css_background_parse_error_round_trips() {
2204 let errors = [
2205 CssBackgroundParseError::Error(""),
2206 CssBackgroundParseError::Error("boom \u{1F600}"),
2207 CssBackgroundParseError::InvalidBackground(ParenthesisParseError::EmptyInput),
2208 CssBackgroundParseError::InvalidBackground(
2209 ParenthesisParseError::StopWordNotFound("nope"),
2210 ),
2211 CssBackgroundParseError::UnclosedGradient(""),
2212 CssBackgroundParseError::NoDirection("nodir"),
2213 CssBackgroundParseError::TooFewGradientStops("few"),
2214 CssBackgroundParseError::DirectionParseError(CssDirectionParseError::Error("d")),
2215 CssBackgroundParseError::DirectionParseError(
2216 CssDirectionParseError::InvalidArguments("args"),
2217 ),
2218 CssBackgroundParseError::GradientParseError(CssGradientStopParseError::Error("g")),
2219 CssBackgroundParseError::ConicGradient(CssConicGradientParseError::NoAngle("a")),
2220 CssBackgroundParseError::ShapeParseError(CssShapeParseError::ShapeErr(
2221 InvalidValueErr("s"),
2222 )),
2223 CssBackgroundParseError::ImageParseError(CssImageParseError::UnclosedQuotes("q")),
2224 CssBackgroundParseError::ColorParseError(CssColorParseError::InvalidColor("c")),
2225 CssBackgroundParseError::ColorParseError(CssColorParseError::EmptyInput),
2226 ];
2227 for e in &errors {
2228 let owned = e.to_contained();
2229 assert_eq!(&owned.to_shared(), e, "round-trip changed {e:?}");
2230 assert_eq!(
2232 alloc::format!("{}", owned.to_shared()),
2233 alloc::format!("{e}")
2234 );
2235 }
2236 }
2237
2238 #[test]
2239 fn autotest_error_round_trip_survives_huge_and_unicode_payloads() {
2240 let huge = "x".repeat(100_000);
2241 let weird = "\u{1F600}\u{0}\u{00a0}e\u{0301}";
2242 for s in [huge.as_str(), weird, "", " "] {
2243 let e = CssBackgroundParseError::UnclosedGradient(s);
2244 assert_eq!(e.to_contained().to_shared(), e);
2245
2246 let e = CssGradientStopParseError::Error(s);
2247 assert_eq!(e.to_contained().to_shared(), e);
2248
2249 let e = CssConicGradientParseError::NoAngle(s);
2250 assert_eq!(e.to_contained().to_shared(), e);
2251
2252 let e = CssShapeParseError::ShapeErr(InvalidValueErr(s));
2253 assert_eq!(e.to_contained().to_shared(), e);
2254
2255 let e = CssBackgroundPositionParseError::NoPosition(s);
2256 assert_eq!(e.to_contained().to_shared(), e);
2257 }
2258 }
2259
2260 #[test]
2261 fn autotest_css_gradient_stop_parse_error_round_trips() {
2262 let errors = [
2263 CssGradientStopParseError::Error("boom"),
2264 CssGradientStopParseError::Percentage(PercentageParseError::NoPercentSign),
2265 CssGradientStopParseError::Percentage(PercentageParseError::InvalidUnit(
2266 "px".to_string().into(),
2267 )),
2268 CssGradientStopParseError::Angle(CssAngleValueParseError::EmptyString),
2269 CssGradientStopParseError::Angle(CssAngleValueParseError::InvalidAngle("q")),
2270 CssGradientStopParseError::ColorParseError(CssColorParseError::InvalidColor("c")),
2271 ];
2272 for e in &errors {
2273 assert_eq!(&e.to_contained().to_shared(), e, "round-trip changed {e:?}");
2274 }
2275 }
2276
2277 #[test]
2278 fn autotest_css_conic_and_shape_parse_error_round_trip() {
2279 let errors = [
2280 CssConicGradientParseError::NoAngle("n"),
2281 CssConicGradientParseError::Angle(CssAngleValueParseError::EmptyString),
2282 CssConicGradientParseError::Position(
2283 CssBackgroundPositionParseError::NoPosition("p"),
2284 ),
2285 ];
2286 for e in &errors {
2287 assert_eq!(&e.to_contained().to_shared(), e);
2288 }
2289
2290 let shape = CssShapeParseError::ShapeErr(InvalidValueErr("blob"));
2291 assert_eq!(shape.to_contained().to_shared(), shape);
2292 }
2293
2294 #[test]
2295 fn autotest_css_background_position_parse_error_round_trips() {
2296 let errors = [
2297 CssBackgroundPositionParseError::NoPosition(""),
2298 CssBackgroundPositionParseError::TooManyComponents("a b c"),
2299 CssBackgroundPositionParseError::FirstComponentWrong(
2300 CssPixelValueParseError::EmptyString,
2301 ),
2302 CssBackgroundPositionParseError::FirstComponentWrong(
2303 CssPixelValueParseError::InvalidPixelValue("q"),
2304 ),
2305 CssBackgroundPositionParseError::SecondComponentWrong(
2306 CssPixelValueParseError::InvalidPixelValue("\u{1F600}"),
2307 ),
2308 ];
2309 for e in &errors {
2310 assert_eq!(&e.to_contained().to_shared(), e, "round-trip changed {e:?}");
2311 }
2312 }
2313
2314 #[test]
2315 fn autotest_real_parse_errors_round_trip_through_the_owned_form() {
2316 for input in ADVERSARIAL {
2318 if let Err(e) = parse_style_background_content(input) {
2319 assert_eq!(e.to_contained().to_shared(), e, "for input {input:?}");
2320 }
2321 if let Err(e) = parse_style_background_position(input) {
2322 assert_eq!(e.to_contained().to_shared(), e, "for input {input:?}");
2323 }
2324 }
2325 }
2326
2327 #[test]
2332 fn autotest_get_extend_mode_is_repeat_exactly_for_the_repeating_variants() {
2333 assert_eq!(
2334 GradientType::LinearGradient.get_extend_mode(),
2335 ExtendMode::Clamp
2336 );
2337 assert_eq!(
2338 GradientType::RadialGradient.get_extend_mode(),
2339 ExtendMode::Clamp
2340 );
2341 assert_eq!(
2342 GradientType::ConicGradient.get_extend_mode(),
2343 ExtendMode::Clamp
2344 );
2345 assert_eq!(
2346 GradientType::RepeatingLinearGradient.get_extend_mode(),
2347 ExtendMode::Repeat
2348 );
2349 assert_eq!(
2350 GradientType::RepeatingRadialGradient.get_extend_mode(),
2351 ExtendMode::Repeat
2352 );
2353 assert_eq!(
2354 GradientType::RepeatingConicGradient.get_extend_mode(),
2355 ExtendMode::Repeat
2356 );
2357 for t in ALL_GRADIENT_TYPES {
2359 assert_eq!(t.get_extend_mode(), t.get_extend_mode());
2360 }
2361 assert_eq!(ExtendMode::default(), ExtendMode::Clamp);
2362 }
2363
2364 #[test]
2369 fn autotest_background_content_never_panics_and_is_deterministic() {
2370 for input in ADVERSARIAL {
2371 let a = parse_style_background_content(input);
2372 let b = parse_style_background_content(input);
2373 assert_eq!(a, b, "non-deterministic for {input:?}");
2374 }
2375 }
2376
2377 #[test]
2378 fn autotest_background_content_rejects_empty_whitespace_and_garbage() {
2379 for input in ["", " ", " ", "\t\n\r", "\u{0}", "!!!", ";", "valid;garbage"] {
2380 assert!(
2381 parse_style_background_content(input).is_err(),
2382 "{input:?} should not parse as a background"
2383 );
2384 }
2385 }
2386
2387 #[test]
2388 fn autotest_background_content_valid_minimal_positive_controls() {
2389 assert_eq!(
2390 parse_style_background_content("red").unwrap(),
2391 StyleBackgroundContent::Color(ColorU::RED)
2392 );
2393 assert_eq!(
2395 parse_style_background_content(" red ").unwrap(),
2396 StyleBackgroundContent::Color(ColorU::RED)
2397 );
2398 assert_eq!(
2399 parse_style_background_content("system:accent").unwrap(),
2400 StyleBackgroundContent::SystemColor(SystemColorRef::Accent)
2401 );
2402 assert_eq!(
2403 parse_style_background_content("url(a.png)").unwrap(),
2404 StyleBackgroundContent::Image("a.png".into())
2405 );
2406 }
2407
2408 #[test]
2409 fn autotest_background_content_unicode_is_rejected_without_panicking() {
2410 for input in [
2411 "\u{1F600}",
2412 "url(\u{1F600}.png)",
2413 "linear-gradient(\u{1F600}, red)",
2414 "e\u{0301}\u{0301}\u{0301}",
2415 "\u{00a0}",
2416 ] {
2417 let parsed = parse_style_background_content(input);
2418 if input.starts_with("url(") {
2420 assert!(parsed.is_ok());
2421 } else {
2422 assert!(parsed.is_err(), "{input:?} unexpectedly parsed");
2423 }
2424 }
2425 }
2426
2427 #[test]
2428 fn autotest_background_content_extremely_long_input_terminates() {
2429 let huge = "a".repeat(100_000);
2430 assert!(parse_style_background_content(&huge).is_err());
2431
2432 let huge_gradient =
2433 alloc::format!("linear-gradient({})", "red, ".repeat(2_000) + "blue");
2434 let g = linear(&huge_gradient);
2435 assert_eq!(g.stops.len(), 2_001);
2436
2437 let huge_url = alloc::format!("url({})", "a".repeat(100_000));
2438 assert!(matches!(
2439 parse_style_background_content(&huge_url),
2440 Ok(StyleBackgroundContent::Image(_))
2441 ));
2442 }
2443
2444 #[test]
2445 fn autotest_background_content_deep_nesting_does_not_stack_overflow() {
2446 let nested = alloc::format!(
2447 "linear-gradient({}red{})",
2448 "(".repeat(10_000),
2449 ")".repeat(10_000)
2450 );
2451 assert!(parse_style_background_content(&nested).is_err());
2452
2453 let unbalanced = alloc::format!("linear-gradient({}", "(".repeat(10_000));
2454 assert!(parse_style_background_content(&unbalanced).is_err());
2455 }
2456
2457 #[test]
2458 fn autotest_unclosed_gradient_reports_a_color_error_not_unclosed_gradient() {
2459 let err = parse_style_background_content("linear-gradient(red, blue").unwrap_err();
2462 assert!(
2463 matches!(err, CssBackgroundParseError::ColorParseError(_)),
2464 "got {err:?}"
2465 );
2466 }
2467
2468 #[test]
2469 fn autotest_empty_gradient_body_is_a_no_direction_error() {
2470 let err = parse_style_background_content("linear-gradient()").unwrap_err();
2471 assert!(matches!(err, CssBackgroundParseError::NoDirection(_)), "got {err:?}");
2472 for f in [
2473 "radial-gradient()",
2474 "conic-gradient()",
2475 "repeating-linear-gradient()",
2476 ] {
2477 assert!(parse_style_background_content(f).is_err(), "{f:?}");
2478 }
2479 }
2480
2481 #[test]
2482 fn autotest_url_with_empty_payload_is_accepted_as_an_empty_image() {
2483 assert_eq!(
2485 parse_style_background_content("url()").unwrap(),
2486 StyleBackgroundContent::Image("".into())
2487 );
2488 }
2489
2490 #[test]
2491 fn autotest_gradient_boundary_number_directions_stay_finite() {
2492 let g = linear("linear-gradient(NaN, red, blue)");
2495 assert_eq!(g.direction, Direction::Angle(AngleValue::deg(0.0)));
2496
2497 for input in [
2499 "linear-gradient(0deg, red, blue)",
2500 "linear-gradient(-0deg, red, blue)",
2501 "linear-gradient(1e40deg, red, blue)",
2502 "linear-gradient(-1e40deg, red, blue)",
2503 "linear-gradient(1e-45deg, red, blue)",
2504 "linear-gradient(inf, red, blue)",
2505 "linear-gradient(-inf, red, blue)",
2506 "linear-gradient(9223372036854775807deg, red, blue)",
2507 ] {
2508 let g = linear(input);
2509 let Direction::Angle(a) = g.direction else {
2510 panic!("expected an angle direction for {input:?}");
2511 };
2512 assert!(a.to_degrees_raw().is_finite(), "non-finite angle for {input:?}");
2513 assert_eq!(g.stops.len(), 2, "{input:?}");
2514 }
2515 }
2516
2517 #[test]
2518 fn autotest_gradient_stop_offsets_are_monotonic_and_finite() {
2519 for input in [
2520 "linear-gradient(red, blue)",
2521 "linear-gradient(red, green, blue)",
2522 "linear-gradient(red 50%, blue 20%)",
2523 "linear-gradient(red -50%, blue)",
2524 "linear-gradient(red 0%, yellow, green, blue 100%)",
2525 "linear-gradient(red 10% 30%, blue)",
2526 "linear-gradient(red 200%, blue 10%)",
2527 "repeating-linear-gradient(red, blue 20%)",
2528 "radial-gradient(circle, red, blue)",
2529 ] {
2530 let content = parse_style_background_content(input).unwrap();
2531 let stops = match &content {
2532 StyleBackgroundContent::LinearGradient(g) => &g.stops,
2533 StyleBackgroundContent::RadialGradient(g) => &g.stops,
2534 other => panic!("unexpected content {other:?}"),
2535 };
2536 let mut prev = f32::NEG_INFINITY;
2537 for o in offsets(stops) {
2538 assert!(o.is_finite(), "non-finite offset in {input:?}");
2539 assert!(o >= prev, "offsets not monotonic in {input:?}: {o} < {prev}");
2540 prev = o;
2541 }
2542 }
2543 }
2544
2545 #[test]
2546 fn autotest_negative_and_overflowing_stop_offsets_are_clamped() {
2547 let g = linear("linear-gradient(red -50%, blue)");
2549 assert_eq!(offsets(&g.stops), alloc::vec![0.0, 100.0]);
2550
2551 let g = linear("linear-gradient(red 200%, blue 10%)");
2554 assert_eq!(offsets(&g.stops), alloc::vec![200.0, 200.0]);
2555 }
2556
2557 #[test]
2558 fn autotest_offsets_that_are_not_percentages_are_rejected() {
2559 let err = parse_style_background_content("linear-gradient(red 50px, blue)").unwrap_err();
2562 assert!(
2563 matches!(
2564 err,
2565 CssBackgroundParseError::GradientParseError(
2566 CssGradientStopParseError::Percentage(_)
2567 )
2568 ),
2569 "got {err:?}"
2570 );
2571
2572 assert!(parse_style_background_content("linear-gradient(red 0.5, blue)").is_err());
2575 assert!(parse_style_background_content("linear-gradient(red NaN%, blue)").is_err());
2577 }
2578
2579 #[test]
2580 fn autotest_huge_stop_offsets_do_not_produce_nan_or_inf() {
2581 let g = linear("linear-gradient(red 1e40%, blue)");
2582 assert_eq!(g.stops.len(), 2);
2583 for o in offsets(&g.stops) {
2584 assert!(o.is_finite(), "offset leaked a non-finite value: {o}");
2585 }
2586 }
2587
2588 #[test]
2593 fn autotest_background_content_multiple_empty_input_yields_an_empty_vec() {
2594 let parsed = parse_style_background_content_multiple("").unwrap();
2597 assert_eq!(parsed.len(), 0);
2598
2599 assert!(parse_style_background_content_multiple(" ").is_err());
2601 assert!(parse_style_background_content_multiple(",").is_err());
2602 assert!(parse_style_background_content_multiple("red,,blue").is_err());
2603 }
2604
2605 #[test]
2606 fn autotest_background_content_multiple_valid_and_adversarial() {
2607 let parsed =
2608 parse_style_background_content_multiple("linear-gradient(red, blue), url(a.png)")
2609 .unwrap();
2610 assert_eq!(parsed.len(), 2);
2611 assert!(matches!(
2612 parsed.as_slice()[0],
2613 StyleBackgroundContent::LinearGradient(_)
2614 ));
2615 assert!(matches!(
2616 parsed.as_slice()[1],
2617 StyleBackgroundContent::Image(_)
2618 ));
2619
2620 assert!(parse_style_background_content_multiple("red, !!!").is_err());
2622
2623 let many = "red,".repeat(2_000) + "blue";
2625 assert_eq!(
2626 parse_style_background_content_multiple(&many).unwrap().len(),
2627 2_001
2628 );
2629
2630 for input in ADVERSARIAL {
2631 let a = parse_style_background_content_multiple(input);
2632 let b = parse_style_background_content_multiple(input);
2633 assert_eq!(a, b, "non-deterministic for {input:?}");
2634 }
2635 }
2636
2637 #[test]
2642 fn autotest_background_position_empty_and_whitespace() {
2643 assert_eq!(
2644 parse_style_background_position(""),
2645 Err(CssBackgroundPositionParseError::NoPosition(""))
2646 );
2647 assert_eq!(
2648 parse_style_background_position(" "),
2649 Err(CssBackgroundPositionParseError::NoPosition(""))
2650 );
2651 assert_eq!(
2652 parse_style_background_position("\t\n\r"),
2653 Err(CssBackgroundPositionParseError::NoPosition(""))
2654 );
2655 }
2656
2657 #[test]
2658 fn autotest_background_position_valid_minimal_and_keyword_order() {
2659 let p = parse_style_background_position("left").unwrap();
2660 assert_eq!(p.horizontal, BackgroundPositionHorizontal::Left);
2661 assert_eq!(p.vertical, BackgroundPositionVertical::Center);
2662
2663 let p = parse_style_background_position("top").unwrap();
2665 assert_eq!(p.horizontal, BackgroundPositionHorizontal::Center);
2666 assert_eq!(p.vertical, BackgroundPositionVertical::Top);
2667
2668 assert_eq!(
2670 parse_style_background_position("left top").unwrap(),
2671 parse_style_background_position("top left").unwrap()
2672 );
2673
2674 assert!(matches!(
2676 parse_style_background_position("left right"),
2677 Err(CssBackgroundPositionParseError::SecondComponentWrong(_))
2678 ));
2679 }
2680
2681 #[test]
2682 fn autotest_background_position_too_many_components() {
2683 assert!(matches!(
2684 parse_style_background_position("left 10px top 20px"),
2685 Err(CssBackgroundPositionParseError::TooManyComponents(_))
2686 ));
2687 assert!(matches!(
2688 parse_style_background_position("a b c"),
2689 Err(CssBackgroundPositionParseError::TooManyComponents(_))
2690 ));
2691 }
2692
2693 #[test]
2694 fn autotest_background_position_boundary_numbers_are_accepted_and_saturate() {
2695 assert_eq!(
2698 parse_style_background_position("NaN").unwrap().horizontal,
2699 BackgroundPositionHorizontal::Exact(PixelValue::px(0.0))
2700 );
2701 assert_eq!(
2702 parse_style_background_position("-0").unwrap().horizontal,
2703 BackgroundPositionHorizontal::Exact(PixelValue::px(0.0))
2704 );
2705 assert_eq!(
2706 parse_style_background_position("inf").unwrap().horizontal,
2707 BackgroundPositionHorizontal::Exact(PixelValue::px(f32::INFINITY))
2708 );
2709 for input in ["0", "1e40px", "-1e40px", "1e-45px", "3.4028235e38px"] {
2710 let p = parse_style_background_position(input).unwrap();
2711 let BackgroundPositionHorizontal::Exact(px) = p.horizontal else {
2712 panic!("expected an exact value for {input:?}");
2713 };
2714 assert!(px.number.get().is_finite(), "non-finite for {input:?}");
2715 }
2716 }
2717
2718 #[test]
2719 fn autotest_background_position_garbage_unicode_and_long_input() {
2720 for input in ["garbage", "!!!", "\u{1F600}", "e\u{0301}", "left;top"] {
2721 assert!(
2722 parse_style_background_position(input).is_err(),
2723 "{input:?} unexpectedly parsed"
2724 );
2725 }
2726 let huge = "a".repeat(100_000);
2727 assert!(parse_style_background_position(&huge).is_err());
2728 let nested = "(".repeat(10_000);
2729 assert!(parse_style_background_position(&nested).is_err());
2730
2731 for input in ADVERSARIAL {
2732 let a = parse_style_background_position(input);
2733 let b = parse_style_background_position(input);
2734 assert_eq!(a, b, "non-deterministic for {input:?}");
2735 }
2736 }
2737
2738 #[test]
2739 fn autotest_background_position_multiple() {
2740 assert_eq!(parse_style_background_position_multiple("").unwrap().len(), 0);
2742
2743 let parsed = parse_style_background_position_multiple("left top, 10px 20px").unwrap();
2744 assert_eq!(parsed.len(), 2);
2745 assert_eq!(
2746 parsed.as_slice()[1].horizontal,
2747 BackgroundPositionHorizontal::Exact(PixelValue::px(10.0))
2748 );
2749
2750 assert!(parse_style_background_position_multiple("left top, !!!").is_err());
2751 assert!(parse_style_background_position_multiple(" ").is_err());
2752
2753 let many = "left top,".repeat(2_000) + "center";
2754 assert_eq!(
2755 parse_style_background_position_multiple(&many).unwrap().len(),
2756 2_001
2757 );
2758 }
2759
2760 #[test]
2765 fn autotest_background_size_empty_whitespace_and_garbage() {
2766 for input in ["", " ", "\t\n", "auto", "!!!", "\u{1F600}", "CONTAIN", "Cover"] {
2767 assert!(
2768 parse_style_background_size(input).is_err(),
2769 "{input:?} unexpectedly parsed"
2770 );
2771 }
2772 let huge = "a".repeat(100_000);
2773 assert!(parse_style_background_size(&huge).is_err());
2774 }
2775
2776 #[test]
2777 fn autotest_background_size_valid_minimal_and_trimming() {
2778 assert_eq!(
2779 parse_style_background_size(" contain ").unwrap(),
2780 StyleBackgroundSize::Contain
2781 );
2782 assert_eq!(
2783 parse_style_background_size("cover").unwrap(),
2784 StyleBackgroundSize::Cover
2785 );
2786 assert_eq!(
2788 parse_style_background_size("50%").unwrap(),
2789 StyleBackgroundSize::ExactSize(PixelValueSize {
2790 width: PixelValue::percent(50.0),
2791 height: PixelValue::percent(50.0),
2792 })
2793 );
2794 }
2795
2796 #[test]
2797 fn autotest_background_size_silently_ignores_extra_components() {
2798 assert_eq!(
2801 parse_style_background_size("10px 20px 30px").unwrap(),
2802 StyleBackgroundSize::ExactSize(PixelValueSize {
2803 width: PixelValue::px(10.0),
2804 height: PixelValue::px(20.0),
2805 })
2806 );
2807 }
2808
2809 #[test]
2810 fn autotest_background_size_boundary_numbers_saturate_without_panicking() {
2811 assert_eq!(
2813 parse_style_background_size("NaN").unwrap(),
2814 StyleBackgroundSize::ExactSize(PixelValueSize {
2815 width: PixelValue::px(0.0),
2816 height: PixelValue::px(0.0),
2817 })
2818 );
2819 assert_eq!(
2820 parse_style_background_size("inf").unwrap(),
2821 StyleBackgroundSize::ExactSize(PixelValueSize {
2822 width: PixelValue::px(f32::INFINITY),
2823 height: PixelValue::px(f32::INFINITY),
2824 })
2825 );
2826 for input in ["0", "-0", "1e40px", "-1e40px", "1e-45px"] {
2827 let StyleBackgroundSize::ExactSize(s) =
2828 parse_style_background_size(input).unwrap()
2829 else {
2830 panic!("expected an exact size for {input:?}");
2831 };
2832 assert!(s.width.number.get().is_finite(), "non-finite for {input:?}");
2833 assert!(s.height.number.get().is_finite(), "non-finite for {input:?}");
2834 }
2835
2836 for input in ADVERSARIAL {
2837 let a = parse_style_background_size(input);
2838 let b = parse_style_background_size(input);
2839 assert_eq!(a, b, "non-deterministic for {input:?}");
2840 }
2841 }
2842
2843 #[test]
2844 fn autotest_background_size_multiple() {
2845 assert_eq!(parse_style_background_size_multiple("").unwrap().len(), 0);
2846
2847 let parsed = parse_style_background_size_multiple("contain, 10px 20px, cover").unwrap();
2848 assert_eq!(parsed.len(), 3);
2849 assert_eq!(parsed.as_slice()[0], StyleBackgroundSize::Contain);
2850 assert_eq!(parsed.as_slice()[2], StyleBackgroundSize::Cover);
2851
2852 assert!(parse_style_background_size_multiple("cover, auto").is_err());
2853 assert!(parse_style_background_size_multiple(" ").is_err());
2854
2855 let many = "cover,".repeat(2_000) + "contain";
2856 assert_eq!(
2857 parse_style_background_size_multiple(&many).unwrap().len(),
2858 2_001
2859 );
2860 }
2861
2862 #[test]
2867 fn autotest_background_repeat_valid_and_invalid() {
2868 assert_eq!(
2869 parse_style_background_repeat(" repeat ").unwrap(),
2870 StyleBackgroundRepeat::PatternRepeat
2871 );
2872 assert_eq!(
2873 parse_style_background_repeat("no-repeat").unwrap(),
2874 StyleBackgroundRepeat::NoRepeat
2875 );
2876 assert_eq!(
2877 parse_style_background_repeat("repeat-x").unwrap(),
2878 StyleBackgroundRepeat::RepeatX
2879 );
2880 assert_eq!(
2881 parse_style_background_repeat("repeat-y").unwrap(),
2882 StyleBackgroundRepeat::RepeatY
2883 );
2884 assert_eq!(StyleBackgroundRepeat::default(), StyleBackgroundRepeat::PatternRepeat);
2885
2886 for input in [
2887 "",
2888 " ",
2889 "\t\n",
2890 "REPEAT",
2891 "Repeat",
2892 "repeat-xy",
2893 "repeat repeat",
2894 "!!!",
2895 "\u{1F600}",
2896 "0",
2897 "NaN",
2898 ] {
2899 assert!(
2900 parse_style_background_repeat(input).is_err(),
2901 "{input:?} unexpectedly parsed"
2902 );
2903 }
2904
2905 let huge = "repeat".repeat(20_000);
2906 assert!(parse_style_background_repeat(&huge).is_err());
2907
2908 for input in ADVERSARIAL {
2909 let a = parse_style_background_repeat(input);
2910 let b = parse_style_background_repeat(input);
2911 assert_eq!(a, b, "non-deterministic for {input:?}");
2912 }
2913 }
2914
2915 #[test]
2916 fn autotest_background_repeat_multiple() {
2917 assert_eq!(parse_style_background_repeat_multiple("").unwrap().len(), 0);
2918
2919 let parsed = parse_style_background_repeat_multiple("repeat, no-repeat").unwrap();
2920 assert_eq!(parsed.len(), 2);
2921 assert_eq!(parsed.as_slice()[0], StyleBackgroundRepeat::PatternRepeat);
2922 assert_eq!(parsed.as_slice()[1], StyleBackgroundRepeat::NoRepeat);
2923
2924 assert!(parse_style_background_repeat_multiple("repeat,,repeat").is_err());
2925 assert!(parse_style_background_repeat_multiple(" ").is_err());
2926
2927 let many = "repeat,".repeat(2_000) + "no-repeat";
2928 assert_eq!(
2929 parse_style_background_repeat_multiple(&many).unwrap().len(),
2930 2_001
2931 );
2932 }
2933
2934 #[test]
2939 fn autotest_parse_gradient_empty_input_is_no_direction_for_every_type() {
2940 for t in ALL_GRADIENT_TYPES {
2941 assert!(
2942 matches!(
2943 parse_gradient("", t),
2944 Err(CssBackgroundParseError::NoDirection(_))
2945 ),
2946 "empty body accepted for {t:?}"
2947 );
2948 }
2949 }
2950
2951 #[test]
2952 fn autotest_parse_gradient_extend_mode_follows_the_gradient_type() {
2953 let StyleBackgroundContent::LinearGradient(g) =
2954 parse_gradient("red, blue", GradientType::LinearGradient).unwrap()
2955 else {
2956 panic!("expected a linear gradient");
2957 };
2958 assert_eq!(g.extend_mode, ExtendMode::Clamp);
2959
2960 let StyleBackgroundContent::LinearGradient(g) =
2961 parse_gradient("red, blue", GradientType::RepeatingLinearGradient).unwrap()
2962 else {
2963 panic!("expected a linear gradient");
2964 };
2965 assert_eq!(g.extend_mode, ExtendMode::Repeat);
2966
2967 let StyleBackgroundContent::RadialGradient(g) =
2968 parse_gradient("red, blue", GradientType::RepeatingRadialGradient).unwrap()
2969 else {
2970 panic!("expected a radial gradient");
2971 };
2972 assert_eq!(g.extend_mode, ExtendMode::Repeat);
2973
2974 let StyleBackgroundContent::ConicGradient(g) =
2975 parse_gradient("red, blue", GradientType::RepeatingConicGradient).unwrap()
2976 else {
2977 panic!("expected a conic gradient");
2978 };
2979 assert_eq!(g.extend_mode, ExtendMode::Repeat);
2980 }
2981
2982 #[test]
2983 fn autotest_parse_gradient_accepts_gradients_with_too_few_stops() {
2984 let StyleBackgroundContent::LinearGradient(g) =
2987 parse_gradient("red", GradientType::LinearGradient).unwrap()
2988 else {
2989 panic!("expected a linear gradient");
2990 };
2991 assert_eq!(g.stops.len(), 1);
2992 assert_eq!(offsets(&g.stops), alloc::vec![0.0]);
2993
2994 let StyleBackgroundContent::LinearGradient(g) =
2996 parse_gradient("to right", GradientType::LinearGradient).unwrap()
2997 else {
2998 panic!("expected a linear gradient");
2999 };
3000 assert_eq!(g.stops.len(), 0);
3001
3002 let StyleBackgroundContent::RadialGradient(g) =
3004 parse_gradient("circle", GradientType::RadialGradient).unwrap()
3005 else {
3006 panic!("expected a radial gradient");
3007 };
3008 assert_eq!(g.shape, Shape::Circle);
3009 assert_eq!(g.stops.len(), 0);
3010 }
3011
3012 #[test]
3013 fn autotest_parse_gradient_never_panics_on_adversarial_input() {
3014 let huge = "a".repeat(100_000);
3015 let nested = "(".repeat(10_000) + &")".repeat(10_000);
3016 let many_commas = ",".repeat(10_000);
3017 for t in ALL_GRADIENT_TYPES {
3018 for input in ADVERSARIAL {
3019 let a = parse_gradient(input, t);
3020 let b = parse_gradient(input, t);
3021 assert_eq!(a, b, "non-deterministic for {input:?} / {t:?}");
3022 }
3023 for input in [huge.as_str(), nested.as_str(), many_commas.as_str()] {
3024 let a = parse_gradient(input, t);
3025 let b = parse_gradient(input, t);
3026 assert_eq!(a, b, "non-deterministic for a long input / {t:?}");
3027 }
3028 assert!(parse_gradient("", t).is_err(), "empty body accepted for {t:?}");
3030 assert!(
3032 parse_gradient(&many_commas, t).is_err(),
3033 "comma soup accepted for {t:?}"
3034 );
3035 }
3036 for t in [
3039 GradientType::LinearGradient,
3040 GradientType::RepeatingLinearGradient,
3041 GradientType::ConicGradient,
3042 GradientType::RepeatingConicGradient,
3043 ] {
3044 assert!(parse_gradient(&huge, t).is_err(), "junk accepted for {t:?}");
3045 assert!(parse_gradient(&nested, t).is_err(), "junk accepted for {t:?}");
3046 assert!(parse_gradient("!!!", t).is_err(), "junk accepted for {t:?}");
3047 }
3048 }
3049
3050 #[test]
3051 fn autotest_radial_gradient_silently_drops_unparseable_items() {
3052 let StyleBackgroundContent::RadialGradient(g) =
3057 parse_gradient("!!!", GradientType::RadialGradient).unwrap()
3058 else {
3059 panic!("expected a radial gradient");
3060 };
3061 assert_eq!(g.stops.len(), 0);
3062
3063 let g = radial("radial-gradient(!!!, red)");
3064 assert_eq!(g.stops.len(), 1, "the junk item should have been dropped");
3065 assert_eq!(
3066 g.stops.as_ref()[0].color,
3067 ColorOrSystem::Color(ColorU::RED)
3068 );
3069
3070 assert!(parse_style_background_content("linear-gradient(!!!, red)").is_err());
3072 }
3073
3074 #[test]
3075 fn autotest_radial_gradient_position_is_ignored_when_combined_with_a_shape() {
3076 let g = radial("radial-gradient(circle at 50% 50%, red, blue)");
3081 assert_eq!(g.shape, Shape::Circle);
3082 assert_eq!(g.position, StyleBackgroundPosition::default());
3083 assert_eq!(g.stops.len(), 2);
3084
3085 let g = radial("radial-gradient(50% 50%, red, blue)");
3087 assert_eq!(
3088 g.position,
3089 StyleBackgroundPosition {
3090 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::percent(50.0)),
3091 vertical: BackgroundPositionVertical::Exact(PixelValue::percent(50.0)),
3092 }
3093 );
3094 assert_eq!(g.stops.len(), 2);
3095 }
3096
3097 #[test]
3098 fn autotest_radial_gradient_shape_and_size_keywords() {
3099 let g = radial("radial-gradient(circle closest-side, red, blue)");
3100 assert_eq!(g.shape, Shape::Circle);
3101 assert_eq!(g.size, RadialGradientSize::ClosestSide);
3102
3103 let g = radial("radial-gradient(ellipse farthest-side, red, blue)");
3104 assert_eq!(g.shape, Shape::Ellipse);
3105 assert_eq!(g.size, RadialGradientSize::FarthestSide);
3106
3107 let g = radial("radial-gradient(red, blue)");
3109 assert_eq!(g.shape, Shape::default());
3110 assert_eq!(g.size, RadialGradientSize::default());
3111 }
3112
3113 #[test]
3118 fn autotest_parse_linear_color_stop_valid_minimal() {
3119 let s = parse_linear_color_stop("red").unwrap();
3120 assert_eq!(s.color, ColorOrSystem::Color(ColorU::RED));
3121 assert_eq!(s.offset1, OptionPercentageValue::None);
3122 assert_eq!(s.offset2, OptionPercentageValue::None);
3123
3124 let s = parse_linear_color_stop(" red 50% ").unwrap();
3125 assert_eq!(
3126 s.offset1,
3127 OptionPercentageValue::Some(PercentageValue::new(50.0))
3128 );
3129 assert_eq!(s.offset2, OptionPercentageValue::None);
3130
3131 let s = parse_linear_color_stop("red 10% 30%").unwrap();
3132 assert_eq!(
3133 s.offset1,
3134 OptionPercentageValue::Some(PercentageValue::new(10.0))
3135 );
3136 assert_eq!(
3137 s.offset2,
3138 OptionPercentageValue::Some(PercentageValue::new(30.0))
3139 );
3140
3141 let s = parse_linear_color_stop("rgba(0, 0, 0, 0.5) 50%").unwrap();
3143 assert_eq!(
3144 s.offset1,
3145 OptionPercentageValue::Some(PercentageValue::new(50.0))
3146 );
3147
3148 let s = parse_linear_color_stop("system:accent 50%").unwrap();
3150 assert_eq!(s.color, ColorOrSystem::System(SystemColorRef::Accent));
3151 }
3152
3153 #[test]
3154 fn autotest_parse_linear_color_stop_rejects_junk() {
3155 for input in [
3156 "",
3157 " ",
3158 "\t\n",
3159 "!!!",
3160 "\u{1F600}",
3161 "red 50px", "red 0.5", "red 10% 20% 30%", "red blue",
3165 ] {
3166 assert!(
3167 parse_linear_color_stop(input).is_err(),
3168 "{input:?} unexpectedly parsed"
3169 );
3170 }
3171 let huge = "a".repeat(100_000);
3172 assert!(parse_linear_color_stop(&huge).is_err());
3173 }
3174
3175 #[test]
3176 fn autotest_parse_radial_color_stop_valid_and_junk() {
3177 let s = parse_radial_color_stop("red").unwrap();
3178 assert_eq!(s.color, ColorOrSystem::Color(ColorU::RED));
3179 assert_eq!(s.offset1, OptionAngleValue::None);
3180
3181 let s = parse_radial_color_stop("red 90deg").unwrap();
3182 assert_eq!(s.offset1, OptionAngleValue::Some(AngleValue::deg(90.0)));
3183 assert_eq!(s.offset2, OptionAngleValue::None);
3184
3185 let s = parse_radial_color_stop("red 45deg 90deg").unwrap();
3186 assert_eq!(s.offset1, OptionAngleValue::Some(AngleValue::deg(45.0)));
3187 assert_eq!(s.offset2, OptionAngleValue::Some(AngleValue::deg(90.0)));
3188
3189 assert!(parse_radial_color_stop("red 50%").is_ok());
3191
3192 for input in ["", " ", "!!!", "\u{1F600}", "red 5", "red 90deg 45deg 10deg"] {
3193 assert!(
3194 parse_radial_color_stop(input).is_err(),
3195 "{input:?} unexpectedly parsed"
3196 );
3197 }
3198 let huge = "a".repeat(100_000);
3199 assert!(parse_radial_color_stop(&huge).is_err());
3200 }
3201
3202 #[test]
3207 fn autotest_split_color_and_offsets_w3c_shapes() {
3208 assert_eq!(split_color_and_offsets("red"), ("red", None, None));
3209 assert_eq!(
3210 split_color_and_offsets("red 50%"),
3211 ("red", Some("50%"), None)
3212 );
3213 assert_eq!(
3214 split_color_and_offsets("red 10% 30%"),
3215 ("red", Some("10%"), Some("30%"))
3216 );
3217 assert_eq!(
3218 split_color_and_offsets("rgba(0, 0, 0, 0.5) 10% 30%"),
3219 ("rgba(0, 0, 0, 0.5)", Some("10%"), Some("30%"))
3220 );
3221 assert_eq!(
3223 split_color_and_offsets("to right bottom"),
3224 ("to right bottom", None, None)
3225 );
3226 }
3227
3228 #[test]
3229 fn autotest_split_color_and_offsets_never_panics_on_edges() {
3230 assert_eq!(split_color_and_offsets(""), ("", None, None));
3231 assert_eq!(split_color_and_offsets(" "), ("", None, None));
3232 assert_eq!(
3234 split_color_and_offsets("\u{1F600} 50%"),
3235 ("\u{1F600}", Some("50%"), None)
3236 );
3237 assert_eq!(
3239 split_color_and_offsets("red\u{00a0}50%"),
3240 ("red", Some("50%"), None)
3241 );
3242 let huge = "a".repeat(100_000);
3243 assert_eq!(split_color_and_offsets(&huge), (huge.as_str(), None, None));
3244
3245 for input in ADVERSARIAL {
3246 assert_eq!(
3247 split_color_and_offsets(input),
3248 split_color_and_offsets(input)
3249 );
3250 }
3251 }
3252
3253 #[test]
3254 fn autotest_try_split_last_offset() {
3255 assert_eq!(try_split_last_offset("red 50%"), Some(("red", "50%")));
3256 assert_eq!(try_split_last_offset("red 10px"), Some(("red", "10px")));
3257 assert_eq!(try_split_last_offset("50%"), None);
3259 assert_eq!(try_split_last_offset("red blue"), None);
3261 assert_eq!(try_split_last_offset("red 5"), None);
3262 assert_eq!(try_split_last_offset("to right"), None);
3263 assert_eq!(try_split_last_offset(""), None);
3265 assert_eq!(try_split_last_offset(" "), None);
3266 assert_eq!(try_split_last_offset("\t\n"), None);
3267
3268 for input in ADVERSARIAL {
3269 assert_eq!(try_split_last_offset(input), try_split_last_offset(input));
3270 }
3271 }
3272
3273 #[test]
3278 fn autotest_is_likely_offset_basic_true_false() {
3279 for s in [
3280 "50%", "10px", "0.5turn", "90deg", "1rem", "2vmin", "3vmax", "4grad", "5rad",
3281 "-50%", "1e40%",
3282 ] {
3283 assert!(is_likely_offset(s), "{s:?} should look like an offset");
3284 }
3285 for s in [
3286 "", " ", "red", "px", "%", "5", "0.5", "NaN%", "to", "right", "\u{1F600}",
3287 "contain",
3288 ] {
3289 assert!(!is_likely_offset(s), "{s:?} should not look like an offset");
3290 }
3291 }
3292
3293 #[test]
3294 fn autotest_is_likely_offset_is_a_shape_check_not_a_validator() {
3295 assert!(is_likely_offset("abc1px"));
3298 assert!(is_likely_offset("\u{1F600}5%"));
3299 assert!(is_likely_offset("--1--px"));
3300 assert!(is_likely_offset("1%%%"));
3301 assert!(!is_likely_offset("\u{0661}%"));
3304
3305 let huge = "9".repeat(100_000) + "%";
3306 assert!(is_likely_offset(&huge));
3307 for input in ADVERSARIAL {
3308 assert_eq!(is_likely_offset(input), is_likely_offset(input));
3309 }
3310 }
3311
3312 #[test]
3317 fn autotest_parse_conic_first_item_valid_and_absent() {
3318 assert_eq!(parse_conic_first_item("").unwrap(), None);
3320 assert_eq!(parse_conic_first_item("red").unwrap(), None);
3321 assert_eq!(parse_conic_first_item(" ").unwrap(), None);
3322
3323 let (angle, pos) = parse_conic_first_item("from 90deg").unwrap().unwrap();
3324 assert_eq!(angle, AngleValue::deg(90.0));
3325 assert_eq!(pos, StyleBackgroundPosition::default());
3326
3327 let (angle, pos) = parse_conic_first_item("from 0deg at center").unwrap().unwrap();
3328 assert_eq!(angle, AngleValue::deg(0.0));
3329 assert_eq!(pos.horizontal, BackgroundPositionHorizontal::Center);
3330 assert_eq!(pos.vertical, BackgroundPositionVertical::Center);
3331 }
3332
3333 #[test]
3334 fn autotest_parse_conic_first_item_rejects_malformed_preludes() {
3335 assert!(parse_conic_first_item("from").is_err());
3337 assert!(parse_conic_first_item("from at center").is_err());
3338 assert!(parse_conic_first_item("fromage").is_err());
3341 assert!(matches!(
3343 parse_conic_first_item("from 90deg at left top center"),
3344 Err(CssConicGradientParseError::Position(_))
3345 ));
3346
3347 let huge = alloc::format!("from {}", "9".repeat(100_000));
3348 let _ = parse_conic_first_item(&huge);
3349 for input in ADVERSARIAL {
3350 let a = parse_conic_first_item(input);
3351 let b = parse_conic_first_item(input);
3352 assert_eq!(a, b, "non-deterministic for {input:?}");
3353 }
3354 }
3355
3356 #[test]
3357 fn autotest_conic_gradient_end_to_end() {
3358 let g = conic("conic-gradient(from 45deg, red, blue)");
3359 assert_eq!(g.angle, AngleValue::deg(45.0));
3360 assert_eq!(g.extend_mode, ExtendMode::Clamp);
3361 assert_eq!(g.stops.len(), 2);
3362 assert_eq!(g.stops.as_ref()[0].angle.to_degrees_raw(), 0.0);
3363 assert_eq!(g.stops.as_ref()[1].angle.to_degrees_raw(), 360.0);
3364
3365 for input in [
3367 "conic-gradient(red, blue)",
3368 "conic-gradient(red 0deg, blue 180deg, green 360deg)",
3369 "conic-gradient(red 180deg, blue 90deg)",
3370 "conic-gradient(red -90deg, blue)",
3371 "repeating-conic-gradient(red, blue 30deg)",
3372 ] {
3373 let g = conic(input);
3374 let mut prev = f32::NEG_INFINITY;
3375 for s in &g.stops {
3376 let deg = s.angle.to_degrees_raw();
3377 assert!(deg.is_finite(), "non-finite angle in {input:?}");
3378 assert!(deg >= prev, "angles not monotonic in {input:?}");
3379 prev = deg;
3380 }
3381 }
3382
3383 let g = conic("conic-gradient(red 1e40deg, blue)");
3385 assert_eq!(g.stops.len(), 2);
3386 for s in &g.stops {
3387 assert!(s.angle.to_degrees_raw().is_finite());
3388 }
3389
3390 assert!(parse_style_background_content("conic-gradient(from, red)").is_err());
3391 }
3392
3393 #[test]
3398 fn autotest_parse_background_position_horizontal() {
3399 assert_eq!(
3400 parse_background_position_horizontal("left").unwrap(),
3401 BackgroundPositionHorizontal::Left
3402 );
3403 assert_eq!(
3404 parse_background_position_horizontal("center").unwrap(),
3405 BackgroundPositionHorizontal::Center
3406 );
3407 assert_eq!(
3408 parse_background_position_horizontal("right").unwrap(),
3409 BackgroundPositionHorizontal::Right
3410 );
3411 assert_eq!(
3412 parse_background_position_horizontal("10px").unwrap(),
3413 BackgroundPositionHorizontal::Exact(PixelValue::px(10.0))
3414 );
3415 assert!(parse_background_position_horizontal("top").is_err());
3417 assert!(parse_background_position_horizontal(" left").is_err());
3419 assert!(parse_background_position_horizontal("").is_err());
3420 assert!(parse_background_position_horizontal("\u{1F600}").is_err());
3421
3422 let huge = "a".repeat(100_000);
3423 assert!(parse_background_position_horizontal(&huge).is_err());
3424 }
3425
3426 #[test]
3427 fn autotest_parse_background_position_vertical() {
3428 assert_eq!(
3429 parse_background_position_vertical("top").unwrap(),
3430 BackgroundPositionVertical::Top
3431 );
3432 assert_eq!(
3433 parse_background_position_vertical("center").unwrap(),
3434 BackgroundPositionVertical::Center
3435 );
3436 assert_eq!(
3437 parse_background_position_vertical("bottom").unwrap(),
3438 BackgroundPositionVertical::Bottom
3439 );
3440 assert_eq!(
3441 parse_background_position_vertical("-10px").unwrap(),
3442 BackgroundPositionVertical::Exact(PixelValue::px(-10.0))
3443 );
3444 assert!(parse_background_position_vertical("left").is_err());
3445 assert!(parse_background_position_vertical("").is_err());
3446 assert!(parse_background_position_vertical("\u{1F600}").is_err());
3447
3448 let huge = "a".repeat(100_000);
3449 assert!(parse_background_position_vertical(&huge).is_err());
3450 }
3451
3452 #[test]
3457 fn autotest_parse_shape() {
3458 assert_eq!(parse_shape("circle").unwrap(), Shape::Circle);
3459 assert_eq!(parse_shape(" ellipse ").unwrap(), Shape::Ellipse);
3460 for input in ["", " ", "Circle", "CIRCLE", "circles", "!!!", "\u{1F600}", "0"] {
3461 assert!(parse_shape(input).is_err(), "{input:?} unexpectedly parsed");
3462 }
3463 let huge = "a".repeat(100_000);
3464 assert!(parse_shape(&huge).is_err());
3465 for input in ADVERSARIAL {
3466 assert_eq!(parse_shape(input), parse_shape(input));
3467 }
3468 }
3469
3470 #[test]
3471 fn autotest_parse_radial_gradient_size() {
3472 assert_eq!(
3473 parse_radial_gradient_size("closest-side").unwrap(),
3474 RadialGradientSize::ClosestSide
3475 );
3476 assert_eq!(
3477 parse_radial_gradient_size(" closest-corner ").unwrap(),
3478 RadialGradientSize::ClosestCorner
3479 );
3480 assert_eq!(
3481 parse_radial_gradient_size("farthest-side").unwrap(),
3482 RadialGradientSize::FarthestSide
3483 );
3484 assert_eq!(
3485 parse_radial_gradient_size("farthest-corner").unwrap(),
3486 RadialGradientSize::FarthestCorner
3487 );
3488 for input in [
3489 "",
3490 " ",
3491 "closest",
3492 "CLOSEST-SIDE",
3493 "farthest-corners",
3494 "!!!",
3495 "\u{1F600}",
3496 ] {
3497 assert!(
3498 parse_radial_gradient_size(input).is_err(),
3499 "{input:?} unexpectedly parsed"
3500 );
3501 }
3502 let huge = "a".repeat(100_000);
3503 assert!(parse_radial_gradient_size(&huge).is_err());
3504 }
3505
3506 #[test]
3511 fn autotest_round_trip_background_repeat() {
3512 for r in [
3513 StyleBackgroundRepeat::NoRepeat,
3514 StyleBackgroundRepeat::PatternRepeat,
3515 StyleBackgroundRepeat::RepeatX,
3516 StyleBackgroundRepeat::RepeatY,
3517 ] {
3518 let printed = r.print_as_css_value();
3519 assert!(!printed.is_empty());
3520 assert_eq!(parse_style_background_repeat(&printed).unwrap(), r);
3521 }
3522 }
3523
3524 #[test]
3525 fn autotest_round_trip_background_size() {
3526 for s in [
3527 StyleBackgroundSize::Contain,
3528 StyleBackgroundSize::Cover,
3529 StyleBackgroundSize::ExactSize(PixelValueSize {
3530 width: PixelValue::px(100.0),
3531 height: PixelValue::em(20.0),
3532 }),
3533 StyleBackgroundSize::ExactSize(PixelValueSize {
3534 width: PixelValue::percent(50.0),
3535 height: PixelValue::percent(50.0),
3536 }),
3537 StyleBackgroundSize::ExactSize(PixelValueSize {
3538 width: PixelValue::px(0.0),
3539 height: PixelValue::px(-25.5),
3540 }),
3541 ] {
3542 let printed = s.print_as_css_value();
3543 assert_eq!(
3544 parse_style_background_size(&printed).unwrap(),
3545 s,
3546 "round-trip failed for {printed:?}"
3547 );
3548 }
3549 }
3550
3551 #[test]
3552 fn autotest_round_trip_background_position() {
3553 let horizontals = [
3554 BackgroundPositionHorizontal::Left,
3555 BackgroundPositionHorizontal::Center,
3556 BackgroundPositionHorizontal::Right,
3557 BackgroundPositionHorizontal::Exact(PixelValue::px(50.0)),
3558 BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0)),
3559 ];
3560 let verticals = [
3561 BackgroundPositionVertical::Top,
3562 BackgroundPositionVertical::Center,
3563 BackgroundPositionVertical::Bottom,
3564 BackgroundPositionVertical::Exact(PixelValue::px(-10.0)),
3565 BackgroundPositionVertical::Exact(PixelValue::em(2.5)),
3566 ];
3567 for horizontal in horizontals {
3568 for vertical in verticals {
3569 let pos = StyleBackgroundPosition {
3570 horizontal,
3571 vertical,
3572 };
3573 let printed = pos.print_as_css_value();
3574 assert_eq!(
3575 parse_style_background_position(&printed).unwrap(),
3576 pos,
3577 "round-trip failed for {printed:?}"
3578 );
3579 }
3580 }
3581 }
3582
3583 #[test]
3584 fn autotest_round_trip_background_content_colors_and_images() {
3585 for content in [
3586 StyleBackgroundContent::Color(ColorU::RED),
3587 StyleBackgroundContent::Color(ColorU::TRANSPARENT),
3588 StyleBackgroundContent::Color(ColorU::rgba(1, 2, 3, 4)),
3589 StyleBackgroundContent::Color(ColorU::WHITE),
3590 StyleBackgroundContent::Image("a.png".into()),
3591 StyleBackgroundContent::Image("some/deep/path.jpeg".into()),
3592 StyleBackgroundContent::SystemColor(SystemColorRef::Accent),
3593 StyleBackgroundContent::SystemColor(SystemColorRef::SelectionText),
3594 ] {
3595 let printed = content.print_as_css_value();
3596 assert_eq!(
3597 parse_style_background_content(&printed).unwrap(),
3598 content,
3599 "round-trip failed for {printed:?}"
3600 );
3601 }
3602 assert_eq!(
3603 StyleBackgroundContent::default(),
3604 StyleBackgroundContent::Color(ColorU::TRANSPARENT)
3605 );
3606 }
3607
3608 #[test]
3609 fn autotest_round_trip_gradients() {
3610 for input in [
3611 "linear-gradient(to right, red 0%, blue 100%)",
3612 "repeating-linear-gradient(to bottom, red 25%, blue 75%)",
3613 "linear-gradient(45deg, red 0%, blue 50%)",
3614 "radial-gradient(circle farthest-corner at left top, red 0%, blue 100%)",
3615 "conic-gradient(from 90deg at left top, red 0deg, blue 360deg)",
3616 "repeating-conic-gradient(from 0deg at left top, red 0deg, blue 180deg)",
3617 ] {
3618 let parsed = parse_style_background_content(input).unwrap();
3619 let printed = parsed.print_as_css_value();
3620 let reparsed = parse_style_background_content(&printed).unwrap();
3621 assert_eq!(
3622 parsed, reparsed,
3623 "gradient did not survive print -> parse ({printed:?})"
3624 );
3625 assert_eq!(printed, reparsed.print_as_css_value());
3627 }
3628 }
3629
3630 #[test]
3631 fn autotest_round_trip_vec_printing_is_comma_separated() {
3632 let contents = parse_style_background_content_multiple("red, blue").unwrap();
3633 assert_eq!(contents.print_as_css_value(), "#ff0000ff, #0000ffff");
3634 assert_eq!(contents.as_slice()[1], StyleBackgroundContent::Color(blue()));
3635 let reparsed =
3636 parse_style_background_content_multiple(&contents.print_as_css_value()).unwrap();
3637 assert_eq!(reparsed, contents);
3638
3639 let sizes = parse_style_background_size_multiple("contain, 10px 20px").unwrap();
3640 assert_eq!(
3641 parse_style_background_size_multiple(&sizes.print_as_css_value()).unwrap(),
3642 sizes
3643 );
3644
3645 let repeats = parse_style_background_repeat_multiple("repeat, no-repeat").unwrap();
3646 assert_eq!(
3647 parse_style_background_repeat_multiple(&repeats.print_as_css_value()).unwrap(),
3648 repeats
3649 );
3650
3651 let positions = parse_style_background_position_multiple("left top, 10px 20px").unwrap();
3652 assert_eq!(
3653 parse_style_background_position_multiple(&positions.print_as_css_value()).unwrap(),
3654 positions
3655 );
3656 }
3657
3658 #[test]
3659 fn autotest_normalized_stop_printing_is_reparseable() {
3660 let stop = NormalizedLinearColorStop::new(PercentageValue::new(25.0), ColorU::RED);
3661 assert_eq!(stop.print_as_css_value(), "#ff0000ff 25%");
3662 let reparsed = parse_linear_color_stop(&stop.print_as_css_value()).unwrap();
3663 assert_eq!(reparsed.color, stop.color);
3664 assert_eq!(
3665 reparsed.offset1,
3666 OptionPercentageValue::Some(PercentageValue::new(25.0))
3667 );
3668
3669 let rstop = NormalizedRadialColorStop::new(AngleValue::deg(90.0), blue());
3670 assert_eq!(rstop.print_as_css_value(), "#0000ffff 90deg");
3671 let reparsed = parse_radial_color_stop(&rstop.print_as_css_value()).unwrap();
3672 assert_eq!(reparsed.color, rstop.color);
3673 assert_eq!(
3674 reparsed.offset1,
3675 OptionAngleValue::Some(AngleValue::deg(90.0))
3676 );
3677
3678 let sys = NormalizedLinearColorStop {
3680 offset: PercentageValue::new(50.0),
3681 color: ColorOrSystem::System(SystemColorRef::Accent),
3682 };
3683 assert_eq!(sys.print_as_css_value(), "system:accent 50%");
3684 assert_eq!(
3685 parse_linear_color_stop(&sys.print_as_css_value()).unwrap().color,
3686 ColorOrSystem::System(SystemColorRef::Accent)
3687 );
3688 }
3689
3690 #[test]
3691 fn autotest_empty_gradient_printing_does_not_panic() {
3692 let lg = StyleBackgroundContent::LinearGradient(LinearGradient::default());
3695 assert!(lg.print_as_css_value().starts_with("linear-gradient("));
3696
3697 let rg = StyleBackgroundContent::RadialGradient(RadialGradient::default());
3698 assert!(rg.print_as_css_value().starts_with("radial-gradient("));
3699
3700 let cg = StyleBackgroundContent::ConicGradient(ConicGradient::default());
3701 assert!(cg.print_as_css_value().starts_with("conic-gradient("));
3702
3703 let empty_stops = StyleBackgroundContent::LinearGradient(LinearGradient {
3705 extend_mode: ExtendMode::Repeat,
3706 stops: Vec::<NormalizedLinearColorStop>::new().into(),
3707 ..LinearGradient::default()
3708 });
3709 assert!(empty_stops
3710 .print_as_css_value()
3711 .starts_with("repeating-linear-gradient("));
3712 }
3713 }
3714}
3715
3716#[cfg(feature = "parser")]
3717pub use self::parser::*;
3718
3719#[cfg(all(test, feature = "parser"))]
3720mod tests {
3721 use super::*;
3722 use crate::props::basic::{DirectionCorner, DirectionCorners};
3723
3724 #[test]
3725 fn test_parse_single_background_content() {
3726 assert_eq!(
3728 parse_style_background_content("red").unwrap(),
3729 StyleBackgroundContent::Color(ColorU::RED)
3730 );
3731 assert_eq!(
3732 parse_style_background_content("#ff00ff").unwrap(),
3733 StyleBackgroundContent::Color(ColorU::new_rgb(255, 0, 255))
3734 );
3735
3736 assert_eq!(
3738 parse_style_background_content("url(\"image.png\")").unwrap(),
3739 StyleBackgroundContent::Image("image.png".into())
3740 );
3741
3742 let lg = parse_style_background_content("linear-gradient(to right, red, blue)").unwrap();
3744 assert!(matches!(lg, StyleBackgroundContent::LinearGradient(_)));
3745 if let StyleBackgroundContent::LinearGradient(grad) = lg {
3746 assert_eq!(grad.stops.len(), 2);
3747 assert_eq!(
3748 grad.direction,
3749 Direction::FromTo(DirectionCorners {
3750 dir_from: DirectionCorner::Left,
3751 dir_to: DirectionCorner::Right
3752 })
3753 );
3754 }
3755
3756 let rg = parse_style_background_content("radial-gradient(circle, white, black)").unwrap();
3758 assert!(matches!(rg, StyleBackgroundContent::RadialGradient(_)));
3759 if let StyleBackgroundContent::RadialGradient(grad) = rg {
3760 assert_eq!(grad.stops.len(), 2);
3761 assert_eq!(grad.shape, Shape::Circle);
3762 }
3763
3764 let cg = parse_style_background_content("conic-gradient(from 90deg, red, blue)").unwrap();
3766 assert!(matches!(cg, StyleBackgroundContent::ConicGradient(_)));
3767 if let StyleBackgroundContent::ConicGradient(grad) = cg {
3768 assert_eq!(grad.stops.len(), 2);
3769 assert_eq!(grad.angle, AngleValue::deg(90.0));
3770 }
3771 }
3772
3773 #[test]
3774 fn test_parse_multiple_background_content() {
3775 let result =
3776 parse_style_background_content_multiple("url(foo.png), linear-gradient(red, blue)")
3777 .unwrap();
3778 assert_eq!(result.len(), 2);
3779 assert!(matches!(
3780 result.as_slice()[0],
3781 StyleBackgroundContent::Image(_)
3782 ));
3783 assert!(matches!(
3784 result.as_slice()[1],
3785 StyleBackgroundContent::LinearGradient(_)
3786 ));
3787 }
3788
3789 #[test]
3790 fn test_parse_background_position() {
3791 let result = parse_style_background_position("center").unwrap();
3793 assert_eq!(result.horizontal, BackgroundPositionHorizontal::Center);
3794 assert_eq!(result.vertical, BackgroundPositionVertical::Center);
3795
3796 let result = parse_style_background_position("25%").unwrap();
3797 assert_eq!(
3798 result.horizontal,
3799 BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0))
3800 );
3801 assert_eq!(result.vertical, BackgroundPositionVertical::Center);
3802
3803 let result = parse_style_background_position("right 50px").unwrap();
3805 assert_eq!(result.horizontal, BackgroundPositionHorizontal::Right);
3806 assert_eq!(
3807 result.vertical,
3808 BackgroundPositionVertical::Exact(PixelValue::px(50.0))
3809 );
3810
3811 assert!(parse_style_background_position("left 10px top 20px").is_err());
3813 }
3814
3815 #[test]
3816 fn test_parse_background_size() {
3817 assert_eq!(
3818 parse_style_background_size("contain").unwrap(),
3819 StyleBackgroundSize::Contain
3820 );
3821 assert_eq!(
3822 parse_style_background_size("cover").unwrap(),
3823 StyleBackgroundSize::Cover
3824 );
3825 assert_eq!(
3826 parse_style_background_size("50%").unwrap(),
3827 StyleBackgroundSize::ExactSize(PixelValueSize {
3828 width: PixelValue::percent(50.0),
3829 height: PixelValue::percent(50.0)
3830 })
3831 );
3832 assert_eq!(
3833 parse_style_background_size("100px 20em").unwrap(),
3834 StyleBackgroundSize::ExactSize(PixelValueSize {
3835 width: PixelValue::px(100.0),
3836 height: PixelValue::em(20.0)
3837 })
3838 );
3839 assert!(parse_style_background_size("auto").is_err());
3840 }
3841
3842 #[test]
3843 fn test_parse_background_repeat() {
3844 assert_eq!(
3845 parse_style_background_repeat("repeat").unwrap(),
3846 StyleBackgroundRepeat::PatternRepeat
3847 );
3848 assert_eq!(
3849 parse_style_background_repeat("repeat-x").unwrap(),
3850 StyleBackgroundRepeat::RepeatX
3851 );
3852 assert_eq!(
3853 parse_style_background_repeat("repeat-y").unwrap(),
3854 StyleBackgroundRepeat::RepeatY
3855 );
3856 assert_eq!(
3857 parse_style_background_repeat("no-repeat").unwrap(),
3858 StyleBackgroundRepeat::NoRepeat
3859 );
3860 assert!(parse_style_background_repeat("repeat-xy").is_err());
3861 }
3862
3863 #[test]
3868 fn test_gradient_no_position_stops() {
3869 let lg = parse_style_background_content("linear-gradient(red, blue)").unwrap();
3871 if let StyleBackgroundContent::LinearGradient(grad) = lg {
3872 assert_eq!(grad.stops.len(), 2);
3873 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
3875 assert!((grad.stops.as_ref()[1].offset.normalized() - 1.0).abs() < 0.001);
3877 } else {
3878 panic!("Expected LinearGradient");
3879 }
3880 }
3881
3882 #[test]
3883 fn test_gradient_single_position_stops() {
3884 let lg = parse_style_background_content("linear-gradient(red 25%, blue 75%)").unwrap();
3886 if let StyleBackgroundContent::LinearGradient(grad) = lg {
3887 assert_eq!(grad.stops.len(), 2);
3888 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.25).abs() < 0.001);
3889 assert!((grad.stops.as_ref()[1].offset.normalized() - 0.75).abs() < 0.001);
3890 } else {
3891 panic!("Expected LinearGradient");
3892 }
3893 }
3894
3895 #[test]
3896 fn test_gradient_multi_position_stops() {
3897 let lg = parse_style_background_content("linear-gradient(red 10% 30%, blue)").unwrap();
3899 if let StyleBackgroundContent::LinearGradient(grad) = lg {
3900 assert_eq!(grad.stops.len(), 3, "Expected 3 stops for multi-position");
3902 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.10).abs() < 0.001);
3903 assert!((grad.stops.as_ref()[1].offset.normalized() - 0.30).abs() < 0.001);
3904 assert!((grad.stops.as_ref()[2].offset.normalized() - 1.0).abs() < 0.001);
3905 assert_eq!(grad.stops.as_ref()[0].color, grad.stops.as_ref()[1].color);
3907 } else {
3908 panic!("Expected LinearGradient");
3909 }
3910 }
3911
3912 #[test]
3913 fn test_gradient_three_colors_no_positions() {
3914 let lg = parse_style_background_content("linear-gradient(red, green, blue)").unwrap();
3916 if let StyleBackgroundContent::LinearGradient(grad) = lg {
3917 assert_eq!(grad.stops.len(), 3);
3918 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
3920 assert!((grad.stops.as_ref()[1].offset.normalized() - 0.5).abs() < 0.001);
3921 assert!((grad.stops.as_ref()[2].offset.normalized() - 1.0).abs() < 0.001);
3922 } else {
3923 panic!("Expected LinearGradient");
3924 }
3925 }
3926
3927 #[test]
3928 fn test_gradient_fixup_ascending_order() {
3929 let lg = parse_style_background_content("linear-gradient(red 50%, blue 20%)").unwrap();
3932 if let StyleBackgroundContent::LinearGradient(grad) = lg {
3933 assert_eq!(grad.stops.len(), 2);
3934 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.50).abs() < 0.001);
3936 assert!((grad.stops.as_ref()[1].offset.normalized() - 0.50).abs() < 0.001);
3938 } else {
3939 panic!("Expected LinearGradient");
3940 }
3941 }
3942
3943 #[test]
3944 fn test_gradient_distribute_unpositioned() {
3945 let lg =
3948 parse_style_background_content("linear-gradient(red 0%, yellow, green, blue 100%)")
3949 .unwrap();
3950 if let StyleBackgroundContent::LinearGradient(grad) = lg {
3951 assert_eq!(grad.stops.len(), 4);
3952 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
3954 assert!((grad.stops.as_ref()[1].offset.normalized() - 0.333).abs() < 0.01);
3955 assert!((grad.stops.as_ref()[2].offset.normalized() - 0.666).abs() < 0.01);
3956 assert!((grad.stops.as_ref()[3].offset.normalized() - 1.0).abs() < 0.001);
3957 } else {
3958 panic!("Expected LinearGradient");
3959 }
3960 }
3961
3962 #[test]
3963 fn test_gradient_direction_to_corner() {
3964 let lg =
3966 parse_style_background_content("linear-gradient(to top right, red, blue)").unwrap();
3967 if let StyleBackgroundContent::LinearGradient(grad) = lg {
3968 assert_eq!(
3969 grad.direction,
3970 Direction::FromTo(DirectionCorners {
3971 dir_from: DirectionCorner::BottomLeft,
3972 dir_to: DirectionCorner::TopRight
3973 })
3974 );
3975 } else {
3976 panic!("Expected LinearGradient");
3977 }
3978 }
3979
3980 #[test]
3981 fn test_gradient_direction_angle() {
3982 let lg = parse_style_background_content("linear-gradient(45deg, red, blue)").unwrap();
3984 if let StyleBackgroundContent::LinearGradient(grad) = lg {
3985 assert_eq!(grad.direction, Direction::Angle(AngleValue::deg(45.0)));
3986 } else {
3987 panic!("Expected LinearGradient");
3988 }
3989 }
3990
3991 #[test]
3992 fn test_repeating_gradient() {
3993 let lg =
3995 parse_style_background_content("repeating-linear-gradient(red, blue 20%)").unwrap();
3996 if let StyleBackgroundContent::LinearGradient(grad) = lg {
3997 assert_eq!(grad.extend_mode, ExtendMode::Repeat);
3998 } else {
3999 panic!("Expected LinearGradient");
4000 }
4001 }
4002
4003 #[test]
4004 fn test_radial_gradient_circle() {
4005 let rg = parse_style_background_content("radial-gradient(circle, red, blue)").unwrap();
4007 if let StyleBackgroundContent::RadialGradient(grad) = rg {
4008 assert_eq!(grad.shape, Shape::Circle);
4009 assert_eq!(grad.stops.len(), 2);
4010 assert_eq!(grad.position.horizontal, BackgroundPositionHorizontal::Left);
4012 assert_eq!(grad.position.vertical, BackgroundPositionVertical::Top);
4013 } else {
4014 panic!("Expected RadialGradient");
4015 }
4016 }
4017
4018 #[test]
4019 fn test_radial_gradient_ellipse() {
4020 let rg = parse_style_background_content("radial-gradient(ellipse, red, blue)").unwrap();
4022 if let StyleBackgroundContent::RadialGradient(grad) = rg {
4023 assert_eq!(grad.shape, Shape::Ellipse);
4024 assert_eq!(grad.stops.len(), 2);
4025 } else {
4026 panic!("Expected RadialGradient");
4027 }
4028 }
4029
4030 #[test]
4031 fn test_radial_gradient_size_keywords() {
4032 let rg = parse_style_background_content("radial-gradient(circle closest-side, red, blue)")
4034 .unwrap();
4035 if let StyleBackgroundContent::RadialGradient(grad) = rg {
4036 assert_eq!(grad.shape, Shape::Circle);
4037 assert_eq!(grad.size, RadialGradientSize::ClosestSide);
4038 } else {
4039 panic!("Expected RadialGradient");
4040 }
4041 }
4042
4043 #[test]
4044 fn test_radial_gradient_stop_positions() {
4045 let rg = parse_style_background_content("radial-gradient(red 0%, blue 100%)").unwrap();
4047 if let StyleBackgroundContent::RadialGradient(grad) = rg {
4048 assert_eq!(grad.stops.len(), 2);
4049 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
4050 assert!((grad.stops.as_ref()[1].offset.normalized() - 1.0).abs() < 0.001);
4051 } else {
4052 panic!("Expected RadialGradient");
4053 }
4054 }
4055
4056 #[test]
4057 fn test_repeating_radial_gradient() {
4058 let rg = parse_style_background_content("repeating-radial-gradient(circle, red, blue 20%)")
4059 .unwrap();
4060 if let StyleBackgroundContent::RadialGradient(grad) = rg {
4061 assert_eq!(grad.extend_mode, ExtendMode::Repeat);
4062 assert_eq!(grad.shape, Shape::Circle);
4063 } else {
4064 panic!("Expected RadialGradient");
4065 }
4066 }
4067
4068 #[test]
4069 fn test_conic_gradient_angle() {
4070 let cg = parse_style_background_content("conic-gradient(from 45deg, red, blue)").unwrap();
4072 if let StyleBackgroundContent::ConicGradient(grad) = cg {
4073 assert_eq!(grad.angle, AngleValue::deg(45.0));
4074 assert_eq!(grad.stops.len(), 2);
4075 } else {
4076 panic!("Expected ConicGradient");
4077 }
4078 }
4079
4080 #[test]
4081 fn test_conic_gradient_default() {
4082 let cg = parse_style_background_content("conic-gradient(red, blue)").unwrap();
4084 if let StyleBackgroundContent::ConicGradient(grad) = cg {
4085 assert_eq!(grad.stops.len(), 2);
4086 assert!(
4088 (grad.stops.as_ref()[0].angle.to_degrees_raw() - 0.0).abs() < 0.001,
4089 "First stop should be 0deg, got {}",
4090 grad.stops.as_ref()[0].angle.to_degrees_raw()
4091 );
4092 assert!(
4094 (grad.stops.as_ref()[1].angle.to_degrees_raw() - 360.0).abs() < 0.001,
4095 "Last stop should be 360deg, got {}",
4096 grad.stops.as_ref()[1].angle.to_degrees_raw()
4097 );
4098 } else {
4099 panic!("Expected ConicGradient");
4100 }
4101 }
4102
4103 #[test]
4104 fn test_conic_gradient_with_positions() {
4105 let cg =
4107 parse_style_background_content("conic-gradient(red 0deg, blue 180deg, green 360deg)")
4108 .unwrap();
4109 if let StyleBackgroundContent::ConicGradient(grad) = cg {
4110 assert_eq!(grad.stops.len(), 3);
4111 assert!(
4113 (grad.stops.as_ref()[0].angle.to_degrees_raw() - 0.0).abs() < 0.001,
4114 "First stop should be 0deg, got {}",
4115 grad.stops.as_ref()[0].angle.to_degrees_raw()
4116 );
4117 assert!(
4118 (grad.stops.as_ref()[1].angle.to_degrees_raw() - 180.0).abs() < 0.001,
4119 "Second stop should be 180deg, got {}",
4120 grad.stops.as_ref()[1].angle.to_degrees_raw()
4121 );
4122 assert!(
4123 (grad.stops.as_ref()[2].angle.to_degrees_raw() - 360.0).abs() < 0.001,
4124 "Last stop should be 360deg, got {}",
4125 grad.stops.as_ref()[2].angle.to_degrees_raw()
4126 );
4127 } else {
4128 panic!("Expected ConicGradient");
4129 }
4130 }
4131
4132 #[test]
4133 fn test_repeating_conic_gradient() {
4134 let cg =
4135 parse_style_background_content("repeating-conic-gradient(red, blue 30deg)").unwrap();
4136 if let StyleBackgroundContent::ConicGradient(grad) = cg {
4137 assert_eq!(grad.extend_mode, ExtendMode::Repeat);
4138 } else {
4139 panic!("Expected ConicGradient");
4140 }
4141 }
4142
4143 #[test]
4144 fn test_gradient_with_rgba_color() {
4145 let lg =
4147 parse_style_background_content("linear-gradient(rgba(255,0,0,0.5), blue)").unwrap();
4148 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4149 assert_eq!(grad.stops.len(), 2);
4150 let first_color = grad.stops.as_ref()[0].color.to_color_u_default();
4152 assert!(first_color.a >= 127 && first_color.a <= 128);
4153 } else {
4154 panic!("Expected LinearGradient");
4155 }
4156 }
4157
4158 #[test]
4159 fn test_gradient_with_rgba_and_position() {
4160 let lg =
4162 parse_style_background_content("linear-gradient(rgba(0,0,0,0.5) 50%, white)").unwrap();
4163 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4164 assert_eq!(grad.stops.len(), 2);
4165 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.5).abs() < 0.001);
4166 } else {
4167 panic!("Expected LinearGradient");
4168 }
4169 }
4170
4171 #[test]
4172 fn test_gradient_resolves_system_color_stop() {
4173 use crate::props::basic::color::ColorOrSystem;
4178 use crate::system::SystemColors;
4179
4180 let lg = parse_style_background_content(
4181 "linear-gradient(red, system:accent)",
4182 )
4183 .unwrap();
4184 let StyleBackgroundContent::LinearGradient(grad) = lg else {
4185 panic!("Expected LinearGradient");
4186 };
4187 let stops = grad.stops.as_ref();
4188 assert_eq!(stops.len(), 2);
4189
4190 let accent_stop = &stops[1];
4191 assert!(matches!(accent_stop.color, ColorOrSystem::System(_)));
4192
4193 let populated = SystemColors {
4194 accent: crate::props::basic::color::OptionColorU::Some(ColorU::new_rgb(0, 122, 255)),
4195 ..SystemColors::default()
4196 };
4197
4198 let resolved = accent_stop.resolve(&populated, ColorU::TRANSPARENT);
4199 assert_eq!(resolved, ColorU::new_rgb(0, 122, 255));
4200
4201 let empty = SystemColors::default();
4202 let fallback = accent_stop.resolve(&empty, ColorU::TRANSPARENT);
4203 assert_eq!(fallback, ColorU::TRANSPARENT);
4204 }
4205}