• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MyIntervals / emogrifier / 19411845501

16 Nov 2025 08:50PM UTC coverage: 96.432% (-0.09%) from 96.518%
19411845501

Pull #1503

github

web-flow
Merge 1cbb08bb4 into 84b03c746
Pull Request #1503: [TASK] Use safe preg functions in `CssVariableEvaluator`

7 of 8 new or added lines in 1 file covered. (87.5%)

892 of 925 relevant lines covered (96.43%)

244.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.51
/src/HtmlProcessor/CssVariableEvaluator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Pelago\Emogrifier\HtmlProcessor;
6

7
use Pelago\Emogrifier\Utilities\DeclarationBlockParser;
8

9
use function Safe\preg_match;
10
use function Safe\preg_replace_callback;
11

12
/**
13
 * This class can evaluate CSS custom properties that are defined and used in inline style attributes.
14
 */
15
final class CssVariableEvaluator extends AbstractHtmlProcessor
16
{
17
    /**
18
     * temporary collection used by {@see replaceVariablesInDeclarations} and callee methods
19
     *
20
     * @var array<non-empty-string, string>
21
     */
22
    private $currentVariableDefinitions = [];
23

24
    /**
25
     * Replaces all CSS custom property references in inline style attributes with their corresponding values where
26
     * defined in inline style attributes (either from the element itself or the nearest ancestor).
27
     *
28
     * @return $this
29
     *
30
     * @throws \UnexpectedValueException
31
     */
32
    public function evaluateVariables(): self
29✔
33
    {
34
        return $this->evaluateVariablesInElementAndDescendants($this->getHtmlElement(), []);
29✔
35
    }
36

37
    /**
38
     * @param array<non-empty-string, string> $declarations
39
     *
40
     * @return array<non-empty-string, string>
41
     */
42
    private function getVariableDefinitionsFromDeclarations(array $declarations): array
28✔
43
    {
44
        return \array_filter(
28✔
45
            $declarations,
28✔
46
            static function (string $key): bool {
28✔
47
                return \substr($key, 0, 2) === '--';
28✔
48
            },
28✔
49
            ARRAY_FILTER_USE_KEY
28✔
50
        );
28✔
51
    }
52

53
    /**
54
     * Callback function for {@see replaceVariablesInPropertyValue} performing regular expression replacement.
55
     *
56
     * @param array<int, string> $matches
57
     */
58
    private function getPropertyValueReplacement(array $matches): string
28✔
59
    {
60
        $variableName = $matches[1];
28✔
61

62
        if (isset($this->currentVariableDefinitions[$variableName])) {
28✔
63
            $variableValue = $this->currentVariableDefinitions[$variableName];
13✔
64
        } else {
65
            $fallbackValueSeparator = $matches[2] ?? '';
16✔
66
            if ($fallbackValueSeparator !== '') {
16✔
67
                $fallbackValue = $matches[3];
14✔
68
                // The fallback value may use other CSS variables, so recurse
69
                $variableValue = $this->replaceVariablesInPropertyValue($fallbackValue);
14✔
70
            } else {
71
                $variableValue = $matches[0];
3✔
72
            }
73
        }
74

75
        return $variableValue;
28✔
76
    }
77

78
    /**
79
     * Regular expression based on {@see https://stackoverflow.com/a/54143883/2511031 a StackOverflow answer}.
80
     */
81
    private function replaceVariablesInPropertyValue(string $propertyValue): string
28✔
82
    {
83
        $pattern = '/
28✔
84
                var\\(
85
                    \\s*+
86
                    # capture variable name including `--` prefix
87
                    (
88
                        --[^\\s\\),]++
89
                    )
90
                    \\s*+
91
                    # capture optional fallback value
92
                    (?:
93
                        # capture separator to confirm there is a fallback value
94
                        (,)\\s*
95
                        # begin capture with named group that can be used recursively
96
                        (?<recursable>
97
                            # begin named group to match sequence without parentheses, except in strings
98
                            (?<noparentheses>
99
                                # repeated zero or more times:
100
                                (?:
101
                                    # sequence without parentheses or quotes
102
                                    [^\\(\\)\'"]++
103
                                    |
104
                                    # string in double quotes
105
                                    "(?>[^"\\\\]++|\\\\.)*"
106
                                    |
107
                                    # string in single quotes
108
                                    \'(?>[^\'\\\\]++|\\\\.)*\'
109
                                )*+
110
                            )
111
                            # repeated zero or more times:
112
                            (?:
113
                                # sequence in parentheses
114
                                \\(
115
                                    # using the named recursable pattern
116
                                    (?&recursable)
117
                                \\)
118
                                # sequence without parentheses, except in strings
119
                                (?&noparentheses)
120
                            )*+
121
                        )
122
                    )?+
123
                \\)
