Skip to main content

stylex_css_parser/css_types/
calc.rs

1/*!
2CSS Calc type parsing with full arithmetic support.
3
4Implements complete calc() expression parsing with operator precedence.
5*/
6
7use stylex_macros::stylex_unreachable;
8
9use crate::{
10  CssParseError,
11  css_types::{calc_constant::CalcConstant, common_types::Percentage},
12  token_parser::TokenParser,
13  token_types::{SimpleToken, TokenList},
14};
15use std::fmt::{self, Display};
16
17/// Dimension with value and unit for calc expressions
18#[derive(Debug, Clone, PartialEq)]
19pub struct CalcDimension {
20  pub value: f32,
21  pub unit: String,
22}
23
24impl CalcDimension {
25  pub fn new(value: f32, unit: String) -> Self {
26    Self { value, unit }
27  }
28}
29
30impl Display for CalcDimension {
31  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32    write!(f, "{}{}", self.value, self.unit)
33  }
34}
35
36/// Addition operation: { type: '+', left: CalcValue, right: CalcValue }
37#[derive(Debug, Clone, PartialEq)]
38pub struct Addition {
39  pub left: Box<CalcValue>,
40  pub right: Box<CalcValue>,
41}
42
43impl Addition {
44  pub fn new(left: CalcValue, right: CalcValue) -> Self {
45    Self {
46      left: Box::new(left),
47      right: Box::new(right),
48    }
49  }
50}
51
52/// Subtraction operation: { type: '-', left: CalcValue, right: CalcValue }
53#[derive(Debug, Clone, PartialEq)]
54pub struct Subtraction {
55  pub left: Box<CalcValue>,
56  pub right: Box<CalcValue>,
57}
58
59impl Subtraction {
60  pub fn new(left: CalcValue, right: CalcValue) -> Self {
61    Self {
62      left: Box::new(left),
63      right: Box::new(right),
64    }
65  }
66}
67
68/// Multiplication operation: { type: '*', left: CalcValue, right: CalcValue }
69#[derive(Debug, Clone, PartialEq)]
70pub struct Multiplication {
71  pub left: Box<CalcValue>,
72  pub right: Box<CalcValue>,
73}
74
75impl Multiplication {
76  pub fn new(left: CalcValue, right: CalcValue) -> Self {
77    Self {
78      left: Box::new(left),
79      right: Box::new(right),
80    }
81  }
82}
83
84/// Division operation: { type: '/', left: CalcValue, right: CalcValue }
85#[derive(Debug, Clone, PartialEq)]
86pub struct Division {
87  pub left: Box<CalcValue>,
88  pub right: Box<CalcValue>,
89}
90
91impl Division {
92  pub fn new(left: CalcValue, right: CalcValue) -> Self {
93    Self {
94      left: Box::new(left),
95      right: Box::new(right),
96    }
97  }
98}
99
100/// Group (parenthesized expression): { type: 'group', expr: CalcValue }
101#[derive(Debug, Clone, PartialEq)]
102pub struct Group {
103  pub expr: Box<CalcValue>,
104}
105
106impl Group {
107  pub fn new(expr: CalcValue) -> Self {
108    Self {
109      expr: Box::new(expr),
110    }
111  }
112}
113
114/// Union type for calc values and operators during parsing
115#[derive(Debug, Clone, PartialEq)]
116pub enum CalcValueOrOperator {
117  Value(CalcValue),
118  Operator(String),
119}
120
121#[derive(Debug, Clone, PartialEq)]
122pub enum CalcValue {
123  /// number
124  Number(f32),
125  /// TokenDimension[4]
126  Dimension(CalcDimension),
127  /// Percentage
128  Percentage(Percentage),
129  /// CalcConstant
130  Constant(CalcConstant),
131  /// Addition
132  Addition(Addition),
133  /// Subtraction
134  Subtraction(Subtraction),
135  /// Multiplication
136  Multiplication(Multiplication),
137  /// Division
138  Division(Division),
139  /// Group
140  Group(Group),
141}
142
143impl CalcValue {
144  /// Parser for individual calc values (no operators)
145  pub fn value_parser() -> TokenParser<CalcValue> {
146    TokenParser::new(Self::parse_calc_value, "calc_value")
147  }
148
149  /// Helper: Parse a basic calc value (number, dimension, percentage, or constant)
150  fn parse_calc_value(tokens: &mut TokenList) -> Result<CalcValue, CssParseError> {
151    let token = tokens
152      .consume_next_token()?
153      .ok_or(CssParseError::ParseError {
154        message: "Expected Number, Dimension, Percentage, or Constant token".to_string(),
155      })?;
156
157    match token {
158      SimpleToken::Number(value) => Ok(CalcValue::Number(value as f32)),
159      SimpleToken::Dimension { value, unit } => {
160        Ok(CalcValue::Dimension(CalcDimension::new(value as f32, unit)))
161      },
162      SimpleToken::Percentage(value) => {
163        // cssparser stores percentage as already converted (0.5 for 50%)
164        // Convert to our format (50.0 for 50%)
165        Ok(CalcValue::Percentage(Percentage::new(
166          (value * 100.0) as f32,
167        )))
168      },
169      SimpleToken::Ident(name) => {
170        // Try to parse as calc constant (pi, e, infinity, -infinity, NaN)
171        match CalcConstant::parse(&name) {
172          Some(constant) => Ok(CalcValue::Constant(constant)),
173          None => Err(CssParseError::ParseError {
174            message: format!("Unknown calc constant: {}", name),
175          }),
176        }
177      },
178      _ => Err(CssParseError::ParseError {
179        message: format!(
180          "Expected Number, Dimension, Percentage, or Constant token, got {:?}",
181          token
182        ),
183      }),
184    }
185  }
186
187  pub fn parser() -> TokenParser<CalcValue> {
188    TokenParser::new(Self::parse_calc_expression, "calc_expression")
189  }
190
191  /// Parse calc expression with operator precedence
192  fn parse_calc_expression(tokens: &mut TokenList) -> Result<CalcValue, CssParseError> {
193    // Skip any leading whitespace
194    while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
195      tokens.consume_next_token()?;
196    }
197
198    // Parse first value or group
199    let first_value = match Self::try_parse_parenthesized_group(tokens) {
200      Ok(grouped) => CalcValue::Group(grouped),
201      Err(_) => Self::parse_calc_value(tokens)?,
202    };
203
204    // Collect values and operators
205    let mut values_and_operators = vec![CalcValueOrOperator::Value(first_value)];
206
207    loop {
208      // Skip whitespace
209      while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
210        tokens.consume_next_token()?;
211      }
212
213      // Check if we're at the end of the expression (RightParen means end for calc)
214      if let Ok(Some(SimpleToken::RightParen)) = tokens.peek() {
215        // Don't consume the RightParen - let the parent parser handle it
216        break;
217      }
218
219      // Try to parse an operator
220      let checkpoint = tokens.current_index;
221      match Self::try_parse_operator(tokens) {
222        Ok(operator) => {
223          values_and_operators.push(CalcValueOrOperator::Operator(operator));
224
225          // Skip whitespace after operator
226          while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
227            tokens.consume_next_token()?;
228          }
229
230          // Parse next value or group
231          let next_value = match Self::try_parse_parenthesized_group(tokens) {
232            Ok(grouped) => CalcValue::Group(grouped),
233            Err(_) => Self::parse_calc_value(tokens)?,
234          };
235          values_and_operators.push(CalcValueOrOperator::Value(next_value));
236        },
237        Err(_) => {
238          // No operator found, we're done
239          tokens.set_current_index(checkpoint);
240          break;
241        },
242      }
243    }
244
245    // Skip any trailing whitespace
246    while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
247      tokens.consume_next_token()?;
248    }
249
250    // Apply operator precedence
251    if values_and_operators.len() == 1 {
252      if let CalcValueOrOperator::Value(value) = &values_and_operators[0] {
253        Ok(value.clone())
254      } else {
255        stylex_unreachable!("First element should always be a value")
256      }
257    } else {
258      Self::split_by_multiplication_or_division(values_and_operators)
259    }
260  }
261
262  /// Apply operator precedence: multiplication/division first, then addition/subtraction
263  fn split_by_multiplication_or_division(
264    values_and_operators: Vec<CalcValueOrOperator>,
265  ) -> Result<CalcValue, CssParseError> {
266    if values_and_operators.len() == 1 {
267      match &values_and_operators[0] {
268        CalcValueOrOperator::Value(value) => return Ok(value.clone()),
269        CalcValueOrOperator::Operator(_) => {
270          return Err(CssParseError::ParseError {
271            message: "Invalid operator".to_string(),
272          });
273        },
274      }
275    }
276
277    // Find first * or / operator
278    let first_operator = values_and_operators
279      .iter()
280      .position(|item| matches!(item, CalcValueOrOperator::Operator(op) if op == "*" || op == "/"));
281
282    match first_operator {
283      None => {
284        // No * or / found, handle + and -
285        Self::compose_add_and_subtraction(values_and_operators)
286      },
287      Some(op_index) => {
288        let left_slice = values_and_operators[..op_index].to_vec();
289        let right_slice = values_and_operators[op_index + 1..].to_vec();
290
291        if let CalcValueOrOperator::Operator(operator) = &values_and_operators[op_index] {
292          let left = Self::compose_add_and_subtraction(left_slice)?;
293          let right = Self::split_by_multiplication_or_division(right_slice)?;
294
295          match operator.as_str() {
296            "*" => Ok(CalcValue::Multiplication(Multiplication::new(left, right))),
297            "/" => Ok(CalcValue::Division(Division::new(left, right))),
298            _ => Err(CssParseError::ParseError {
299              message: "Invalid operator".to_string(),
300            }),
301          }
302        } else {
303          Err(CssParseError::ParseError {
304            message: "Expected operator".to_string(),
305          })
306        }
307      },
308    }
309  }
310
311  /// Handle addition and subtraction operations
312  fn compose_add_and_subtraction(
313    values_and_operators: Vec<CalcValueOrOperator>,
314  ) -> Result<CalcValue, CssParseError> {
315    if values_and_operators.len() == 1 {
316      match &values_and_operators[0] {
317        CalcValueOrOperator::Value(value) => return Ok(value.clone()),
318        CalcValueOrOperator::Operator(op) => {
319          return Err(CssParseError::ParseError {
320            message: format!("Invalid operator: {}", op),
321          });
322        },
323      }
324    }
325
326    // Find first + or - operator
327    let first_operator = values_and_operators
328      .iter()
329      .position(|item| matches!(item, CalcValueOrOperator::Operator(op) if op == "+" || op == "-"));
330
331    match first_operator {
332      None => Err(CssParseError::ParseError {
333        message: "No valid operator found".to_string(),
334      }),
335      Some(op_index) => {
336        let left_slice = values_and_operators[..op_index].to_vec();
337        let right_slice = values_and_operators[op_index + 1..].to_vec();
338
339        if let CalcValueOrOperator::Operator(operator) = &values_and_operators[op_index] {
340          let left = Self::compose_add_and_subtraction(left_slice)?;
341          let right = Self::compose_add_and_subtraction(right_slice)?;
342
343          match operator.as_str() {
344            "+" => Ok(CalcValue::Addition(Addition::new(left, right))),
345            "-" => Ok(CalcValue::Subtraction(Subtraction::new(left, right))),
346            _ => Err(CssParseError::ParseError {
347              message: "Invalid operator".to_string(),
348            }),
349          }
350        } else {
351          Err(CssParseError::ParseError {
352            message: "Expected operator".to_string(),
353          })
354        }
355      },
356    }
357  }
358
359  /// Try to parse a parenthesized group
360  fn try_parse_parenthesized_group(tokens: &mut TokenList) -> Result<Group, CssParseError> {
361    // Save checkpoint in case we need to rollback
362    let checkpoint = tokens.current_index;
363
364    // Expect '('
365    let open_paren_token = tokens
366      .consume_next_token()?
367      .ok_or(CssParseError::ParseError {
368        message: "Expected opening parenthesis".to_string(),
369      })?;
370
371    if !matches!(open_paren_token, SimpleToken::LeftParen) {
372      // Rollback and return error
373      tokens.set_current_index(checkpoint);
374      return Err(CssParseError::ParseError {
375        message: "Expected '(' token".to_string(),
376      });
377    }
378
379    // Skip optional whitespace
380    while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
381      tokens.consume_next_token()?;
382    }
383
384    // Parse the inner expression recursively
385    let inner_expr = Self::parse_calc_expression(tokens)?;
386
387    // Skip optional whitespace
388    while let Ok(Some(SimpleToken::Whitespace)) = tokens.peek() {
389      tokens.consume_next_token()?;
390    }
391
392    // Expect ')'
393    let close_paren_token = tokens
394      .consume_next_token()?
395      .ok_or(CssParseError::ParseError {
396        message: "Expected closing parenthesis".to_string(),
397      })?;
398
399    if !matches!(close_paren_token, SimpleToken::RightParen) {
400      return Err(CssParseError::ParseError {
401        message: "Expected ')' token".to_string(),
402      });
403    }
404
405    Ok(Group::new(inner_expr))
406  }
407
408  /// Try to parse an operator token
409  fn try_parse_operator(tokens: &mut TokenList) -> Result<String, CssParseError> {
410    let token = tokens
411      .consume_next_token()?
412      .ok_or(CssParseError::ParseError {
413        message: "Expected operator token".to_string(),
414      })?;
415
416    match token {
417      SimpleToken::Delim('+') => Ok("+".to_string()),
418      SimpleToken::Delim('-') => Ok("-".to_string()),
419      SimpleToken::Delim('*') => Ok("*".to_string()),
420      SimpleToken::Delim('/') => Ok("/".to_string()),
421      _ => Err(CssParseError::ParseError {
422        message: format!("Expected operator (+, -, *, /), got {:?}", token),
423      }),
424    }
425  }
426}
427
428impl Display for CalcValue {
429  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430    match self {
431      CalcValue::Number(n) => write!(f, "{}", n),
432      CalcValue::Dimension(d) => write!(f, "{}", d),
433      CalcValue::Percentage(p) => write!(f, "{}", p),
434      CalcValue::Constant(c) => write!(f, "{}", c),
435      CalcValue::Addition(op) => write!(f, "{} + {}", op.left, op.right),
436      CalcValue::Subtraction(op) => write!(f, "{} - {}", op.left, op.right),
437      CalcValue::Multiplication(op) => write!(f, "{} * {}", op.left, op.right),
438      CalcValue::Division(op) => write!(f, "{} / {}", op.left, op.right),
439      CalcValue::Group(group) => write!(f, "({})", group.expr),
440    }
441  }
442}
443
444/// Main Calc expression container
445#[derive(Debug, Clone, PartialEq)]
446pub struct Calc {
447  pub value: CalcValue,
448}
449
450impl Calc {
451  /// Create a new calc expression
452  pub fn new(value: CalcValue) -> Self {
453    Calc { value }
454  }
455
456  /// Parse method for compatibility with existing tests
457  pub fn parse() -> TokenParser<Calc> {
458    Self::parser()
459  }
460
461  /// Parser for calc() expressions
462  pub fn parser() -> TokenParser<Calc> {
463    // Custom parser that handles the specific calc() tokenization structure
464    TokenParser::new(
465      |tokens| {
466        // First, expect Function("calc")
467        let token = tokens
468          .consume_next_token()?
469          .ok_or(CssParseError::ParseError {
470            message: "Expected calc function".to_string(),
471          })?;
472
473        if let SimpleToken::Function(fn_name) = token {
474          if fn_name != "calc" {
475            return Err(CssParseError::ParseError {
476              message: format!("Expected calc function, got {}", fn_name),
477            });
478          }
479        } else {
480          return Err(CssParseError::ParseError {
481            message: "Expected function token".to_string(),
482          });
483        }
484
485        // Parse the calc expression content (everything until RightParen)
486        let calc_value = CalcValue::parse_calc_expression(tokens)?;
487
488        // Consume the closing RightParen token
489        let close_token = tokens
490          .consume_next_token()?
491          .ok_or(CssParseError::ParseError {
492            message: "Expected closing parenthesis".to_string(),
493          })?;
494
495        if !matches!(close_token, SimpleToken::RightParen) {
496          return Err(CssParseError::ParseError {
497            message: "Expected closing parenthesis".to_string(),
498          });
499        }
500
501        Ok(Calc::new(calc_value))
502      },
503      "calc_parser",
504    )
505  }
506}
507
508impl Display for Calc {
509  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510    write!(f, "calc({})", self.value)
511  }
512}
513
514pub fn calc_value_to_string(value: &CalcValue) -> String {
515  match value {
516    CalcValue::Number(n) => n.to_string(),
517    CalcValue::Dimension(d) => d.to_string(),
518    CalcValue::Percentage(p) => p.to_string(),
519    CalcValue::Constant(c) => c.to_string(),
520    CalcValue::Addition(op) => format!(
521      "{} + {}",
522      calc_value_to_string(&op.left),
523      calc_value_to_string(&op.right)
524    ),
525    CalcValue::Subtraction(op) => format!(
526      "{} - {}",
527      calc_value_to_string(&op.left),
528      calc_value_to_string(&op.right)
529    ),
530    CalcValue::Multiplication(op) => format!(
531      "{} * {}",
532      calc_value_to_string(&op.left),
533      calc_value_to_string(&op.right)
534    ),
535    CalcValue::Division(op) => format!(
536      "{} / {}",
537      calc_value_to_string(&op.left),
538      calc_value_to_string(&op.right)
539    ),
540    CalcValue::Group(group) => format!("({})", calc_value_to_string(&group.expr)),
541  }
542}
543
544// Tests are in calc_parsing_tests.rs