1
//! CSS properties for backgrounds, including colors, images, and gradients.
2

            
3
use alloc::{
4
    string::{String, ToString},
5
    vec::Vec,
6
};
7
use core::fmt;
8

            
9
#[cfg(feature = "parser")]
10
use 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
};
19
use 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
// --- TYPE DEFINITIONS ---
46

            
47
/// Whether a `gradient` should be repeated or clamped to the edges.
48
#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
49
#[repr(C)]
50
#[derive(Default)]
51
pub enum ExtendMode {
52
    #[default]
53
    Clamp,
54
    Repeat,
55
}
56

            
57
// -- Main Background Content Type --
58

            
59
/// A single CSS background layer: a solid color, image URL, or gradient.
60
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61
#[repr(C, u8)]
62
pub enum StyleBackgroundContent {
63
    LinearGradient(LinearGradient),
64
    RadialGradient(RadialGradient),
65
    ConicGradient(ConicGradient),
66
    Image(AzString),
67
    Color(ColorU),
68
    /// A theme-aware system color (e.g. `background: system:accent`), kept unresolved
69
    /// and resolved at render time. Mirrors the `ColorOrSystem::System` value that
70
    /// gradient color stops already accept.
71
    SystemColor(SystemColorRef),
72
}
73

            
74
impl_option!(
75
    StyleBackgroundContent,
76
    OptionStyleBackgroundContent,
77
    copy = false,
78
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
79
);
80

            
81
impl_vec!(StyleBackgroundContent, StyleBackgroundContentVec, StyleBackgroundContentVecDestructor, StyleBackgroundContentVecDestructorType, StyleBackgroundContentVecSlice, OptionStyleBackgroundContent);
82
impl_vec_debug!(StyleBackgroundContent, StyleBackgroundContentVec);
83
impl_vec_partialord!(StyleBackgroundContent, StyleBackgroundContentVec);
84
impl_vec_ord!(StyleBackgroundContent, StyleBackgroundContentVec);
85
impl_vec_clone!(
86
    StyleBackgroundContent,
87
    StyleBackgroundContentVec,
88
    StyleBackgroundContentVecDestructor
89
);
90
impl_vec_partialeq!(StyleBackgroundContent, StyleBackgroundContentVec);
91
impl_vec_eq!(StyleBackgroundContent, StyleBackgroundContentVec);
92
impl_vec_hash!(StyleBackgroundContent, StyleBackgroundContentVec);
93

            
94
impl Default for StyleBackgroundContent {
95
1
    fn default() -> Self {
96
1
        Self::Color(ColorU::TRANSPARENT)
97
1
    }
98
}
99

            
100
impl PrintAsCssValue for StyleBackgroundContent {
101
223
    fn print_as_css_value(&self) -> String {
102
223
        match self {
103
8
            Self::LinearGradient(lg) => {
104
8
                let prefix = if lg.extend_mode == ExtendMode::Repeat {
105
3
                    "repeating-linear-gradient"
106
                } else {
107
5
                    "linear-gradient"
108
                };
109
8
                format!("{}({})", prefix, lg.print_as_css_value())
110
            }
111
3
            Self::RadialGradient(rg) => {
112
3
                let prefix = if rg.extend_mode == ExtendMode::Repeat {
113
                    "repeating-radial-gradient"
114
                } else {
115
3
                    "radial-gradient"
116
                };
117
3
                format!("{}({})", prefix, rg.print_as_css_value())
118
            }
119
5
            Self::ConicGradient(cg) => {
120
5
                let prefix = if cg.extend_mode == ExtendMode::Repeat {
121
2
                    "repeating-conic-gradient"
122
                } else {
123
3
                    "conic-gradient"
124
                };
125
5
                format!("{}({})", prefix, cg.print_as_css_value())
126
            }
127
2
            Self::Image(id) => format!("url(\"{}\")", id.as_str()),
128
203
            Self::Color(c) => c.to_hash(),
129
2
            Self::SystemColor(s) => s.as_css_str().to_string(),
130
        }
131
223
    }
132
}
133

            
134
// Formatting to Rust code for background-related vecs
135

            
136
impl crate::codegen::format::FormatAsRustCode for StyleBackgroundContent {
137
    fn format_as_rust_code(&self, _tabs: usize) -> String {
138
        // Delegate to the CSS value representation for single backgrounds
139
        format!("StyleBackgroundContent::from_css(\"{}\")", self.print_as_css_value())
140
    }
141
}
142

            
143
impl 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

            
152
impl 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

            
161
impl 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

            
170
impl PrintAsCssValue for StyleBackgroundContentVec {
171
182
    fn print_as_css_value(&self) -> String {
172
182
        self.as_ref()
173
182
            .iter()
174
182
            .map(PrintAsCssValue::print_as_css_value)
175
182
            .collect::<Vec<_>>()
176
182
            .join(", ")
177
182
    }
178
}
179

            
180
// -- Gradient Types --
181

            
182
/// A CSS `linear-gradient()` or `repeating-linear-gradient()` value.
183
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
184
#[repr(C)]
185
pub struct LinearGradient {
186
    pub direction: Direction,
187
    pub extend_mode: ExtendMode,
188
    pub stops: NormalizedLinearColorStopVec,
189
}
190
impl Default for LinearGradient {
191
521
    fn default() -> Self {
192
521
        Self {
193
521
            direction: Direction::default(),
194
521
            extend_mode: ExtendMode::default(),
195
521
            stops: Vec::new().into(),
196
521
        }
197
521
    }
198
}
199
impl PrintAsCssValue for LinearGradient {
200
8
    fn print_as_css_value(&self) -> String {
201
8
        let dir_str = self.direction.print_as_css_value();
202
8
        let stops_str = self
203
8
            .stops
204
8
            .iter()
205
8
            .map(PrintAsCssValue::print_as_css_value)
206
8
            .collect::<Vec<_>>()
207
8
            .join(", ");
208
8
        if stops_str.is_empty() {
209
2
            dir_str
210
        } else {
211
6
            format!("{dir_str}, {stops_str}")
212
        }
213
8
    }
214
}
215

            
216
/// A CSS `radial-gradient()` or `repeating-radial-gradient()` value.
217
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
218
#[repr(C)]
219
pub struct RadialGradient {
220
    pub shape: Shape,
221
    pub size: RadialGradientSize,
222
    pub position: StyleBackgroundPosition,
223
    pub extend_mode: ExtendMode,
224
    pub stops: NormalizedLinearColorStopVec,
225
}
226
impl Default for RadialGradient {
227
257
    fn default() -> Self {
228
257
        Self {
229
257
            shape: Shape::default(),
230
257
            size: RadialGradientSize::default(),
231
257
            position: StyleBackgroundPosition::default(),
232
257
            extend_mode: ExtendMode::default(),
233
257
            stops: Vec::new().into(),
234
257
        }
235
257
    }
236
}
237
impl PrintAsCssValue for RadialGradient {
238
3
    fn print_as_css_value(&self) -> String {
239
3
        let stops_str = self
240
3
            .stops
241
3
            .iter()
242
3
            .map(PrintAsCssValue::print_as_css_value)
243
3
            .collect::<Vec<_>>()
244
3
            .join(", ");
245
3
        format!(
246
3
            "{} {} at {}, {}",
247
            self.shape,
248
            self.size,
249
3
            self.position.print_as_css_value(),
250
            stops_str
251
        )
252
3
    }
253
}
254

            
255
/// A CSS `conic-gradient()` or `repeating-conic-gradient()` value.
256
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
257
#[repr(C)]
258
pub struct ConicGradient {
259
    pub extend_mode: ExtendMode,
260
    pub center: StyleBackgroundPosition,
261
    pub angle: AngleValue,
262
    pub stops: NormalizedRadialColorStopVec,
263
}
264
impl Default for ConicGradient {
265
251
    fn default() -> Self {
266
251
        Self {
267
251
            extend_mode: ExtendMode::default(),
268
251
            // CSS default for conic-gradient is `at center` (50% 50%), NOT the generic
269
251
            // Left/Top of StyleBackgroundPosition::default() — a corner-anchored cone maps
270
251
            // the whole element into one angular slice and renders a flat color.
271
251
            center: StyleBackgroundPosition {
272
251
                horizontal: BackgroundPositionHorizontal::Center,
273
251
                vertical: BackgroundPositionVertical::Center,
274
251
            },
275
251
            angle: AngleValue::default(),
276
251
            stops: Vec::new().into(),
277
251
        }
278
251
    }
279
}
280
impl PrintAsCssValue for ConicGradient {
281
5
    fn print_as_css_value(&self) -> String {
282
5
        let stops_str = self
283
5
            .stops
284
5
            .iter()
285
5
            .map(PrintAsCssValue::print_as_css_value)
286
5
            .collect::<Vec<_>>()
287
5
            .join(", ");
288
5
        format!(
289
5
            "from {} at {}, {}",
290
            self.angle,
291
5
            self.center.print_as_css_value(),
292
            stops_str
293
        )
294
5
    }
295
}
296

            
297
// -- Gradient Sub-types --
298

            
299
/// The shape of a radial gradient: `circle` or `ellipse`.
300
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
301
#[repr(C)]
302
#[derive(Default)]
303
pub enum Shape {
304
    #[default]
305
    Ellipse,
306
    Circle,
307
}
308
impl fmt::Display for Shape {
309
10
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310
10
        write!(
311
10
            f,
312
10
            "{}",
313
10
            match self {
314
5
                Self::Ellipse => "ellipse",
315
5
                Self::Circle => "circle",
316
            }
317
        )
318
10
    }
319
}
320

            
321
/// The sizing keyword for a radial gradient (e.g. `closest-side`, `farthest-corner`).
322
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
323
#[repr(C)]
324
#[derive(Default)]
325
pub enum RadialGradientSize {
326
    ClosestSide,
327
    ClosestCorner,
328
    FarthestSide,
329
    #[default]
330
    FarthestCorner,
331
}
332
impl fmt::Display for RadialGradientSize {
333
15
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334
15
        write!(
335
15
            f,
336
15
            "{}",
337
15
            match self {
338
3
                Self::ClosestSide => "closest-side",
339
3
                Self::ClosestCorner => "closest-corner",
340
3
                Self::FarthestSide => "farthest-side",
341
6
                Self::FarthestCorner => "farthest-corner",
342
            }
343
        )
344
15
    }
345
}
346

            
347
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
348
#[repr(C)]
349
pub struct NormalizedLinearColorStop {
350
    pub offset: PercentageValue,
351
    /// Color for this gradient stop. Can be a concrete color or a system color reference.
352
    pub color: ColorOrSystem,
353
}
354

            
355
impl NormalizedLinearColorStop {
356
    /// Create a new normalized linear color stop with a concrete color.
357
14
    #[must_use] pub const fn new(offset: PercentageValue, color: ColorU) -> Self {
358
14
        Self { offset, color: ColorOrSystem::color(color) }
359
14
    }
360

            
361
    /// Resolve the color against system colors.
362
15
    #[must_use] pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
363
15
        self.color.resolve(system_colors, fallback)
364
15
    }
365
}
366

            
367
impl_option!(
368
    NormalizedLinearColorStop,
369
    OptionNormalizedLinearColorStop,
370
    copy = false,
371
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
372
);
373
impl_vec!(NormalizedLinearColorStop, NormalizedLinearColorStopVec, NormalizedLinearColorStopVecDestructor, NormalizedLinearColorStopVecDestructorType, NormalizedLinearColorStopVecSlice, OptionNormalizedLinearColorStop);
374
impl_vec_debug!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
375
impl_vec_partialord!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
376
impl_vec_ord!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
377
impl_vec_clone!(
378
    NormalizedLinearColorStop,
379
    NormalizedLinearColorStopVec,
380
    NormalizedLinearColorStopVecDestructor
381
);
382
impl_vec_partialeq!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
383
impl_vec_eq!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
384
impl_vec_hash!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
385
impl PrintAsCssValue for NormalizedLinearColorStop {
386
20
    fn print_as_css_value(&self) -> String {
387
20
        match &self.color {
388
18
            ColorOrSystem::Color(c) => format!("{} {}", c.to_hash(), self.offset),
389
2
            ColorOrSystem::System(s) => format!("{} {}", s.as_css_str(), self.offset),
390
        }
391
20
    }
392
}
393

            
394
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
395
#[repr(C)]
396
pub struct NormalizedRadialColorStop {
397
    pub angle: AngleValue,
398
    /// Color for this gradient stop. Can be a concrete color or a system color reference.
399
    pub color: ColorOrSystem,
400
}
401

            
402
impl NormalizedRadialColorStop {
403
    /// Create a new normalized radial color stop with a concrete color.
404
13
    #[must_use] pub const fn new(angle: AngleValue, color: ColorU) -> Self {
405
13
        Self { angle, color: ColorOrSystem::color(color) }
406
13
    }
407

            
408
    /// Resolve the color against system colors.
409
10
    #[must_use] pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
410
10
        self.color.resolve(system_colors, fallback)
411
10
    }
412
}
413

            
414
impl_option!(
415
    NormalizedRadialColorStop,
416
    OptionNormalizedRadialColorStop,
417
    copy = false,
418
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
419
);
420
impl_vec!(NormalizedRadialColorStop, NormalizedRadialColorStopVec, NormalizedRadialColorStopVecDestructor, NormalizedRadialColorStopVecDestructorType, NormalizedRadialColorStopVecSlice, OptionNormalizedRadialColorStop);
421
impl_vec_debug!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
422
impl_vec_partialord!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
423
impl_vec_ord!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
424
impl_vec_clone!(
425
    NormalizedRadialColorStop,
426
    NormalizedRadialColorStopVec,
427
    NormalizedRadialColorStopVecDestructor
428
);
429
impl_vec_partialeq!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
430
impl_vec_eq!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
431
impl_vec_hash!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
432
impl PrintAsCssValue for NormalizedRadialColorStop {
433
10
    fn print_as_css_value(&self) -> String {
434
10
        match &self.color {
435
10
            ColorOrSystem::Color(c) => format!("{} {}", c.to_hash(), self.angle),
436
            ColorOrSystem::System(s) => format!("{} {}", s.as_css_str(), self.angle),
437
        }
438
10
    }
439
}
440

            
441
/// Transient struct for parsing linear color stops before normalization.
442
///
443
/// Per W3C CSS Images Level 3, a color stop can have 0, 1, or 2 positions:
444
/// - `red` (no position)
445
/// - `red 50%` (one position)
446
/// - `red 10% 30%` (two positions - creates two stops at same color)
447
/// 
448
/// Supports system colors like `system:accent` for theme-aware gradients.
449
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
450
pub struct LinearColorStop {
451
    pub color: ColorOrSystem,
452
    /// First position (optional)
453
    pub offset1: OptionPercentageValue,
454
    /// Second position (optional, only valid if offset1 is Some)
455
    /// When present, creates two color stops at the same color.
456
    pub offset2: OptionPercentageValue,
457
}
458

            
459
/// Transient struct for parsing radial/conic color stops before normalization.
460
///
461
/// Per W3C CSS Images Level 3, a color stop can have 0, 1, or 2 positions:
462
/// - `red` (no position)
463
/// - `red 90deg` (one position)
464
/// - `red 45deg 90deg` (two positions - creates two stops at same color)
465
/// 
466
/// Supports system colors like `system:accent` for theme-aware gradients.
467
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
468
pub struct RadialColorStop {
469
    pub color: ColorOrSystem,
470
    /// First position (optional)
471
    pub offset1: OptionAngleValue,
472
    /// Second position (optional, only valid if offset1 is Some)
473
    /// When present, creates two color stops at the same color.
474
    pub offset2: OptionAngleValue,
475
}
476

            
477
// -- Other Background Properties --
478

            
479
/// The `background-position` property (horizontal + vertical components).
480
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
481
#[repr(C)]
482
pub struct StyleBackgroundPosition {
483
    pub horizontal: BackgroundPositionHorizontal,
484
    pub vertical: BackgroundPositionVertical,
485
}
486

            
487
impl_option!(
488
    StyleBackgroundPosition,
489
    OptionStyleBackgroundPosition,
490
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
491
);
492
impl_vec!(StyleBackgroundPosition, StyleBackgroundPositionVec, StyleBackgroundPositionVecDestructor, StyleBackgroundPositionVecDestructorType, StyleBackgroundPositionVecSlice, OptionStyleBackgroundPosition);
493
impl_vec_debug!(StyleBackgroundPosition, StyleBackgroundPositionVec);
494
impl_vec_partialord!(StyleBackgroundPosition, StyleBackgroundPositionVec);
495
impl_vec_ord!(StyleBackgroundPosition, StyleBackgroundPositionVec);
496
impl_vec_clone!(
497
    StyleBackgroundPosition,
498
    StyleBackgroundPositionVec,
499
    StyleBackgroundPositionVecDestructor
500
);
501
impl_vec_partialeq!(StyleBackgroundPosition, StyleBackgroundPositionVec);
502
impl_vec_eq!(StyleBackgroundPosition, StyleBackgroundPositionVec);
503
impl_vec_hash!(StyleBackgroundPosition, StyleBackgroundPositionVec);
504
impl Default for StyleBackgroundPosition {
505
276
    fn default() -> Self {
506
276
        Self {
507
276
            horizontal: BackgroundPositionHorizontal::Left,
508
276
            vertical: BackgroundPositionVertical::Top,
509
276
        }
510
276
    }
511
}
512

            
513
impl StyleBackgroundPosition {
514
8
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
515
8
        self.horizontal.scale_for_dpi(scale_factor);
516
8
        self.vertical.scale_for_dpi(scale_factor);
517
8
    }
518
}
519

            
520
impl PrintAsCssValue for StyleBackgroundPosition {
521
35
    fn print_as_css_value(&self) -> String {
522
35
        format!(
523
35
            "{} {}",
524
35
            self.horizontal.print_as_css_value(),
525
35
            self.vertical.print_as_css_value()
526
        )
527
35
    }
528
}
529
impl PrintAsCssValue for StyleBackgroundPositionVec {
530
1
    fn print_as_css_value(&self) -> String {
531
1
        self.iter()
532
1
            .map(PrintAsCssValue::print_as_css_value)
533
1
            .collect::<Vec<_>>()
534
1
            .join(", ")
535
1
    }
536
}
537

            
538
// Formatting to Rust code for StyleBackgroundPositionVec
539
impl 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)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
548
/// Horizontal component of `background-position`: a keyword or exact pixel value.
549
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
550
#[repr(C, u8)]
551
pub enum BackgroundPositionHorizontal {
552
    Left,
553
    Center,
554
    Right,
555
    Exact(PixelValue),
556
}
557

            
558
impl BackgroundPositionHorizontal {
559
37
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
560
37
        if let Self::Exact(s) = self {
561
10
            s.scale_for_dpi(scale_factor);
562
27
        }
563
37
    }
564
}
565

            
566
impl PrintAsCssValue for BackgroundPositionHorizontal {
567
53
    fn print_as_css_value(&self) -> String {
568
53
        match self {
569
17
            Self::Left => "left".to_string(),
570
11
            Self::Center => "center".to_string(),
571
9
            Self::Right => "right".to_string(),
572
16
            Self::Exact(px) => px.print_as_css_value(),
573
        }
574
53
    }
575
}
576
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
577
/// Vertical component of `background-position`: a keyword or exact pixel value.
578
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
579
#[repr(C, u8)]
580
pub enum BackgroundPositionVertical {
581
    Top,
582
    Center,
583
    Bottom,
584
    Exact(PixelValue),
585
}
586

            
587
impl BackgroundPositionVertical {
588
33
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
589
33
        if let Self::Exact(s) = self {
590
6
            s.scale_for_dpi(scale_factor);
591
27
        }
592
33
    }
