Skip to content

Commit aec3025

Browse files
committed
Enhance component serialization with inf and nan support
Modern syntax colors support NaN and infinity values. These values are now allowed, where previously for some lab/lch/oklab/oklch components, NaN values were truncated to 0.0. NaN and infinity now also correctly serializes to calc(NaN) and calc(infinity) respectively.
1 parent 2fd15ba commit aec3025

File tree

1 file changed

+100
-22
lines changed

1 file changed

+100
-22
lines changed

src/color.rs

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,6 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
1616

1717
const OPAQUE: f32 = 1.0;
1818

19-
fn serialize_none_or<T>(dest: &mut impl fmt::Write, value: &Option<T>) -> fmt::Result
20-
where
21-
T: ToCss,
22-
{
23-
match value {
24-
Some(v) => v.to_css(dest),
25-
None => dest.write_str("none"),
26-
}
27-
}
28-
2919
/// Serialize the alpha copmonent of a color according to the specification.
3020
/// <https://drafts.csswg.org/css-color-4/#serializing-alpha-values>
3121
#[inline]
@@ -55,6 +45,33 @@ pub fn serialize_color_alpha(
5545
rounded_alpha.to_css(dest)
5646
}
5747

48+
/// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and
49+
/// floating point values.
50+
struct ModernComponent<'a>(&'a Option<f32>);
51+
52+
impl<'a> ToCss for ModernComponent<'a> {
53+
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
54+
where
55+
W: fmt::Write,
56+
{
57+
if let Some(value) = self.0 {
58+
if value.is_infinite() {
59+
if value.is_sign_negative() {
60+
dest.write_str("calc(-infinity)")
61+
} else {
62+
dest.write_str("calc(infinity)")
63+
}
64+
} else if value.is_nan() {
65+
dest.write_str("calc(NaN)")
66+
} else {
67+
value.to_css(dest)
68+
}
69+
} else {
70+
dest.write_str("none")
71+
}
72+
}
73+
}
74+
5875
// Guaratees hue in [0..360)
5976
fn normalize_hue(hue: f32) -> f32 {
6077
// <https://drafts.csswg.org/css-values/#angles>
@@ -363,11 +380,11 @@ macro_rules! impl_lab_like {
363380
{
364381
dest.write_str($fname)?;
365382
dest.write_str("(")?;
366-
serialize_none_or(dest, &self.lightness)?;
383+
ModernComponent(&self.lightness).to_css(dest)?;
367384
dest.write_char(' ')?;
368-
serialize_none_or(dest, &self.a)?;
385+
ModernComponent(&self.a).to_css(dest)?;
369386
dest.write_char(' ')?;
370-
serialize_none_or(dest, &self.b)?;
387+
ModernComponent(&self.b).to_css(dest)?;
371388
serialize_color_alpha(dest, self.alpha, false)?;
372389
dest.write_char(')')
373390
}
@@ -454,11 +471,11 @@ macro_rules! impl_lch_like {
454471
{
455472
dest.write_str($fname)?;
456473
dest.write_str("(")?;
457-
serialize_none_or(dest, &self.lightness)?;
474+
ModernComponent(&self.lightness).to_css(dest)?;
458475
dest.write_char(' ')?;
459-
serialize_none_or(dest, &self.chroma)?;
476+
ModernComponent(&self.chroma).to_css(dest)?;
460477
dest.write_char(' ')?;
461-
serialize_none_or(dest, &self.hue)?;
478+
ModernComponent(&self.hue).to_css(dest)?;
462479
serialize_color_alpha(dest, self.alpha, false)?;
463480
dest.write_char(')')
464481
}
@@ -579,11 +596,11 @@ impl ToCss for ColorFunction {
579596
dest.write_str("color(")?;
580597
self.color_space.to_css(dest)?;
581598
dest.write_char(' ')?;
582-
serialize_none_or(dest, &self.c1)?;
599+
ModernComponent(&self.c1).to_css(dest)?;
583600
dest.write_char(' ')?;
584-
serialize_none_or(dest, &self.c2)?;
601+
ModernComponent(&self.c2).to_css(dest)?;
585602
dest.write_char(' ')?;
586-
serialize_none_or(dest, &self.c3)?;
603+
ModernComponent(&self.c3).to_css(dest)?;
587604

588605
serialize_color_alpha(dest, self.alpha, false)?;
589606

@@ -1473,6 +1490,15 @@ pub fn hsl_to_rgb(hue: f32, saturation: f32, lightness: f32) -> (f32, f32, f32)
14731490
(red, green, blue)
14741491
}
14751492

1493+
#[inline]
1494+
fn max_preserve_nan(value: f32, max: f32) -> f32 {
1495+
if value.is_nan() {
1496+
value
1497+
} else {
1498+
value.max(max)
1499+
}
1500+
}
1501+
14761502
type IntoColorFn<Output> =
14771503
fn(l: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>) -> Output;
14781504

@@ -1495,7 +1521,7 @@ where
14951521
P::parse_number_or_percentage,
14961522
)?;
14971523

1498-
let lightness = lightness.map(|l| l.value(lightness_range).max(0.0));
1524+
let lightness = lightness.map(|l| max_preserve_nan(l.value(lightness_range), 0.0));
14991525
let a = a.map(|a| a.value(a_b_range));
15001526
let b = b.map(|b| b.value(a_b_range));
15011527

@@ -1521,8 +1547,8 @@ where
15211547
P::parse_angle_or_number,
15221548
)?;
15231549

1524-
let lightness = lightness.map(|l| l.value(lightness_range).max(0.0));
1525-
let chroma = chroma.map(|c| c.value(chroma_range).max(0.0));
1550+
let lightness = lightness.map(|l| max_preserve_nan(l.value(lightness_range), 0.0));
1551+
let chroma = chroma.map(|c| max_preserve_nan(c.value(chroma_range), 0.0));
15261552
let hue = hue.map(|h| normalize_hue(h.degrees()));
15271553

15281554
Ok(into_color(lightness, chroma, hue, alpha))
@@ -1591,3 +1617,55 @@ where
15911617

15921618
Ok((r1, r2, r3, alpha))
15931619
}
1620+
1621+
#[cfg(test)]
1622+
mod tests {
1623+
use super::*;
1624+
1625+
#[test]
1626+
fn serialize_modern_components() {
1627+
// None.
1628+
assert_eq!(ModernComponent(&None).to_css_string(), "none".to_string());
1629+
1630+
// Finite values.
1631+
assert_eq!(
1632+
ModernComponent(&Some(10.0)).to_css_string(),
1633+
"10".to_string()
1634+
);
1635+
assert_eq!(
1636+
ModernComponent(&Some(-10.0)).to_css_string(),
1637+
"-10".to_string()
1638+
);
1639+
assert_eq!(ModernComponent(&Some(0.0)).to_css_string(), "0".to_string());
1640+
assert_eq!(
1641+
ModernComponent(&Some(-0.0)).to_css_string(),
1642+
"0".to_string()
1643+
);
1644+
1645+
// Infinite values.
1646+
assert_eq!(
1647+
ModernComponent(&Some(f32::INFINITY)).to_css_string(),
1648+
"calc(infinity)".to_string()
1649+
);
1650+
assert_eq!(
1651+
ModernComponent(&Some(f32::NEG_INFINITY)).to_css_string(),
1652+
"calc(-infinity)".to_string()
1653+
);
1654+
1655+
// NaN.
1656+
assert_eq!(
1657+
ModernComponent(&Some(f32::NAN)).to_css_string(),
1658+
"calc(NaN)".to_string()
1659+
);
1660+
}
1661+
1662+
#[test]
1663+
fn max_preserve_nan_preserves_nan() {
1664+
assert!(max_preserve_nan(f32::NAN, 0.0).is_nan());
1665+
assert_eq!(max_preserve_nan(10.0, 0.0), 10.0);
1666+
assert_eq!(max_preserve_nan(-10.0, 0.0), 0.0);
1667+
assert_eq!(max_preserve_nan(-10.0, -5.0), -5.0);
1668+
assert_eq!(max_preserve_nan(f32::INFINITY, 0.0), f32::INFINITY);
1669+
assert_eq!(max_preserve_nan(f32::NEG_INFINITY, 0.0), 0.0);
1670+
}
1671+
}

0 commit comments

Comments
 (0)