1
//! CSS properties for border style, width, and color.
2

            
3
use alloc::string::{String, ToString};
4
use core::fmt;
5
use crate::corety::AzString;
6

            
7
#[cfg(feature = "parser")]
8
use crate::props::basic::{color::parse_css_color, pixel::parse_pixel_value};
9
use crate::{
10
    css::PrintAsCssValue,
11
    props::{
12
        basic::{
13
            color::{ColorU, CssColorParseError, CssColorParseErrorOwned},
14
            pixel::{
15
                CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue,
16
                MEDIUM_BORDER_THICKNESS, THICK_BORDER_THICKNESS, THIN_BORDER_THICKNESS,
17
            },
18
        },
19
        macros::PixelValueTaker,
20
    },
21
};
22

            
23
/// Style of a `border`: solid, double, dash, ridge, etc.
24
#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
25
#[repr(C)]
26
// +spec:box-model:28fad6 - Border style variants including groove/ridge/inset/outset for separated/collapsing border models
27
#[derive(Default)]
28
pub enum BorderStyle {
29
    #[default]
30
    None,
31
    Solid,
32
    Double,
33
    Dotted,
34
    Dashed,
35
    Hidden,
36
    Groove,
37
    Ridge,
38
    Inset,
39
    Outset,
40
}
41

            
42

            
43
impl fmt::Display for BorderStyle {
44
806
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45
806
        write!(
46
806
            f,
47
806
            "{}",
48
806
            match self {
49
444
                Self::None => "none",
50
201
                Self::Solid => "solid",
51
56
                Self::Double => "double",
52
56
                Self::Dotted => "dotted",
53
8
                Self::Dashed => "dashed",
54
8
                Self::Hidden => "hidden",
55
9
                Self::Groove => "groove",
56
8
                Self::Ridge => "ridge",
57
8
                Self::Inset => "inset",
58
8
                Self::Outset => "outset",
59
            }
60
        )
61
806
    }
62
}
63

            
64
impl PrintAsCssValue for BorderStyle {
65
31
    fn print_as_css_value(&self) -> String {
66
31
        self.to_string()
67
31
    }
68
}
69

            
70
/// Internal macro to reduce boilerplate for defining border-top, -right, -bottom, -left properties.
71
macro_rules! define_border_side_property {
72
    // For types that have a simple inner value and can be formatted with Display
73
    ($struct_name:ident, $inner_type:ty, $default:expr) => {
74
        #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
75
        #[repr(C)]
76
        pub struct $struct_name {
77
            pub inner: $inner_type,
78
        }
79
        impl ::core::fmt::Debug for $struct_name {
80
1587
            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
81
1587
                write!(f, "{}", self.inner)
82
1587
            }
83
        }
84
        impl Default for $struct_name {
85
262
            fn default() -> Self {
86
262
                Self { inner: $default }
87
262
            }
88
        }
89
    };
90
    // Specialization for ColorU
91
    ($struct_name:ident,ColorU) => {
92
        #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
93
        #[repr(C)]
94
        pub struct $struct_name {
95
            pub inner: ColorU,
96
        }
97
        impl ::core::fmt::Debug for $struct_name {
98
289
            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
99
289
                write!(f, "{}", self.inner.to_hash())
100
289
            }
101
        }
102
        // The default border color is 'currentcolor', but for simplicity we default to BLACK.
103
        // The style property resolver should handle the 'currentcolor' logic.
104
        impl Default for $struct_name {
105
5
            fn default() -> Self {
106
5
                Self {
107
5
                    inner: ColorU::BLACK,
108
5
                }
109
5
            }
110
        }
111
        impl $struct_name {
112
15
            #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
113
15
                Self {
114
15
                    inner: self.inner.interpolate(&other.inner, t),
115
15
                }
116
15
            }
117
        }
118
    };
119
    // NOTE: no separate `PixelValue` specialization arm — the generic
120
    // `($struct_name, $inner_type:ty, $default)` arm above already matches
121
    // `define_border_side_property!(.., PixelValue, ..)` (PixelValue is a `:ty`),
122
    // so a 3-arg PixelValue arm here would be unreachable (unused_macro_rules).
123
}
124

            
125
// --- Individual Property Structs ---
126

            
127
// +spec:box-model:8c49fe - Border style properties (none, solid, double, dashed, etc.) and border color defaulting to element's color
128
// Border Style (border-*-style)
129
/// CSS `border-top-style` property (e.g. `solid`, `dashed`, `none`).
130
define_border_side_property!(StyleBorderTopStyle, BorderStyle, BorderStyle::None);
131
/// CSS `border-right-style` property (e.g. `solid`, `dashed`, `none`).
132
define_border_side_property!(StyleBorderRightStyle, BorderStyle, BorderStyle::None);
133
/// CSS `border-bottom-style` property (e.g. `solid`, `dashed`, `none`).
134
define_border_side_property!(StyleBorderBottomStyle, BorderStyle, BorderStyle::None);
135
/// CSS `border-left-style` property (e.g. `solid`, `dashed`, `none`).
136
define_border_side_property!(StyleBorderLeftStyle, BorderStyle, BorderStyle::None);
137

            
138
// Formatting implementations for border side style values
139
impl crate::codegen::format::FormatAsRustCode for StyleBorderTopStyle {
140
    fn format_as_rust_code(&self, tabs: usize) -> String {
141
        format!(
142
            "StyleBorderTopStyle {{ inner: {} }}",
143
            &self.inner.format_as_rust_code(tabs)
144
        )
145
    }
146
}
147

            
148
impl crate::codegen::format::FormatAsRustCode for StyleBorderRightStyle {
149
    fn format_as_rust_code(&self, tabs: usize) -> String {
150
        format!(
151
            "StyleBorderRightStyle {{ inner: {} }}",
152
            &self.inner.format_as_rust_code(tabs)
153
        )
154
    }
155
}
156

            
157
impl crate::codegen::format::FormatAsRustCode for StyleBorderLeftStyle {
158
    fn format_as_rust_code(&self, tabs: usize) -> String {
159
        format!(
160
            "StyleBorderLeftStyle {{ inner: {} }}",
161
            &self.inner.format_as_rust_code(tabs)
162
        )
163
    }
164
}
165

            
166
impl crate::codegen::format::FormatAsRustCode for StyleBorderBottomStyle {
167
    fn format_as_rust_code(&self, tabs: usize) -> String {
168
        format!(
169
            "StyleBorderBottomStyle {{ inner: {} }}",
170
            &self.inner.format_as_rust_code(tabs)
171
        )
172
    }
173
}
174

            
175
// Border Color (border-*-color)
176
/// CSS `border-top-color` property. Defaults to `ColorU::BLACK`.
177
define_border_side_property!(StyleBorderTopColor, ColorU);
178
/// CSS `border-right-color` property. Defaults to `ColorU::BLACK`.
179
define_border_side_property!(StyleBorderRightColor, ColorU);
180
/// CSS `border-bottom-color` property. Defaults to `ColorU::BLACK`.
181
define_border_side_property!(StyleBorderBottomColor, ColorU);
182
/// CSS `border-left-color` property. Defaults to `ColorU::BLACK`.
183
define_border_side_property!(StyleBorderLeftColor, ColorU);
184

            
185
// Border Width (border-*-width)
186
// The default width is 'medium', which corresponds to 3px.
187
// Import from pixel.rs for consistency.
188
/// CSS `border-top-width` property. Defaults to `MEDIUM_BORDER_THICKNESS` (3px).
189
define_border_side_property!(LayoutBorderTopWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
190
/// CSS `border-right-width` property. Defaults to `MEDIUM_BORDER_THICKNESS` (3px).
191
define_border_side_property!(LayoutBorderRightWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
192
/// CSS `border-bottom-width` property. Defaults to `MEDIUM_BORDER_THICKNESS` (3px).
193
define_border_side_property!(LayoutBorderBottomWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
194
/// CSS `border-left-width` property. Defaults to `MEDIUM_BORDER_THICKNESS` (3px).
195
define_border_side_property!(LayoutBorderLeftWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
196

            
197
macro_rules! impl_border_width_helpers {
198
    ($($t:ty),+) => { $(
199
        impl $t {
200
39
            #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
201
39
                Self { inner: self.inner.interpolate(&other.inner, t) }
202
39
            }
203
339666
            #[must_use] pub const fn const_px(value: isize) -> Self {
204
339666
                Self { inner: PixelValue::const_px(value) }
205
339666
            }
206
        }
207
    )+ };
208
}
209

            
210
impl_border_width_helpers!(
211
    LayoutBorderTopWidth,
212
    LayoutBorderRightWidth,
213
    LayoutBorderBottomWidth,
214
    LayoutBorderLeftWidth
215
);
216

            
217
/// Represents the three components of a border shorthand property, used as an intermediate
218
/// representation during parsing.
219
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
220
pub struct StyleBorderSide {
221
    pub border_width: PixelValue,
222
    pub border_style: BorderStyle,
223
    pub border_color: ColorU,
224
}
225

            
226
// --- PARSERS ---
227

            
228
// -- BorderStyle Parser --
229

            
230
#[cfg(feature = "parser")]
231
#[derive(Clone, PartialEq, Eq)]
232
pub enum CssBorderStyleParseError<'a> {
233
    InvalidStyle(&'a str),
234
}
235

            
236
#[cfg(feature = "parser")]
237
impl_debug_as_display!(CssBorderStyleParseError<'a>);
238
#[cfg(feature = "parser")]
239
impl_display! { CssBorderStyleParseError<'a>, {
240
    InvalidStyle(val) => format!("Invalid border style: \"{}\"", val),
241
}}
242

            
243
#[cfg(feature = "parser")]
244
#[derive(Debug, Clone, PartialEq, Eq)]
245
#[repr(C, u8)]
246
pub enum CssBorderStyleParseErrorOwned {
247
    InvalidStyle(AzString),
248
}
249

            
250
#[cfg(feature = "parser")]
251
impl CssBorderStyleParseError<'_> {
252
12
    #[must_use] pub fn to_contained(&self) -> CssBorderStyleParseErrorOwned {
253
12
        match self {
254
12
            CssBorderStyleParseError::InvalidStyle(s) => {
255
12
                CssBorderStyleParseErrorOwned::InvalidStyle((*s).to_string().into())
256
            }
257
        }
258
12
    }
259
}
260

            
261
#[cfg(feature = "parser")]
262
impl CssBorderStyleParseErrorOwned {
263
12
    #[must_use] pub fn to_shared(&self) -> CssBorderStyleParseError<'_> {
264
12
        match self {
265
12
            Self::InvalidStyle(s) => {
266
12
                CssBorderStyleParseError::InvalidStyle(s.as_str())
267
            }
268
        }
269
12
    }
