pub use style::moli_numeric::{ ContainerQueryLengthContext, CssNumericContext, CssNumericKind, CssNumericValue, UnitlessAngle, UnitlessLength, css_number_value_is_supported, css_numeric_value_is_supported, css_time_value_is_supported, parse_angle_degrees, parse_number, parse_px_length, resolve_css_number, resolve_css_numeric, resolve_length_percentage, resolve_time_seconds, starts_with_supported_math_function, }; pub fn balanced_function_len(raw: &str) -> Option { let mut depth = 0usize; for (index, ch) in raw.char_indices() { match ch { '(' => depth += 1, ')' => { depth = depth.checked_sub(1)?; if depth == 0 { return Some(index + ch.len_utf8()); } } _ => {} } } None } pub fn number_len(value: &str) -> Option { let bytes = value.as_bytes(); let mut index = 0; if matches!(bytes.get(index), Some(b'+' | b'-')) { index += 1; } let integer_start = index; while matches!(bytes.get(index), Some(b'0'..=b'9')) { index += 1; } let integer_digits = index - integer_start; let mut fraction_digits = 0; if matches!(bytes.get(index), Some(b'.')) { index += 1; let fraction_start = index; while matches!(bytes.get(index), Some(b'0'..=b'9')) { index += 1; } fraction_digits = index - fraction_start; } if integer_digits == 0 && fraction_digits == 0 { return None; } if matches!(bytes.get(index), Some(b'e' | b'E')) { index += 1; if matches!(bytes.get(index), Some(b'+' | b'-')) { index += 1; } let exponent_start = index; while matches!(bytes.get(index), Some(b'0'..=b'9')) { index += 1; } if index == exponent_start { return None; } } Some(index) } #[cfg(test)] mod tests { use super::{ ContainerQueryLengthContext, CssNumericContext, CssNumericKind, CssNumericValue, UnitlessAngle, UnitlessLength, balanced_function_len, css_number_value_is_supported, css_time_value_is_supported, number_len, parse_angle_degrees, parse_number, parse_px_length, resolve_css_number, resolve_css_numeric, resolve_length_percentage, resolve_time_seconds, starts_with_supported_math_function, }; fn assert_close(actual: f64, expected: f64) { assert!( (actual - expected).abs() < 1e-5, "expected {actual} to be close to {expected}" ); } #[test] fn number_scanner_handles_svg_and_css_numeric_tokens() { assert_eq!(number_len("-1.25e+3px"), Some(8)); assert_eq!(number_len(".5"), Some(2)); assert_eq!(number_len("e10"), None); assert_eq!(parse_number(" 12.5 "), Some(12.5)); assert_eq!(parse_number("Infinity"), None); } #[test] fn css_math_lengths_and_angles_parse_absolute_values() { assert_close(parse_px_length("10px", UnitlessLength::Any).unwrap(), 10.0); assert_close(parse_px_length("10", UnitlessLength::Any).unwrap(), 10.0); assert_eq!(parse_px_length("10", UnitlessLength::ZeroOnly), None); assert_close( parse_px_length("calc(10px + 2px)", UnitlessLength::Any).unwrap(), 12.0, ); assert_close( parse_px_length("calc(2px * 3)", UnitlessLength::Any).unwrap(), 6.0, ); assert_close( parse_px_length("clamp(1px, max(2px, 3px), 4px)", UnitlessLength::Any).unwrap(), 3.0, ); assert_close( parse_angle_degrees("0.5turn", UnitlessAngle::Degrees).unwrap(), 180.0, ); assert_close( parse_angle_degrees("calc(90deg + 1rad)", UnitlessAngle::Degrees).unwrap(), 90.0 + 180.0 / std::f64::consts::PI, ); } #[test] fn balanced_function_helpers_find_supported_css_math() { assert!(starts_with_supported_math_function("calc(1px + 2px)")); assert!(starts_with_supported_math_function("round(up, 2.2px, 1px)")); assert_eq!(balanced_function_len("calc(min(1px, 2px)) tail"), Some(19)); assert_eq!(balanced_function_len("calc(1px"), None); } #[test] fn css_math_uses_stylo_parser_for_extended_functions_and_unit_algebra() { assert_close( parse_px_length("abs(-2px)", UnitlessLength::ZeroOnly).unwrap(), 2.0, ); assert_close( parse_px_length("round(up, 2.2px, 1px)", UnitlessLength::ZeroOnly).unwrap(), 3.0, ); assert_eq!(resolve_css_number("calc(5px / 1px)", None), None); assert_eq!( parse_px_length("calc(5px / 1px)", UnitlessLength::ZeroOnly), None ); } #[test] fn css_math_resolves_length_percentages_against_basis() { assert_close( resolve_length_percentage("25%", 200.0, UnitlessLength::ZeroOnly).unwrap(), 50.0, ); assert_close( resolve_length_percentage("calc(25% - 2px)", 200.0, UnitlessLength::ZeroOnly).unwrap(), 48.0, ); assert_close( resolve_length_percentage("max(10%, 3px)", 20.0, UnitlessLength::ZeroOnly).unwrap(), 3.0, ); assert_close( resolve_length_percentage( "max(10px + (2 * (10px + min(10%, 30px))), 5% + 80px)", 100.0, UnitlessLength::ZeroOnly, ) .unwrap(), 85.0, ); } #[test] fn css_math_resolves_percentages_without_accepting_lengths() { assert_close( resolve_css_numeric( "calc(min(50%, 60%))", CssNumericKind::Percentage, CssNumericContext::supports_probe(), ) .unwrap() .percentage() .unwrap(), 50.0, ); assert!( resolve_css_numeric( "calc(50px - 50%)", CssNumericKind::Percentage, CssNumericContext::supports_probe(), ) .is_none() ); } #[test] fn css_math_resolves_sign_with_container_units_for_animation_values() { let container = ContainerQueryLengthContext::from_inline_size(100.0); assert!(css_time_value_is_supported( "calc(10s + (sign(2cqw - 10px) * 5s))" )); assert!(css_number_value_is_supported( "calc(10 + (sign(2cqw - 10px) * 5))" )); assert_close( resolve_time_seconds("calc(10s + (sign(2cqw - 10px) * 5s))", Some(container)).unwrap(), 5.0, ); assert_close( resolve_css_number("calc(10 + (sign(2cqw - 10px) * 5))", Some(container)).unwrap(), 5.0, ); } #[test] fn css_math_resolves_shared_numeric_context_units() { let context = CssNumericContext { container_lengths: Some(ContainerQueryLengthContext::from_inline_size(200.0)), font_size_px: Some(16.0), root_font_size_px: Some(30.0), line_height_px: Some(20.0), viewport_width_px: Some(1920.0), viewport_height_px: Some(1080.0), ..CssNumericContext::default() }; assert_close( resolve_css_numeric( "10%", CssNumericKind::LengthPercentage { basis: 520.0, unitless: UnitlessLength::ZeroOnly, }, context, ) .unwrap() .px_length() .unwrap(), 52.0, ); assert_close( resolve_css_numeric( "2em", CssNumericKind::LengthPercentage { basis: 520.0, unitless: UnitlessLength::ZeroOnly, }, context, ) .unwrap() .px_length() .unwrap(), 32.0, ); assert!( resolve_css_numeric( "calc(10% + 2px)", CssNumericKind::LengthPercentage { basis: 520.0, unitless: UnitlessLength::ZeroOnly, }, context, ) .is_some(), "percentage plus px should resolve" ); assert!( resolve_css_numeric( "calc(10px + 2em)", CssNumericKind::LengthPercentage { basis: 520.0, unitless: UnitlessLength::ZeroOnly, }, context, ) .is_some(), "px plus em should resolve" ); assert_close( resolve_css_numeric( "calc(10% + 2em)", CssNumericKind::LengthPercentage { basis: 520.0, unitless: UnitlessLength::ZeroOnly, }, context, ) .unwrap() .px_length() .unwrap(), 84.0, ); assert_close( resolve_css_numeric( "calc(8lh + 7px)", CssNumericKind::LengthPercentage { basis: 100.0, unitless: UnitlessLength::ZeroOnly, }, context, ) .unwrap() .px_length() .unwrap(), 167.0, ); let resolve_width = |value| { resolve_css_numeric( value, CssNumericKind::LengthPercentage { basis: 520.0, unitless: UnitlessLength::ZeroOnly, }, context, ) .and_then(CssNumericValue::px_length) }; assert_close(resolve_width("calc(5px * 10)").unwrap(), 50.0); assert_close(resolve_width("calc(20% * 0.5)").unwrap(), 52.0); assert_close(resolve_width("calc(4px * 4)").unwrap(), 16.0); assert_close(resolve_width("calc(400px / 4)").unwrap(), 100.0); assert_close(resolve_width("calc((20% + 1em) * 0.5)").unwrap(), 60.0); assert_close(resolve_width("calc(100px / 1 / 1)").unwrap(), 100.0); assert_eq!(resolve_width("calc(5px * 10lh / 1px)"), None); assert_eq!(resolve_width("calc(20% * 0.5em / 1px)"), None); assert_eq!(resolve_width("calc(400px / 4lh * 1px)"), None); assert_eq!(resolve_width("calc(20% / 0.5em * 1px)"), None); assert_eq!(resolve_width("calc(52px * 1px / 10%)"), None); assert_eq!(resolve_width("calc(100px * 1px / 1px / 1)"), None); assert_close( resolve_css_numeric( "calc(10 + sign(1em - 1000px))", CssNumericKind::Number, context, ) .unwrap() .number() .unwrap(), 9.0, ); assert_close( resolve_css_numeric("calc(10 + sign(1 - 2))", CssNumericKind::Number, context) .unwrap() .number() .unwrap(), 9.0, ); assert_close( resolve_css_numeric( "calc(10 + sign(30deg - 40deg))", CssNumericKind::Number, context, ) .unwrap() .number() .unwrap(), 9.0, ); assert_close( resolve_css_numeric( "calc(2 * sibling-index())", CssNumericKind::Number, CssNumericContext { sibling_index: Some(3.0), ..context }, ) .unwrap() .number() .unwrap(), 6.0, ); } }