Skip to main content

azul_css/props/style/
border.rs

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