270
}
271

            
272
#[cfg(feature = "parser")]
273
/// # Errors
274
///
275
/// Returns an error if `input` is not a valid CSS `border-style` value.
276
9920
pub fn parse_border_style(input: &str) -> Result<BorderStyle, CssBorderStyleParseError<'_>> {
277
9920
    match input.trim() {
278
9920
        "none" => Ok(BorderStyle::None),
279
9908
        "solid" => Ok(BorderStyle::Solid),
280
257
        "double" => Ok(BorderStyle::Double),
281
232
        "dotted" => Ok(BorderStyle::Dotted),
282
205
        "dashed" => Ok(BorderStyle::Dashed),
283
172
        "hidden" => Ok(BorderStyle::Hidden),
284
151
        "groove" => Ok(BorderStyle::Groove),
285
141
        "ridge" => Ok(BorderStyle::Ridge),
286
131
        "inset" => Ok(BorderStyle::Inset),
287
118
        "outset" => Ok(BorderStyle::Outset),
288
109
        _ => Err(CssBorderStyleParseError::InvalidStyle(input)),
289
    }
290
9920
}
291

            
292
// -- Shorthand Parser (for `border`, `border-top`, etc.) --
293

            
294
#[cfg(feature = "parser")]
295
#[derive(Clone, PartialEq)]
296
pub enum CssBorderSideParseError<'a> {
297
    InvalidDeclaration(&'a str),
298
    Width(CssPixelValueParseError<'a>),
299
    Style(CssBorderStyleParseError<'a>),
300
    Color(CssColorParseError<'a>),
301
}
302

            
303
#[cfg(feature = "parser")]
304
impl_debug_as_display!(CssBorderSideParseError<'a>);
305
#[cfg(feature = "parser")]
306
impl_display! { CssBorderSideParseError<'a>, {
307
    InvalidDeclaration(e) => format!("Invalid border declaration: \"{}\"", e),
308
    Width(e) => format!("Invalid border-width component: {}", e),
309
    Style(e) => format!("Invalid border-style component: {}", e),
310
    Color(e) => format!("Invalid border-color component: {}", e),
311
}}
312

            
313
#[cfg(feature = "parser")]
314
impl_from!(CssPixelValueParseError<'a>, CssBorderSideParseError::Width);
315
#[cfg(feature = "parser")]
316
impl_from!(CssBorderStyleParseError<'a>, CssBorderSideParseError::Style);
317
#[cfg(feature = "parser")]
318
impl_from!(CssColorParseError<'a>, CssBorderSideParseError::Color);
319

            
320
#[cfg(feature = "parser")]
321
#[derive(Debug, Clone, PartialEq)]
322
#[repr(C, u8)]
323
pub enum CssBorderSideParseErrorOwned {
324
    InvalidDeclaration(AzString),
325
    Width(CssPixelValueParseErrorOwned),
326
    Style(CssBorderStyleParseErrorOwned),
327
    Color(CssColorParseErrorOwned),
328
}
329

            
330
#[cfg(feature = "parser")]
331
impl CssBorderSideParseError<'_> {
332
12
    #[must_use] pub fn to_contained(&self) -> CssBorderSideParseErrorOwned {
333
12
        match self {
334
5
            CssBorderSideParseError::InvalidDeclaration(s) => {
335
5
                CssBorderSideParseErrorOwned::InvalidDeclaration((*s).to_string().into())
336
            }
337
2
            CssBorderSideParseError::Width(e) => {
338
2
                CssBorderSideParseErrorOwned::Width(e.to_contained())
339
            }
340
2
            CssBorderSideParseError::Style(e) => {
341
2
                CssBorderSideParseErrorOwned::Style(e.to_contained())
342
            }
343
3
            CssBorderSideParseError::Color(e) => {
344
3
                CssBorderSideParseErrorOwned::Color(e.to_contained())
345
            }
346
        }
347
12
    }
348
}
349

            
350
#[cfg(feature = "parser")]
351
impl CssBorderSideParseErrorOwned {
352
11
    #[must_use] pub fn to_shared(&self) -> CssBorderSideParseError<'_> {
353
11
        match self {
354
4
            Self::InvalidDeclaration(s) => {
355
4
                CssBorderSideParseError::InvalidDeclaration(s.as_str())
356
            }
357
2
            Self::Width(e) => CssBorderSideParseError::Width(e.to_shared()),
358
2
            Self::Style(e) => CssBorderSideParseError::Style(e.to_shared()),
359
3
            Self::Color(e) => CssBorderSideParseError::Color(e.to_shared()),
360
        }
361
11
    }
362
}
363

            
364
// Type alias for compatibility with old code
365
#[cfg(feature = "parser")]
366
pub type CssBorderParseError<'a> = CssBorderSideParseError<'a>;
367

            
368
/// Newtype wrapper around `CssBorderSideParseErrorOwned` for the `border` shorthand.
369
#[cfg(feature = "parser")]
370
#[derive(Debug, Clone, PartialEq)]
371
#[repr(C)]
372
pub struct CssBorderParseErrorOwned {
373
    pub inner: CssBorderSideParseErrorOwned,
374
}
375

            
376
#[cfg(feature = "parser")]
377
impl From<CssBorderSideParseErrorOwned> for CssBorderParseErrorOwned {
378
1
    fn from(v: CssBorderSideParseErrorOwned) -> Self {
379
1
        Self { inner: v }
380
1
    }
381
}
382

            
383
/// Parses a border shorthand property such as "1px solid red".
384
/// Handles any order of components and applies defaults for missing values.
385
#[cfg(feature = "parser")]
386
9706
fn parse_border_side(
387
9706
    input: &str,
388
9706
) -> Result<StyleBorderSide, CssBorderSideParseError<'_>> {
389
9706
    let mut width = None;
390
9706
    let mut style = None;
391
9706
    let mut color = None;
392

            
393
9706
    if input.trim().is_empty() {
394
9
        return Err(CssBorderSideParseError::InvalidDeclaration(input));
395
9697
    }
396

            
397
29016
    for part in input.split_whitespace() {
398
        // Try to parse as a width.
399
29016
        if width.is_none() {
400
9737
            if let Ok(w) = parse_border_width_value(part) {
401
9657
                width = Some(w);
402
9657
                continue;
403
80
            }
404
19279
        }
405

            
406
        // Try to parse as a style.
407
19359
        if style.is_none() {
408
9701
            if let Ok(s) = parse_border_style(part) {
409
9677
                style = Some(s);
410
9677
                continue;
411
24
            }
412
9658
        }
413

            
414
        // Try to parse as a color.
415
9682
        if color.is_none() {
416
9676
            if let Ok(c) = parse_css_color(part) {
417
9627
                color = Some(c);
418
9627
                continue;
419
49
            }
420
6
        }
421

            
422
        // If we get here, the part didn't match anything, or a value was specified twice.
423
55
        return Err(CssBorderSideParseError::InvalidDeclaration(input));
424
    }
425

            
426
9642
    Ok(StyleBorderSide {
427
9642
        border_width: width.unwrap_or(MEDIUM_BORDER_THICKNESS),
428
9642
        border_style: style.unwrap_or(BorderStyle::None),
429
9642
        border_color: color.unwrap_or(ColorU::BLACK),
430
9642
    })
431
9706
}
432

            
433
// --- Individual Property Parsers ---
434

            
435
#[cfg(feature = "parser")]
436
9866
fn parse_border_width_value(
437
9866
    input: &str,
438
9866
) -> Result<PixelValue, CssPixelValueParseError<'_>> {
439
9866
    match input.trim() {
440
9866
        "thin" => Ok(THIN_BORDER_THICKNESS),
441
9858
        "medium" => Ok(MEDIUM_BORDER_THICKNESS),
442
9852
        "thick" => Ok(THICK_BORDER_THICKNESS),
443
9842
        _ => parse_pixel_value(input),
444
    }
445
9866
}
446

            
447
#[cfg(feature = "parser")]
448
/// # Errors
449
///
450
/// Returns an error if `input` is not a valid CSS `border-top-width` value.
451
34
pub fn parse_border_top_width(
452
34
    input: &str,
453
34
) -> Result<LayoutBorderTopWidth, CssPixelValueParseError<'_>> {
454
34
    parse_border_width_value(input).map(|inner| LayoutBorderTopWidth { inner })
455
34
}
456

            
457
#[cfg(feature = "parser")]
458
/// # Errors
459
///
460
/// Returns an error if `input` is not a valid CSS `border-right-width` value.
461
14
pub fn parse_border_right_width(
462
14
    input: &str,
463
14
) -> Result<LayoutBorderRightWidth, CssPixelValueParseError<'_>> {
464
14
    parse_border_width_value(input).map(|inner| LayoutBorderRightWidth { inner })
465
14
}
466

            
467
#[cfg(feature = "parser")]
468
/// # Errors
469
///
470
/// Returns an error if `input` is not a valid CSS `border-bottom-width` value.
471
14
pub fn parse_border_bottom_width(
472
14
    input: &str,
473
14
) -> Result<LayoutBorderBottomWidth, CssPixelValueParseError<'_>> {
474
14
    parse_border_width_value(input).map(|inner| LayoutBorderBottomWidth { inner })
