Skip to main content

azul_css/props/style/
border.rs

1//! CSS properties for border style, width, and color.
2
3use alloc::string::{String, ToString};
4use core::fmt;
5use crate::corety::AzString;
6
7#[cfg(feature = "parser")]
8use crate::props::basic::{color::parse_css_color, pixel::parse_pixel_value};
9use 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)]
28pub 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
43impl fmt::Display for BorderStyle {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(
46            f,
47            "{}",
48            match self {
49                Self::None => "none",
50                Self::Solid => "solid",
51                Self::Double => "double",
52                Self::Dotted => "dotted",
53                Self::Dashed => "dashed",
54                Self::Hidden => "hidden",
55                Self::Groove => "groove",
56                Self::Ridge => "ridge",
57                Self::Inset => "inset",
58                Self::Outset => "outset",
59            }
60        )
61    }
62}
63
64impl PrintAsCssValue for BorderStyle {
65    fn print_as_css_value(&self) -> String {
66        self.to_string()
67    }
68}
69
70/// Internal macro to reduce boilerplate for defining border-top, -right, -bottom, -left properties.
71macro_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            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
81                write!(f, "{}", self.inner)
82            }
83        }
84        impl Default for $struct_name {
85            fn default() -> Self {
86                Self { inner: $default }
87            }
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            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
99                write!(f, "{}", self.inner.to_hash())
100            }
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            fn default() -> Self {
106                Self {
107                    inner: ColorU::BLACK,
108                }
109            }
110        }
111        impl $struct_name {
112            #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
113                Self {
114                    inner: self.inner.interpolate(&other.inner, t),
115                }
116            }
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`).
130define_border_side_property!(StyleBorderTopStyle, BorderStyle, BorderStyle::None);
131/// CSS `border-right-style` property (e.g. `solid`, `dashed`, `none`).
132define_border_side_property!(StyleBorderRightStyle, BorderStyle, BorderStyle::None);
133/// CSS `border-bottom-style` property (e.g. `solid`, `dashed`, `none`).
134define_border_side_property!(StyleBorderBottomStyle, BorderStyle, BorderStyle::None);
135/// CSS `border-left-style` property (e.g. `solid`, `dashed`, `none`).
136define_border_side_property!(StyleBorderLeftStyle, BorderStyle, BorderStyle::None);
137
138// Formatting implementations for border side style values
139impl 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
148impl 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
157impl 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
166impl 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`.
177define_border_side_property!(StyleBorderTopColor, ColorU);
178/// CSS `border-right-color` property. Defaults to `ColorU::BLACK`.
179define_border_side_property!(StyleBorderRightColor, ColorU);
180/// CSS `border-bottom-color` property. Defaults to `ColorU::BLACK`.
181define_border_side_property!(StyleBorderBottomColor, ColorU);
182/// CSS `border-left-color` property. Defaults to `ColorU::BLACK`.
183define_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).
189define_border_side_property!(LayoutBorderTopWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
190/// CSS `border-right-width` property. Defaults to `MEDIUM_BORDER_THICKNESS` (3px).
191define_border_side_property!(LayoutBorderRightWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
192/// CSS `border-bottom-width` property. Defaults to `MEDIUM_BORDER_THICKNESS` (3px).
193define_border_side_property!(LayoutBorderBottomWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
194/// CSS `border-left-width` property. Defaults to `MEDIUM_BORDER_THICKNESS` (3px).
195define_border_side_property!(LayoutBorderLeftWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
196
197macro_rules! impl_border_width_helpers {
198    ($($t:ty),+) => { $(
199        impl $t {
200            #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
201                Self { inner: self.inner.interpolate(&other.inner, t) }
202            }
203            #[must_use] pub const fn const_px(value: isize) -> Self {
204                Self { inner: PixelValue::const_px(value) }
205            }
206        }
207    )+ };
208}
209
210impl_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)]
220pub 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)]
232pub enum CssBorderStyleParseError<'a> {
233    InvalidStyle(&'a str),
234}
235
236#[cfg(feature = "parser")]
237impl_debug_as_display!(CssBorderStyleParseError<'a>);
238#[cfg(feature = "parser")]
239impl_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)]
246pub enum CssBorderStyleParseErrorOwned {
247    InvalidStyle(AzString),
248}
249
250#[cfg(feature = "parser")]
251impl CssBorderStyleParseError<'_> {
252    #[must_use] pub fn to_contained(&self) -> CssBorderStyleParseErrorOwned {
253        match self {
254            CssBorderStyleParseError::InvalidStyle(s) => {
255                CssBorderStyleParseErrorOwned::InvalidStyle((*s).to_string().into())
256            }
257        }
258    }
259}
260
261#[cfg(feature = "parser")]
262impl CssBorderStyleParseErrorOwned {
263    #[must_use] pub fn to_shared(&self) -> CssBorderStyleParseError<'_> {
264        match self {
265            Self::InvalidStyle(s) => {
266                CssBorderStyleParseError::InvalidStyle(s.as_str())
267            }
268        }
269    }
270}
271
272#[cfg(feature = "parser")]
273/// # Errors
274///
275/// Returns an error if `input` is not a valid CSS `border-style` value.
276pub fn parse_border_style(input: &str) -> Result<BorderStyle, CssBorderStyleParseError<'_>> {
277    match input.trim() {
278        "none" => Ok(BorderStyle::None),
279        "solid" => Ok(BorderStyle::Solid),
280        "double" => Ok(BorderStyle::Double),
281        "dotted" => Ok(BorderStyle::Dotted),
282        "dashed" => Ok(BorderStyle::Dashed),
283        "hidden" => Ok(BorderStyle::Hidden),
284        "groove" => Ok(BorderStyle::Groove),
285        "ridge" => Ok(BorderStyle::Ridge),
286        "inset" => Ok(BorderStyle::Inset),
287        "outset" => Ok(BorderStyle::Outset),
288        _ => Err(CssBorderStyleParseError::InvalidStyle(input)),
289    }
290}
291
292// -- Shorthand Parser (for `border`, `border-top`, etc.) --
293
294#[cfg(feature = "parser")]
295#[derive(Clone, PartialEq)]
296pub 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")]
304impl_debug_as_display!(CssBorderSideParseError<'a>);
305#[cfg(feature = "parser")]
306impl_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")]
314impl_from!(CssPixelValueParseError<'a>, CssBorderSideParseError::Width);
315#[cfg(feature = "parser")]
316impl_from!(CssBorderStyleParseError<'a>, CssBorderSideParseError::Style);
317#[cfg(feature = "parser")]
318impl_from!(CssColorParseError<'a>, CssBorderSideParseError::Color);
319
320#[cfg(feature = "parser")]
321#[derive(Debug, Clone, PartialEq)]
322#[repr(C, u8)]
323pub enum CssBorderSideParseErrorOwned {
324    InvalidDeclaration(AzString),
325    Width(CssPixelValueParseErrorOwned),
326    Style(CssBorderStyleParseErrorOwned),
327    Color(CssColorParseErrorOwned),
328}
329
330#[cfg(feature = "parser")]
331impl CssBorderSideParseError<'_> {
332    #[must_use] pub fn to_contained(&self) -> CssBorderSideParseErrorOwned {
333        match self {
334            CssBorderSideParseError::InvalidDeclaration(s) => {
335                CssBorderSideParseErrorOwned::InvalidDeclaration((*s).to_string().into())
336            }
337            CssBorderSideParseError::Width(e) => {
338                CssBorderSideParseErrorOwned::Width(e.to_contained())
339            }
340            CssBorderSideParseError::Style(e) => {
341                CssBorderSideParseErrorOwned::Style(e.to_contained())
342            }
343            CssBorderSideParseError::Color(e) => {
344                CssBorderSideParseErrorOwned::Color(e.to_contained())
345            }
346        }
347    }
348}
349
350#[cfg(feature = "parser")]
351impl CssBorderSideParseErrorOwned {
352    #[must_use] pub fn to_shared(&self) -> CssBorderSideParseError<'_> {
353        match self {
354            Self::InvalidDeclaration(s) => {
355                CssBorderSideParseError::InvalidDeclaration(s.as_str())
356            }
357            Self::Width(e) => CssBorderSideParseError::Width(e.to_shared()),
358            Self::Style(e) => CssBorderSideParseError::Style(e.to_shared()),
359            Self::Color(e) => CssBorderSideParseError::Color(e.to_shared()),
360        }
361    }
362}
363
364// Type alias for compatibility with old code
365#[cfg(feature = "parser")]
366pub 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)]
372pub struct CssBorderParseErrorOwned {
373    pub inner: CssBorderSideParseErrorOwned,
374}
375
376#[cfg(feature = "parser")]
377impl From<CssBorderSideParseErrorOwned> for CssBorderParseErrorOwned {
378    fn from(v: CssBorderSideParseErrorOwned) -> Self {
379        Self { inner: v }
380    }
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")]
386fn parse_border_side(
387    input: &str,
388) -> Result<StyleBorderSide, CssBorderSideParseError<'_>> {
389    let mut width = None;
390    let mut style = None;
391    let mut color = None;
392
393    if input.trim().is_empty() {
394        return Err(CssBorderSideParseError::InvalidDeclaration(input));
395    }
396
397    for part in input.split_whitespace() {
398        // Try to parse as a width.
399        if width.is_none() {
400            if let Ok(w) = parse_border_width_value(part) {
401                width = Some(w);
402                continue;
403            }
404        }
405
406        // Try to parse as a style.
407        if style.is_none() {
408            if let Ok(s) = parse_border_style(part) {
409                style = Some(s);
410                continue;
411            }
412        }
413
414        // Try to parse as a color.
415        if color.is_none() {
416            if let Ok(c) = parse_css_color(part) {
417                color = Some(c);
418                continue;
419            }
420        }
421
422        // If we get here, the part didn't match anything, or a value was specified twice.
423        return Err(CssBorderSideParseError::InvalidDeclaration(input));
424    }
425
426    Ok(StyleBorderSide {
427        border_width: width.unwrap_or(MEDIUM_BORDER_THICKNESS),
428        border_style: style.unwrap_or(BorderStyle::None),
429        border_color: color.unwrap_or(ColorU::BLACK),
430    })
431}
432
433// --- Individual Property Parsers ---
434
435#[cfg(feature = "parser")]
436fn parse_border_width_value(
437    input: &str,
438) -> Result<PixelValue, CssPixelValueParseError<'_>> {
439    match input.trim() {
440        "thin" => Ok(THIN_BORDER_THICKNESS),
441        "medium" => Ok(MEDIUM_BORDER_THICKNESS),
442        "thick" => Ok(THICK_BORDER_THICKNESS),
443        _ => parse_pixel_value(input),
444    }
445}
446
447#[cfg(feature = "parser")]
448/// # Errors
449///
450/// Returns an error if `input` is not a valid CSS `border-top-width` value.
451pub fn parse_border_top_width(
452    input: &str,
453) -> Result<LayoutBorderTopWidth, CssPixelValueParseError<'_>> {
454    parse_border_width_value(input).map(|inner| LayoutBorderTopWidth { inner })
455}
456
457#[cfg(feature = "parser")]
458/// # Errors
459///
460/// Returns an error if `input` is not a valid CSS `border-right-width` value.
461pub fn parse_border_right_width(
462    input: &str,
463) -> Result<LayoutBorderRightWidth, CssPixelValueParseError<'_>> {
464    parse_border_width_value(input).map(|inner| LayoutBorderRightWidth { inner })
465}
466
467#[cfg(feature = "parser")]
468/// # Errors
469///
470/// Returns an error if `input` is not a valid CSS `border-bottom-width` value.
471pub fn parse_border_bottom_width(
472    input: &str,
473) -> Result<LayoutBorderBottomWidth, CssPixelValueParseError<'_>> {
474    parse_border_width_value(input).map(|inner| LayoutBorderBottomWidth { inner })
475}
476
477#[cfg(feature = "parser")]
478/// # Errors
479///
480/// Returns an error if `input` is not a valid CSS `border-left-width` value.
481pub fn parse_border_left_width(
482    input: &str,
483) -> Result<LayoutBorderLeftWidth, CssPixelValueParseError<'_>> {
484    parse_border_width_value(input).map(|inner| LayoutBorderLeftWidth { inner })
485}
486
487#[cfg(feature = "parser")]
488/// # Errors
489///
490/// Returns an error if `input` is not a valid CSS `border-top-style` value.
491pub fn parse_border_top_style(
492    input: &str,
493) -> Result<StyleBorderTopStyle, CssBorderStyleParseError<'_>> {
494    parse_border_style(input).map(|inner| StyleBorderTopStyle { inner })
495}
496#[cfg(feature = "parser")]
497/// # Errors
498///
499/// Returns an error if `input` is not a valid CSS `border-right-style` value.
500pub fn parse_border_right_style(
501    input: &str,
502) -> Result<StyleBorderRightStyle, CssBorderStyleParseError<'_>> {
503    parse_border_style(input).map(|inner| StyleBorderRightStyle { inner })
504}
505#[cfg(feature = "parser")]
506/// # Errors
507///
508/// Returns an error if `input` is not a valid CSS `border-bottom-style` value.
509pub fn parse_border_bottom_style(
510    input: &str,
511) -> Result<StyleBorderBottomStyle, CssBorderStyleParseError<'_>> {
512    parse_border_style(input).map(|inner| StyleBorderBottomStyle { inner })
513}
514#[cfg(feature = "parser")]
515/// # Errors
516///
517/// Returns an error if `input` is not a valid CSS `border-left-style` value.
518pub fn parse_border_left_style(
519    input: &str,
520) -> Result<StyleBorderLeftStyle, CssBorderStyleParseError<'_>> {
521    parse_border_style(input).map(|inner| StyleBorderLeftStyle { inner })
522}
523
524#[cfg(feature = "parser")]
525/// # Errors
526///
527/// Returns an error if `input` is not a valid CSS `border-top-color` value.
528pub fn parse_border_top_color(
529    input: &str,
530) -> Result<StyleBorderTopColor, CssColorParseError<'_>> {
531    parse_css_color(input).map(|inner| StyleBorderTopColor { inner })
532}
533#[cfg(feature = "parser")]
534/// # Errors
535///
536/// Returns an error if `input` is not a valid CSS `border-right-color` value.
537pub fn parse_border_right_color(
538    input: &str,
539) -> Result<StyleBorderRightColor, CssColorParseError<'_>> {
540    parse_css_color(input).map(|inner| StyleBorderRightColor { inner })
541}
542#[cfg(feature = "parser")]
543/// # Errors
544///
545/// Returns an error if `input` is not a valid CSS `border-bottom-color` value.
546pub fn parse_border_bottom_color(
547    input: &str,
548) -> Result<StyleBorderBottomColor, CssColorParseError<'_>> {
549    parse_css_color(input).map(|inner| StyleBorderBottomColor { inner })
550}
551#[cfg(feature = "parser")]
552/// # Errors
553///
554/// Returns an error if `input` is not a valid CSS `border-left-color` value.
555pub fn parse_border_left_color(
556    input: &str,
557) -> Result<StyleBorderLeftColor, CssColorParseError<'_>> {
558    parse_css_color(input).map(|inner| StyleBorderLeftColor { inner })
559}
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)]
566pub 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.
582pub fn parse_style_border_color(
583    input: &str,
584) -> Result<StyleBorderColors, CssColorParseError<'_>> {
585    let input = input.trim();
586    let parts: Vec<&str> = input.split_whitespace().collect();
587
588    match parts.len() {
589        1 => {
590            let color = parse_css_color(parts[0])?;
591            Ok(StyleBorderColors {
592                top: color,
593                right: color,
594                bottom: color,
595                left: color,
596            })
597        }
598        2 => {
599            let top_bottom = parse_css_color(parts[0])?;
600            let left_right = parse_css_color(parts[1])?;
601            Ok(StyleBorderColors {
602                top: top_bottom,
603                right: left_right,
604                bottom: top_bottom,
605                left: left_right,
606            })
607        }
608        3 => {
609            let top = parse_css_color(parts[0])?;
610            let left_right = parse_css_color(parts[1])?;
611            let bottom = parse_css_color(parts[2])?;
612            Ok(StyleBorderColors {
613                top,
614                right: left_right,
615                bottom,
616                left: left_right,
617            })
618        }
619        4 => {
620            let top = parse_css_color(parts[0])?;
621            let right = parse_css_color(parts[1])?;
622            let bottom = parse_css_color(parts[2])?;
623            let left = parse_css_color(parts[3])?;
624            Ok(StyleBorderColors {
625                top,
626                right,
627                bottom,
628                left,
629            })
630        }
631        _ => Err(CssColorParseError::InvalidColor(input)),
632    }
633}
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)]
640pub 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.
652pub fn parse_style_border_style(
653    input: &str,
654) -> Result<StyleBorderStyles, CssBorderStyleParseError<'_>> {
655    let input = input.trim();
656    let parts: Vec<&str> = input.split_whitespace().collect();
657
658    match parts.len() {
659        1 => {
660            let style = parse_border_style(parts[0])?;
661            Ok(StyleBorderStyles {
662                top: style,
663                right: style,
664                bottom: style,
665                left: style,
666            })
667        }
668        2 => {
669            let top_bottom = parse_border_style(parts[0])?;
670            let left_right = parse_border_style(parts[1])?;
671            Ok(StyleBorderStyles {
672                top: top_bottom,
673                right: left_right,
674                bottom: top_bottom,
675                left: left_right,
676            })
677        }
678        3 => {
679            let top = parse_border_style(parts[0])?;
680            let left_right = parse_border_style(parts[1])?;
681            let bottom = parse_border_style(parts[2])?;
682            Ok(StyleBorderStyles {
683                top,
684                right: left_right,
685                bottom,
686                left: left_right,
687            })
688        }
689        4 => {
690            let top = parse_border_style(parts[0])?;
691            let right = parse_border_style(parts[1])?;
692            let bottom = parse_border_style(parts[2])?;
693            let left = parse_border_style(parts[3])?;
694            Ok(StyleBorderStyles {
695                top,
696                right,
697                bottom,
698                left,
699            })
700        }
701        _ => Err(CssBorderStyleParseError::InvalidStyle(input)),
702    }
703}
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)]
710pub 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.
722pub fn parse_style_border_width(
723    input: &str,
724) -> Result<StyleBorderWidths, CssPixelValueParseError<'_>> {
725    let input = input.trim();
726    let parts: Vec<&str> = input.split_whitespace().collect();
727
728    match parts.len() {
729        1 => {
730            let width = parse_pixel_value(parts[0])?;
731            Ok(StyleBorderWidths {
732                top: width,
733                right: width,
734                bottom: width,
735                left: width,
736            })
737        }
738        2 => {
739            let top_bottom = parse_pixel_value(parts[0])?;
740            let left_right = parse_pixel_value(parts[1])?;
741            Ok(StyleBorderWidths {
742                top: top_bottom,
743                right: left_right,
744                bottom: top_bottom,
745                left: left_right,
746            })
747        }
748        3 => {
749            let top = parse_pixel_value(parts[0])?;
750            let left_right = parse_pixel_value(parts[1])?;
751            let bottom = parse_pixel_value(parts[2])?;
752            Ok(StyleBorderWidths {
753                top,
754                right: left_right,
755                bottom,
756                left: left_right,
757            })
758        }
759        4 => {
760            let top = parse_pixel_value(parts[0])?;
761            let right = parse_pixel_value(parts[1])?;
762            let bottom = parse_pixel_value(parts[2])?;
763            let left = parse_pixel_value(parts[3])?;
764            Ok(StyleBorderWidths {
765                top,
766                right,
767                bottom,
768                left,
769            })
770        }
771        _ => Err(CssPixelValueParseError::InvalidPixelValue(input)),
772    }
773}
774
775// Compatibility alias
776#[cfg(feature = "parser")]
777/// # Errors
778///
779/// Returns an error if `input` is not a valid CSS `border` value.
780pub fn parse_style_border(input: &str) -> Result<StyleBorderSide, CssBorderParseError<'_>> {
781    parse_border_side(input)
782}
783
784#[cfg(all(test, feature = "parser"))]
785mod tests {
786    use super::*;
787
788    #[test]
789    fn test_parse_border_style() {
790        assert_eq!(parse_border_style("solid").unwrap(), BorderStyle::Solid);
791        assert_eq!(parse_border_style("dotted").unwrap(), BorderStyle::Dotted);
792        assert_eq!(parse_border_style("none").unwrap(), BorderStyle::None);
793        assert_eq!(
794            parse_border_style("  dashed  ").unwrap(),
795            BorderStyle::Dashed
796        );
797        assert!(parse_border_style("solidd").is_err());
798    }
799
800    #[test]
801    fn test_parse_border_side_shorthand() {
802        // Full
803        let result = parse_border_side("2px dotted #ff0000").unwrap();
804        assert_eq!(result.border_width, PixelValue::px(2.0));
805        assert_eq!(result.border_style, BorderStyle::Dotted);
806        assert_eq!(result.border_color, ColorU::new_rgb(255, 0, 0));
807
808        // Different order
809        let result = parse_border_side("solid green 1em").unwrap();
810        assert_eq!(result.border_width, PixelValue::em(1.0));
811        assert_eq!(result.border_style, BorderStyle::Solid);
812        assert_eq!(result.border_color, ColorU::new_rgb(0, 128, 0));
813
814        // Missing width
815        let result = parse_border_side("ridge #f0f").unwrap();
816        assert_eq!(result.border_width, MEDIUM_BORDER_THICKNESS); // default
817        assert_eq!(result.border_style, BorderStyle::Ridge);
818        assert_eq!(result.border_color, ColorU::new_rgb(255, 0, 255));
819
820        // Missing style
821        let result = parse_border_side("5pt blue").unwrap();
822        assert_eq!(result.border_width, PixelValue::pt(5.0));
823        assert_eq!(result.border_style, BorderStyle::None); // default
824        assert_eq!(result.border_color, ColorU::BLUE);
825
826        // Missing color
827        let result = parse_border_side("thick double").unwrap();
828        assert_eq!(result.border_width, PixelValue::px(5.0));
829        assert_eq!(result.border_style, BorderStyle::Double);
830        assert_eq!(result.border_color, ColorU::BLACK); // default
831
832        // Only one value
833        let result = parse_border_side("inset").unwrap();
834        assert_eq!(result.border_width, MEDIUM_BORDER_THICKNESS);
835        assert_eq!(result.border_style, BorderStyle::Inset);
836        assert_eq!(result.border_color, ColorU::BLACK);
837    }
838
839    #[test]
840    fn test_parse_border_side_invalid() {
841        // Two widths
842        assert!(parse_border_side("1px 2px solid red").is_err());
843        // Two styles
844        assert!(parse_border_side("solid dashed red").is_err());
845        // Two colors
846        assert!(parse_border_side("red blue solid").is_err());
847        // Empty
848        assert!(parse_border_side("").is_err());
849        // Unknown keyword
850        assert!(parse_border_side("1px unknown red").is_err());
851    }
852
853    #[test]
854    fn test_parse_longhand_border() {
855        assert_eq!(
856            parse_border_top_width("1.5em").unwrap().inner,
857            PixelValue::em(1.5)
858        );
859        assert_eq!(
860            parse_border_left_style("groove").unwrap().inner,
861            BorderStyle::Groove
862        );
863        assert_eq!(
864            parse_border_right_color("rgba(10, 20, 30, 0.5)")
865                .unwrap()
866                .inner,
867            ColorU::new(10, 20, 30, 128)
868        );
869    }
870}
871
872#[cfg(test)]
873mod 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)
1000            .inner
1001            .number
1002            .get()
1003            .is_finite());
1004    }
1005
1006    #[test]
1007    fn layout_border_width_interpolate_endpoints_are_exact() {
1008        let a = LayoutBorderTopWidth::const_px(0);
1009        let b = LayoutBorderTopWidth::const_px(10);
1010        assert_eq!(a.interpolate(&b, 0.0), a);
1011        assert_eq!(a.interpolate(&b, 1.0), b);
1012        assert_eq!(a.interpolate(&b, 0.5).inner, PixelValue::px(5.0));
1013    }
1014
1015    #[test]
1016    fn layout_border_width_interpolate_stays_finite_for_hostile_t() {
1017        let a = LayoutBorderTopWidth::const_px(0);
1018        let b = LayoutBorderTopWidth::const_px(10);
1019        for t in [
1020            0.0,
1021            1.0,
1022            -1.0,
1023            2.0,
1024            f32::NAN,
1025            f32::INFINITY,
1026            f32::NEG_INFINITY,
1027            f32::MAX,
1028            f32::MIN,
1029        ] {
1030            // A NaN/inf must never leak out of the animation path: FloatValue
1031            // stores an isize, so the cast saturates instead of propagating.
1032            assert!(
1033                a.interpolate(&b, t).inner.number.get().is_finite(),
1034                "interpolate(t = {t}) produced a non-finite width"
1035            );
1036            assert!(b.interpolate(&a, t).inner.number.get().is_finite());
1037        }
1038    }
1039
1040    #[test]
1041    fn layout_border_width_interpolate_across_metrics_stays_finite() {
1042        let px = LayoutBorderRightWidth {
1043            inner: PixelValue::px(4.0),
1044        };
1045        let em = LayoutBorderRightWidth {
1046            inner: PixelValue::em(2.0),
1047        };
1048        let percent = LayoutBorderRightWidth {
1049            inner: PixelValue::percent(100.0),
1050        };
1051        for t in [0.0, 0.5, 1.0, -3.0, f32::NAN, f32::INFINITY] {
1052            assert!(px.interpolate(&em, t).inner.number.get().is_finite());
1053            assert!(em.interpolate(&percent, t).inner.number.get().is_finite());
1054            assert!(percent.interpolate(&px, t).inner.number.get().is_finite());
1055        }
1056    }
1057
1058    #[test]
1059    fn style_border_color_interpolate_endpoints_are_exact() {
1060        let black = StyleBorderTopColor {
1061            inner: ColorU::BLACK,
1062        };
1063        let white = StyleBorderTopColor {
1064            inner: ColorU::WHITE,
1065        };
1066        assert_eq!(black.interpolate(&white, 0.0).inner, ColorU::BLACK);
1067        assert_eq!(black.interpolate(&white, 1.0).inner, ColorU::WHITE);
1068        let mid = black.interpolate(&white, 0.5).inner;
1069        assert_eq!((mid.r, mid.g, mid.b), (128, 128, 128));
1070    }
1071
1072    #[test]
1073    fn style_border_color_interpolate_hostile_t_does_not_panic() {
1074        let a = StyleBorderLeftColor {
1075            inner: ColorU::new(10, 20, 30, 40),
1076        };
1077        let b = StyleBorderLeftColor {
1078            inner: ColorU::new(200, 210, 220, 230),
1079        };
1080        for t in [
1081            -1000.0,
1082            1000.0,
1083            f32::NAN,
1084            f32::INFINITY,
1085            f32::NEG_INFINITY,
1086            f32::MAX,
1087        ] {
1088            // u8 channels saturate; the only requirement is that this returns.
1089            let _ = a.interpolate(&b, t);
1090            let _ = b.interpolate(&a, t);
1091        }
1092    }
1093
1094    // =====================================================================
1095    // parse_border_style
1096    // =====================================================================
1097
1098    #[cfg(feature = "parser")]
1099    #[test]
1100    fn parse_border_style_accepts_every_keyword() {
1101        assert_eq!(parse_border_style("none").unwrap(), BorderStyle::None);
1102        assert_eq!(parse_border_style("solid").unwrap(), BorderStyle::Solid);
1103        assert_eq!(parse_border_style("double").unwrap(), BorderStyle::Double);
1104        assert_eq!(parse_border_style("dotted").unwrap(), BorderStyle::Dotted);
1105        assert_eq!(parse_border_style("dashed").unwrap(), BorderStyle::Dashed);
1106        assert_eq!(parse_border_style("hidden").unwrap(), BorderStyle::Hidden);
1107        assert_eq!(parse_border_style("groove").unwrap(), BorderStyle::Groove);
1108        assert_eq!(parse_border_style("ridge").unwrap(), BorderStyle::Ridge);
1109        assert_eq!(parse_border_style("inset").unwrap(), BorderStyle::Inset);
1110        assert_eq!(parse_border_style("outset").unwrap(), BorderStyle::Outset);
1111    }
1112
1113    #[cfg(feature = "parser")]
1114    #[test]
1115    fn parse_border_style_round_trips_through_display() {
1116        for style in ALL_STYLES {
1117            let encoded = style.to_string();
1118            assert_eq!(
1119                parse_border_style(&encoded).unwrap(),
1120                style,
1121                "{encoded} did not round-trip"
1122            );
1123            // and through the PrintAsCssValue path, which must agree
1124            assert_eq!(
1125                parse_border_style(&style.print_as_css_value()).unwrap(),
1126                style
1127            );
1128        }
1129    }
1130
1131    #[cfg(feature = "parser")]
1132    #[test]
1133    fn parse_border_style_trims_surrounding_whitespace() {
1134        for input in [" solid", "solid ", "\t\nsolid\r\n ", "   solid   "] {
1135            assert_eq!(
1136                parse_border_style(input).unwrap(),
1137                BorderStyle::Solid,
1138                "{input:?} should trim to `solid`"
1139            );
1140        }
1141    }
1142
1143    #[cfg(feature = "parser")]
1144    #[test]
1145    fn parse_border_style_empty_and_whitespace_only_are_errors() {
1146        for input in ["", " ", "   ", "\t", "\n", "\r\n\t "] {
1147            assert!(
1148                parse_border_style(input).is_err(),
1149                "{input:?} must not parse as a border style"
1150            );
1151        }
1152    }
1153
1154    #[cfg(feature = "parser")]
1155    #[test]
1156    fn parse_border_style_error_carries_the_untrimmed_input() {
1157        // The Ok path trims, but the Err path hands back the *raw* input.
1158        let input = "  bogus  ";
1159        let err = parse_border_style(input).unwrap_err();
1160        assert!(
1161            matches!(err, CssBorderStyleParseError::InvalidStyle(s) if s == input),
1162            "unexpected error payload: {err:?}"
1163        );
1164    }
1165
1166    #[cfg(feature = "parser")]
1167    #[test]
1168    fn parse_border_style_rejects_uppercase_keywords() {
1169        // NOTE: CSS keywords are ASCII case-insensitive, so a spec-conformant
1170        // parser would accept these. This parser does not — asserted here so
1171        // the divergence is visible rather than silent.
1172        for input in ["SOLID", "Solid", "sOlId", "NONE", "Dashed"] {
1173            assert!(
1174                parse_border_style(input).is_err(),
1175                "{input:?} unexpectedly parsed (case-insensitivity was added?)"
1176            );
1177        }
1178    }
1179
1180    #[cfg(feature = "parser")]
1181    #[test]
1182    fn parse_border_style_rejects_garbage_unicode_and_numbers() {
1183        for input in [
1184            "solidd",
1185            "soli",
1186            "solid solid",
1187            "solid;garbage",
1188            "solid!important",
1189            "0",
1190            "-0",
1191            "1px",
1192            "9223372036854775807",
1193            "NaN",
1194            "inf",
1195            "-inf",
1196            "\u{1F600}",
1197            "s\u{0301}olid",
1198            "sölid",
1199            "\u{0}",
1200            "\u{202e}solid",
1201            "sol\tid",
1202            "()",
1203            "solid()",
1204        ] {
1205            assert!(
1206                parse_border_style(input).is_err(),
1207                "{input:?} unexpectedly parsed as a border style"
1208            );
1209        }
1210    }
1211
1212    #[cfg(feature = "parser")]
1213    #[test]
1214    fn parse_border_style_handles_huge_and_nested_input_without_panicking() {
1215        let huge = "a".repeat(100_000);
1216        assert!(parse_border_style(&huge).is_err());
1217
1218        let repeated = "solid ".repeat(50_000);
1219        assert!(parse_border_style(&repeated).is_err());
1220
1221        let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1222        assert!(parse_border_style(&nested).is_err());
1223
1224        let padded = format!("{}solid{}", " ".repeat(100_000), " ".repeat(100_000));
1225        assert_eq!(parse_border_style(&padded).unwrap(), BorderStyle::Solid);
1226    }
1227
1228    // =====================================================================
1229    // border-*-style longhands
1230    // =====================================================================
1231
1232    #[cfg(feature = "parser")]
1233    #[test]
1234    fn border_style_longhands_agree_with_parse_border_style() {
1235        for style in ALL_STYLES {
1236            let input = style.to_string();
1237            assert_eq!(parse_border_top_style(&input).unwrap().inner, style);
1238            assert_eq!(parse_border_right_style(&input).unwrap().inner, style);
1239            assert_eq!(parse_border_bottom_style(&input).unwrap().inner, style);
1240            assert_eq!(parse_border_left_style(&input).unwrap().inner, style);
1241        }
1242    }
1243
1244    #[cfg(feature = "parser")]
1245    #[test]
1246    fn border_style_longhands_reject_everything_parse_border_style_rejects() {
1247        for input in ["", "   ", "SOLID", "solidd", "\u{1F600}", "1px", "solid red"] {
1248            assert!(parse_border_top_style(input).is_err(), "top: {input:?}");
1249            assert!(parse_border_right_style(input).is_err(), "right: {input:?}");
1250            assert!(
1251                parse_border_bottom_style(input).is_err(),
1252                "bottom: {input:?}"
1253            );
1254            assert!(parse_border_left_style(input).is_err(), "left: {input:?}");
1255        }
1256    }
1257
1258    // =====================================================================
1259    // parse_border_width_value (private)
1260    // =====================================================================
1261
1262    #[cfg(feature = "parser")]
1263    #[test]
1264    fn parse_border_width_value_accepts_the_three_keywords() {
1265        assert_eq!(
1266            parse_border_width_value("thin").unwrap(),
1267            THIN_BORDER_THICKNESS
1268        );
1269        assert_eq!(
1270            parse_border_width_value("medium").unwrap(),
1271            MEDIUM_BORDER_THICKNESS
1272        );
1273        assert_eq!(
1274            parse_border_width_value("thick").unwrap(),
1275            THICK_BORDER_THICKNESS
1276        );
1277        // keywords are trimmed like everything else
1278        assert_eq!(
1279            parse_border_width_value("  \tthick\n ").unwrap(),
1280            THICK_BORDER_THICKNESS
1281        );
1282    }
1283
1284    #[cfg(feature = "parser")]
1285    #[test]
1286    fn parse_border_width_value_rejects_uppercase_keywords() {
1287        // Same case-sensitivity divergence as parse_border_style.
1288        for input in ["THIN", "Medium", "THICK"] {
1289            assert!(
1290                parse_border_width_value(input).is_err(),
1291                "{input:?} unexpectedly parsed"
1292            );
1293        }
1294    }
1295
1296    #[cfg(feature = "parser")]
1297    #[test]
1298    fn parse_border_width_value_empty_and_whitespace_only_are_errors() {
1299        for input in ["", " ", "\t\n", "    "] {
1300            let err = parse_border_width_value(input).unwrap_err();
1301            assert!(
1302                matches!(err, CssPixelValueParseError::EmptyString),
1303                "{input:?} -> {err:?}"
1304            );
1305        }
1306    }
1307
1308    #[cfg(feature = "parser")]
1309    #[test]
1310    fn parse_border_width_value_bare_number_is_interpreted_as_px() {
1311        assert_eq!(parse_border_width_value("0").unwrap(), PixelValue::px(0.0));
1312        assert_eq!(parse_border_width_value("42").unwrap(), PixelValue::px(42.0));
1313        assert_eq!(
1314            parse_border_width_value("1.5").unwrap(),
1315            PixelValue::px(1.5)
1316        );
1317        // -0 collapses to +0 once quantized into the isize-backed FloatValue
1318        assert_eq!(parse_border_width_value("-0").unwrap(), PixelValue::px(0.0));
1319        // negative widths are *accepted* (CSS would reject them) — pinned so a
1320        // future validity check is a deliberate change, not an accident.
1321        assert_eq!(
1322            parse_border_width_value("-5px").unwrap(),
1323            PixelValue::px(-5.0)
1324        );
1325    }
1326
1327    #[cfg(feature = "parser")]
1328    #[test]
1329    fn parse_border_width_value_nan_saturates_to_zero() {
1330        // "NaN" is a valid f32 literal for Rust's FromStr, so this reaches
1331        // FloatValue::new(NaN) — which saturates to 0 rather than storing NaN.
1332        let parsed = parse_border_width_value("NaN").unwrap();
1333        assert!(parsed.number.get().is_finite());
1334        assert_eq!(parsed, PixelValue::px(0.0));
1335
1336        let parsed = parse_border_width_value("NaNpx").unwrap();
1337        assert_eq!(parsed, PixelValue::px(0.0));
1338    }
1339
1340    #[cfg(feature = "parser")]
1341    #[test]
1342    fn parse_border_width_value_infinities_and_overflow_saturate_finite() {
1343        for input in [
1344            "inf",
1345            "-inf",
1346            "infpx",
1347            "1e999",
1348            "-1e999",
1349            "1e40px",
1350            "340282350000000000000000000000000000000px", // ~f32::MAX
1351            "9223372036854775807",                       // i64::MAX
1352            "-9223372036854775808",                      // i64::MIN
1353        ] {
1354            let parsed = parse_border_width_value(input)
1355                .unwrap_or_else(|e| panic!("{input:?} failed to parse: {e:?}"));
1356            assert!(
1357                parsed.number.get().is_finite(),
1358                "{input:?} produced a non-finite width: {parsed:?}"
1359            );
1360        }
1361    }
1362
1363    #[cfg(feature = "parser")]
1364    #[test]
1365    fn parse_border_width_value_rejects_garbage_and_unicode() {
1366        for input in [
1367            "px",
1368            "em",
1369            "abc",
1370            "1px 2px",
1371            "1px;",
1372            "--1px",
1373            "1PX",
1374            "\u{1F600}",
1375            "1\u{1F600}px",
1376            "١px", // arabic-indic digit one
1377            "()",
1378            "calc(1px + 2px)",
1379        ] {
1380            assert!(
1381                parse_border_width_value(input).is_err(),
1382                "{input:?} unexpectedly parsed as a border width"
1383            );
1384        }
1385
1386        // ...but note the suffix strip trims what's left of the number, so a
1387        // space between value and unit is silently accepted:
1388        assert_eq!(parse_border_width_value("1 px").unwrap(), PixelValue::px(1.0));
1389    }
1390
1391    #[cfg(feature = "parser")]
1392    #[test]
1393    fn parse_border_width_value_bare_unit_reports_no_value_given() {
1394        let err = parse_border_width_value("px").unwrap_err();
1395        assert!(
1396            matches!(err, CssPixelValueParseError::NoValueGiven(..)),
1397            "expected NoValueGiven, got {err:?}"
1398        );
1399    }
1400
1401    #[cfg(feature = "parser")]
1402    #[test]
1403    fn parse_border_width_value_huge_input_does_not_hang() {
1404        let huge_digits = format!("{}px", "9".repeat(1_000));
1405        let parsed = parse_border_width_value(&huge_digits).unwrap();
1406        assert!(parsed.number.get().is_finite());
1407
1408        let huge_garbage = "z".repeat(100_000);
1409        assert!(parse_border_width_value(&huge_garbage).is_err());
1410
1411        let padded = format!("{}1px{}", " ".repeat(50_000), " ".repeat(50_000));
1412        assert_eq!(
1413            parse_border_width_value(&padded).unwrap(),
1414            PixelValue::px(1.0)
1415        );
1416    }
1417
1418    // =====================================================================
1419    // border-*-width longhands
1420    // =====================================================================
1421
1422    #[cfg(feature = "parser")]
1423    #[test]
1424    fn border_width_longhands_agree_with_each_other() {
1425        for input in ["thin", "medium", "thick", "0", "1.5em", "3px", "50%", "-2pt"] {
1426            let expected = parse_border_width_value(input).unwrap();
1427            assert_eq!(parse_border_top_width(input).unwrap().inner, expected);
1428            assert_eq!(parse_border_right_width(input).unwrap().inner, expected);
1429            assert_eq!(parse_border_bottom_width(input).unwrap().inner, expected);
1430            assert_eq!(parse_border_left_width(input).unwrap().inner, expected);
1431        }
1432        for input in ["", "   ", "px", "abc", "\u{1F600}", "1px 2px"] {
1433            assert!(parse_border_top_width(input).is_err(), "top: {input:?}");
1434            assert!(parse_border_right_width(input).is_err(), "right: {input:?}");
1435            assert!(
1436                parse_border_bottom_width(input).is_err(),
1437                "bottom: {input:?}"
1438            );
1439            assert!(parse_border_left_width(input).is_err(), "left: {input:?}");
1440        }
1441    }
1442
1443    #[cfg(feature = "parser")]
1444    #[test]
1445    fn border_width_longhands_round_trip_through_display() {
1446        for value in [
1447            PixelValue::px(0.0),
1448            PixelValue::px(1.0),
1449            PixelValue::px(1.5),
1450            PixelValue::px(-3.25),
1451            PixelValue::em(2.0),
1452            PixelValue::rem(0.5),
1453            PixelValue::pt(12.0),
1454            PixelValue::inch(1.0),
1455            PixelValue::cm(2.5),
1456            PixelValue::mm(10.0),
1457            PixelValue::percent(50.0),
1458            THIN_BORDER_THICKNESS,
1459            MEDIUM_BORDER_THICKNESS,
1460            THICK_BORDER_THICKNESS,
1461        ] {
1462            let encoded = value.to_string();
1463            let decoded = parse_border_top_width(&encoded)
1464                .unwrap_or_else(|e| panic!("{encoded} failed to re-parse: {e:?}"))
1465                .inner;
1466            assert_eq!(decoded, value, "{encoded} did not round-trip");
1467        }
1468    }
1469
1470    #[cfg(feature = "parser")]
1471    #[test]
1472    fn border_width_longhands_inherit_the_vmin_suffix_shadowing_bug() {
1473        // FIXED (this pin flipped, as intended): the suffix table used to test "in"
1474        // before "vmin", so "5vmin" stripped "in" and failed to parse "5vm" — every
1475        // `border-width: 5vmin` was rejected. The table now orders "vmin" ahead of "in".
1476        assert_eq!(
1477            parse_border_top_width("5vmin").unwrap().inner,
1478            PixelValue::from_metric(crate::props::basic::SizeMetric::Vmin, 5.0)
1479        );
1480        assert_eq!(
1481            parse_border_left_width("5vmin").unwrap().inner,
1482            PixelValue::from_metric(crate::props::basic::SizeMetric::Vmin, 5.0)
1483        );
1484        assert_eq!(
1485            parse_border_top_width("5vmax").unwrap().inner,
1486            PixelValue::from_metric(crate::props::basic::SizeMetric::Vmax, 5.0)
1487        );
1488        assert_eq!(
1489            parse_border_top_width("5vw").unwrap().inner,
1490            PixelValue::from_metric(crate::props::basic::SizeMetric::Vw, 5.0)
1491        );
1492    }
1493
1494    // =====================================================================
1495    // border-*-color longhands
1496    // =====================================================================
1497
1498    #[cfg(feature = "parser")]
1499    #[test]
1500    fn border_color_longhands_agree_with_each_other() {
1501        for input in [
1502            "#ff0000",
1503            "#f0f",
1504            "#11223344",
1505            "red",
1506            "transparent",
1507            "rgb(1, 2, 3)",
1508            "rgba(10, 20, 30, 0.5)",
1509            "hsl(0, 100%, 50%)",
1510        ] {
1511            let expected = parse_css_color(input)
1512                .unwrap_or_else(|e| panic!("{input:?} failed to parse: {e:?}"));
1513            assert_eq!(parse_border_top_color(input).unwrap().inner, expected);
1514            assert_eq!(parse_border_right_color(input).unwrap().inner, expected);
1515            assert_eq!(parse_border_bottom_color(input).unwrap().inner, expected);
1516            assert_eq!(parse_border_left_color(input).unwrap().inner, expected);
1517        }
1518    }
1519
1520    #[cfg(feature = "parser")]
1521    #[test]
1522    fn border_color_longhands_round_trip_through_to_hash() {
1523        for color in [
1524            ColorU::BLACK,
1525            ColorU::WHITE,
1526            ColorU::RED,
1527            ColorU::BLUE,
1528            ColorU::TRANSPARENT,
1529            ColorU::new(1, 2, 3, 4),
1530            ColorU::new(255, 254, 253, 252),
1531            ColorU::new(0, 128, 0, 255),
1532        ] {
1533            let encoded = color.to_hash();
1534            assert_eq!(
1535                parse_border_left_color(&encoded)
1536                    .unwrap_or_else(|e| panic!("{encoded} failed to re-parse: {e:?}"))
1537                    .inner,
1538                color,
1539                "{encoded} did not round-trip"
1540            );
1541        }
1542    }
1543
1544    #[cfg(feature = "parser")]
1545    #[test]
1546    fn border_color_longhands_empty_input_is_an_error() {
1547        for input in ["", " ", "\t\n  "] {
1548            let err = parse_border_top_color(input).unwrap_err();
1549            assert!(
1550                matches!(err, CssColorParseError::EmptyInput),
1551                "{input:?} -> {err:?}"
1552            );
1553        }
1554    }
1555
1556    #[cfg(feature = "parser")]
1557    #[test]
1558    fn border_color_longhands_reject_garbage_and_unicode() {
1559        for input in [
1560            "#",
1561            "#z",
1562            "#ff",
1563            "#fffff",
1564            "#\u{1F600}",
1565            "notacolor",
1566            "rgb(1, 2)",
1567            "rgb(1, 2, 3, 4, 5)",
1568            "\u{1F600}",
1569            "red;",
1570            "red blue",
1571            "0",
1572        ] {
1573            assert!(
1574                parse_border_top_color(input).is_err(),
1575                "{input:?} unexpectedly parsed as a color"
1576            );
1577        }
1578    }
1579
1580    #[cfg(feature = "parser")]
1581    #[test]
1582    fn border_color_longhands_never_panic_on_hostile_input() {
1583        let long = "f".repeat(100_000);
1584        let nested = format!("rgb{}1,2,3{}", "(".repeat(10_000), ")".repeat(10_000));
1585        let deep_parens = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1586        let hostile: [&str; 18] = [
1587            "#",
1588            "##ff0000",
1589            "rgb(",
1590            "rgb()",
1591            "rgba(0,0,0,NaN)",
1592            "rgba(0,0,0,inf)",
1593            "rgb(-1,-2,-3)",
1594            "rgb(999,999,999)",
1595            "rgb(NaN, NaN, NaN)",
1596            "hsl(inf, 0%, 0%)",
1597            "hsla(NaN, NaN%, NaN%, NaN)",
1598            "\u{0}",
1599            "\u{202e}",
1600            "s\u{0301}",
1601            ")))",
1602            &long,
1603            &nested,
1604            &deep_parens,
1605        ];
1606        for input in hostile {
1607            // The contract is only "returns, never panics / never overflows the stack".
1608            let _ = parse_border_top_color(input);
1609            let _ = parse_border_right_color(input);
1610            let _ = parse_border_bottom_color(input);
1611            let _ = parse_border_left_color(input);
1612        }
1613    }
1614
1615    // =====================================================================
1616    // parse_border_side / parse_style_border
1617    // =====================================================================
1618
1619    #[cfg(feature = "parser")]
1620    #[test]
1621    fn parse_border_side_positive_control() {
1622        let side = parse_border_side("1px solid red").unwrap();
1623        assert_eq!(side.border_width, PixelValue::px(1.0));
1624        assert_eq!(side.border_style, BorderStyle::Solid);
1625        assert_eq!(side.border_color, ColorU::RED);
1626    }
1627
1628    #[cfg(feature = "parser")]
1629    #[test]
1630    fn parse_border_side_is_component_order_independent() {
1631        let expected = StyleBorderSide {
1632            border_width: PixelValue::px(2.0),
1633            border_style: BorderStyle::Dashed,
1634            border_color: ColorU::new_rgb(0, 255, 0),
1635        };
1636        for input in [
1637            "2px dashed #00ff00",
1638            "2px #00ff00 dashed",
1639            "dashed 2px #00ff00",
1640            "dashed #00ff00 2px",
1641            "#00ff00 2px dashed",
1642            "#00ff00 dashed 2px",
1643            "  2px   dashed   #00ff00  ",
1644            "\t2px\ndashed\r#00ff00\t",
1645        ] {
1646            assert_eq!(
1647                parse_border_side(input).unwrap(),
1648                expected,
1649                "{input:?} parsed differently"
1650            );
1651        }
1652    }
1653
1654    #[cfg(feature = "parser")]
1655    #[test]
1656    fn parse_border_side_applies_defaults_for_missing_components() {
1657        // Missing components fall back to medium / none / black.
1658        let only_style = parse_border_side("inset").unwrap();
1659        assert_eq!(only_style.border_width, MEDIUM_BORDER_THICKNESS);
1660        assert_eq!(only_style.border_style, BorderStyle::Inset);
1661        assert_eq!(only_style.border_color, ColorU::BLACK);
1662
1663        let only_width = parse_border_side("7px").unwrap();
1664        assert_eq!(only_width.border_width, PixelValue::px(7.0));
1665        assert_eq!(only_width.border_style, BorderStyle::None);
1666        assert_eq!(only_width.border_color, ColorU::BLACK);
1667
1668        let only_color = parse_border_side("blue").unwrap();
1669        assert_eq!(only_color.border_width, MEDIUM_BORDER_THICKNESS);
1670        assert_eq!(only_color.border_style, BorderStyle::None);
1671        assert_eq!(only_color.border_color, ColorU::BLUE);
1672
1673        // keyword widths work in the shorthand too
1674        let keyword = parse_border_side("thin solid").unwrap();
1675        assert_eq!(keyword.border_width, THIN_BORDER_THICKNESS);
1676        assert_eq!(keyword.border_style, BorderStyle::Solid);
1677    }
1678
1679    #[cfg(feature = "parser")]
1680    #[test]
1681    fn parse_border_side_rejects_duplicate_components() {
1682        for input in [
1683            "1px 2px solid red",
1684            "solid dashed red",
1685            "red blue solid",
1686            "1px solid red 2px",
1687            "1px solid red solid",
1688            "1px solid red red",
1689        ] {
1690            let err = parse_border_side(input).unwrap_err();
1691            assert!(
1692                matches!(err, CssBorderSideParseError::InvalidDeclaration(s) if s == input),
1693                "{input:?} -> {err:?}"
1694            );
1695        }
1696    }
1697
1698    #[cfg(feature = "parser")]
1699    #[test]
1700    fn parse_border_side_empty_and_whitespace_only_are_errors() {
1701        for input in ["", " ", "\t\n", "      "] {
1702            let err = parse_border_side(input).unwrap_err();
1703            // The raw (untrimmed) input is echoed back in the error.
1704            assert!(
1705                matches!(err, CssBorderSideParseError::InvalidDeclaration(s) if s == input),
1706                "{input:?} -> {err:?}"
1707            );
1708        }
1709    }
1710
1711    #[cfg(feature = "parser")]
1712    #[test]
1713    fn parse_border_side_rejects_unknown_tokens() {
1714        for input in [
1715            "1px unknown red",
1716            "1px solid red !important",
1717            "1px solid red;",
1718            "\u{1F600}",
1719            "1px solid \u{1F600}",
1720            "solid \u{0}",
1721            "1px, solid, red",
1722        ] {
1723            assert!(
1724                parse_border_side(input).is_err(),
1725                "{input:?} unexpectedly parsed as a border shorthand"
1726            );
1727        }
1728    }
1729
1730    #[cfg(feature = "parser")]
1731    #[test]
1732    fn parse_border_side_hostile_numbers_stay_finite() {
1733        // "NaN" / "inf" are valid f32 literals, so they *do* parse as widths —
1734        // but the isize-backed FloatValue saturates them to a finite value.
1735        for input in ["NaN solid red", "inf solid red", "1e999 solid red"] {
1736            let side = parse_border_side(input)
1737                .unwrap_or_else(|e| panic!("{input:?} failed to parse: {e:?}"));
1738            assert!(
1739                side.border_width.number.get().is_finite(),
1740                "{input:?} produced a non-finite width: {:?}",
1741                side.border_width
1742            );
1743            assert_eq!(side.border_style, BorderStyle::Solid);
1744            assert_eq!(side.border_color, ColorU::RED);
1745        }
1746        assert_eq!(
1747            parse_border_side("NaN solid red").unwrap().border_width,
1748            PixelValue::px(0.0)
1749        );
1750    }
1751
1752    #[cfg(feature = "parser")]
1753    #[test]
1754    fn parse_border_side_long_and_nested_input_does_not_hang() {
1755        // Repeated tokens: the 2nd `solid` cannot be re-assigned, so this must
1756        // bail out immediately rather than scanning all 50k tokens.
1757        let repeated = "solid ".repeat(50_000);
1758        assert!(parse_border_side(&repeated).is_err());
1759
1760        let repeated_px = "1px ".repeat(50_000);
1761        assert!(parse_border_side(&repeated_px).is_err());
1762
1763        let huge_token = "z".repeat(100_000);
1764        assert!(parse_border_side(&huge_token).is_err());
1765
1766        let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1767        assert!(parse_border_side(&nested).is_err());
1768
1769        let padded = format!("{}1px solid red{}", " ".repeat(50_000), " ".repeat(50_000));
1770        assert_eq!(
1771            parse_border_side(&padded).unwrap().border_style,
1772            BorderStyle::Solid
1773        );
1774    }
1775
1776    #[cfg(feature = "parser")]
1777    #[test]
1778    fn parse_style_border_is_an_alias_of_parse_border_side() {
1779        for input in [
1780            "1px solid red",
1781            "thick double",
1782            "inset",
1783            "",
1784            "   ",
1785            "1px 2px solid red",
1786            "\u{1F600}",
1787            "solid green 1em",
1788        ] {
1789            match (parse_style_border(input), parse_border_side(input)) {
1790                (Ok(a), Ok(b)) => assert_eq!(a, b, "{input:?}"),
1791                (Err(a), Err(b)) => assert_eq!(a, b, "{input:?}"),
1792                (a, b) => panic!("{input:?}: alias disagrees: {a:?} vs {b:?}"),
1793            }
1794        }
1795    }
1796
1797    // =====================================================================
1798    // border-color shorthand
1799    // =====================================================================
1800
1801    #[cfg(feature = "parser")]
1802    #[test]
1803    fn parse_style_border_color_expands_one_to_four_values() {
1804        let red = ColorU::RED;
1805        let blue = ColorU::BLUE;
1806        let green = ColorU::new_rgb(0, 128, 0);
1807        let white = ColorU::WHITE;
1808
1809        let one = parse_style_border_color("red").unwrap();
1810        assert_eq!(
1811            one,
1812            StyleBorderColors {
1813                top: red,
1814                right: red,
1815                bottom: red,
1816                left: red
1817            }
1818        );
1819
1820        // 2 values: top/bottom, left/right
1821        let two = parse_style_border_color("red blue").unwrap();
1822        assert_eq!(
1823            two,
1824            StyleBorderColors {
1825                top: red,
1826                right: blue,
1827                bottom: red,
1828                left: blue
1829            }
1830        );
1831
1832        // 3 values: top, left/right, bottom
1833        let three = parse_style_border_color("red blue green").unwrap();
1834        assert_eq!(
1835            three,
1836            StyleBorderColors {
1837                top: red,
1838                right: blue,
1839                bottom: green,
1840                left: blue
1841            }
1842        );
1843
1844        // 4 values: top, right, bottom, left
1845        let four = parse_style_border_color("red blue green white").unwrap();
1846        assert_eq!(
1847            four,
1848            StyleBorderColors {
1849                top: red,
1850                right: blue,
1851                bottom: green,
1852                left: white
1853            }
1854        );
1855    }
1856
1857    #[cfg(feature = "parser")]
1858    #[test]
1859    fn parse_style_border_color_normalizes_whitespace() {
1860        let expected = parse_style_border_color("red blue green").unwrap();
1861        for input in [
1862            "  red blue green  ",
1863            "red\tblue\ngreen",
1864            "red   blue \r\n green",
1865        ] {
1866            assert_eq!(parse_style_border_color(input).unwrap(), expected, "{input:?}");
1867        }
1868    }
1869
1870    #[cfg(feature = "parser")]
1871    #[test]
1872    fn parse_style_border_color_rejects_zero_and_more_than_four_values() {
1873        for input in ["", "   ", "\t\n"] {
1874            let err = parse_style_border_color(input).unwrap_err();
1875            assert!(
1876                matches!(err, CssColorParseError::InvalidColor(_)),
1877                "{input:?} -> {err:?}"
1878            );
1879        }
1880        let too_many = "red ".repeat(1_000);
1881        let inputs: [&str; 3] = [
1882            "red red red red red",
1883            "red blue green white black yellow",
1884            &too_many,
1885        ];
1886        for input in inputs {
1887            assert!(
1888                parse_style_border_color(input).is_err(),
1889                "{input:?} unexpectedly parsed"
1890            );
1891        }
1892    }
1893
1894    #[cfg(feature = "parser")]
1895    #[test]
1896    fn parse_style_border_color_propagates_component_errors() {
1897        for input in [
1898            "notacolor",
1899            "red notacolor",
1900            "red blue notacolor",
1901            "red blue green notacolor",
1902            "red \u{1F600}",
1903            "#zzz",
1904        ] {
1905            assert!(
1906                parse_style_border_color(input).is_err(),
1907                "{input:?} unexpectedly parsed"
1908            );
1909        }
1910    }
1911
1912    #[cfg(feature = "parser")]
1913    #[test]
1914    fn parse_style_border_color_round_trips_through_to_hash() {
1915        let colors = StyleBorderColors {
1916            top: ColorU::new(1, 2, 3, 4),
1917            right: ColorU::new(255, 0, 0, 255),
1918            bottom: ColorU::TRANSPARENT,
1919            left: ColorU::new(9, 8, 7, 6),
1920        };
1921        let encoded = format!(
1922            "{} {} {} {}",
1923            colors.top.to_hash(),
1924            colors.right.to_hash(),
1925            colors.bottom.to_hash(),
1926            colors.left.to_hash()
1927        );
1928        assert_eq!(parse_style_border_color(&encoded).unwrap(), colors);
1929    }
1930
1931    // =====================================================================
1932    // border-style shorthand
1933    // =====================================================================
1934
1935    #[cfg(feature = "parser")]
1936    #[test]
1937    fn parse_style_border_style_expands_one_to_four_values() {
1938        assert_eq!(
1939            parse_style_border_style("solid").unwrap(),
1940            StyleBorderStyles {
1941                top: BorderStyle::Solid,
1942                right: BorderStyle::Solid,
1943                bottom: BorderStyle::Solid,
1944                left: BorderStyle::Solid,
1945            }
1946        );
1947        assert_eq!(
1948            parse_style_border_style("solid dashed").unwrap(),
1949            StyleBorderStyles {
1950                top: BorderStyle::Solid,
1951                right: BorderStyle::Dashed,
1952                bottom: BorderStyle::Solid,
1953                left: BorderStyle::Dashed,
1954            }
1955        );
1956        assert_eq!(
1957            parse_style_border_style("solid dashed dotted").unwrap(),
1958            StyleBorderStyles {
1959                top: BorderStyle::Solid,
1960                right: BorderStyle::Dashed,
1961                bottom: BorderStyle::Dotted,
1962                left: BorderStyle::Dashed,
1963            }
1964        );
1965        assert_eq!(
1966            parse_style_border_style("solid dashed dotted double").unwrap(),
1967            StyleBorderStyles {
1968                top: BorderStyle::Solid,
1969                right: BorderStyle::Dashed,
1970                bottom: BorderStyle::Dotted,
1971                left: BorderStyle::Double,
1972            }
1973        );
1974    }
1975
1976    #[cfg(feature = "parser")]
1977    #[test]
1978    fn parse_style_border_style_rejects_zero_and_more_than_four_values() {
1979        for input in ["", "   ", "\t\n"] {
1980            let err = parse_style_border_style(input).unwrap_err();
1981            // NOTE: the error payload here is the *trimmed* input, unlike
1982            // parse_border_style, which echoes the raw input back.
1983            assert!(
1984                matches!(err, CssBorderStyleParseError::InvalidStyle(s) if s == input.trim()),
1985                "{input:?} -> {err:?}"
1986            );
1987        }
1988        let too_many = "dotted ".repeat(1_000);
1989        let inputs: [&str; 2] = ["solid solid solid solid solid", &too_many];
1990        for input in inputs {
1991            assert!(
1992                parse_style_border_style(input).is_err(),
1993                "{input:?} unexpectedly parsed"
1994            );
1995        }
1996    }
1997
1998    #[cfg(feature = "parser")]
1999    #[test]
2000    fn parse_style_border_style_propagates_component_errors() {
2001        for input in [
2002            "bogus",
2003            "solid bogus",
2004            "solid solid bogus",
2005            "solid solid solid bogus",
2006            "solid \u{1F600}",
2007            "SOLID",
2008        ] {
2009            assert!(
2010                parse_style_border_style(input).is_err(),
2011                "{input:?} unexpectedly parsed"
2012            );
2013        }
2014    }
2015
2016    #[cfg(feature = "parser")]
2017    #[test]
2018    fn parse_style_border_style_round_trips_through_display() {
2019        for (t, r, b, l) in [
2020            (
2021                BorderStyle::Solid,
2022                BorderStyle::Dashed,
2023                BorderStyle::Dotted,
2024                BorderStyle::Double,
2025            ),
2026            (
2027                BorderStyle::None,
2028                BorderStyle::Hidden,
2029                BorderStyle::Groove,
2030                BorderStyle::Ridge,
2031            ),
2032            (
2033                BorderStyle::Inset,
2034                BorderStyle::Outset,
2035                BorderStyle::None,
2036                BorderStyle::Solid,
2037            ),
2038        ] {
2039            let expected = StyleBorderStyles {
2040                top: t,
2041                right: r,
2042                bottom: b,
2043                left: l,
2044            };
2045            let encoded = format!("{t} {r} {b} {l}");
2046            assert_eq!(
2047                parse_style_border_style(&encoded).unwrap(),
2048                expected,
2049                "{encoded} did not round-trip"
2050            );
2051        }
2052    }
2053
2054    // =====================================================================
2055    // border-width shorthand
2056    // =====================================================================
2057
2058    #[cfg(feature = "parser")]
2059    #[test]
2060    fn parse_style_border_width_expands_one_to_four_values() {
2061        assert_eq!(
2062            parse_style_border_width("1px").unwrap(),
2063            StyleBorderWidths {
2064                top: PixelValue::px(1.0),
2065                right: PixelValue::px(1.0),
2066                bottom: PixelValue::px(1.0),
2067                left: PixelValue::px(1.0),
2068            }
2069        );
2070        assert_eq!(
2071            parse_style_border_width("1px 2px").unwrap(),
2072            StyleBorderWidths {
2073                top: PixelValue::px(1.0),
2074                right: PixelValue::px(2.0),
2075                bottom: PixelValue::px(1.0),
2076                left: PixelValue::px(2.0),
2077            }
2078        );
2079        assert_eq!(
2080            parse_style_border_width("1px 2px 3px").unwrap(),
2081            StyleBorderWidths {
2082                top: PixelValue::px(1.0),
2083                right: PixelValue::px(2.0),
2084                bottom: PixelValue::px(3.0),
2085                left: PixelValue::px(2.0),
2086            }
2087        );
2088        assert_eq!(
2089            parse_style_border_width("1px 2em 3pt 4%").unwrap(),
2090            StyleBorderWidths {
2091                top: PixelValue::px(1.0),
2092                right: PixelValue::em(2.0),
2093                bottom: PixelValue::pt(3.0),
2094                left: PixelValue::percent(4.0),
2095            }
2096        );
2097    }
2098
2099    #[cfg(feature = "parser")]
2100    #[test]
2101    fn parse_style_border_width_rejects_zero_and_more_than_four_values() {
2102        for input in ["", "   ", "\t\n"] {
2103            let err = parse_style_border_width(input).unwrap_err();
2104            assert!(
2105                matches!(err, CssPixelValueParseError::InvalidPixelValue(s) if s == input.trim()),
2106                "{input:?} -> {err:?}"
2107            );
2108        }
2109        let too_many = "1px ".repeat(1_000);
2110        let inputs: [&str; 2] = ["1px 1px 1px 1px 1px", &too_many];
2111        for input in inputs {
2112            assert!(
2113                parse_style_border_width(input).is_err(),
2114                "{input:?} unexpectedly parsed"
2115            );
2116        }
2117    }
2118
2119    #[cfg(feature = "parser")]
2120    #[test]
2121    fn parse_style_border_width_rejects_the_thin_medium_thick_keywords() {
2122        // KNOWN DIVERGENCE: the longhands (parse_border_top_width) go through
2123        // parse_border_width_value and DO accept thin/medium/thick, but this
2124        // shorthand calls parse_pixel_value directly, so `border-width: thin`
2125        // is rejected. Worse, "thin" ends with the "in" (inch) suffix, so it is
2126        // reported as a broken *inches* value rather than an unknown keyword.
2127        let err = parse_style_border_width("thin").unwrap_err();
2128        assert!(
2129            matches!(err, CssPixelValueParseError::ValueParseErr(_, s) if s == "th"),
2130            "expected `thin` to be misread as inches, got {err:?}"
2131        );
2132        assert!(parse_style_border_width("medium").is_err());
2133        assert!(parse_style_border_width("thick").is_err());
2134        assert!(parse_style_border_width("thin thick").is_err());
2135
2136        // ...while the longhand happily accepts all three:
2137        assert_eq!(
2138            parse_border_top_width("thin").unwrap().inner,
2139            THIN_BORDER_THICKNESS
2140        );
2141    }
2142
2143    #[cfg(feature = "parser")]
2144    #[test]
2145    fn parse_style_border_width_propagates_component_errors() {
2146        for input in [
2147            "abc",
2148            "1px abc",
2149            "1px 2px abc",
2150            "1px 2px 3px abc",
2151            "1px \u{1F600}",
2152            "1PX",
2153        ] {
2154            assert!(
2155                parse_style_border_width(input).is_err(),
2156                "{input:?} unexpectedly parsed"
2157            );
2158        }
2159    }
2160
2161    #[cfg(feature = "parser")]
2162    #[test]
2163    fn parse_style_border_width_hostile_numbers_stay_finite() {
2164        let widths = parse_style_border_width("NaN inf -inf 1e999").unwrap();
2165        for w in [widths.top, widths.right, widths.bottom, widths.left] {
2166            assert!(
2167                w.number.get().is_finite(),
2168                "hostile width did not saturate: {w:?}"
2169            );
2170        }
2171        assert_eq!(widths.top, PixelValue::px(0.0)); // NaN -> 0
2172    }
2173
2174    #[cfg(feature = "parser")]
2175    #[test]
2176    fn parse_style_border_width_round_trips_through_display() {
2177        let widths = StyleBorderWidths {
2178            top: PixelValue::px(1.5),
2179            right: PixelValue::em(2.0),
2180            bottom: PixelValue::pt(3.25),
2181            left: PixelValue::percent(50.0),
2182        };
2183        let encoded = format!(
2184            "{} {} {} {}",
2185            widths.top, widths.right, widths.bottom, widths.left
2186        );
2187        assert_eq!(parse_style_border_width(&encoded).unwrap(), widths);
2188    }
2189
2190    // =====================================================================
2191    // Error types: to_contained / to_shared / From / Display
2192    // =====================================================================
2193
2194    #[cfg(feature = "parser")]
2195    #[test]
2196    fn css_border_style_parse_error_round_trips_owned_and_shared() {
2197        let huge = "x".repeat(10_000);
2198        let payloads: [&str; 7] = ["", " ", "bogus", "\u{1F600}", "s\u{0301}", "\u{0}", &huge];
2199        for payload in payloads {
2200            let shared = CssBorderStyleParseError::InvalidStyle(payload);
2201            let owned = shared.to_contained();
2202            assert_eq!(owned.to_shared(), shared, "{payload:?} did not round-trip");
2203            assert!(
2204                matches!(&owned, CssBorderStyleParseErrorOwned::InvalidStyle(s) if s.as_str() == payload)
2205            );
2206        }
2207    }
2208
2209    #[cfg(feature = "parser")]
2210    #[test]
2211    fn css_border_style_parse_error_from_a_real_parse_failure_round_trips() {
2212        let err = parse_border_style("\u{1F600}bogus").unwrap_err();
2213        let owned = err.to_contained();
2214        assert_eq!(owned.to_shared(), err);
2215        // Debug is routed through Display, so both must mention the input.
2216        assert!(format!("{err}").contains("bogus"));
2217        assert!(format!("{err:?}").contains("bogus"));
2218        assert!(!format!("{owned:?}").is_empty());
2219    }
2220
2221    #[cfg(feature = "parser")]
2222    #[test]
2223    fn css_border_side_parse_error_round_trips_for_every_variant() {
2224        let variants = [
2225            CssBorderSideParseError::InvalidDeclaration(""),
2226            CssBorderSideParseError::InvalidDeclaration("1px 2px solid"),
2227            CssBorderSideParseError::InvalidDeclaration("\u{1F600}"),
2228            CssBorderSideParseError::Width(CssPixelValueParseError::EmptyString),
2229            CssBorderSideParseError::Width(CssPixelValueParseError::InvalidPixelValue("zz")),
2230            CssBorderSideParseError::Style(CssBorderStyleParseError::InvalidStyle("zz")),
2231            CssBorderSideParseError::Style(CssBorderStyleParseError::InvalidStyle("")),
2232            CssBorderSideParseError::Color(CssColorParseError::InvalidColor("zz")),
2233            CssBorderSideParseError::Color(CssColorParseError::EmptyInput),
2234            CssBorderSideParseError::Color(CssColorParseError::InvalidColorComponent(u8::MAX)),
2235        ];
2236        for shared in variants {
2237            let owned = shared.to_contained();
2238            assert_eq!(owned.to_shared(), shared, "{shared:?} did not round-trip");
2239            assert!(!format!("{shared}").is_empty());
2240            assert!(!format!("{owned:?}").is_empty());
2241        }
2242    }
2243
2244    #[cfg(feature = "parser")]
2245    #[test]
2246    fn css_border_side_parse_error_from_conversions_pick_the_right_variant() {
2247        let width: CssBorderSideParseError<'_> =
2248            CssPixelValueParseError::InvalidPixelValue("zz").into();
2249        assert!(matches!(width, CssBorderSideParseError::Width(_)));
2250
2251        let style: CssBorderSideParseError<'_> =
2252            CssBorderStyleParseError::InvalidStyle("zz").into();
2253        assert!(matches!(style, CssBorderSideParseError::Style(_)));
2254
2255        let color: CssBorderSideParseError<'_> = CssColorParseError::InvalidColor("zz").into();
2256        assert!(matches!(color, CssBorderSideParseError::Color(_)));
2257    }
2258
2259    #[cfg(feature = "parser")]
2260    #[test]
2261    fn css_border_parse_error_owned_newtype_wraps_the_side_error() {
2262        let owned = CssBorderSideParseError::InvalidDeclaration("bogus").to_contained();
2263        let wrapped = CssBorderParseErrorOwned::from(owned.clone());
2264        assert_eq!(wrapped.inner, owned);
2265        assert_eq!(wrapped.inner.to_shared().to_contained(), owned);
2266    }
2267
2268    #[cfg(feature = "parser")]
2269    #[test]
2270    fn error_display_mentions_the_offending_input() {
2271        let err = parse_border_side("1px bogus red").unwrap_err();
2272        let msg = format!("{err}");
2273        assert!(msg.contains("1px bogus red"), "unhelpful message: {msg}");
2274
2275        let err = parse_border_style("bogus").unwrap_err();
2276        assert!(format!("{err}").contains("bogus"));
2277
2278        let err = parse_border_top_width("bogus").unwrap_err();
2279        assert!(format!("{err}").contains("bogus"));
2280    }
2281}