Skip to main content

fop_types/
expression.rs

1//! Expression type for CSS calc() expressions in property values
2//!
3//! This module implements mathematical expressions that can be used in CSS calc()
4//! functions, supporting mixed units, nested expressions, and basic arithmetic.
5
6use crate::{FopError, Length, Percentage, Result};
7use std::fmt;
8
9/// Mathematical expression for CSS calc() values
10///
11/// Supports:
12/// - Literals (absolute lengths)
13/// - Percentages (relative to base value)
14/// - Binary operations: +, -, *, /
15/// - Nested expressions
16///
17/// # Examples
18///
19/// ```
20/// use fop_types::{Expression, Length, Percentage, expression::EvalContext};
21///
22/// // Parse calc(100% - 20pt)
23/// let expr = Expression::parse("calc(100% - 20pt)").unwrap();
24///
25/// // Evaluate with context
26/// let ctx = EvalContext {
27///     base_width: Some(Length::from_pt(200.0)),
28///     base_height: None,
29///     font_size: Length::from_pt(12.0),
30/// };
31/// let result = expr.evaluate(&ctx).unwrap();
32/// assert_eq!(result, Length::from_pt(180.0));
33/// ```
34#[derive(Debug, Clone, PartialEq)]
35pub enum Expression {
36    /// A literal length value
37    Literal(Length),
38
39    /// A percentage value (needs base to resolve)
40    Percentage(Percentage),
41
42    /// Addition of two expressions
43    Add(Box<Expression>, Box<Expression>),
44
45    /// Subtraction of two expressions
46    Sub(Box<Expression>, Box<Expression>),
47
48    /// Multiplication of expression by scalar
49    Mul(Box<Expression>, f64),
50
51    /// Division of expression by scalar
52    Div(Box<Expression>, f64),
53}
54
55/// Context for evaluating expressions
56///
57/// Provides base values needed to resolve percentages and other
58/// context-dependent expressions.
59#[derive(Debug, Clone, Copy)]
60pub struct EvalContext {
61    /// Base width for percentage resolution
62    pub base_width: Option<Length>,
63
64    /// Base height for percentage resolution
65    pub base_height: Option<Length>,
66
67    /// Current font size
68    pub font_size: Length,
69}
70
71impl EvalContext {
72    /// Create a new evaluation context
73    #[inline]
74    #[must_use = "this returns a new value without modifying anything"]
75    pub const fn new(
76        base_width: Option<Length>,
77        base_height: Option<Length>,
78        font_size: Length,
79    ) -> Self {
80        Self {
81            base_width,
82            base_height,
83            font_size,
84        }
85    }
86
87    /// Create a context with only width
88    #[inline]
89    #[must_use = "this returns a new value without modifying anything"]
90    pub const fn with_width(base_width: Length, font_size: Length) -> Self {
91        Self {
92            base_width: Some(base_width),
93            base_height: None,
94            font_size,
95        }
96    }
97
98    /// Create a context with only height
99    #[inline]
100    #[must_use = "this returns a new value without modifying anything"]
101    pub const fn with_height(base_height: Length, font_size: Length) -> Self {
102        Self {
103            base_width: None,
104            base_height: Some(base_height),
105            font_size,
106        }
107    }
108}
109
110impl Expression {
111    /// Parse a calc() expression from a string
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use fop_types::Expression;
117    ///
118    /// let expr = Expression::parse("calc(100% - 20pt)").unwrap();
119    /// let expr2 = Expression::parse("calc(50% + 10mm)").unwrap();
120    /// let expr3 = Expression::parse("calc((100% - 40pt) / 2)").unwrap();
121    /// ```
122    pub fn parse(input: &str) -> Result<Self> {
123        let input = input.trim();
124
125        // Check for calc() wrapper
126        if let Some(stripped) = input.strip_prefix("calc(") {
127            if let Some(content) = stripped.strip_suffix(')') {
128                return Self::parse_expression(content.trim());
129            }
130        }
131
132        Err(FopError::ParseError(format!(
133            "Expression must be wrapped in calc(): {}",
134            input
135        )))
136    }
137
138    /// Parse an expression (internal)
139    fn parse_expression(input: &str) -> Result<Self> {
140        // Try to parse as addition/subtraction first (lowest precedence)
141        if let Some(pos) = Self::find_operator(input, &['+', '-']) {
142            let op = input.chars().nth(pos).ok_or_else(|| {
143                FopError::ParseError(format!(
144                    "Operator not found at position {} in: {}",
145                    pos, input
146                ))
147            })?;
148            let left = Self::parse_expression(input[..pos].trim())?;
149            let right = Self::parse_expression(input[pos + 1..].trim())?;
150
151            return match op {
152                '+' => Ok(Expression::Add(Box::new(left), Box::new(right))),
153                '-' => Ok(Expression::Sub(Box::new(left), Box::new(right))),
154                _ => unreachable!(),
155            };
156        }
157
158        // Try to parse as multiplication/division (higher precedence)
159        if let Some(pos) = Self::find_operator(input, &['*', '/']) {
160            let op = input.chars().nth(pos).ok_or_else(|| {
161                FopError::ParseError(format!(
162                    "Operator not found at position {} in: {}",
163                    pos, input
164                ))
165            })?;
166            let left = Self::parse_expression(input[..pos].trim())?;
167            let right_str = input[pos + 1..].trim();
168
169            // Right side of * or / must be a number
170            let scalar = right_str.parse::<f64>().map_err(|_| {
171                FopError::ParseError(format!(
172                    "Right side of {} must be a number: {}",
173                    op, right_str
174                ))
175            })?;
176
177            return match op {
178                '*' => Ok(Expression::Mul(Box::new(left), scalar)),
179                '/' => {
180                    if scalar == 0.0 {
181                        Err(FopError::ParseError("Division by zero".to_string()))
182                    } else {
183                        Ok(Expression::Div(Box::new(left), scalar))
184                    }
185                }
186                _ => unreachable!(),
187            };
188        }
189
190        // Try to parse parenthesized expression
191        if let Some(stripped) = input.strip_prefix('(') {
192            if let Some(content) = stripped.strip_suffix(')') {
193                return Self::parse_expression(content.trim());
194            }
195        }
196
197        // Try to parse as percentage
198        if input.ends_with('%') {
199            let pct_str = input.trim_end_matches('%').trim();
200            let pct_value = pct_str
201                .parse::<f64>()
202                .map_err(|_| FopError::ParseError(format!("Invalid percentage: {}", input)))?;
203            return Ok(Expression::Percentage(Percentage::from_percent(pct_value)));
204        }
205
206        // Try to parse as length
207        Self::parse_length(input)
208    }
209
210    /// Find the position of an operator at the top level (not in parentheses)
211    fn find_operator(input: &str, operators: &[char]) -> Option<usize> {
212        let mut depth = 0;
213        let mut last_op_pos = None;
214
215        for (i, c) in input.chars().enumerate() {
216            match c {
217                '(' => depth += 1,
218                ')' => depth -= 1,
219                _ if depth == 0 && operators.contains(&c) => {
220                    last_op_pos = Some(i);
221                }
222                _ => {}
223            }
224        }
225
226        last_op_pos
227    }
228
229    /// Parse a length value with units
230    fn parse_length(input: &str) -> Result<Self> {
231        let input = input.trim();
232
233        // Find where the number ends and the unit begins
234        let mut split_pos = 0;
235        for (i, c) in input.chars().enumerate() {
236            if !c.is_numeric() && c != '.' && c != '-' && c != '+' {
237                split_pos = i;
238                break;
239            }
240        }
241
242        if split_pos == 0 {
243            return Err(FopError::ParseError(format!("Invalid length: {}", input)));
244        }
245
246        let value_str = &input[..split_pos];
247        let unit = &input[split_pos..];
248
249        let value = value_str
250            .parse::<f64>()
251            .map_err(|_| FopError::ParseError(format!("Invalid number: {}", value_str)))?;
252
253        let length = match unit {
254            "pt" => Length::from_pt(value),
255            "mm" => Length::from_mm(value),
256            "cm" => Length::from_cm(value),
257            "in" => Length::from_inch(value),
258            _ => return Err(FopError::ParseError(format!("Unknown unit: {}", unit))),
259        };
260
261        Ok(Expression::Literal(length))
262    }
263
264    /// Evaluate the expression to a concrete length
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// use fop_types::{Expression, Length, expression::EvalContext};
270    ///
271    /// let expr = Expression::parse("calc(100% - 20pt)").unwrap();
272    /// let ctx = EvalContext::with_width(Length::from_pt(200.0), Length::from_pt(12.0));
273    /// let result = expr.evaluate(&ctx).unwrap();
274    /// assert_eq!(result, Length::from_pt(180.0));
275    /// ```
276    pub fn evaluate(&self, context: &EvalContext) -> Result<Length> {
277        match self {
278            Expression::Literal(len) => Ok(*len),
279
280            Expression::Percentage(pct) => {
281                // Use base_width as the default base for percentage resolution
282                let base = context.base_width.ok_or_else(|| {
283                    FopError::Generic("No base value available for percentage".to_string())
284                })?;
285                Ok(pct.of(base))
286            }
287
288            Expression::Add(left, right) => {
289                let left_val = left.evaluate(context)?;
290                let right_val = right.evaluate(context)?;
291                Ok(left_val + right_val)
292            }
293
294            Expression::Sub(left, right) => {
295                let left_val = left.evaluate(context)?;
296                let right_val = right.evaluate(context)?;
297                Ok(left_val - right_val)
298            }
299
300            Expression::Mul(expr, scalar) => {
301                let val = expr.evaluate(context)?;
302                let millipoints = (val.millipoints() as f64 * scalar).round() as i32;
303                Ok(Length::from_millipoints(millipoints))
304            }
305
306            Expression::Div(expr, scalar) => {
307                let val = expr.evaluate(context)?;
308                let millipoints = (val.millipoints() as f64 / scalar).round() as i32;
309                Ok(Length::from_millipoints(millipoints))
310            }
311        }
312    }
313}
314
315impl fmt::Display for Expression {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        match self {
318            Expression::Literal(len) => write!(f, "{}", len),
319            Expression::Percentage(pct) => write!(f, "{}", pct),
320            Expression::Add(left, right) => write!(f, "({} + {})", left, right),
321            Expression::Sub(left, right) => write!(f, "({} - {})", left, right),
322            Expression::Mul(expr, scalar) => write!(f, "({} * {})", expr, scalar),
323            Expression::Div(expr, scalar) => write!(f, "({} / {})", expr, scalar),
324        }
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_parse_literal() {
334        let expr = Expression::parse("calc(20pt)").expect("test: should succeed");
335        assert_eq!(expr, Expression::Literal(Length::from_pt(20.0)));
336    }
337
338    #[test]
339    fn test_parse_percentage() {
340        let expr = Expression::parse("calc(50%)").expect("test: should succeed");
341        assert_eq!(expr, Expression::Percentage(Percentage::from_percent(50.0)));
342    }
343
344    #[test]
345    fn test_parse_addition() {
346        let expr = Expression::parse("calc(50% + 10pt)").expect("test: should succeed");
347        match expr {
348            Expression::Add(left, right) => {
349                assert_eq!(
350                    *left,
351                    Expression::Percentage(Percentage::from_percent(50.0))
352                );
353                assert_eq!(*right, Expression::Literal(Length::from_pt(10.0)));
354            }
355            _ => panic!("Expected Add expression"),
356        }
357    }
358
359    #[test]
360    fn test_parse_subtraction() {
361        let expr = Expression::parse("calc(100% - 20pt)").expect("test: should succeed");
362        match expr {
363            Expression::Sub(left, right) => {
364                assert_eq!(
365                    *left,
366                    Expression::Percentage(Percentage::from_percent(100.0))
367                );
368                assert_eq!(*right, Expression::Literal(Length::from_pt(20.0)));
369            }
370            _ => panic!("Expected Sub expression"),
371        }
372    }
373
374    #[test]
375    fn test_parse_multiplication() {
376        let expr = Expression::parse("calc(50% * 2)").expect("test: should succeed");
377        match expr {
378            Expression::Mul(inner, scalar) => {
379                assert_eq!(
380                    *inner,
381                    Expression::Percentage(Percentage::from_percent(50.0))
382                );
383                assert!((scalar - 2.0).abs() < 0.001);
384            }
385            _ => panic!("Expected Mul expression"),
386        }
387    }
388
389    #[test]
390    fn test_parse_division() {
391        let expr = Expression::parse("calc(100% / 3)").expect("test: should succeed");
392        match expr {
393            Expression::Div(inner, scalar) => {
394                assert_eq!(
395                    *inner,
396                    Expression::Percentage(Percentage::from_percent(100.0))
397                );
398                assert!((scalar - 3.0).abs() < 0.001);
399            }
400            _ => panic!("Expected Div expression"),
401        }
402    }
403
404    #[test]
405    fn test_parse_nested_expression() {
406        let expr = Expression::parse("calc((100% - 40pt) / 2)").expect("test: should succeed");
407        match expr {
408            Expression::Div(inner, scalar) => {
409                assert!((scalar - 2.0).abs() < 0.001);
410                match *inner {
411                    Expression::Sub(left, right) => {
412                        assert_eq!(
413                            *left,
414                            Expression::Percentage(Percentage::from_percent(100.0))
415                        );
416                        assert_eq!(*right, Expression::Literal(Length::from_pt(40.0)));
417                    }
418                    _ => panic!("Expected Sub expression inside Div"),
419                }
420            }
421            _ => panic!("Expected Div expression"),
422        }
423    }
424
425    #[test]
426    fn test_parse_mixed_units() {
427        let expr = Expression::parse("calc(100% - 10mm)").expect("test: should succeed");
428        match expr {
429            Expression::Sub(left, right) => {
430                assert_eq!(
431                    *left,
432                    Expression::Percentage(Percentage::from_percent(100.0))
433                );
434                assert_eq!(*right, Expression::Literal(Length::from_mm(10.0)));
435            }
436            _ => panic!("Expected Sub expression"),
437        }
438    }
439
440    #[test]
441    fn test_parse_invalid_no_calc() {
442        let result = Expression::parse("50%");
443        assert!(result.is_err());
444    }
445
446    #[test]
447    fn test_parse_invalid_division_by_zero() {
448        let result = Expression::parse("calc(100% / 0)");
449        assert!(result.is_err());
450    }
451
452    #[test]
453    fn test_evaluate_literal() {
454        let expr = Expression::Literal(Length::from_pt(20.0));
455        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
456        let result = expr.evaluate(&ctx).expect("test: should succeed");
457        assert_eq!(result, Length::from_pt(20.0));
458    }
459
460    #[test]
461    fn test_evaluate_percentage() {
462        let expr = Expression::Percentage(Percentage::from_percent(50.0));
463        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
464        let result = expr.evaluate(&ctx).expect("test: should succeed");
465        assert_eq!(result, Length::from_pt(50.0));
466    }
467
468    #[test]
469    fn test_evaluate_addition() {
470        let expr = Expression::parse("calc(50% + 10pt)").expect("test: should succeed");
471        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
472        let result = expr.evaluate(&ctx).expect("test: should succeed");
473        // 50% of 100pt + 10pt = 50pt + 10pt = 60pt
474        assert_eq!(result, Length::from_pt(60.0));
475    }
476
477    #[test]
478    fn test_evaluate_subtraction() {
479        let expr = Expression::parse("calc(100% - 20pt)").expect("test: should succeed");
480        let ctx = EvalContext::with_width(Length::from_pt(200.0), Length::from_pt(12.0));
481        let result = expr.evaluate(&ctx).expect("test: should succeed");
482        // 100% of 200pt - 20pt = 200pt - 20pt = 180pt
483        assert_eq!(result, Length::from_pt(180.0));
484    }
485
486    #[test]
487    fn test_evaluate_multiplication() {
488        let expr = Expression::parse("calc(50pt * 2)").expect("test: should succeed");
489        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
490        let result = expr.evaluate(&ctx).expect("test: should succeed");
491        assert_eq!(result, Length::from_pt(100.0));
492    }
493
494    #[test]
495    fn test_evaluate_division() {
496        let expr = Expression::parse("calc(100pt / 3)").expect("test: should succeed");
497        let ctx = EvalContext::with_width(Length::from_pt(200.0), Length::from_pt(12.0));
498        let result = expr.evaluate(&ctx).expect("test: should succeed");
499        assert!((result.to_pt() - 33.333).abs() < 0.01);
500    }
501
502    #[test]
503    fn test_evaluate_nested() {
504        let expr = Expression::parse("calc((100% - 40pt) / 2)").expect("test: should succeed");
505        let ctx = EvalContext::with_width(Length::from_pt(200.0), Length::from_pt(12.0));
506        let result = expr.evaluate(&ctx).expect("test: should succeed");
507        // (100% of 200pt - 40pt) / 2 = (200pt - 40pt) / 2 = 160pt / 2 = 80pt
508        assert_eq!(result, Length::from_pt(80.0));
509    }
510
511    #[test]
512    fn test_evaluate_complex_nested() {
513        let expr = Expression::parse("calc(50% + (20pt * 2))").expect("test: should succeed");
514        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
515        let result = expr.evaluate(&ctx).expect("test: should succeed");
516        // 50% of 100pt + (20pt * 2) = 50pt + 40pt = 90pt
517        assert_eq!(result, Length::from_pt(90.0));
518    }
519
520    #[test]
521    fn test_evaluate_percentage_no_base() {
522        let expr = Expression::Percentage(Percentage::from_percent(50.0));
523        let ctx = EvalContext::new(None, None, Length::from_pt(12.0));
524        let result = expr.evaluate(&ctx);
525        assert!(result.is_err());
526    }
527
528    #[test]
529    fn test_display() {
530        let expr = Expression::parse("calc(100% - 20pt)").expect("test: should succeed");
531        let display = format!("{}", expr);
532        assert!(display.contains("100%"));
533        assert!(display.contains("20pt"));
534    }
535
536    #[test]
537    fn test_parse_millimeters() {
538        let expr = Expression::parse("calc(10mm)").expect("test: should succeed");
539        assert_eq!(expr, Expression::Literal(Length::from_mm(10.0)));
540    }
541
542    #[test]
543    fn test_parse_centimeters() {
544        let expr = Expression::parse("calc(2.5cm)").expect("test: should succeed");
545        assert_eq!(expr, Expression::Literal(Length::from_cm(2.5)));
546    }
547
548    #[test]
549    fn test_parse_inches() {
550        let expr = Expression::parse("calc(1in)").expect("test: should succeed");
551        assert_eq!(expr, Expression::Literal(Length::from_inch(1.0)));
552    }
553}
554
555#[cfg(test)]
556mod expression_extra_tests {
557    use super::*;
558
559    // --- EvalContext constructors ---
560
561    #[test]
562    fn test_eval_context_new() {
563        let ctx = EvalContext::new(
564            Some(Length::from_pt(100.0)),
565            Some(Length::from_pt(200.0)),
566            Length::from_pt(12.0),
567        );
568        assert_eq!(ctx.base_width, Some(Length::from_pt(100.0)));
569        assert_eq!(ctx.base_height, Some(Length::from_pt(200.0)));
570        assert_eq!(ctx.font_size, Length::from_pt(12.0));
571    }
572
573    #[test]
574    fn test_eval_context_with_width() {
575        let ctx = EvalContext::with_width(Length::from_pt(400.0), Length::from_pt(10.0));
576        assert_eq!(ctx.base_width, Some(Length::from_pt(400.0)));
577        assert!(ctx.base_height.is_none());
578    }
579
580    #[test]
581    fn test_eval_context_with_height() {
582        let ctx = EvalContext::with_height(Length::from_pt(300.0), Length::from_pt(10.0));
583        assert!(ctx.base_width.is_none());
584        assert_eq!(ctx.base_height, Some(Length::from_pt(300.0)));
585    }
586
587    #[test]
588    fn test_eval_context_no_base() {
589        let ctx = EvalContext::new(None, None, Length::from_pt(12.0));
590        assert!(ctx.base_width.is_none());
591        assert!(ctx.base_height.is_none());
592    }
593
594    // --- Expression variants ---
595
596    #[test]
597    fn test_literal_expression_evaluate() {
598        let expr = Expression::Literal(Length::from_mm(10.0));
599        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
600        let result = expr.evaluate(&ctx).expect("test: should succeed");
601        assert!((result.to_mm() - 10.0).abs() < 0.01);
602    }
603
604    #[test]
605    fn test_percentage_expression_25_percent() {
606        let expr = Expression::Percentage(Percentage::from_percent(25.0));
607        let ctx = EvalContext::with_width(Length::from_pt(200.0), Length::from_pt(12.0));
608        let result = expr.evaluate(&ctx).expect("test: should succeed");
609        assert!((result.to_pt() - 50.0).abs() < 0.01);
610    }
611
612    #[test]
613    fn test_percentage_no_base_errors() {
614        let expr = Expression::Percentage(Percentage::from_percent(50.0));
615        let ctx = EvalContext::new(None, None, Length::from_pt(12.0));
616        assert!(expr.evaluate(&ctx).is_err());
617    }
618
619    #[test]
620    fn test_add_two_literals() {
621        let expr = Expression::Add(
622            Box::new(Expression::Literal(Length::from_pt(30.0))),
623            Box::new(Expression::Literal(Length::from_pt(20.0))),
624        );
625        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
626        let result = expr.evaluate(&ctx).expect("test: should succeed");
627        assert!((result.to_pt() - 50.0).abs() < 0.01);
628    }
629
630    #[test]
631    fn test_sub_two_literals() {
632        let expr = Expression::Sub(
633            Box::new(Expression::Literal(Length::from_pt(30.0))),
634            Box::new(Expression::Literal(Length::from_pt(12.0))),
635        );
636        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
637        let result = expr.evaluate(&ctx).expect("test: should succeed");
638        assert!((result.to_pt() - 18.0).abs() < 0.01);
639    }
640
641    #[test]
642    fn test_mul_literal_by_scalar() {
643        let expr = Expression::Mul(Box::new(Expression::Literal(Length::from_pt(15.0))), 4.0);
644        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
645        let result = expr.evaluate(&ctx).expect("test: should succeed");
646        assert!((result.to_pt() - 60.0).abs() < 0.01);
647    }
648
649    #[test]
650    fn test_div_literal_by_scalar() {
651        let expr = Expression::Div(Box::new(Expression::Literal(Length::from_pt(90.0))), 3.0);
652        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
653        let result = expr.evaluate(&ctx).expect("test: should succeed");
654        assert!((result.to_pt() - 30.0).abs() < 0.01);
655    }
656
657    // --- Parsing ---
658
659    #[test]
660    fn test_parse_literal_mm() {
661        let expr = Expression::parse("calc(25mm)").expect("test: should succeed");
662        assert_eq!(expr, Expression::Literal(Length::from_mm(25.0)));
663    }
664
665    #[test]
666    fn test_parse_literal_cm() {
667        let expr = Expression::parse("calc(5cm)").expect("test: should succeed");
668        assert_eq!(expr, Expression::Literal(Length::from_cm(5.0)));
669    }
670
671    #[test]
672    fn test_parse_literal_in() {
673        let expr = Expression::parse("calc(2in)").expect("test: should succeed");
674        assert_eq!(expr, Expression::Literal(Length::from_inch(2.0)));
675    }
676
677    #[test]
678    fn test_parse_literal_pt() {
679        let expr = Expression::parse("calc(72pt)").expect("test: should succeed");
680        assert_eq!(expr, Expression::Literal(Length::from_pt(72.0)));
681    }
682
683    #[test]
684    fn test_parse_100_percent() {
685        let expr = Expression::parse("calc(100%)").expect("test: should succeed");
686        assert_eq!(
687            expr,
688            Expression::Percentage(Percentage::from_percent(100.0))
689        );
690    }
691
692    #[test]
693    fn test_parse_0_percent() {
694        let expr = Expression::parse("calc(0%)").expect("test: should succeed");
695        assert_eq!(expr, Expression::Percentage(Percentage::ZERO));
696    }
697
698    #[test]
699    fn test_parse_requires_calc_wrapper() {
700        assert!(Expression::parse("100%").is_err());
701        assert!(Expression::parse("10pt").is_err());
702        assert!(Expression::parse("50% + 10pt").is_err());
703    }
704
705    #[test]
706    fn test_parse_div_by_zero_errors() {
707        assert!(Expression::parse("calc(100% / 0)").is_err());
708        assert!(Expression::parse("calc(100pt / 0)").is_err());
709    }
710
711    #[test]
712    fn test_parse_add_mm_plus_pt() {
713        let expr = Expression::parse("calc(10mm + 20pt)").expect("test: should succeed");
714        match expr {
715            Expression::Add(left, right) => {
716                assert_eq!(*left, Expression::Literal(Length::from_mm(10.0)));
717                assert_eq!(*right, Expression::Literal(Length::from_pt(20.0)));
718            }
719            _ => panic!("Expected Add"),
720        }
721    }
722
723    // --- Evaluate parsed expressions ---
724
725    #[test]
726    fn test_eval_100pct_minus_margin_a4_width() {
727        // Simulate: page content width = 100% - 2*margin(20pt)
728        let expr = Expression::parse("calc(100% - 40pt)").expect("test: should succeed");
729        let ctx = EvalContext::with_width(Length::from_pt(595.0), Length::from_pt(12.0));
730        let result = expr.evaluate(&ctx).expect("test: should succeed");
731        assert!((result.to_pt() - 555.0).abs() < 0.1);
732    }
733
734    #[test]
735    fn test_eval_mul_gives_larger_result() {
736        let expr = Expression::parse("calc(10pt * 5)").expect("test: should succeed");
737        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
738        let result = expr.evaluate(&ctx).expect("test: should succeed");
739        assert!((result.to_pt() - 50.0).abs() < 0.01);
740    }
741
742    #[test]
743    fn test_eval_nested_add_in_div() {
744        // calc((10pt + 20pt) / 3) = 30pt / 3 = 10pt
745        let expr = Expression::parse("calc((10pt + 20pt) / 3)").expect("test: should succeed");
746        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
747        let result = expr.evaluate(&ctx).expect("test: should succeed");
748        assert!((result.to_pt() - 10.0).abs() < 0.1);
749    }
750
751    // --- Display ---
752
753    #[test]
754    fn test_display_literal() {
755        let expr = Expression::Literal(Length::from_pt(10.0));
756        let s = format!("{}", expr);
757        assert!(s.contains("10pt"));
758    }
759
760    #[test]
761    fn test_display_percentage() {
762        let expr = Expression::Percentage(Percentage::from_percent(50.0));
763        let s = format!("{}", expr);
764        assert!(s.contains("50%"));
765    }
766
767    #[test]
768    fn test_display_add() {
769        let expr = Expression::Add(
770            Box::new(Expression::Literal(Length::from_pt(10.0))),
771            Box::new(Expression::Literal(Length::from_pt(20.0))),
772        );
773        let s = format!("{}", expr);
774        assert!(s.contains('+'));
775    }
776
777    #[test]
778    fn test_display_sub() {
779        let expr = Expression::Sub(
780            Box::new(Expression::Literal(Length::from_pt(10.0))),
781            Box::new(Expression::Literal(Length::from_pt(5.0))),
782        );
783        let s = format!("{}", expr);
784        assert!(s.contains('-'));
785    }
786
787    #[test]
788    fn test_display_mul() {
789        let expr = Expression::Mul(Box::new(Expression::Literal(Length::from_pt(10.0))), 3.0);
790        let s = format!("{}", expr);
791        assert!(s.contains('*'));
792    }
793
794    #[test]
795    fn test_display_div() {
796        let expr = Expression::Div(Box::new(Expression::Literal(Length::from_pt(10.0))), 2.0);
797        let s = format!("{}", expr);
798        assert!(s.contains('/'));
799    }
800
801    // --- Clone / PartialEq ---
802
803    #[test]
804    fn test_expression_clone() {
805        let expr = Expression::Literal(Length::from_pt(12.0));
806        let cloned = expr.clone();
807        assert_eq!(expr, cloned);
808    }
809
810    #[test]
811    fn test_expression_inequality() {
812        let a = Expression::Literal(Length::from_pt(10.0));
813        let b = Expression::Literal(Length::from_pt(20.0));
814        assert_ne!(a, b);
815    }
816}
817
818#[cfg(test)]
819mod expression_eval_tests {
820    use super::*;
821
822    // --- Parsing various unit types ---
823
824    #[test]
825    fn test_parse_pt_literal() {
826        let expr = Expression::parse("calc(12pt)").expect("test: should succeed");
827        assert_eq!(expr, Expression::Literal(Length::from_pt(12.0)));
828    }
829
830    #[test]
831    fn test_parse_mm_literal() {
832        let expr = Expression::parse("calc(10mm)").expect("test: should succeed");
833        assert_eq!(expr, Expression::Literal(Length::from_mm(10.0)));
834    }
835
836    #[test]
837    fn test_parse_cm_literal() {
838        let expr = Expression::parse("calc(2cm)").expect("test: should succeed");
839        assert_eq!(expr, Expression::Literal(Length::from_cm(2.0)));
840    }
841
842    #[test]
843    fn test_parse_in_literal() {
844        let expr = Expression::parse("calc(1in)").expect("test: should succeed");
845        assert_eq!(expr, Expression::Literal(Length::from_inch(1.0)));
846    }
847
848    #[test]
849    fn test_parse_zero_percent() {
850        let expr = Expression::parse("calc(0%)").expect("test: should succeed");
851        assert_eq!(expr, Expression::Percentage(Percentage::ZERO));
852    }
853
854    #[test]
855    fn test_parse_100_percent() {
856        let expr = Expression::parse("calc(100%)").expect("test: should succeed");
857        assert_eq!(
858            expr,
859            Expression::Percentage(Percentage::from_percent(100.0))
860        );
861    }
862
863    #[test]
864    fn test_parse_fractional_percent() {
865        let expr = Expression::parse("calc(33%)").expect("test: should succeed");
866        match expr {
867            Expression::Percentage(p) => {
868                assert!((p.to_percent() - 33.0).abs() < 0.001);
869            }
870            _ => panic!("Expected Percentage"),
871        }
872    }
873
874    // --- Addition ---
875
876    #[test]
877    fn test_parse_add_pt_plus_pt() {
878        let expr = Expression::parse("calc(10pt + 20pt)").expect("test: should succeed");
879        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
880        let result = expr.evaluate(&ctx).expect("test: should succeed");
881        assert!((result.to_pt() - 30.0).abs() < 0.01);
882    }
883
884    #[test]
885    fn test_parse_add_pct_plus_pt() {
886        let expr = Expression::parse("calc(50% + 10pt)").expect("test: should succeed");
887        let ctx = EvalContext::with_width(Length::from_pt(200.0), Length::from_pt(12.0));
888        let result = expr.evaluate(&ctx).expect("test: should succeed");
889        // 50% of 200 = 100 + 10 = 110
890        assert!((result.to_pt() - 110.0).abs() < 0.01);
891    }
892
893    #[test]
894    fn test_parse_add_mm_plus_pt() {
895        let expr = Expression::parse("calc(10mm + 20pt)").expect("test: should succeed");
896        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
897        let result = expr.evaluate(&ctx).expect("test: should succeed");
898        let expected = Length::from_mm(10.0) + Length::from_pt(20.0);
899        assert!((result.to_pt() - expected.to_pt()).abs() < 0.1);
900    }
901
902    // --- Subtraction ---
903
904    #[test]
905    fn test_parse_subtract_pt_from_pct() {
906        let expr = Expression::parse("calc(100% - 20pt)").expect("test: should succeed");
907        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
908        let result = expr.evaluate(&ctx).expect("test: should succeed");
909        assert!((result.to_pt() - 80.0).abs() < 0.01);
910    }
911
912    #[test]
913    fn test_parse_subtract_larger_from_smaller_gives_negative() {
914        let expr = Expression::parse("calc(10pt - 30pt)").expect("test: should succeed");
915        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
916        let result = expr.evaluate(&ctx).expect("test: should succeed");
917        assert!(result.to_pt() < 0.0);
918        assert!((result.to_pt() - (-20.0)).abs() < 0.01);
919    }
920
921    // --- Multiplication ---
922
923    #[test]
924    fn test_parse_mul_pt_by_scalar() {
925        let expr = Expression::parse("calc(10pt * 5)").expect("test: should succeed");
926        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
927        let result = expr.evaluate(&ctx).expect("test: should succeed");
928        assert!((result.to_pt() - 50.0).abs() < 0.01);
929    }
930
931    #[test]
932    fn test_parse_mul_pct_by_scalar() {
933        let expr = Expression::parse("calc(50% * 2)").expect("test: should succeed");
934        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
935        let result = expr.evaluate(&ctx).expect("test: should succeed");
936        // 50% of 100 * 2 via mul: first eval pct (=50pt) then * 2 = 100pt
937        // However the expression structure is Mul(Pct(50%), 2) so result = 50 * 2 = 100pt
938        assert!((result.to_pt() - 100.0).abs() < 0.01);
939    }
940
941    // --- Division ---
942
943    #[test]
944    fn test_parse_div_pt_by_scalar() {
945        let expr = Expression::parse("calc(60pt / 4)").expect("test: should succeed");
946        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
947        let result = expr.evaluate(&ctx).expect("test: should succeed");
948        assert!((result.to_pt() - 15.0).abs() < 0.01);
949    }
950
951    #[test]
952    fn test_parse_div_by_zero_fails() {
953        assert!(Expression::parse("calc(100% / 0)").is_err());
954        assert!(Expression::parse("calc(100pt / 0)").is_err());
955    }
956
957    // --- Nested expressions ---
958
959    #[test]
960    fn test_nested_paren_sub_then_div() {
961        // calc((100% - 40pt) / 2)
962        let expr = Expression::parse("calc((100% - 40pt) / 2)").expect("test: should succeed");
963        let ctx = EvalContext::with_width(Length::from_pt(200.0), Length::from_pt(12.0));
964        let result = expr.evaluate(&ctx).expect("test: should succeed");
965        // (200 - 40) / 2 = 80pt
966        assert!((result.to_pt() - 80.0).abs() < 0.01);
967    }
968
969    #[test]
970    fn test_nested_paren_add_in_mul() {
971        // calc((10pt + 20pt) * 3)
972        let expr = Expression::parse("calc((10pt + 20pt) * 3)").expect("test: should succeed");
973        let ctx = EvalContext::with_width(Length::from_pt(100.0), Length::from_pt(12.0));
974        let result = expr.evaluate(&ctx).expect("test: should succeed");
975        // (10 + 20) * 3 = 90pt
976        assert!((result.to_pt() - 90.0).abs() < 0.01);
977    }
978
979    #[test]
980    fn test_a4_content_width_calc() {
981        // A4 = 595pt wide; typical 2cm margins = 2 * 56.7pt ≈ 113.4pt
982        // Content = 100% - 2*margin
983        let expr = Expression::parse("calc(100% - 40pt)").expect("test: should succeed");
984        let ctx = EvalContext::with_width(Length::from_pt(595.0), Length::from_pt(12.0));
985        let result = expr.evaluate(&ctx).expect("test: should succeed");
986        assert!((result.to_pt() - 555.0).abs() < 0.1);
987    }
988
989    // --- No base value for percentage ---
990
991    #[test]
992    fn test_eval_percentage_no_base_returns_error() {
993        let expr = Expression::parse("calc(50%)").expect("test: should succeed");
994        let ctx = EvalContext::new(None, None, Length::from_pt(12.0));
995        assert!(expr.evaluate(&ctx).is_err());
996    }
997
998    // --- Error: missing calc() wrapper ---
999
1000    #[test]
1001    fn test_parse_no_calc_wrapper_fails() {
1002        assert!(Expression::parse("100%").is_err());
1003        assert!(Expression::parse("10pt + 5pt").is_err());
1004        assert!(Expression::parse("50mm").is_err());
1005    }
1006
1007    // --- Display ---
1008
1009    #[test]
1010    fn test_display_contains_operands() {
1011        let expr = Expression::parse("calc(100% - 20pt)").expect("test: should succeed");
1012        let s = format!("{}", expr);
1013        assert!(s.contains("100%"));
1014        assert!(s.contains("20pt"));
1015        assert!(s.contains('-'));
1016    }
1017
1018    #[test]
1019    fn test_display_literal_pt() {
1020        let expr = Expression::Literal(Length::from_pt(36.0));
1021        assert_eq!(format!("{}", expr), "36pt");
1022    }
1023
1024    #[test]
1025    fn test_display_percentage() {
1026        let expr = Expression::Percentage(Percentage::from_percent(75.0));
1027        let s = format!("{}", expr);
1028        assert!(s.contains("75%"));
1029    }
1030
1031    // --- Clone and PartialEq ---
1032
1033    #[test]
1034    fn test_clone_and_eq() {
1035        let expr = Expression::parse("calc(50% + 10pt)").expect("test: should succeed");
1036        let cloned = expr.clone();
1037        assert_eq!(expr, cloned);
1038    }
1039
1040    #[test]
1041    fn test_ne_different_expressions() {
1042        let a = Expression::Literal(Length::from_pt(10.0));
1043        let b = Expression::Literal(Length::from_pt(20.0));
1044        assert_ne!(a, b);
1045    }
1046
1047    // --- EvalContext helpers ---
1048
1049    #[test]
1050    fn test_eval_context_with_width_has_no_height() {
1051        let ctx = EvalContext::with_width(Length::from_pt(200.0), Length::from_pt(12.0));
1052        assert!(ctx.base_height.is_none());
1053        assert_eq!(ctx.base_width, Some(Length::from_pt(200.0)));
1054    }
1055
1056    #[test]
1057    fn test_eval_context_with_height_has_no_width() {
1058        let ctx = EvalContext::with_height(Length::from_pt(300.0), Length::from_pt(12.0));
1059        assert!(ctx.base_width.is_none());
1060        assert_eq!(ctx.base_height, Some(Length::from_pt(300.0)));
1061    }
1062}