475
14
}
476

            
477
#[cfg(feature = "parser")]
478
/// # Errors
479
///
480
/// Returns an error if `input` is not a valid CSS `border-left-width` value.
481
15
pub fn parse_border_left_width(
482
15
    input: &str,
483
15
) -> Result<LayoutBorderLeftWidth, CssPixelValueParseError<'_>> {
484
15
    parse_border_width_value(input).map(|inner| LayoutBorderLeftWidth { inner })
485
15
}
486

            
487
#[cfg(feature = "parser")]
488
/// # Errors
489
///
490
/// Returns an error if `input` is not a valid CSS `border-top-style` value.
491
17
pub fn parse_border_top_style(
492
17
    input: &str,
493
17
) -> Result<StyleBorderTopStyle, CssBorderStyleParseError<'_>> {
494
17
    parse_border_style(input).map(|inner| StyleBorderTopStyle { inner })
495
17
}
496
#[cfg(feature = "parser")]
497
/// # Errors
498
///
499
/// Returns an error if `input` is not a valid CSS `border-right-style` value.
500
17
pub fn parse_border_right_style(
501
17
    input: &str,
502
17
) -> Result<StyleBorderRightStyle, CssBorderStyleParseError<'_>> {
503
17
    parse_border_style(input).map(|inner| StyleBorderRightStyle { inner })
504
17
}
505
#[cfg(feature = "parser")]
506
/// # Errors
507
///
508
/// Returns an error if `input` is not a valid CSS `border-bottom-style` value.
509
17
pub fn parse_border_bottom_style(
510
17
    input: &str,
511
17
) -> Result<StyleBorderBottomStyle, CssBorderStyleParseError<'_>> {
512
17
    parse_border_style(input).map(|inner| StyleBorderBottomStyle { inner })
513
17
}
514
#[cfg(feature = "parser")]
515
/// # Errors
516
///
517
/// Returns an error if `input` is not a valid CSS `border-left-style` value.
518
18
pub fn parse_border_left_style(
519
18
    input: &str,
520
18
) -> Result<StyleBorderLeftStyle, CssBorderStyleParseError<'_>> {
521
18
    parse_border_style(input).map(|inner| StyleBorderLeftStyle { inner })
522
18
}
523

            
524
#[cfg(feature = "parser")]
525
/// # Errors
526
///
527
/// Returns an error if `input` is not a valid CSS `border-top-color` value.
528
41
pub fn parse_border_top_color(
529
41
    input: &str,
530
41
) -> Result<StyleBorderTopColor, CssColorParseError<'_>> {
531
41
    parse_css_color(input).map(|inner| StyleBorderTopColor { inner })
532
41
}
533
#[cfg(feature = "parser")]
534
/// # Errors
535
///
536
/// Returns an error if `input` is not a valid CSS `border-right-color` value.
537
27
pub fn parse_border_right_color(
538
27
    input: &str,
539
27
) -> Result<StyleBorderRightColor, CssColorParseError<'_>> {
540
27
    parse_css_color(input).map(|inner| StyleBorderRightColor { inner })
541
27
}
542
#[cfg(feature = "parser")]
543
/// # Errors
544
///
545
/// Returns an error if `input` is not a valid CSS `border-bottom-color` value.
546
26
pub fn parse_border_bottom_color(
547
26
    input: &str,
548
26
) -> Result<StyleBorderBottomColor, CssColorParseError<'_>> {
549
26
    parse_css_color(input).map(|inner| StyleBorderBottomColor { inner })
550
26
}
551
#[cfg(feature = "parser")]
552
/// # Errors
553
///
554
/// Returns an error if `input` is not a valid CSS `border-left-color` value.
555
34
pub fn parse_border_left_color(
556
34
    input: &str,
557
34
) -> Result<StyleBorderLeftColor, CssColorParseError<'_>> {
558
34
    parse_css_color(input).map(|inner| StyleBorderLeftColor { inner })
559
34
}
560

            
561
// --- Border Color Shorthand ---
562

            
563
/// Parsed result of `border-color` shorthand (1-4 color values)
564
#[cfg(feature = "parser")]
565
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
566
pub struct StyleBorderColors {
567
    pub top: ColorU,
568
    pub right: ColorU,
569
    pub bottom: ColorU,
570
    pub left: ColorU,
571
}
572

            
573
/// Parses `border-color` shorthand: 1-4 color values
574
/// - 1 value: all sides
575
/// - 2 values: top/bottom, left/right
576
/// - 3 values: top, left/right, bottom
577
/// - 4 values: top, right, bottom, left
578
#[cfg(feature = "parser")]
579
/// # Errors
580
///
581
/// Returns an error if `input` is not a valid CSS `border-color` value.
582
21
pub fn parse_style_border_color(
583
21
    input: &str,
584
21
) -> Result<StyleBorderColors, CssColorParseError<'_>> {
585
21
    let input = input.trim();
586
21
    let parts: Vec<&str> = input.split_whitespace().collect();
587

            
588
21
    match parts.len() {
589
        1 => {
590
3
            let color = parse_css_color(parts[0])?;
591
1
            Ok(StyleBorderColors {
592
1
                top: color,
593
1
                right: color,
594
1
                bottom: color,
595
1
                left: color,
596
1
            })
597
        }
598
        2 => {
599
3
            let top_bottom = parse_css_color(parts[0])?;
600
3
            let left_right = parse_css_color(parts[1])?;
601
1
            Ok(StyleBorderColors {
602
1
                top: top_bottom,
603
1
                right: left_right,
604
1
                bottom: top_bottom,
605
1
                left: left_right,
606
1
            })
607
        }
608
        3 => {
609
6
            let top = parse_css_color(parts[0])?;
610
6
            let left_right = parse_css_color(parts[1])?;
611
6
            let bottom = parse_css_color(parts[2])?;
612
5
            Ok(StyleBorderColors {
613
5
                top,
614
5
                right: left_right,
615
5
                bottom,
616
5
                left: left_right,
617
5
            })
618
        }
619
        4 => {
620
3
            let top = parse_css_color(parts[0])?;
621
3
            let right = parse_css_color(parts[1])?;
622
3
            let bottom = parse_css_color(parts[2])?;
623
3
            let left = parse_css_color(parts[3])?;
624
2
            Ok(StyleBorderColors {
625
2
                top,
626
2
                right,
627
2
                bottom,
628
2
                left,
629
2
            })
630
        }
631
6
        _ => Err(CssColorParseError::InvalidColor(input)),
632
    }
633
21
}
634

            
635
// --- Border Style Shorthand ---
636

            
637
/// Parsed result of `border-style` shorthand (1-4 style values)
638
#[cfg(feature = "parser")]
639
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
640
pub struct StyleBorderStyles {
641
    pub top: BorderStyle,
642
    pub right: BorderStyle,
643
    pub bottom: BorderStyle,
644
    pub left: BorderStyle,
645
}
646

            
647
/// Parses `border-style` shorthand: 1-4 style values
648
#[cfg(feature = "parser")]
649
/// # Errors
650
///
651
/// Returns an error if `input` is not a valid CSS `border-style` value.
652
30
pub fn parse_style_border_style(
653
30
    input: &str,
654
30
) -> Result<StyleBorderStyles, CssBorderStyleParseError<'_>> {
655
30
    let input = input.trim();
656
30
    let parts: Vec<&str> = input.split_whitespace().collect();
657

            
658
30
    match parts.len() {
659
        1 => {
660
15
            let style = parse_border_style(parts[0])?;
661
13
            Ok(StyleBorderStyles {
662
13
                top: style,
663
13
                right: style,
664
13
                bottom: style,
665
13
                left: style,
666
13
            })
667
        }
668
        2 => {
669
3
            let top_bottom = parse_border_style(parts[0])?;
670
3
            let left_right = parse_border_style(parts[1])?;
671
1
            Ok(StyleBorderStyles {
672
1
                top: top_bottom,
673
1
                right: left_right,
674
1
                bottom: top_bottom,
675
1
                left: left_right,
676
1
            })
677
        }
678
        3 => {
679
2
            let top = parse_border_style(parts[0])?;
680
2
            let left_right = parse_border_style(parts[1])?;
681
2
            let bottom = parse_border_style(parts[2])?;
682
1
            Ok(StyleBorderStyles {
683
1
                top,
684
1
                right: left_right,
685
1
                bottom,
686
1
                left: left_right,
687
1
            })
688
        }
689
        4 => {
690
5
            let top = parse_border_style(parts[0])?;
691
5
            let right = parse_border_style(parts[1])?;
692
5
            let bottom = parse_border_style(parts[2])?;
693
5
            let left = parse_border_style(parts[3])?;
694
4
            Ok(StyleBorderStyles {
695
4
                top,
696
4
                right,
697
4
                bottom,
698
4
                left,
699
4
            })
700
        }
701
5
        _ => Err(CssBorderStyleParseError::InvalidStyle(input)),
702
    }
703
30
}
704

            
705
// --- Border Width Shorthand ---
706

            
707
/// Parsed result of `border-width` shorthand (1-4 width values)
708
#[cfg(feature = "parser")]
709
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
710
pub struct StyleBorderWidths {
711
    pub top: PixelValue,
712
    pub right: PixelValue,
713
    pub bottom: PixelValue,
714
    pub left: PixelValue,
715
}
716

            
717
/// Parses `border-width` shorthand: 1-4 width values
718
#[cfg(feature = "parser")]
719
/// # Errors
720
///
721
/// Returns an error if `input` is not a valid CSS `border-width` value.
722
45
pub fn parse_style_border_width(
723
45
    input: &str,
724
45
) -> Result<StyleBorderWidths, CssPixelValueParseError<'_>> {
725
45
    let input = input.trim();
726
45
    let parts: Vec<&str> = input.split_whitespace().collect();
