Skip to main content

azul_css/props/basic/
length.rs

1//! Hash-able floating-point wrappers, percentage values, and CSS size
2//! metric types used by the CSS property system.
3
4use core::fmt;
5use std::num::ParseFloatError;
6
7use crate::corety::AzString;
8
9/// Multiplier for floating point accuracy.
10///
11/// Elements such as px or %
12/// are only accurate until a certain number of decimal points, therefore
13/// they have to be casted to isizes in order to make the f32 values
14/// hash-able: Css has a relatively low precision here, roughly 3 digits, i.e
15/// `1.001 == 1.0`
16pub const FP_PRECISION_MULTIPLIER: f32 = 1000.0;
17const FP_PRECISION_MULTIPLIER_CONST: isize = crate::cast::f32_to_isize(FP_PRECISION_MULTIPLIER);
18
19/// Wrapper around `FloatValue`, represents a percentage instead
20/// of just being a regular floating-point value, i.e `5` = `5%`
21#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22#[repr(C)]
23pub struct PercentageValue {
24    number: FloatValue,
25}
26
27impl_option!(
28    PercentageValue,
29    OptionPercentageValue,
30    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
31);
32
33impl fmt::Display for PercentageValue {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        write!(f, "{}%", self.normalized() * 100.0)
36    }
37}
38
39impl PercentageValue {
40    /// Same as `PercentageValue::new()`, but only accepts whole numbers.
41    /// Uses isize arithmetic to avoid floating-point in const context.
42    #[inline]
43    #[must_use]
44    pub const fn const_new(value: isize) -> Self {
45        Self {
46            number: FloatValue::const_new(value),
47        }
48    }
49
50    /// Creates a `PercentageValue` from a fractional number in const context.
51    ///
52    /// # Arguments
53    /// * `pre_comma` - The integer part (e.g., 100 for 100.5%)
54    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5%)
55    ///
56    /// # Examples
57    /// ```
58    /// // 100% = const_new_fractional(100, 0)
59    /// // 50.5% = const_new_fractional(50, 5)
60    /// ```
61    #[inline]
62    #[must_use]
63    pub const fn const_new_fractional(pre_comma: isize, post_comma: isize) -> Self {
64        Self {
65            number: FloatValue::const_new_fractional(pre_comma, post_comma),
66        }
67    }
68
69    #[inline]
70    #[must_use]
71    pub fn new(value: f32) -> Self {
72        Self {
73            number: value.into(),
74        }
75    }
76
77    // NOTE: no get() function, to avoid confusion with "150%"
78
79    #[inline]
80    #[must_use]
81    pub fn normalized(&self) -> f32 {
82        self.number.get() / 100.0
83    }
84
85    #[inline]
86    #[must_use]
87    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
88        Self {
89            number: self.number.interpolate(&other.number, t),
90        }
91    }
92}
93
94/// Wrapper around an f32 value that is internally casted to an isize,
95/// in order to provide hash-ability (to avoid numerical instability).
96#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
97#[repr(C)]
98pub struct FloatValue {
99    pub(crate) number: isize,
100}
101
102impl fmt::Display for FloatValue {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        write!(f, "{}", self.get())
105    }
106}
107
108impl ::core::fmt::Debug for FloatValue {
109    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
110        write!(f, "{self}")
111    }
112}
113
114impl Default for FloatValue {
115    fn default() -> Self {
116        const DEFAULT_FLV: FloatValue = FloatValue::const_new(0);
117        DEFAULT_FLV
118    }
119}
120
121impl FloatValue {
122    /// Same as `FloatValue::new()`, but only accepts whole numbers.
123    /// Uses isize arithmetic to avoid floating-point in const context.
124    #[inline]
125    #[must_use]
126    pub const fn const_new(value: isize) -> Self {
127        Self {
128            number: value * FP_PRECISION_MULTIPLIER_CONST,
129        }
130    }
131
132    /// Creates a `FloatValue` from a fractional number in const context.
133    ///
134    /// This uses integer arithmetic to represent fractional values like 1.5, 0.83, etc.
135    /// in const context without relying on f32 operations.
136    ///
137    /// The function automatically detects the number of decimal places in `post_comma`
138    /// and supports up to 3 decimal places. If more digits are provided, only the first
139    /// 3 are used (truncation, not rounding).
140    ///
141    /// # Arguments
142    /// * `pre_comma` - The integer part (e.g., 1 for 1.5)
143    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5, 52 for 0.52, 523 for 0.523)
144    ///
145    /// # Examples
146    /// ```
147    /// // 1.5 = const_new_fractional(1, 5)
148    /// // 1.52 = const_new_fractional(1, 52)
149    /// // 1.523 = const_new_fractional(1, 523)
150    /// // 0.83 = const_new_fractional(0, 83)
151    /// // 1.17 = const_new_fractional(1, 17)
152    /// // 2.123456 -> 2.123 (truncated to 3 decimal places)
153    /// ```
154    #[inline]
155    #[must_use]
156    pub const fn const_new_fractional(pre_comma: isize, post_comma: isize) -> Self {
157        // Get absolute value for digit counting
158        let abs_post = if post_comma < 0 {
159            -post_comma
160        } else {
161            post_comma
162        };
163
164        // Determine the number of digits and extract only the first 3
165        // Note: We limit to values that fit in 32-bit isize for WASM compatibility
166        let (normalized_post, divisor) = if abs_post < 10 {
167            // 1 digit: 5 → 0.5
168            (abs_post, 10)
169        } else if abs_post < 100 {
170            // 2 digits: 83 → 0.83
171            (abs_post, 100)
172        } else if abs_post < 1000 {
173            // 3 digits: 523 → 0.523
174            (abs_post, 1000)
175        } else {
176            // 4+ digits: keep only the first 3 (e.g. 5234 → 523 → 0.523).
177            // A fixed division ladder cannot bound the digit count for
178            // arbitrarily large `post_comma` (an 11-digit value keeps 4 digits,
179            // etc.), letting the "fraction" grow past 1.0 and corrupt the
180            // integer part. Reduce until strictly below 1000 so the result is
181            // always a proper 3-digit fraction.
182            let mut reduced = abs_post;
183            while reduced >= 1000 {
184                reduced /= 10;
185            }
186            (reduced, 1000)
187        };
188
189        // Calculate fractional part
190        let fractional_part = normalized_post * (FP_PRECISION_MULTIPLIER_CONST / divisor);
191
192        // Apply sign: if post_comma is negative, negate the fractional part
193        let signed_fractional = if post_comma < 0 {
194            -fractional_part
195        } else {
196            fractional_part
197        };
198
199        // For negative pre_comma, the fractional part should also be negative
200        // E.g., -1.5 = -1 + (-0.5), not -1 + 0.5
201        let final_fractional = if pre_comma < 0 && post_comma >= 0 {
202            -signed_fractional
203        } else {
204            signed_fractional
205        };
206
207        Self {
208            number: pre_comma * FP_PRECISION_MULTIPLIER_CONST + final_fractional,
209        }
210    }
211
212    #[inline]
213    #[must_use]
214    pub fn new(value: f32) -> Self {
215        Self {
216            number: crate::cast::f32_to_isize(value * FP_PRECISION_MULTIPLIER),
217        }
218    }
219
220    #[inline]
221    #[must_use]
222    pub fn get(&self) -> f32 {
223        crate::cast::isize_to_f32(self.number) / FP_PRECISION_MULTIPLIER
224    }
225
226    /// Returns the raw encoded `isize` (the f32 value scaled by
227    /// `FP_PRECISION_MULTIPLIER`). Exposed so external callers can
228    /// round-trip the value through the compact-cache encoding without
229    /// re-multiplying through f32.
230    #[inline]
231    #[must_use]
232    pub const fn number(&self) -> isize {
233        self.number
234    }
235
236    #[inline]
237    #[allow(clippy::suboptimal_flops)] // explicit FP; mul_add slower without +fma
238    #[must_use]
239    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
240        let self_val_f32 = self.get();
241        let other_val_f32 = other.get();
242        let interpolated = self_val_f32 + ((other_val_f32 - self_val_f32) * t);
243        Self::new(interpolated)
244    }
245}
246
247impl From<f32> for FloatValue {
248    #[inline]
249    fn from(val: f32) -> Self {
250        Self::new(val)
251    }
252}
253
254/// Enum representing the metric associated with a number (px, pt, em, etc.)
255#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
256#[repr(C)]
257#[derive(Default)]
258pub enum SizeMetric {
259    #[default]
260    Px,
261    Pt,
262    Em,
263    Rem,
264    In,
265    Cm,
266    Mm,
267    Percent,
268    /// Viewport width: 1vw = 1% of viewport width
269    Vw,
270    /// Viewport height: 1vh = 1% of viewport height
271    Vh,
272    /// Viewport minimum: 1vmin = 1% of smaller viewport dimension
273    Vmin,
274    /// Viewport maximum: 1vmax = 1% of larger viewport dimension
275    Vmax,
276}
277
278impl fmt::Display for SizeMetric {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        use self::SizeMetric::{Cm, Em, In, Mm, Percent, Pt, Px, Rem, Vh, Vmax, Vmin, Vw};
281        match self {
282            Px => write!(f, "px"),
283            Pt => write!(f, "pt"),
284            Em => write!(f, "em"),
285            Rem => write!(f, "rem"),
286            In => write!(f, "in"),
287            Cm => write!(f, "cm"),
288            Mm => write!(f, "mm"),
289            Percent => write!(f, "%"),
290            Vw => write!(f, "vw"),
291            Vh => write!(f, "vh"),
292            Vmin => write!(f, "vmin"),
293            Vmax => write!(f, "vmax"),
294        }
295    }
296}
297
298/// # Errors
299///
300/// Returns an error if `input` is not a valid CSS `float-value` value.
301pub fn parse_float_value(input: &str) -> Result<FloatValue, ParseFloatError> {
302    Ok(FloatValue::new(input.trim().parse::<f32>()?))
303}
304#[allow(variant_size_differences)]
305// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
306#[derive(Clone, PartialEq, Eq)]
307#[repr(C, u8)]
308pub enum PercentageParseError {
309    ValueParseErr(crate::props::basic::error::ParseFloatError),
310    NoPercentSign,
311    InvalidUnit(AzString),
312}
313
314impl_debug_as_display!(PercentageParseError);
315
316impl From<ParseFloatError> for PercentageParseError {
317    fn from(e: ParseFloatError) -> Self {
318        Self::ValueParseErr(crate::props::basic::error::ParseFloatError::from(e))
319    }
320}
321
322impl_display! { PercentageParseError, {
323    ValueParseErr(e) => format!("\"{}\"", e),
324    NoPercentSign => format!("No percent sign after number"),
325    InvalidUnit(u) => format!("Error parsing percentage: invalid unit \"{}\"", u.as_str()),
326}}
327#[allow(variant_size_differences)]
328// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
329#[derive(Debug, Clone, PartialEq, Eq)]
330#[repr(C, u8)]
331pub enum PercentageParseErrorOwned {
332    ValueParseErr(crate::props::basic::error::ParseFloatError),
333    NoPercentSign,
334    InvalidUnit(AzString),
335}
336
337impl PercentageParseError {
338    #[must_use]
339    pub fn to_contained(&self) -> PercentageParseErrorOwned {
340        match self {
341            Self::ValueParseErr(e) => PercentageParseErrorOwned::ValueParseErr(*e),
342            Self::NoPercentSign => PercentageParseErrorOwned::NoPercentSign,
343            Self::InvalidUnit(u) => PercentageParseErrorOwned::InvalidUnit(u.clone()),
344        }
345    }
346}
347
348impl PercentageParseErrorOwned {
349    #[must_use]
350    pub fn to_shared(&self) -> PercentageParseError {
351        match self {
352            Self::ValueParseErr(e) => PercentageParseError::ValueParseErr(*e),
353            Self::NoPercentSign => PercentageParseError::NoPercentSign,
354            Self::InvalidUnit(u) => PercentageParseError::InvalidUnit(u.clone()),
355        }
356    }
357}
358
359/// Parse "1.2" or "120%" (similar to `parse_pixel_value`)
360/// # Errors
361///
362/// Returns an error if `input` is not a valid CSS `percentage-value` value.
363pub fn parse_percentage_value(input: &str) -> Result<PercentageValue, PercentageParseError> {
364    let input = input.trim();
365
366    if input.is_empty() {
367        return Err(PercentageParseError::ValueParseErr(
368            crate::props::basic::error::ParseFloatError::from(
369                "empty string".parse::<f32>().unwrap_err(),
370            ),
371        ));
372    }
373
374    let mut split_pos = 0;
375    let mut found_numeric = false;
376    for (idx, ch) in input.char_indices() {
377        if ch.is_numeric() || ch == '.' || ch == '-' {
378            // Advance past the *whole* char: `is_numeric()` matches multi-byte
379            // Unicode digits (½ U+00BD, ٥ U+0665, 5 U+FF15). Using `idx + 1`
380            // would land inside the codepoint and panic on the slice below.
381            split_pos = idx + ch.len_utf8();
382            found_numeric = true;
383        }
384    }
385
386    if !found_numeric {
387        return Err(PercentageParseError::ValueParseErr(
388            crate::props::basic::error::ParseFloatError::from(
389                "no numeric value".parse::<f32>().unwrap_err(),
390            ),
391        ));
392    }
393
394    let unit = input[split_pos..].trim();
395    let mut number = input[..split_pos].trim().parse::<f32>().map_err(|e| {
396        PercentageParseError::ValueParseErr(crate::props::basic::error::ParseFloatError::from(e))
397    })?;
398
399    match unit {
400        "" => {
401            number *= 100.0;
402        } // 0.5 => 50%
403        "%" => {} // 50% => PercentageValue(50.0)
404        other => {
405            return Err(PercentageParseError::InvalidUnit(other.to_string().into()));
406        }
407    }
408
409    Ok(PercentageValue::new(number))
410}
411
412#[cfg(all(test, feature = "parser"))]
413mod tests {
414    // Tests assert that parsed values equal the exact source literals.
415    #![allow(clippy::float_cmp)]
416    use super::*;
417
418    #[test]
419    fn test_parse_float_value() {
420        assert_eq!(parse_float_value("10").unwrap().get(), 10.0);
421        assert_eq!(parse_float_value("2.5").unwrap().get(), 2.5);
422        assert_eq!(parse_float_value("-50.2").unwrap().get(), -50.2);
423        assert_eq!(parse_float_value("  0  ").unwrap().get(), 0.0);
424        assert!(parse_float_value("10a").is_err());
425        assert!(parse_float_value("").is_err());
426    }
427
428    #[test]
429    fn test_parse_percentage_value() {
430        // With percent sign
431        assert_eq!(parse_percentage_value("50%").unwrap().normalized(), 0.5);
432        assert_eq!(parse_percentage_value("120%").unwrap().normalized(), 1.2);
433        assert_eq!(parse_percentage_value("-25%").unwrap().normalized(), -0.25);
434        assert_eq!(
435            parse_percentage_value("  75.5%  ").unwrap().normalized(),
436            0.755
437        );
438
439        // As a ratio
440        assert!((parse_percentage_value("0.5").unwrap().normalized() - 0.5).abs() < 1e-6);
441        assert!((parse_percentage_value("1.2").unwrap().normalized() - 1.2).abs() < 1e-6);
442        assert!((parse_percentage_value("1").unwrap().normalized() - 1.0).abs() < 1e-6);
443
444        // Errors
445        assert!(matches!(
446            parse_percentage_value("50px").err().unwrap(),
447            PercentageParseError::InvalidUnit(_)
448        ));
449        assert!(parse_percentage_value("fifty%").is_err());
450        assert!(parse_percentage_value("").is_err());
451    }
452
453    #[test]
454    fn test_const_new_fractional_single_digit() {
455        // Single digit post_comma (1 decimal place)
456        let val = FloatValue::const_new_fractional(1, 5);
457        assert_eq!(val.get(), 1.5);
458
459        let val = FloatValue::const_new_fractional(0, 5);
460        assert_eq!(val.get(), 0.5);
461
462        let val = FloatValue::const_new_fractional(2, 3);
463        assert_eq!(val.get(), 2.3);
464
465        let val = FloatValue::const_new_fractional(0, 0);
466        assert_eq!(val.get(), 0.0);
467
468        let val = FloatValue::const_new_fractional(10, 9);
469        assert_eq!(val.get(), 10.9);
470    }
471
472    #[test]
473    fn test_const_new_fractional_two_digits() {
474        // Two digits post_comma (2 decimal places)
475        let val = FloatValue::const_new_fractional(0, 83);
476        assert!((val.get() - 0.83).abs() < 0.001);
477
478        let val = FloatValue::const_new_fractional(1, 17);
479        assert!((val.get() - 1.17).abs() < 0.001);
480
481        let val = FloatValue::const_new_fractional(1, 52);
482        assert!((val.get() - 1.52).abs() < 0.001);
483
484        let val = FloatValue::const_new_fractional(0, 33);
485        assert!((val.get() - 0.33).abs() < 0.001);
486
487        let val = FloatValue::const_new_fractional(2, 67);
488        assert!((val.get() - 2.67).abs() < 0.001);
489
490        let val = FloatValue::const_new_fractional(0, 10);
491        assert!((val.get() - 0.10).abs() < 0.001);
492
493        let val = FloatValue::const_new_fractional(0, 99);
494        assert!((val.get() - 0.99).abs() < 0.001);
495    }
496
497    #[test]
498    fn test_const_new_fractional_three_digits() {
499        // Three digits post_comma (3 decimal places)
500        let val = FloatValue::const_new_fractional(1, 523);
501        assert!((val.get() - 1.523).abs() < 0.001);
502
503        let val = FloatValue::const_new_fractional(0, 123);
504        assert!((val.get() - 0.123).abs() < 0.001);
505
506        let val = FloatValue::const_new_fractional(2, 999);
507        assert!((val.get() - 2.999).abs() < 0.001);
508
509        let val = FloatValue::const_new_fractional(0, 100);
510        assert!((val.get() - 0.100).abs() < 0.001);
511
512        let val = FloatValue::const_new_fractional(5, 1);
513        assert!((val.get() - 5.1).abs() < 0.001);
514    }
515
516    #[test]
517    fn test_const_new_fractional_truncation() {
518        // More than 3 digits should be truncated (not rounded)
519
520        // 4 digits: 5234 → 523 → 0.523
521        let val = FloatValue::const_new_fractional(0, 5234);
522        assert!((val.get() - 0.523).abs() < 0.001);
523
524        // 5 digits: 12345 → 123 → 0.123
525        let val = FloatValue::const_new_fractional(1, 12345);
526        assert!((val.get() - 1.123).abs() < 0.001);
527
528        // 6 digits: 123456 → 123 → 1.123
529        let val = FloatValue::const_new_fractional(1, 123_456);
530        assert!((val.get() - 1.123).abs() < 0.001);
531
532        // 7 digits: 9876543 → 987 → 0.987
533        let val = FloatValue::const_new_fractional(0, 9_876_543);
534        assert!((val.get() - 0.987).abs() < 0.001);
535
536        // 10 digits
537        let val = FloatValue::const_new_fractional(2, 1_234_567_890);
538        assert!((val.get() - 2.123).abs() < 0.001);
539    }
540
541    #[test]
542    fn test_const_new_fractional_negative() {
543        // Negative pre_comma values
544        let val = FloatValue::const_new_fractional(-1, 5);
545        assert_eq!(val.get(), -1.5);
546
547        let val = FloatValue::const_new_fractional(0, 83);
548        assert!((val.get() - 0.83).abs() < 0.001);
549
550        let val = FloatValue::const_new_fractional(-2, 123);
551        assert!((val.get() - -2.123).abs() < 0.001);
552
553        // Negative post_comma (unusual case - treated as negative fractional part)
554        let val = FloatValue::const_new_fractional(1, -5);
555        assert_eq!(val.get(), 0.5); // 1 + (-0.5) = 0.5
556
557        let val = FloatValue::const_new_fractional(0, -50);
558        assert!((val.get() - -0.5).abs() < 0.001); // 0 + (-0.5) = -0.5
559    }
560
561    #[test]
562    fn test_const_new_fractional_edge_cases() {
563        // Zero
564        let val = FloatValue::const_new_fractional(0, 0);
565        assert_eq!(val.get(), 0.0);
566
567        // Large integer part
568        let val = FloatValue::const_new_fractional(100, 5);
569        assert_eq!(val.get(), 100.5);
570
571        let val = FloatValue::const_new_fractional(1000, 99);
572        assert!((val.get() - 1000.99).abs() < 0.001);
573
574        // Maximum precision (3 digits)
575        let val = FloatValue::const_new_fractional(0, 999);
576        assert!((val.get() - 0.999).abs() < 0.001);
577
578        // Small fractional values
579        let val = FloatValue::const_new_fractional(1, 1);
580        assert!((val.get() - 1.1).abs() < 0.001);
581
582        let val = FloatValue::const_new_fractional(1, 10);
583        assert!((val.get() - 1.10).abs() < 0.001);
584    }
585
586    #[test]
587    fn test_const_new_fractional_ua_css_values() {
588        // Test actual values used in ua_css.rs
589
590        // H1: 2em
591        let val = FloatValue::const_new_fractional(2, 0);
592        assert_eq!(val.get(), 2.0);
593
594        // H2: 1.5em
595        let val = FloatValue::const_new_fractional(1, 5);
596        assert_eq!(val.get(), 1.5);
597
598        // H3: 1.17em
599        let val = FloatValue::const_new_fractional(1, 17);
600        assert!((val.get() - 1.17).abs() < 0.001);
601
602        // H4: 1em
603        let val = FloatValue::const_new_fractional(1, 0);
604        assert_eq!(val.get(), 1.0);
605
606        // H5: 0.83em
607        let val = FloatValue::const_new_fractional(0, 83);
608        assert!((val.get() - 0.83).abs() < 0.001);
609
610        // H6: 0.67em
611        let val = FloatValue::const_new_fractional(0, 67);
612        assert!((val.get() - 0.67).abs() < 0.001);
613
614        // Margins: 0.67em
615        let val = FloatValue::const_new_fractional(0, 67);
616        assert!((val.get() - 0.67).abs() < 0.001);
617
618        // Margins: 0.83em
619        let val = FloatValue::const_new_fractional(0, 83);
620        assert!((val.get() - 0.83).abs() < 0.001);
621
622        // Margins: 1.33em
623        let val = FloatValue::const_new_fractional(1, 33);
624        assert!((val.get() - 1.33).abs() < 0.001);
625
626        // Margins: 1.67em
627        let val = FloatValue::const_new_fractional(1, 67);
628        assert!((val.get() - 1.67).abs() < 0.001);
629
630        // Margins: 2.33em
631        let val = FloatValue::const_new_fractional(2, 33);
632        assert!((val.get() - 2.33).abs() < 0.001);
633    }
634
635    #[test]
636    fn test_const_new_fractional_consistency() {
637        // Verify consistency between const_new_fractional and new()
638
639        let const_val = FloatValue::const_new_fractional(1, 5);
640        let runtime_val = FloatValue::new(1.5);
641        assert_eq!(const_val.get(), runtime_val.get());
642
643        let const_val = FloatValue::const_new_fractional(0, 83);
644        let runtime_val = FloatValue::new(0.83);
645        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
646
647        let const_val = FloatValue::const_new_fractional(1, 523);
648        let runtime_val = FloatValue::new(1.523);
649        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
650
651        let const_val = FloatValue::const_new_fractional(2, 99);
652        let runtime_val = FloatValue::new(2.99);
653        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
654    }
655}
656
657#[cfg(test)]
658#[allow(
659    clippy::float_cmp,
660    clippy::unreadable_literal,
661    clippy::excessive_precision
662)]
663mod autotest_generated {
664    use std::{
665        collections::{hash_map::DefaultHasher, HashSet},
666        hash::{Hash, Hasher},
667    };
668
669    use super::*;
670    use crate::props::basic::error::ParseFloatError as CssParseFloatError;
671
672    /// Largest `isize` that `const_new` can scale by `FP_PRECISION_MULTIPLIER`
673    /// without overflowing the multiplication.
674    const MAX_SAFE_CONST_NEW: isize = isize::MAX / 1000;
675    const MIN_SAFE_CONST_NEW: isize = isize::MIN / 1000;
676
677    fn hash_of<T: Hash>(v: &T) -> u64 {
678        let mut h = DefaultHasher::new();
679        v.hash(&mut h);
680        h.finish()
681    }
682
683    // ------------------------------------------------------- FloatValue::new ---
684
685    #[test]
686    fn float_value_new_never_produces_a_non_finite_get() {
687        // `get()` decodes an isize, so it must be finite for *every* input,
688        // including the ones that overflow the f32 multiply inside `new()`.
689        for v in [
690            f32::NAN,
691            f32::INFINITY,
692            f32::NEG_INFINITY,
693            f32::MAX,
694            f32::MIN,
695            f32::MIN_POSITIVE,
696            -f32::MIN_POSITIVE,
697            0.0,
698            -0.0,
699            1e30,
700            -1e30,
701        ] {
702            let got = FloatValue::new(v).get();
703            assert!(
704                got.is_finite(),
705                "FloatValue::new({v}).get() leaked a non-finite value: {got}"
706            );
707        }
708    }
709
710    #[test]
711    fn float_value_new_saturates_at_the_isize_bounds() {
712        // f32 -> isize `as` casts saturate; +inf/-inf and anything that overflows
713        // the *1000 multiply must clamp instead of wrapping.
714        assert_eq!(FloatValue::new(f32::INFINITY).number(), isize::MAX);
715        assert_eq!(FloatValue::new(f32::NEG_INFINITY).number(), isize::MIN);
716        // f32::MAX * 1000.0 overflows to +inf before the cast.
717        assert_eq!(FloatValue::new(f32::MAX).number(), isize::MAX);
718        assert_eq!(FloatValue::new(f32::MIN).number(), isize::MIN);
719    }
720
721    #[test]
722    fn float_value_new_collapses_nan_to_zero() {
723        // NaN `as isize` is defined to be 0 — assert it, so a future hand-rolled
724        // cast that panics or wraps is caught.
725        let nan = FloatValue::new(f32::NAN);
726        assert_eq!(nan.number(), 0);
727        assert_eq!(nan.get(), 0.0);
728        // ...and NaN is therefore *equal* to the default value, not unequal-to-itself.
729        assert_eq!(nan, FloatValue::default());
730        assert_eq!(hash_of(&nan), hash_of(&FloatValue::default()));
731    }
732
733    #[test]
734    fn float_value_new_does_not_leak_negative_zero() {
735        let neg_zero = FloatValue::new(-0.0);
736        assert_eq!(neg_zero.number(), 0);
737        assert!(
738            neg_zero.get().is_sign_positive(),
739            "-0.0 round-tripped back out as a negative zero"
740        );
741        assert_eq!(neg_zero, FloatValue::new(0.0));
742    }
743
744    #[test]
745    fn float_value_new_underflows_subnormals_to_zero() {
746        // Anything below 1/1000 truncates away entirely.
747        assert_eq!(FloatValue::new(f32::MIN_POSITIVE).number(), 0);
748        assert_eq!(FloatValue::new(1e-30).number(), 0);
749        assert_eq!(FloatValue::new(0.0009).number(), 0);
750    }
751
752    #[test]
753    fn float_value_new_truncates_toward_zero_not_to_nearest() {
754        // Encoding is `(v * 1000) as isize`, i.e. truncation — 0.0019 must NOT
755        // round up to 0.002, and the negative side must truncate toward zero too.
756        assert_eq!(FloatValue::new(0.0019).number(), 1);
757        assert_eq!(FloatValue::new(0.0019).get(), 0.001);
758        assert_eq!(FloatValue::new(-0.0019).number(), -1);
759        assert_eq!(FloatValue::new(-0.0019).get(), -0.001);
760    }
761
762    #[test]
763    fn float_value_quantizes_below_the_precision_limit() {
764        // The type's whole purpose: sub-precision differences collapse, so that
765        // Eq/Hash are stable. 4th decimal is dropped, 3rd is kept.
766        assert_eq!(FloatValue::new(1.0001), FloatValue::new(1.0));
767        assert_ne!(FloatValue::new(1.001), FloatValue::new(1.0));
768    }
769
770    #[test]
771    fn float_value_eq_implies_equal_hash() {
772        // Eq + Hash must agree — the type exists purely to be hash-able.
773        for (a, b) in [
774            (1.0_f32, 1.0004_f32),
775            (-2.5, -2.5001),
776            (0.0, -0.0),
777            (f32::NAN, f32::NAN),
778        ] {
779            let (a, b) = (FloatValue::new(a), FloatValue::new(b));
780            assert_eq!(a, b, "expected {a:?} == {b:?}");
781            assert_eq!(hash_of(&a), hash_of(&b), "{a:?} == {b:?} but hashes differ");
782        }
783    }
784
785    #[test]
786    fn float_value_ord_agrees_with_get() {
787        // Ord is derived on the encoded isize; it must stay monotonic w.r.t. get().
788        let mut vals: Vec<FloatValue> = [3.5_f32, -1.0, 0.0, 100.25, -0.001, 2.0]
789            .into_iter()
790            .map(FloatValue::new)
791            .collect();
792        vals.sort();
793        for w in vals.windows(2) {
794            assert!(
795                w[0].get() <= w[1].get(),
796                "sort order disagrees with get(): {:?} then {:?}",
797                w[0],
798                w[1]
799            );
800        }
801    }
802
803    // -------------------------------------------------- FloatValue::const_new ---
804
805    #[test]
806    fn const_new_matches_the_documented_encoding() {
807        assert_eq!(FP_PRECISION_MULTIPLIER, 1000.0);
808        assert_eq!(FloatValue::const_new(0).number(), 0);
809        assert_eq!(FloatValue::const_new(1).number(), 1000);
810        assert_eq!(FloatValue::const_new(-1).number(), -1000);
811        assert_eq!(FloatValue::const_new(0), FloatValue::default());
812    }
813
814    #[test]
815    fn const_new_agrees_with_new_for_whole_numbers() {
816        for n in [-1000_isize, -7, -1, 0, 1, 7, 1000, 65_536] {
817            let c = FloatValue::const_new(n);
818            let r = FloatValue::new(n as f32);
819            assert_eq!(
820                c, r,
821                "const_new({n}) = {c:?} disagrees with new({n}.0) = {r:?}"
822            );
823        }
824    }
825
826    #[test]
827    fn const_new_survives_the_largest_non_overflowing_inputs() {
828        // `const_new` is a bare `value * 1000`, so isize::MAX/1000 is the last
829        // input it can take without overflowing. Pin that boundary: anything at
830        // or below it must be exact and must not panic.
831        let hi = FloatValue::const_new(MAX_SAFE_CONST_NEW);
832        assert_eq!(hi.number(), MAX_SAFE_CONST_NEW * 1000);
833        assert!(hi.get().is_finite());
834
835        let lo = FloatValue::const_new(MIN_SAFE_CONST_NEW);
836        assert_eq!(lo.number(), MIN_SAFE_CONST_NEW * 1000);
837        assert!(lo.get().is_finite());
838
839        assert!(lo < hi);
840    }
841
842    // --------------------------------------- FloatValue::const_new_fractional ---
843
844    #[test]
845    fn const_new_fractional_zero_and_sign_handling() {
846        assert_eq!(FloatValue::const_new_fractional(0, 0).number(), 0);
847        // Negative pre_comma pulls the fraction negative too (-1.5, not -0.5).
848        assert_eq!(FloatValue::const_new_fractional(-1, 5).number(), -1500);
849        // Negative post_comma subtracts from a positive pre_comma.
850        assert_eq!(FloatValue::const_new_fractional(1, -5).number(), 500);
851        assert_eq!(FloatValue::const_new_fractional(0, -50).number(), -500);
852    }
853
854    #[test]
855    fn const_new_fractional_never_panics_on_extreme_post_comma() {
856        // post_comma is an unbounded isize; the digit-count ladder must not
857        // divide by zero, overflow, or produce a non-finite decode.
858        for post in [
859            9_isize,
860            99,
861            999,
862            9_999,
863            99_999,
864            999_999,
865            9_999_999,
866            99_999_999,
867            999_999_999,
868            isize::MAX,
869        ] {
870            let v = FloatValue::const_new_fractional(0, post);
871            assert!(
872                v.get().is_finite(),
873                "const_new_fractional(0, {post}) decoded to a non-finite value"
874            );
875        }
876    }
877
878    #[test]
879    fn const_new_fractional_truncates_to_three_decimals() {
880        // Documented: only the first 3 digits of post_comma are used, truncated.
881        assert_eq!(FloatValue::const_new_fractional(0, 5234).number(), 523);
882        assert_eq!(FloatValue::const_new_fractional(1, 123_456).number(), 1123);
883        // 10 digits is the largest post_comma the ladder still truncates correctly.
884        assert_eq!(
885            FloatValue::const_new_fractional(2, 1_234_567_890).number(),
886            2123
887        );
888    }
889
890    #[test]
891    fn const_new_fractional_boundary_between_digit_buckets() {
892        // Every `abs_post < 10^k` bucket edge: 9/10, 99/100, 999/1000.
893        assert_eq!(FloatValue::const_new_fractional(0, 9).get(), 0.9);
894        assert_eq!(FloatValue::const_new_fractional(0, 10).get(), 0.1);
895        assert_eq!(FloatValue::const_new_fractional(0, 99).get(), 0.99);
896        assert_eq!(FloatValue::const_new_fractional(0, 100).get(), 0.1);
897        assert_eq!(FloatValue::const_new_fractional(0, 999).get(), 0.999);
898    }
899
900    #[test]
901    fn const_new_fractional_cannot_express_a_leading_zero_fraction() {
902        // The bucket is picked from the *digit count* of post_comma, so a leading
903        // zero is unrepresentable in an integer argument: 0.05 has no spelling.
904        // Both of the obvious attempts land on 0.5 instead. Pin the footgun so a
905        // caller writing `(0, 50)` for "0.05em" is caught by this test, not by a
906        // 10x-too-large margin on screen.
907        assert_eq!(FloatValue::const_new_fractional(0, 5).get(), 0.5);
908        assert_eq!(FloatValue::const_new_fractional(0, 50).get(), 0.5);
909        assert_eq!(FloatValue::const_new_fractional(0, 500).get(), 0.5);
910    }
911
912    // ------------------------------------------------- FloatValue::interpolate ---
913
914    #[test]
915    fn interpolate_endpoints_are_exact() {
916        let a = FloatValue::new(0.0);
917        let b = FloatValue::new(10.0);
918        assert_eq!(a.interpolate(&b, 0.0), a);
919        assert_eq!(a.interpolate(&b, 1.0), b);
920        assert_eq!(a.interpolate(&b, 0.5).get(), 5.0);
921        // Reversed direction.
922        assert_eq!(b.interpolate(&a, 0.5).get(), 5.0);
923    }
924
925    #[test]
926    fn interpolate_extrapolates_outside_zero_one() {
927        // t is not clamped — assert the (documented-by-absence) extrapolation
928        // rather than silently assuming a clamp that isn't there.
929        let a = FloatValue::new(0.0);
930        let b = FloatValue::new(10.0);
931        assert_eq!(a.interpolate(&b, 2.0).get(), 20.0);
932        assert_eq!(a.interpolate(&b, -1.0).get(), -10.0);
933    }
934
935    #[test]
936    fn interpolate_with_nan_or_infinite_t_stays_finite() {
937        let a = FloatValue::new(0.0);
938        let b = FloatValue::new(10.0);
939
940        // NaN t -> NaN interpolant -> `as isize` collapses to 0.
941        assert_eq!(a.interpolate(&b, f32::NAN).number(), 0);
942
943        // +inf t with a non-zero delta -> +inf -> saturates to isize::MAX.
944        assert_eq!(a.interpolate(&b, f32::INFINITY).number(), isize::MAX);
945        assert_eq!(a.interpolate(&b, f32::NEG_INFINITY).number(), isize::MIN);
946
947        // inf * 0.0 delta is NaN -> collapses to 0 (self is NOT preserved here).
948        assert_eq!(a.interpolate(&a, f32::INFINITY).number(), 0);
949
950        for t in [
951            f32::NAN,
952            f32::INFINITY,
953            f32::NEG_INFINITY,
954            f32::MAX,
955            f32::MIN,
956        ] {
957            assert!(
958                a.interpolate(&b, t).get().is_finite(),
959                "interpolate(t = {t}) leaked a non-finite value"
960            );
961        }
962    }
963
964    #[test]
965    fn interpolate_between_saturated_extremes_does_not_panic() {
966        let lo = FloatValue::new(f32::NEG_INFINITY); // isize::MIN
967        let hi = FloatValue::new(f32::INFINITY); // isize::MAX
968        for t in [0.0, 0.5, 1.0, -1.0, 2.0, f32::NAN] {
969            assert!(lo.interpolate(&hi, t).get().is_finite());
970            assert!(hi.interpolate(&lo, t).get().is_finite());
971        }
972    }
973
974    // -------------------------------------------------------- round-tripping ---
975
976    #[test]
977    fn float_value_round_trips_through_display_and_parse() {
978        // encode == decode: every value that is exactly representable at 3
979        // decimals must survive Display -> parse_float_value -> FloatValue.
980        for v in [0.0_f32, 1.5, -2.25, 100.0, 0.001, -0.001, 999.999, -0.5] {
981            let fv = FloatValue::new(v);
982            let round_tripped = parse_float_value(&fv.to_string())
983                .unwrap_or_else(|e| panic!("Display of {fv:?} did not re-parse: {e}"));
984            assert_eq!(
985                fv, round_tripped,
986                "round-trip changed {fv:?} into {round_tripped:?}"
987            );
988        }
989    }
990
991    #[test]
992    fn float_value_number_round_trips_through_get() {
993        // number() is the compact-cache encoding; get() must be its exact inverse
994        // (scaled) for values inside the f32-exact integer range.
995        for raw in [0_isize, 1, -1, 1500, -1500, 999_999, -999_999] {
996            let fv = FloatValue::new(raw as f32 / 1000.0);
997            assert_eq!(fv.number(), raw, "number() lost the encoding for {raw}");
998        }
999    }
1000
1001    #[test]
1002    fn float_value_display_and_debug_agree() {
1003        // Debug is hand-written to forward to Display; a divergence means the
1004        // manual impl drifted.
1005        for v in [0.0_f32, -1.25, 1e6, f32::INFINITY, f32::NAN] {
1006            let fv = FloatValue::new(v);
1007            assert_eq!(format!("{fv:?}"), format!("{fv}"));
1008            assert!(!format!("{fv}").is_empty());
1009            // Whatever we print must itself be a parseable float.
1010            assert!(fv.to_string().parse::<f32>().is_ok());
1011        }
1012        assert_eq!(FloatValue::default().to_string(), "0");
1013    }
1014
1015    // ---------------------------------------------------------- SizeMetric ---
1016
1017    #[test]
1018    fn size_metric_display_is_non_empty_and_unique() {
1019        use SizeMetric::{Cm, Em, In, Mm, Percent, Pt, Px, Rem, Vh, Vmax, Vmin, Vw};
1020
1021        let all = [Px, Pt, Em, Rem, In, Cm, Mm, Percent, Vw, Vh, Vmin, Vmax];
1022        let mut seen = HashSet::new();
1023        for m in all {
1024            let s = m.to_string();
1025            assert!(!s.is_empty(), "{m:?} renders as an empty string");
1026            assert!(
1027                seen.insert(s.clone()),
1028                "two SizeMetric variants both render as {s:?} (copy-paste in Display)"
1029            );
1030        }
1031        assert_eq!(seen.len(), all.len());
1032    }
1033
1034    #[test]
1035    fn size_metric_display_matches_the_css_unit_tokens() {
1036        assert_eq!(SizeMetric::Px.to_string(), "px");
1037        assert_eq!(SizeMetric::Pt.to_string(), "pt");
1038        assert_eq!(SizeMetric::Em.to_string(), "em");
1039        assert_eq!(SizeMetric::Rem.to_string(), "rem");
1040        assert_eq!(SizeMetric::In.to_string(), "in");
1041        assert_eq!(SizeMetric::Cm.to_string(), "cm");
1042        assert_eq!(SizeMetric::Mm.to_string(), "mm");
1043        assert_eq!(SizeMetric::Percent.to_string(), "%");
1044        assert_eq!(SizeMetric::Vw.to_string(), "vw");
1045        assert_eq!(SizeMetric::Vh.to_string(), "vh");
1046        assert_eq!(SizeMetric::Vmin.to_string(), "vmin");
1047        assert_eq!(SizeMetric::Vmax.to_string(), "vmax");
1048    }
1049
1050    #[test]
1051    fn size_metric_default_is_px() {
1052        assert_eq!(SizeMetric::default(), SizeMetric::Px);
1053        assert_eq!(SizeMetric::default().to_string(), "px");
1054    }
1055
1056    // -------------------------------------------------------- PercentageValue ---
1057
1058    #[test]
1059    fn percentage_value_normalized_divides_by_a_hundred() {
1060        assert_eq!(PercentageValue::new(50.0).normalized(), 0.5);
1061        assert_eq!(PercentageValue::new(0.0).normalized(), 0.0);
1062        assert_eq!(PercentageValue::new(-25.0).normalized(), -0.25);
1063        assert_eq!(PercentageValue::const_new(100).normalized(), 1.0);
1064        assert_eq!(PercentageValue::default().normalized(), 0.0);
1065    }
1066
1067    #[test]
1068    fn percentage_value_normalized_is_always_finite() {
1069        for v in [
1070            f32::NAN,
1071            f32::INFINITY,
1072            f32::NEG_INFINITY,
1073            f32::MAX,
1074            f32::MIN,
1075        ] {
1076            let n = PercentageValue::new(v).normalized();
1077            assert!(
1078                n.is_finite(),
1079                "PercentageValue::new({v}).normalized() leaked {n}"
1080            );
1081        }
1082        // NaN collapses to the default, exactly like FloatValue.
1083        assert_eq!(PercentageValue::new(f32::NAN), PercentageValue::default());
1084    }
1085
1086    #[test]
1087    fn percentage_value_const_new_boundaries_do_not_panic() {
1088        assert_eq!(PercentageValue::const_new(0), PercentageValue::default());
1089        assert!(PercentageValue::const_new(MAX_SAFE_CONST_NEW)
1090            .normalized()
1091            .is_finite());
1092        assert!(PercentageValue::const_new(MIN_SAFE_CONST_NEW)
1093            .normalized()
1094            .is_finite());
1095        assert!(
1096            PercentageValue::const_new(MIN_SAFE_CONST_NEW)
1097                < PercentageValue::const_new(MAX_SAFE_CONST_NEW)
1098        );
1099    }
1100
1101    #[test]
1102    fn percentage_value_const_new_fractional_matches_the_docs() {
1103        // 100% = const_new_fractional(100, 0); 50.5% = const_new_fractional(50, 5)
1104        assert_eq!(
1105            PercentageValue::const_new_fractional(100, 0).normalized(),
1106            1.0
1107        );
1108        assert!((PercentageValue::const_new_fractional(50, 5).normalized() - 0.505).abs() < 1e-5);
1109        assert_eq!(
1110            PercentageValue::const_new_fractional(100, 0),
1111            PercentageValue::const_new(100)
1112        );
1113    }
1114
1115    #[test]
1116    fn percentage_value_interpolate_endpoints_and_nan() {
1117        let a = PercentageValue::new(0.0);
1118        let b = PercentageValue::new(100.0);
1119        assert_eq!(a.interpolate(&b, 0.0), a);
1120        assert_eq!(a.interpolate(&b, 1.0), b);
1121        assert_eq!(a.interpolate(&b, 0.5).normalized(), 0.5);
1122        // NaN / inf t must not panic and must stay finite.
1123        assert_eq!(a.interpolate(&b, f32::NAN).normalized(), 0.0);
1124        assert!(a.interpolate(&b, f32::INFINITY).normalized().is_finite());
1125        assert!(a
1126            .interpolate(&b, f32::NEG_INFINITY)
1127            .normalized()
1128            .is_finite());
1129    }
1130
1131    #[test]
1132    fn percentage_value_display_round_trips_through_the_parser() {
1133        for v in [0.0_f32, 50.0, 100.0, 150.0, -25.0, 75.5, 0.5] {
1134            let p = PercentageValue::new(v);
1135            let s = p.to_string();
1136            assert!(s.ends_with('%'), "Display lost the percent sign: {s:?}");
1137            let back = parse_percentage_value(&s)
1138                .unwrap_or_else(|e| panic!("Display of {p:?} ({s:?}) did not re-parse: {e}"));
1139            assert!(
1140                (back.normalized() - p.normalized()).abs() < 1e-4,
1141                "round-trip drifted: {p:?} -> {s:?} -> {back:?}"
1142            );
1143        }
1144    }
1145
1146    // ----------------------------------------------------- parse_float_value ---
1147
1148    #[test]
1149    fn parse_float_value_positive_control() {
1150        assert_eq!(parse_float_value("0").unwrap().number(), 0);
1151        assert_eq!(parse_float_value("1.5").unwrap().number(), 1500);
1152        assert_eq!(parse_float_value("-1.5").unwrap().number(), -1500);
1153        assert_eq!(parse_float_value("+2").unwrap().number(), 2000);
1154        // Rust's f32 parser accepts these shorthand forms.
1155        assert_eq!(parse_float_value(".5").unwrap().number(), 500);
1156        assert_eq!(parse_float_value("5.").unwrap().number(), 5000);
1157    }
1158
1159    #[test]
1160    fn parse_float_value_rejects_empty_and_whitespace() {
1161        assert!(parse_float_value("").is_err());
1162        assert!(parse_float_value("   ").is_err());
1163        assert!(parse_float_value("\t\n\r ").is_err());
1164    }
1165
1166    #[test]
1167    fn parse_float_value_rejects_garbage() {
1168        for input in [
1169            "abc",
1170            "1_000",
1171            "1,5",
1172            "0x10",
1173            "1.2.3",
1174            "--1",
1175            "1e",
1176            "e5",
1177            "5 5",
1178            "1/2",
1179            ";",
1180            "\0",
1181            "5;garbage",
1182            "50px",
1183            "5%",
1184        ] {
1185            assert!(
1186                parse_float_value(input).is_err(),
1187                "garbage input {input:?} was accepted"
1188            );
1189        }
1190    }
1191
1192    #[test]
1193    fn parse_float_value_trims_but_does_not_tolerate_inner_junk() {
1194        assert_eq!(parse_float_value("  1.5  ").unwrap().number(), 1500);
1195        assert!(parse_float_value("1.5 garbage").is_err());
1196    }
1197
1198    #[test]
1199    fn parse_float_value_boundary_numbers_saturate_instead_of_panicking() {
1200        // -0 must not leak a negative zero out of the encoding.
1201        assert_eq!(parse_float_value("-0").unwrap().number(), 0);
1202        assert!(parse_float_value("-0").unwrap().get().is_sign_positive());
1203
1204        // Rust parses "NaN"/"inf" successfully; the encoding must then defuse them.
1205        assert_eq!(parse_float_value("NaN").unwrap().number(), 0);
1206        assert_eq!(parse_float_value("inf").unwrap().number(), isize::MAX);
1207        assert_eq!(parse_float_value("infinity").unwrap().number(), isize::MAX);
1208        assert_eq!(parse_float_value("-inf").unwrap().number(), isize::MIN);
1209
1210        // Overflow of the f32 parse itself is Ok(inf) in Rust, then saturates.
1211        assert_eq!(parse_float_value("1e400").unwrap().number(), isize::MAX);
1212        assert_eq!(parse_float_value("-1e400").unwrap().number(), isize::MIN);
1213        // Underflow is Ok(0.0).
1214        assert_eq!(parse_float_value("1e-400").unwrap().number(), 0);
1215
1216        // i64::MAX / f64::MAX as literals: no panic, still finite after decode.
1217        for input in [
1218            "9223372036854775807",
1219            "-9223372036854775808",
1220            "179769313486231570000000000000000000000000000000000",
1221        ] {
1222            let v = parse_float_value(input)
1223                .unwrap_or_else(|e| panic!("{input:?} should parse as f32, got {e}"));
1224            assert!(v.get().is_finite(), "{input:?} decoded to {}", v.get());
1225        }
1226    }
1227
1228    #[test]
1229    fn parse_float_value_unicode_does_not_panic() {
1230        // Multi-byte input must be rejected, never sliced mid-codepoint.
1231        for input in [
1232            "\u{1F600}",  // emoji
1233            "5\u{1F600}", // digit + emoji
1234            "\u{0665}",   // ARABIC-INDIC DIGIT FIVE (is_numeric() == true)
1235            "5\u{0301}",  // digit + combining acute
1236            "\u{00BD}",   // ½ (No category, is_numeric() == true)
1237            "\u{FF15}",   // FULLWIDTH DIGIT FIVE
1238            "\u{200B}5",  // zero-width space + digit
1239            "\u{2212}5",  // U+2212 MINUS SIGN (not ASCII '-')
1240        ] {
1241            assert!(
1242                parse_float_value(input).is_err(),
1243                "non-ASCII input {input:?} was accepted as a float"
1244            );
1245        }
1246    }
1247
1248    #[test]
1249    fn parse_float_value_extremely_long_input_terminates() {
1250        // 200k digits: must not hang, must not panic; Rust yields Ok(inf), which
1251        // then saturates in the encoding.
1252        let huge = "9".repeat(200_000);
1253        // Rejecting is acceptable too — just don't panic/hang on the huge input.
1254        if let Ok(v) = parse_float_value(&huge) {
1255            assert!(v.get().is_finite(), "200k digits decoded to {}", v.get());
1256        }
1257
1258        // Long *garbage* must be rejected rather than scanned quadratically.
1259        let long_junk = "a".repeat(200_000);
1260        assert!(parse_float_value(&long_junk).is_err());
1261    }
1262
1263    #[test]
1264    fn parse_float_value_deeply_nested_input_does_not_stack_overflow() {
1265        let nested = "(".repeat(10_000);
1266        assert!(parse_float_value(&nested).is_err());
1267        let nested_pair = format!("{}5{}", "(".repeat(10_000), ")".repeat(10_000));
1268        assert!(parse_float_value(&nested_pair).is_err());
1269    }
1270
1271    // ------------------------------------------------ parse_percentage_value ---
1272
1273    #[test]
1274    fn parse_percentage_value_positive_control() {
1275        assert_eq!(parse_percentage_value("50%").unwrap().normalized(), 0.5);
1276        assert_eq!(parse_percentage_value("0%").unwrap().normalized(), 0.0);
1277        assert_eq!(parse_percentage_value("-25%").unwrap().normalized(), -0.25);
1278        // A bare number is a *ratio*, not a percent: "0.5" == "50%".
1279        assert_eq!(
1280            parse_percentage_value("0.5").unwrap(),
1281            parse_percentage_value("50%").unwrap()
1282        );
1283    }
1284
1285    #[test]
1286    fn parse_percentage_value_bare_number_is_multiplied_by_a_hundred() {
1287        // Easy to misread: "50" (no sign) is 5000%, not 50%.
1288        assert_eq!(parse_percentage_value("50").unwrap().normalized(), 50.0);
1289        assert_ne!(
1290            parse_percentage_value("50").unwrap(),
1291            parse_percentage_value("50%").unwrap()
1292        );
1293    }
1294
1295    #[test]
1296    fn parse_percentage_value_rejects_empty_and_whitespace() {
1297        assert!(matches!(
1298            parse_percentage_value(""),
1299            Err(PercentageParseError::ValueParseErr(_))
1300        ));
1301        assert!(matches!(
1302            parse_percentage_value("   "),
1303            Err(PercentageParseError::ValueParseErr(_))
1304        ));
1305        assert!(matches!(
1306            parse_percentage_value("\t\n"),
1307            Err(PercentageParseError::ValueParseErr(_))
1308        ));
1309        assert!(parse_percentage_value("%").is_err());
1310    }
1311
1312    #[test]
1313    fn parse_percentage_value_rejects_garbage_without_panicking() {
1314        for input in [
1315            "abc", "fifty%", "%50", "50%%", "5 0 %", "--5%", "1.2.3%", ";", "\0", "NaN", "inf",
1316            "-inf",
1317        ] {
1318            assert!(
1319                parse_percentage_value(input).is_err(),
1320                "garbage input {input:?} was accepted"
1321            );
1322        }
1323    }
1324
1325    #[test]
1326    fn parse_percentage_value_reports_invalid_units() {
1327        for (input, unit) in [("50px", "px"), ("50em", "em"), ("1.5rem", "rem")] {
1328            match parse_percentage_value(input) {
1329                Err(PercentageParseError::InvalidUnit(u)) => assert_eq!(u.as_str(), unit),
1330                other => panic!("{input:?} should be InvalidUnit({unit:?}), got {other:?}"),
1331            }
1332        }
1333    }
1334
1335    #[test]
1336    fn parse_percentage_value_trims_leading_and_trailing_whitespace() {
1337        assert_eq!(
1338            parse_percentage_value("  75.5%  ").unwrap().normalized(),
1339            0.755
1340        );
1341        // Whitespace *between* the number and the unit is trimmed as well.
1342        assert_eq!(parse_percentage_value("50 %").unwrap().normalized(), 0.5);
1343    }
1344
1345    #[test]
1346    fn parse_percentage_value_boundary_numbers_stay_finite() {
1347        // -0 must not leak a negative zero.
1348        let neg_zero = parse_percentage_value("-0%").unwrap();
1349        assert_eq!(neg_zero.normalized(), 0.0);
1350        assert!(neg_zero.normalized().is_sign_positive());
1351
1352        // Overflowing exponent parses to inf, then saturates in the encoding.
1353        let huge = parse_percentage_value("1e400%").unwrap();
1354        assert!(
1355            huge.normalized().is_finite(),
1356            "1e400% leaked {}",
1357            huge.normalized()
1358        );
1359        let huge_neg = parse_percentage_value("-1e400%").unwrap();
1360        assert!(huge_neg.normalized().is_finite());
1361        // Underflowing exponent parses to 0.
1362        assert_eq!(parse_percentage_value("1e-400%").unwrap().normalized(), 0.0);
1363
1364        // i64::MAX-sized literal: no panic, still finite.
1365        let big = parse_percentage_value("9223372036854775807%").unwrap();
1366        assert!(big.normalized().is_finite());
1367    }
1368
1369    #[test]
1370    fn parse_percentage_value_ascii_unicode_neighbours_do_not_panic() {
1371        // Multi-byte chars that are NOT `char::is_numeric()` are safe to slice
1372        // around; they must be rejected, not panic.
1373        for input in [
1374            "\u{1F600}",   // emoji only
1375            "50\u{1F600}", // digits then emoji -> InvalidUnit
1376            "\u{20AC}50",  // €50 -> unparseable number
1377            "abc\u{00E9}%",
1378            "\u{200B}%", // zero-width space
1379        ] {
1380            assert!(
1381                parse_percentage_value(input).is_err(),
1382                "{input:?} was accepted"
1383            );
1384        }
1385        // The emoji suffix is reported as an invalid unit, not a parse error.
1386        assert!(matches!(
1387            parse_percentage_value("50\u{1F600}"),
1388            Err(PercentageParseError::InvalidUnit(_))
1389        ));
1390    }
1391
1392    #[test]
1393    fn parse_percentage_value_extremely_long_input_terminates() {
1394        let huge = format!("{}%", "9".repeat(200_000));
1395        if let Ok(v) = parse_percentage_value(&huge) {
1396            assert!(v.normalized().is_finite())
1397        }
1398        let long_junk = format!("{}%", "a".repeat(200_000));
1399        assert!(parse_percentage_value(&long_junk).is_err());
1400    }
1401
1402    #[test]
1403    fn parse_percentage_value_deeply_nested_input_does_not_stack_overflow() {
1404        assert!(parse_percentage_value(&"(".repeat(10_000)).is_err());
1405        // A numeric char buried behind 10k brackets: the scanner must still just
1406        // split and fail on the number, not recurse.
1407        let nested = format!("{}5%", "(".repeat(10_000));
1408        assert!(parse_percentage_value(&nested).is_err());
1409    }
1410
1411    // --------------------------------------------- PercentageParseError glue ---
1412
1413    #[test]
1414    fn percentage_parse_error_round_trips_through_owned() {
1415        let variants = [
1416            PercentageParseError::ValueParseErr(CssParseFloatError::Empty),
1417            PercentageParseError::ValueParseErr(CssParseFloatError::Invalid),
1418            PercentageParseError::NoPercentSign,
1419            PercentageParseError::InvalidUnit(String::new().into()),
1420            PercentageParseError::InvalidUnit("px".to_string().into()),
1421            // A unit that is itself multi-byte must survive the AzString clone.
1422            PercentageParseError::InvalidUnit("\u{1F600}".to_string().into()),
1423        ];
1424        for e in variants {
1425            let round_tripped = e.to_contained().to_shared();
1426            assert_eq!(
1427                e, round_tripped,
1428                "to_contained/to_shared is not the identity for {e:?}"
1429            );
1430        }
1431    }
1432
1433    #[test]
1434    fn percentage_parse_error_owned_round_trips_through_shared() {
1435        let variants = [
1436            PercentageParseErrorOwned::ValueParseErr(CssParseFloatError::Invalid),
1437            PercentageParseErrorOwned::NoPercentSign,
1438            PercentageParseErrorOwned::InvalidUnit("vh".to_string().into()),
1439        ];
1440        for e in variants {
1441            assert_eq!(e.to_shared().to_contained(), e);
1442        }
1443    }
1444
1445    #[test]
1446    fn percentage_parse_error_display_is_non_empty() {
1447        // Debug forwards to Display (impl_debug_as_display); neither may be empty
1448        // nor panic, including for an empty invalid unit.
1449        for e in [
1450            PercentageParseError::ValueParseErr(CssParseFloatError::Empty),
1451            PercentageParseError::NoPercentSign,
1452            PercentageParseError::InvalidUnit(String::new().into()),
1453        ] {
1454            let shown = e.to_string();
1455            assert!(!shown.is_empty(), "{e:?} renders as an empty message");
1456            assert_eq!(format!("{e:?}"), shown);
1457        }
1458    }
1459
1460    // ---------------------------------------------------- former known bugs ---
1461    //
1462    // The two regression tests below pin behaviour these functions used to get
1463    // wrong (a multi-byte-digit slice panic and a fraction escaping [0, 1)).
1464    // Both are now fixed and asserted un-ignored.
1465
1466    #[test]
1467    fn known_bug_percentage_multibyte_numeric_char_panics() {
1468        // `char::is_numeric()` is true for Nd/Nl/No — including multi-byte chars
1469        // like '½' (U+00BD, 2 bytes) and '٥' (U+0665, 2 bytes). The scanner
1470        // records their *start* byte index, then slices at `split_pos + 1`, which
1471        // lands inside the codepoint => `input[split_pos..]` panics.
1472        //
1473        // Reachable from any author stylesheet (`width: ½%`), so this panics the
1474        // CSS parser on untrusted input.
1475        for input in ["\u{00BD}%", "\u{0665}%", "5\u{00BD}", "\u{FF15}%"] {
1476            assert!(
1477                parse_percentage_value(input).is_err(),
1478                "{input:?} should be rejected"
1479            );
1480        }
1481    }
1482
1483    #[test]
1484    #[cfg(target_pointer_width = "64")]
1485    fn known_bug_const_new_fractional_huge_post_comma_escapes_the_fraction() {
1486        // The digit-count ladder's last arm divides by 10_000_000, which only
1487        // truncates a 10-digit post_comma down to 3 digits. An 11-digit value
1488        // keeps 4 digits, a 12-digit value keeps 5, ... so the "fractional" part
1489        // grows past 1.0 and corrupts the integer part.
1490        for post in [12_345_678_901_isize, 123_456_789_012, isize::MAX] {
1491            let frac = FloatValue::const_new_fractional(0, post).get();
1492            assert!(
1493                (0.0..1.0).contains(&frac),
1494                "const_new_fractional(0, {post}) produced {frac}, which is not a fraction"
1495            );
1496        }
1497    }
1498}