Skip to main content

azul_css/props/style/
transform.rs

1//! CSS properties for 2D and 3D transformations.
2
3use alloc::{
4    string::{String, ToString},
5    vec::Vec,
6};
7use core::fmt;
8use std::num::ParseFloatError;
9use crate::corety::AzString;
10
11#[cfg(feature = "parser")]
12use crate::props::basic::{
13    error::WrongComponentCountError,
14    length::parse_float_value,
15    parse::{parse_parentheses, ParenthesisParseError, ParenthesisParseErrorOwned},
16};
17use crate::{
18    codegen::format::GetHash,
19    props::{
20        basic::{
21            angle::{
22                parse_angle_value, AngleValue, CssAngleValueParseError,
23                CssAngleValueParseErrorOwned,
24            },
25            length::{PercentageParseError, PercentageValue},
26            pixel::{
27                parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
28                PixelValue,
29            },
30            FloatValue,
31        },
32        formatter::PrintAsCssValue,
33    },
34};
35
36// -- Data Structures --
37
38/// Represents a `perspective-origin` attribute
39#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
40#[repr(C)]
41pub struct StylePerspectiveOrigin {
42    pub x: PixelValue,
43    pub y: PixelValue,
44}
45
46impl StylePerspectiveOrigin {
47    #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
48        Self {
49            x: self.x.interpolate(&other.x, t),
50            y: self.y.interpolate(&other.y, t),
51        }
52    }
53}
54
55impl PrintAsCssValue for StylePerspectiveOrigin {
56    fn print_as_css_value(&self) -> String {
57        format!("{} {}", self.x, self.y)
58    }
59}
60
61/// Represents a `transform-origin` attribute
62#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
63#[repr(C)]
64pub struct StyleTransformOrigin {
65    pub x: PixelValue,
66    pub y: PixelValue,
67}
68
69impl Default for StyleTransformOrigin {
70    fn default() -> Self {
71        Self {
72            x: PixelValue::const_percent(50),
73            y: PixelValue::const_percent(50),
74        }
75    }
76}
77
78impl StyleTransformOrigin {
79    #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
80        Self {
81            x: self.x.interpolate(&other.x, t),
82            y: self.y.interpolate(&other.y, t),
83        }
84    }
85}
86
87impl PrintAsCssValue for StyleTransformOrigin {
88    fn print_as_css_value(&self) -> String {
89        format!("{} {}", self.x, self.y)
90    }
91}
92
93// Formatting to Rust code
94impl crate::codegen::format::FormatAsRustCode for StylePerspectiveOrigin {
95    fn format_as_rust_code(&self, _tabs: usize) -> String {
96        format!(
97            "StylePerspectiveOrigin {{ x: {}, y: {} }}",
98            crate::codegen::format::format_pixel_value(&self.x),
99            crate::codegen::format::format_pixel_value(&self.y)
100        )
101    }
102}
103
104// Formatting to Rust code for StyleTransformOrigin
105impl crate::codegen::format::FormatAsRustCode for StyleTransformOrigin {
106    fn format_as_rust_code(&self, _tabs: usize) -> String {
107        format!(
108            "StyleTransformOrigin {{ x: {}, y: {} }}",
109            crate::codegen::format::format_pixel_value(&self.x),
110            crate::codegen::format::format_pixel_value(&self.y)
111        )
112    }
113}
114
115/// Represents a `backface-visibility` attribute
116#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
117#[repr(C)]
118pub enum StyleBackfaceVisibility {
119    #[default]
120    Visible,
121    Hidden,
122}
123
124impl PrintAsCssValue for StyleBackfaceVisibility {
125    fn print_as_css_value(&self) -> String {
126        String::from(match self {
127            Self::Hidden => "hidden",
128            Self::Visible => "visible",
129        })
130    }
131}
132
133/// Represents one component of a `transform` attribute
134#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
135#[repr(C, u8)]
136pub enum StyleTransform {
137    Matrix(StyleTransformMatrix2D),
138    Matrix3D(StyleTransformMatrix3D),
139    Translate(StyleTransformTranslate2D),
140    Translate3D(StyleTransformTranslate3D),
141    TranslateX(PixelValue),
142    TranslateY(PixelValue),
143    TranslateZ(PixelValue),
144    Rotate(AngleValue),
145    Rotate3D(StyleTransformRotate3D),
146    RotateX(AngleValue),
147    RotateY(AngleValue),
148    RotateZ(AngleValue),
149    Scale(StyleTransformScale2D),
150    Scale3D(StyleTransformScale3D),
151    ScaleX(PercentageValue),
152    ScaleY(PercentageValue),
153    ScaleZ(PercentageValue),
154    Skew(StyleTransformSkew2D),
155    SkewX(AngleValue),
156    SkewY(AngleValue),
157    Perspective(PixelValue),
158}
159
160impl_option!(
161    StyleTransform,
162    OptionStyleTransform,
163    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
164);
165
166impl_vec!(StyleTransform, StyleTransformVec, StyleTransformVecDestructor, StyleTransformVecDestructorType, StyleTransformVecSlice, OptionStyleTransform);
167impl_vec_debug!(StyleTransform, StyleTransformVec);
168impl_vec_partialord!(StyleTransform, StyleTransformVec);
169impl_vec_ord!(StyleTransform, StyleTransformVec);
170impl_vec_clone!(
171    StyleTransform,
172    StyleTransformVec,
173    StyleTransformVecDestructor
174);
175impl_vec_partialeq!(StyleTransform, StyleTransformVec);
176impl_vec_eq!(StyleTransform, StyleTransformVec);
177impl_vec_hash!(StyleTransform, StyleTransformVec);
178
179impl PrintAsCssValue for StyleTransformVec {
180    fn print_as_css_value(&self) -> String {
181        self.as_ref()
182            .iter()
183            .map(PrintAsCssValue::print_as_css_value)
184            .collect::<Vec<_>>()
185            .join(" ")
186    }
187}
188
189// Formatting to Rust code for StyleTransformVec
190impl crate::codegen::format::FormatAsRustCode for StyleTransformVec {
191    fn format_as_rust_code(&self, _tabs: usize) -> String {
192        format!(
193            "StyleTransformVec::from_const_slice(STYLE_TRANSFORM_{}_ITEMS)",
194            self.get_hash()
195        )
196    }
197}
198
199impl PrintAsCssValue for StyleTransform {
200    fn print_as_css_value(&self) -> String {
201        match self {
202            Self::Matrix(m) => format!(
203                "matrix({}, {}, {}, {}, {}, {})",
204                m.a, m.b, m.c, m.d, m.tx, m.ty
205            ),
206            Self::Matrix3D(m) => format!(
207                "matrix3d({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})",
208                m.m11,
209                m.m12,
210                m.m13,
211                m.m14,
212                m.m21,
213                m.m22,
214                m.m23,
215                m.m24,
216                m.m31,
217                m.m32,
218                m.m33,
219                m.m34,
220                m.m41,
221                m.m42,
222                m.m43,
223                m.m44
224            ),
225            Self::Translate(t) => format!("translate({}, {})", t.x, t.y),
226            Self::Translate3D(t) => format!("translate3d({}, {}, {})", t.x, t.y, t.z),
227            Self::TranslateX(x) => format!("translateX({x})"),
228            Self::TranslateY(y) => format!("translateY({y})"),
229            Self::TranslateZ(z) => format!("translateZ({z})"),
230            Self::Rotate(r) => format!("rotate({r})"),
231            Self::Rotate3D(r) => {
232                format!("rotate3d({}, {}, {}, {})", r.x, r.y, r.z, r.angle)
233            }
234            Self::RotateX(x) => format!("rotateX({x})"),
235            Self::RotateY(y) => format!("rotateY({y})"),
236            Self::RotateZ(z) => format!("rotateZ({z})"),
237            Self::Scale(s) => format!("scale({}, {})", s.x, s.y),
238            Self::Scale3D(s) => format!("scale3d({}, {}, {})", s.x, s.y, s.z),
239            Self::ScaleX(x) => format!("scaleX({x})"),
240            Self::ScaleY(y) => format!("scaleY({y})"),
241            Self::ScaleZ(z) => format!("scaleZ({z})"),
242            Self::Skew(sk) => format!("skew({}, {})", sk.x, sk.y),
243            Self::SkewX(x) => format!("skewX({x})"),
244            Self::SkewY(y) => format!("skewY({y})"),
245            Self::Perspective(dist) => format!("perspective({dist})"),
246        }
247    }
248}
249
250/// Represents a CSS `matrix(a, b, c, d, tx, ty)` 2D transform function.
251#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
252#[repr(C)]
253pub struct StyleTransformMatrix2D {
254    pub a: FloatValue,
255    pub b: FloatValue,
256    pub c: FloatValue,
257    pub d: FloatValue,
258    pub tx: FloatValue,
259    pub ty: FloatValue,
260}
261
262impl Default for StyleTransformMatrix2D {
263    fn default() -> Self {
264        Self {
265            a: FloatValue::const_new(1),
266            b: FloatValue::const_new(0),
267            c: FloatValue::const_new(0),
268            d: FloatValue::const_new(1),
269            tx: FloatValue::const_new(0),
270            ty: FloatValue::const_new(0),
271        }
272    }
273}
274
275/// Represents a CSS `matrix3d(...)` 3D transform function (4x4 matrix).
276#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
277#[repr(C)]
278pub struct StyleTransformMatrix3D {
279    pub m11: FloatValue,
280    pub m12: FloatValue,
281    pub m13: FloatValue,
282    pub m14: FloatValue,
283    pub m21: FloatValue,
284    pub m22: FloatValue,
285    pub m23: FloatValue,
286    pub m24: FloatValue,
287    pub m31: FloatValue,
288    pub m32: FloatValue,
289    pub m33: FloatValue,
290    pub m34: FloatValue,
291    pub m41: FloatValue,
292    pub m42: FloatValue,
293    pub m43: FloatValue,
294    pub m44: FloatValue,
295}
296
297impl Default for StyleTransformMatrix3D {
298    fn default() -> Self {
299        Self {
300            m11: FloatValue::const_new(1),
301            m12: FloatValue::const_new(0),
302            m13: FloatValue::const_new(0),
303            m14: FloatValue::const_new(0),
304            m21: FloatValue::const_new(0),
305            m22: FloatValue::const_new(1),
306            m23: FloatValue::const_new(0),
307            m24: FloatValue::const_new(0),
308            m31: FloatValue::const_new(0),
309            m32: FloatValue::const_new(0),
310            m33: FloatValue::const_new(1),
311            m34: FloatValue::const_new(0),
312            m41: FloatValue::const_new(0),
313            m42: FloatValue::const_new(0),
314            m43: FloatValue::const_new(0),
315            m44: FloatValue::const_new(1),
316        }
317    }
318}
319
320/// Represents a CSS `translate(x, y)` 2D translation.
321#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
322#[repr(C)]
323pub struct StyleTransformTranslate2D {
324    pub x: PixelValue,
325    pub y: PixelValue,
326}
327
328/// Represents a CSS `translate3d(x, y, z)` 3D translation.
329#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
330#[repr(C)]
331pub struct StyleTransformTranslate3D {
332    pub x: PixelValue,
333    pub y: PixelValue,
334    pub z: PixelValue,
335}
336
337/// Represents a CSS `rotate3d(x, y, z, angle)` 3D rotation.
338#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
339#[repr(C)]
340pub struct StyleTransformRotate3D {
341    pub x: FloatValue,
342    pub y: FloatValue,
343    pub z: FloatValue,
344    pub angle: AngleValue,
345}
346
347/// Represents a CSS `scale(x, y)` 2D scaling.
348#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
349#[repr(C)]
350pub struct StyleTransformScale2D {
351    pub x: FloatValue,
352    pub y: FloatValue,
353}
354
355/// Represents a CSS `scale3d(x, y, z)` 3D scaling.
356#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
357#[repr(C)]
358pub struct StyleTransformScale3D {
359    pub x: FloatValue,
360    pub y: FloatValue,
361    pub z: FloatValue,
362}
363
364/// Represents a CSS `skew(x, y)` 2D skew transformation.
365#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
366#[repr(C)]
367pub struct StyleTransformSkew2D {
368    pub x: AngleValue,
369    pub y: AngleValue,
370}
371
372// -- Errors --
373
374#[derive(Clone, PartialEq, Eq)]
375pub enum CssStyleTransformParseError<'a> {
376    InvalidTransform(&'a str),
377    InvalidParenthesis(ParenthesisParseError<'a>),
378    WrongNumberOfComponents {
379        expected: usize,
380        got: usize,
381        input: &'a str,
382    },
383    NumberParseError(ParseFloatError),
384    PixelValueParseError(CssPixelValueParseError<'a>),
385    AngleValueParseError(CssAngleValueParseError<'a>),
386    PercentageValueParseError(PercentageParseError),
387}
388
389impl_debug_as_display!(CssStyleTransformParseError<'a>);
390impl_display! { CssStyleTransformParseError<'a>, {
391    InvalidTransform(e) => format!("Invalid transform property: \"{}\"", e),
392    InvalidParenthesis(e) => format!("Invalid transform property - parenthesis error: {}", e),
393    WrongNumberOfComponents { expected, got, input } => format!("Invalid number of components: expected {}, got {}: \"{}\"", expected, got, input),
394    NumberParseError(e) => format!("Could not parse number: {}", e),
395    PixelValueParseError(e) => format!("Invalid pixel value: {}", e),
396    AngleValueParseError(e) => format!("Invalid angle value: {}", e),
397    PercentageValueParseError(e) => format!("Error parsing percentage: {}", e),
398}}
399
400impl_from! { ParenthesisParseError<'a>, CssStyleTransformParseError::InvalidParenthesis }
401impl_from! { CssPixelValueParseError<'a>, CssStyleTransformParseError::PixelValueParseError }
402impl_from! { CssAngleValueParseError<'a>, CssStyleTransformParseError::AngleValueParseError }
403// Written out (not impl_from!): ParseFloatError carries no lifetime, so the
404// macro's `<'a>` would be used only by the target type (single_use_lifetimes).
405impl From<ParseFloatError> for CssStyleTransformParseError<'_> {
406    fn from(e: ParseFloatError) -> Self {
407        Self::NumberParseError(e)
408    }
409}
410
411impl From<PercentageParseError> for CssStyleTransformParseError<'_> {
412    fn from(p: PercentageParseError) -> Self {
413        Self::PercentageValueParseError(p)
414    }
415}
416
417#[derive(Debug, Clone, PartialEq, Eq)]
418#[repr(C, u8)]
419pub enum CssStyleTransformParseErrorOwned {
420    InvalidTransform(AzString),
421    InvalidParenthesis(ParenthesisParseErrorOwned),
422    WrongNumberOfComponents(WrongComponentCountError),
423    NumberParseError(crate::props::basic::error::ParseFloatError),
424    PixelValueParseError(CssPixelValueParseErrorOwned),
425    AngleValueParseError(CssAngleValueParseErrorOwned),
426    PercentageValueParseError(PercentageParseError),
427}
428
429impl CssStyleTransformParseError<'_> {
430    #[must_use] pub fn to_contained(&self) -> CssStyleTransformParseErrorOwned {
431        match self {
432            Self::InvalidTransform(s) => {
433                CssStyleTransformParseErrorOwned::InvalidTransform((*s).to_string().into())
434            }
435            Self::InvalidParenthesis(e) => {
436                CssStyleTransformParseErrorOwned::InvalidParenthesis(e.to_contained())
437            }
438            Self::WrongNumberOfComponents {
439                expected,
440                got,
441                input,
442            } => CssStyleTransformParseErrorOwned::WrongNumberOfComponents(WrongComponentCountError {
443                expected: *expected,
444                got: *got,
445                input: (*input).to_string().into(),
446            }),
447            Self::NumberParseError(e) => {
448                CssStyleTransformParseErrorOwned::NumberParseError(e.clone().into())
449            }
450            Self::PixelValueParseError(e) => {
451                CssStyleTransformParseErrorOwned::PixelValueParseError(e.to_contained())
452            }
453            Self::AngleValueParseError(e) => {
454                CssStyleTransformParseErrorOwned::AngleValueParseError(e.to_contained())
455            }
456            Self::PercentageValueParseError(e) => {
457                CssStyleTransformParseErrorOwned::PercentageValueParseError(e.clone())
458            }
459        }
460    }
461}
462
463impl CssStyleTransformParseErrorOwned {
464    #[must_use] pub fn to_shared(&self) -> CssStyleTransformParseError<'_> {
465        match self {
466            Self::InvalidTransform(s) => CssStyleTransformParseError::InvalidTransform(s),
467            Self::InvalidParenthesis(e) => {
468                CssStyleTransformParseError::InvalidParenthesis(e.to_shared())
469            }
470            Self::WrongNumberOfComponents(e) => CssStyleTransformParseError::WrongNumberOfComponents {
471                expected: e.expected,
472                got: e.got,
473                input: e.input.as_str(),
474            },
475            Self::NumberParseError(e) => CssStyleTransformParseError::NumberParseError(e.to_std()),
476            Self::PixelValueParseError(e) => {
477                CssStyleTransformParseError::PixelValueParseError(e.to_shared())
478            }
479            Self::AngleValueParseError(e) => {
480                CssStyleTransformParseError::AngleValueParseError(e.to_shared())
481            }
482            Self::PercentageValueParseError(e) => {
483                CssStyleTransformParseError::PercentageValueParseError(e.clone())
484            }
485        }
486    }
487}
488
489#[derive(Clone, PartialEq, Eq)]
490pub enum CssStyleTransformOriginParseError<'a> {
491    WrongNumberOfComponents {
492        expected: usize,
493        got: usize,
494        input: &'a str,
495    },
496    PixelValueParseError(CssPixelValueParseError<'a>),
497}
498
499impl_debug_as_display!(CssStyleTransformOriginParseError<'a>);
500impl_display! { CssStyleTransformOriginParseError<'a>, {
501    WrongNumberOfComponents { expected, got, input } => format!("Invalid number of components: expected {}, got {}: \"{}\"", expected, got, input),
502    PixelValueParseError(e) => format!("Invalid pixel value: {}", e),
503}}
504impl_from! { CssPixelValueParseError<'a>, CssStyleTransformOriginParseError::PixelValueParseError }
505
506#[derive(Debug, Clone, PartialEq, Eq)]
507#[repr(C, u8)]
508pub enum CssStyleTransformOriginParseErrorOwned {
509    WrongNumberOfComponents(WrongComponentCountError),
510    PixelValueParseError(CssPixelValueParseErrorOwned),
511}
512
513impl CssStyleTransformOriginParseError<'_> {
514    #[must_use] pub fn to_contained(&self) -> CssStyleTransformOriginParseErrorOwned {
515        match self {
516            Self::WrongNumberOfComponents {
517                expected,
518                got,
519                input,
520            } => CssStyleTransformOriginParseErrorOwned::WrongNumberOfComponents(WrongComponentCountError {
521                expected: *expected,
522                got: *got,
523                input: (*input).to_string().into(),
524            }),
525            Self::PixelValueParseError(e) => {
526                CssStyleTransformOriginParseErrorOwned::PixelValueParseError(e.to_contained())
527            }
528        }
529    }
530}
531
532impl CssStyleTransformOriginParseErrorOwned {
533    #[must_use] pub fn to_shared(&self) -> CssStyleTransformOriginParseError<'_> {
534        match self {
535            Self::WrongNumberOfComponents(e) => CssStyleTransformOriginParseError::WrongNumberOfComponents {
536                expected: e.expected,
537                got: e.got,
538                input: e.input.as_str(),
539            },
540            Self::PixelValueParseError(e) => {
541                CssStyleTransformOriginParseError::PixelValueParseError(e.to_shared())
542            }
543        }
544    }
545}
546
547#[derive(Clone, PartialEq, Eq)]
548pub enum CssStylePerspectiveOriginParseError<'a> {
549    WrongNumberOfComponents {
550        expected: usize,
551        got: usize,
552        input: &'a str,
553    },
554    PixelValueParseError(CssPixelValueParseError<'a>),
555}
556
557impl_debug_as_display!(CssStylePerspectiveOriginParseError<'a>);
558impl_display! { CssStylePerspectiveOriginParseError<'a>, {
559    WrongNumberOfComponents { expected, got, input } => format!("Invalid number of components: expected {}, got {}: \"{}\"", expected, got, input),
560    PixelValueParseError(e) => format!("Invalid pixel value: {}", e),
561}}
562impl_from! { CssPixelValueParseError<'a>, CssStylePerspectiveOriginParseError::PixelValueParseError }
563
564#[derive(Debug, Clone, PartialEq, Eq)]
565#[repr(C, u8)]
566pub enum CssStylePerspectiveOriginParseErrorOwned {
567    WrongNumberOfComponents(WrongComponentCountError),
568    PixelValueParseError(CssPixelValueParseErrorOwned),
569}
570
571impl CssStylePerspectiveOriginParseError<'_> {
572    #[must_use] pub fn to_contained(&self) -> CssStylePerspectiveOriginParseErrorOwned {
573        match self {
574            Self::WrongNumberOfComponents {
575                expected,
576                got,
577                input,
578            } => CssStylePerspectiveOriginParseErrorOwned::WrongNumberOfComponents(WrongComponentCountError {
579                expected: *expected,
580                got: *got,
581                input: (*input).to_string().into(),
582            }),
583            Self::PixelValueParseError(e) => {
584                CssStylePerspectiveOriginParseErrorOwned::PixelValueParseError(e.to_contained())
585            }
586        }
587    }
588}
589
590impl CssStylePerspectiveOriginParseErrorOwned {
591    #[must_use] pub fn to_shared(&self) -> CssStylePerspectiveOriginParseError<'_> {
592        match self {
593            Self::WrongNumberOfComponents(e) => CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
594                expected: e.expected,
595                got: e.got,
596                input: e.input.as_str(),
597            },
598            Self::PixelValueParseError(e) => {
599                CssStylePerspectiveOriginParseError::PixelValueParseError(e.to_shared())
600            }
601        }
602    }
603}
604
605#[derive(Clone, PartialEq, Eq)]
606pub enum CssBackfaceVisibilityParseError<'a> {
607    InvalidValue(&'a str),
608}
609
610impl_debug_as_display!(CssBackfaceVisibilityParseError<'a>);
611impl_display! { CssBackfaceVisibilityParseError<'a>, {
612    InvalidValue(s) => format!("Invalid value for backface-visibility: \"{}\", expected \"visible\" or \"hidden\"", s),
613}}
614
615#[derive(Debug, Clone, PartialEq, Eq)]
616#[repr(C, u8)]
617pub enum CssBackfaceVisibilityParseErrorOwned {
618    InvalidValue(AzString),
619}
620
621impl CssBackfaceVisibilityParseError<'_> {
622    #[must_use] pub fn to_contained(&self) -> CssBackfaceVisibilityParseErrorOwned {
623        match self {
624            Self::InvalidValue(s) => {
625                CssBackfaceVisibilityParseErrorOwned::InvalidValue((*s).to_string().into())
626            }
627        }
628    }
629}
630
631impl CssBackfaceVisibilityParseErrorOwned {
632    #[must_use] pub fn to_shared(&self) -> CssBackfaceVisibilityParseError<'_> {
633        match self {
634            Self::InvalidValue(s) => CssBackfaceVisibilityParseError::InvalidValue(s),
635        }
636    }
637}
638
639// -- Parsers --
640
641#[cfg(feature = "parser")]
642/// # Errors
643///
644/// Returns an error if `input` is not a valid CSS `transform-vec` value.
645pub fn parse_style_transform_vec(
646    input: &str,
647) -> Result<StyleTransformVec, CssStyleTransformParseError<'_>> {
648    crate::props::basic::parse::split_string_respect_whitespace(input)
649        .iter()
650        .map(|i| parse_style_transform(i))
651        .collect::<Result<Vec<_>, _>>()
652        .map(Into::into)
653}
654
655#[cfg(feature = "parser")]
656#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose CSS parser/formatter/dispatch table (one branch per property/variant)
657/// # Errors
658///
659/// Returns an error if `input` is not a valid CSS `transform` value.
660pub fn parse_style_transform(
661    input: &str,
662) -> Result<StyleTransform, CssStyleTransformParseError<'_>> {
663    fn get_numbers(
664        input: &str,
665        expected: usize,
666    ) -> Result<Vec<f32>, CssStyleTransformParseError<'_>> {
667        let numbers: Vec<_> = input
668            .split(',')
669            .map(|s| s.trim().parse::<f32>())
670            .collect::<Result<_, _>>()?;
671        if numbers.len() == expected {
672            Ok(numbers)
673        } else {
674            Err(CssStyleTransformParseError::WrongNumberOfComponents {
675                expected,
676                got: numbers.len(),
677                input,
678            })
679        }
680    }
681
682    let (transform_type, transform_values) = parse_parentheses(
683        input,
684        &[
685            "matrix",
686            "matrix3d",
687            "translate",
688            "translate3d",
689            "translateX",
690            "translateY",
691            "translateZ",
692            "rotate",
693            "rotate3d",
694            "rotateX",
695            "rotateY",
696            "rotateZ",
697            "scale",
698            "scale3d",
699            "scaleX",
700            "scaleY",
701            "scaleZ",
702            "skew",
703            "skewX",
704            "skewY",
705            "perspective",
706        ],
707    )?;
708
709    match transform_type {
710        "matrix" => {
711            let nums = get_numbers(transform_values, 6)?;
712            Ok(StyleTransform::Matrix(StyleTransformMatrix2D {
713                a: FloatValue::new(nums[0]),
714                b: FloatValue::new(nums[1]),
715                c: FloatValue::new(nums[2]),
716                d: FloatValue::new(nums[3]),
717                tx: FloatValue::new(nums[4]),
718                ty: FloatValue::new(nums[5]),
719            }))
720        }
721        "matrix3d" => {
722            let nums = get_numbers(transform_values, 16)?;
723            Ok(StyleTransform::Matrix3D(StyleTransformMatrix3D {
724                m11: FloatValue::new(nums[0]),
725                m12: FloatValue::new(nums[1]),
726                m13: FloatValue::new(nums[2]),
727                m14: FloatValue::new(nums[3]),
728                m21: FloatValue::new(nums[4]),
729                m22: FloatValue::new(nums[5]),
730                m23: FloatValue::new(nums[6]),
731                m24: FloatValue::new(nums[7]),
732                m31: FloatValue::new(nums[8]),
733                m32: FloatValue::new(nums[9]),
734                m33: FloatValue::new(nums[10]),
735                m34: FloatValue::new(nums[11]),
736                m41: FloatValue::new(nums[12]),
737                m42: FloatValue::new(nums[13]),
738                m43: FloatValue::new(nums[14]),
739                m44: FloatValue::new(nums[15]),
740            }))
741        }
742        "translate" => {
743            let components: Vec<_> = transform_values.split(',').collect();
744
745            // translate() takes exactly 1 or 2 parameters (x, or x and y)
746            if components.len() > 2 {
747                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
748                    expected: 2,
749                    got: components.len(),
750                    input: transform_values,
751                });
752            }
753
754            let x = parse_pixel_value(
755                components.first()
756                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
757                        expected: 2,
758                        got: 0,
759                        input: transform_values,
760                    })?
761                    .trim(),
762            )?;
763            let y = match components.get(1) {
764                Some(c) => parse_pixel_value(c.trim())?,
765                None => PixelValue::px(0.0),
766            };
767            Ok(StyleTransform::Translate(StyleTransformTranslate2D {
768                x,
769                y,
770            }))
771        }
772        "translate3d" => {
773            let components: Vec<_> = transform_values.split(',').collect();
774            let x = parse_pixel_value(
775                components.first()
776                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
777                        expected: 3,
778                        got: 0,
779                        input: transform_values,
780                    })?
781                    .trim(),
782            )?;
783            let y = parse_pixel_value(
784                components
785                    .get(1)
786                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
787                        expected: 3,
788                        got: 1,
789                        input: transform_values,
790                    })?
791                    .trim(),
792            )?;
793            let z = parse_pixel_value(
794                components
795                    .get(2)
796                    .ok_or(CssStyleTransformParseError::WrongNumberOfComponents {
797                        expected: 3,
798                        got: 2,
799                        input: transform_values,
800                    })?
801                    .trim(),
802            )?;
803            Ok(StyleTransform::Translate3D(StyleTransformTranslate3D {
804                x,
805                y,
806                z,
807            }))
808        }
809        "translateX" => Ok(StyleTransform::TranslateX(parse_pixel_value(
810            transform_values,
811        )?)),
812        "translateY" => Ok(StyleTransform::TranslateY(parse_pixel_value(
813            transform_values,
814        )?)),
815        "translateZ" => Ok(StyleTransform::TranslateZ(parse_pixel_value(
816            transform_values,
817        )?)),
818        "rotate" => Ok(StyleTransform::Rotate(parse_angle_value(transform_values)?)),
819        "rotate3d" => {
820            let parts: Vec<_> = transform_values.splitn(4, ',').collect();
821            if parts.len() != 4 {
822                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
823                    expected: 4,
824                    got: parts.len(),
825                    input: transform_values,
826                });
827            }
828            let x = parts[0].trim().parse::<f32>()?;
829            let y = parts[1].trim().parse::<f32>()?;
830            let z = parts[2].trim().parse::<f32>()?;
831            let angle = parse_angle_value(parts[3].trim())?;
832            Ok(StyleTransform::Rotate3D(StyleTransformRotate3D {
833                x: FloatValue::new(x),
834                y: FloatValue::new(y),
835                z: FloatValue::new(z),
836                angle,
837            }))
838        }
839        "rotateX" => Ok(StyleTransform::RotateX(parse_angle_value(
840            transform_values,
841        )?)),
842        "rotateY" => Ok(StyleTransform::RotateY(parse_angle_value(
843            transform_values,
844        )?)),
845        "rotateZ" => Ok(StyleTransform::RotateZ(parse_angle_value(
846            transform_values,
847        )?)),
848        "scale" => {
849            let parts: Vec<_> = transform_values.split(',').collect();
850            if parts.is_empty() || parts.len() > 2 {
851                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
852                    expected: 2,
853                    got: parts.len(),
854                    input: transform_values,
855                });
856            }
857            let x = parts[0].trim().parse::<f32>()?;
858            let y = if parts.len() == 2 {
859                parts[1].trim().parse::<f32>()?
860            } else {
861                x
862            };
863            Ok(StyleTransform::Scale(StyleTransformScale2D {
864                x: FloatValue::new(x),
865                y: FloatValue::new(y),
866            }))
867        }
868        "scale3d" => {
869            let nums = get_numbers(transform_values, 3)?;
870            Ok(StyleTransform::Scale3D(StyleTransformScale3D {
871                x: FloatValue::new(nums[0]),
872                y: FloatValue::new(nums[1]),
873                z: FloatValue::new(nums[2]),
874            }))
875        }
876        "scaleX" => Ok(StyleTransform::ScaleX(PercentageValue::new(
877            transform_values.trim().parse::<f32>()? * 100.0,
878        ))),
879        "scaleY" => Ok(StyleTransform::ScaleY(PercentageValue::new(
880            transform_values.trim().parse::<f32>()? * 100.0,
881        ))),
882        "scaleZ" => Ok(StyleTransform::ScaleZ(PercentageValue::new(
883            transform_values.trim().parse::<f32>()? * 100.0,
884        ))),
885        "skew" => {
886            let components: Vec<_> = transform_values.split(',').collect();
887            if components.is_empty() || components.len() > 2 {
888                return Err(CssStyleTransformParseError::WrongNumberOfComponents {
889                    expected: 2,
890                    got: components.len(),
891                    input: transform_values,
892                });
893            }
894            let x = parse_angle_value(components[0].trim())?;
895            let y = match components.get(1) {
896                Some(c) => parse_angle_value(c.trim())?,
897                None => AngleValue::deg(0.0),
898            };
899            Ok(StyleTransform::Skew(StyleTransformSkew2D { x, y }))
900        }
901        "skewX" => Ok(StyleTransform::SkewX(parse_angle_value(transform_values)?)),
902        "skewY" => Ok(StyleTransform::SkewY(parse_angle_value(transform_values)?)),
903        "perspective" => Ok(StyleTransform::Perspective(parse_pixel_value(
904            transform_values,
905        )?)),
906        _ => unreachable!(),
907    }
908}
909
910#[cfg(feature = "parser")]
911/// # Errors
912///
913/// Returns an error if `input` is not a valid CSS `transform-origin` value.
914pub fn parse_style_transform_origin(
915    input: &str,
916) -> Result<StyleTransformOrigin, CssStyleTransformOriginParseError<'_>> {
917    // Helper to parse position keywords or pixel values
918    fn parse_position_component(
919        s: &str,
920        is_horizontal: bool,
921    ) -> Result<PixelValue, CssPixelValueParseError<'_>> {
922        match s.trim() {
923            "left" if is_horizontal => Ok(PixelValue::percent(0.0)),
924            "center" => Ok(PixelValue::percent(50.0)),
925            "right" if is_horizontal => Ok(PixelValue::percent(100.0)),
926            "top" if !is_horizontal => Ok(PixelValue::percent(0.0)),
927            "bottom" if !is_horizontal => Ok(PixelValue::percent(100.0)),
928            _ => parse_pixel_value(s),
929        }
930    }
931
932    let components: Vec<_> = input.split_whitespace().collect();
933    if components.len() != 2 {
934        return Err(CssStyleTransformOriginParseError::WrongNumberOfComponents {
935            expected: 2,
936            got: components.len(),
937            input,
938        });
939    }
940
941    let x = parse_position_component(components[0], true)?;
942    let y = parse_position_component(components[1], false)?;
943    Ok(StyleTransformOrigin { x, y })
944}
945
946#[cfg(feature = "parser")]
947/// # Errors
948///
949/// Returns an error if `input` is not a valid CSS `perspective-origin` value.
950pub fn parse_style_perspective_origin(
951    input: &str,
952) -> Result<StylePerspectiveOrigin, CssStylePerspectiveOriginParseError<'_>> {
953    let components: Vec<_> = input.split_whitespace().collect();
954    if components.len() != 2 {
955        return Err(
956            CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
957                expected: 2,
958                got: components.len(),
959                input,
960            },
961        );
962    }
963    let x = parse_pixel_value(components[0])?;
964    let y = parse_pixel_value(components[1])?;
965    Ok(StylePerspectiveOrigin { x, y })
966}
967
968#[cfg(feature = "parser")]
969/// # Errors
970///
971/// Returns an error if `input` is not a valid CSS `backface-visibility` value.
972pub fn parse_style_backface_visibility(
973    input: &str,
974) -> Result<StyleBackfaceVisibility, CssBackfaceVisibilityParseError<'_>> {
975    match input.trim() {
976        "visible" => Ok(StyleBackfaceVisibility::Visible),
977        "hidden" => Ok(StyleBackfaceVisibility::Hidden),
978        _ => Err(CssBackfaceVisibilityParseError::InvalidValue(input)),
979    }
980}
981
982#[cfg(all(test, feature = "parser"))]
983mod tests {
984    // Tests assert that parsed values equal the exact source literals.
985    #![allow(clippy::float_cmp)]
986    use super::*;
987
988    #[test]
989    fn test_parse_transform_vec() {
990        let result =
991            parse_style_transform_vec("translateX(10px) rotate(90deg) scale(0.5, 0.5)").unwrap();
992        assert_eq!(result.len(), 3);
993        assert!(matches!(
994            result.as_slice()[0],
995            StyleTransform::TranslateX(_)
996        ));
997        assert!(matches!(result.as_slice()[1], StyleTransform::Rotate(_)));
998        assert!(matches!(result.as_slice()[2], StyleTransform::Scale(_)));
999    }
1000
1001    #[test]
1002    fn test_parse_transform_functions() {
1003        // Translate
1004        assert_eq!(
1005            parse_style_transform("translateX(50%)").unwrap(),
1006            StyleTransform::TranslateX(PixelValue::percent(50.0))
1007        );
1008        let translate = parse_style_transform("translate(10px, -20px)").unwrap();
1009        if let StyleTransform::Translate(t) = translate {
1010            assert_eq!(t.x, PixelValue::px(10.0));
1011            assert_eq!(t.y, PixelValue::px(-20.0));
1012        } else {
1013            panic!("Expected Translate");
1014        }
1015
1016        // Scale
1017        assert_eq!(
1018            parse_style_transform("scaleY(1.2)").unwrap(),
1019            StyleTransform::ScaleY(PercentageValue::new(120.0))
1020        );
1021        let scale = parse_style_transform("scale(2, 0.5)").unwrap();
1022        if let StyleTransform::Scale(s) = scale {
1023            assert_eq!(s.x.get(), 2.0);
1024            assert_eq!(s.y.get(), 0.5);
1025        } else {
1026            panic!("Expected Scale");
1027        }
1028
1029        // Rotate
1030        assert_eq!(
1031            parse_style_transform("rotate(0.25turn)").unwrap(),
1032            StyleTransform::Rotate(AngleValue::turn(0.25))
1033        );
1034
1035        // Skew
1036        assert_eq!(
1037            parse_style_transform("skewX(-10deg)").unwrap(),
1038            StyleTransform::SkewX(AngleValue::deg(-10.0))
1039        );
1040        let skew = parse_style_transform("skew(20deg, 30deg)").unwrap();
1041        if let StyleTransform::Skew(s) = skew {
1042            assert_eq!(s.x, AngleValue::deg(20.0));
1043            assert_eq!(s.y, AngleValue::deg(30.0));
1044        } else {
1045            panic!("Expected Skew");
1046        }
1047    }
1048
1049    #[test]
1050    fn test_parse_transform_origin() {
1051        let result = parse_style_transform_origin("50% 50%").unwrap();
1052        assert_eq!(result.x, PixelValue::percent(50.0));
1053        assert_eq!(result.y, PixelValue::percent(50.0));
1054
1055        let result = parse_style_transform_origin("left top").unwrap();
1056        assert_eq!(result.x, PixelValue::percent(0.0));
1057        assert_eq!(result.y, PixelValue::percent(0.0));
1058
1059        let result = parse_style_transform_origin("20px bottom").unwrap();
1060        assert_eq!(result.x, PixelValue::px(20.0));
1061        assert_eq!(result.y, PixelValue::percent(100.0));
1062    }
1063
1064    #[test]
1065    fn test_parse_backface_visibility() {
1066        assert_eq!(
1067            parse_style_backface_visibility("visible").unwrap(),
1068            StyleBackfaceVisibility::Visible
1069        );
1070        assert_eq!(
1071            parse_style_backface_visibility("hidden").unwrap(),
1072            StyleBackfaceVisibility::Hidden
1073        );
1074        assert!(parse_style_backface_visibility("none").is_err());
1075    }
1076
1077    #[test]
1078    fn test_parse_transform_errors() {
1079        // Wrong function name
1080        assert!(parse_style_transform("translatex(10px)").is_err());
1081        // Wrong number of args
1082        assert!(parse_style_transform("translate(1, 2, 3)").is_err());
1083        // Single-arg forms (CSS spec compliant)
1084        let scale1 = parse_style_transform("scale(2)").unwrap();
1085        if let StyleTransform::Scale(s) = scale1 {
1086            assert_eq!(s.x.get(), 2.0);
1087            assert_eq!(s.y.get(), 2.0);
1088        } else {
1089            panic!("Expected Scale");
1090        }
1091        let translate1 = parse_style_transform("translate(10px)").unwrap();
1092        if let StyleTransform::Translate(t) = translate1 {
1093            assert_eq!(t.x, PixelValue::px(10.0));
1094            assert_eq!(t.y, PixelValue::px(0.0));
1095        } else {
1096            panic!("Expected Translate");
1097        }
1098        let skew1 = parse_style_transform("skew(20deg)").unwrap();
1099        if let StyleTransform::Skew(s) = skew1 {
1100            assert_eq!(s.x, AngleValue::deg(20.0));
1101            assert_eq!(s.y, AngleValue::deg(0.0));
1102        } else {
1103            panic!("Expected Skew");
1104        }
1105        // rotate3d with angle unit
1106        let rot3d = parse_style_transform("rotate3d(1, 0, 0, 45deg)").unwrap();
1107        if let StyleTransform::Rotate3D(r) = rot3d {
1108            assert_eq!(r.x.get(), 1.0);
1109            assert_eq!(r.angle, AngleValue::deg(45.0));
1110        } else {
1111            panic!("Expected Rotate3D");
1112        }
1113        // Invalid value
1114        assert!(parse_style_transform("rotate(10px)").is_err());
1115        assert!(parse_style_transform("translateX(auto)").is_err());
1116    }
1117}
1118
1119#[cfg(all(test, feature = "parser"))]
1120#[allow(clippy::too_many_lines, clippy::float_cmp)]
1121mod autotest_generated {
1122    // Tests compare parsed values against exact source literals, and deliberately
1123    // feed NaN/inf through the numeric encoders.
1124
1125    use super::*;
1126    use crate::props::basic::length::SizeMetric;
1127
1128    // ---------------------------------------------------------------------
1129    // helpers
1130    // ---------------------------------------------------------------------
1131
1132    /// Every `FloatValue` is stored as an `isize`, so `get()` can never be
1133    /// NaN/inf no matter what went in. Used as a blanket invariant below.
1134    fn assert_encodable(pv: PixelValue) {
1135        assert!(pv.number.get().is_finite());
1136    }
1137
1138    fn all_roundtrippable_transforms() -> Vec<StyleTransform> {
1139        vec![
1140            StyleTransform::Matrix(StyleTransformMatrix2D::default()),
1141            StyleTransform::Matrix3D(StyleTransformMatrix3D::default()),
1142            StyleTransform::Translate(StyleTransformTranslate2D {
1143                x: PixelValue::px(10.0),
1144                y: PixelValue::px(-20.0),
1145            }),
1146            StyleTransform::Translate3D(StyleTransformTranslate3D {
1147                x: PixelValue::px(1.0),
1148                y: PixelValue::em(2.0),
1149                z: PixelValue::pt(-3.5),
1150            }),
1151            StyleTransform::TranslateX(PixelValue::percent(50.0)),
1152            StyleTransform::TranslateY(PixelValue::px(0.0)),
1153            StyleTransform::TranslateZ(PixelValue::rem(1.25)),
1154            StyleTransform::Rotate(AngleValue::deg(90.0)),
1155            StyleTransform::Rotate3D(StyleTransformRotate3D {
1156                x: FloatValue::new(1.0),
1157                y: FloatValue::new(0.0),
1158                z: FloatValue::new(0.0),
1159                angle: AngleValue::turn(0.25),
1160            }),
1161            StyleTransform::RotateX(AngleValue::rad(1.5)),
1162            StyleTransform::RotateY(AngleValue::grad(100.0)),
1163            StyleTransform::RotateZ(AngleValue::deg(-45.0)),
1164            StyleTransform::Scale(StyleTransformScale2D {
1165                x: FloatValue::new(2.0),
1166                y: FloatValue::new(0.5),
1167            }),
1168            StyleTransform::Scale3D(StyleTransformScale3D {
1169                x: FloatValue::new(1.0),
1170                y: FloatValue::new(-1.0),
1171                z: FloatValue::new(0.25),
1172            }),
1173            StyleTransform::Skew(StyleTransformSkew2D {
1174                x: AngleValue::deg(20.0),
1175                y: AngleValue::deg(30.0),
1176            }),
1177            StyleTransform::SkewX(AngleValue::deg(-10.0)),
1178            StyleTransform::SkewY(AngleValue::deg(10.0)),
1179            StyleTransform::Perspective(PixelValue::px(500.0)),
1180        ]
1181    }
1182
1183    // =====================================================================
1184    // parse_style_transform  --  malformed / boundary / unicode
1185    // =====================================================================
1186
1187    #[test]
1188    fn transform_rejects_empty_and_whitespace_only_input() {
1189        for input in ["", "   ", "\t\n", "\r\n\t "] {
1190            let err = parse_style_transform(input).unwrap_err();
1191            assert!(
1192                matches!(
1193                    err,
1194                    CssStyleTransformParseError::InvalidParenthesis(
1195                        ParenthesisParseError::EmptyInput
1196                    )
1197                ),
1198                "expected EmptyInput for {input:?}, got {err}"
1199            );
1200        }
1201    }
1202
1203    #[test]
1204    fn transform_rejects_garbage_without_panicking() {
1205        // No opening brace at all.
1206        assert!(matches!(
1207            parse_style_transform("garbage").unwrap_err(),
1208            CssStyleTransformParseError::InvalidParenthesis(
1209                ParenthesisParseError::NoOpeningBraceFound
1210            )
1211        ));
1212        // Opening brace, no closing brace.
1213        assert!(matches!(
1214            parse_style_transform("rotate(90deg").unwrap_err(),
1215            CssStyleTransformParseError::InvalidParenthesis(
1216                ParenthesisParseError::NoClosingBraceFound
1217            )
1218        ));
1219        // Known-but-miscased function name is NOT accepted (CSS is case-insensitive
1220        // for function names; azul's stopword table is case-sensitive).
1221        assert!(matches!(
1222            parse_style_transform("translatex(10px)").unwrap_err(),
1223            CssStyleTransformParseError::InvalidParenthesis(
1224                ParenthesisParseError::StopWordNotFound("translatex")
1225            )
1226        ));
1227        assert!(matches!(
1228            parse_style_transform("ROTATE(90deg)").unwrap_err(),
1229            CssStyleTransformParseError::InvalidParenthesis(
1230                ParenthesisParseError::StopWordNotFound("ROTATE")
1231            )
1232        ));
1233        // Random byte soup, none of which forms a grammar.
1234        for input in [
1235            "((((",
1236            ")",
1237            "()",
1238            "(rotate)",
1239            "rotate",
1240            ";;;",
1241            "\0(\0)",
1242            "-1",
1243            "rotate(,,,,)",
1244            "matrix(,)",
1245        ] {
1246            assert!(
1247                parse_style_transform(input).is_err(),
1248                "expected Err for {input:?}"
1249            );
1250        }
1251    }
1252
1253    #[test]
1254    fn transform_never_hits_the_unreachable_arm_for_near_miss_stopwords() {
1255        // parse_style_transform ends in `_ => unreachable!()`; it is only sound as
1256        // long as parse_parentheses can never hand back a non-listed stopword.
1257        // Probe names that are prefixes/suffixes/case-variants of real ones.
1258        for name in [
1259            "translat", "translateXX", "xtranslateX", "rotate3", "rotate3D", "scale4d", "skewZ",
1260            "perspectives", "matrix2d", "MATRIX", "", " rotate",
1261        ] {
1262            let input = alloc::format!("{name}(1)");
1263            let res = parse_style_transform(&input);
1264            // Either a clean parse error or (for " rotate", which trims to "rotate")
1265            // a normal result - but never a panic.
1266            let _ = res;
1267        }
1268        // " rotate(1)" trims down to a valid rotate with a bare-number degree.
1269        assert_eq!(
1270            parse_style_transform("  rotate(1)  ").unwrap(),
1271            StyleTransform::Rotate(AngleValue::deg(1.0))
1272        );
1273    }
1274
1275    #[test]
1276    fn transform_handles_non_ascii_and_multibyte_input() {
1277        // parse_parentheses slices `input[..first_open_brace]`; a multibyte char
1278        // right before the '(' must not split a UTF-8 boundary.
1279        for input in [
1280            "\u{1F600}",
1281            "\u{1F600}(1)",
1282            "rotate(\u{1F600})",
1283            "rotate\u{0301}(1deg)",
1284            "\u{1F600}rotate(1deg)",
1285            "translateX(\u{1F600}px)",
1286            "translate(\u{1F600}, \u{1F600})",
1287            "matrix3d(\u{4F60}\u{597D})",
1288            "sk\u{0435}wX(10deg)", // cyrillic 'е' homoglyph
1289        ] {
1290            assert!(
1291                parse_style_transform(input).is_err(),
1292                "expected Err for {input:?}"
1293            );
1294        }
1295        assert!(matches!(
1296            parse_style_transform("rotate(\u{1F600})").unwrap_err(),
1297            CssStyleTransformParseError::AngleValueParseError(
1298                CssAngleValueParseError::InvalidAngle("\u{1F600}")
1299            )
1300        ));
1301    }
1302
1303    #[test]
1304    fn transform_boundary_numbers_saturate_instead_of_panicking() {
1305        // -0 collapses to +0 in the isize-backed encoding.
1306        assert_eq!(
1307            parse_style_transform("translateX(-0)").unwrap(),
1308            StyleTransform::TranslateX(PixelValue::px(0.0))
1309        );
1310        assert_eq!(
1311            parse_style_transform("translateX(0)").unwrap(),
1312            StyleTransform::TranslateX(PixelValue::px(0.0))
1313        );
1314
1315        // NaN parses as a float (Rust accepts "NaN"), and the f32 -> isize cast
1316        // maps NaN to 0. So `translateX(NaN)` silently becomes `0px`.
1317        let StyleTransform::TranslateX(nan_px) = parse_style_transform("translateX(NaN)").unwrap()
1318        else {
1319            panic!("expected TranslateX");
1320        };
1321        assert_eq!(nan_px.number.number(), 0);
1322        assert_encodable(nan_px);
1323
1324        // Infinities (literal, and via decimal overflow) saturate to isize::MAX/MIN.
1325        for input in ["translateX(inf)", "translateX(1e400)", "translateX(1e400px)"] {
1326            let StyleTransform::TranslateX(px) = parse_style_transform(input).unwrap() else {
1327                panic!("expected TranslateX for {input}");
1328            };
1329            assert_eq!(px.number.number(), isize::MAX, "{input}");
1330            assert_encodable(px);
1331        }
1332        let StyleTransform::TranslateX(neg) = parse_style_transform("translateX(-inf)").unwrap()
1333        else {
1334            panic!("expected TranslateX");
1335        };
1336        assert_eq!(neg.number.number(), isize::MIN);
1337        assert_encodable(neg);
1338
1339        // i64::MAX / f64-scale magnitudes: fine, just saturated.
1340        for input in [
1341            "translateX(9223372036854775807px)",
1342            "translateX(-9223372036854775808px)",
1343            "translateX(1e-400px)",
1344            "translateX(0.0000000000001px)",
1345        ] {
1346            assert!(parse_style_transform(input).is_ok(), "{input}");
1347        }
1348
1349        // Angles take the same path: NaN -> 0deg, inf -> saturated.
1350        assert_eq!(
1351            parse_style_transform("rotate(NaN)").unwrap(),
1352            StyleTransform::Rotate(AngleValue::deg(0.0))
1353        );
1354        let StyleTransform::Rotate(a) = parse_style_transform("rotate(infdeg)").unwrap() else {
1355            panic!("expected Rotate");
1356        };
1357        assert_eq!(a.number.number(), isize::MAX);
1358
1359        // scaleX multiplies by 100 before encoding - inf * 100 must not trap.
1360        let StyleTransform::ScaleX(p) = parse_style_transform("scaleX(NaN)").unwrap() else {
1361            panic!("expected ScaleX");
1362        };
1363        assert_eq!(p, PercentageValue::new(0.0));
1364        assert!(parse_style_transform("scaleX(inf)").is_ok());
1365        assert!(parse_style_transform("scaleX(1e40)").is_ok());
1366    }
1367
1368    #[test]
1369    fn transform_component_counts_are_enforced() {
1370        // matrix wants exactly 6.
1371        assert!(matches!(
1372            parse_style_transform("matrix(1,2,3,4,5)").unwrap_err(),
1373            CssStyleTransformParseError::WrongNumberOfComponents {
1374                expected: 6,
1375                got: 5,
1376                ..
1377            }
1378        ));
1379        assert!(matches!(
1380            parse_style_transform("matrix(1,2,3,4,5,6,7)").unwrap_err(),
1381            CssStyleTransformParseError::WrongNumberOfComponents {
1382                expected: 6,
1383                got: 7,
1384                ..
1385            }
1386        ));
1387        // matrix3d wants exactly 16.
1388        assert!(matches!(
1389            parse_style_transform("matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0)").unwrap_err(),
1390            CssStyleTransformParseError::WrongNumberOfComponents {
1391                expected: 16,
1392                got: 15,
1393                ..
1394            }
1395        ));
1396        // translate takes at most 2.
1397        assert!(matches!(
1398            parse_style_transform("translate(1px, 2px, 3px)").unwrap_err(),
1399            CssStyleTransformParseError::WrongNumberOfComponents {
1400                expected: 2,
1401                got: 3,
1402                ..
1403            }
1404        ));
1405        // scale takes at most 2.
1406        assert!(matches!(
1407            parse_style_transform("scale(1, 2, 3)").unwrap_err(),
1408            CssStyleTransformParseError::WrongNumberOfComponents {
1409                expected: 2,
1410                got: 3,
1411                ..
1412            }
1413        ));
1414        // skew takes at most 2.
1415        assert!(matches!(
1416            parse_style_transform("skew(1deg, 2deg, 3deg)").unwrap_err(),
1417            CssStyleTransformParseError::WrongNumberOfComponents {
1418                expected: 2,
1419                got: 3,
1420                ..
1421            }
1422        ));
1423        // rotate3d wants exactly 4 (splitn(4) makes >4 fold into the angle, which
1424        // then fails to parse as an angle).
1425        assert!(matches!(
1426            parse_style_transform("rotate3d(1, 0, 0)").unwrap_err(),
1427            CssStyleTransformParseError::WrongNumberOfComponents {
1428                expected: 4,
1429                got: 3,
1430                ..
1431            }
1432        ));
1433        assert!(parse_style_transform("rotate3d(1, 0, 0, 45deg, 99)").is_err());
1434        // translate3d wants exactly 3 when short...
1435        assert!(matches!(
1436            parse_style_transform("translate3d(1px, 2px)").unwrap_err(),
1437            CssStyleTransformParseError::WrongNumberOfComponents {
1438                expected: 3,
1439                got: 2,
1440                ..
1441            }
1442        ));
1443        // scale3d wants exactly 3.
1444        assert!(matches!(
1445            parse_style_transform("scale3d(1, 2)").unwrap_err(),
1446            CssStyleTransformParseError::WrongNumberOfComponents {
1447                expected: 3,
1448                got: 2,
1449                ..
1450            }
1451        ));
1452    }
1453
1454    #[test]
1455    fn transform_translate3d_silently_ignores_extra_components() {
1456        // BUG (leniency): unlike matrix/scale3d (which go through get_numbers and
1457        // check the count), translate3d only indexes [0], [1], [2] and never
1458        // rejects a 4th+ component. Per CSS this must be a parse error.
1459        // Pinned as current behaviour so a future fix shows up as a diff here.
1460        let parsed = parse_style_transform("translate3d(1px, 2px, 3px, 4px, 5px)").unwrap();
1461        assert_eq!(
1462            parsed,
1463            StyleTransform::Translate3D(StyleTransformTranslate3D {
1464                x: PixelValue::px(1.0),
1465                y: PixelValue::px(2.0),
1466                z: PixelValue::px(3.0),
1467            })
1468        );
1469    }
1470
1471    #[test]
1472    fn transform_ignores_junk_after_the_closing_paren() {
1473        // BUG (leniency): parse_parentheses uses find('(') .. rfind(')'), so any
1474        // trailing junk that contains no ')' is silently dropped. Per CSS,
1475        // "rotate(90deg)garbage" is invalid. Pinned as current behaviour.
1476        assert_eq!(
1477            parse_style_transform("rotate(90deg)garbage").unwrap(),
1478            StyleTransform::Rotate(AngleValue::deg(90.0))
1479        );
1480        assert_eq!(
1481            parse_style_transform("rotate(90deg) ;drop table").unwrap(),
1482            StyleTransform::Rotate(AngleValue::deg(90.0))
1483        );
1484        // ...but junk containing a ')' gets swallowed INTO the argument, which then
1485        // fails - so the leniency is content-dependent, not a clean "trim" rule.
1486        assert!(parse_style_transform("rotate(90deg))").is_err());
1487        assert!(parse_style_transform("rotate(90deg) rotate(1deg)").is_err());
1488    }
1489
1490    #[test]
1491    fn transform_empty_argument_lists_are_rejected() {
1492        for input in [
1493            "matrix()",
1494            "matrix3d()",
1495            "translate()",
1496            "translate3d()",
1497            "translateX()",
1498            "translateY()",
1499            "translateZ()",
1500            "rotate()",
1501            "rotate3d()",
1502            "rotateX()",
1503            "rotateY()",
1504            "rotateZ()",
1505            "scale()",
1506            "scale3d()",
1507            "scaleX()",
1508            "scaleY()",
1509            "scaleZ()",
1510            "skew()",
1511            "skewX()",
1512            "skewY()",
1513            "perspective()",
1514        ] {
1515            assert!(
1516                parse_style_transform(input).is_err(),
1517                "expected Err for {input:?}"
1518            );
1519        }
1520        // Trailing-comma forms, too.
1521        for input in [
1522            "translate(10px,)",
1523            "translate3d(1px,2px,)",
1524            "scale(2,)",
1525            "skew(10deg,)",
1526            "matrix(1,2,3,4,5,)",
1527        ] {
1528            assert!(
1529                parse_style_transform(input).is_err(),
1530                "expected Err for {input:?}"
1531            );
1532        }
1533    }
1534
1535    #[test]
1536    fn transform_wrong_unit_kinds_are_rejected() {
1537        // A length where an angle is expected, and vice versa.
1538        assert!(parse_style_transform("rotate(10px)").is_err());
1539        assert!(parse_style_transform("rotateX(10px)").is_err());
1540        assert!(parse_style_transform("skewY(10px)").is_err());
1541        assert!(parse_style_transform("translateX(10deg)").is_err());
1542        assert!(parse_style_transform("perspective(10deg)").is_err());
1543        // Keywords are not lengths.
1544        assert!(parse_style_transform("translateX(auto)").is_err());
1545        assert!(parse_style_transform("translateX(none)").is_err());
1546        // scaleX takes a bare number, NOT a percentage (see the round-trip test).
1547        assert!(parse_style_transform("scaleX(120%)").is_err());
1548    }
1549
1550    #[test]
1551    fn transform_extremely_long_input_does_not_hang_or_panic() {
1552        // ~100k-digit number: must scan in linear time and saturate, not panic.
1553        let huge = alloc::format!("translateX({}px)", "9".repeat(100_000));
1554        let StyleTransform::TranslateX(px) = parse_style_transform(&huge).unwrap() else {
1555            panic!("expected TranslateX");
1556        };
1557        assert_eq!(px.number.number(), isize::MAX);
1558
1559        // Long garbage of the same size must simply be an error.
1560        let junk = alloc::format!("translateX({})", "a".repeat(100_000));
1561        assert!(parse_style_transform(&junk).is_err());
1562
1563        // Long stopword: no quadratic blowup in the stopword scan.
1564        let long_name = alloc::format!("{}(1px)", "x".repeat(100_000));
1565        assert!(parse_style_transform(&long_name).is_err());
1566    }
1567
1568    #[test]
1569    fn transform_deeply_nested_parens_do_not_stack_overflow() {
1570        // parse_parentheses is iterative; prove there is no recursion by feeding it
1571        // 10k levels of nesting.
1572        let open_only = "rotate(".repeat(10_000);
1573        assert!(matches!(
1574            parse_style_transform(&open_only).unwrap_err(),
1575            CssStyleTransformParseError::InvalidParenthesis(
1576                ParenthesisParseError::NoClosingBraceFound
1577            )
1578        ));
1579
1580        let braces = "(".repeat(10_000);
1581        assert!(matches!(
1582            parse_style_transform(&braces).unwrap_err(),
1583            CssStyleTransformParseError::InvalidParenthesis(
1584                ParenthesisParseError::StopWordNotFound("")
1585            )
1586        ));
1587
1588        let balanced = alloc::format!(
1589            "translateX({}1px{})",
1590            "translateX(".repeat(1_000),
1591            ")".repeat(1_000)
1592        );
1593        assert!(parse_style_transform(&balanced).is_err());
1594
1595        let closers = ")".repeat(10_000);
1596        assert!(parse_style_transform(&closers).is_err());
1597    }
1598
1599    #[test]
1600    fn transform_valid_minimal_positive_control() {
1601        assert_eq!(
1602            parse_style_transform("rotate(0deg)").unwrap(),
1603            StyleTransform::Rotate(AngleValue::deg(0.0))
1604        );
1605    }
1606
1607    // =====================================================================
1608    // parse_style_transform_vec
1609    // =====================================================================
1610
1611    #[test]
1612    fn transform_vec_accepts_empty_and_whitespace_only_input_as_an_empty_list() {
1613        // NOTE: "" is NOT an error - split_string_respect_whitespace yields zero
1614        // tokens and the collect() succeeds with an empty Vec. Callers relying on
1615        // `parse_style_transform_vec("").is_err()` to reject an empty declaration
1616        // will not get one. Pinned as current behaviour.
1617        for input in ["", "   ", "\t\n", "\r \n \t"] {
1618            let v = parse_style_transform_vec(input).unwrap();
1619            assert_eq!(v.len(), 0, "{input:?}");
1620        }
1621    }
1622
1623    #[test]
1624    fn transform_vec_propagates_the_first_error() {
1625        assert!(parse_style_transform_vec("translateX(10px) garbage").is_err());
1626        assert!(parse_style_transform_vec("garbage translateX(10px)").is_err());
1627        assert!(parse_style_transform_vec("translateX(10px) rotate(10px)").is_err());
1628        assert!(parse_style_transform_vec("\u{1F600}").is_err());
1629    }
1630
1631    #[test]
1632    fn transform_vec_keeps_whitespace_inside_parens_together() {
1633        // "scale(2, 0.5)" contains a space at depth 1 - it must stay one token.
1634        let v = parse_style_transform_vec("scale(2, 0.5) matrix(1, 0, 0, 1, 0, 0)").unwrap();
1635        assert_eq!(v.len(), 2);
1636        assert!(matches!(v.as_slice()[0], StyleTransform::Scale(_)));
1637        assert!(matches!(v.as_slice()[1], StyleTransform::Matrix(_)));
1638
1639        // Repeated / redundant whitespace collapses.
1640        let v = parse_style_transform_vec("  translateX(1px)\t\trotate(2deg)\n ").unwrap();
1641        assert_eq!(v.len(), 2);
1642    }
1643
1644    #[test]
1645    fn transform_vec_extremely_long_list_does_not_hang() {
1646        let long = "translateX(1px) ".repeat(20_000);
1647        let v = parse_style_transform_vec(&long).unwrap();
1648        assert_eq!(v.len(), 20_000);
1649        for t in v.as_slice() {
1650            assert_eq!(*t, StyleTransform::TranslateX(PixelValue::px(1.0)));
1651        }
1652    }
1653
1654    #[test]
1655    fn transform_vec_unbalanced_parens_do_not_underflow_the_depth_counter() {
1656        // split_string_respect_whitespace does `depth -= 1` on every ')' with no
1657        // floor; a run of closers drives it negative. Must not panic in debug.
1658        let closers = ")".repeat(10_000);
1659        assert!(parse_style_transform_vec(&closers).is_err());
1660        let mixed = alloc::format!("{} {}", ")".repeat(5_000), "(".repeat(5_000));
1661        assert!(parse_style_transform_vec(&mixed).is_err());
1662    }
1663
1664    // =====================================================================
1665    // parse_style_transform_origin
1666    // =====================================================================
1667
1668    #[test]
1669    fn transform_origin_requires_exactly_two_components() {
1670        for (input, got) in [("", 0), ("50%", 1), ("left", 1), ("50% 50% 50%", 3)] {
1671            let err = parse_style_transform_origin(input).unwrap_err();
1672            assert!(
1673                matches!(
1674                    err,
1675                    CssStyleTransformOriginParseError::WrongNumberOfComponents {
1676                        expected: 2,
1677                        got: g,
1678                        ..
1679                    } if g == got
1680                ),
1681                "{input:?} -> {err}"
1682            );
1683        }
1684        // Whitespace-only collapses to zero components.
1685        assert!(matches!(
1686            parse_style_transform_origin("   \t\n ").unwrap_err(),
1687            CssStyleTransformOriginParseError::WrongNumberOfComponents { got: 0, .. }
1688        ));
1689    }
1690
1691    #[test]
1692    fn transform_origin_keywords_are_position_sensitive() {
1693        assert_eq!(
1694            parse_style_transform_origin("left top").unwrap(),
1695            StyleTransformOrigin {
1696                x: PixelValue::percent(0.0),
1697                y: PixelValue::percent(0.0),
1698            }
1699        );
1700        assert_eq!(
1701            parse_style_transform_origin("right bottom").unwrap(),
1702            StyleTransformOrigin {
1703                x: PixelValue::percent(100.0),
1704                y: PixelValue::percent(100.0),
1705            }
1706        );
1707        assert_eq!(
1708            parse_style_transform_origin("center center").unwrap(),
1709            StyleTransformOrigin::default()
1710        );
1711        // BUG (spec deviation): CSS allows the keywords in either order
1712        // ("top left" == "left top"). Here the horizontal slot rejects
1713        // "top"/"bottom" and the vertical slot rejects "left"/"right", so the
1714        // swapped form is an error. Pinned as current behaviour.
1715        assert!(parse_style_transform_origin("top left").is_err());
1716        assert!(parse_style_transform_origin("bottom right").is_err());
1717        assert!(parse_style_transform_origin("left left").is_err());
1718        assert!(parse_style_transform_origin("top top").is_err());
1719    }
1720
1721    #[test]
1722    fn transform_origin_garbage_and_unicode_do_not_panic() {
1723        for input in [
1724            "\u{1F600} \u{1F600}",
1725            "left \u{1F600}",
1726            "NaN NaN",
1727            "auto auto",
1728            "-- --",
1729            "10 20",   // bare numbers -> px, actually valid
1730            "1px;2px", // no whitespace -> 1 component
1731        ] {
1732            let _ = parse_style_transform_origin(input);
1733        }
1734        // Bare numbers fall through to parse_pixel_value's px default.
1735        assert_eq!(
1736            parse_style_transform_origin("10 20").unwrap(),
1737            StyleTransformOrigin {
1738                x: PixelValue::px(10.0),
1739                y: PixelValue::px(20.0),
1740            }
1741        );
1742        assert!(matches!(
1743            parse_style_transform_origin("\u{1F600} \u{1F600}").unwrap_err(),
1744            CssStyleTransformOriginParseError::PixelValueParseError(_)
1745        ));
1746    }
1747
1748    #[test]
1749    fn transform_origin_boundary_numbers_saturate() {
1750        let o = parse_style_transform_origin("inf% -inf%").unwrap();
1751        assert_eq!(o.x.number.number(), isize::MAX);
1752        assert_eq!(o.y.number.number(), isize::MIN);
1753        assert_encodable(o.x);
1754        assert_encodable(o.y);
1755
1756        // NaN -> 0, keeping the metric.
1757        let o = parse_style_transform_origin("NaNpx NaN%").unwrap();
1758        assert_eq!(o.x, PixelValue::px(0.0));
1759        assert_eq!(o.y, PixelValue::percent(0.0));
1760
1761        let o = parse_style_transform_origin("-0px 1e400px").unwrap();
1762        assert_eq!(o.x.number.number(), 0);
1763        assert_eq!(o.y.number.number(), isize::MAX);
1764    }
1765
1766    #[test]
1767    fn transform_origin_extremely_long_input_does_not_hang() {
1768        let many = "50% ".repeat(20_000);
1769        assert!(matches!(
1770            parse_style_transform_origin(&many).unwrap_err(),
1771            CssStyleTransformOriginParseError::WrongNumberOfComponents { got: 20_000, .. }
1772        ));
1773        let huge = alloc::format!("{}px 0px", "9".repeat(100_000));
1774        assert!(parse_style_transform_origin(&huge).is_ok());
1775    }
1776
1777    #[test]
1778    fn transform_origin_round_trips_through_its_css_repr() {
1779        for origin in [
1780            StyleTransformOrigin::default(),
1781            StyleTransformOrigin {
1782                x: PixelValue::px(20.0),
1783                y: PixelValue::percent(100.0),
1784            },
1785            StyleTransformOrigin {
1786                x: PixelValue::em(-1.5),
1787                y: PixelValue::rem(2.25),
1788            },
1789            StyleTransformOrigin {
1790                x: PixelValue::px(0.0),
1791                y: PixelValue::px(0.0),
1792            },
1793        ] {
1794            let css = origin.print_as_css_value();
1795            let reparsed = parse_style_transform_origin(&css)
1796                .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
1797            assert_eq!(reparsed, origin, "round-trip failed for {css:?}");
1798        }
1799    }
1800
1801    // =====================================================================
1802    // parse_style_perspective_origin
1803    // =====================================================================
1804
1805    #[test]
1806    fn perspective_origin_requires_exactly_two_components() {
1807        for (input, got) in [("", 0), ("50%", 1), ("1px 2px 3px", 3)] {
1808            let err = parse_style_perspective_origin(input).unwrap_err();
1809            assert!(
1810                matches!(
1811                    err,
1812                    CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
1813                        expected: 2,
1814                        got: g,
1815                        ..
1816                    } if g == got
1817                ),
1818                "{input:?} -> {err}"
1819            );
1820        }
1821    }
1822
1823    #[test]
1824    fn perspective_origin_does_not_accept_position_keywords() {
1825        // BUG (spec deviation): CSS `perspective-origin` accepts the same
1826        // left/center/right/top/bottom keywords as `transform-origin`, but this
1827        // parser only takes pixel values. Pinned as current behaviour.
1828        assert!(parse_style_perspective_origin("left top").is_err());
1829        assert!(parse_style_perspective_origin("center center").is_err());
1830        assert!(matches!(
1831            parse_style_perspective_origin("center center").unwrap_err(),
1832            CssStylePerspectiveOriginParseError::PixelValueParseError(_)
1833        ));
1834    }
1835
1836    #[test]
1837    fn perspective_origin_garbage_boundary_and_unicode() {
1838        for input in ["\u{1F600} \u{1F600}", "auto auto", "-- --", "px px"] {
1839            assert!(
1840                parse_style_perspective_origin(input).is_err(),
1841                "expected Err for {input:?}"
1842            );
1843        }
1844        let o = parse_style_perspective_origin("inf -inf").unwrap();
1845        assert_eq!(o.x.number.number(), isize::MAX);
1846        assert_eq!(o.y.number.number(), isize::MIN);
1847        assert_encodable(o.x);
1848        assert_encodable(o.y);
1849
1850        let o = parse_style_perspective_origin("NaN -0").unwrap();
1851        assert_eq!(o.x, PixelValue::px(0.0));
1852        assert_eq!(o.y, PixelValue::px(0.0));
1853
1854        let huge = alloc::format!("{}px 0px", "9".repeat(100_000));
1855        assert!(parse_style_perspective_origin(&huge).is_ok());
1856    }
1857
1858    #[test]
1859    fn perspective_origin_round_trips_through_its_css_repr() {
1860        for origin in [
1861            StylePerspectiveOrigin::default(),
1862            StylePerspectiveOrigin {
1863                x: PixelValue::px(100.0),
1864                y: PixelValue::percent(50.0),
1865            },
1866            StylePerspectiveOrigin {
1867                x: PixelValue::pt(-3.5),
1868                y: PixelValue::cm(1.0),
1869            },
1870        ] {
1871            let css = origin.print_as_css_value();
1872            let reparsed = parse_style_perspective_origin(&css)
1873                .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
1874            assert_eq!(reparsed, origin, "round-trip failed for {css:?}");
1875        }
1876    }
1877
1878    // =====================================================================
1879    // parse_style_backface_visibility
1880    // =====================================================================
1881
1882    #[test]
1883    fn backface_visibility_accepts_only_the_two_keywords() {
1884        assert_eq!(
1885            parse_style_backface_visibility("visible").unwrap(),
1886            StyleBackfaceVisibility::Visible
1887        );
1888        assert_eq!(
1889            parse_style_backface_visibility("hidden").unwrap(),
1890            StyleBackfaceVisibility::Hidden
1891        );
1892        // Surrounding whitespace is trimmed.
1893        assert_eq!(
1894            parse_style_backface_visibility("  \t visible \n ").unwrap(),
1895            StyleBackfaceVisibility::Visible
1896        );
1897        // Everything else is rejected - including case variants, substrings,
1898        // both keywords at once and zero-width joiners.
1899        for input in [
1900            "",
1901            "   ",
1902            "Visible",
1903            "HIDDEN",
1904            "visible hidden",
1905            "vis",
1906            "visiblee",
1907            "none",
1908            "0",
1909            "NaN",
1910            "\u{1F600}",
1911            "visible\u{200B}",
1912            "hidden;",
1913        ] {
1914            assert!(
1915                parse_style_backface_visibility(input).is_err(),
1916                "expected Err for {input:?}"
1917            );
1918        }
1919    }
1920
1921    #[test]
1922    fn backface_visibility_error_carries_the_untrimmed_input() {
1923        // The match is on `input.trim()` but the error is built from `input`,
1924        // so the original (untrimmed) slice is what shows up in the message.
1925        let err = parse_style_backface_visibility("  bogus  ").unwrap_err();
1926        assert_eq!(err, CssBackfaceVisibilityParseError::InvalidValue("  bogus  "));
1927        assert!(alloc::format!("{err}").contains("  bogus  "));
1928    }
1929
1930    #[test]
1931    fn backface_visibility_extremely_long_input_does_not_hang() {
1932        let huge = "visible".repeat(100_000);
1933        assert!(parse_style_backface_visibility(&huge).is_err());
1934    }
1935
1936    #[test]
1937    fn backface_visibility_round_trips_through_its_css_repr() {
1938        for v in [
1939            StyleBackfaceVisibility::Visible,
1940            StyleBackfaceVisibility::Hidden,
1941        ] {
1942            let css = v.print_as_css_value();
1943            assert_eq!(parse_style_backface_visibility(&css).unwrap(), v);
1944        }
1945        assert_eq!(
1946            StyleBackfaceVisibility::default(),
1947            StyleBackfaceVisibility::Visible
1948        );
1949    }
1950
1951    // =====================================================================
1952    // StyleTransform / StyleTransformVec round-trips (encode == decode)
1953    // =====================================================================
1954
1955    #[test]
1956    fn transform_print_parse_round_trip() {
1957        for t in all_roundtrippable_transforms() {
1958            let css = t.print_as_css_value();
1959            let reparsed = parse_style_transform(&css)
1960                .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
1961            assert_eq!(reparsed, t, "round-trip failed for {css:?}");
1962        }
1963    }
1964
1965    #[test]
1966    fn transform_vec_print_parse_round_trip() {
1967        let v: StyleTransformVec = all_roundtrippable_transforms().into();
1968        let css = v.print_as_css_value();
1969        let reparsed = parse_style_transform_vec(&css)
1970            .unwrap_or_else(|e| panic!("{css:?} did not re-parse: {e}"));
1971        assert_eq!(reparsed.len(), v.len());
1972        assert_eq!(reparsed.as_slice(), v.as_slice());
1973    }
1974
1975    #[test]
1976    fn scale_axis_print_does_not_round_trip() {
1977        // BUG: `StyleTransform::ScaleX/Y/Z` hold a `PercentageValue`, whose Display
1978        // appends a '%' ("scaleX(120%)"), but the parser reads the argument with a
1979        // bare `parse::<f32>()` and multiplies by 100. So print -> parse fails for
1980        // every ScaleX/ScaleY/ScaleZ, and the printed CSS is invalid per spec
1981        // (`scaleX()` takes a <number>, not a <percentage>).
1982        // Pinned as current behaviour; the fix is to print the raw number.
1983        for t in [
1984            StyleTransform::ScaleX(PercentageValue::new(120.0)),
1985            StyleTransform::ScaleY(PercentageValue::new(120.0)),
1986            StyleTransform::ScaleZ(PercentageValue::new(120.0)),
1987        ] {
1988            let css = t.print_as_css_value();
1989            assert!(css.contains('%'), "{css:?}");
1990            assert!(
1991                parse_style_transform(&css).is_err(),
1992                "{css:?} unexpectedly re-parsed - the ScaleX round-trip bug may be fixed"
1993            );
1994        }
1995        // The parser's own accepted form (a bare number) does work.
1996        assert_eq!(
1997            parse_style_transform("scaleX(1.2)").unwrap(),
1998            StyleTransform::ScaleX(PercentageValue::new(120.0))
1999        );
2000    }
2001
2002    // =====================================================================
2003    // StyleTransformOrigin::interpolate / StylePerspectiveOrigin::interpolate
2004    // =====================================================================
2005
2006    #[test]
2007    fn transform_origin_interpolate_endpoints_are_exact() {
2008        let a = StyleTransformOrigin {
2009            x: PixelValue::px(10.0),
2010            y: PixelValue::percent(0.0),
2011        };
2012        let b = StyleTransformOrigin {
2013            x: PixelValue::px(30.0),
2014            y: PixelValue::percent(100.0),
2015        };
2016        assert_eq!(a.interpolate(&b, 0.0), a);
2017        assert_eq!(a.interpolate(&b, 1.0), b);
2018        assert_eq!(
2019            a.interpolate(&b, 0.5),
2020            StyleTransformOrigin {
2021                x: PixelValue::px(20.0),
2022                y: PixelValue::percent(50.0),
2023            }
2024        );
2025        // Interpolating a value with itself is the identity for every finite t.
2026        for t in [-1.0, 0.0, 0.25, 1.0, 2.0, 1e30] {
2027            assert_eq!(a.interpolate(&a, t), a, "t = {t}");
2028        }
2029    }
2030
2031    #[test]
2032    fn transform_origin_interpolate_extrapolates_outside_zero_one() {
2033        let a = StyleTransformOrigin {
2034            x: PixelValue::px(10.0),
2035            y: PixelValue::px(10.0),
2036        };
2037        let b = StyleTransformOrigin {
2038            x: PixelValue::px(30.0),
2039            y: PixelValue::px(30.0),
2040        };
2041        // t is NOT clamped.
2042        assert_eq!(a.interpolate(&b, -1.0).x, PixelValue::px(-10.0));
2043        assert_eq!(a.interpolate(&b, 2.0).x, PixelValue::px(50.0));
2044    }
2045
2046    #[test]
2047    fn transform_origin_interpolate_with_nan_or_infinite_t_stays_defined() {
2048        let a = StyleTransformOrigin {
2049            x: PixelValue::px(10.0),
2050            y: PixelValue::percent(10.0),
2051        };
2052        let b = StyleTransformOrigin {
2053            x: PixelValue::px(30.0),
2054            y: PixelValue::percent(30.0),
2055        };
2056
2057        // NaN t -> NaN value -> the f32->isize cast maps NaN to 0.
2058        let nan = a.interpolate(&b, f32::NAN);
2059        assert_eq!(nan.x.number.number(), 0);
2060        assert_eq!(nan.y.number.number(), 0);
2061        assert_eq!(nan.x.metric, SizeMetric::Px);
2062        assert_eq!(nan.y.metric, SizeMetric::Percent);
2063        assert_encodable(nan.x);
2064        assert_encodable(nan.y);
2065
2066        // +inf t on an increasing range saturates to isize::MAX, -inf to isize::MIN.
2067        let pos = a.interpolate(&b, f32::INFINITY);
2068        assert_eq!(pos.x.number.number(), isize::MAX);
2069        assert_encodable(pos.x);
2070        let neg = a.interpolate(&b, f32::NEG_INFINITY);
2071        assert_eq!(neg.x.number.number(), isize::MIN);
2072        assert_encodable(neg.x);
2073
2074        // inf * 0 (identical endpoints) is NaN, which collapses to 0.
2075        let degenerate = a.interpolate(&a, f32::INFINITY);
2076        assert_eq!(degenerate.x.number.number(), 0);
2077        assert_encodable(degenerate.x);
2078    }
2079
2080    #[test]
2081    fn transform_origin_interpolate_between_saturated_extremes_does_not_panic() {
2082        let a = StyleTransformOrigin {
2083            x: PixelValue::px(f32::MAX),
2084            y: PixelValue::px(f32::MIN),
2085        };
2086        let b = StyleTransformOrigin {
2087            x: PixelValue::px(f32::MIN),
2088            y: PixelValue::px(f32::MAX),
2089        };
2090        // Both endpoints are already clamped to isize::MAX / isize::MIN.
2091        assert_eq!(a.x.number.number(), isize::MAX);
2092        assert_eq!(a.y.number.number(), isize::MIN);
2093
2094        for t in [
2095            -1e30,
2096            -1.0,
2097            0.0,
2098            0.5,
2099            1.0,
2100            1e30,
2101            f32::NAN,
2102            f32::INFINITY,
2103            f32::NEG_INFINITY,
2104        ] {
2105            let out = a.interpolate(&b, t);
2106            assert_encodable(out.x);
2107            assert_encodable(out.y);
2108        }
2109    }
2110
2111    #[test]
2112    fn transform_origin_interpolate_across_metrics_falls_back_to_px() {
2113        let a = StyleTransformOrigin {
2114            x: PixelValue::px(0.0),
2115            y: PixelValue::px(0.0),
2116        };
2117        let b = StyleTransformOrigin {
2118            x: PixelValue::percent(100.0),
2119            y: PixelValue::em(2.0),
2120        };
2121        for t in [0.0, 0.5, 1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2122            let out = a.interpolate(&b, t);
2123            assert_eq!(out.x.metric, SizeMetric::Px, "t = {t}");
2124            assert_eq!(out.y.metric, SizeMetric::Px, "t = {t}");
2125            assert_encodable(out.x);
2126            assert_encodable(out.y);
2127        }
2128    }
2129
2130    #[test]
2131    fn perspective_origin_interpolate_matches_transform_origin_semantics() {
2132        let a = StylePerspectiveOrigin {
2133            x: PixelValue::px(10.0),
2134            y: PixelValue::percent(0.0),
2135        };
2136        let b = StylePerspectiveOrigin {
2137            x: PixelValue::px(30.0),
2138            y: PixelValue::percent(100.0),
2139        };
2140        assert_eq!(a.interpolate(&b, 0.0), a);
2141        assert_eq!(a.interpolate(&b, 1.0), b);
2142        assert_eq!(
2143            a.interpolate(&b, 0.5),
2144            StylePerspectiveOrigin {
2145                x: PixelValue::px(20.0),
2146                y: PixelValue::percent(50.0),
2147            }
2148        );
2149        // Default is 0px 0px, and interpolating it with itself is stable.
2150        let d = StylePerspectiveOrigin::default();
2151        assert_eq!(d.interpolate(&d, 0.5), d);
2152
2153        // NaN / inf are defined, not panics.
2154        let nan = a.interpolate(&b, f32::NAN);
2155        assert_eq!(nan.x.number.number(), 0);
2156        assert_encodable(nan.x);
2157        let inf = a.interpolate(&b, f32::INFINITY);
2158        assert_eq!(inf.x.number.number(), isize::MAX);
2159        assert_encodable(inf.x);
2160
2161        // Saturated extremes.
2162        let lo = StylePerspectiveOrigin {
2163            x: PixelValue::px(f32::MIN),
2164            y: PixelValue::px(f32::MIN),
2165        };
2166        let hi = StylePerspectiveOrigin {
2167            x: PixelValue::px(f32::MAX),
2168            y: PixelValue::px(f32::MAX),
2169        };
2170        for t in [0.0, 0.5, 1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2171            let out = lo.interpolate(&hi, t);
2172            assert_encodable(out.x);
2173            assert_encodable(out.y);
2174        }
2175    }
2176
2177    // =====================================================================
2178    // Error types: to_contained / to_shared round-trips + Display invariants
2179    // =====================================================================
2180
2181    fn transform_errors() -> Vec<CssStyleTransformParseError<'static>> {
2182        vec![
2183            CssStyleTransformParseError::InvalidTransform("rotate"),
2184            // Edge: empty payload.
2185            CssStyleTransformParseError::InvalidTransform(""),
2186            CssStyleTransformParseError::InvalidTransform("\u{1F600}"),
2187            CssStyleTransformParseError::InvalidParenthesis(ParenthesisParseError::EmptyInput),
2188            CssStyleTransformParseError::InvalidParenthesis(
2189                ParenthesisParseError::UnclosedBraces,
2190            ),
2191            CssStyleTransformParseError::InvalidParenthesis(
2192                ParenthesisParseError::NoOpeningBraceFound,
2193            ),
2194            CssStyleTransformParseError::InvalidParenthesis(
2195                ParenthesisParseError::NoClosingBraceFound,
2196            ),
2197            CssStyleTransformParseError::InvalidParenthesis(
2198                ParenthesisParseError::StopWordNotFound("nope"),
2199            ),
2200            CssStyleTransformParseError::WrongNumberOfComponents {
2201                expected: 6,
2202                got: 5,
2203                input: "1,2,3,4,5",
2204            },
2205            // Edge: extreme counts + empty input.
2206            CssStyleTransformParseError::WrongNumberOfComponents {
2207                expected: usize::MAX,
2208                got: usize::MAX,
2209                input: "",
2210            },
2211            CssStyleTransformParseError::WrongNumberOfComponents {
2212                expected: 0,
2213                got: 0,
2214                input: "",
2215            },
2216            CssStyleTransformParseError::NumberParseError("x".parse::<f32>().unwrap_err()),
2217            CssStyleTransformParseError::NumberParseError("".parse::<f32>().unwrap_err()),
2218            CssStyleTransformParseError::PixelValueParseError(
2219                CssPixelValueParseError::EmptyString,
2220            ),
2221            CssStyleTransformParseError::PixelValueParseError(
2222                CssPixelValueParseError::InvalidPixelValue("auto"),
2223            ),
2224            CssStyleTransformParseError::AngleValueParseError(
2225                CssAngleValueParseError::EmptyString,
2226            ),
2227            CssStyleTransformParseError::AngleValueParseError(CssAngleValueParseError::InvalidAngle(
2228                "",
2229            )),
2230            CssStyleTransformParseError::PercentageValueParseError(
2231                PercentageParseError::NoPercentSign,
2232            ),
2233            CssStyleTransformParseError::PercentageValueParseError(
2234                PercentageParseError::InvalidUnit(AzString::from("")),
2235            ),
2236        ]
2237    }
2238
2239    #[test]
2240    fn transform_parse_error_round_trips_through_owned() {
2241        for err in transform_errors() {
2242            let owned = err.to_contained();
2243            let shared = owned.to_shared();
2244            assert_eq!(shared, err, "round-trip failed for {err}");
2245            // Re-owning the shared copy must be stable.
2246            assert_eq!(shared.to_contained(), owned);
2247        }
2248    }
2249
2250    #[test]
2251    fn transform_parse_error_round_trips_errors_from_the_real_parsers() {
2252        // Errors that actually come out of the parsers (rather than hand-built).
2253        for input in [
2254            "",
2255            "garbage",
2256            "translatex(1px)",
2257            "matrix(1,2,3)",
2258            "rotate(10px)",
2259            "translateX(auto)",
2260            "scaleX(abc)",
2261            "translate3d(1px,2px)",
2262        ] {
2263            let err = parse_style_transform(input).unwrap_err();
2264            assert_eq!(err.to_contained().to_shared(), err, "for {input:?}");
2265            assert!(!alloc::format!("{err}").is_empty());
2266            // Debug is implemented as Display (impl_debug_as_display!).
2267            assert_eq!(alloc::format!("{err:?}"), alloc::format!("{err}"));
2268        }
2269    }
2270
2271    #[test]
2272    fn transform_origin_parse_error_round_trips_through_owned() {
2273        let errs = [
2274            CssStyleTransformOriginParseError::WrongNumberOfComponents {
2275                expected: 2,
2276                got: 0,
2277                input: "",
2278            },
2279            CssStyleTransformOriginParseError::WrongNumberOfComponents {
2280                expected: usize::MAX,
2281                got: usize::MAX,
2282                input: "\u{1F600}",
2283            },
2284            CssStyleTransformOriginParseError::PixelValueParseError(
2285                CssPixelValueParseError::EmptyString,
2286            ),
2287            CssStyleTransformOriginParseError::PixelValueParseError(
2288                CssPixelValueParseError::InvalidPixelValue(""),
2289            ),
2290        ];
2291        for err in errs {
2292            let owned = err.to_contained();
2293            assert_eq!(owned.to_shared(), err, "round-trip failed for {err}");
2294            assert!(!alloc::format!("{err}").is_empty());
2295        }
2296        // ...and one straight out of the parser.
2297        let err = parse_style_transform_origin("top left").unwrap_err();
2298        assert_eq!(err.to_contained().to_shared(), err);
2299    }
2300
2301    #[test]
2302    fn perspective_origin_parse_error_round_trips_through_owned() {
2303        let errs = [
2304            CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
2305                expected: 2,
2306                got: 0,
2307                input: "",
2308            },
2309            CssStylePerspectiveOriginParseError::WrongNumberOfComponents {
2310                expected: 0,
2311                got: usize::MAX,
2312                input: "\u{1F600}\u{0301}",
2313            },
2314            CssStylePerspectiveOriginParseError::PixelValueParseError(
2315                CssPixelValueParseError::EmptyString,
2316            ),
2317        ];
2318        for err in errs {
2319            let owned = err.to_contained();
2320            assert_eq!(owned.to_shared(), err, "round-trip failed for {err}");
2321            assert!(!alloc::format!("{err}").is_empty());
2322        }
2323        let err = parse_style_perspective_origin("center center").unwrap_err();
2324        assert_eq!(err.to_contained().to_shared(), err);
2325    }
2326
2327    #[test]
2328    fn backface_visibility_parse_error_round_trips_through_owned() {
2329        for payload in ["", "none", "\u{1F600}", "  bogus  "] {
2330            let err = CssBackfaceVisibilityParseError::InvalidValue(payload);
2331            let owned = err.to_contained();
2332            assert_eq!(owned.to_shared(), err, "round-trip failed for {payload:?}");
2333            assert!(alloc::format!("{err}").contains(payload) || payload.is_empty());
2334        }
2335        let err = parse_style_backface_visibility("nope").unwrap_err();
2336        assert_eq!(err.to_contained().to_shared(), err);
2337    }
2338
2339    #[test]
2340    fn owned_errors_borrow_from_themselves_not_from_the_original_input() {
2341        // to_contained() must deep-copy the &str payload: the owned error has to
2342        // outlive the input it was parsed from.
2343        let owned = {
2344            let input = String::from("translatex(1px)");
2345            parse_style_transform(&input).unwrap_err().to_contained()
2346        };
2347        assert_eq!(
2348            owned,
2349            CssStyleTransformParseErrorOwned::InvalidParenthesis(
2350                ParenthesisParseErrorOwned::StopWordNotFound(AzString::from("translatex"))
2351            )
2352        );
2353        // And the re-shared borrow points at the owned buffer.
2354        assert!(matches!(
2355            owned.to_shared(),
2356            CssStyleTransformParseError::InvalidParenthesis(
2357                ParenthesisParseError::StopWordNotFound("translatex")
2358            )
2359        ));
2360    }
2361
2362    #[test]
2363    fn wrong_number_of_components_preserves_counts_across_the_owned_conversion() {
2364        let err = CssStyleTransformParseError::WrongNumberOfComponents {
2365            expected: usize::MAX,
2366            got: 0,
2367            input: "\u{1F600}",
2368        };
2369        let CssStyleTransformParseErrorOwned::WrongNumberOfComponents(WrongComponentCountError {
2370            expected,
2371            got,
2372            input,
2373        }) = err.to_contained()
2374        else {
2375            panic!("expected WrongNumberOfComponents");
2376        };
2377        assert_eq!(expected, usize::MAX);
2378        assert_eq!(got, 0);
2379        assert_eq!(input.as_str(), "\u{1F600}");
2380    }
2381
2382    // =====================================================================
2383    // parse_float_value (re-exported into this module's parse path)
2384    // =====================================================================
2385
2386    #[test]
2387    fn float_value_parse_helper_saturates_and_rejects_garbage() {
2388        assert_eq!(parse_float_value("1.5").unwrap(), FloatValue::new(1.5));
2389        assert_eq!(parse_float_value("  -0  ").unwrap(), FloatValue::new(0.0));
2390        assert_eq!(parse_float_value("inf").unwrap().number(), isize::MAX);
2391        assert_eq!(parse_float_value("-inf").unwrap().number(), isize::MIN);
2392        assert_eq!(parse_float_value("NaN").unwrap().number(), 0);
2393        assert!(parse_float_value("").is_err());
2394        assert!(parse_float_value("abc").is_err());
2395        assert!(parse_float_value("\u{1F600}").is_err());
2396    }
2397}