593
}
594

            
595
impl PrintAsCssValue for BackgroundPositionVertical {
596
53
    fn print_as_css_value(&self) -> String {
597
53
        match self {
598
17
            Self::Top => "top".to_string(),
599
11
            Self::Center => "center".to_string(),
600
9
            Self::Bottom => "bottom".to_string(),
601
16
            Self::Exact(px) => px.print_as_css_value(),
602
        }
603
53
    }
604
}
605
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
606
/// The `background-size` property: `contain`, `cover`, or an exact size.
607
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
608
#[repr(C, u8)]
609
#[derive(Default)]
610
pub enum StyleBackgroundSize {
611
    ExactSize(PixelValueSize),
612
    #[default]
613
    Contain,
614
    Cover,
615
}
616

            
617
impl_option!(
618
    StyleBackgroundSize,
619
    OptionStyleBackgroundSize,
620
    copy = false,
621
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
622
);
623

            
624
/// Two-dimensional size in `PixelValue` units (width, height)
625
/// Used for background-size and similar properties
626
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
627
#[repr(C)]
628
pub struct PixelValueSize {
629
    pub width: PixelValue,
630
    pub height: PixelValue,
631
}
632

            
633
impl_vec!(StyleBackgroundSize, StyleBackgroundSizeVec, StyleBackgroundSizeVecDestructor, StyleBackgroundSizeVecDestructorType, StyleBackgroundSizeVecSlice, OptionStyleBackgroundSize);
634
impl_vec_debug!(StyleBackgroundSize, StyleBackgroundSizeVec);
635
impl_vec_partialord!(StyleBackgroundSize, StyleBackgroundSizeVec);
636
impl_vec_ord!(StyleBackgroundSize, StyleBackgroundSizeVec);
637
impl_vec_clone!(
638
    StyleBackgroundSize,
639
    StyleBackgroundSizeVec,
640
    StyleBackgroundSizeVecDestructor
641
);
642
impl_vec_partialeq!(StyleBackgroundSize, StyleBackgroundSizeVec);
643
impl_vec_eq!(StyleBackgroundSize, StyleBackgroundSizeVec);
644
impl_vec_hash!(StyleBackgroundSize, StyleBackgroundSizeVec);
645

            
646
impl StyleBackgroundSize {
647
15
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
648
15
        if let Self::ExactSize(size) = self {
649
3
            size.width.scale_for_dpi(scale_factor);
650
3
            size.height.scale_for_dpi(scale_factor);
651
12
        }
652
15
    }
653
}
654

            
655
impl PrintAsCssValue for StyleBackgroundSize {
656
7
    fn print_as_css_value(&self) -> String {
657
7
        match self {
658
2
            Self::Contain => "contain".to_string(),
659
1
            Self::Cover => "cover".to_string(),
660
4
            Self::ExactSize(size) => {
661
4
                format!(
662
4
                    "{} {}",
663
4
                    size.width.print_as_css_value(),
664
4
                    size.height.print_as_css_value()
665
                )
666
            }
667
        }
668
7
    }
669
}
670
impl PrintAsCssValue for StyleBackgroundSizeVec {
671
1
    fn print_as_css_value(&self) -> String {
672
1
        self.iter()
673
1
            .map(PrintAsCssValue::print_as_css_value)
674
1
            .collect::<Vec<_>>()
675
1
            .join(", ")
676
1
    }
677
}
678

            
679
/// The `background-repeat` property.
680
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
681
#[repr(C)]
682
#[derive(Default)]
683
pub enum StyleBackgroundRepeat {
684
    NoRepeat,
685
    #[default]
686
    PatternRepeat,
687
    RepeatX,
688
    RepeatY,
689
}
690

            
691
impl_option!(
692
    StyleBackgroundRepeat,
693
    OptionStyleBackgroundRepeat,
694
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
695
);
696
impl_vec!(StyleBackgroundRepeat, StyleBackgroundRepeatVec, StyleBackgroundRepeatVecDestructor, StyleBackgroundRepeatVecDestructorType, StyleBackgroundRepeatVecSlice, OptionStyleBackgroundRepeat);
697
impl_vec_debug!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
698
impl_vec_partialord!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
699
impl_vec_ord!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
700
impl_vec_clone!(
701
    StyleBackgroundRepeat,
702
    StyleBackgroundRepeatVec,
703
    StyleBackgroundRepeatVecDestructor
704
);
705
impl_vec_partialeq!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
706
impl_vec_eq!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
707
impl_vec_hash!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
708
impl PrintAsCssValue for StyleBackgroundRepeat {
709
6
    fn print_as_css_value(&self) -> String {
710
6
        match self {
711
2
            Self::NoRepeat => "no-repeat".to_string(),
712
2
            Self::PatternRepeat => "repeat".to_string(),
713
1
            Self::RepeatX => "repeat-x".to_string(),
714
1
            Self::RepeatY => "repeat-y".to_string(),
715
        }
716
6
    }
717
}
718
impl PrintAsCssValue for StyleBackgroundRepeatVec {
719
1
    fn print_as_css_value(&self) -> String {
720
1
        self.iter()
721
1
            .map(PrintAsCssValue::print_as_css_value)
722
1
            .collect::<Vec<_>>()
723
1
            .join(", ")
724
1
    }
725
}
726

            
727
// --- ERROR DEFINITIONS ---
728

            
729
#[derive(Clone, PartialEq)]
730
pub 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

            
744
impl_debug_as_display!(CssBackgroundParseError<'a>);
745
impl_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")]
760
impl_from!(
761
    ParenthesisParseError<'a>,
762
    CssBackgroundParseError::InvalidBackground
763
);
764
#[cfg(feature = "parser")]
765
impl_from!(
766
    CssDirectionParseError<'a>,
767
    CssBackgroundParseError::DirectionParseError
768
);
769
#[cfg(feature = "parser")]
770
impl_from!(
771
    CssGradientStopParseError<'a>,
772
    CssBackgroundParseError::GradientParseError
773
);
774
#[cfg(feature = "parser")]
775
impl_from!(
776
    CssShapeParseError<'a>,
777
    CssBackgroundParseError::ShapeParseError
778
);
779
#[cfg(feature = "parser")]
780
impl_from!(
781
    CssImageParseError<'a>,
782
    CssBackgroundParseError::ImageParseError
783
);
784
#[cfg(feature = "parser")]
785
impl_from!(
786
    CssColorParseError<'a>,
787
    CssBackgroundParseError::ColorParseError
788
);
789
#[cfg(feature = "parser")]
790
impl_from!(
791
    CssConicGradientParseError<'a>,
792
    CssBackgroundParseError::ConicGradient
793
);
794

            
795
#[derive(Debug, Clone, PartialEq)]
796
#[repr(C, u8)]
797
pub 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

            
811
impl CssBackgroundParseError<'_> {
812
62
    #[must_use] pub fn to_contained(&self) -> CssBackgroundParseErrorOwned {
813
62
        match self {
814
2
            Self::Error(s) => CssBackgroundParseErrorOwned::Error((*s).to_string().into()),
815
2
            Self::InvalidBackground(e) => {
816
2
                CssBackgroundParseErrorOwned::InvalidBackground(e.to_contained())
817
            }
818
5
            Self::UnclosedGradient(s) => {
819
5
                CssBackgroundParseErrorOwned::UnclosedGradient((*s).to_string().into())
820
            }
821
2
            Self::NoDirection(s) => CssBackgroundParseErrorOwned::NoDirection((*s).to_string().into()),
822
1
            Self::TooFewGradientStops(s) => {
823
1
                CssBackgroundParseErrorOwned::TooFewGradientStops((*s).to_string().into())
824
            }
825
2
            Self::DirectionParseError(e) => {
826
2
                CssBackgroundParseErrorOwned::DirectionParseError(e.to_contained())
827
            }
828
1
            Self::GradientParseError(e) => {
829
1
                CssBackgroundParseErrorOwned::GradientParseError(e.to_contained())
830
            }
831
1
            Self::ConicGradient(e) => CssBackgroundParseErrorOwned::ConicGradient(e.to_contained()),
832
1
            Self::ShapeParseError(e) => {
833
1
                CssBackgroundParseErrorOwned::ShapeParseError(e.to_contained())
834
            }
835
1
            Self::ImageParseError(e) => {
836
1
                CssBackgroundParseErrorOwned::ImageParseError(e.to_contained())
837
            }
838
44
            Self::ColorParseError(e) => {
839
44
                CssBackgroundParseErrorOwned::ColorParseError(e.to_contained())
840
            }
841
        }
842
62
    }
843
}
844

            
845
impl CssBackgroundParseErrorOwned {
846
77
    #[must_use] pub fn to_shared(&self) -> CssBackgroundParseError<'_> {
847
77
        match self {
848
4
            Self::Error(s) => CssBackgroundParseError::Error(s),
849
4
            Self::InvalidBackground(e) => CssBackgroundParseError::InvalidBackground(e.to_shared()),
850
6
            Self::UnclosedGradient(s) => CssBackgroundParseError::UnclosedGradient(s),
851
3
            Self::NoDirection(s) => CssBackgroundParseError::NoDirection(s),
852
2
            Self::TooFewGradientStops(s) => CssBackgroundParseError::TooFewGradientStops(s),
853
4
            Self::DirectionParseError(e) => {
854
4
                CssBackgroundParseError::DirectionParseError(e.to_shared())
855
            }
856
2
            Self::GradientParseError(e) => {
857
2
                CssBackgroundParseError::GradientParseError(e.to_shared())
858
            }
859
2
            Self::ConicGradient(e) => CssBackgroundParseError::ConicGradient(e.to_shared()),
860
2
            Self::ShapeParseError(e) => CssBackgroundParseError::ShapeParseError(e.to_shared()),
861
2
            Self::ImageParseError(e) => CssBackgroundParseError::ImageParseError(e.to_shared()),
862
46
            Self::ColorParseError(e) => CssBackgroundParseError::ColorParseError(e.to_shared()),
863
        }
864
77
    }
865
}
866

            
867
#[derive(Clone, PartialEq)]
868
pub enum CssGradientStopParseError<'a> {
869
    Error(&'a str),
870
    Percentage(PercentageParseError),
871
    Angle(CssAngleValueParseError<'a>),
872
    ColorParseError(CssColorParseError<'a>),
873
}
874

            
875
impl_debug_as_display!(CssGradientStopParseError<'a>);
876
impl_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")]
883
impl_from!(
884
    CssColorParseError<'a>,
885
    CssGradientStopParseError::ColorParseError
886
);
887

            
888
#[derive(Debug, Clone, PartialEq)]
889
#[repr(C, u8)]
890
pub enum CssGradientStopParseErrorOwned {
891
    Error(AzString),
892
    Percentage(PercentageParseErrorOwned),
893
    Angle(CssAngleValueParseErrorOwned),
894
    ColorParseError(CssColorParseErrorOwned),
895
}
896

            
897
impl CssGradientStopParseError<'_> {
898
11
    #[must_use] pub fn to_contained(&self) -> CssGradientStopParseErrorOwned {
899
11
        match self {
900
6
            Self::Error(s) => CssGradientStopParseErrorOwned::Error((*s).to_string().into()),
901
2
            Self::Percentage(e) => CssGradientStopParseErrorOwned::Percentage(e.to_contained()),
902
2
            Self::Angle(e) => CssGradientStopParseErrorOwned::Angle(e.to_contained()),
903
1
            Self::ColorParseError(e) => {
904
1
                CssGradientStopParseErrorOwned::ColorParseError(e.to_contained())
905
            }
906
        }
907
11
    }
908
}
909

            
910
impl CssGradientStopParseErrorOwned {
911
12
    #[must_use] pub fn to_shared(&self) -> CssGradientStopParseError<'_> {
912
12
        match self {
913
7
            Self::Error(s) => CssGradientStopParseError::Error(s),
914
2
            Self::Percentage(e) => CssGradientStopParseError::Percentage(e.to_shared()),
915
2
            Self::Angle(e) => CssGradientStopParseError::Angle(e.to_shared()),
916
1
            Self::ColorParseError(e) => CssGradientStopParseError::ColorParseError(e.to_shared()),
917
        }
918
12
    }
919
}
920

            
921
#[derive(Clone, PartialEq, Eq)]
922
pub enum CssConicGradientParseError<'a> {
923
    Position(CssBackgroundPositionParseError<'a>),
924
    Angle(CssAngleValueParseError<'a>),
925
    NoAngle(&'a str),
926
}
927
impl_debug_as_display!(CssConicGradientParseError<'a>);
928
impl_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")]
934
impl_from!(
935
    CssAngleValueParseError<'a>,
936
    CssConicGradientParseError::Angle
937
);
938
#[cfg(feature = "parser")]
939
impl_from!(
940
    CssBackgroundPositionParseError<'a>,
941
    CssConicGradientParseError::Position
942
);
943

            
944
#[derive(Debug, Clone, PartialEq, Eq)]
945
#[repr(C, u8)]
946
pub enum CssConicGradientParseErrorOwned {
947
    Position(CssBackgroundPositionParseErrorOwned),
948
    Angle(CssAngleValueParseErrorOwned),
949
    NoAngle(AzString),
950
}
951
impl CssConicGradientParseError<'_> {
952
8
    #[must_use] pub fn to_contained(&self) -> CssConicGradientParseErrorOwned {
953
8
        match self {
954
1
            Self::Position(e) => CssConicGradientParseErrorOwned::Position(e.to_contained()),
955
1
            Self::Angle(e) => CssConicGradientParseErrorOwned::Angle(e.to_contained()),
956
6
            Self::NoAngle(s) => CssConicGradientParseErrorOwned::NoAngle((*s).to_string().into()),
957
        }
958
8
    }
959
}
960
impl CssConicGradientParseErrorOwned {
961
9
    #[must_use] pub fn to_shared(&self) -> CssConicGradientParseError<'_> {
962
9
        match self {
963
1
            Self::Position(e) => CssConicGradientParseError::Position(e.to_shared()),
964
1
            Self::Angle(e) => CssConicGradientParseError::Angle(e.to_shared()),
965
7
            Self::NoAngle(s) => CssConicGradientParseError::NoAngle(s),
966
        }
967
9
    }
968
}
969

            
970
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
971
pub enum CssShapeParseError<'a> {
972
    ShapeErr(InvalidValueErr<'a>),
973
}
974
impl_display! {CssShapeParseError<'a>, {
975
    ShapeErr(e) => format!("\"{}\"", e.0),
976
}}
977
#[derive(Debug, Clone, PartialEq, Eq)]
978
#[repr(C, u8)]
979
pub enum CssShapeParseErrorOwned {
980
    ShapeErr(InvalidValueErrOwned),
981
}
982
impl CssShapeParseError<'_> {
983
6
    #[must_use] pub fn to_contained(&self) -> CssShapeParseErrorOwned {
984
6
        match self {
985
6
            Self::ShapeErr(err) => CssShapeParseErrorOwned::ShapeErr(err.to_contained()),
986
        }
987
6
    }
988
}
989
impl CssShapeParseErrorOwned {
990
7
    #[must_use] pub fn to_shared(&self) -> CssShapeParseError<'_> {
991
7
        match self {
992
7
            Self::ShapeErr(err) => CssShapeParseError::ShapeErr(err.to_shared()),
993
        }
994
7
    }
995
}
996

            
997
#[derive(Debug, Clone, PartialEq, Eq)]
998
pub enum CssBackgroundPositionParseError<'a> {
999
    NoPosition(&'a str),
    TooManyComponents(&'a str),
    FirstComponentWrong(CssPixelValueParseError<'a>),
    SecondComponentWrong(CssPixelValueParseError<'a>),
}
impl_display! {CssBackgroundPositionParseError<'a>, {
    NoPosition(e) => format!("First background position missing: \"{}\"", e),
    TooManyComponents(e) => format!("background-position can only have one or two components, not more: \"{}\"", e),
    FirstComponentWrong(e) => format!("Failed to parse first component: \"{}\"", e),
    SecondComponentWrong(e) => format!("Failed to parse second component: \"{}\"", e),
}}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum CssBackgroundPositionParseErrorOwned {
    NoPosition(AzString),
    TooManyComponents(AzString),
    FirstComponentWrong(CssPixelValueParseErrorOwned),
    SecondComponentWrong(CssPixelValueParseErrorOwned),
}
impl CssBackgroundPositionParseError<'_> {
40
    #[must_use] pub fn to_contained(&self) -> CssBackgroundPositionParseErrorOwned {
40
        match self {
11
            Self::NoPosition(s) => CssBackgroundPositionParseErrorOwned::NoPosition((*s).to_string().into()),
1
            Self::TooManyComponents(s) => {
1
                CssBackgroundPositionParseErrorOwned::TooManyComponents((*s).to_string().into())
            }
27
            Self::FirstComponentWrong(e) => {
27
                CssBackgroundPositionParseErrorOwned::FirstComponentWrong(e.to_contained())
            }
1
            Self::SecondComponentWrong(e) => {
1
                CssBackgroundPositionParseErrorOwned::SecondComponentWrong(e.to_contained())
            }
        }
40
    }
}
impl CssBackgroundPositionParseErrorOwned {
40
    #[must_use] pub fn to_shared(&self) -> CssBackgroundPositionParseError<'_> {
40
        match self {
11
            Self::NoPosition(s) => CssBackgroundPositionParseError::NoPosition(s),
1
            Self::TooManyComponents(s) => CssBackgroundPositionParseError::TooManyComponents(s),
27
            Self::FirstComponentWrong(e) => {
27
                CssBackgroundPositionParseError::FirstComponentWrong(e.to_shared())
            }
1
            Self::SecondComponentWrong(e) => {
1
                CssBackgroundPositionParseError::SecondComponentWrong(e.to_shared())
            }
        }
40
    }
}
// --- PARSERS ---
#[cfg(feature = "parser")]
pub mod parser {
    #[allow(clippy::wildcard_imports)] // parser submodule reuses the parent module's value types
    use super::*;
    // the `*Gradient` suffix mirrors the CSS gradient function names this enum
    // parses (linear-gradient, radial-gradient, conic-gradient, …).
    #[allow(clippy::enum_variant_names)]
    #[derive(Debug, Copy, Clone, PartialEq, Eq)]
    enum GradientType {
        LinearGradient,
        RepeatingLinearGradient,
        RadialGradient,
        RepeatingRadialGradient,
        ConicGradient,
        RepeatingConicGradient,
    }
    impl GradientType {
695
        pub(crate) const fn get_extend_mode(self) -> ExtendMode {
695
            match self {
                Self::LinearGradient | Self::RadialGradient | Self::ConicGradient => {
419
                    ExtendMode::Clamp
                }
                Self::RepeatingLinearGradient
                | Self::RepeatingRadialGradient
276
                | Self::RepeatingConicGradient => ExtendMode::Repeat,
            }
695
        }
    }
    // -- Top-level Parsers for background-* properties --
    /// Parses multiple backgrounds, such as "linear-gradient(red, green), url(image.png)".
    /// # Errors
    ///
    /// Returns an error if `input` is not a valid CSS `background-content-multiple` value.
31517
    pub fn parse_style_background_content_multiple(
31517
        input: &str,
31517
    ) -> Result<StyleBackgroundContentVec, CssBackgroundParseError<'_>> {
31517
        Ok(split_string_respect_comma(input)
31517
            .iter()
33520
            .map(|i| parse_style_background_content(i))
31517
            .collect::<Result<Vec<_>, _>>()?
31419
            .into())
31517
    }
    /// Parses a single background value, which can be a color, image, or gradient.
    /// # Errors
    ///
    /// Returns an error if `input` is not a valid CSS `background-content` value.
