Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.06% covered (success)
97.06%
66 / 68
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
CalcEvaluator
97.06% covered (success)
97.06%
66 / 68
60.00% covered (warning)
60.00%
3 / 5
66
0.00% covered (danger)
0.00%
0 / 1
 evaluate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 eval
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
12.02
 resolveLeaf
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
7.02
 callFunc
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
43
 resolveValue
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Css\Cascade;
6
7use Phpdftk\Css\Value\Calc;
8use Phpdftk\Css\Value\CalcBinary;
9use Phpdftk\Css\Value\CalcExpression;
10use Phpdftk\Css\Value\CalcFunc;
11use Phpdftk\Css\Value\CalcFunction;
12use Phpdftk\Css\Value\CalcLeaf;
13use Phpdftk\Css\Value\CalcOp;
14use Phpdftk\Css\Value\Integer;
15use Phpdftk\Css\Value\Length;
16use Phpdftk\Css\Value\LengthUnit;
17use Phpdftk\Css\Value\Number;
18use Phpdftk\Css\Value\Percentage;
19use Phpdftk\Css\Value\Value;
20
21/**
22 * Reduce a {@see Calc} tree (or any {@see CalcExpression} subtree) to a
23 * concrete numeric value in CSS pixels against a {@see LengthContext}.
24 *
25 * Scope (CSS Values 4 §10):
26 *  - `calc()` — recursive arithmetic over Length / Percentage / Number leaves.
27 *  - `min()` / `max()` — variadic min / max over evaluated args.
28 *  - `clamp(min, val, max)` — clamps the middle arg.
29 *  - `abs()` / `sign()` / `hypot()` — straightforward unary / variadic helpers.
30 *  - Trig + power functions are out of v1 scope — they evaluate to NAN so
31 *    callers can detect and route to legacy length parsing.
32 *
33 * Leaf resolution:
34 *  - {@see Length} → {@see LengthResolver::toPx} (handles px / em / rem /
35 *    vw / vh / etc).
36 *  - {@see Percentage} → resolved against `LengthContext::percentageBasis`
37 *    if non-zero, otherwise returns NAN so the caller can defer.
38 *  - {@see Number} / {@see Integer} → the unitless value (callers use this
39 *    in multiplications inside the tree; a bare `calc(2)` outside a length
40 *    context degenerates to a unitless 2).
41 *
42 * Returns NAN whenever the tree contains an unresolvable percentage,
43 * an unsupported function, or a leaf that isn't a numeric primitive.
44 * Callers should treat NAN as "leave the Calc as-is, defer to the
45 * legacy Length parser, or skip evaluation".
46 */
47final class CalcEvaluator
48{
49    /** Evaluate a top-level `Calc` value to a pixel quantity. */
50    public static function evaluate(Calc $calc, LengthContext $ctx): float
51    {
52        return self::eval($calc->expression, $ctx);
53    }
54
55    public static function eval(CalcExpression $expr, LengthContext $ctx): float
56    {
57        if ($expr instanceof CalcLeaf) {
58            return self::resolveLeaf($expr->value, $ctx);
59        }
60        if ($expr instanceof CalcBinary) {
61            $left = self::eval($expr->left, $ctx);
62            $right = self::eval($expr->right, $ctx);
63            if (is_nan($left) || is_nan($right)) {
64                return NAN;
65            }
66            return match ($expr->op) {
67                CalcOp::Add => $left + $right,
68                CalcOp::Sub => $left - $right,
69                CalcOp::Mul => $left * $right,
70                CalcOp::Div => $right == 0.0 ? NAN : $left / $right,
71            };
72        }
73        if ($expr instanceof CalcFunc) {
74            $args = [];
75            foreach ($expr->args as $arg) {
76                $args[] = self::eval($arg, $ctx);
77            }
78            return self::callFunc($expr->func, $args);
79        }
80        return NAN;
81    }
82
83    private static function resolveLeaf(Value $value, LengthContext $ctx): float
84    {
85        if ($value instanceof Length) {
86            return LengthResolver::toPx($value, $ctx);
87        }
88        if ($value instanceof Percentage) {
89            // Percentage requires a basis. Zero basis = unknown to us;
90            // defer by returning NAN so the caller can leave the Calc
91            // untouched (e.g. background-position-percent resolves at
92            // paint time, not cascade time).
93            if ($ctx->percentageBasis === 0.0) {
94                return NAN;
95            }
96            return $value->value / 100.0 * $ctx->percentageBasis;
97        }
98        if ($value instanceof Number) {
99            return $value->value;
100        }
101        if ($value instanceof Integer) {
102            return (float) $value->value;
103        }
104        if ($value instanceof Calc) {
105            return self::evaluate($value, $ctx);
106        }
107        return NAN;
108    }
109
110    /**
111     * @param list<float> $args
112     */
113    private static function callFunc(CalcFunction $func, array $args): float
114    {
115        foreach ($args as $a) {
116            if (is_nan($a)) {
117                return NAN;
118            }
119        }
120        return match ($func) {
121            CalcFunction::Calc => $args[0] ?? NAN,
122            // PHP's `min()` / `max()` require an array or at least
123            // two arguments — the single-element case (`max(50%)`)
124            // would throw "must be of type array, float given".
125            CalcFunction::Min => count($args) === 1
126                ? $args[0]
127                : ($args === [] ? NAN : min(...$args)),
128            CalcFunction::Max => count($args) === 1
129                ? $args[0]
130                : ($args === [] ? NAN : max(...$args)),
131            CalcFunction::Clamp => count($args) === 3
132                ? max($args[0], min($args[1], $args[2]))
133                : NAN,
134            CalcFunction::Abs => count($args) === 1 ? abs($args[0]) : NAN,
135            CalcFunction::Sign => count($args) === 1
136                ? ($args[0] <=> 0.0)
137                : NAN,
138            CalcFunction::Hypot => $args === [] ? NAN : sqrt(array_sum(array_map(static fn(float $a): float => $a * $a, $args))),
139            CalcFunction::Sqrt => count($args) === 1 && $args[0] >= 0.0 ? sqrt($args[0]) : NAN,
140            CalcFunction::Pow => count($args) === 2 ? $args[0] ** $args[1] : NAN,
141            CalcFunction::Exp => count($args) === 1 ? exp($args[0]) : NAN,
142            CalcFunction::Log => count($args) === 1 ? log($args[0]) : (count($args) === 2 ? log($args[0], $args[1]) : NAN),
143            // Trig functions take radians per CSS Values 4 §10.8.
144            // Phase-1: only honour them when callers pre-converted any
145            // `deg` / `grad` / `turn` to radians at parse time.
146            CalcFunction::Sin => count($args) === 1 ? sin($args[0]) : NAN,
147            CalcFunction::Cos => count($args) === 1 ? cos($args[0]) : NAN,
148            CalcFunction::Tan => count($args) === 1 ? tan($args[0]) : NAN,
149            CalcFunction::Asin => count($args) === 1 ? asin($args[0]) : NAN,
150            CalcFunction::Acos => count($args) === 1 ? acos($args[0]) : NAN,
151            CalcFunction::Atan => count($args) === 1 ? atan($args[0]) : NAN,
152            CalcFunction::Atan2 => count($args) === 2 ? atan2($args[0], $args[1]) : NAN,
153            // round / mod / rem — out of v1 scope; defer.
154            CalcFunction::Round, CalcFunction::Mod, CalcFunction::Rem => NAN,
155        };
156    }
157
158    /**
159     * Reduce a {@see Calc} value to a {@see Length} when possible.
160     * Returns the original Calc untouched when evaluation hits a NAN
161     * (unresolvable percentage, unsupported function, etc.) so the
162     * caller can defer to later resolution.
163     */
164    public static function resolveValue(Value $value, LengthContext $ctx): Value
165    {
166        if (!$value instanceof Calc) {
167            return $value;
168        }
169        $px = self::evaluate($value, $ctx);
170        if (is_nan($px)) {
171            return $value;
172        }
173        return new Length($px, LengthUnit::Px);
174    }
175}