1use crate::{
2 NumericValueV0, parse_whole_function_value_arguments, parse_whole_function_value_inner,
3};
4
5pub fn compress_numeric_token_text(text: &str) -> Option<String> {
6 let split = numeric_prefix_end(text)?;
7 let (number, suffix) = text.split_at(split);
8 let compressed = compress_number_prefix(number);
9 let rewritten = format!("{compressed}{suffix}");
10 (rewritten != text).then_some(rewritten)
11}
12
13pub fn parse_reducible_calc_value(value: &str) -> Option<String> {
14 let inner = parse_whole_function_value_inner(value, "calc")?;
15 let reduced = parse_reducible_numeric_expression(inner)?;
16 Some(format_numeric_value_with_unit(reduced))
17}
18
19pub fn reduce_static_numeric_expression(value: &str) -> Option<String> {
21 let reduced = parse_reducible_numeric_expression(value)?;
22 Some(format_numeric_value_with_unit(reduced))
23}
24
25pub fn parse_reducible_abs_value(value: &str) -> Option<String> {
26 let inner = parse_whole_function_value_inner(value, "abs")?;
27 let parsed = parse_reducible_numeric_expression(inner)?;
28 Some(format_numeric_value_with_unit(NumericValueV0 {
29 value: parsed.value.abs(),
30 unit: parsed.unit,
31 }))
32}
33
34pub fn parse_reducible_sign_value(value: &str) -> Option<String> {
35 let inner = parse_whole_function_value_inner(value, "sign")?;
36 let parsed = parse_reducible_numeric_expression(inner)?;
37 let value = if parsed.value > 0.0 {
38 1.0
39 } else if parsed.value < 0.0 {
40 -1.0
41 } else {
42 0.0
43 };
44 Some(format_css_number(value))
45}
46
47pub fn parse_reducible_ceil_value(value: &str) -> Option<String> {
48 parse_reducible_unary_numeric_transform_value(value, "ceil", f64::ceil)
49}
50
51pub fn parse_reducible_floor_value(value: &str) -> Option<String> {
52 parse_reducible_unary_numeric_transform_value(value, "floor", f64::floor)
53}
54
55pub fn parse_reducible_round_to_integer_value(value: &str) -> Option<String> {
56 parse_reducible_unary_numeric_transform_value(value, "round", f64::round)
57}
58
59pub fn parse_reducible_round_value(value: &str) -> Option<String> {
60 let arguments = parse_whole_function_value_arguments(value, "round")?;
61 let (strategy, value, interval) = match arguments.as_slice() {
62 [value, interval] => (
63 StaticRoundStrategy::Nearest,
64 value.as_str(),
65 interval.as_str(),
66 ),
67 [strategy, value, interval] => (
68 StaticRoundStrategy::parse(strategy.trim())?,
69 value.as_str(),
70 interval.as_str(),
71 ),
72 _ => return None,
73 };
74 let value = parse_reducible_numeric_expression(value.trim())?;
75 let interval = parse_reducible_numeric_expression(interval.trim())?;
76 if value.unit != interval.unit || interval.value <= 0.0 {
77 return None;
78 }
79 let quotient = value.value / interval.value;
80 let rounded = strategy.apply(quotient)?;
81 Some(format_numeric_value_with_unit(NumericValueV0 {
82 value: rounded * interval.value,
83 unit: value.unit,
84 }))
85}
86
87pub fn parse_reducible_mod_value(value: &str) -> Option<String> {
88 parse_reducible_positive_remainder_value(value, "mod")
89}
90
91pub fn parse_reducible_rem_value(value: &str) -> Option<String> {
92 parse_reducible_positive_remainder_value(value, "rem")
93}
94
95pub fn parse_reducible_hypot_value(value: &str) -> Option<String> {
96 let arguments = parse_whole_function_value_arguments(value, "hypot")?;
97 let first_argument = arguments.first()?;
98 let first = parse_reducible_numeric_expression(first_argument.trim())?;
99 let mut sum_of_squares = first.value * first.value;
100
101 for argument in arguments.iter().skip(1) {
102 let parsed = parse_reducible_numeric_expression(argument.trim())?;
103 if parsed.unit != first.unit {
104 return None;
105 }
106 sum_of_squares += parsed.value * parsed.value;
107 }
108
109 Some(format_numeric_value_with_unit(NumericValueV0 {
110 value: sum_of_squares.sqrt(),
111 unit: first.unit,
112 }))
113}
114
115pub fn parse_reducible_sqrt_value(value: &str) -> Option<String> {
116 let inner = parse_whole_function_value_inner(value, "sqrt")?;
117 let parsed = parse_reducible_numeric_expression(inner)?;
118 if !parsed.unit.is_empty() || parsed.value < 0.0 {
119 return None;
120 }
121 Some(format_css_number(parsed.value.sqrt()))
122}
123
124pub fn parse_reducible_pow_value(value: &str) -> Option<String> {
125 let arguments = parse_whole_function_value_arguments(value, "pow")?;
126 let [base, exponent] = arguments.as_slice() else {
127 return None;
128 };
129 let base = parse_reducible_numeric_expression(base.trim())?;
130 let exponent = parse_reducible_numeric_expression(exponent.trim())?;
131 if !base.unit.is_empty() || !exponent.unit.is_empty() {
132 return None;
133 }
134 let value = base.value.powf(exponent.value);
135 value.is_finite().then(|| format_css_number(value))
136}
137
138pub fn parse_reducible_exp_value(value: &str) -> Option<String> {
139 let inner = parse_whole_function_value_inner(value, "exp")?;
140 let parsed = parse_reducible_numeric_expression(inner)?;
141 if !parsed.unit.is_empty() {
142 return None;
143 }
144 let value = parsed.value.exp();
145 value.is_finite().then(|| format_css_number(value))
146}
147
148pub fn parse_reducible_log_value(value: &str) -> Option<String> {
149 let arguments = parse_whole_function_value_arguments(value, "log")?;
150 let value = match arguments.as_slice() {
151 [value] | [value, _] => value,
152 _ => return None,
153 };
154 let value = parse_reducible_numeric_expression(value.trim())?;
155 if !value.unit.is_empty() || value.value <= 0.0 {
156 return None;
157 };
158 let base = match arguments.as_slice() {
159 [_] => std::f64::consts::E,
160 [_, base] => {
161 let base = parse_reducible_numeric_expression(base.trim())?;
162 if !base.unit.is_empty() || base.value <= 0.0 || base.value == 1.0 {
163 return None;
164 }
165 base.value
166 }
167 _ => return None,
168 };
169 let result = value.value.log(base);
170 result.is_finite().then(|| format_css_number(result))
171}
172
173pub fn parse_reducible_min_value(value: &str) -> Option<String> {
174 parse_reducible_extreme_value(value, "min", f64::min)
175}
176
177pub fn parse_reducible_max_value(value: &str) -> Option<String> {
178 parse_reducible_extreme_value(value, "max", f64::max)
179}
180
181pub fn parse_reducible_clamp_value(value: &str) -> Option<String> {
182 let arguments = parse_whole_function_value_arguments(value, "clamp")?;
183 let [minimum, preferred, maximum] = arguments.as_slice() else {
184 return None;
185 };
186 let minimum = parse_numeric_value_with_unit(minimum.trim())?;
187 let preferred = parse_numeric_value_with_unit(preferred.trim())?;
188 let maximum = parse_numeric_value_with_unit(maximum.trim())?;
189 if preferred.unit != minimum.unit || maximum.unit != minimum.unit {
190 return None;
191 }
192 let selected = preferred.value.min(maximum.value).max(minimum.value);
193 Some(format!("{}{}", format_css_number(selected), minimum.unit))
194}
195
196fn parse_reducible_extreme_value(
197 value: &str,
198 function_name: &str,
199 reduce: fn(f64, f64) -> f64,
200) -> Option<String> {
201 let arguments = parse_whole_function_value_arguments(value, function_name)?;
202 let first = arguments.first()?;
203 let first = parse_numeric_value_with_unit(first.trim())?;
204 let mut selected = first.value;
205 let unit = first.unit;
206
207 for argument in arguments.iter().skip(1) {
208 let candidate = parse_numeric_value_with_unit(argument.trim())?;
209 if candidate.unit != unit {
210 return None;
211 }
212 selected = reduce(selected, candidate.value);
213 }
214
215 Some(format!("{}{}", format_css_number(selected), unit))
216}
217
218fn parse_reducible_positive_remainder_value(value: &str, function_name: &str) -> Option<String> {
219 let arguments = parse_whole_function_value_arguments(value, function_name)?;
220 let [dividend, divisor] = arguments.as_slice() else {
221 return None;
222 };
223 let dividend = parse_reducible_numeric_expression(dividend.trim())?;
224 let divisor = parse_reducible_numeric_expression(divisor.trim())?;
225 if dividend.unit != divisor.unit || dividend.value < 0.0 || divisor.value <= 0.0 {
226 return None;
227 }
228 Some(format_numeric_value_with_unit(NumericValueV0 {
229 value: dividend.value % divisor.value,
230 unit: dividend.unit,
231 }))
232}
233
234fn parse_reducible_unary_numeric_transform_value(
235 value: &str,
236 function_name: &str,
237 transform: fn(f64) -> f64,
238) -> Option<String> {
239 let inner = parse_whole_function_value_inner(value, function_name)?;
240 let parsed = parse_reducible_numeric_expression(inner)?;
241 let value = transform(parsed.value);
242 value.is_finite().then(|| {
243 format_numeric_value_with_unit(NumericValueV0 {
244 value,
245 unit: parsed.unit,
246 })
247 })
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251enum StaticRoundStrategy {
252 Nearest,
253 Up,
254 Down,
255 ToZero,
256}
257
258impl StaticRoundStrategy {
259 fn parse(text: &str) -> Option<Self> {
260 match text.to_ascii_lowercase().as_str() {
261 "nearest" => Some(Self::Nearest),
262 "up" => Some(Self::Up),
263 "down" => Some(Self::Down),
264 "to-zero" => Some(Self::ToZero),
265 _ => None,
266 }
267 }
268
269 fn apply(self, value: f64) -> Option<f64> {
270 match self {
271 Self::Nearest if quotient_is_halfway_between_integers(value) => None,
272 Self::Nearest => Some(value.round()),
273 Self::Up => Some(value.ceil()),
274 Self::Down => Some(value.floor()),
275 Self::ToZero => Some(value.trunc()),
276 }
277 }
278}
279
280fn quotient_is_halfway_between_integers(value: f64) -> bool {
281 (value.abs().fract() - 0.5).abs() < f64::EPSILON
282}
283
284pub fn parse_numeric_value_with_unit(text: &str) -> Option<NumericValueV0<'_>> {
285 let text = text.trim();
286 let mut parser = NumericExpressionParser::new(text);
287 let parsed = parser.parse_number()?;
288 parser.skip_whitespace();
289 (parser.is_eof()).then_some(parsed)
290}
291
292fn parse_reducible_numeric_expression(inner: &str) -> Option<NumericValueV0<'_>> {
293 let mut parser = NumericExpressionParser::new(inner);
294 let parsed = parser.parse_expression()?;
295 parser.skip_whitespace();
296 parser.is_eof().then_some(parsed)
297}
298
299struct NumericExpressionParser<'a> {
300 text: &'a str,
301 index: usize,
302}
303
304impl<'a> NumericExpressionParser<'a> {
305 fn new(text: &'a str) -> Self {
306 Self { text, index: 0 }
307 }
308
309 fn parse_expression(&mut self) -> Option<NumericValueV0<'a>> {
310 let mut left = self.parse_term()?;
311 loop {
312 self.skip_whitespace();
313 let Some(operator) = self.peek_char().filter(|ch| matches!(ch, '+' | '-')) else {
314 break;
315 };
316 self.index += operator.len_utf8();
317 let right = self.parse_term()?;
318 left = combine_numeric_additive(left, right, operator)?;
319 }
320 Some(left)
321 }
322
323 fn parse_term(&mut self) -> Option<NumericValueV0<'a>> {
324 let mut left = self.parse_factor()?;
325 loop {
326 self.skip_whitespace();
327 let Some(operator) = self.peek_char().filter(|ch| matches!(ch, '*' | '/')) else {
328 break;
329 };
330 self.index += operator.len_utf8();
331 let right = self.parse_factor()?;
332 left = combine_numeric_multiplicative(left, right, operator)?;
333 }
334 Some(left)
335 }
336
337 fn parse_factor(&mut self) -> Option<NumericValueV0<'a>> {
338 self.skip_whitespace();
339 if self.consume_char('(') {
340 let parsed = self.parse_expression()?;
341 self.skip_whitespace();
342 self.consume_char(')').then_some(parsed)
343 } else {
344 self.parse_number()
345 }
346 }
347
348 fn parse_number(&mut self) -> Option<NumericValueV0<'a>> {
349 self.skip_whitespace();
350 let start = self.index;
351 let split = numeric_prefix_end(&self.text[start..])?;
352 let number_end = start + split;
353 let unit_start = number_end;
354 self.index = number_end;
355 if self.peek_char() == Some('%') {
356 self.index += '%'.len_utf8();
357 } else {
358 while self.peek_char().is_some_and(is_css_numeric_unit_continue) {
359 let ch = self.peek_char()?;
360 self.index += ch.len_utf8();
361 }
362 }
363 let number = &self.text[start..number_end];
364 let unit = &self.text[unit_start..self.index];
365 let value = number.parse::<f64>().ok()?;
366 value.is_finite().then_some(NumericValueV0 { value, unit })
367 }
368
369 fn skip_whitespace(&mut self) {
370 while let Some(ch) = self.peek_char() {
371 if !ch.is_whitespace() {
372 break;
373 }
374 self.index += ch.len_utf8();
375 }
376 }
377
378 fn consume_char(&mut self, expected: char) -> bool {
379 if self.peek_char() == Some(expected) {
380 self.index += expected.len_utf8();
381 true
382 } else {
383 false
384 }
385 }
386
387 fn peek_char(&self) -> Option<char> {
388 self.text[self.index..].chars().next()
389 }
390
391 fn is_eof(&self) -> bool {
392 self.index == self.text.len()
393 }
394}
395
396fn combine_numeric_additive<'a>(
397 left: NumericValueV0<'a>,
398 right: NumericValueV0<'a>,
399 operator: char,
400) -> Option<NumericValueV0<'a>> {
401 if left.unit != right.unit {
402 return None;
403 }
404 let value = if operator == '+' {
405 left.value + right.value
406 } else {
407 left.value - right.value
408 };
409 Some(NumericValueV0 {
410 value,
411 unit: left.unit,
412 })
413}
414
415fn combine_numeric_multiplicative<'a>(
416 left: NumericValueV0<'a>,
417 right: NumericValueV0<'a>,
418 operator: char,
419) -> Option<NumericValueV0<'a>> {
420 match operator {
421 '*' if left.unit.is_empty() && right.unit.is_empty() => Some(NumericValueV0 {
422 value: left.value * right.value,
423 unit: "",
424 }),
425 '*' if left.unit.is_empty() => Some(NumericValueV0 {
426 value: left.value * right.value,
427 unit: right.unit,
428 }),
429 '*' if right.unit.is_empty() => Some(NumericValueV0 {
430 value: left.value * right.value,
431 unit: left.unit,
432 }),
433 '/' if right.unit.is_empty() && right.value != 0.0 => Some(NumericValueV0 {
434 value: left.value / right.value,
435 unit: left.unit,
436 }),
437 _ => None,
438 }
439}
440
441fn format_numeric_value_with_unit(value: NumericValueV0<'_>) -> String {
442 format!("{}{}", format_css_number(value.value), value.unit)
443}
444
445fn is_css_numeric_unit_continue(ch: char) -> bool {
446 ch.is_ascii_alphabetic()
447}
448
449pub fn format_css_number(value: f64) -> String {
450 if value.fract() == 0.0 {
451 return format!("{value:.0}");
452 }
453 let formatted = format!("{value:.6}");
454 formatted
455 .trim_end_matches('0')
456 .trim_end_matches('.')
457 .to_string()
458}
459
460pub fn numeric_prefix_end(text: &str) -> Option<usize> {
461 let bytes = text.as_bytes();
462 let mut index = 0;
463
464 if matches!(bytes.get(index), Some(b'+') | Some(b'-')) {
465 index += 1;
466 }
467
468 let integer_start = index;
469 while matches!(bytes.get(index), Some(b'0'..=b'9')) {
470 index += 1;
471 }
472 let saw_integer_digit = index > integer_start;
473
474 if bytes.get(index) == Some(&b'.') {
475 index += 1;
476 let fraction_start = index;
477 while matches!(bytes.get(index), Some(b'0'..=b'9')) {
478 index += 1;
479 }
480 if !saw_integer_digit && index == fraction_start {
481 return None;
482 }
483 } else if !saw_integer_digit {
484 return None;
485 }
486
487 if matches!(bytes.get(index), Some(b'e') | Some(b'E')) {
488 let exponent_marker = index;
489 let mut exponent_index = index + 1;
490 if matches!(bytes.get(exponent_index), Some(b'+') | Some(b'-')) {
491 exponent_index += 1;
492 }
493 let exponent_digit_start = exponent_index;
494 while matches!(bytes.get(exponent_index), Some(b'0'..=b'9')) {
495 exponent_index += 1;
496 }
497 if exponent_index > exponent_digit_start {
498 index = exponent_index;
499 } else {
500 index = exponent_marker;
501 }
502 }
503
504 Some(index)
505}
506
507pub fn compress_number_prefix(number: &str) -> String {
508 let (sign, unsigned) = match number.as_bytes().first() {
509 Some(b'+') | Some(b'-') => (&number[..1], &number[1..]),
510 _ => ("", number),
511 };
512 let sign = if sign == "+" || is_zero_number_prefix(unsigned) {
513 ""
514 } else {
515 sign
516 };
517 let (mantissa, exponent) = split_number_exponent(unsigned);
518 let compressed_mantissa = compress_decimal_mantissa(mantissa);
519 let mut compressed = format!("{sign}{compressed_mantissa}");
520
521 if let Some(exponent) = exponent {
522 let normalized_exponent = normalize_exponent_suffix(exponent);
523 if normalized_exponent != "0" && !is_zero_number_prefix(&compressed) {
524 compressed.push('e');
525 compressed.push_str(&normalized_exponent);
526 }
527 }
528
529 compressed
530}
531
532fn split_number_exponent(number: &str) -> (&str, Option<&str>) {
533 if let Some(index) = number.find(['e', 'E']) {
534 (&number[..index], Some(&number[index + 1..]))
535 } else {
536 (number, None)
537 }
538}
539
540fn compress_decimal_mantissa(mantissa: &str) -> String {
541 let Some((before_dot, after_dot)) = mantissa.split_once('.') else {
542 return compress_integer_digits(mantissa);
543 };
544
545 let trimmed_fraction = after_dot.trim_end_matches('0');
546 let compressed_integer = compress_integer_digits(before_dot);
547 let mut compressed_unsigned = if trimmed_fraction.is_empty() {
548 compressed_integer
549 } else {
550 format!("{compressed_integer}.{trimmed_fraction}")
551 };
552
553 if let Some(rest) = compressed_unsigned.strip_prefix("0.") {
554 compressed_unsigned = format!(".{rest}");
555 }
556
557 if compressed_unsigned.is_empty() {
558 compressed_unsigned.push('0');
559 }
560
561 compressed_unsigned
562}
563
564fn compress_integer_digits(digits: &str) -> String {
565 let trimmed = digits.trim_start_matches('0');
566 if trimmed.is_empty() {
567 "0".to_string()
568 } else {
569 trimmed.to_string()
570 }
571}
572
573fn normalize_exponent_suffix(exponent: &str) -> String {
574 let (sign, digits) = match exponent.as_bytes().first() {
575 Some(b'+') => ("", &exponent[1..]),
576 Some(b'-') => ("-", &exponent[1..]),
577 _ => ("", exponent),
578 };
579 let digits = digits.trim_start_matches('0');
580 let digits = if digits.is_empty() { "0" } else { digits };
581 if digits == "0" {
582 digits.to_string()
583 } else {
584 format!("{sign}{digits}")
585 }
586}
587
588pub fn css_number_is_zero(number: &str) -> bool {
589 parse_numeric_value_with_unit(number)
590 .is_some_and(|value| value.unit.is_empty() && value.value == 0.0)
591}
592
593fn is_zero_number_prefix(number: &str) -> bool {
594 css_number_is_zero(number)
595}