use crate::keyword_set::ascii_keyword_set;
use std::borrow::Cow;
pub(crate) fn normalize_dimension_from_source(raw: &str) -> Cow<'_, str> {
let (num_part, unit_part) = split_number_and_unit(raw);
if num_part.is_empty() {
return Cow::Borrowed(raw);
}
let normalized_num = normalize_css_number(num_part);
let unit = canonical_unit(unit_part);
match (normalized_num, unit) {
(Cow::Borrowed(_), Cow::Borrowed(_)) => Cow::Borrowed(raw),
(num, unit) => Cow::Owned(format!("{num}{unit}")),
}
}
fn split_number_and_unit(raw: &str) -> (&str, &str) {
raw.split_at(crate::number::number_part_len(raw))
}
pub(crate) fn normalize_css_number(num: &str) -> Cow<'_, str> {
let Some(e_idx) = num.as_bytes().iter().position(|&b| b == b'e' || b == b'E') else {
return normalize_decimal_preserving_prefix(num);
};
let mantissa = &num[..e_idx];
let exponent = &num[e_idx + 1..];
let normalized_mantissa = normalize_decimal_preserving_prefix(mantissa);
let (exp_sign, exp_digits) = if let Some(rest) = exponent.strip_prefix('-') {
("-", rest)
} else if let Some(rest) = exponent.strip_prefix('+') {
("", rest)
} else {
("", exponent)
};
let trimmed_digits = exp_digits.trim_start_matches('0');
if trimmed_digits.is_empty() {
return Cow::Owned(normalized_mantissa.into_owned());
}
Cow::Owned(format!("{normalized_mantissa}e{exp_sign}{trimmed_digits}"))
}
ascii_keyword_set! {
static CSS_UNITS;
pub(crate) fn is_known_css_unit;
"px", "cm", "mm", "in", "pt", "pc", "q",
"em", "rem", "ex", "rex", "ch", "rch", "cap", "rcap", "ic", "ric", "lh", "rlh",
"vw", "vh", "vi", "vb", "vmin", "vmax",
"svw", "svh", "svi", "svb", "svmin", "svmax",
"lvw", "lvh", "lvi", "lvb", "lvmin", "lvmax",
"dvw", "dvh", "dvi", "dvb", "dvmin", "dvmax",
"cqw", "cqh", "cqi", "cqb", "cqmin", "cqmax",
"deg", "grad", "rad", "turn",
"s", "ms",
"hz", "khz",
"dpi", "dpcm", "dppx", "x",
"fr",
}
#[inline]
pub(crate) fn canonical_unit(unit: &str) -> Cow<'_, str> {
if !unit.bytes().any(|b| b.is_ascii_uppercase()) {
return Cow::Borrowed(unit);
}
canonical_cased_unit(unit)
}
#[cold]
#[inline(never)]
fn canonical_cased_unit(unit: &str) -> Cow<'_, str> {
if !is_known_css_unit(unit) {
return Cow::Borrowed(unit);
}
Cow::Owned(unit.to_ascii_lowercase())
}
fn normalize_decimal_preserving_prefix(num: &str) -> Cow<'_, str> {
let (sign, rest) = if let Some(stripped) = num.strip_prefix('-') {
("-", stripped)
} else if let Some(stripped) = num.strip_prefix('+') {
("+", stripped)
} else {
("", num)
};
let needs_leading_zero = rest.starts_with('.');
let bytes = rest.as_bytes();
let mut trim_to = rest.len();
if rest.contains('.') {
while trim_to > 0 && bytes[trim_to - 1] == b'0' {
trim_to -= 1;
}
if trim_to > 0 && bytes[trim_to - 1] == b'.' {
trim_to -= 1;
}
}
if !needs_leading_zero && trim_to == rest.len() {
return Cow::Borrowed(num);
}
let leading = if needs_leading_zero { "0" } else { "" };
Cow::Owned(format!("{sign}{leading}{}", &rest[..trim_to]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canonical_unit() {
assert_eq!(canonical_unit("PX"), "px");
assert_eq!(canonical_unit("DEG"), "deg");
assert_eq!(canonical_unit("Turn"), "turn");
assert_eq!(canonical_unit("FR"), "fr");
assert!(matches!(canonical_unit("px"), Cow::Borrowed("px")));
assert_eq!(canonical_unit("HZ"), "hz");
assert_eq!(canonical_unit("KHZ"), "khz");
assert_eq!(canonical_unit("Q"), "q");
assert!(matches!(canonical_unit("hz"), Cow::Borrowed("hz")));
assert!(matches!(canonical_unit("khz"), Cow::Borrowed("khz")));
assert!(matches!(canonical_unit("q"), Cow::Borrowed("q")));
assert_eq!(canonical_unit("FOO"), "FOO");
assert_eq!(canonical_unit(""), "");
}
#[test]
fn test_normalize_css_number_table() {
assert_eq!(normalize_css_number(".5"), "0.5");
assert_eq!(normalize_css_number("5."), "5");
assert_eq!(normalize_css_number("1.50"), "1.5");
assert_eq!(normalize_css_number("00.500"), "00.5");
assert_eq!(normalize_css_number("-0.0"), "-0");
assert_eq!(normalize_css_number("5e0"), "5");
assert_eq!(normalize_css_number("1e+0010"), "1e10");
assert_eq!(normalize_css_number("1.5E-3"), "1.5e-3");
assert_eq!(normalize_css_number("1e"), "1");
}
#[test]
fn test_normalize_css_number_borrows_when_canonical() {
for canonical in ["0", "5", "123", "10", "0.5", "1.5", "00.5", "-0", "+10"] {
assert!(
matches!(normalize_css_number(canonical), Cow::Borrowed(_)),
"canonical {canonical:?} must borrow"
);
}
for rewritten in [".5", "5.", "1.50", "-0.0", "5e0", "1e+0010", "1.5E-3", "1e"] {
assert!(
matches!(normalize_css_number(rewritten), Cow::Owned(_)),
"rewritten {rewritten:?} must own"
);
}
}
#[test]
fn test_normalize_dimension_borrows_when_canonical() {
for canonical in ["10px", "0.5rem", "100", "1.5", "0", "-0", "+10em"] {
assert!(
matches!(normalize_dimension_from_source(canonical), Cow::Borrowed(_)),
"canonical dimension {canonical:?} must borrow"
);
}
for rewritten in [".5px", "1.50rem", "10PX", "5e0"] {
assert!(
matches!(normalize_dimension_from_source(rewritten), Cow::Owned(_)),
"rewritten dimension {rewritten:?} must own"
);
}
assert!(matches!(
normalize_dimension_from_source("auto"),
Cow::Borrowed("auto")
));
}
}