Skip to main content

whisker_css/
to_css.rs

1//! The `ToCss` trait — formats a value to its canonical CSS form.
2
3use core::fmt;
4
5/// Format a value as the CSS source text Lynx will parse.
6///
7/// Implementors must write a string that, when fed back into Lynx's
8/// CSS parser for the property accepting the value, produces an
9/// equivalent computed value. Whitespace, casing, and unit choice
10/// follow the canonical CSS form documented at
11/// <https://lynxjs.org/api/css>.
12///
13/// A blanket [`fmt::Display`] is **not** provided automatically so
14/// implementors stay explicit about which surface (CSS-text vs.
15/// debug) they're writing.
16pub trait ToCss {
17    /// Write the CSS representation of `self` into `dest`.
18    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result;
19
20    /// Convenience: allocate a fresh [`String`] and write into it.
21    fn to_css_string(&self) -> String {
22        let mut s = String::new();
23        // Writing into a `String` cannot fail in practice (no I/O).
24        let _ = self.to_css(&mut s);
25        s
26    }
27}
28
29impl<T: ToCss + ?Sized> ToCss for &T {
30    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
31        (**self).to_css(dest)
32    }
33}
34
35/// Render an `f32` without a trailing `.0` for integer values so the
36/// emitted CSS stays compact.
37pub(crate) fn write_number(dest: &mut dyn fmt::Write, n: f32) -> fmt::Result {
38    if n.fract() == 0.0 && n.is_finite() && n.abs() < 1e16 {
39        write!(dest, "{}", n as i64)
40    } else {
41        write!(dest, "{n}")
42    }
43}
44
45/// Convenience: render an `f32` to a fresh [`String`] using the same
46/// rules as [`write_number`].
47pub(crate) fn number_to_string(n: f32) -> String {
48    let mut s = String::new();
49    let _ = write_number(&mut s, n);
50    s
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    /// A tiny helper type to exercise the blanket [`ToCss`] impl on
58    /// references.
59    struct Token(&'static str);
60    impl ToCss for Token {
61        fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
62            dest.write_str(self.0)
63        }
64    }
65
66    #[test]
67    fn write_number_drops_decimal_for_integers() {
68        let mut buf = String::new();
69        write_number(&mut buf, 1.0).unwrap();
70        assert_eq!(buf, "1");
71        buf.clear();
72        write_number(&mut buf, -3.0).unwrap();
73        assert_eq!(buf, "-3");
74        buf.clear();
75        write_number(&mut buf, 0.0).unwrap();
76        assert_eq!(buf, "0");
77    }
78
79    #[test]
80    fn write_number_keeps_decimal_for_fractions() {
81        let mut buf = String::new();
82        write_number(&mut buf, 0.5).unwrap();
83        assert_eq!(buf, "0.5");
84        buf.clear();
85        write_number(&mut buf, -1.25).unwrap();
86        assert_eq!(buf, "-1.25");
87    }
88
89    #[test]
90    fn write_number_handles_non_finite_safely() {
91        let mut buf = String::new();
92        // Non-finite values fall through to `{n}` formatting; the
93        // helper just needs to not panic and to keep something on
94        // the buffer.
95        let _ = write_number(&mut buf, f32::NAN);
96        let _ = write_number(&mut buf, f32::INFINITY);
97        // Smoke check — output is non-empty.
98        assert!(!buf.is_empty());
99    }
100
101    #[test]
102    fn write_number_handles_huge_floats() {
103        let mut buf = String::new();
104        // Values past the safe-integer threshold fall through to
105        // floating-point formatting.
106        write_number(&mut buf, 1e20).unwrap();
107        assert!(!buf.is_empty());
108    }
109
110    #[test]
111    fn number_to_string_matches_write_number() {
112        assert_eq!(number_to_string(1.0), "1");
113        assert_eq!(number_to_string(0.25), "0.25");
114        assert_eq!(number_to_string(-2.5), "-2.5");
115    }
116
117    #[test]
118    fn to_css_blanket_reference_impl() {
119        // Verify `&T: ToCss` where `T: ToCss` works.
120        let t = Token("ident");
121        let r: &Token = &t;
122        let s = r.to_css_string();
123        assert_eq!(s, "ident");
124    }
125
126    #[test]
127    fn to_css_string_uses_the_same_path() {
128        let t = Token("abc");
129        assert_eq!(t.to_css_string(), "abc");
130    }
131}