Files
moli/moli-css-parse/src/root_margin.rs
T
2026-08-11 00:10:12 +08:00

134 lines
4.1 KiB
Rust

use cssparser::{Parser, ParserInput, Token};
pub fn normalize_root_margin(value: &str) -> Option<String> {
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::<f64>().unwrap_or(0.0)
} else if let Some(number) = part.strip_suffix('%') {
number
.parse::<f64>()
.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<Vec<String>, 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<String, cssparser::ParseError<'i, ()>> {
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<String> {
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]
);
}
}