727

            
728
45
    match parts.len() {
729
        1 => {
730
30
            let width = parse_pixel_value(parts[0])?;
731
25
            Ok(StyleBorderWidths {
732
25
                top: width,
733
25
                right: width,
734
25
                bottom: width,
735
25
                left: width,
736
25
            })
737
        }
738
        2 => {
739
4
            let top_bottom = parse_pixel_value(parts[0])?;
740
3
            let left_right = parse_pixel_value(parts[1])?;
741
1
            Ok(StyleBorderWidths {
742
1
                top: top_bottom,
743
1
                right: left_right,
744
1
                bottom: top_bottom,
745
1
                left: left_right,
746
1
            })
747
        }
748
        3 => {
749
2
            let top = parse_pixel_value(parts[0])?;
750
2
            let left_right = parse_pixel_value(parts[1])?;
751
2
            let bottom = parse_pixel_value(parts[2])?;
752
1
            Ok(StyleBorderWidths {
753
1
                top,
754
1
                right: left_right,
755
1
                bottom,
756
1
                left: left_right,
757
1
            })
758
        }
759
        4 => {
760
4
            let top = parse_pixel_value(parts[0])?;
761
4
            let right = parse_pixel_value(parts[1])?;
762
4
            let bottom = parse_pixel_value(parts[2])?;
763
4
            let left = parse_pixel_value(parts[3])?;
764
3
            Ok(StyleBorderWidths {
765
3
                top,
766
3
                right,
767
3
                bottom,
768
3
                left,
769
3
            })
770
        }
771
5
        _ => Err(CssPixelValueParseError::InvalidPixelValue(input)),
772
    }
773
45
}
774

            
775
// Compatibility alias
776
#[cfg(feature = "parser")]
777
/// # Errors
778
///
779
/// Returns an error if `input` is not a valid CSS `border` value.
780
9647
pub fn parse_style_border(input: &str) -> Result<StyleBorderSide, CssBorderParseError<'_>> {
781
9647
    parse_border_side(input)