33787
    pub fn parse_style_background_content(
33787
        input: &str,
33787
    ) -> Result<StyleBackgroundContent, CssBackgroundParseError<'_>> {
33787
        match parse_parentheses(
33787
            input,
33787
            &[
33787
                "linear-gradient",
33787
                "repeating-linear-gradient",
33787
                "radial-gradient",
33787
                "repeating-radial-gradient",
33787
                "conic-gradient",
33787
                "repeating-conic-gradient",
33787
                "image",
33787
                "url",
33787
            ],
33787
        ) {
206
            Ok((background_type, brace_contents)) => {
206
                let gradient_type = match background_type {
206
                    "linear-gradient" => GradientType::LinearGradient,
89
                    "repeating-linear-gradient" => GradientType::RepeatingLinearGradient,
84
                    "radial-gradient" => GradientType::RadialGradient,
57
                    "repeating-radial-gradient" => GradientType::RepeatingRadialGradient,
56
                    "conic-gradient" => GradientType::ConicGradient,
42
                    "repeating-conic-gradient" => GradientType::RepeatingConicGradient,
38
                    "image" | "url" => {
                        return Ok(StyleBackgroundContent::Image(
38
                            parse_image(brace_contents)?,
                        ))
                    }
                    _ => unreachable!(),
                };
168
                parse_gradient(brace_contents, gradient_type)
            }
            // A bare `background:` value is a color. Accept system colors here too
            // (`system:accent`, `system:text`, ...), matching the gradient color stops
            // (which use `parse_color_or_system`). System colors stay unresolved and are
            // theme-resolved at render time. `parse_color_or_system` is a superset of
            // `parse_css_color`, so ordinary colors keep parsing exactly as before.
33581
            Err(_) => Ok(match parse_color_or_system(input)? {
16581
                ColorOrSystem::Color(c) => StyleBackgroundContent::Color(c),
16767
                ColorOrSystem::System(s) => StyleBackgroundContent::SystemColor(s),
            }),
        }
33787
    }
    /// Parses multiple `background-position` values.
    /// # Errors
    ///
    /// Returns an error if `input` is not a valid CSS `background-position-multiple` value.
7
    pub fn parse_style_background_position_multiple(
7
        input: &str,
7
    ) -> Result<StyleBackgroundPositionVec, CssBackgroundPositionParseError<'_>> {
7
        Ok(split_string_respect_comma(input)
7
            .iter()
2010
            .map(|i| parse_style_background_position(i))
7
            .collect::<Result<Vec<_>, _>>()?
5
            .into())
7
    }
    /// Parses a single `background-position` value.
    /// # Errors
    ///
    /// Returns an error if `input` is not a valid CSS `background-position` value.
2397
    pub fn parse_style_background_position(
2397
        input: &str,
2397
    ) -> Result<StyleBackgroundPosition, CssBackgroundPositionParseError<'_>> {
2397
        let input = input.trim();
2397
        let mut whitespace_iter = input.split_whitespace();
2397
        let first = whitespace_iter
2397
            .next()
2397
            .ok_or(CssBackgroundPositionParseError::NoPosition(input))?;
2378
        let second = whitespace_iter.next();
2378
        if whitespace_iter.next().is_some() {
13
            return Err(CssBackgroundPositionParseError::TooManyComponents(input));
2365
        }
        // Try to parse as horizontal first, if that fails, maybe it's a vertical keyword
2365
        if let Ok(horizontal) = parse_background_position_horizontal(first) {
2144
            let vertical = match second {
2040
                Some(s) => parse_background_position_vertical(s)
2040
                    .map_err(CssBackgroundPositionParseError::SecondComponentWrong)?,
104
                None => BackgroundPositionVertical::Center,
            };
2143
            return Ok(StyleBackgroundPosition {
2143
                horizontal,
2143
                vertical,
2143
            });
221
        }
        // If the first part wasn't a horizontal keyword, maybe it's a vertical one
221
        if let Ok(vertical) = parse_background_position_vertical(first) {
2
            let horizontal = match second {
1
                Some(s) => parse_background_position_horizontal(s)
1
                    .map_err(CssBackgroundPositionParseError::FirstComponentWrong)?,
1
                None => BackgroundPositionHorizontal::Center,
            };
2
            return Ok(StyleBackgroundPosition {
2
                horizontal,
2
                vertical,
2
            });
219
        }
219
        Err(CssBackgroundPositionParseError::FirstComponentWrong(
219
            CssPixelValueParseError::InvalidPixelValue(first),
219
        ))
2397
    }
    /// Parses multiple `background-size` values.
    /// # Errors
    ///
    /// Returns an error if `input` is not a valid CSS `background-size-multiple` value.
7
    pub fn parse_style_background_size_multiple(
7
        input: &str,
7
    ) -> Result<StyleBackgroundSizeVec, InvalidValueErr<'_>> {
7
        Ok(split_string_respect_comma(input)
7
            .iter()
2011
            .map(|i| parse_style_background_size(i))
7
            .collect::<Result<Vec<_>, _>>()?
5
            .into())
7
    }
    /// Parses a single `background-size` value.
    /// # Errors
    ///
    /// Returns an error if `input` is not a valid CSS `background-size` value.
2127
    pub fn parse_style_background_size(
2127
        input: &str,
2127
    ) -> Result<StyleBackgroundSize, InvalidValueErr<'_>> {
2127
        let input = input.trim();
2127
        match input {
2127
            "contain" => Ok(StyleBackgroundSize::Contain),
2120
            "cover" => Ok(StyleBackgroundSize::Cover),
115
            other => {
115
                let mut iter = other.split_whitespace();
115
                let x_val = iter.next().ok_or(InvalidValueErr(input))?;
101
                let x_pos = parse_pixel_value(x_val).map_err(|_| InvalidValueErr(input))?;
43
                let y_pos = match iter.next() {
8
                    Some(y_val) => parse_pixel_value(y_val).map_err(|_| InvalidValueErr(input))?,
35
                    None => x_pos, // If only one value, it applies to both width and height
                };
43
                Ok(StyleBackgroundSize::ExactSize(PixelValueSize {
43
                    width: x_pos,
43
                    height: y_pos,
43
                }))
            }
        }
2127
    }
    /// Parses multiple `background-repeat` values.
    /// # Errors
    ///
    /// Returns an error if `input` is not a valid CSS `background-repeat-multiple` value.
7
    pub fn parse_style_background_repeat_multiple(
7
        input: &str,
7
    ) -> Result<StyleBackgroundRepeatVec, InvalidValueErr<'_>> {
7
        Ok(split_string_respect_comma(input)
7
            .iter()
2010
            .map(|i| parse_style_background_repeat(i))
7
            .collect::<Result<Vec<_>, _>>()?
5
            .into())
7
    }
    /// Parses a single `background-repeat` value.
    /// # Errors
    ///
    /// Returns an error if `input` is not a valid CSS `background-repeat` value.
2121
    pub fn parse_style_background_repeat(
2121
        input: &str,
2121
    ) -> Result<StyleBackgroundRepeat, InvalidValueErr<'_>> {
2121
        match input.trim() {
2121
            "no-repeat" => Ok(StyleBackgroundRepeat::NoRepeat),
2114
            "repeat" => Ok(StyleBackgroundRepeat::PatternRepeat),
107
            "repeat-x" => Ok(StyleBackgroundRepeat::RepeatX),
104
            "repeat-y" => Ok(StyleBackgroundRepeat::RepeatY),
101
            _ => Err(InvalidValueErr(input)),
        }
2121
    }
    // -- Gradient Parsing Logic --
    /// Parses the contents of a gradient function.
758
    fn parse_gradient(
758
        input: &str,
758
        gradient_type: GradientType,
758
    ) -> Result<StyleBackgroundContent, CssBackgroundParseError<'_>> {
758
        let input = input.trim();
758
        let comma_separated_items = split_string_respect_comma(input);
758
        let mut brace_iterator = comma_separated_items.iter();
758
        let first_brace_item = brace_iterator
758
            .next()
758
            .ok_or(CssBackgroundParseError::NoDirection(input))?;
677
        match gradient_type {
            GradientType::LinearGradient | GradientType::RepeatingLinearGradient => {
291
                let mut linear_gradient = LinearGradient {
291
                    extend_mode: gradient_type.get_extend_mode(),
291
                    ..Default::default()
291
                };
291
                let mut linear_stops = Vec::new();
291
                if let Ok(dir) = parse_direction(first_brace_item) {
142
                    linear_gradient.direction = dir;
142
                } else {
149
                    linear_stops.push(parse_linear_color_stop(first_brace_item)?);
                }
2374
                for item in brace_iterator {
2201
                    linear_stops.push(parse_linear_color_stop(item)?);
                }
173
                linear_gradient.stops = get_normalized_linear_stops(&linear_stops).into();
173
                Ok(StyleBackgroundContent::LinearGradient(linear_gradient))
            }
            GradientType::RadialGradient | GradientType::RepeatingRadialGradient => {
                // Simplified parsing: assumes shape/size/position come first, then stops.
                // A more robust parser would handle them in any order.
196
                let mut radial_gradient = RadialGradient {
196
                    extend_mode: gradient_type.get_extend_mode(),
196
                    ..Default::default()
196
                };
196
                let mut radial_stops = Vec::new();
196
                let mut current_item = *first_brace_item;
196
                let mut items_consumed = false;
                // Greedily consume shape, size, position keywords
                loop {
220
                    let mut consumed_in_iteration = false;
220
                    let mut temp_iter = current_item.split_whitespace();
398
                    for word in temp_iter {
231
                        if let Ok(shape) = parse_shape(word) {
28
                            radial_gradient.shape = shape;
28
                            consumed_in_iteration = true;
203
                        } else if let Ok(size) = parse_radial_gradient_size(word) {
5
                            radial_gradient.size = size;
5
                            consumed_in_iteration = true;
198
                        } else if let Ok(pos) = parse_style_background_position(current_item) {
53
                            radial_gradient.position = pos;
53
                            consumed_in_iteration = true;
53
                            break; // position can have multiple words, so consume the rest of the
                                   // item
145
                        }
                    }
220
                    if consumed_in_iteration {
81
                        if let Some(next_item) = brace_iterator.next() {
24
                            current_item = next_item;
24
                            items_consumed = true;
24
                        } else {
57
                            break;
                        }
                    } else {
139
                        break;
                    }
                }
196
                if items_consumed || parse_linear_color_stop(current_item).is_ok() {
31
                    radial_stops.push(parse_linear_color_stop(current_item)?);
165
                }
224
                for item in brace_iterator {
38
                    radial_stops.push(parse_linear_color_stop(item)?);
                }
186
                radial_gradient.stops = get_normalized_linear_stops(&radial_stops).into();
186
                Ok(StyleBackgroundContent::RadialGradient(radial_gradient))
            }
            GradientType::ConicGradient | GradientType::RepeatingConicGradient => {
190
                let mut conic_gradient = ConicGradient {
190
                    extend_mode: gradient_type.get_extend_mode(),
190
                    ..Default::default()
190
                };
190
                let mut conic_stops = Vec::new();
190
                if let Some((angle, center)) = parse_conic_first_item(first_brace_item)? {
7
                    conic_gradient.angle = angle;
7
                    conic_gradient.center = center;
7
                } else {
178
                    conic_stops.push(parse_radial_color_stop(first_brace_item)?);
                }
47
                for item in brace_iterator {
26
                    conic_stops.push(parse_radial_color_stop(item)?);
                }
21
                conic_gradient.stops = get_normalized_radial_stops(&conic_stops).into();
21
                Ok(StyleBackgroundContent::ConicGradient(conic_gradient))
            }
        }
758
    }
    // -- Gradient Parsing Helpers --
    /// Parses color stops per W3C CSS Images Level 3:
    /// - "red" (no position)
    /// - "red 5%" (one position)
    /// - "red 10% 30%" (two positions - creates a hard color band)
    /// 
    /// Also supports system colors like `system:accent 50%` for theme-aware gradients.
2608
    fn parse_linear_color_stop(
2608
        input: &str,
2608
    ) -> Result<LinearColorStop, CssGradientStopParseError<'_>> {
2608
        let input = input.trim();
2608
        let (color_str, offset1_str, offset2_str) = split_color_and_offsets(input);
2608
        let color = parse_color_or_system(color_str)?;
2307
        let offset1 = match offset1_str {
2245
            None => OptionPercentageValue::None,
62
            Some(s) => OptionPercentageValue::Some(
62
                parse_percentage_value(s).map_err(CssGradientStopParseError::Percentage)?,
            ),
        };
2305
        let offset2 = match offset2_str {
2302
            None => OptionPercentageValue::None,
3
            Some(s) => OptionPercentageValue::Some(
3
                parse_percentage_value(s).map_err(CssGradientStopParseError::Percentage)?,
            ),
        };
2305
        Ok(LinearColorStop {
2305
            color,
2305
            offset1,
2305
            offset2,
2305
        })
2608
    }
    /// Parses color stops per W3C CSS Images Level 3:
    /// - "red" (no position)
    /// - "red 90deg" (one position)
    /// - "red 45deg 90deg" (two positions - creates a hard color band)
    /// 
    /// Also supports system colors like `system:accent 90deg` for theme-aware gradients.
216
    fn parse_radial_color_stop(
216
        input: &str,
216
    ) -> Result<RadialColorStop, CssGradientStopParseError<'_>> {
216
        let input = input.trim();
216
        let (color_str, offset1_str, offset2_str) = split_color_and_offsets(input);
216
        let color = parse_color_or_system(color_str)?;
45
        let offset1 = match offset1_str {
17
            None => OptionAngleValue::None,
28
            Some(s) => OptionAngleValue::Some(
28
                parse_angle_value(s).map_err(CssGradientStopParseError::Angle)?,
            ),
        };
45
        let offset2 = match offset2_str {
44
            None => OptionAngleValue::None,
1
            Some(s) => OptionAngleValue::Some(
1
                parse_angle_value(s).map_err(CssGradientStopParseError::Angle)?,
            ),
        };
45
        Ok(RadialColorStop {
45
            color,
45
            offset1,
45
            offset2,
45
        })
216
    }
    /// Helper to robustly split a string like "rgba(0,0,0,0.5) 10% 30%" into color and offset
    /// parts. Returns (`color_str`, offset1, offset2) where offsets are optional.
    ///
    /// Per W3C CSS Images Level 3, a color stop can have 0, 1, or 2 positions:
    /// - "red" -> ("red", None, None)
    /// - "red 50%" -> ("red", Some("50%"), None)
    /// - "red 10% 30%" -> ("red", Some("10%"), Some("30%"))
2920
    fn split_color_and_offsets(input: &str) -> (&str, Option<&str>, Option<&str>) {
        // Strategy: scan from the end to find position values (contain digits + % or unit).
        // We need to handle complex colors like "rgba(0, 0, 0, 0.5)" that contain spaces and
        // digits.
2920
        let input = input.trim();
        // Try to find the last position value (might be second of two)
2920
        if let Some((remaining, last_offset)) = try_split_last_offset(input) {
            // Try to find another position value before it
99
            if let Some((color_part, first_offset)) = try_split_last_offset(remaining) {
8
                return (color_part.trim(), Some(first_offset), Some(last_offset));
91
            }
91
            return (remaining.trim(), Some(last_offset), None);
2821
        }
2821
        (input, None, None)
2920
    }
    /// Try to split off the last whitespace-separated token if it looks like a position value.
    /// Returns (remaining, `offset_str`) if successful.
3114
    fn try_split_last_offset(input: &str) -> Option<(&str, &str)> {
3114
        let input = input.trim();
3114
        if let Some(last_ws_idx) = input.rfind(char::is_whitespace) {
133
            let (potential_color, potential_offset) = input.split_at(last_ws_idx);
133
            let potential_offset = potential_offset.trim();
            // A valid offset must contain a digit and typically ends with % or a unit
            // This avoids misinterpreting "to right bottom" as containing offsets
133
            if is_likely_offset(potential_offset) {
111
                return Some((potential_color, potential_offset));
22
            }
2981
        }
3003
        None
3114
    }
    /// Check if a string looks like a position value (percentage or length).
    /// Must contain a digit and typically ends with %, px, em, etc.
248
    fn is_likely_offset(s: &str) -> bool {
605
        if !s.contains(|c: char| c.is_ascii_digit()) {
92
            return false;
156
        }
        // Check if it ends with a known unit or %
156
        let units = [
156
            "%", "px", "em", "rem", "ex", "ch", "vw", "vh", "vmin", "vmax", "cm", "mm", "in", "pt",
156
            "pc", "deg", "rad", "grad", "turn",
156
        ];
1122
        units.iter().any(|u| s.ends_with(u))
248
    }
    /// Parses the `from <angle> at <position>` part of a conic gradient.
286
    fn parse_conic_first_item(
286
        input: &str,
286
    ) -> Result<Option<(AngleValue, StyleBackgroundPosition)>, CssConicGradientParseError<'_>> {
286
        let input = input.trim();
286
        if !input.starts_with("from") {
265
            return Ok(None);
21
        }
21
        let mut parts = input["from".len()..].trim().split("at");
21
        let angle_part = parts
21
            .next()
21
            .ok_or(CssConicGradientParseError::NoAngle(input))?
21
            .trim();
21
        let angle = parse_angle_value(angle_part)?;
11
        let position = match parts.next() {
6
            Some(pos_part) => parse_style_background_position(pos_part.trim())?,
5
            None => StyleBackgroundPosition::default(),
        };
10
        Ok(Some((angle, position)))
