PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.35.0
MailPoet – Newsletters, Email Marketing, and Automation v5.35.0
5.36.1 5.36.0 5.35.1 5.35.0 5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / vendor-prefixed / pelago / emogrifier / src / CssInliner.php
mailpoet / vendor-prefixed / pelago / emogrifier / src Last commit date
Caching 2 years ago Css 2 years ago HtmlProcessor 2 years ago Utilities 2 years ago CssInliner.php 2 years ago index.php 2 years ago
CssInliner.php
522 lines
1 <?php
2 declare (strict_types=1);
3 namespace MailPoetVendor\Pelago\Emogrifier;
4 if (!defined('ABSPATH')) exit;
5 use MailPoetVendor\Pelago\Emogrifier\Css\CssDocument;
6 use MailPoetVendor\Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor;
7 use MailPoetVendor\Pelago\Emogrifier\Utilities\CssConcatenator;
8 use MailPoetVendor\Symfony\Component\CssSelector\CssSelectorConverter;
9 use MailPoetVendor\Symfony\Component\CssSelector\Exception\ParseException;
10 class CssInliner extends AbstractHtmlProcessor
11 {
12 private const CACHE_KEY_SELECTOR = 0;
13 private const CACHE_KEY_CSS_DECLARATIONS_BLOCK = 1;
14 private const CACHE_KEY_COMBINED_STYLES = 2;
15 private const PSEUDO_CLASS_MATCHER = 'empty|(?:first|last|nth(?:-last)?+|only)-(?:child|of-type)|not\\([[:ascii:]]*\\)';
16 private const OF_TYPE_PSEUDO_CLASS_MATCHER = '(?:first|last|nth(?:-last)?+|only)-of-type';
17 private const COMBINATOR_MATCHER = '(?:\\s++|\\s*+[>+~]\\s*+)(?=[[:alpha:]_\\-.#*:\\[])';
18 private $excludedSelectors = [];
19 private $allowedMediaTypes = ['all' => \true, 'screen' => \true, 'print' => \true];
20 private $caches = [self::CACHE_KEY_SELECTOR => [], self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [], self::CACHE_KEY_COMBINED_STYLES => []];
21 private $cssSelectorConverter = null;
22 private $visitedNodes = [];
23 private $styleAttributesForNodes = [];
24 private $isInlineStyleAttributesParsingEnabled = \true;
25 private $isStyleBlocksParsingEnabled = \true;
26 private $selectorPrecedenceMatchers = [
27 // IDs: worth 10000
28 '\\#' => 10000,
29 // classes, attributes, pseudo-classes (not pseudo-elements) except `:not`: worth 100
30 '(?:\\.|\\[|(?<!:):(?!not\\())' => 100,
31 // elements (not attribute values or `:not`), pseudo-elements: worth 1
32 '(?:(?<![="\':\\w\\-])|::)' => 1,
33 ];
34 private $matchingUninlinableCssRules = null;
35 private $debug = \false;
36 public function inlineCss(string $css = '') : self
37 {
38 $this->clearAllCaches();
39 $this->purgeVisitedNodes();
40 $this->normalizeStyleAttributesOfAllNodes();
41 $combinedCss = $css;
42 // grab any existing style blocks from the HTML and append them to the existing CSS
43 // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS)
44 if ($this->isStyleBlocksParsingEnabled) {
45 $combinedCss .= $this->getCssFromAllStyleNodes();
46 }
47 $parsedCss = new CssDocument($combinedCss, $this->debug);
48 $excludedNodes = $this->getNodesToExclude();
49 $cssRules = $this->collateCssRules($parsedCss);
50 $cssSelectorConverter = $this->getCssSelectorConverter();
51 foreach ($cssRules['inlinable'] as $cssRule) {
52 try {
53 $nodesMatchingCssSelectors = $this->getXPath()->query($cssSelectorConverter->toXPath($cssRule['selector']));
54 foreach ($nodesMatchingCssSelectors as $node) {
55 if (\in_array($node, $excludedNodes, \true)) {
56 continue;
57 }
58 $this->copyInlinableCssToStyleAttribute($node, $cssRule);
59 }
60 } catch (ParseException $e) {
61 if ($this->debug) {
62 throw $e;
63 }
64 }
65 }
66 if ($this->isInlineStyleAttributesParsingEnabled) {
67 $this->fillStyleAttributesWithMergedStyles();
68 }
69 $this->removeImportantAnnotationFromAllInlineStyles();
70 $this->determineMatchingUninlinableCssRules($cssRules['uninlinable']);
71 $this->copyUninlinableCssToStyleNode($parsedCss);
72 return $this;
73 }
74 public function disableInlineStyleAttributesParsing() : self
75 {
76 $this->isInlineStyleAttributesParsingEnabled = \false;
77 return $this;
78 }
79 public function disableStyleBlocksParsing() : self
80 {
81 $this->isStyleBlocksParsingEnabled = \false;
82 return $this;
83 }
84 public function addAllowedMediaType(string $mediaName) : self
85 {
86 $this->allowedMediaTypes[$mediaName] = \true;
87 return $this;
88 }
89 public function removeAllowedMediaType(string $mediaName) : self
90 {
91 if (isset($this->allowedMediaTypes[$mediaName])) {
92 unset($this->allowedMediaTypes[$mediaName]);
93 }
94 return $this;
95 }
96 public function addExcludedSelector(string $selector) : self
97 {
98 $this->excludedSelectors[$selector] = \true;
99 return $this;
100 }
101 public function removeExcludedSelector(string $selector) : self
102 {
103 if (isset($this->excludedSelectors[$selector])) {
104 unset($this->excludedSelectors[$selector]);
105 }
106 return $this;
107 }
108 public function setDebug(bool $debug) : self
109 {
110 $this->debug = $debug;
111 return $this;
112 }
113 public function getMatchingUninlinableSelectors() : array
114 {
115 return \array_column($this->getMatchingUninlinableCssRules(), 'selector');
116 }
117 private function getMatchingUninlinableCssRules() : array
118 {
119 if (!\is_array($this->matchingUninlinableCssRules)) {
120 throw new \BadMethodCallException('inlineCss must be called first', 1568385221);
121 }
122 return $this->matchingUninlinableCssRules;
123 }
124 private function clearAllCaches() : void
125 {
126 $this->caches = [self::CACHE_KEY_SELECTOR => [], self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [], self::CACHE_KEY_COMBINED_STYLES => []];
127 }
128 private function purgeVisitedNodes() : void
129 {
130 $this->visitedNodes = [];
131 $this->styleAttributesForNodes = [];
132 }
133 private function normalizeStyleAttributesOfAllNodes() : void
134 {
135 foreach ($this->getAllNodesWithStyleAttribute() as $node) {
136 if ($this->isInlineStyleAttributesParsingEnabled) {
137 $this->normalizeStyleAttributes($node);
138 }
139 // Remove style attribute in every case, so we can add them back (if inline style attributes
140 // parsing is enabled) to the end of the style list, thus keeping the right priority of CSS rules;
141 // else original inline style rules may remain at the beginning of the final inline style definition
142 // of a node, which may give not the desired results
143 $node->removeAttribute('style');
144 }
145 }
146 private function getAllNodesWithStyleAttribute() : \DOMNodeList
147 {
148 $query = '//*[@style]';
149 $matches = $this->getXPath()->query($query);
150 if (!$matches instanceof \DOMNodeList) {
151 throw new \RuntimeException('XPatch query failed: ' . $query, 1618577797);
152 }
153 return $matches;
154 }
155 private function normalizeStyleAttributes(\DOMElement $node) : void
156 {
157 $normalizedOriginalStyle = \preg_replace_callback(
158 '/-?+[_a-zA-Z][\\w\\-]*+(?=:)/S',
159 static function (array $propertyNameMatches) : string {
160 return \strtolower($propertyNameMatches[0]);
161 },
162 $node->getAttribute('style')
163 );
164 // In order to not overwrite existing style attributes in the HTML, we have to save the original HTML styles.
165 $nodePath = $node->getNodePath();
166 if (\is_string($nodePath) && !isset($this->styleAttributesForNodes[$nodePath])) {
167 $this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationsBlock($normalizedOriginalStyle);
168 $this->visitedNodes[$nodePath] = $node;
169 }
170 $node->setAttribute('style', $normalizedOriginalStyle);
171 }
172 private function parseCssDeclarationsBlock(string $cssDeclarationsBlock) : array
173 {
174 if (isset($this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock])) {
175 return $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock];
176 }
177 $properties = [];
178 foreach (\preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock) as $declaration) {
179 $matches = [];
180 if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) {
181 continue;
182 }
183 $propertyName = \strtolower($matches[1]);
184 $propertyValue = $matches[2];
185 $properties[$propertyName] = $propertyValue;
186 }
187 $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock] = $properties;
188 return $properties;
189 }
190 private function getCssFromAllStyleNodes() : string
191 {
192 $styleNodes = $this->getXPath()->query('//style');
193 if ($styleNodes === \false) {
194 return '';
195 }
196 $css = '';
197 foreach ($styleNodes as $styleNode) {
198 if (\is_string($styleNode->nodeValue)) {
199 $css .= "\n\n" . $styleNode->nodeValue;
200 }
201 $parentNode = $styleNode->parentNode;
202 if ($parentNode instanceof \DOMNode) {
203 $parentNode->removeChild($styleNode);
204 }
205 }
206 return $css;
207 }
208 private function getNodesToExclude() : array
209 {
210 $excludedNodes = [];
211 foreach (\array_keys($this->excludedSelectors) as $selectorToExclude) {
212 try {
213 $matchingNodes = $this->getXPath()->query($this->getCssSelectorConverter()->toXPath($selectorToExclude));
214 foreach ($matchingNodes as $node) {
215 if (!$node instanceof \DOMElement) {
216 $path = $node->getNodePath() ?? '$node';
217 throw new \UnexpectedValueException($path . ' is not a DOMElement.', 1617975914);
218 }
219 $excludedNodes[] = $node;
220 }
221 } catch (ParseException $e) {
222 if ($this->debug) {
223 throw $e;
224 }
225 }
226 }
227 return $excludedNodes;
228 }
229 private function getCssSelectorConverter() : CssSelectorConverter
230 {
231 if (!$this->cssSelectorConverter instanceof CssSelectorConverter) {
232 $this->cssSelectorConverter = new CssSelectorConverter();
233 }
234 return $this->cssSelectorConverter;
235 }
236 private function collateCssRules(CssDocument $parsedCss) : array
237 {
238 $matches = $parsedCss->getStyleRulesData(\array_keys($this->allowedMediaTypes));
239 $cssRules = ['inlinable' => [], 'uninlinable' => []];
240 foreach ($matches as $key => $cssRule) {
241 if (!$cssRule->hasAtLeastOneDeclaration()) {
242 continue;
243 }
244 $mediaQuery = $cssRule->getContainingAtRule();
245 $declarationsBlock = $cssRule->getDeclarationAsText();
246 foreach ($cssRule->getSelectors() as $selector) {
247 // don't process pseudo-elements and behavioral (dynamic) pseudo-classes;
248 // only allow structural pseudo-classes
249 $hasPseudoElement = \strpos($selector, '::') !== \false;
250 $hasUnmatchablePseudo = $hasPseudoElement || $this->hasUnsupportedPseudoClass($selector);
251 $parsedCssRule = [
252 'media' => $mediaQuery,
253 'selector' => $selector,
254 'hasUnmatchablePseudo' => $hasUnmatchablePseudo,
255 'declarationsBlock' => $declarationsBlock,
256 // keep track of where it appears in the file, since order is important
257 'line' => $key,
258 ];
259 $ruleType = !$cssRule->hasContainingAtRule() && !$hasUnmatchablePseudo ? 'inlinable' : 'uninlinable';
260 $cssRules[$ruleType][] = $parsedCssRule;
261 }
262 }
263 \usort(
264 $cssRules['inlinable'],
265 function (array $first, array $second) : int {
266 return $this->sortBySelectorPrecedence($first, $second);
267 }
268 );
269 return $cssRules;
270 }
271 private function hasUnsupportedPseudoClass(string $selector) : bool
272 {
273 if (\preg_match('/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i', $selector)) {
274 return \true;
275 }
276 if (!\preg_match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selector)) {
277 return \false;
278 }
279 foreach (\preg_split('/' . self::COMBINATOR_MATCHER . '/', $selector) as $selectorPart) {
280 if ($this->selectorPartHasUnsupportedOfTypePseudoClass($selectorPart)) {
281 return \true;
282 }
283 }
284 return \false;
285 }
286 private function selectorPartHasUnsupportedOfTypePseudoClass(string $selectorPart) : bool
287 {
288 if (\preg_match('/^[\\w\\-]/', $selectorPart)) {
289 return \false;
290 }
291 return (bool) \preg_match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selectorPart);
292 }
293 private function sortBySelectorPrecedence(array $first, array $second) : int
294 {
295 $precedenceOfFirst = $this->getCssSelectorPrecedence($first['selector']);
296 $precedenceOfSecond = $this->getCssSelectorPrecedence($second['selector']);
297 // We want these sorted in ascending order so selectors with lesser precedence get processed first and
298 // selectors with greater precedence get sorted last.
299 $precedenceForEquals = $first['line'] < $second['line'] ? -1 : 1;
300 $precedenceForNotEquals = $precedenceOfFirst < $precedenceOfSecond ? -1 : 1;
301 return $precedenceOfFirst === $precedenceOfSecond ? $precedenceForEquals : $precedenceForNotEquals;
302 }
303 private function getCssSelectorPrecedence(string $selector) : int
304 {
305 $selectorKey = \md5($selector);
306 if (isset($this->caches[self::CACHE_KEY_SELECTOR][$selectorKey])) {
307 return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey];
308 }
309 $precedence = 0;
310 foreach ($this->selectorPrecedenceMatchers as $matcher => $value) {
311 if (\trim($selector) === '') {
312 break;
313 }
314 $number = 0;
315 $selector = \preg_replace('/' . $matcher . '\\w+/', '', $selector, -1, $number);
316 $precedence += $value * (int) $number;
317 }
318 $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence;
319 return $precedence;
320 }
321 private function copyInlinableCssToStyleAttribute(\DOMElement $node, array $cssRule) : void
322 {
323 $declarationsBlock = $cssRule['declarationsBlock'];
324 $newStyleDeclarations = $this->parseCssDeclarationsBlock($declarationsBlock);
325 if ($newStyleDeclarations === []) {
326 return;
327 }
328 // if it has a style attribute, get it, process it, and append (overwrite) new stuff
329 if ($node->hasAttribute('style')) {
330 // break it up into an associative array
331 $oldStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
332 } else {
333 $oldStyleDeclarations = [];
334 }
335 $node->setAttribute('style', $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations));
336 }
337 private function generateStyleStringFromDeclarationsArrays(array $oldStyles, array $newStyles) : string
338 {
339 $cacheKey = \serialize([$oldStyles, $newStyles]);
340 if (isset($this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey])) {
341 return $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey];
342 }
343 // Unset the overridden styles to preserve order, important if shorthand and individual properties are mixed
344 foreach ($oldStyles as $attributeName => $attributeValue) {
345 if (!isset($newStyles[$attributeName])) {
346 continue;
347 }
348 $newAttributeValue = $newStyles[$attributeName];
349 if ($this->attributeValueIsImportant($attributeValue) && !$this->attributeValueIsImportant($newAttributeValue)) {
350 unset($newStyles[$attributeName]);
351 } else {
352 unset($oldStyles[$attributeName]);
353 }
354 }
355 $combinedStyles = \array_merge($oldStyles, $newStyles);
356 $style = '';
357 foreach ($combinedStyles as $attributeName => $attributeValue) {
358 $style .= \strtolower(\trim($attributeName)) . ': ' . \trim($attributeValue) . '; ';
359 }
360 $trimmedStyle = \rtrim($style);
361 $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey] = $trimmedStyle;
362 return $trimmedStyle;
363 }
364 private function attributeValueIsImportant(string $attributeValue) : bool
365 {
366 return (bool) \preg_match('/!\\s*+important$/i', $attributeValue);
367 }
368 private function fillStyleAttributesWithMergedStyles() : void
369 {
370 foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) {
371 $node = $this->visitedNodes[$nodePath];
372 $currentStyleAttributes = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
373 $node->setAttribute('style', $this->generateStyleStringFromDeclarationsArrays($currentStyleAttributes, $styleAttributesForNode));
374 }
375 }
376 private function removeImportantAnnotationFromAllInlineStyles() : void
377 {
378 foreach ($this->getAllNodesWithStyleAttribute() as $node) {
379 $this->removeImportantAnnotationFromNodeInlineStyle($node);
380 }
381 }
382 private function removeImportantAnnotationFromNodeInlineStyle(\DOMElement $node) : void
383 {
384 $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
385 $regularStyleDeclarations = [];
386 $importantStyleDeclarations = [];
387 foreach ($inlineStyleDeclarations as $property => $value) {
388 if ($this->attributeValueIsImportant($value)) {
389 $importantStyleDeclarations[$property] = $this->pregReplace('/\\s*+!\\s*+important$/i', '', $value);
390 } else {
391 $regularStyleDeclarations[$property] = $value;
392 }
393 }
394 $inlineStyleDeclarationsInNewOrder = \array_merge($regularStyleDeclarations, $importantStyleDeclarations);
395 $node->setAttribute('style', $this->generateStyleStringFromSingleDeclarationsArray($inlineStyleDeclarationsInNewOrder));
396 }
397 private function generateStyleStringFromSingleDeclarationsArray(array $styleDeclarations) : string
398 {
399 return $this->generateStyleStringFromDeclarationsArrays([], $styleDeclarations);
400 }
401 private function determineMatchingUninlinableCssRules(array $cssRules) : void
402 {
403 $this->matchingUninlinableCssRules = \array_filter($cssRules, function (array $cssRule) : bool {
404 return $this->existsMatchForSelectorInCssRule($cssRule);
405 });
406 }
407 private function existsMatchForSelectorInCssRule(array $cssRule) : bool
408 {
409 $selector = $cssRule['selector'];
410 if ($cssRule['hasUnmatchablePseudo']) {
411 $selector = $this->removeUnmatchablePseudoComponents($selector);
412 }
413 return $this->existsMatchForCssSelector($selector);
414 }
415 private function existsMatchForCssSelector(string $cssSelector) : bool
416 {
417 try {
418 $nodesMatchingSelector = $this->getXPath()->query($this->getCssSelectorConverter()->toXPath($cssSelector));
419 } catch (ParseException $e) {
420 if ($this->debug) {
421 throw $e;
422 }
423 return \true;
424 }
425 return $nodesMatchingSelector !== \false && $nodesMatchingSelector->length !== 0;
426 }
427 private function removeUnmatchablePseudoComponents(string $selector) : string
428 {
429 // The regex allows nested brackets via `(?2)`.
430 // A space is temporarily prepended because the callback can't determine if the match was at the very start.
431 $selectorWithoutNots = \ltrim(\preg_replace_callback(
432 '/([\\s>+~]?+):not(\\([^()]*+(?:(?2)[^()]*+)*+\\))/i',
433 function (array $matches) : string {
434 return $this->replaceUnmatchableNotComponent($matches);
435 },
436 ' ' . $selector
437 ));
438 $selectorWithoutUnmatchablePseudoComponents = $this->removeSelectorComponents(':(?!' . self::PSEUDO_CLASS_MATCHER . '):?+[\\w\\-]++(?:\\([^\\)]*+\\))?+', $selectorWithoutNots);
439 if (!\preg_match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selectorWithoutUnmatchablePseudoComponents)) {
440 return $selectorWithoutUnmatchablePseudoComponents;
441 }
442 return \implode('', \array_map(function (string $selectorPart) : string {
443 return $this->removeUnsupportedOfTypePseudoClasses($selectorPart);
444 }, \preg_split('/(' . self::COMBINATOR_MATCHER . ')/', $selectorWithoutUnmatchablePseudoComponents, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY)));
445 }
446 private function replaceUnmatchableNotComponent(array $matches) : string
447 {
448 [$notComponentWithAnyPrecedingCombinator, $anyPrecedingCombinator, $notArgumentInBrackets] = $matches;
449 if ($this->hasUnsupportedPseudoClass($notArgumentInBrackets)) {
450 return $anyPrecedingCombinator !== '' ? $anyPrecedingCombinator . '*' : '';
451 }
452 return $notComponentWithAnyPrecedingCombinator;
453 }
454 private function removeSelectorComponents(string $matcher, string $selector) : string
455 {
456 return \preg_replace(['/([\\s>+~]|^)' . $matcher . '/i', '/' . $matcher . '/i'], ['$1*', ''], $selector);
457 }
458 private function removeUnsupportedOfTypePseudoClasses(string $selectorPart) : string
459 {
460 if (!$this->selectorPartHasUnsupportedOfTypePseudoClass($selectorPart)) {
461 return $selectorPart;
462 }
463 return $this->removeSelectorComponents(':(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')(?:\\([^\\)]*+\\))?+', $selectorPart);
464 }
465 private function copyUninlinableCssToStyleNode(CssDocument $parsedCss) : void
466 {
467 $css = $parsedCss->renderNonConditionalAtRules();
468 // avoid including unneeded class dependency if there are no rules
469 if ($this->getMatchingUninlinableCssRules() !== []) {
470 $cssConcatenator = new CssConcatenator();
471 foreach ($this->getMatchingUninlinableCssRules() as $cssRule) {
472 $cssConcatenator->append([$cssRule['selector']], $cssRule['declarationsBlock'], $cssRule['media']);
473 }
474 $css .= $cssConcatenator->getCss();
475 }
476 // avoid adding empty style element
477 if ($css !== '') {
478 $this->addStyleElementToDocument($css);
479 }
480 }
481 protected function addStyleElementToDocument(string $css) : void
482 {
483 $domDocument = $this->getDomDocument();
484 $styleElement = $domDocument->createElement('style', $css);
485 $styleAttribute = $domDocument->createAttribute('type');
486 $styleAttribute->value = 'text/css';
487 $styleElement->appendChild($styleAttribute);
488 $headElement = $this->getHeadElement();
489 $headElement->appendChild($styleElement);
490 }
491 private function getHeadElement() : \DOMElement
492 {
493 $node = $this->getDomDocument()->getElementsByTagName('head')->item(0);
494 if (!$node instanceof \DOMElement) {
495 throw new \UnexpectedValueException('There is no HEAD element. This should never happen.', 1617923227);
496 }
497 return $node;
498 }
499 private function pregReplace(string $pattern, string $replacement, string $subject) : string
500 {
501 $result = \preg_replace($pattern, $replacement, $subject);
502 if (!\is_string($result)) {
503 $this->logOrThrowPregLastError();
504 $result = $subject;
505 }
506 return $result;
507 }
508 private function logOrThrowPregLastError() : void
509 {
510 $pcreConstants = \get_defined_constants(\true)['pcre'];
511 $pcreErrorConstantNames = \array_flip(\array_filter($pcreConstants, static function (string $key) : bool {
512 return \substr($key, -6) === '_ERROR';
513 }, \ARRAY_FILTER_USE_KEY));
514 $pregLastError = \preg_last_error();
515 $message = 'PCRE regex execution error `' . (string) ($pcreErrorConstantNames[$pregLastError] ?? $pregLastError) . '`';
516 if ($this->debug) {
517 throw new \RuntimeException($message, 1592870147);
518 }
519 \trigger_error($message);
520 }
521 }
522