782
9647
}
783

            
784
#[cfg(all(test, feature = "parser"))]
785
mod tests {
786
    use super::*;
787

            
788
    #[test]
789
1
    fn test_parse_border_style() {
790
1
        assert_eq!(parse_border_style("solid").unwrap(), BorderStyle::Solid);
791
1
        assert_eq!(parse_border_style("dotted").unwrap(), BorderStyle::Dotted);
792
1
        assert_eq!(parse_border_style("none").unwrap(), BorderStyle::None);
793
1
        assert_eq!(
794
1
            parse_border_style("  dashed  ").unwrap(),
795
            BorderStyle::Dashed
796
        );
797
1
        assert!(parse_border_style("solidd").is_err());
798
1
    }
799

            
800
    #[test]
801
1
    fn test_parse_border_side_shorthand() {
802
        // Full
803
1
        let result = parse_border_side("2px dotted #ff0000").unwrap();
804
1
        assert_eq!(result.border_width, PixelValue::px(2.0));
805
1
        assert_eq!(result.border_style, BorderStyle::Dotted);
806
1
        assert_eq!(result.border_color, ColorU::new_rgb(255, 0, 0));
807

            
808
        // Different order
809
1
        let result = parse_border_side("solid green 1em").unwrap();
810
1
        assert_eq!(result.border_width, PixelValue::em(1.0));
811
1
        assert_eq!(result.border_style, BorderStyle::Solid);
812
1
        assert_eq!(result.border_color, ColorU::new_rgb(0, 128, 0));
813

            
814
        // Missing width
815
1
        let result = parse_border_side("ridge #f0f").unwrap();
816
1
        assert_eq!(result.border_width, MEDIUM_BORDER_THICKNESS); // default
817
1
        assert_eq!(result.border_style, BorderStyle::Ridge);
818
1
        assert_eq!(result.border_color, ColorU::new_rgb(255, 0, 255));
819

            
820
        // Missing style
821
1
        let result = parse_border_side("5pt blue").unwrap();
822
1
        assert_eq!(result.border_width, PixelValue::pt(5.0));
823
1
        assert_eq!(result.border_style, BorderStyle::None); // default
824
1
        assert_eq!(result.border_color, ColorU::BLUE);
825

            
826
        // Missing color
827
1
        let result = parse_border_side("thick double").unwrap();
828
1
        assert_eq!(result.border_width, PixelValue::px(5.0));
829
1
        assert_eq!(result.border_style, BorderStyle::Double);
830
1
        assert_eq!(result.border_color, ColorU::BLACK); // default
831

            
832
        // Only one value
833
1
        let result = parse_border_side("inset").unwrap();
834
1
        assert_eq!(result.border_width, MEDIUM_BORDER_THICKNESS);
835
1
        assert_eq!(result.border_style, BorderStyle::Inset);
836
1
        assert_eq!(result.border_color, ColorU::BLACK);
837
1
    }
838

            
839
    #[test]
840
1
    fn test_parse_border_side_invalid() {
841
        // Two widths
842
1
        assert!(parse_border_side("1px 2px solid red").is_err());
843
        // Two styles
844
1
        assert!(parse_border_side("solid dashed red").is_err());
845
        // Two colors
846
1
        assert!(parse_border_side("red blue solid").is_err());
847
        // Empty
848
1
        assert!(parse_border_side("").is_err());
849
        // Unknown keyword
850
1
        assert!(parse_border_side("1px unknown red").is_err());
851
1
    }
852

            
853
    #[test]
854
1
    fn test_parse_longhand_border() {
855
1
        assert_eq!(
856
1
            parse_border_top_width("1.5em").unwrap().inner,
857
1
            PixelValue::em(1.5)
858
        );
859
1
        assert_eq!(
860
1
            parse_border_left_style("groove").unwrap().inner,
861
            BorderStyle::Groove
862
        );
863
1
        assert_eq!(
864
1
            parse_border_right_color("rgba(10, 20, 30, 0.5)")
865
1
                .unwrap()
866
                .inner,
867
1
            ColorU::new(10, 20, 30, 128)
868
        );
869
1
    }
870
}
871

            
872
#[cfg(test)]
873
mod autotest_generated {
874
    use super::*;
875

            
876
    const ALL_STYLES: [BorderStyle; 10] = [
877
        BorderStyle::None,
878
        BorderStyle::Solid,
879
        BorderStyle::Double,
880
        BorderStyle::Dotted,
881
        BorderStyle::Dashed,
882
        BorderStyle::Hidden,
883
        BorderStyle::Groove,
884
        BorderStyle::Ridge,
885
        BorderStyle::Inset,
886
        BorderStyle::Outset,
887
    ];
888

            
889
    // =====================================================================
890
    // BorderStyle: Display / PrintAsCssValue / Default
891
    // =====================================================================
892

            
893
    #[test]
894
    fn border_style_display_is_a_unique_lowercase_keyword_for_every_variant() {
895
        let mut seen: Vec<String> = Vec::new();
896
        for style in ALL_STYLES {
897
            let s = style.to_string();
898
            assert!(!s.is_empty(), "{style:?} renders as the empty string");
899
            assert!(
900
                s.chars().all(|c| c.is_ascii_lowercase()),
901
                "{style:?} renders as {s:?}, which is not a lowercase ASCII keyword"
902
            );
903
            assert!(
904
                !seen.contains(&s),
905
                "two BorderStyle variants both render as {s:?} (copy-paste in Display)"
906
            );
907
            seen.push(s);
908
        }
909
        assert_eq!(seen.len(), ALL_STYLES.len());
910
    }
911

            
912
    #[test]
913
    fn border_style_print_as_css_value_matches_display() {
914
        for style in ALL_STYLES {
915
            assert_eq!(style.print_as_css_value(), style.to_string());
916
        }
917
    }
918

            
919
    #[test]
920
    fn border_style_default_is_none_and_formats_as_none() {
921
        assert_eq!(BorderStyle::default(), BorderStyle::None);
922
        assert_eq!(BorderStyle::default().to_string(), "none");
923
    }
924

            
925
    // =====================================================================
926
    // Side-property structs: Default / Debug / const_px / interpolate
927
    // =====================================================================
928

            
929
    #[test]
930
    fn border_side_property_defaults_match_the_css_initial_values() {
931
        // border-*-style initial value is `none`
932
        assert_eq!(StyleBorderTopStyle::default().inner, BorderStyle::None);
933
        assert_eq!(StyleBorderRightStyle::default().inner, BorderStyle::None);
934
        assert_eq!(StyleBorderBottomStyle::default().inner, BorderStyle::None);
935
        assert_eq!(StyleBorderLeftStyle::default().inner, BorderStyle::None);
936

            
937
        // border-*-color has no `currentcolor` here; the documented stand-in is BLACK
938
        assert_eq!(StyleBorderTopColor::default().inner, ColorU::BLACK);
939
        assert_eq!(StyleBorderRightColor::default().inner, ColorU::BLACK);
940
        assert_eq!(StyleBorderBottomColor::default().inner, ColorU::BLACK);
941
        assert_eq!(StyleBorderLeftColor::default().inner, ColorU::BLACK);
942

            
943
        // border-*-width initial value is `medium` (3px)
944
        assert_eq!(
945
            LayoutBorderTopWidth::default().inner,
946
            MEDIUM_BORDER_THICKNESS
947
        );
948
        assert_eq!(
949
            LayoutBorderRightWidth::default().inner,
950
            MEDIUM_BORDER_THICKNESS
951
        );
952
        assert_eq!(
953
            LayoutBorderBottomWidth::default().inner,
954
            MEDIUM_BORDER_THICKNESS
955
        );
956
        assert_eq!(
957
            LayoutBorderLeftWidth::default().inner,
958
            MEDIUM_BORDER_THICKNESS
959
        );
960
        assert_eq!(MEDIUM_BORDER_THICKNESS, PixelValue::px(3.0));
961
    }
962

            
963
    #[test]
964
    fn border_side_property_debug_impls_are_the_documented_shapes() {
965
        // The macro deliberately overrides Debug: styles print the keyword,
966
        // colors print the 8-digit hash, widths print the pixel value.
967
        assert_eq!(format!("{:?}", StyleBorderTopStyle::default()), "none");
968
        assert_eq!(
969
            format!(
970
                "{:?}",
971
                StyleBorderLeftStyle {
972
                    inner: BorderStyle::Groove
973
                }
974
            ),
975
            "groove"
976
        );
977
        assert_eq!(
978
            format!("{:?}", StyleBorderTopColor::default()),
979
            "#000000ff"
980
        );
981
        assert_eq!(format!("{:?}", LayoutBorderTopWidth::default()), "3px");
982
    }
983

            
984
    #[test]
985
    fn layout_border_width_const_px_matches_the_runtime_constructor() {
986
        assert_eq!(
987
            LayoutBorderTopWidth::const_px(5).inner,
988
            PixelValue::px(5.0)
989
        );
990
        assert_eq!(LayoutBorderRightWidth::const_px(0).inner, PixelValue::zero());
991
        assert_eq!(
992
            LayoutBorderBottomWidth::const_px(-2).inner,
993
            PixelValue::px(-2.0)
994
        );
995
        // The largest magnitude `const_px` can scale by FP_PRECISION_MULTIPLIER
996
        // (1000) without overflowing the isize multiply. Anything beyond this
997
        // overflows — see the FloatValue::const_new tests in length.rs.
998
        let max_safe = isize::MAX / 1000;
999
        assert!(LayoutBorderLeftWidth::const_px(max_safe)
            .inner
            .number
            .get()
            .is_finite());
    }
    #[test]
    fn layout_border_width_interpolate_endpoints_are_exact() {
        let a = LayoutBorderTopWidth::const_px(0);
        let b = LayoutBorderTopWidth::const_px(10);
        assert_eq!(a.interpolate(&b, 0.0), a);
        assert_eq!(a.interpolate(&b, 1.0), b);
        assert_eq!(a.interpolate(&b, 0.5).inner, PixelValue::px(5.0));
    }
    #[test]
    fn layout_border_width_interpolate_stays_finite_for_hostile_t() {
        let a = LayoutBorderTopWidth::const_px(0);
        let b = LayoutBorderTopWidth::const_px(10);
        for t in [
            0.0,
            1.0,
            -1.0,
            2.0,
            f32::NAN,
            f32::INFINITY,
            f32::NEG_INFINITY,
            f32::MAX,
            f32::MIN,
        ] {
            // A NaN/inf must never leak out of the animation path: FloatValue
            // stores an isize, so the cast saturates instead of propagating.
            assert!(
                a.interpolate(&b, t).inner.number.get().is_finite(),
                "interpolate(t = {t}) produced a non-finite width"
            );
            assert!(b.interpolate(&a, t).inner.number.get().is_finite());
        }
    }
    #[test]
    fn layout_border_width_interpolate_across_metrics_stays_finite() {
        let px = LayoutBorderRightWidth {
            inner: PixelValue::px(4.0),
        };
        let em = LayoutBorderRightWidth {
            inner: PixelValue::em(2.0),
        };
        let percent = LayoutBorderRightWidth {
            inner: PixelValue::percent(100.0),
        };
        for t in [0.0, 0.5, 1.0, -3.0, f32::NAN, f32::INFINITY] {
            assert!(px.interpolate(&em, t).inner.number.get().is_finite());
            assert!(em.interpolate(&percent, t).inner.number.get().is_finite());
            assert!(percent.interpolate(&px, t).inner.number.get().is_finite());
        }
    }
    #[test]
    fn style_border_color_interpolate_endpoints_are_exact() {
        let black = StyleBorderTopColor {
            inner: ColorU::BLACK,
        };
        let white = StyleBorderTopColor {
            inner: ColorU::WHITE,
        };
        assert_eq!(black.interpolate(&white, 0.0).inner, ColorU::BLACK);
        assert_eq!(black.interpolate(&white, 1.0).inner, ColorU::WHITE);
        let mid = black.interpolate(&white, 0.5).inner;
        assert_eq!((mid.r, mid.g, mid.b), (128, 128, 128));
    }
    #[test]
    fn style_border_color_interpolate_hostile_t_does_not_panic() {
        let a = StyleBorderLeftColor {
            inner: ColorU::new(10, 20, 30, 40),
        };
        let b = StyleBorderLeftColor {
            inner: ColorU::new(200, 210, 220, 230),
        };
        for t in [
            -1000.0,
            1000.0,
            f32::NAN,
            f32::INFINITY,
            f32::NEG_INFINITY,
            f32::MAX,
        ] {
            // u8 channels saturate; the only requirement is that this returns.
            let _ = a.interpolate(&b, t);
            let _ = b.interpolate(&a, t);
        }
    }
    // =====================================================================
    // parse_border_style
    // =====================================================================
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_style_accepts_every_keyword() {
        assert_eq!(parse_border_style("none").unwrap(), BorderStyle::None);
        assert_eq!(parse_border_style("solid").unwrap(), BorderStyle::Solid);
        assert_eq!(parse_border_style("double").unwrap(), BorderStyle::Double);
        assert_eq!(parse_border_style("dotted").unwrap(), BorderStyle::Dotted);
        assert_eq!(parse_border_style("dashed").unwrap(), BorderStyle::Dashed);
        assert_eq!(parse_border_style("hidden").unwrap(), BorderStyle::Hidden);
        assert_eq!(parse_border_style("groove").unwrap(), BorderStyle::Groove);
        assert_eq!(parse_border_style("ridge").unwrap(), BorderStyle::Ridge);
        assert_eq!(parse_border_style("inset").unwrap(), BorderStyle::Inset);
        assert_eq!(parse_border_style("outset").unwrap(), BorderStyle::Outset);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_style_round_trips_through_display() {
        for style in ALL_STYLES {
            let encoded = style.to_string();
            assert_eq!(
                parse_border_style(&encoded).unwrap(),
                style,
                "{encoded} did not round-trip"
            );
            // and through the PrintAsCssValue path, which must agree
            assert_eq!(
                parse_border_style(&style.print_as_css_value()).unwrap(),
                style
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_style_trims_surrounding_whitespace() {
        for input in [" solid", "solid ", "\t\nsolid\r\n ", "   solid   "] {
            assert_eq!(
                parse_border_style(input).unwrap(),
                BorderStyle::Solid,
                "{input:?} should trim to `solid`"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_style_empty_and_whitespace_only_are_errors() {
        for input in ["", " ", "   ", "\t", "\n", "\r\n\t "] {
            assert!(
                parse_border_style(input).is_err(),
                "{input:?} must not parse as a border style"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_style_error_carries_the_untrimmed_input() {
        // The Ok path trims, but the Err path hands back the *raw* input.
        let input = "  bogus  ";
        let err = parse_border_style(input).unwrap_err();
        assert!(
            matches!(err, CssBorderStyleParseError::InvalidStyle(s) if s == input),
            "unexpected error payload: {err:?}"
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_style_rejects_uppercase_keywords() {
        // NOTE: CSS keywords are ASCII case-insensitive, so a spec-conformant
        // parser would accept these. This parser does not — asserted here so
        // the divergence is visible rather than silent.
        for input in ["SOLID", "Solid", "sOlId", "NONE", "Dashed"] {
            assert!(
                parse_border_style(input).is_err(),
                "{input:?} unexpectedly parsed (case-insensitivity was added?)"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_style_rejects_garbage_unicode_and_numbers() {
        for input in [
            "solidd",
            "soli",
            "solid solid",
            "solid;garbage",
            "solid!important",
            "0",
            "-0",
            "1px",
            "9223372036854775807",
            "NaN",
            "inf",
            "-inf",
            "\u{1F600}",
            "s\u{0301}olid",
            "sölid",
            "\u{0}",
            "\u{202e}solid",
            "sol\tid",
            "()",
            "solid()",
        ] {
            assert!(
                parse_border_style(input).is_err(),
                "{input:?} unexpectedly parsed as a border style"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_style_handles_huge_and_nested_input_without_panicking() {
        let huge = "a".repeat(100_000);
        assert!(parse_border_style(&huge).is_err());
        let repeated = "solid ".repeat(50_000);
        assert!(parse_border_style(&repeated).is_err());
        let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
        assert!(parse_border_style(&nested).is_err());
        let padded = format!("{}solid{}", " ".repeat(100_000), " ".repeat(100_000));
        assert_eq!(parse_border_style(&padded).unwrap(), BorderStyle::Solid);
    }
    // =====================================================================
    // border-*-style longhands
    // =====================================================================
    #[cfg(feature = "parser")]
    #[test]
    fn border_style_longhands_agree_with_parse_border_style() {
        for style in ALL_STYLES {
            let input = style.to_string();
            assert_eq!(parse_border_top_style(&input).unwrap().inner, style);
            assert_eq!(parse_border_right_style(&input).unwrap().inner, style);
            assert_eq!(parse_border_bottom_style(&input).unwrap().inner, style);
            assert_eq!(parse_border_left_style(&input).unwrap().inner, style);
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn border_style_longhands_reject_everything_parse_border_style_rejects() {
        for input in ["", "   ", "SOLID", "solidd", "\u{1F600}", "1px", "solid red"] {
            assert!(parse_border_top_style(input).is_err(), "top: {input:?}");
            assert!(parse_border_right_style(input).is_err(), "right: {input:?}");
            assert!(
                parse_border_bottom_style(input).is_err(),
                "bottom: {input:?}"
            );
            assert!(parse_border_left_style(input).is_err(), "left: {input:?}");
        }
    }
    // =====================================================================
    // parse_border_width_value (private)
    // =====================================================================
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_width_value_accepts_the_three_keywords() {
        assert_eq!(
            parse_border_width_value("thin").unwrap(),
            THIN_BORDER_THICKNESS
        );
        assert_eq!(
            parse_border_width_value("medium").unwrap(),
            MEDIUM_BORDER_THICKNESS
        );
        assert_eq!(
            parse_border_width_value("thick").unwrap(),
            THICK_BORDER_THICKNESS
        );
        // keywords are trimmed like everything else
        assert_eq!(
            parse_border_width_value("  \tthick\n ").unwrap(),
            THICK_BORDER_THICKNESS
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_width_value_rejects_uppercase_keywords() {
        // Same case-sensitivity divergence as parse_border_style.
        for input in ["THIN", "Medium", "THICK"] {
            assert!(
                parse_border_width_value(input).is_err(),
                "{input:?} unexpectedly parsed"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_width_value_empty_and_whitespace_only_are_errors() {
        for input in ["", " ", "\t\n", "    "] {
            let err = parse_border_width_value(input).unwrap_err();
            assert!(
                matches!(err, CssPixelValueParseError::EmptyString),
                "{input:?} -> {err:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_width_value_bare_number_is_interpreted_as_px() {
        assert_eq!(parse_border_width_value("0").unwrap(), PixelValue::px(0.0));
        assert_eq!(parse_border_width_value("42").unwrap(), PixelValue::px(42.0));
        assert_eq!(
            parse_border_width_value("1.5").unwrap(),
            PixelValue::px(1.5)
        );
        // -0 collapses to +0 once quantized into the isize-backed FloatValue
        assert_eq!(parse_border_width_value("-0").unwrap(), PixelValue::px(0.0));
        // negative widths are *accepted* (CSS would reject them) — pinned so a
        // future validity check is a deliberate change, not an accident.
        assert_eq!(
            parse_border_width_value("-5px").unwrap(),
            PixelValue::px(-5.0)
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_width_value_nan_saturates_to_zero() {
        // "NaN" is a valid f32 literal for Rust's FromStr, so this reaches
        // FloatValue::new(NaN) — which saturates to 0 rather than storing NaN.
        let parsed = parse_border_width_value("NaN").unwrap();
        assert!(parsed.number.get().is_finite());
        assert_eq!(parsed, PixelValue::px(0.0));
        let parsed = parse_border_width_value("NaNpx").unwrap();
        assert_eq!(parsed, PixelValue::px(0.0));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_width_value_infinities_and_overflow_saturate_finite() {
        for input in [
            "inf",
            "-inf",
            "infpx",
            "1e999",
            "-1e999",
            "1e40px",
            "340282350000000000000000000000000000000px", // ~f32::MAX
            "9223372036854775807",                       // i64::MAX
            "-9223372036854775808",                      // i64::MIN
        ] {
            let parsed = parse_border_width_value(input)
                .unwrap_or_else(|e| panic!("{input:?} failed to parse: {e:?}"));
            assert!(
                parsed.number.get().is_finite(),
                "{input:?} produced a non-finite width: {parsed:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_width_value_rejects_garbage_and_unicode() {
        for input in [
            "px",
            "em",
            "abc",
            "1px 2px",
            "1px;",
            "--1px",
            "1PX",
            "\u{1F600}",
            "1\u{1F600}px",
            "Ù¡px", // arabic-indic digit one
            "()",
            "calc(1px + 2px)",
        ] {
            assert!(
                parse_border_width_value(input).is_err(),
                "{input:?} unexpectedly parsed as a border width"
            );
        }
        // ...but note the suffix strip trims what's left of the number, so a
        // space between value and unit is silently accepted:
        assert_eq!(parse_border_width_value("1 px").unwrap(), PixelValue::px(1.0));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_width_value_bare_unit_reports_no_value_given() {
        let err = parse_border_width_value("px").unwrap_err();
        assert!(
            matches!(err, CssPixelValueParseError::NoValueGiven(..)),
            "expected NoValueGiven, got {err:?}"
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_width_value_huge_input_does_not_hang() {
        let huge_digits = format!("{}px", "9".repeat(1_000));
        let parsed = parse_border_width_value(&huge_digits).unwrap();
        assert!(parsed.number.get().is_finite());
        let huge_garbage = "z".repeat(100_000);
        assert!(parse_border_width_value(&huge_garbage).is_err());
        let padded = format!("{}1px{}", " ".repeat(50_000), " ".repeat(50_000));
        assert_eq!(
            parse_border_width_value(&padded).unwrap(),
            PixelValue::px(1.0)
        );
    }
    // =====================================================================
    // border-*-width longhands
    // =====================================================================
    #[cfg(feature = "parser")]
    #[test]
    fn border_width_longhands_agree_with_each_other() {
        for input in ["thin", "medium", "thick", "0", "1.5em", "3px", "50%", "-2pt"] {
            let expected = parse_border_width_value(input).unwrap();
            assert_eq!(parse_border_top_width(input).unwrap().inner, expected);
            assert_eq!(parse_border_right_width(input).unwrap().inner, expected);
            assert_eq!(parse_border_bottom_width(input).unwrap().inner, expected);
            assert_eq!(parse_border_left_width(input).unwrap().inner, expected);
        }
        for input in ["", "   ", "px", "abc", "\u{1F600}", "1px 2px"] {
            assert!(parse_border_top_width(input).is_err(), "top: {input:?}");
            assert!(parse_border_right_width(input).is_err(), "right: {input:?}");
            assert!(
                parse_border_bottom_width(input).is_err(),
                "bottom: {input:?}"
            );
            assert!(parse_border_left_width(input).is_err(), "left: {input:?}");
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn border_width_longhands_round_trip_through_display() {
        for value in [
            PixelValue::px(0.0),
            PixelValue::px(1.0),
            PixelValue::px(1.5),
            PixelValue::px(-3.25),
            PixelValue::em(2.0),
            PixelValue::rem(0.5),
            PixelValue::pt(12.0),
            PixelValue::inch(1.0),
            PixelValue::cm(2.5),
            PixelValue::mm(10.0),
            PixelValue::percent(50.0),
            THIN_BORDER_THICKNESS,
            MEDIUM_BORDER_THICKNESS,
            THICK_BORDER_THICKNESS,
        ] {
            let encoded = value.to_string();
            let decoded = parse_border_top_width(&encoded)
                .unwrap_or_else(|e| panic!("{encoded} failed to re-parse: {e:?}"))
                .inner;
            assert_eq!(decoded, value, "{encoded} did not round-trip");
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn border_width_longhands_inherit_the_vmin_suffix_shadowing_bug() {
        // FIXED (this pin flipped, as intended): the suffix table used to test "in"
        // before "vmin", so "5vmin" stripped "in" and failed to parse "5vm" — every
        // `border-width: 5vmin` was rejected. The table now orders "vmin" ahead of "in".
        assert_eq!(
            parse_border_top_width("5vmin").unwrap().inner,
            PixelValue::from_metric(crate::props::basic::SizeMetric::Vmin, 5.0)
        );
        assert_eq!(
            parse_border_left_width("5vmin").unwrap().inner,
            PixelValue::from_metric(crate::props::basic::SizeMetric::Vmin, 5.0)
        );
        assert_eq!(
            parse_border_top_width("5vmax").unwrap().inner,
            PixelValue::from_metric(crate::props::basic::SizeMetric::Vmax, 5.0)
        );
        assert_eq!(
            parse_border_top_width("5vw").unwrap().inner,
            PixelValue::from_metric(crate::props::basic::SizeMetric::Vw, 5.0)
        );
    }
    // =====================================================================
    // border-*-color longhands
    // =====================================================================
    #[cfg(feature = "parser")]
    #[test]
    fn border_color_longhands_agree_with_each_other() {
        for input in [
            "#ff0000",
            "#f0f",
            "#11223344",
            "red",
            "transparent",
            "rgb(1, 2, 3)",
            "rgba(10, 20, 30, 0.5)",
            "hsl(0, 100%, 50%)",
        ] {
            let expected = parse_css_color(input)
                .unwrap_or_else(|e| panic!("{input:?} failed to parse: {e:?}"));
            assert_eq!(parse_border_top_color(input).unwrap().inner, expected);
            assert_eq!(parse_border_right_color(input).unwrap().inner, expected);
            assert_eq!(parse_border_bottom_color(input).unwrap().inner, expected);
            assert_eq!(parse_border_left_color(input).unwrap().inner, expected);
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn border_color_longhands_round_trip_through_to_hash() {
        for color in [
            ColorU::BLACK,
            ColorU::WHITE,
            ColorU::RED,
            ColorU::BLUE,
            ColorU::TRANSPARENT,
            ColorU::new(1, 2, 3, 4),
            ColorU::new(255, 254, 253, 252),
            ColorU::new(0, 128, 0, 255),
        ] {
            let encoded = color.to_hash();
            assert_eq!(
                parse_border_left_color(&encoded)
                    .unwrap_or_else(|e| panic!("{encoded} failed to re-parse: {e:?}"))
                    .inner,
                color,
                "{encoded} did not round-trip"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn border_color_longhands_empty_input_is_an_error() {
        for input in ["", " ", "\t\n  "] {
            let err = parse_border_top_color(input).unwrap_err();
            assert!(
                matches!(err, CssColorParseError::EmptyInput),
                "{input:?} -> {err:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn border_color_longhands_reject_garbage_and_unicode() {
        for input in [
            "#",
            "#z",
            "#ff",
            "#fffff",
            "#\u{1F600}",
            "notacolor",
            "rgb(1, 2)",
            "rgb(1, 2, 3, 4, 5)",
            "\u{1F600}",
            "red;",
            "red blue",
            "0",
        ] {
            assert!(
                parse_border_top_color(input).is_err(),
                "{input:?} unexpectedly parsed as a color"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn border_color_longhands_never_panic_on_hostile_input() {
        let long = "f".repeat(100_000);
        let nested = format!("rgb{}1,2,3{}", "(".repeat(10_000), ")".repeat(10_000));
        let deep_parens = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
        let hostile: [&str; 18] = [
            "#",
            "##ff0000",
            "rgb(",
            "rgb()",
            "rgba(0,0,0,NaN)",
            "rgba(0,0,0,inf)",
            "rgb(-1,-2,-3)",
            "rgb(999,999,999)",
            "rgb(NaN, NaN, NaN)",
            "hsl(inf, 0%, 0%)",
            "hsla(NaN, NaN%, NaN%, NaN)",
            "\u{0}",
            "\u{202e}",
            "s\u{0301}",
            ")))",
            &long,
            &nested,
            &deep_parens,
        ];
        for input in hostile {
            // The contract is only "returns, never panics / never overflows the stack".
            let _ = parse_border_top_color(input);
            let _ = parse_border_right_color(input);
            let _ = parse_border_bottom_color(input);
            let _ = parse_border_left_color(input);
        }
    }
    // =====================================================================
    // parse_border_side / parse_style_border
    // =====================================================================
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_side_positive_control() {
        let side = parse_border_side("1px solid red").unwrap();
        assert_eq!(side.border_width, PixelValue::px(1.0));
        assert_eq!(side.border_style, BorderStyle::Solid);
        assert_eq!(side.border_color, ColorU::RED);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_side_is_component_order_independent() {
        let expected = StyleBorderSide {
            border_width: PixelValue::px(2.0),
            border_style: BorderStyle::Dashed,
            border_color: ColorU::new_rgb(0, 255, 0),
        };
        for input in [
            "2px dashed #00ff00",
            "2px #00ff00 dashed",
            "dashed 2px #00ff00",
            "dashed #00ff00 2px",
            "#00ff00 2px dashed",
            "#00ff00 dashed 2px",
            "  2px   dashed   #00ff00  ",
            "\t2px\ndashed\r#00ff00\t",
        ] {
            assert_eq!(
                parse_border_side(input).unwrap(),
                expected,
                "{input:?} parsed differently"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_side_applies_defaults_for_missing_components() {
        // Missing components fall back to medium / none / black.
        let only_style = parse_border_side("inset").unwrap();
        assert_eq!(only_style.border_width, MEDIUM_BORDER_THICKNESS);
        assert_eq!(only_style.border_style, BorderStyle::Inset);
        assert_eq!(only_style.border_color, ColorU::BLACK);
        let only_width = parse_border_side("7px").unwrap();
        assert_eq!(only_width.border_width, PixelValue::px(7.0));
        assert_eq!(only_width.border_style, BorderStyle::None);
        assert_eq!(only_width.border_color, ColorU::BLACK);
        let only_color = parse_border_side("blue").unwrap();
        assert_eq!(only_color.border_width, MEDIUM_BORDER_THICKNESS);
        assert_eq!(only_color.border_style, BorderStyle::None);
        assert_eq!(only_color.border_color, ColorU::BLUE);
        // keyword widths work in the shorthand too
        let keyword = parse_border_side("thin solid").unwrap();
        assert_eq!(keyword.border_width, THIN_BORDER_THICKNESS);
        assert_eq!(keyword.border_style, BorderStyle::Solid);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_side_rejects_duplicate_components() {
        for input in [
            "1px 2px solid red",
            "solid dashed red",
            "red blue solid",
            "1px solid red 2px",
            "1px solid red solid",
            "1px solid red red",
        ] {
            let err = parse_border_side(input).unwrap_err();
            assert!(
                matches!(err, CssBorderSideParseError::InvalidDeclaration(s) if s == input),
                "{input:?} -> {err:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_side_empty_and_whitespace_only_are_errors() {
        for input in ["", " ", "\t\n", "      "] {
            let err = parse_border_side(input).unwrap_err();
            // The raw (untrimmed) input is echoed back in the error.
            assert!(
                matches!(err, CssBorderSideParseError::InvalidDeclaration(s) if s == input),
                "{input:?} -> {err:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_side_rejects_unknown_tokens() {
        for input in [
            "1px unknown red",
            "1px solid red !important",
            "1px solid red;",
            "\u{1F600}",
            "1px solid \u{1F600}",
            "solid \u{0}",
            "1px, solid, red",
        ] {
            assert!(
                parse_border_side(input).is_err(),
                "{input:?} unexpectedly parsed as a border shorthand"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_side_hostile_numbers_stay_finite() {
        // "NaN" / "inf" are valid f32 literals, so they *do* parse as widths —
        // but the isize-backed FloatValue saturates them to a finite value.
        for input in ["NaN solid red", "inf solid red", "1e999 solid red"] {
            let side = parse_border_side(input)
                .unwrap_or_else(|e| panic!("{input:?} failed to parse: {e:?}"));
            assert!(
                side.border_width.number.get().is_finite(),
                "{input:?} produced a non-finite width: {:?}",
                side.border_width
            );
            assert_eq!(side.border_style, BorderStyle::Solid);
            assert_eq!(side.border_color, ColorU::RED);
        }
        assert_eq!(
            parse_border_side("NaN solid red").unwrap().border_width,
            PixelValue::px(0.0)
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_border_side_long_and_nested_input_does_not_hang() {
        // Repeated tokens: the 2nd `solid` cannot be re-assigned, so this must
        // bail out immediately rather than scanning all 50k tokens.
        let repeated = "solid ".repeat(50_000);
        assert!(parse_border_side(&repeated).is_err());
        let repeated_px = "1px ".repeat(50_000);
        assert!(parse_border_side(&repeated_px).is_err());
        let huge_token = "z".repeat(100_000);
        assert!(parse_border_side(&huge_token).is_err());
        let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
        assert!(parse_border_side(&nested).is_err());
        let padded = format!("{}1px solid red{}", " ".repeat(50_000), " ".repeat(50_000));
        assert_eq!(
            parse_border_side(&padded).unwrap().border_style,
            BorderStyle::Solid
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_is_an_alias_of_parse_border_side() {
        for input in [
            "1px solid red",
            "thick double",
            "inset",
            "",
            "   ",
            "1px 2px solid red",
            "\u{1F600}",
            "solid green 1em",
        ] {
            match (parse_style_border(input), parse_border_side(input)) {
                (Ok(a), Ok(b)) => assert_eq!(a, b, "{input:?}"),
                (Err(a), Err(b)) => assert_eq!(a, b, "{input:?}"),
                (a, b) => panic!("{input:?}: alias disagrees: {a:?} vs {b:?}"),
            }
        }
    }
    // =====================================================================
    // border-color shorthand
    // =====================================================================
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_color_expands_one_to_four_values() {
        let red = ColorU::RED;
        let blue = ColorU::BLUE;
        let green = ColorU::new_rgb(0, 128, 0);
        let white = ColorU::WHITE;
        let one = parse_style_border_color("red").unwrap();
        assert_eq!(
            one,
            StyleBorderColors {
                top: red,
                right: red,
                bottom: red,
                left: red
            }
        );
        // 2 values: top/bottom, left/right
        let two = parse_style_border_color("red blue").unwrap();
        assert_eq!(
            two,
            StyleBorderColors {
                top: red,
                right: blue,
                bottom: red,
                left: blue
            }
        );
        // 3 values: top, left/right, bottom
        let three = parse_style_border_color("red blue green").unwrap();
        assert_eq!(
            three,
            StyleBorderColors {
                top: red,
                right: blue,
                bottom: green,
                left: blue
            }
        );
        // 4 values: top, right, bottom, left
        let four = parse_style_border_color("red blue green white").unwrap();
        assert_eq!(
            four,
            StyleBorderColors {
                top: red,
                right: blue,
                bottom: green,
                left: white
            }
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_color_normalizes_whitespace() {
        let expected = parse_style_border_color("red blue green").unwrap();
        for input in [
            "  red blue green  ",
            "red\tblue\ngreen",
            "red   blue \r\n green",
        ] {
            assert_eq!(parse_style_border_color(input).unwrap(), expected, "{input:?}");
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_color_rejects_zero_and_more_than_four_values() {
        for input in ["", "   ", "\t\n"] {
            let err = parse_style_border_color(input).unwrap_err();
            assert!(
                matches!(err, CssColorParseError::InvalidColor(_)),
                "{input:?} -> {err:?}"
            );
        }
        let too_many = "red ".repeat(1_000);
        let inputs: [&str; 3] = [
            "red red red red red",
            "red blue green white black yellow",
            &too_many,
        ];
        for input in inputs {
            assert!(
                parse_style_border_color(input).is_err(),
                "{input:?} unexpectedly parsed"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_color_propagates_component_errors() {
        for input in [
            "notacolor",
            "red notacolor",
            "red blue notacolor",
            "red blue green notacolor",
            "red \u{1F600}",
            "#zzz",
        ] {
            assert!(
                parse_style_border_color(input).is_err(),
                "{input:?} unexpectedly parsed"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_color_round_trips_through_to_hash() {
        let colors = StyleBorderColors {
            top: ColorU::new(1, 2, 3, 4),
            right: ColorU::new(255, 0, 0, 255),
            bottom: ColorU::TRANSPARENT,
            left: ColorU::new(9, 8, 7, 6),
        };
        let encoded = format!(
            "{} {} {} {}",
            colors.top.to_hash(),
            colors.right.to_hash(),
            colors.bottom.to_hash(),
            colors.left.to_hash()
        );
        assert_eq!(parse_style_border_color(&encoded).unwrap(), colors);
    }
    // =====================================================================
    // border-style shorthand
    // =====================================================================
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_style_expands_one_to_four_values() {
        assert_eq!(
            parse_style_border_style("solid").unwrap(),
            StyleBorderStyles {
                top: BorderStyle::Solid,
                right: BorderStyle::Solid,
                bottom: BorderStyle::Solid,
                left: BorderStyle::Solid,
            }
        );
        assert_eq!(
            parse_style_border_style("solid dashed").unwrap(),
            StyleBorderStyles {
                top: BorderStyle::Solid,
                right: BorderStyle::Dashed,
                bottom: BorderStyle::Solid,
                left: BorderStyle::Dashed,
            }
        );
        assert_eq!(
            parse_style_border_style("solid dashed dotted").unwrap(),
            StyleBorderStyles {
                top: BorderStyle::Solid,
                right: BorderStyle::Dashed,
                bottom: BorderStyle::Dotted,
                left: BorderStyle::Dashed,
            }
        );
        assert_eq!(
            parse_style_border_style("solid dashed dotted double").unwrap(),
            StyleBorderStyles {
                top: BorderStyle::Solid,
                right: BorderStyle::Dashed,
                bottom: BorderStyle::Dotted,
                left: BorderStyle::Double,
            }
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_style_rejects_zero_and_more_than_four_values() {
        for input in ["", "   ", "\t\n"] {
            let err = parse_style_border_style(input).unwrap_err();
            // NOTE: the error payload here is the *trimmed* input, unlike
            // parse_border_style, which echoes the raw input back.
            assert!(
                matches!(err, CssBorderStyleParseError::InvalidStyle(s) if s == input.trim()),
                "{input:?} -> {err:?}"
            );
        }
        let too_many = "dotted ".repeat(1_000);
        let inputs: [&str; 2] = ["solid solid solid solid solid", &too_many];
        for input in inputs {
            assert!(
                parse_style_border_style(input).is_err(),
                "{input:?} unexpectedly parsed"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_style_propagates_component_errors() {
        for input in [
            "bogus",
            "solid bogus",
            "solid solid bogus",
            "solid solid solid bogus",
            "solid \u{1F600}",
            "SOLID",
        ] {
            assert!(
                parse_style_border_style(input).is_err(),
                "{input:?} unexpectedly parsed"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_style_round_trips_through_display() {
        for (t, r, b, l) in [
            (
                BorderStyle::Solid,
                BorderStyle::Dashed,
                BorderStyle::Dotted,
                BorderStyle::Double,
            ),
            (
                BorderStyle::None,
                BorderStyle::Hidden,
                BorderStyle::Groove,
                BorderStyle::Ridge,
            ),
            (
                BorderStyle::Inset,
                BorderStyle::Outset,
                BorderStyle::None,
                BorderStyle::Solid,
            ),
        ] {
            let expected = StyleBorderStyles {
                top: t,
                right: r,
                bottom: b,
                left: l,
            };
            let encoded = format!("{t} {r} {b} {l}");
            assert_eq!(
                parse_style_border_style(&encoded).unwrap(),
                expected,
                "{encoded} did not round-trip"
            );
        }
    }
    // =====================================================================
    // border-width shorthand
    // =====================================================================
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_width_expands_one_to_four_values() {
        assert_eq!(
            parse_style_border_width("1px").unwrap(),
            StyleBorderWidths {
                top: PixelValue::px(1.0),
                right: PixelValue::px(1.0),
                bottom: PixelValue::px(1.0),
                left: PixelValue::px(1.0),
            }
        );
        assert_eq!(
            parse_style_border_width("1px 2px").unwrap(),
            StyleBorderWidths {
                top: PixelValue::px(1.0),
                right: PixelValue::px(2.0),
                bottom: PixelValue::px(1.0),
                left: PixelValue::px(2.0),
            }
        );
        assert_eq!(
            parse_style_border_width("1px 2px 3px").unwrap(),
            StyleBorderWidths {
                top: PixelValue::px(1.0),
                right: PixelValue::px(2.0),
                bottom: PixelValue::px(3.0),
                left: PixelValue::px(2.0),
            }
        );
        assert_eq!(
            parse_style_border_width("1px 2em 3pt 4%").unwrap(),
            StyleBorderWidths {
                top: PixelValue::px(1.0),
                right: PixelValue::em(2.0),
                bottom: PixelValue::pt(3.0),
                left: PixelValue::percent(4.0),
            }
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_width_rejects_zero_and_more_than_four_values() {
        for input in ["", "   ", "\t\n"] {
            let err = parse_style_border_width(input).unwrap_err();
            assert!(
                matches!(err, CssPixelValueParseError::InvalidPixelValue(s) if s == input.trim()),
                "{input:?} -> {err:?}"
            );
        }
        let too_many = "1px ".repeat(1_000);
        let inputs: [&str; 2] = ["1px 1px 1px 1px 1px", &too_many];
        for input in inputs {
            assert!(
                parse_style_border_width(input).is_err(),
                "{input:?} unexpectedly parsed"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_width_rejects_the_thin_medium_thick_keywords() {
        // KNOWN DIVERGENCE: the longhands (parse_border_top_width) go through
        // parse_border_width_value and DO accept thin/medium/thick, but this
        // shorthand calls parse_pixel_value directly, so `border-width: thin`
        // is rejected. Worse, "thin" ends with the "in" (inch) suffix, so it is
        // reported as a broken *inches* value rather than an unknown keyword.
        let err = parse_style_border_width("thin").unwrap_err();
        assert!(
            matches!(err, CssPixelValueParseError::ValueParseErr(_, s) if s == "th"),
            "expected `thin` to be misread as inches, got {err:?}"
        );
        assert!(parse_style_border_width("medium").is_err());
        assert!(parse_style_border_width("thick").is_err());
        assert!(parse_style_border_width("thin thick").is_err());
        // ...while the longhand happily accepts all three:
        assert_eq!(
            parse_border_top_width("thin").unwrap().inner,
            THIN_BORDER_THICKNESS
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_width_propagates_component_errors() {
        for input in [
            "abc",
            "1px abc",
            "1px 2px abc",
            "1px 2px 3px abc",
            "1px \u{1F600}",
            "1PX",
        ] {
            assert!(
                parse_style_border_width(input).is_err(),
                "{input:?} unexpectedly parsed"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_width_hostile_numbers_stay_finite() {
        let widths = parse_style_border_width("NaN inf -inf 1e999").unwrap();
        for w in [widths.top, widths.right, widths.bottom, widths.left] {
            assert!(
                w.number.get().is_finite(),
                "hostile width did not saturate: {w:?}"
            );
        }
        assert_eq!(widths.top, PixelValue::px(0.0)); // NaN -> 0
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_border_width_round_trips_through_display() {
        let widths = StyleBorderWidths {
            top: PixelValue::px(1.5),
            right: PixelValue::em(2.0),
            bottom: PixelValue::pt(3.25),
            left: PixelValue::percent(50.0),
        };
        let encoded = format!(
            "{} {} {} {}",
            widths.top, widths.right, widths.bottom, widths.left
        );
        assert_eq!(parse_style_border_width(&encoded).unwrap(), widths);
    }
    // =====================================================================
    // Error types: to_contained / to_shared / From / Display
    // =====================================================================
    #[cfg(feature = "parser")]
    #[test]
    fn css_border_style_parse_error_round_trips_owned_and_shared() {
        let huge = "x".repeat(10_000);
        let payloads: [&str; 7] = ["", " ", "bogus", "\u{1F600}", "s\u{0301}", "\u{0}", &huge];
        for payload in payloads {
            let shared = CssBorderStyleParseError::InvalidStyle(payload);
            let owned = shared.to_contained();
            assert_eq!(owned.to_shared(), shared, "{payload:?} did not round-trip");
            assert!(
                matches!(&owned, CssBorderStyleParseErrorOwned::InvalidStyle(s) if s.as_str() == payload)
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn css_border_style_parse_error_from_a_real_parse_failure_round_trips() {
        let err = parse_border_style("\u{1F600}bogus").unwrap_err();
        let owned = err.to_contained();
        assert_eq!(owned.to_shared(), err);
        // Debug is routed through Display, so both must mention the input.
        assert!(format!("{err}").contains("bogus"));
        assert!(format!("{err:?}").contains("bogus"));
        assert!(!format!("{owned:?}").is_empty());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn css_border_side_parse_error_round_trips_for_every_variant() {
        let variants = [
            CssBorderSideParseError::InvalidDeclaration(""),
            CssBorderSideParseError::InvalidDeclaration("1px 2px solid"),
            CssBorderSideParseError::InvalidDeclaration("\u{1F600}"),
            CssBorderSideParseError::Width(CssPixelValueParseError::EmptyString),
            CssBorderSideParseError::Width(CssPixelValueParseError::InvalidPixelValue("zz")),
            CssBorderSideParseError::Style(CssBorderStyleParseError::InvalidStyle("zz")),
            CssBorderSideParseError::Style(CssBorderStyleParseError::InvalidStyle("")),
            CssBorderSideParseError::Color(CssColorParseError::InvalidColor("zz")),
            CssBorderSideParseError::Color(CssColorParseError::EmptyInput),
            CssBorderSideParseError::Color(CssColorParseError::InvalidColorComponent(u8::MAX)),
        ];
        for shared in variants {
            let owned = shared.to_contained();
            assert_eq!(owned.to_shared(), shared, "{shared:?} did not round-trip");
            assert!(!format!("{shared}").is_empty());
            assert!(!format!("{owned:?}").is_empty());
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn css_border_side_parse_error_from_conversions_pick_the_right_variant() {
        let width: CssBorderSideParseError<'_> =
            CssPixelValueParseError::InvalidPixelValue("zz").into();
        assert!(matches!(width, CssBorderSideParseError::Width(_)));
        let style: CssBorderSideParseError<'_> =
            CssBorderStyleParseError::InvalidStyle("zz").into();
        assert!(matches!(style, CssBorderSideParseError::Style(_)));
        let color: CssBorderSideParseError<'_> = CssColorParseError::InvalidColor("zz").into();
        assert!(matches!(color, CssBorderSideParseError::Color(_)));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn css_border_parse_error_owned_newtype_wraps_the_side_error() {
        let owned = CssBorderSideParseError::InvalidDeclaration("bogus").to_contained();
        let wrapped = CssBorderParseErrorOwned::from(owned.clone());
        assert_eq!(wrapped.inner, owned);
        assert_eq!(wrapped.inner.to_shared().to_contained(), owned);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn error_display_mentions_the_offending_input() {
        let err = parse_border_side("1px bogus red").unwrap_err();
        let msg = format!("{err}");
        assert!(msg.contains("1px bogus red"), "unhelpful message: {msg}");
        let err = parse_border_style("bogus").unwrap_err();
        assert!(format!("{err}").contains("bogus"));
        let err = parse_border_top_width("bogus").unwrap_err();
        assert!(format!("{err}").contains("bogus"));
    }
}