286
    }
    // -- Normalization Functions --
    macro_rules! impl_get_normalized_stops {
        (
            fn $fn_name:ident($input_stop:ty) -> Vec<$output_stop:ident>,
            pos_type = $pos_ty:ty,
            default_start = $default_start:expr,
            default_end = $default_end:expr,
            pos_ctor = $pos_ctor:expr,
            pos_to_f32 = $pos_to_f32:expr,
            output_field = $out_field:ident,
        ) => {
            #[allow(clippy::suboptimal_flops)] // explicit FP; mul_add slower without +fma
380
            fn $fn_name(stops: &[$input_stop]) -> Vec<$output_stop> {
380
                if stops.is_empty() {
211
                    return Vec::new();
169
                }
169
                let mut expanded: Vec<(ColorOrSystem, Option<$pos_ty>)> = Vec::new();
2500
                for stop in stops {
2331
                    match (stop.offset1.into_option(), stop.offset2.into_option()) {
2258
                        (None, _) => {
2258
                            expanded.push((stop.color, None));
2258
                        }
71
                        (Some(pos1), None) => {
71
                            expanded.push((stop.color, Some(pos1)));
71
                        }
2
                        (Some(pos1), Some(pos2)) => {
2
                            expanded.push((stop.color, Some(pos1)));
2
                            expanded.push((stop.color, Some(pos2)));
2
                        }
                    }
                }
169
                if expanded.is_empty() {
                    return Vec::new();
169
                }
169
                let pos_ctor: fn(f32) -> $pos_ty = $pos_ctor;
169
                let pos_to_f32: fn(&$pos_ty) -> f32 = $pos_to_f32;
169
                if expanded[0].1.is_none() {
126
                    expanded[0].1 = Some(pos_ctor($default_start));
126
                }
169
                let last_idx = expanded.len() - 1;
169
                if expanded[last_idx].1.is_none() {
127
                    expanded[last_idx].1 = Some(pos_ctor($default_end));
127
                }
169
                let mut max_so_far: f32 = 0.0;
2333
                for (_, pos) in expanded.iter_mut() {
2333
                    if let Some(p) = pos {
328
                        let val = pos_to_f32(p);
328
                        if val < max_so_far {
10
                            *p = pos_ctor(max_so_far);
318
                        } else {
318
                            max_so_far = val;
318
                        }
2005
                    }
                }
169
                let mut i = 0;
502
                while i < expanded.len() {
333
                    if expanded[i].1.is_none() {
5
                        let run_start = i;
5
                        let mut run_end = i;
2010
                        while run_end < expanded.len() && expanded[run_end].1.is_none() {
2005
                            run_end += 1;
2005
                        }
5
                        let prev_pos = if run_start > 0 {
5
                            pos_to_f32(&expanded[run_start - 1].1.unwrap())
                        } else {
                            $default_start
                        };
5
                        let next_pos = if run_end < expanded.len() {
5
                            pos_to_f32(&expanded[run_end].1.unwrap())
                        } else {
                            $default_end
                        };
5
                        let run_len = run_end - run_start;
5
                        let step = (next_pos - prev_pos) / crate::cast::usize_to_f32(run_len + 1);
2005
                        for j in 0..run_len {
2005
                            expanded[run_start + j].1 =
2005
                                Some(pos_ctor(prev_pos + step * crate::cast::usize_to_f32(j + 1)));
2005
                        }
5
                        i = run_end;
328
                    } else {
328
                        i += 1;
328
                    }
                }
169
                expanded
169
                    .into_iter()
2333
                    .map(|(color, pos)| {
2333
                        $output_stop {
2333
                            $out_field: pos.unwrap_or(pos_ctor($default_start)),
2333
                            color,
2333
                        }
2333
                    })
169
                    .collect()
380
            }
        };
    }
    impl_get_normalized_stops! {
        fn get_normalized_linear_stops(LinearColorStop) -> Vec<NormalizedLinearColorStop>,
        pos_type = PercentageValue,
        default_start = 0.0,
        default_end = 100.0,
4542
        pos_ctor = (|v| PercentageValue::new(v)),
298
        pos_to_f32 = (|p: &PercentageValue| p.normalized() * 100.0),
        output_field = offset,
    }
    impl_get_normalized_stops! {
        fn get_normalized_radial_stops(RadialColorStop) -> Vec<NormalizedRadialColorStop>,
        pos_type = AngleValue,
        default_start = 0.0,
        default_end = 360.0,
59
        pos_ctor = (|v| AngleValue::deg(v)),
40
        pos_to_f32 = (|p: &AngleValue| p.to_degrees_raw()),
        output_field = angle,
    }
    // -- Other Background Helpers --
2375
    fn parse_background_position_horizontal(
2375
        input: &str,
2375
    ) -> Result<BackgroundPositionHorizontal, CssPixelValueParseError<'_>> {
2375
        Ok(match input {
2375
            "left" => BackgroundPositionHorizontal::Left,
357
            "center" => BackgroundPositionHorizontal::Center,
348
            "right" => BackgroundPositionHorizontal::Right,
341
            other => BackgroundPositionHorizontal::Exact(parse_pixel_value(other)?),
        })
2375
    }
2269
    fn parse_background_position_vertical(
2269
        input: &str,
2269
    ) -> Result<BackgroundPositionVertical, CssPixelValueParseError<'_>> {
2269
        Ok(match input {
2269
            "top" => BackgroundPositionVertical::Top,
252
            "center" => BackgroundPositionVertical::Center,
246
            "bottom" => BackgroundPositionVertical::Bottom,
240
            other => BackgroundPositionVertical::Exact(parse_pixel_value(other)?),
        })
2269
    }
330
    fn parse_shape(input: &str) -> Result<Shape, CssShapeParseError<'_>> {
330
        match input.trim() {
330
            "circle" => Ok(Shape::Circle),
300
            "ellipse" => Ok(Shape::Ellipse),
296
            _ => Err(CssShapeParseError::ShapeErr(InvalidValueErr(input))),
        }
330
    }
219
    fn parse_radial_gradient_size(
219
        input: &str,
219
    ) -> Result<RadialGradientSize, InvalidValueErr<'_>> {
219
        match input.trim() {
219
            "closest-side" => Ok(RadialGradientSize::ClosestSide),
215
            "closest-corner" => Ok(RadialGradientSize::ClosestCorner),
213
            "farthest-side" => Ok(RadialGradientSize::FarthestSide),
210
            "farthest-corner" => Ok(RadialGradientSize::FarthestCorner),
206
            _ => Err(InvalidValueErr(input)),
        }