124
            /x';
28✔
125

126
        $callable = function (array $matches): string {
28✔
127
            /** @var array<int, string> $matches */
128
            return $this->getPropertyValueReplacement($matches);
28✔
129
        };
28✔
130
        // The safe version is only available in "thecodingmachine/safe" for PHP >= 8.1.
131
        if (\function_exists('Safe\\preg_replace_callback')) {
28✔
132
            $result = preg_replace_callback($pattern, $callable, $propertyValue);
28✔
133
        } else {
NEW
134
            $result = \preg_replace_callback($pattern, $callable, $propertyValue);
×
135
        }
136
        \assert(\is_string($result));
28✔
137

138
        return $result;
28✔
139
    }
140

141
    /**
142
     * @param array<non-empty-string, string> $declarations
143
     *
144
     * @return array<non-empty-string, string>|null `null` is returned if no substitutions were made.
145
     */
146
    private function replaceVariablesInDeclarations(array $declarations): ?array
28✔
147
    {
148
        $substitutionsMade = false;
28✔
149
        $result = \array_map(
28✔
150
            function (string $propertyValue) use (&$substitutionsMade): string {
28✔
151
                $newPropertyValue = $this->replaceVariablesInPropertyValue($propertyValue);
28✔
152
                if ($newPropertyValue !== $propertyValue) {
28✔
153
                    $substitutionsMade = true;
26✔
154
                }
155
                return $newPropertyValue;
28✔
156
            },
28✔
157
            $declarations
28✔
158
        );
28✔
159

160
        return $substitutionsMade ? $result : null;
28✔
161
    }
162

163
    /**
164
     * @param array<non-empty-string, string> $declarations
165
     */
166
    private function getDeclarationsAsString(array $declarations): string
26✔
167
    {
168
        $declarationStrings = \array_map(
26✔
169
            static function (string $key, string $value): string {
26✔
170
                return $key . ': ' . $value;
26✔
171
            },
26✔
172
            \array_keys($declarations),
26✔
173
            \array_values($declarations)
26✔
174
        );
26✔
175

176
        return \implode('; ', $declarationStrings) . ';';
26✔
177
    }
178

179
    /**
180
     * @param array<non-empty-string, string> $ancestorVariableDefinitions
181
     *
182
     * @return $this
183
     */
184
    private function evaluateVariablesInElementAndDescendants(
29✔
185
        \DOMElement $element,
186
        array $ancestorVariableDefinitions
187
    ): self {
188
        $style = $element->getAttribute('style');
29✔
189

190
        // Avoid parsing declarations if none use or define a variable
191
        if (preg_match('/(?<![\\w\\-])--[\\w\\-]/', $style) !== 0) {
29✔
192
            $declarations = (new DeclarationBlockParser())->parse($style);
28✔
193
            $variableDefinitions =
28✔
194
                $this->getVariableDefinitionsFromDeclarations($declarations) + $ancestorVariableDefinitions;
28✔
195
            $this->currentVariableDefinitions = $variableDefinitions;
28✔
196

197
            $newDeclarations = $this->replaceVariablesInDeclarations($declarations);
28✔
198
            if ($newDeclarations !== null) {
28✔
199
                $element->setAttribute('style', $this->getDeclarationsAsString($newDeclarations));
26✔
200
            }
201
        } else {
202
            $variableDefinitions = $ancestorVariableDefinitions;
29✔
203
        }
204

205
        foreach ($element->childNodes as $child) {
29✔
206
            if ($child instanceof \DOMElement) {
29✔
207
                $this->evaluateVariablesInElementAndDescendants($child, $variableDefinitions);
29✔
208
            }
209
        }
210

211
        return $this;
29✔
212
    }
213
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc