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