219
    }
    /// Adversarial tests. Lives inside `mod parser` (not at file scope) because
    /// the interesting helpers -- `parse_gradient`, `split_color_and_offsets`,
    /// `try_split_last_offset`, `is_likely_offset`, `parse_conic_first_item`,
    /// `parse_shape`, ... -- are private to this module.
    #[cfg(test)]
    #[allow(
        clippy::float_cmp,
        clippy::too_many_lines,
        clippy::unreadable_literal,
        clippy::cognitive_complexity,
        clippy::wildcard_imports
    )]
    mod autotest_generated {
        // `super::*` = the private parser helpers under test; the second glob pulls in
        // the value/error types from the enclosing `background` module.
        use super::*;
        use crate::props::style::background::*;
        use crate::{
            props::basic::{
                angle::CssAngleValueParseError,
                color::{CssColorParseError, OptionColorU, SystemColorRef},
                direction::CssDirectionParseError,
                error::InvalidValueErr,
                length::PercentageParseError,
                parse::{CssImageParseError, ParenthesisParseError},
                pixel::CssPixelValueParseError,
            },
            system::SystemColors,
        };
        use alloc::{string::ToString, vec::Vec};
        // ---------------------------------------------------------------
        // fixtures
        // ---------------------------------------------------------------
        /// Inputs that every `&str` parser in this file is swept over: empty,
        /// whitespace, garbage, boundary numbers, unbalanced braces, unicode.
        const ADVERSARIAL: &[&str] = &[
            "",
            " ",
            "   ",
            "\t\n\r",
            "\u{0}",
            "!!!",
            ";",
            ",",
            ",,",
            "(",
            ")",
            "()",
            "((((",
            "0",
            "-0",
            "+0",
            "NaN",
            "nan",
            "inf",
            "-inf",
            "1e40",
            "-1e40",
            "1e-45",
            "3.4028235e38",
            "9223372036854775807",
            "-9223372036854775808",
            "\u{1F600}",
            "e\u{0301}\u{0301}\u{0301}",
            "\u{00a0}",
            "red\u{00a0}50%",
            "  valid  ",
            "valid;garbage",
            "red;blue",
            "linear-gradient",
            "linear-gradient(",
            "linear-gradient()",
            "url(",
            "url()",
            "rgba(",
            "rgb(0,0,0",
            "to right",
            "circle",
            "from",
        ];
        const ALL_SYSTEM_REFS: [SystemColorRef; 9] = [
            SystemColorRef::Text,
            SystemColorRef::Background,
            SystemColorRef::Accent,
            SystemColorRef::AccentText,
            SystemColorRef::ButtonFace,
            SystemColorRef::ButtonText,
            SystemColorRef::WindowBackground,
            SystemColorRef::SelectionBackground,
            SystemColorRef::SelectionText,
        ];
        const ALL_GRADIENT_TYPES: [GradientType; 6] = [
            GradientType::LinearGradient,
            GradientType::RepeatingLinearGradient,
            GradientType::RadialGradient,
            GradientType::RepeatingRadialGradient,
            GradientType::ConicGradient,
            GradientType::RepeatingConicGradient,
        ];
        fn blue() -> ColorU {
            ColorU::new_rgb(0, 0, 255)
        }
        fn linear(input: &str) -> LinearGradient {
            match parse_style_background_content(input) {
                Ok(StyleBackgroundContent::LinearGradient(g)) => g,
                other => panic!("expected a linear gradient for {input:?}, got {other:?}"),
            }
        }
        fn radial(input: &str) -> RadialGradient {
            match parse_style_background_content(input) {
                Ok(StyleBackgroundContent::RadialGradient(g)) => g,
                other => panic!("expected a radial gradient for {input:?}, got {other:?}"),
            }
        }
        fn conic(input: &str) -> ConicGradient {
            match parse_style_background_content(input) {
                Ok(StyleBackgroundContent::ConicGradient(g)) => g,
                other => panic!("expected a conic gradient for {input:?}, got {other:?}"),
            }
        }
        /// Offsets of a linear/radial gradient, in percent.
        fn offsets(stops: &NormalizedLinearColorStopVec) -> Vec<f32> {
            stops
                .iter()
                .map(|s| s.offset.normalized() * 100.0)
                .collect()
        }
        // ---------------------------------------------------------------
        // serializers: Shape::fmt / RadialGradientSize::fmt
        // ---------------------------------------------------------------
        #[test]
        fn autotest_shape_display_is_exact_and_never_empty() {
            assert_eq!(Shape::Ellipse.to_string(), "ellipse");
            assert_eq!(Shape::Circle.to_string(), "circle");
            assert_eq!(Shape::default(), Shape::Ellipse);
            assert_eq!(Shape::default().to_string(), "ellipse");
            for s in [Shape::Ellipse, Shape::Circle] {
                assert!(!s.to_string().is_empty());
                // The serialized form is a valid input for the parser.
                assert_eq!(parse_shape(&s.to_string()).unwrap(), s);
            }
        }
        #[test]
        fn autotest_radial_gradient_size_display_is_exact_and_never_empty() {
            assert_eq!(RadialGradientSize::ClosestSide.to_string(), "closest-side");
            assert_eq!(
                RadialGradientSize::ClosestCorner.to_string(),
                "closest-corner"
            );
            assert_eq!(
                RadialGradientSize::FarthestSide.to_string(),
                "farthest-side"
            );
            assert_eq!(
                RadialGradientSize::FarthestCorner.to_string(),
                "farthest-corner"
            );
            assert_eq!(
                RadialGradientSize::default(),
                RadialGradientSize::FarthestCorner
            );
            for s in [
                RadialGradientSize::ClosestSide,
                RadialGradientSize::ClosestCorner,
                RadialGradientSize::FarthestSide,
                RadialGradientSize::FarthestCorner,
            ] {
                assert!(!s.to_string().is_empty());
                assert_eq!(parse_radial_gradient_size(&s.to_string()).unwrap(), s);
            }
        }
        // ---------------------------------------------------------------
        // constructors: Normalized{Linear,Radial}ColorStop::new
        // ---------------------------------------------------------------
        #[test]
        fn autotest_normalized_linear_stop_new_keeps_its_arguments() {
            let stop = NormalizedLinearColorStop::new(PercentageValue::new(42.5), ColorU::RED);
            assert_eq!(stop.offset.normalized() * 100.0, 42.5);
            assert_eq!(stop.color, ColorOrSystem::Color(ColorU::RED));
            // Extreme offsets must not panic, and must stay finite: FloatValue
            // encodes f32*1000 into an isize, and `as` saturates (NaN -> 0).
            for f in [
                0.0_f32,
                -0.0,
                f32::NAN,
                f32::INFINITY,
                f32::NEG_INFINITY,
                f32::MAX,
                f32::MIN,
                f32::MIN_POSITIVE,
                -100.0,
                1e30,
            ] {
                let stop = NormalizedLinearColorStop::new(
                    PercentageValue::new(f),
                    ColorU::TRANSPARENT,
                );
                assert!(
                    stop.offset.normalized().is_finite(),
                    "offset went non-finite for {f}"
                );
                assert_eq!(stop.color, ColorOrSystem::Color(ColorU::TRANSPARENT));
            }
            // NaN is flushed to 0, not propagated.
            assert_eq!(
                NormalizedLinearColorStop::new(PercentageValue::new(f32::NAN), ColorU::RED).offset,
                PercentageValue::new(0.0)
            );
        }
        #[test]
        fn autotest_normalized_radial_stop_new_keeps_its_arguments() {
            let stop = NormalizedRadialColorStop::new(AngleValue::deg(90.0), ColorU::RED);
            assert_eq!(stop.angle, AngleValue::deg(90.0));
            assert_eq!(stop.angle.to_degrees_raw(), 90.0);
            assert_eq!(stop.color, ColorOrSystem::Color(ColorU::RED));
            for f in [
                0.0_f32,
                -0.0,
                f32::NAN,
                f32::INFINITY,
                f32::NEG_INFINITY,
                f32::MAX,
                f32::MIN,
                720.0,
                -360.0,
            ] {
                let stop = NormalizedRadialColorStop::new(AngleValue::deg(f), ColorU::WHITE);
                assert!(
                    stop.angle.to_degrees_raw().is_finite(),
                    "angle went non-finite for {f}"
                );
                assert_eq!(stop.color, ColorOrSystem::Color(ColorU::WHITE));
            }
            assert_eq!(
                NormalizedRadialColorStop::new(AngleValue::deg(f32::NAN), ColorU::RED).angle,
                AngleValue::deg(0.0)
            );
        }
        // ---------------------------------------------------------------
        // Normalized{Linear,Radial}ColorStop::resolve
        // ---------------------------------------------------------------
        #[test]
        fn autotest_resolve_concrete_color_ignores_system_colors() {
            let stop = NormalizedLinearColorStop::new(PercentageValue::new(0.0), ColorU::RED);
            assert_eq!(stop.resolve(&SystemColors::default(), ColorU::WHITE), ColorU::RED);
            let populated = SystemColors {
                accent: OptionColorU::Some(ColorU::new_rgb(1, 2, 3)),
                ..SystemColors::default()
            };
            assert_eq!(stop.resolve(&populated, ColorU::WHITE), ColorU::RED);
            let rstop = NormalizedRadialColorStop::new(AngleValue::deg(0.0), ColorU::RED);
            assert_eq!(rstop.resolve(&populated, ColorU::WHITE), ColorU::RED);
        }
        #[test]
        fn autotest_resolve_system_stop_falls_back_for_every_variant() {
            let fallback = ColorU::rgba(9, 8, 7, 6);
            for r in ALL_SYSTEM_REFS {
                let lin = NormalizedLinearColorStop {
                    offset: PercentageValue::new(50.0),
                    color: ColorOrSystem::System(r),
                };
                let rad = NormalizedRadialColorStop {
                    angle: AngleValue::deg(180.0),
                    color: ColorOrSystem::System(r),
                };
                // Nothing is populated -> every variant resolves to the fallback.
                assert_eq!(lin.resolve(&SystemColors::default(), fallback), fallback);
                assert_eq!(rad.resolve(&SystemColors::default(), fallback), fallback);
            }
            // A populated key resolves; the others still fall back.
            let accent = ColorU::new_rgb(0, 122, 255);
            let populated = SystemColors {
                accent: OptionColorU::Some(accent),
                ..SystemColors::default()
            };
            let stop = NormalizedLinearColorStop {
                offset: PercentageValue::new(0.0),
                color: ColorOrSystem::System(SystemColorRef::Accent),
            };
            assert_eq!(stop.resolve(&populated, fallback), accent);
            let other = NormalizedLinearColorStop {
                offset: PercentageValue::new(0.0),
                color: ColorOrSystem::System(SystemColorRef::ButtonText),
            };
            assert_eq!(other.resolve(&populated, fallback), fallback);
        }
        // ---------------------------------------------------------------
        // numeric: scale_for_dpi
        // ---------------------------------------------------------------
        #[test]
        fn autotest_background_position_horizontal_scale_for_dpi() {
            // Keywords are immune to scaling, for *any* factor.
            for f in [0.0_f32, 1.0, -1.0, f32::NAN, f32::INFINITY, f32::MIN, f32::MAX] {
                for keyword in [
                    BackgroundPositionHorizontal::Left,
                    BackgroundPositionHorizontal::Center,
                    BackgroundPositionHorizontal::Right,
                ] {
                    let mut k = keyword;
                    k.scale_for_dpi(f);
                    assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
                }
            }
            let mut exact = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
            exact.scale_for_dpi(2.0);
            assert_eq!(exact, BackgroundPositionHorizontal::Exact(PixelValue::px(20.0)));
            // zero, negative
            let mut zeroed = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
            zeroed.scale_for_dpi(0.0);
            assert_eq!(zeroed, BackgroundPositionHorizontal::Exact(PixelValue::px(0.0)));
            let mut negated = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
            negated.scale_for_dpi(-1.0);
            assert_eq!(
                negated,
                BackgroundPositionHorizontal::Exact(PixelValue::px(-10.0))
            );
            // NaN is flushed to 0 by the isize cast, never propagated.
            let mut nan = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
            nan.scale_for_dpi(f32::NAN);
            assert_eq!(nan, BackgroundPositionHorizontal::Exact(PixelValue::px(0.0)));
            // +-inf and MIN/MAX saturate to the isize bounds -- finite, no panic.
            for f in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
                let mut v = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
                v.scale_for_dpi(f);
                let BackgroundPositionHorizontal::Exact(px) = v else {
                    panic!("variant changed under scaling");
                };
                assert!(px.number.get().is_finite(), "non-finite result for {f}");
                assert_eq!(px.number.get().is_sign_negative(), f.is_sign_negative());
            }
        }
        #[test]
        fn autotest_background_position_vertical_scale_for_dpi() {
            for f in [0.0_f32, 1.0, -1.0, f32::NAN, f32::INFINITY, f32::MIN, f32::MAX] {
                for keyword in [
                    BackgroundPositionVertical::Top,
                    BackgroundPositionVertical::Center,
                    BackgroundPositionVertical::Bottom,
                ] {
                    let mut k = keyword;
                    k.scale_for_dpi(f);
                    assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
                }
            }
            let mut exact = BackgroundPositionVertical::Exact(PixelValue::em(4.0));
            exact.scale_for_dpi(0.5);
            assert_eq!(exact, BackgroundPositionVertical::Exact(PixelValue::em(2.0)));
            let mut nan = BackgroundPositionVertical::Exact(PixelValue::px(10.0));
            nan.scale_for_dpi(f32::NAN);
            assert_eq!(nan, BackgroundPositionVertical::Exact(PixelValue::px(0.0)));
            // Saturation is a fixed point: scaling an already-saturated value again
            // must not wrap around into a negative number.
            let mut saturated = BackgroundPositionVertical::Exact(PixelValue::px(f32::MAX));
            saturated.scale_for_dpi(f32::MAX);
            let once = saturated;
            saturated.scale_for_dpi(f32::MAX);
            assert_eq!(saturated, once);
            let BackgroundPositionVertical::Exact(px) = saturated else {
                panic!("variant changed under scaling");
            };
            assert!(px.number.get() > 0.0);
            assert!(px.number.get().is_finite());
        }
        #[test]
        fn autotest_style_background_position_scale_for_dpi_scales_both_axes() {
            let mut pos = StyleBackgroundPosition {
                horizontal: BackgroundPositionHorizontal::Exact(PixelValue::px(10.0)),
                vertical: BackgroundPositionVertical::Exact(PixelValue::px(20.0)),
            };
            pos.scale_for_dpi(3.0);
            assert_eq!(
                pos.horizontal,
                BackgroundPositionHorizontal::Exact(PixelValue::px(30.0))
            );
            assert_eq!(
                pos.vertical,
                BackgroundPositionVertical::Exact(PixelValue::px(60.0))
            );
            // Scaling compounds -- pinned, because a double-applied DPI scale is a
            // classic layout bug.
            pos.scale_for_dpi(2.0);
            assert_eq!(
                pos.horizontal,
                BackgroundPositionHorizontal::Exact(PixelValue::px(60.0))
            );
            // The all-keyword default is a fixed point for every factor.
            for f in [0.0_f32, 1.0, -2.5, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
                let mut default = StyleBackgroundPosition::default();
                default.scale_for_dpi(f);
                assert_eq!(default, StyleBackgroundPosition::default());
            }
        }
        #[test]
        fn autotest_style_background_size_scale_for_dpi() {
            // Contain / Cover carry no number and must survive any factor.
            for f in [0.0_f32, 2.0, -1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
                for keyword in [StyleBackgroundSize::Contain, StyleBackgroundSize::Cover] {
                    let mut k = keyword;
                    k.scale_for_dpi(f);
                    assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
                }
            }
            let mut size = StyleBackgroundSize::ExactSize(PixelValueSize {
                width: PixelValue::px(10.0),
                height: PixelValue::percent(50.0),
            });
            size.scale_for_dpi(2.0);
            assert_eq!(
                size,
                StyleBackgroundSize::ExactSize(PixelValueSize {
                    width: PixelValue::px(20.0),
                    // NOTE: percentages are scaled too, which is arguably wrong for a
                    // DPI change -- pinned as current behaviour.
                    height: PixelValue::percent(100.0),
                })
            );
            let mut nan = StyleBackgroundSize::ExactSize(PixelValueSize {
                width: PixelValue::px(10.0),
                height: PixelValue::px(20.0),
            });
            nan.scale_for_dpi(f32::NAN);
            assert_eq!(
                nan,
                StyleBackgroundSize::ExactSize(PixelValueSize {
                    width: PixelValue::px(0.0),
                    height: PixelValue::px(0.0),
                })
            );
            let mut inf = StyleBackgroundSize::ExactSize(PixelValueSize {
                width: PixelValue::px(1.0),
                height: PixelValue::px(-1.0),
            });
            inf.scale_for_dpi(f32::INFINITY);
            let StyleBackgroundSize::ExactSize(s) = inf else {
                panic!("variant changed under scaling");
            };
            assert!(s.width.number.get().is_finite() && s.width.number.get() > 0.0);
            assert!(s.height.number.get().is_finite() && s.height.number.get() < 0.0);
        }
        // ---------------------------------------------------------------
        // getters: to_contained / to_shared round-trips
        // ---------------------------------------------------------------
        #[test]
        fn autotest_css_background_parse_error_round_trips() {
            let errors = [
                CssBackgroundParseError::Error(""),
                CssBackgroundParseError::Error("boom \u{1F600}"),
                CssBackgroundParseError::InvalidBackground(ParenthesisParseError::EmptyInput),
                CssBackgroundParseError::InvalidBackground(
                    ParenthesisParseError::StopWordNotFound("nope"),
                ),
                CssBackgroundParseError::UnclosedGradient(""),
                CssBackgroundParseError::NoDirection("nodir"),
                CssBackgroundParseError::TooFewGradientStops("few"),
                CssBackgroundParseError::DirectionParseError(CssDirectionParseError::Error("d")),
                CssBackgroundParseError::DirectionParseError(
                    CssDirectionParseError::InvalidArguments("args"),
                ),
                CssBackgroundParseError::GradientParseError(CssGradientStopParseError::Error("g")),
                CssBackgroundParseError::ConicGradient(CssConicGradientParseError::NoAngle("a")),
                CssBackgroundParseError::ShapeParseError(CssShapeParseError::ShapeErr(
                    InvalidValueErr("s"),
                )),
                CssBackgroundParseError::ImageParseError(CssImageParseError::UnclosedQuotes("q")),
                CssBackgroundParseError::ColorParseError(CssColorParseError::InvalidColor("c")),
                CssBackgroundParseError::ColorParseError(CssColorParseError::EmptyInput),
            ];
            for e in &errors {
                let owned = e.to_contained();
                assert_eq!(&owned.to_shared(), e, "round-trip changed {e:?}");
                // Display must survive the round-trip as well.
                assert_eq!(
                    alloc::format!("{}", owned.to_shared()),
                    alloc::format!("{e}")
                );
            }
        }
        #[test]
        fn autotest_error_round_trip_survives_huge_and_unicode_payloads() {
            let huge = "x".repeat(100_000);
            let weird = "\u{1F600}\u{0}\u{00a0}e\u{0301}";
            for s in [huge.as_str(), weird, "", " "] {
                let e = CssBackgroundParseError::UnclosedGradient(s);
                assert_eq!(e.to_contained().to_shared(), e);
                let e = CssGradientStopParseError::Error(s);
                assert_eq!(e.to_contained().to_shared(), e);
                let e = CssConicGradientParseError::NoAngle(s);
                assert_eq!(e.to_contained().to_shared(), e);
                let e = CssShapeParseError::ShapeErr(InvalidValueErr(s));
                assert_eq!(e.to_contained().to_shared(), e);
                let e = CssBackgroundPositionParseError::NoPosition(s);
                assert_eq!(e.to_contained().to_shared(), e);
            }
        }
        #[test]
        fn autotest_css_gradient_stop_parse_error_round_trips() {
            let errors = [
                CssGradientStopParseError::Error("boom"),
                CssGradientStopParseError::Percentage(PercentageParseError::NoPercentSign),
                CssGradientStopParseError::Percentage(PercentageParseError::InvalidUnit(
                    "px".to_string().into(),
                )),
                CssGradientStopParseError::Angle(CssAngleValueParseError::EmptyString),
                CssGradientStopParseError::Angle(CssAngleValueParseError::InvalidAngle("q")),
                CssGradientStopParseError::ColorParseError(CssColorParseError::InvalidColor("c")),
            ];
            for e in &errors {
                assert_eq!(&e.to_contained().to_shared(), e, "round-trip changed {e:?}");
            }
        }
        #[test]
        fn autotest_css_conic_and_shape_parse_error_round_trip() {
            let errors = [
                CssConicGradientParseError::NoAngle("n"),
                CssConicGradientParseError::Angle(CssAngleValueParseError::EmptyString),
                CssConicGradientParseError::Position(
                    CssBackgroundPositionParseError::NoPosition("p"),
                ),
            ];
            for e in &errors {
                assert_eq!(&e.to_contained().to_shared(), e);
            }
            let shape = CssShapeParseError::ShapeErr(InvalidValueErr("blob"));
            assert_eq!(shape.to_contained().to_shared(), shape);
        }
        #[test]
        fn autotest_css_background_position_parse_error_round_trips() {
            let errors = [
                CssBackgroundPositionParseError::NoPosition(""),
                CssBackgroundPositionParseError::TooManyComponents("a b c"),
                CssBackgroundPositionParseError::FirstComponentWrong(
                    CssPixelValueParseError::EmptyString,
                ),
                CssBackgroundPositionParseError::FirstComponentWrong(
                    CssPixelValueParseError::InvalidPixelValue("q"),
                ),
                CssBackgroundPositionParseError::SecondComponentWrong(
                    CssPixelValueParseError::InvalidPixelValue("\u{1F600}"),
                ),
            ];
            for e in &errors {
                assert_eq!(&e.to_contained().to_shared(), e, "round-trip changed {e:?}");
            }
        }
        #[test]
        fn autotest_real_parse_errors_round_trip_through_the_owned_form() {
            // Errors as actually produced by the parsers, not hand-built ones.
            for input in ADVERSARIAL {
                if let Err(e) = parse_style_background_content(input) {
                    assert_eq!(e.to_contained().to_shared(), e, "for input {input:?}");
                }
                if let Err(e) = parse_style_background_position(input) {
                    assert_eq!(e.to_contained().to_shared(), e, "for input {input:?}");
                }
            }
        }
        // ---------------------------------------------------------------
        // GradientType::get_extend_mode (private)
        // ---------------------------------------------------------------
        #[test]
        fn autotest_get_extend_mode_is_repeat_exactly_for_the_repeating_variants() {
            assert_eq!(
                GradientType::LinearGradient.get_extend_mode(),
                ExtendMode::Clamp
            );
            assert_eq!(
                GradientType::RadialGradient.get_extend_mode(),
                ExtendMode::Clamp
            );
            assert_eq!(
                GradientType::ConicGradient.get_extend_mode(),
                ExtendMode::Clamp
            );
            assert_eq!(
                GradientType::RepeatingLinearGradient.get_extend_mode(),
                ExtendMode::Repeat
            );
            assert_eq!(
                GradientType::RepeatingRadialGradient.get_extend_mode(),
                ExtendMode::Repeat
            );
            assert_eq!(
                GradientType::RepeatingConicGradient.get_extend_mode(),
                ExtendMode::Repeat
            );
            // Total + pure: same input, same answer.
            for t in ALL_GRADIENT_TYPES {
                assert_eq!(t.get_extend_mode(), t.get_extend_mode());
            }
            assert_eq!(ExtendMode::default(), ExtendMode::Clamp);
        }
        // ---------------------------------------------------------------
        // parser: parse_style_background_content
        // ---------------------------------------------------------------
        #[test]
        fn autotest_background_content_never_panics_and_is_deterministic() {
            for input in ADVERSARIAL {
                let a = parse_style_background_content(input);
                let b = parse_style_background_content(input);
                assert_eq!(a, b, "non-deterministic for {input:?}");
            }
        }
        #[test]
        fn autotest_background_content_rejects_empty_whitespace_and_garbage() {
            for input in ["", " ", "   ", "\t\n\r", "\u{0}", "!!!", ";", "valid;garbage"] {
                assert!(
                    parse_style_background_content(input).is_err(),
                    "{input:?} should not parse as a background"
                );
            }
        }
        #[test]
        fn autotest_background_content_valid_minimal_positive_controls() {
            assert_eq!(
                parse_style_background_content("red").unwrap(),
                StyleBackgroundContent::Color(ColorU::RED)
            );
            // Leading/trailing whitespace is trimmed, not rejected.
            assert_eq!(
                parse_style_background_content("  red  ").unwrap(),
                StyleBackgroundContent::Color(ColorU::RED)
            );
            assert_eq!(
                parse_style_background_content("system:accent").unwrap(),
                StyleBackgroundContent::SystemColor(SystemColorRef::Accent)
            );
            assert_eq!(
                parse_style_background_content("url(a.png)").unwrap(),
                StyleBackgroundContent::Image("a.png".into())
            );
        }
        #[test]
        fn autotest_background_content_unicode_is_rejected_without_panicking() {
            for input in [
                "\u{1F600}",
                "url(\u{1F600}.png)",
                "linear-gradient(\u{1F600}, red)",
                "e\u{0301}\u{0301}\u{0301}",
                "\u{00a0}",
            ] {
                let parsed = parse_style_background_content(input);
                // url() accepts any payload; everything else must be an error.
                if input.starts_with("url(") {
                    assert!(parsed.is_ok());
                } else {
                    assert!(parsed.is_err(), "{input:?} unexpectedly parsed");
                }
            }
        }
        #[test]
        fn autotest_background_content_extremely_long_input_terminates() {
            let huge = "a".repeat(100_000);
            assert!(parse_style_background_content(&huge).is_err());
            let huge_gradient =
                alloc::format!("linear-gradient({})", "red, ".repeat(2_000) + "blue");
            let g = linear(&huge_gradient);
            assert_eq!(g.stops.len(), 2_001);
            let huge_url = alloc::format!("url({})", "a".repeat(100_000));
            assert!(matches!(
                parse_style_background_content(&huge_url),
                Ok(StyleBackgroundContent::Image(_))
            ));
        }
        #[test]
        fn autotest_background_content_deep_nesting_does_not_stack_overflow() {
            let nested = alloc::format!(
                "linear-gradient({}red{})",
                "(".repeat(10_000),
                ")".repeat(10_000)
            );
            assert!(parse_style_background_content(&nested).is_err());
            let unbalanced = alloc::format!("linear-gradient({}", "(".repeat(10_000));
            assert!(parse_style_background_content(&unbalanced).is_err());
        }
        #[test]
        fn autotest_unclosed_gradient_reports_a_color_error_not_unclosed_gradient() {
            // parse_parentheses fails (no ')'), so the input falls through to the
            // color branch -- the `UnclosedGradient` variant is never produced here.
            let err = parse_style_background_content("linear-gradient(red, blue").unwrap_err();
            assert!(
                matches!(err, CssBackgroundParseError::ColorParseError(_)),
                "got {err:?}"
            );
        }
        #[test]
        fn autotest_empty_gradient_body_is_a_no_direction_error() {
            let err = parse_style_background_content("linear-gradient()").unwrap_err();
            assert!(matches!(err, CssBackgroundParseError::NoDirection(_)), "got {err:?}");
            for f in [
                "radial-gradient()",
                "conic-gradient()",
                "repeating-linear-gradient()",
            ] {
                assert!(parse_style_background_content(f).is_err(), "{f:?}");
            }
        }
        #[test]
        fn autotest_url_with_empty_payload_is_accepted_as_an_empty_image() {
            // Pinned: `url()` yields an empty image id rather than an error.
            assert_eq!(
                parse_style_background_content("url()").unwrap(),
                StyleBackgroundContent::Image("".into())
            );
        }
        #[test]
        fn autotest_gradient_boundary_number_directions_stay_finite() {
            // "NaN" parses as a bare number -> a NaN angle, which the isize cast
            // flushes to 0deg. Pinned: it is silently accepted, not rejected.
            let g = linear("linear-gradient(NaN, red, blue)");
            assert_eq!(g.direction, Direction::Angle(AngleValue::deg(0.0)));
            // Overflowing / tiny literals must not panic and must stay finite.
            for input in [
                "linear-gradient(0deg, red, blue)",
                "linear-gradient(-0deg, red, blue)",
                "linear-gradient(1e40deg, red, blue)",
                "linear-gradient(-1e40deg, red, blue)",
                "linear-gradient(1e-45deg, red, blue)",
                "linear-gradient(inf, red, blue)",
                "linear-gradient(-inf, red, blue)",
                "linear-gradient(9223372036854775807deg, red, blue)",
            ] {
                let g = linear(input);
                let Direction::Angle(a) = g.direction else {
                    panic!("expected an angle direction for {input:?}");
                };
                assert!(a.to_degrees_raw().is_finite(), "non-finite angle for {input:?}");
                assert_eq!(g.stops.len(), 2, "{input:?}");
            }
        }
        #[test]
        fn autotest_gradient_stop_offsets_are_monotonic_and_finite() {
            for input in [
                "linear-gradient(red, blue)",
                "linear-gradient(red, green, blue)",
                "linear-gradient(red 50%, blue 20%)",
                "linear-gradient(red -50%, blue)",
                "linear-gradient(red 0%, yellow, green, blue 100%)",
                "linear-gradient(red 10% 30%, blue)",
                "linear-gradient(red 200%, blue 10%)",
                "repeating-linear-gradient(red, blue 20%)",
                "radial-gradient(circle, red, blue)",
            ] {
                let content = parse_style_background_content(input).unwrap();
                let stops = match &content {
                    StyleBackgroundContent::LinearGradient(g) => &g.stops,
                    StyleBackgroundContent::RadialGradient(g) => &g.stops,
                    other => panic!("unexpected content {other:?}"),
                };
                let mut prev = f32::NEG_INFINITY;
                for o in offsets(stops) {
                    assert!(o.is_finite(), "non-finite offset in {input:?}");
                    assert!(o >= prev, "offsets not monotonic in {input:?}: {o} < {prev}");
                    prev = o;
                }
            }
        }
        #[test]
        fn autotest_negative_and_overflowing_stop_offsets_are_clamped() {
            // A negative first offset is clamped to the running maximum (0%).
            let g = linear("linear-gradient(red -50%, blue)");
            assert_eq!(offsets(&g.stops), alloc::vec![0.0, 100.0]);
            // An out-of-range offset is *not* clamped down to 100% -- the later
            // stop is dragged up to it instead.
            let g = linear("linear-gradient(red 200%, blue 10%)");
            assert_eq!(offsets(&g.stops), alloc::vec![200.0, 200.0]);
        }
        #[test]
        fn autotest_offsets_that_are_not_percentages_are_rejected() {
            // "50px" looks like an offset (is_likely_offset), but a linear stop
            // offset must be a percentage -> hard error, no silent fallback.
            let err = parse_style_background_content("linear-gradient(red 50px, blue)").unwrap_err();
            assert!(
                matches!(
                    err,
                    CssBackgroundParseError::GradientParseError(
                        CssGradientStopParseError::Percentage(_)
                    )
                ),
                "got {err:?}"
            );
            // A bare number is *not* recognised as an offset at all, so the whole
            // token is treated as part of the color and fails to parse.
            assert!(parse_style_background_content("linear-gradient(red 0.5, blue)").is_err());
            // Neither is "NaN%" (no ASCII digit).
            assert!(parse_style_background_content("linear-gradient(red NaN%, blue)").is_err());
        }
        #[test]
        fn autotest_huge_stop_offsets_do_not_produce_nan_or_inf() {
            let g = linear("linear-gradient(red 1e40%, blue)");
            assert_eq!(g.stops.len(), 2);
            for o in offsets(&g.stops) {
                assert!(o.is_finite(), "offset leaked a non-finite value: {o}");
            }
        }
        // ---------------------------------------------------------------
        // parser: parse_style_background_content_multiple
        // ---------------------------------------------------------------
        #[test]
        fn autotest_background_content_multiple_empty_input_yields_an_empty_vec() {
            // Pinned: empty input is *not* an error -- split_string_respect_comma
            // returns no items, so the result is an empty layer list.
            let parsed = parse_style_background_content_multiple("").unwrap();
            assert_eq!(parsed.len(), 0);
            // Whitespace-only *is* an error (one empty item that fails to parse).
            assert!(parse_style_background_content_multiple("   ").is_err());
            assert!(parse_style_background_content_multiple(",").is_err());
            assert!(parse_style_background_content_multiple("red,,blue").is_err());
        }
        #[test]
        fn autotest_background_content_multiple_valid_and_adversarial() {
            let parsed =
                parse_style_background_content_multiple("linear-gradient(red, blue), url(a.png)")
                    .unwrap();
            assert_eq!(parsed.len(), 2);
            assert!(matches!(
                parsed.as_slice()[0],
                StyleBackgroundContent::LinearGradient(_)
            ));
            assert!(matches!(
                parsed.as_slice()[1],
                StyleBackgroundContent::Image(_)
            ));
            // One bad layer poisons the whole list.
            assert!(parse_style_background_content_multiple("red, !!!").is_err());
            // Long repeated input terminates.
            let many = "red,".repeat(2_000) + "blue";
            assert_eq!(
                parse_style_background_content_multiple(&many).unwrap().len(),
                2_001
            );
            for input in ADVERSARIAL {
                let a = parse_style_background_content_multiple(input);
                let b = parse_style_background_content_multiple(input);
                assert_eq!(a, b, "non-deterministic for {input:?}");
            }
        }
        // ---------------------------------------------------------------
        // parser: parse_style_background_position(_multiple)
        // ---------------------------------------------------------------
        #[test]
        fn autotest_background_position_empty_and_whitespace() {
            assert_eq!(
                parse_style_background_position(""),
                Err(CssBackgroundPositionParseError::NoPosition(""))
            );
            assert_eq!(
                parse_style_background_position("   "),
                Err(CssBackgroundPositionParseError::NoPosition(""))
            );
            assert_eq!(
                parse_style_background_position("\t\n\r"),
                Err(CssBackgroundPositionParseError::NoPosition(""))
            );
        }
        #[test]
        fn autotest_background_position_valid_minimal_and_keyword_order() {
            let p = parse_style_background_position("left").unwrap();
            assert_eq!(p.horizontal, BackgroundPositionHorizontal::Left);
            assert_eq!(p.vertical, BackgroundPositionVertical::Center);
            // A lone vertical keyword also works: the horizontal falls back to center.
            let p = parse_style_background_position("top").unwrap();
            assert_eq!(p.horizontal, BackgroundPositionHorizontal::Center);
            assert_eq!(p.vertical, BackgroundPositionVertical::Top);
            // Either order is accepted for keyword pairs.
            assert_eq!(
                parse_style_background_position("left top").unwrap(),
                parse_style_background_position("top left").unwrap()
            );
            // ... but "left right" is a vertical-slot error, not silently accepted.
            assert!(matches!(
                parse_style_background_position("left right"),
                Err(CssBackgroundPositionParseError::SecondComponentWrong(_))
            ));
        }
        #[test]
        fn autotest_background_position_too_many_components() {
            assert!(matches!(
                parse_style_background_position("left 10px top 20px"),
                Err(CssBackgroundPositionParseError::TooManyComponents(_))
            ));
            assert!(matches!(
                parse_style_background_position("a b c"),
                Err(CssBackgroundPositionParseError::TooManyComponents(_))
            ));
        }
        #[test]
        fn autotest_background_position_boundary_numbers_are_accepted_and_saturate() {
            // Pinned: parse_pixel_value accepts a bare number as px -- so "NaN" and
            // "inf" are *valid* background positions, flushed to 0 / saturated.
            assert_eq!(
                parse_style_background_position("NaN").unwrap().horizontal,
                BackgroundPositionHorizontal::Exact(PixelValue::px(0.0))
            );
            assert_eq!(
                parse_style_background_position("-0").unwrap().horizontal,
                BackgroundPositionHorizontal::Exact(PixelValue::px(0.0))
            );
            assert_eq!(
                parse_style_background_position("inf").unwrap().horizontal,
                BackgroundPositionHorizontal::Exact(PixelValue::px(f32::INFINITY))
            );
            for input in ["0", "1e40px", "-1e40px", "1e-45px", "3.4028235e38px"] {
                let p = parse_style_background_position(input).unwrap();
                let BackgroundPositionHorizontal::Exact(px) = p.horizontal else {
                    panic!("expected an exact value for {input:?}");
                };
                assert!(px.number.get().is_finite(), "non-finite for {input:?}");
            }
        }
        #[test]
        fn autotest_background_position_garbage_unicode_and_long_input() {
            for input in ["garbage", "!!!", "\u{1F600}", "e\u{0301}", "left;top"] {
                assert!(
                    parse_style_background_position(input).is_err(),
                    "{input:?} unexpectedly parsed"
                );
            }
            let huge = "a".repeat(100_000);
            assert!(parse_style_background_position(&huge).is_err());
            let nested = "(".repeat(10_000);
            assert!(parse_style_background_position(&nested).is_err());
            for input in ADVERSARIAL {
                let a = parse_style_background_position(input);
                let b = parse_style_background_position(input);
                assert_eq!(a, b, "non-deterministic for {input:?}");
            }
        }
        #[test]
        fn autotest_background_position_multiple() {
            // Empty input -> empty vec (no error), same as the other *_multiple fns.
            assert_eq!(parse_style_background_position_multiple("").unwrap().len(), 0);
            let parsed = parse_style_background_position_multiple("left top, 10px 20px").unwrap();
            assert_eq!(parsed.len(), 2);
            assert_eq!(
                parsed.as_slice()[1].horizontal,
                BackgroundPositionHorizontal::Exact(PixelValue::px(10.0))
            );
            assert!(parse_style_background_position_multiple("left top, !!!").is_err());
            assert!(parse_style_background_position_multiple("   ").is_err());
            let many = "left top,".repeat(2_000) + "center";
            assert_eq!(
                parse_style_background_position_multiple(&many).unwrap().len(),
                2_001
            );
        }
        // ---------------------------------------------------------------
        // parser: parse_style_background_size(_multiple)
        // ---------------------------------------------------------------
        #[test]
        fn autotest_background_size_empty_whitespace_and_garbage() {
            for input in ["", "   ", "\t\n", "auto", "!!!", "\u{1F600}", "CONTAIN", "Cover"] {
                assert!(
                    parse_style_background_size(input).is_err(),
                    "{input:?} unexpectedly parsed"
                );
            }
            let huge = "a".repeat(100_000);
            assert!(parse_style_background_size(&huge).is_err());
        }
        #[test]
        fn autotest_background_size_valid_minimal_and_trimming() {
            assert_eq!(
                parse_style_background_size("  contain  ").unwrap(),
                StyleBackgroundSize::Contain
            );
            assert_eq!(
                parse_style_background_size("cover").unwrap(),
                StyleBackgroundSize::Cover
            );
            // A single value applies to both axes.
            assert_eq!(
                parse_style_background_size("50%").unwrap(),
                StyleBackgroundSize::ExactSize(PixelValueSize {
                    width: PixelValue::percent(50.0),
                    height: PixelValue::percent(50.0),
                })
            );
        }
        #[test]
        fn autotest_background_size_silently_ignores_extra_components() {
            // BUG-ish, pinned: unlike background-position (TooManyComponents), a third
            // component is dropped on the floor instead of being rejected.
            assert_eq!(
                parse_style_background_size("10px 20px 30px").unwrap(),
                StyleBackgroundSize::ExactSize(PixelValueSize {
                    width: PixelValue::px(10.0),
                    height: PixelValue::px(20.0),
                })
            );
        }
        #[test]
        fn autotest_background_size_boundary_numbers_saturate_without_panicking() {
            // Bare numbers parse as px, so "NaN" is accepted and flushed to 0px.
            assert_eq!(
                parse_style_background_size("NaN").unwrap(),
                StyleBackgroundSize::ExactSize(PixelValueSize {
                    width: PixelValue::px(0.0),
                    height: PixelValue::px(0.0),
                })
            );
            assert_eq!(
                parse_style_background_size("inf").unwrap(),
                StyleBackgroundSize::ExactSize(PixelValueSize {
                    width: PixelValue::px(f32::INFINITY),
                    height: PixelValue::px(f32::INFINITY),
                })
            );
            for input in ["0", "-0", "1e40px", "-1e40px", "1e-45px"] {
                let StyleBackgroundSize::ExactSize(s) =
                    parse_style_background_size(input).unwrap()
                else {
                    panic!("expected an exact size for {input:?}");
                };
                assert!(s.width.number.get().is_finite(), "non-finite for {input:?}");
                assert!(s.height.number.get().is_finite(), "non-finite for {input:?}");
            }
            for input in ADVERSARIAL {
                let a = parse_style_background_size(input);
                let b = parse_style_background_size(input);
                assert_eq!(a, b, "non-deterministic for {input:?}");
            }
        }
        #[test]
        fn autotest_background_size_multiple() {
            assert_eq!(parse_style_background_size_multiple("").unwrap().len(), 0);
            let parsed = parse_style_background_size_multiple("contain, 10px 20px, cover").unwrap();
            assert_eq!(parsed.len(), 3);
            assert_eq!(parsed.as_slice()[0], StyleBackgroundSize::Contain);
            assert_eq!(parsed.as_slice()[2], StyleBackgroundSize::Cover);
            assert!(parse_style_background_size_multiple("cover, auto").is_err());
            assert!(parse_style_background_size_multiple("   ").is_err());
            let many = "cover,".repeat(2_000) + "contain";
            assert_eq!(
                parse_style_background_size_multiple(&many).unwrap().len(),
                2_001
            );
        }
        // ---------------------------------------------------------------
        // parser: parse_style_background_repeat(_multiple)
        // ---------------------------------------------------------------
        #[test]
        fn autotest_background_repeat_valid_and_invalid() {
            assert_eq!(
                parse_style_background_repeat("  repeat  ").unwrap(),
                StyleBackgroundRepeat::PatternRepeat
            );
            assert_eq!(
                parse_style_background_repeat("no-repeat").unwrap(),
                StyleBackgroundRepeat::NoRepeat
            );
            assert_eq!(
                parse_style_background_repeat("repeat-x").unwrap(),
                StyleBackgroundRepeat::RepeatX
            );
            assert_eq!(
                parse_style_background_repeat("repeat-y").unwrap(),
                StyleBackgroundRepeat::RepeatY
            );
            assert_eq!(StyleBackgroundRepeat::default(), StyleBackgroundRepeat::PatternRepeat);
            for input in [
                "",
                "   ",
                "\t\n",
                "REPEAT",
                "Repeat",
                "repeat-xy",
                "repeat repeat",
                "!!!",
                "\u{1F600}",
                "0",
                "NaN",
            ] {
                assert!(
                    parse_style_background_repeat(input).is_err(),
                    "{input:?} unexpectedly parsed"
                );
            }
            let huge = "repeat".repeat(20_000);
            assert!(parse_style_background_repeat(&huge).is_err());
            for input in ADVERSARIAL {
                let a = parse_style_background_repeat(input);
                let b = parse_style_background_repeat(input);
                assert_eq!(a, b, "non-deterministic for {input:?}");
            }
        }
        #[test]
        fn autotest_background_repeat_multiple() {
            assert_eq!(parse_style_background_repeat_multiple("").unwrap().len(), 0);
            let parsed = parse_style_background_repeat_multiple("repeat, no-repeat").unwrap();
            assert_eq!(parsed.len(), 2);
            assert_eq!(parsed.as_slice()[0], StyleBackgroundRepeat::PatternRepeat);
            assert_eq!(parsed.as_slice()[1], StyleBackgroundRepeat::NoRepeat);
            assert!(parse_style_background_repeat_multiple("repeat,,repeat").is_err());
            assert!(parse_style_background_repeat_multiple("   ").is_err());
            let many = "repeat,".repeat(2_000) + "no-repeat";
            assert_eq!(
                parse_style_background_repeat_multiple(&many).unwrap().len(),
                2_001
            );
        }
        // ---------------------------------------------------------------
        // parser: parse_gradient (private)
        // ---------------------------------------------------------------
        #[test]
        fn autotest_parse_gradient_empty_input_is_no_direction_for_every_type() {
            for t in ALL_GRADIENT_TYPES {
                assert!(
                    matches!(
                        parse_gradient("", t),
                        Err(CssBackgroundParseError::NoDirection(_))
                    ),
                    "empty body accepted for {t:?}"
                );
            }
        }
        #[test]
        fn autotest_parse_gradient_extend_mode_follows_the_gradient_type() {
            let StyleBackgroundContent::LinearGradient(g) =
                parse_gradient("red, blue", GradientType::LinearGradient).unwrap()
            else {
                panic!("expected a linear gradient");
            };
            assert_eq!(g.extend_mode, ExtendMode::Clamp);
            let StyleBackgroundContent::LinearGradient(g) =
                parse_gradient("red, blue", GradientType::RepeatingLinearGradient).unwrap()
            else {
                panic!("expected a linear gradient");
            };
            assert_eq!(g.extend_mode, ExtendMode::Repeat);
            let StyleBackgroundContent::RadialGradient(g) =
                parse_gradient("red, blue", GradientType::RepeatingRadialGradient).unwrap()
            else {
                panic!("expected a radial gradient");
            };
            assert_eq!(g.extend_mode, ExtendMode::Repeat);
            let StyleBackgroundContent::ConicGradient(g) =
                parse_gradient("red, blue", GradientType::RepeatingConicGradient).unwrap()
            else {
                panic!("expected a conic gradient");
            };
            assert_eq!(g.extend_mode, ExtendMode::Repeat);
        }
        #[test]
        fn autotest_parse_gradient_accepts_gradients_with_too_few_stops() {
            // W3C requires >= 2 color stops. Pinned: this parser happily returns
            // gradients with one or zero stops -- `TooFewGradientStops` is dead code.
            let StyleBackgroundContent::LinearGradient(g) =
                parse_gradient("red", GradientType::LinearGradient).unwrap()
            else {
                panic!("expected a linear gradient");
            };
            assert_eq!(g.stops.len(), 1);
            assert_eq!(offsets(&g.stops), alloc::vec![0.0]);
            // A direction with no stops at all -> zero stops, still Ok.
            let StyleBackgroundContent::LinearGradient(g) =
                parse_gradient("to right", GradientType::LinearGradient).unwrap()
            else {
                panic!("expected a linear gradient");
            };
            assert_eq!(g.stops.len(), 0);
            // Same for a radial gradient that only names a shape.
            let StyleBackgroundContent::RadialGradient(g) =
                parse_gradient("circle", GradientType::RadialGradient).unwrap()
            else {
                panic!("expected a radial gradient");
            };
            assert_eq!(g.shape, Shape::Circle);
            assert_eq!(g.stops.len(), 0);
        }
        #[test]
        fn autotest_parse_gradient_never_panics_on_adversarial_input() {
            let huge = "a".repeat(100_000);
            let nested = "(".repeat(10_000) + &")".repeat(10_000);
            let many_commas = ",".repeat(10_000);
            for t in ALL_GRADIENT_TYPES {
                for input in ADVERSARIAL {
                    let a = parse_gradient(input, t);
                    let b = parse_gradient(input, t);
                    assert_eq!(a, b, "non-deterministic for {input:?} / {t:?}");
                }
                for input in [huge.as_str(), nested.as_str(), many_commas.as_str()] {
                    let a = parse_gradient(input, t);
                    let b = parse_gradient(input, t);
                    assert_eq!(a, b, "non-deterministic for a long input / {t:?}");
                }
                // An empty body is rejected for every gradient type.
                assert!(parse_gradient("", t).is_err(), "empty body accepted for {t:?}");
                // A comma-only body has nothing but empty stops -> always an error.
                assert!(
                    parse_gradient(&many_commas, t).is_err(),
                    "comma soup accepted for {t:?}"
                );
            }
            // Linear and conic reject junk outright. (Radial does not -- see
            // `autotest_radial_gradient_silently_drops_unparseable_items`.)
            for t in [
                GradientType::LinearGradient,
                GradientType::RepeatingLinearGradient,
                GradientType::ConicGradient,
                GradientType::RepeatingConicGradient,
            ] {
                assert!(parse_gradient(&huge, t).is_err(), "junk accepted for {t:?}");
                assert!(parse_gradient(&nested, t).is_err(), "junk accepted for {t:?}");
                assert!(parse_gradient("!!!", t).is_err(), "junk accepted for {t:?}");
            }
        }
        #[test]
        fn autotest_radial_gradient_silently_drops_unparseable_items() {
            // BUG (pinned): in the radial branch, a comma-item that is neither a
            // shape/size/position *nor* a valid color stop is skipped rather than
            // rejected -- so pure garbage parses as a gradient with no stops, and a
            // junk leading item simply disappears.
            let StyleBackgroundContent::RadialGradient(g) =
                parse_gradient("!!!", GradientType::RadialGradient).unwrap()
            else {
                panic!("expected a radial gradient");
            };
            assert_eq!(g.stops.len(), 0);
            let g = radial("radial-gradient(!!!, red)");
            assert_eq!(g.stops.len(), 1, "the junk item should have been dropped");
            assert_eq!(
                g.stops.as_ref()[0].color,
                ColorOrSystem::Color(ColorU::RED)
            );
            // The same input is a hard error for a linear gradient.
            assert!(parse_style_background_content("linear-gradient(!!!, red)").is_err());
        }
        #[test]
        fn autotest_radial_gradient_position_is_ignored_when_combined_with_a_shape() {
            // BUG (pinned): `parse_style_background_position` is handed the *whole*
            // comma-item ("circle at 50% 50%"), which has 4 whitespace components and
            // therefore fails -- so the `at <position>` part is silently dropped and
            // the position stays at its Left/Top default.
            let g = radial("radial-gradient(circle at 50% 50%, red, blue)");
            assert_eq!(g.shape, Shape::Circle);
            assert_eq!(g.position, StyleBackgroundPosition::default());
            assert_eq!(g.stops.len(), 2);
            // A position on its own (no shape/size in the same item) *is* honoured.
            let g = radial("radial-gradient(50% 50%, red, blue)");
            assert_eq!(
                g.position,
                StyleBackgroundPosition {
                    horizontal: BackgroundPositionHorizontal::Exact(PixelValue::percent(50.0)),
                    vertical: BackgroundPositionVertical::Exact(PixelValue::percent(50.0)),
                }
            );
            assert_eq!(g.stops.len(), 2);
        }
        #[test]
        fn autotest_radial_gradient_shape_and_size_keywords() {
            let g = radial("radial-gradient(circle closest-side, red, blue)");
            assert_eq!(g.shape, Shape::Circle);
            assert_eq!(g.size, RadialGradientSize::ClosestSide);
            let g = radial("radial-gradient(ellipse farthest-side, red, blue)");
            assert_eq!(g.shape, Shape::Ellipse);
            assert_eq!(g.size, RadialGradientSize::FarthestSide);
            // Defaults when nothing is named.
            let g = radial("radial-gradient(red, blue)");
            assert_eq!(g.shape, Shape::default());
            assert_eq!(g.size, RadialGradientSize::default());
        }
        // ---------------------------------------------------------------
        // parser: parse_linear_color_stop / parse_radial_color_stop (private)
        // ---------------------------------------------------------------
        #[test]
        fn autotest_parse_linear_color_stop_valid_minimal() {
            let s = parse_linear_color_stop("red").unwrap();
            assert_eq!(s.color, ColorOrSystem::Color(ColorU::RED));
            assert_eq!(s.offset1, OptionPercentageValue::None);
            assert_eq!(s.offset2, OptionPercentageValue::None);
            let s = parse_linear_color_stop("  red 50%  ").unwrap();
            assert_eq!(
                s.offset1,
                OptionPercentageValue::Some(PercentageValue::new(50.0))
            );
            assert_eq!(s.offset2, OptionPercentageValue::None);
            let s = parse_linear_color_stop("red 10% 30%").unwrap();
            assert_eq!(
                s.offset1,
                OptionPercentageValue::Some(PercentageValue::new(10.0))
            );
            assert_eq!(
                s.offset2,
                OptionPercentageValue::Some(PercentageValue::new(30.0))
            );
            // Colors that themselves contain spaces and digits still split correctly.
            let s = parse_linear_color_stop("rgba(0, 0, 0, 0.5) 50%").unwrap();
            assert_eq!(
                s.offset1,
                OptionPercentageValue::Some(PercentageValue::new(50.0))
            );
            // System colors are accepted as stop colors.
            let s = parse_linear_color_stop("system:accent 50%").unwrap();
            assert_eq!(s.color, ColorOrSystem::System(SystemColorRef::Accent));
        }
        #[test]
        fn autotest_parse_linear_color_stop_rejects_junk() {
            for input in [
                "",
                "   ",
                "\t\n",
                "!!!",
                "\u{1F600}",
                "red 50px",       // offset must be a percentage
                "red 0.5",        // bare number is not recognised as an offset
                "red 10% 20% 30%", // three offsets -> the color part is junk
                "red blue",
            ] {
                assert!(
                    parse_linear_color_stop(input).is_err(),
                    "{input:?} unexpectedly parsed"
                );
            }
            let huge = "a".repeat(100_000);
            assert!(parse_linear_color_stop(&huge).is_err());
        }
        #[test]
        fn autotest_parse_radial_color_stop_valid_and_junk() {
            let s = parse_radial_color_stop("red").unwrap();
            assert_eq!(s.color, ColorOrSystem::Color(ColorU::RED));
            assert_eq!(s.offset1, OptionAngleValue::None);
            let s = parse_radial_color_stop("red 90deg").unwrap();
            assert_eq!(s.offset1, OptionAngleValue::Some(AngleValue::deg(90.0)));
            assert_eq!(s.offset2, OptionAngleValue::None);
            let s = parse_radial_color_stop("red 45deg 90deg").unwrap();
            assert_eq!(s.offset1, OptionAngleValue::Some(AngleValue::deg(45.0)));
            assert_eq!(s.offset2, OptionAngleValue::Some(AngleValue::deg(90.0)));
            // Pinned: a *percentage* is a valid angle for a conic stop.
            assert!(parse_radial_color_stop("red 50%").is_ok());
            for input in ["", "   ", "!!!", "\u{1F600}", "red 5", "red 90deg 45deg 10deg"] {
                assert!(
                    parse_radial_color_stop(input).is_err(),
                    "{input:?} unexpectedly parsed"
                );
            }
            let huge = "a".repeat(100_000);
            assert!(parse_radial_color_stop(&huge).is_err());
        }
        // ---------------------------------------------------------------
        // other: split_color_and_offsets / try_split_last_offset (private)
        // ---------------------------------------------------------------
        #[test]
        fn autotest_split_color_and_offsets_w3c_shapes() {
            assert_eq!(split_color_and_offsets("red"), ("red", None, None));
            assert_eq!(
                split_color_and_offsets("red 50%"),
                ("red", Some("50%"), None)
            );
            assert_eq!(
                split_color_and_offsets("red 10% 30%"),
                ("red", Some("10%"), Some("30%"))
            );
            assert_eq!(
                split_color_and_offsets("rgba(0, 0, 0, 0.5) 10% 30%"),
                ("rgba(0, 0, 0, 0.5)", Some("10%"), Some("30%"))
            );
            // A direction is never mistaken for offsets (no digits).
            assert_eq!(
                split_color_and_offsets("to right bottom"),
                ("to right bottom", None, None)
            );
        }
        #[test]
        fn autotest_split_color_and_offsets_never_panics_on_edges() {
            assert_eq!(split_color_and_offsets(""), ("", None, None));
            assert_eq!(split_color_and_offsets("   "), ("", None, None));
            // Multibyte input: splitting must land on a char boundary.
            assert_eq!(
                split_color_and_offsets("\u{1F600} 50%"),
                ("\u{1F600}", Some("50%"), None)
            );
            // A non-breaking space counts as whitespace for rfind *and* for trim.
            assert_eq!(
                split_color_and_offsets("red\u{00a0}50%"),
                ("red", Some("50%"), None)
            );
            let huge = "a".repeat(100_000);
            assert_eq!(split_color_and_offsets(&huge), (huge.as_str(), None, None));
            for input in ADVERSARIAL {
                assert_eq!(
                    split_color_and_offsets(input),
                    split_color_and_offsets(input)
                );
            }
        }
        #[test]
        fn autotest_try_split_last_offset() {
            assert_eq!(try_split_last_offset("red 50%"), Some(("red", "50%")));
            assert_eq!(try_split_last_offset("red 10px"), Some(("red", "10px")));
            // No whitespace -> nothing to split off.
            assert_eq!(try_split_last_offset("50%"), None);
            // Not offset-shaped.
            assert_eq!(try_split_last_offset("red blue"), None);
            assert_eq!(try_split_last_offset("red 5"), None);
            assert_eq!(try_split_last_offset("to right"), None);
            // Empty / whitespace-only.
            assert_eq!(try_split_last_offset(""), None);
            assert_eq!(try_split_last_offset("   "), None);
            assert_eq!(try_split_last_offset("\t\n"), None);
            for input in ADVERSARIAL {
                assert_eq!(try_split_last_offset(input), try_split_last_offset(input));
            }
        }
        // ---------------------------------------------------------------
        // predicate: is_likely_offset (private)
        // ---------------------------------------------------------------
        #[test]
        fn autotest_is_likely_offset_basic_true_false() {
            for s in [
                "50%", "10px", "0.5turn", "90deg", "1rem", "2vmin", "3vmax", "4grad", "5rad",
                "-50%", "1e40%",
            ] {
                assert!(is_likely_offset(s), "{s:?} should look like an offset");
            }
            for s in [
                "", " ", "red", "px", "%", "5", "0.5", "NaN%", "to", "right", "\u{1F600}",
                "contain",
            ] {
                assert!(!is_likely_offset(s), "{s:?} should not look like an offset");
            }
        }
        #[test]
        fn autotest_is_likely_offset_is_a_shape_check_not_a_validator() {
            // Pinned: it only requires "contains an ASCII digit" + "ends with a unit",
            // so plainly invalid tokens pass. The real parse still rejects them.
            assert!(is_likely_offset("abc1px"));
            assert!(is_likely_offset("\u{1F600}5%"));
            assert!(is_likely_offset("--1--px"));
            assert!(is_likely_offset("1%%%"));
            // ... and the digit check must be ASCII: an Arabic-Indic digit does not
            // count, so this is *not* treated as an offset.
            assert!(!is_likely_offset("\u{0661}%"));
            let huge = "9".repeat(100_000) + "%";
            assert!(is_likely_offset(&huge));
            for input in ADVERSARIAL {
                assert_eq!(is_likely_offset(input), is_likely_offset(input));
            }
        }
        // ---------------------------------------------------------------
        // parser: parse_conic_first_item (private)
        // ---------------------------------------------------------------
        #[test]
        fn autotest_parse_conic_first_item_valid_and_absent() {
            // Not a "from ..." prelude -> Ok(None), so the item is a color stop.
            assert_eq!(parse_conic_first_item("").unwrap(), None);
            assert_eq!(parse_conic_first_item("red").unwrap(), None);
            assert_eq!(parse_conic_first_item("   ").unwrap(), None);
            let (angle, pos) = parse_conic_first_item("from 90deg").unwrap().unwrap();
            assert_eq!(angle, AngleValue::deg(90.0));
            assert_eq!(pos, StyleBackgroundPosition::default());
            let (angle, pos) = parse_conic_first_item("from 0deg at center").unwrap().unwrap();
            assert_eq!(angle, AngleValue::deg(0.0));
            assert_eq!(pos.horizontal, BackgroundPositionHorizontal::Center);
            assert_eq!(pos.vertical, BackgroundPositionVertical::Center);
        }
        #[test]
        fn autotest_parse_conic_first_item_rejects_malformed_preludes() {
            // "from" with no angle.
            assert!(parse_conic_first_item("from").is_err());
            assert!(parse_conic_first_item("from at center").is_err());
            // Pinned quirk: any token *starting with* "from" enters the prelude branch,
            // so a would-be color like "fromage" becomes an angle error.
            assert!(parse_conic_first_item("fromage").is_err());
            // Too many position components.
            assert!(matches!(
                parse_conic_first_item("from 90deg at left top center"),
                Err(CssConicGradientParseError::Position(_))
            ));
            let huge = alloc::format!("from {}", "9".repeat(100_000));
            let _ = parse_conic_first_item(&huge);
            for input in ADVERSARIAL {
                let a = parse_conic_first_item(input);
                let b = parse_conic_first_item(input);
                assert_eq!(a, b, "non-deterministic for {input:?}");
            }
        }
        #[test]
        fn autotest_conic_gradient_end_to_end() {
            let g = conic("conic-gradient(from 45deg, red, blue)");
            assert_eq!(g.angle, AngleValue::deg(45.0));
            assert_eq!(g.extend_mode, ExtendMode::Clamp);
            assert_eq!(g.stops.len(), 2);
            assert_eq!(g.stops.as_ref()[0].angle.to_degrees_raw(), 0.0);
            assert_eq!(g.stops.as_ref()[1].angle.to_degrees_raw(), 360.0);
            // Conic stop angles are monotonic and finite, even for silly inputs.
            for input in [
                "conic-gradient(red, blue)",
                "conic-gradient(red 0deg, blue 180deg, green 360deg)",
                "conic-gradient(red 180deg, blue 90deg)",
                "conic-gradient(red -90deg, blue)",
                "repeating-conic-gradient(red, blue 30deg)",
            ] {
                let g = conic(input);
                let mut prev = f32::NEG_INFINITY;
                for s in &g.stops {
                    let deg = s.angle.to_degrees_raw();
                    assert!(deg.is_finite(), "non-finite angle in {input:?}");
                    assert!(deg >= prev, "angles not monotonic in {input:?}");
                    prev = deg;
                }
            }
            // An overflowing angle saturates instead of leaking inf into the stops.
            let g = conic("conic-gradient(red 1e40deg, blue)");
            assert_eq!(g.stops.len(), 2);
            for s in &g.stops {
                assert!(s.angle.to_degrees_raw().is_finite());
            }
            assert!(parse_style_background_content("conic-gradient(from, red)").is_err());
        }
        // ---------------------------------------------------------------
        // parser: parse_background_position_{horizontal,vertical} (private)
        // ---------------------------------------------------------------
        #[test]
        fn autotest_parse_background_position_horizontal() {
            assert_eq!(
                parse_background_position_horizontal("left").unwrap(),
                BackgroundPositionHorizontal::Left
            );
            assert_eq!(
                parse_background_position_horizontal("center").unwrap(),
                BackgroundPositionHorizontal::Center
            );
            assert_eq!(
                parse_background_position_horizontal("right").unwrap(),
                BackgroundPositionHorizontal::Right
            );
            assert_eq!(
                parse_background_position_horizontal("10px").unwrap(),
                BackgroundPositionHorizontal::Exact(PixelValue::px(10.0))
            );
            // Vertical keywords are not horizontal ones.
            assert!(parse_background_position_horizontal("top").is_err());
            // Pinned: no trimming here (callers pass whitespace-split tokens).
            assert!(parse_background_position_horizontal(" left").is_err());
            assert!(parse_background_position_horizontal("").is_err());
            assert!(parse_background_position_horizontal("\u{1F600}").is_err());
            let huge = "a".repeat(100_000);
            assert!(parse_background_position_horizontal(&huge).is_err());
        }
        #[test]
        fn autotest_parse_background_position_vertical() {
            assert_eq!(
                parse_background_position_vertical("top").unwrap(),
                BackgroundPositionVertical::Top
            );
            assert_eq!(
                parse_background_position_vertical("center").unwrap(),
                BackgroundPositionVertical::Center
            );
            assert_eq!(
                parse_background_position_vertical("bottom").unwrap(),
                BackgroundPositionVertical::Bottom
            );
            assert_eq!(
                parse_background_position_vertical("-10px").unwrap(),
                BackgroundPositionVertical::Exact(PixelValue::px(-10.0))
            );
            assert!(parse_background_position_vertical("left").is_err());
            assert!(parse_background_position_vertical("").is_err());
            assert!(parse_background_position_vertical("\u{1F600}").is_err());
            let huge = "a".repeat(100_000);
            assert!(parse_background_position_vertical(&huge).is_err());
        }
        // ---------------------------------------------------------------
        // parser: parse_shape / parse_radial_gradient_size (private)
        // ---------------------------------------------------------------
        #[test]
        fn autotest_parse_shape() {
            assert_eq!(parse_shape("circle").unwrap(), Shape::Circle);
            assert_eq!(parse_shape("  ellipse  ").unwrap(), Shape::Ellipse);
            for input in ["", "   ", "Circle", "CIRCLE", "circles", "!!!", "\u{1F600}", "0"] {
                assert!(parse_shape(input).is_err(), "{input:?} unexpectedly parsed");
            }
            let huge = "a".repeat(100_000);
            assert!(parse_shape(&huge).is_err());
            for input in ADVERSARIAL {
                assert_eq!(parse_shape(input), parse_shape(input));
            }
        }
        #[test]
        fn autotest_parse_radial_gradient_size() {
            assert_eq!(
                parse_radial_gradient_size("closest-side").unwrap(),
                RadialGradientSize::ClosestSide
            );
            assert_eq!(
                parse_radial_gradient_size("  closest-corner ").unwrap(),
                RadialGradientSize::ClosestCorner
            );
            assert_eq!(
                parse_radial_gradient_size("farthest-side").unwrap(),
                RadialGradientSize::FarthestSide
            );
            assert_eq!(
                parse_radial_gradient_size("farthest-corner").unwrap(),
                RadialGradientSize::FarthestCorner
            );
            for input in [
                "",
                "   ",
                "closest",
                "CLOSEST-SIDE",
                "farthest-corners",
                "!!!",
                "\u{1F600}",
            ] {
                assert!(
                    parse_radial_gradient_size(input).is_err(),
                    "{input:?} unexpectedly parsed"
                );
            }
            let huge = "a".repeat(100_000);
            assert!(parse_radial_gradient_size(&huge).is_err());
        }
        // ---------------------------------------------------------------
        // round-trips: print_as_css_value -> parse
        // ---------------------------------------------------------------
        #[test]
        fn autotest_round_trip_background_repeat() {
            for r in [
                StyleBackgroundRepeat::NoRepeat,
                StyleBackgroundRepeat::PatternRepeat,
                StyleBackgroundRepeat::RepeatX,
                StyleBackgroundRepeat::RepeatY,
            ] {
                let printed = r.print_as_css_value();
                assert!(!printed.is_empty());
                assert_eq!(parse_style_background_repeat(&printed).unwrap(), r);
            }
        }
        #[test]
        fn autotest_round_trip_background_size() {
            for s in [
                StyleBackgroundSize::Contain,
                StyleBackgroundSize::Cover,
                StyleBackgroundSize::ExactSize(PixelValueSize {
                    width: PixelValue::px(100.0),
                    height: PixelValue::em(20.0),
                }),
                StyleBackgroundSize::ExactSize(PixelValueSize {
                    width: PixelValue::percent(50.0),
                    height: PixelValue::percent(50.0),
                }),
                StyleBackgroundSize::ExactSize(PixelValueSize {
                    width: PixelValue::px(0.0),
                    height: PixelValue::px(-25.5),
                }),
            ] {
                let printed = s.print_as_css_value();
                assert_eq!(
                    parse_style_background_size(&printed).unwrap(),
                    s,
                    "round-trip failed for {printed:?}"
                );
            }
        }
        #[test]
        fn autotest_round_trip_background_position() {
            let horizontals = [
                BackgroundPositionHorizontal::Left,
                BackgroundPositionHorizontal::Center,
                BackgroundPositionHorizontal::Right,
                BackgroundPositionHorizontal::Exact(PixelValue::px(50.0)),
                BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0)),
            ];
            let verticals = [
                BackgroundPositionVertical::Top,
                BackgroundPositionVertical::Center,
                BackgroundPositionVertical::Bottom,
                BackgroundPositionVertical::Exact(PixelValue::px(-10.0)),
                BackgroundPositionVertical::Exact(PixelValue::em(2.5)),
            ];
            for horizontal in horizontals {
                for vertical in verticals {
                    let pos = StyleBackgroundPosition {
                        horizontal,
                        vertical,
                    };
                    let printed = pos.print_as_css_value();
                    assert_eq!(
                        parse_style_background_position(&printed).unwrap(),
                        pos,
                        "round-trip failed for {printed:?}"
                    );
                }
            }
        }
        #[test]
        fn autotest_round_trip_background_content_colors_and_images() {
            for content in [
                StyleBackgroundContent::Color(ColorU::RED),
                StyleBackgroundContent::Color(ColorU::TRANSPARENT),
                StyleBackgroundContent::Color(ColorU::rgba(1, 2, 3, 4)),
                StyleBackgroundContent::Color(ColorU::WHITE),
                StyleBackgroundContent::Image("a.png".into()),
                StyleBackgroundContent::Image("some/deep/path.jpeg".into()),
                StyleBackgroundContent::SystemColor(SystemColorRef::Accent),
                StyleBackgroundContent::SystemColor(SystemColorRef::SelectionText),
            ] {
                let printed = content.print_as_css_value();
                assert_eq!(
                    parse_style_background_content(&printed).unwrap(),
                    content,
                    "round-trip failed for {printed:?}"
                );
            }
            assert_eq!(
                StyleBackgroundContent::default(),
                StyleBackgroundContent::Color(ColorU::TRANSPARENT)
            );
        }
        #[test]
        fn autotest_round_trip_gradients() {
            for input in [
                "linear-gradient(to right, red 0%, blue 100%)",
                "repeating-linear-gradient(to bottom, red 25%, blue 75%)",
                "linear-gradient(45deg, red 0%, blue 50%)",
                "radial-gradient(circle farthest-corner at left top, red 0%, blue 100%)",
                "conic-gradient(from 90deg at left top, red 0deg, blue 360deg)",
                "repeating-conic-gradient(from 0deg at left top, red 0deg, blue 180deg)",
            ] {
                let parsed = parse_style_background_content(input).unwrap();
                let printed = parsed.print_as_css_value();
                let reparsed = parse_style_background_content(&printed).unwrap();
                assert_eq!(
                    parsed, reparsed,
                    "gradient did not survive print -> parse ({printed:?})"
                );
                // ... and printing is stable across the round-trip.
                assert_eq!(printed, reparsed.print_as_css_value());
            }
        }
        #[test]
        fn autotest_round_trip_vec_printing_is_comma_separated() {
            let contents = parse_style_background_content_multiple("red, blue").unwrap();
            assert_eq!(contents.print_as_css_value(), "#ff0000ff, #0000ffff");
            assert_eq!(contents.as_slice()[1], StyleBackgroundContent::Color(blue()));
            let reparsed =
                parse_style_background_content_multiple(&contents.print_as_css_value()).unwrap();
            assert_eq!(reparsed, contents);
            let sizes = parse_style_background_size_multiple("contain, 10px 20px").unwrap();
            assert_eq!(
                parse_style_background_size_multiple(&sizes.print_as_css_value()).unwrap(),
                sizes
            );
            let repeats = parse_style_background_repeat_multiple("repeat, no-repeat").unwrap();
            assert_eq!(
                parse_style_background_repeat_multiple(&repeats.print_as_css_value()).unwrap(),
                repeats
            );
            let positions = parse_style_background_position_multiple("left top, 10px 20px").unwrap();
            assert_eq!(
                parse_style_background_position_multiple(&positions.print_as_css_value()).unwrap(),
                positions
            );
        }
        #[test]
        fn autotest_normalized_stop_printing_is_reparseable() {
            let stop = NormalizedLinearColorStop::new(PercentageValue::new(25.0), ColorU::RED);
            assert_eq!(stop.print_as_css_value(), "#ff0000ff 25%");
            let reparsed = parse_linear_color_stop(&stop.print_as_css_value()).unwrap();
            assert_eq!(reparsed.color, stop.color);
            assert_eq!(
                reparsed.offset1,
                OptionPercentageValue::Some(PercentageValue::new(25.0))
            );
            let rstop = NormalizedRadialColorStop::new(AngleValue::deg(90.0), blue());
            assert_eq!(rstop.print_as_css_value(), "#0000ffff 90deg");
            let reparsed = parse_radial_color_stop(&rstop.print_as_css_value()).unwrap();
            assert_eq!(reparsed.color, rstop.color);
            assert_eq!(
                reparsed.offset1,
                OptionAngleValue::Some(AngleValue::deg(90.0))
            );
            // System-colored stops print their `system:*` name and re-parse.
            let sys = NormalizedLinearColorStop {
                offset: PercentageValue::new(50.0),
                color: ColorOrSystem::System(SystemColorRef::Accent),
            };
            assert_eq!(sys.print_as_css_value(), "system:accent 50%");
            assert_eq!(
                parse_linear_color_stop(&sys.print_as_css_value()).unwrap().color,
                ColorOrSystem::System(SystemColorRef::Accent)
            );
        }
        #[test]
        fn autotest_empty_gradient_printing_does_not_panic() {
            // Default gradients have zero stops -- printing must still produce
            // something well-formed (and not, say, index out of bounds).
            let lg = StyleBackgroundContent::LinearGradient(LinearGradient::default());
            assert!(lg.print_as_css_value().starts_with("linear-gradient("));
            let rg = StyleBackgroundContent::RadialGradient(RadialGradient::default());
            assert!(rg.print_as_css_value().starts_with("radial-gradient("));
            let cg = StyleBackgroundContent::ConicGradient(ConicGradient::default());
            assert!(cg.print_as_css_value().starts_with("conic-gradient("));
            // A gradient built from an empty stop list is also printable.
            let empty_stops = StyleBackgroundContent::LinearGradient(LinearGradient {
                extend_mode: ExtendMode::Repeat,
                stops: Vec::<NormalizedLinearColorStop>::new().into(),
                ..LinearGradient::default()
            });
            assert!(empty_stops
                .print_as_css_value()
                .starts_with("repeating-linear-gradient("));
        }
    }
}
#[cfg(feature = "parser")]
pub use self::parser::*;
#[cfg(all(test, feature = "parser"))]
mod tests {
    use super::*;
    use crate::props::basic::{DirectionCorner, DirectionCorners};
    #[test]
1
    fn test_parse_single_background_content() {
        // Color
1
        assert_eq!(
1
            parse_style_background_content("red").unwrap(),
            StyleBackgroundContent::Color(ColorU::RED)
        );
1
        assert_eq!(
1
            parse_style_background_content("#ff00ff").unwrap(),
1
            StyleBackgroundContent::Color(ColorU::new_rgb(255, 0, 255))
        );
        // Image
1
        assert_eq!(
1
            parse_style_background_content("url(\"image.png\")").unwrap(),
1
            StyleBackgroundContent::Image("image.png".into())
        );
        // Linear Gradient
1
        let lg = parse_style_background_content("linear-gradient(to right, red, blue)").unwrap();
1
        assert!(matches!(lg, StyleBackgroundContent::LinearGradient(_)));
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1
            assert_eq!(grad.stops.len(), 2);
1
            assert_eq!(
                grad.direction,
                Direction::FromTo(DirectionCorners {
                    dir_from: DirectionCorner::Left,
                    dir_to: DirectionCorner::Right
                })
            );
        }
        // Radial Gradient
1
        let rg = parse_style_background_content("radial-gradient(circle, white, black)").unwrap();
1
        assert!(matches!(rg, StyleBackgroundContent::RadialGradient(_)));
1
        if let StyleBackgroundContent::RadialGradient(grad) = rg {
1
            assert_eq!(grad.stops.len(), 2);
1
            assert_eq!(grad.shape, Shape::Circle);
        }
        // Conic Gradient
1
        let cg = parse_style_background_content("conic-gradient(from 90deg, red, blue)").unwrap();
1
        assert!(matches!(cg, StyleBackgroundContent::ConicGradient(_)));
1
        if let StyleBackgroundContent::ConicGradient(grad) = cg {
1
            assert_eq!(grad.stops.len(), 2);
1
            assert_eq!(grad.angle, AngleValue::deg(90.0));
        }
1
    }
    #[test]
