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