use cssparser::{Parser, ParserInput, Token}; pub fn normalize_root_margin(value: &str) -> Option { if value.trim().is_empty() { return Some("0px 0px 0px 0px".to_owned()); } let mut input = ParserInput::new(value); let mut parser = Parser::new(&mut input); let raw_parts = parser.parse_entirely(parse_root_margin_components).ok()?; let parts: Vec<&str> = match raw_parts.as_slice() { [] => return None, [a] => vec![a.as_str(), a.as_str(), a.as_str(), a.as_str()], [a, b] => vec![a.as_str(), b.as_str(), a.as_str(), b.as_str()], [a, b, c] => vec![a.as_str(), b.as_str(), c.as_str(), b.as_str()], [a, b, c, d] => vec![a.as_str(), b.as_str(), c.as_str(), d.as_str()], _ => return None, }; Some(parts.join(" ")) } pub fn root_margin_components(value: &str, root_width: f64) -> [f64; 4] { let mut result = [0.0; 4]; for (index, part) in value.split_ascii_whitespace().take(4).enumerate() { result[index] = if let Some(number) = part.strip_suffix("px") { number.parse::().unwrap_or(0.0) } else if let Some(number) = part.strip_suffix('%') { number .parse::() .map(|percentage| (percentage / 100.0) * root_width) .unwrap_or(0.0) } else { 0.0 }; } result } fn parse_root_margin_components<'i, 't>( input: &mut Parser<'i, 't>, ) -> Result, cssparser::ParseError<'i, ()>> { let mut parts = Vec::new(); while !input.is_exhausted() { if parts.len() == 4 { return Err(input.new_custom_error(())); } parts.push(parse_root_margin_component(input)?); } if parts.is_empty() { return Err(input.new_custom_error(())); } Ok(parts) } fn parse_root_margin_component<'i, 't>( input: &mut Parser<'i, 't>, ) -> Result> { match input.next()?.clone() { Token::Number { value: 0.0, .. } => Ok("0px".to_owned()), Token::Dimension { value, unit, .. } if unit.eq_ignore_ascii_case("px") => { format_css_number(value) .map(|number| format!("{number}px")) .ok_or_else(|| input.new_custom_error(())) } Token::Percentage { unit_value, .. } => format_css_number(unit_value * 100.0) .map(|number| format!("{number}%")) .ok_or_else(|| input.new_custom_error(())), _ => Err(input.new_custom_error(())), } } fn format_css_number(value: f32) -> Option { if !value.is_finite() { return None; } if value == 0.0 { return Some("0".to_owned()); } let mut text = value.to_string(); if let Some(dot) = text.find('.') { while text.ends_with('0') { text.pop(); } if text.len() == dot + 1 { text.pop(); } } Some(text) } #[cfg(test)] mod tests { use super::{normalize_root_margin, root_margin_components}; #[test] fn parser_uses_cssparser_tokens() { assert_eq!( normalize_root_margin("").as_deref(), Some("0px 0px 0px 0px") ); assert_eq!( normalize_root_margin(" \n\t").as_deref(), Some("0px 0px 0px 0px") ); assert_eq!( normalize_root_margin("10px /*comment*/ 5% 0 -2.5px").as_deref(), Some("10px 5% 0px -2.5px") ); assert_eq!( normalize_root_margin("\n1PX\t2px").as_deref(), Some("1px 2px 1px 2px") ); } #[test] fn parser_rejects_non_margin_tokens() { assert_eq!(normalize_root_margin("calc(1px)").as_deref(), None); assert_eq!(normalize_root_margin("1px, 2px").as_deref(), None); assert_eq!(normalize_root_margin("1em").as_deref(), None); assert_eq!( normalize_root_margin("1px 2px 3px 4px 5px").as_deref(), None ); } #[test] fn components_resolve_percentages_against_root_width() { assert_eq!( root_margin_components("10px 5% 0px -2.5px", 200.0), [10.0, 10.0, 0.0, -2.5] ); } }