1
    fn test_parse_multiple_background_content() {
1
        let result =
1
            parse_style_background_content_multiple("url(foo.png), linear-gradient(red, blue)")
1
                .unwrap();
1
        assert_eq!(result.len(), 2);
1
        assert!(matches!(
1
            result.as_slice()[0],
            StyleBackgroundContent::Image(_)
        ));
1
        assert!(matches!(
1
            result.as_slice()[1],
            StyleBackgroundContent::LinearGradient(_)
        ));
1
    }
    #[test]
1
    fn test_parse_background_position() {
        // One value
1
        let result = parse_style_background_position("center").unwrap();
1
        assert_eq!(result.horizontal, BackgroundPositionHorizontal::Center);
1
        assert_eq!(result.vertical, BackgroundPositionVertical::Center);
1
        let result = parse_style_background_position("25%").unwrap();
1
        assert_eq!(
            result.horizontal,
1
            BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0))
        );
1
        assert_eq!(result.vertical, BackgroundPositionVertical::Center);
        // Two values
1
        let result = parse_style_background_position("right 50px").unwrap();
1
        assert_eq!(result.horizontal, BackgroundPositionHorizontal::Right);
1
        assert_eq!(
            result.vertical,
1
            BackgroundPositionVertical::Exact(PixelValue::px(50.0))
        );
        // Four values (not supported by this parser, should fail)
1
        assert!(parse_style_background_position("left 10px top 20px").is_err());
1
    }
    #[test]
1
    fn test_parse_background_size() {
1
        assert_eq!(
1
            parse_style_background_size("contain").unwrap(),
            StyleBackgroundSize::Contain
        );
1
        assert_eq!(
1
            parse_style_background_size("cover").unwrap(),
            StyleBackgroundSize::Cover
        );
1
        assert_eq!(
1
            parse_style_background_size("50%").unwrap(),
1
            StyleBackgroundSize::ExactSize(PixelValueSize {
1
                width: PixelValue::percent(50.0),
1
                height: PixelValue::percent(50.0)
1
            })
        );
1
        assert_eq!(
1
            parse_style_background_size("100px 20em").unwrap(),
1
            StyleBackgroundSize::ExactSize(PixelValueSize {
1
                width: PixelValue::px(100.0),
1
                height: PixelValue::em(20.0)
1
            })
        );
1
        assert!(parse_style_background_size("auto").is_err());
1
    }
    #[test]
1
    fn test_parse_background_repeat() {
1
        assert_eq!(
1
            parse_style_background_repeat("repeat").unwrap(),
            StyleBackgroundRepeat::PatternRepeat
        );
1
        assert_eq!(
1
            parse_style_background_repeat("repeat-x").unwrap(),
            StyleBackgroundRepeat::RepeatX
        );
1
        assert_eq!(
1
            parse_style_background_repeat("repeat-y").unwrap(),
            StyleBackgroundRepeat::RepeatY
        );
1
        assert_eq!(
1
            parse_style_background_repeat("no-repeat").unwrap(),
            StyleBackgroundRepeat::NoRepeat
        );
1
        assert!(parse_style_background_repeat("repeat-xy").is_err());
1
    }
    // =========================================================================
    // W3C CSS Images Level 3 - Gradient Parsing Tests
    // =========================================================================
    #[test]
1
    fn test_gradient_no_position_stops() {
        // "linear-gradient(red, blue)" - no positions specified
1
        let lg = parse_style_background_content("linear-gradient(red, blue)").unwrap();
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1
            assert_eq!(grad.stops.len(), 2);
            // First stop should default to 0%
1
            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
            // Last stop should default to 100%
1
            assert!((grad.stops.as_ref()[1].offset.normalized() - 1.0).abs() < 0.001);
        } else {
            panic!("Expected LinearGradient");
        }
1
    }
    #[test]
1
    fn test_gradient_single_position_stops() {
        // "linear-gradient(red 25%, blue 75%)" - one position per stop
1
        let lg = parse_style_background_content("linear-gradient(red 25%, blue 75%)").unwrap();
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1
            assert_eq!(grad.stops.len(), 2);
1
            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.25).abs() < 0.001);
1
            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.75).abs() < 0.001);
        } else {
            panic!("Expected LinearGradient");
        }
1
    }
    #[test]
1
    fn test_gradient_multi_position_stops() {
        // "linear-gradient(red 10% 30%, blue)" - two positions create two stops
1
        let lg = parse_style_background_content("linear-gradient(red 10% 30%, blue)").unwrap();
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
            // Should have 3 stops: red@10%, red@30%, blue@100%
1
            assert_eq!(grad.stops.len(), 3, "Expected 3 stops for multi-position");
1
            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.10).abs() < 0.001);
1
            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.30).abs() < 0.001);
1
            assert!((grad.stops.as_ref()[2].offset.normalized() - 1.0).abs() < 0.001);
            // Both first two stops should have same color (red)
1
            assert_eq!(grad.stops.as_ref()[0].color, grad.stops.as_ref()[1].color);
        } else {
            panic!("Expected LinearGradient");
        }
1
    }
    #[test]
1
    fn test_gradient_three_colors_no_positions() {
        // "linear-gradient(red, green, blue)" - evenly distributed
1
        let lg = parse_style_background_content("linear-gradient(red, green, blue)").unwrap();
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1
            assert_eq!(grad.stops.len(), 3);
            // Positions: 0%, 50%, 100%
1
            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
1
            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.5).abs() < 0.001);
1
            assert!((grad.stops.as_ref()[2].offset.normalized() - 1.0).abs() < 0.001);
        } else {
            panic!("Expected LinearGradient");
        }
1
    }
    #[test]
1
    fn test_gradient_fixup_ascending_order() {
        // "linear-gradient(red 50%, blue 20%)" - blue position < red position
        // W3C says: clamp to max of previous positions
1
        let lg = parse_style_background_content("linear-gradient(red 50%, blue 20%)").unwrap();
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1
            assert_eq!(grad.stops.len(), 2);
            // First stop at 50%
1
            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.50).abs() < 0.001);
            // Second stop clamped to 50% (not 20%)
1
            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.50).abs() < 0.001);
        } else {
            panic!("Expected LinearGradient");
        }
1
    }
    #[test]
1
    fn test_gradient_distribute_unpositioned() {
        // "linear-gradient(red 0%, yellow, green, blue 100%)"
        // yellow and green should be distributed evenly between 0% and 100%
1
        let lg =
1
            parse_style_background_content("linear-gradient(red 0%, yellow, green, blue 100%)")
1
                .unwrap();
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1
            assert_eq!(grad.stops.len(), 4);
            // Positions: 0%, 33.3%, 66.6%, 100%
1
            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
1
            assert!((grad.stops.as_ref()[1].offset.normalized() - 0.333).abs() < 0.01);
1
            assert!((grad.stops.as_ref()[2].offset.normalized() - 0.666).abs() < 0.01);
1
            assert!((grad.stops.as_ref()[3].offset.normalized() - 1.0).abs() < 0.001);
        } else {
            panic!("Expected LinearGradient");
        }
1
    }
    #[test]
1
    fn test_gradient_direction_to_corner() {
        // "linear-gradient(to top right, red, blue)"
1
        let lg =
1
            parse_style_background_content("linear-gradient(to top right, red, blue)").unwrap();
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1
            assert_eq!(
                grad.direction,
                Direction::FromTo(DirectionCorners {
                    dir_from: DirectionCorner::BottomLeft,
                    dir_to: DirectionCorner::TopRight
                })
            );
        } else {
            panic!("Expected LinearGradient");
        }
1
    }
    #[test]
1
    fn test_gradient_direction_angle() {
        // "linear-gradient(45deg, red, blue)"
1
        let lg = parse_style_background_content("linear-gradient(45deg, red, blue)").unwrap();
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1
            assert_eq!(grad.direction, Direction::Angle(AngleValue::deg(45.0)));
        } else {
            panic!("Expected LinearGradient");
        }
1
    }
    #[test]
1
    fn test_repeating_gradient() {
        // "repeating-linear-gradient(red, blue 20%)"
1
        let lg =
1
            parse_style_background_content("repeating-linear-gradient(red, blue 20%)").unwrap();
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1
            assert_eq!(grad.extend_mode, ExtendMode::Repeat);
        } else {
            panic!("Expected LinearGradient");
        }
1
    }
    #[test]
1
    fn test_radial_gradient_circle() {
        // "radial-gradient(circle, red, blue)"
1
        let rg = parse_style_background_content("radial-gradient(circle, red, blue)").unwrap();
1
        if let StyleBackgroundContent::RadialGradient(grad) = rg {
1
            assert_eq!(grad.shape, Shape::Circle);
1
            assert_eq!(grad.stops.len(), 2);
            // Check default position is center
1
            assert_eq!(grad.position.horizontal, BackgroundPositionHorizontal::Left);
1
            assert_eq!(grad.position.vertical, BackgroundPositionVertical::Top);
        } else {
            panic!("Expected RadialGradient");
        }
1
    }
    #[test]
1
    fn test_radial_gradient_ellipse() {
        // "radial-gradient(ellipse, red, blue)"
1
        let rg = parse_style_background_content("radial-gradient(ellipse, red, blue)").unwrap();
1
        if let StyleBackgroundContent::RadialGradient(grad) = rg {
1
            assert_eq!(grad.shape, Shape::Ellipse);
1
            assert_eq!(grad.stops.len(), 2);
        } else {
            panic!("Expected RadialGradient");
        }
1
    }
    #[test]
1
    fn test_radial_gradient_size_keywords() {
        // Test different size keywords
1
        let rg = parse_style_background_content("radial-gradient(circle closest-side, red, blue)")
1
            .unwrap();
1
        if let StyleBackgroundContent::RadialGradient(grad) = rg {
1
            assert_eq!(grad.shape, Shape::Circle);
1
            assert_eq!(grad.size, RadialGradientSize::ClosestSide);
        } else {
            panic!("Expected RadialGradient");
        }
1
    }
    #[test]
1
    fn test_radial_gradient_stop_positions() {
        // "radial-gradient(red 0%, blue 100%)"
1
        let rg = parse_style_background_content("radial-gradient(red 0%, blue 100%)").unwrap();
1
        if let StyleBackgroundContent::RadialGradient(grad) = rg {
1
            assert_eq!(grad.stops.len(), 2);
1
            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
1
            assert!((grad.stops.as_ref()[1].offset.normalized() - 1.0).abs() < 0.001);
        } else {
            panic!("Expected RadialGradient");
        }
1
    }
    #[test]
1
    fn test_repeating_radial_gradient() {
1
        let rg = parse_style_background_content("repeating-radial-gradient(circle, red, blue 20%)")
1
            .unwrap();
1
        if let StyleBackgroundContent::RadialGradient(grad) = rg {
1
            assert_eq!(grad.extend_mode, ExtendMode::Repeat);
1
            assert_eq!(grad.shape, Shape::Circle);
        } else {
            panic!("Expected RadialGradient");
        }
1
    }
    #[test]
1
    fn test_conic_gradient_angle() {
        // "conic-gradient(from 45deg, red, blue)"
1
        let cg = parse_style_background_content("conic-gradient(from 45deg, red, blue)").unwrap();
1
        if let StyleBackgroundContent::ConicGradient(grad) = cg {
1
            assert_eq!(grad.angle, AngleValue::deg(45.0));
1
            assert_eq!(grad.stops.len(), 2);
        } else {
            panic!("Expected ConicGradient");
        }
1
    }
    #[test]
1
    fn test_conic_gradient_default() {
        // "conic-gradient(red, blue)" - no angle specified
1
        let cg = parse_style_background_content("conic-gradient(red, blue)").unwrap();
1
        if let StyleBackgroundContent::ConicGradient(grad) = cg {
1
            assert_eq!(grad.stops.len(), 2);
            // First stop defaults to 0deg
1
            assert!(
1
                (grad.stops.as_ref()[0].angle.to_degrees_raw() - 0.0).abs() < 0.001,
                "First stop should be 0deg, got {}",
                grad.stops.as_ref()[0].angle.to_degrees_raw()
            );
            // Last stop defaults to 360deg (use to_degrees_raw to preserve 360)
1
            assert!(
1
                (grad.stops.as_ref()[1].angle.to_degrees_raw() - 360.0).abs() < 0.001,
                "Last stop should be 360deg, got {}",
                grad.stops.as_ref()[1].angle.to_degrees_raw()
            );
        } else {
            panic!("Expected ConicGradient");
        }
1
    }
    #[test]
1
    fn test_conic_gradient_with_positions() {
        // "conic-gradient(red 0deg, blue 180deg, green 360deg)"
1
        let cg =
1
            parse_style_background_content("conic-gradient(red 0deg, blue 180deg, green 360deg)")
1
                .unwrap();
1
        if let StyleBackgroundContent::ConicGradient(grad) = cg {
1
            assert_eq!(grad.stops.len(), 3);
            // Use to_degrees_raw() to preserve 360deg
1
            assert!(
1
                (grad.stops.as_ref()[0].angle.to_degrees_raw() - 0.0).abs() < 0.001,
                "First stop should be 0deg, got {}",
                grad.stops.as_ref()[0].angle.to_degrees_raw()
            );
1
            assert!(
1
                (grad.stops.as_ref()[1].angle.to_degrees_raw() - 180.0).abs() < 0.001,
                "Second stop should be 180deg, got {}",
                grad.stops.as_ref()[1].angle.to_degrees_raw()
            );
1
            assert!(
1
                (grad.stops.as_ref()[2].angle.to_degrees_raw() - 360.0).abs() < 0.001,
                "Last stop should be 360deg, got {}",
                grad.stops.as_ref()[2].angle.to_degrees_raw()
            );
        } else {
            panic!("Expected ConicGradient");
        }
1
    }
    #[test]
1
    fn test_repeating_conic_gradient() {
1
        let cg =
1
            parse_style_background_content("repeating-conic-gradient(red, blue 30deg)").unwrap();
1
        if let StyleBackgroundContent::ConicGradient(grad) = cg {
1
            assert_eq!(grad.extend_mode, ExtendMode::Repeat);
        } else {
            panic!("Expected ConicGradient");
        }
1
    }
    #[test]
1
    fn test_gradient_with_rgba_color() {
        // Test parsing gradient with rgba color (contains spaces)
1
        let lg =
1
            parse_style_background_content("linear-gradient(rgba(255,0,0,0.5), blue)").unwrap();
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1
            assert_eq!(grad.stops.len(), 2);
            // First color should have alpha of ~128 (0.5 * 255, may be 127 or 128 due to rounding)
1
            let first_color = grad.stops.as_ref()[0].color.to_color_u_default();
1
            assert!(first_color.a >= 127 && first_color.a <= 128);
        } else {
            panic!("Expected LinearGradient");
        }
1
    }
    #[test]
1
    fn test_gradient_with_rgba_and_position() {
        // Test parsing "rgba(0,0,0,0.5) 50%"
1
        let lg =
1
            parse_style_background_content("linear-gradient(rgba(0,0,0,0.5) 50%, white)").unwrap();
1
        if let StyleBackgroundContent::LinearGradient(grad) = lg {
1
            assert_eq!(grad.stops.len(), 2);
1
            assert!((grad.stops.as_ref()[0].offset.normalized() - 0.5).abs() < 0.001);
        } else {
            panic!("Expected LinearGradient");
        }
1
    }
    #[test]
1
    fn test_gradient_resolves_system_color_stop() {
        // A `system:accent` stop should round-trip through the parser as a
        // System variant and resolve against a populated `SystemColors` to
        // the live accent color, falling back to the supplied default when
        // the key is unset.
        use crate::props::basic::color::ColorOrSystem;
        use crate::system::SystemColors;
1
        let lg = parse_style_background_content(
1
            "linear-gradient(red, system:accent)",
        )
1
        .unwrap();
1
        let StyleBackgroundContent::LinearGradient(grad) = lg else {
            panic!("Expected LinearGradient");
        };
1
        let stops = grad.stops.as_ref();
1
        assert_eq!(stops.len(), 2);
1
        let accent_stop = &stops[1];
1
        assert!(matches!(accent_stop.color, ColorOrSystem::System(_)));
1
        let populated = SystemColors {
1
            accent: crate::props::basic::color::OptionColorU::Some(ColorU::new_rgb(0, 122, 255)),
1
            ..SystemColors::default()
1
        };
1
        let resolved = accent_stop.resolve(&populated, ColorU::TRANSPARENT);
1
        assert_eq!(resolved, ColorU::new_rgb(0, 122, 255));
1
        let empty = SystemColors::default();
1
        let fallback = accent_stop.resolve(&empty, ColorU::TRANSPARENT);
1
        assert_eq!(fallback, ColorU::TRANSPARENT);
1
    }
}