CssToInlineStyles   F
last analyzed

Complexity

Total Complexity 96

Size/Duplication

Total Lines 937
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 99.18%

Importance

Changes 0
Metric Value
wmc 96
lcom 1
cbo 4
dl 0
loc 937
ccs 241
cts 243
cp 0.9918
rs 1.663
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 4
C convert() 0 71 13
A getCssFromInlineHtmlStyleBlock() 0 21 3
A setCSS() 0 8 1
A setCleanup() 0 6 1
A setEncoding() 0 6 1
A setExcludeConditionalInlineStylesBlock() 0 6 1
A setExcludeCssCharset() 0 6 1
A setExcludeMediaQueries() 0 6 1
A setHTML() 0 7 1
A setLoadCSSFromHTML() 0 6 1
A setStripOriginalStyleTags() 0 6 1
A setUseInlineStylesBlock() 0 6 1
A doCleanup() 0 27 3
A getMediaQueries() 0 9 1
B processCSS() 0 70 5
A processCSSProperties() 0 38 5
A cleanupHTML() 0 10 3
B createPropertyChunks() 0 49 8
F createXPath() 0 100 23
A sortOnSpecificity() 0 12 2
A splitIntoProperties() 0 22 4
A splitStyleIntoChunks() 0 30 5
A stripOriginalStyleTags() 0 26 5
A stripeCharsetInCss() 0 4 1
A stripeMediaQueries() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like CssToInlineStyles often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CssToInlineStyles, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\CssToInlineStyles;
6
7
use Symfony\Component\CssSelector\CssSelectorConverter;
8
use Symfony\Component\CssSelector\Exception\ExceptionInterface;
9
use voku\helper\HtmlDomParser;
10
11
/**
12
 * CSS to Inline Styles class
13
 */
14
class CssToInlineStyles
15
{
16
    /**
17
     * regular expression: css media queries
18
     *
19
     * @var string
20
     */
21
    private static $cssMediaQueriesRegEx = '#(?:____SIMPLE_HTML_DOM__VOKU__AT____|@)media\\s+(?:only\\s)?(?:[\\s{\\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misuU';
22
23
    /**
24
     * regular expression: css charset
25
     *
26
     * @var string
27
     */
28
    private static $cssCharsetRegEx = '/@charset [\'"][^\'"]+[\'"];/i';
29
30
    /**
31
     * regular expression: conditional inline style tags
32
     *
33
     * @var string
34
     */
35
    private static $excludeConditionalInlineStylesBlockRegEx = '/<!--\[if.*<style.*-->/isU';
36
37
    /**
38
     * regular expression: inline style tags
39
     *
40
     * @var string
41
     */
42
    private static $styleTagRegEx = '|<style(?:\s.*)?>(.*)</style>|isuU';
43
44
    /**
45
     * regular expression: html-comments without conditional comments
46
     *
47
     * @var string
48
     */
49
    private static $htmlCommentWithoutConditionalCommentRegEx = '|<!--(?!\[if).*-->|isU';
50
51
    /**
52
     * regular expression: style-tag with 'cleanup'-css-class
53
     *
54
     * @var string
55
     */
56
    private static $styleTagWithCleanupClassRegEx = '|<style[^>]+class="cleanup"[^>]*>.*</style>|isU';
57
58
    /**
59
     * regular expression: css-comments
60
     *
61
     * @var string
62
     */
63
    private static $styleCommentRegEx = '/\\/\\*.*\\*\\//sU';
64
65
    /**
66
     * @var CssSelectorConverter
67
     */
68
    private $cssConverter;
69
70
    /**
71
     * The CSS to use.
72
     *
73
     * @var string
74
     */
75
    private $css;
76
77
    /**
78
     * The CSS-Media-Queries to use.
79
     *
80
     * @var string
81
     */
82
    private $css_media_queries;
83
84
    /**
85
     * Should the generated HTML be cleaned.
86
     *
87
     * @var bool
88
     */
89
    private $cleanup = false;
90
91
    /**
92
     * The encoding to use.
93
     *
94
     * @var string
95
     */
96
    private $encoding = 'UTF-8';
97
98
    /**
99
     * The HTML to process.
100
     *
101
     * @var string
102
     */
103
    private $html;
104
105
    /**
106
     * Use inline-styles block as CSS.
107
     *
108
     * @var bool
109
     */
110
    private $useInlineStylesBlock = false;
111
112
    /**
113
     * Use link block reference as CSS.
114
     *
115
     * @var bool
116
     */
117
    private $loadCSSFromHTML = false;
118
119
    /**
120
     * Strip original style tags.
121
     *
122
     * @var bool
123
     */
124
    private $stripOriginalStyleTags = false;
125
126
    /**
127
     * Exclude conditional inline-style blocks.
128
     *
129
     * @var bool
130
     */
131
    private $excludeConditionalInlineStylesBlock = true;
132
133
    /**
134
     * Exclude media queries from "$this->css" and keep media queries for inline-styles blocks.
135
     *
136
     * @var bool
137
     */
138
    private $excludeMediaQueries = true;
139
140
    /**
141
     * Exclude media queries from "$this->css" and keep media queries for inline-styles blocks.
142
     *
143
     * @var bool
144
     */
145
    private $excludeCssCharset = true;
146
147
    /**
148
     * Creates an instance, you could set the HTML and CSS here, or load it later.
149
     *
150
     * @param string|null $html the HTML to process
151
     * @param string|null $css  the CSS to use
152
     */
153 58
    public function __construct(string $html = null, string $css = null)
154
    {
155 58
        if ($html !== null) {
156 2
            $this->setHTML($html);
157
        }
158
159 58
        if ($css !== null) {
160 2
            $this->setCSS($css);
161
        }
162
163 58
        if (\class_exists(CssSelectorConverter::class)) {
164 58
            $this->cssConverter = new CssSelectorConverter();
165
        }
166 58
    }
167
168
    /**
169
     * Converts the loaded HTML into an HTML-string with inline styles based on the loaded CSS.
170
     *
171
     * @param bool        $outputXHTML        [optional] <p>Should we output valid XHTML?</p>
172
     * @param int|null    $libXMLExtraOptions [optional] <p>$libXMLExtraOptions Since PHP 5.4.0 and Libxml 2.6.0,
173
     *                                        you may also use the options parameter to specify additional
174
     *                                        Libxml parameters.
175
     *                                        </p>
176
     * @param string|null $path               [optional] <p>Set the path to your external css-files.</p>
177
     *
178
     * @throws Exception
179
     *
180
     * @return string
181
     */
182 56
    public function convert(bool $outputXHTML = false, int $libXMLExtraOptions = null, $path = null): string
183
    {
184
        // validate
185 56
        if (!$this->html) {
186 1
            throw new Exception('No HTML provided.');
187
        }
188
189
        // use local variables
190 55
        $css = $this->css;
191
192
        // create new HtmlDomParser
193 55
        $dom = HtmlDomParser::str_get_html($this->html, $libXMLExtraOptions);
194
195
        // check if there is some link css reference
196 55
        if ($this->loadCSSFromHTML) {
197 1
            foreach ($dom->findMulti('link') as $node) {
198 1
                $file = ($path ?: __DIR__) . '/' . $node->getAttribute('href');
199
200 1
                if (\file_exists($file)) {
201 1
                    $css .= \file_get_contents($file);
202
203
                    // converting to inline css because we don't need/want to load css files, so remove the link
204 1
                    $node->outertext = '';
205
                }
206
            }
207
        }
208
209
        // should we use inline style-block
210 55
        if ($this->useInlineStylesBlock) {
211 31
            if ($this->excludeConditionalInlineStylesBlock === true) {
212 27
                $this->html = (string) \preg_replace(self::$excludeConditionalInlineStylesBlockRegEx, '', $this->html);
213
            }
214
215 31
            $css .= $this->getCssFromInlineHtmlStyleBlock($this->html);
216
        }
217
218
        // process css
219 55
        $cssRules = $this->processCSS($css);
220
221
        // create new XPath
222 55
        $xPath = $this->createXPath($dom->getDocument(), $cssRules);
223
224
        // strip original style tags if we need to
225 55
        if ($this->stripOriginalStyleTags === true) {
226 14
            $this->stripOriginalStyleTags($xPath);
227
        }
228
229
        // cleanup the HTML if we need to
230 55
        if ($this->cleanup === true) {
231 3
            $this->cleanupHTML($xPath);
232
        }
233
234
        // should we output XHTML?
235 55
        if ($outputXHTML === true) {
236 5
            return $dom->xml();
237
        }
238
239
        // just regular HTML 4.01 as it should be used in newsletters
240 51
        $html = $dom->html();
241
242
        // add css media queries from "$this->setCSS()"
243
        if (
244 51
            $this->stripOriginalStyleTags === false
245
            &&
246 51
            $this->css_media_queries
247
        ) {
248 3
            $html = \str_ireplace('</head>', "\n" . '<style type="text/css">' . "\n" . $this->css_media_queries . "\n" . '</style>' . "\n" . '</head>', $html);
249
        }
250
251 51
        return $html;
252
    }
253
254
    /**
255
     * get css from inline-html style-block
256
     *
257
     * @param string $html
258
     *
259
     * @return string
260
     */
261 33
    public function getCssFromInlineHtmlStyleBlock($html): string
262
    {
263
        // init var
264 33
        $css = '';
265 33
        $matches = [];
266
267 33
        $htmlNoComments = (string) \preg_replace(self::$htmlCommentWithoutConditionalCommentRegEx, '', $html);
268
269
        // match the style blocks
270 33
        \preg_match_all(self::$styleTagRegEx, $htmlNoComments, $matches);
271
272
        // any style-blocks found?
273 33
        if (!empty($matches[1])) {
274
            // add
275 32
            foreach ($matches[1] as $match) {
276 32
                $css .= \trim($match) . "\n";
277
            }
278
        }
279
280 33
        return $css;
281
    }
282
283
    /**
284
     * Set CSS to use.
285
     *
286
     * @param string $css <p>The CSS to use.</p>
287
     *
288
     * @return $this
289
     */
290 51
    public function setCSS(string $css): self
291
    {
292 51
        $this->css = $css;
293
294 51
        $this->css_media_queries = $this->getMediaQueries($css);
295
296 51
        return $this;
297
    }
298
299
    /**
300
     * Should the IDs and classes be removed?
301
     *
302
     * @param bool $on Should we enable cleanup?
303
     *
304
     * @return $this
305
     */
306 3
    public function setCleanup(bool $on = true): self
307
    {
308 3
        $this->cleanup = $on;
309
310 3
        return $this;
311
    }
312
313
    /**
314
     * Set the encoding to use with the DOMDocument.
315
     *
316
     * @param string $encoding the encoding to use
317
     *
318
     * @return $this
319
     *
320
     * @deprecated Doesn't have any effect
321
     */
322 2
    public function setEncoding(string $encoding): self
323
    {
324 2
        $this->encoding = $encoding;
325
326 2
        return $this;
327
    }
328
329
    /**
330
     * Set exclude conditional inline-style blocks.
331
     *
332
     * e.g.: <!--[if gte mso 9]><style>.foo { bar } </style><![endif]-->
333
     *
334
     * @param bool $on
335
     *
336
     * @return $this
337
     */
338 6
    public function setExcludeConditionalInlineStylesBlock(bool $on = true): self
339
    {
340 6
        $this->excludeConditionalInlineStylesBlock = $on;
341
342 6
        return $this;
343
    }
344
345
    /**
346
     * Set exclude charset.
347
     *
348
     * @param bool $on
349
     *
350
     * @return $this
351
     */
352 1
    public function setExcludeCssCharset(bool $on = true): self
353
    {
354 1
        $this->excludeCssCharset = $on;
355
356 1
        return $this;
357
    }
358
359
    /**
360
     * Set exclude media queries.
361
     *
362
     * Info: If this is enabled the media queries will be removed before inlining the rules.
363
     *
364
     * WARNING: If you use inline styles block "<style>" this option will keep the media queries.
365
     *
366
     * @param bool $on
367
     *
368
     * @return $this
369
     */
370 16
    public function setExcludeMediaQueries(bool $on = true): self
371
    {
372 16
        $this->excludeMediaQueries = $on;
373
374 16
        return $this;
375
    }
376
377
    /**
378
     * Set HTML to process.
379
     *
380
     * @param string $html <p>The HTML to process.</p>
381
     *
382
     * @return $this
383
     */
384 56
    public function setHTML(string $html): self
385
    {
386
        // strip style definitions, if we use css-class "cleanup" on a style-element
387 56
        $this->html = (string) \preg_replace(self::$styleTagWithCleanupClassRegEx, ' ', $html);
388
389 56
        return $this;
390
    }
391
392
    /**
393
     * Set use of inline link block.
394
     *
395
     * Info: If this is enabled the class will use the links reference in the HTML.
396
     *
397
     * @param bool $on [optional] Should we process link styles?
398
     *
399
     * @return $this
400
     */
401 2
    public function setLoadCSSFromHTML(bool $on = true): self
402
    {
403 2
        $this->loadCSSFromHTML = $on;
404
405 2
        return $this;
406
    }
407
408
    /**
409
     * Set strip original style tags.
410
     *
411
     * Info: If this is enabled the class will remove all style tags in the HTML.
412
     *
413
     * @param bool $on Should we process inline styles?
414
     *
415
     * @return $this
416
     */
417 18
    public function setStripOriginalStyleTags(bool $on = true): self
418
    {
419 18
        $this->stripOriginalStyleTags = $on;
420
421 18
        return $this;
422
    }
423
424
    /**
425
     * Set use of inline styles block.
426
     *
427
     * Info: If this is enabled the class will use the style-block in the HTML.
428
     *
429
     * @param bool $on Should we process inline styles?
430
     *
431
     * @return $this
432
     */
433 31
    public function setUseInlineStylesBlock(bool $on = true): self
434
    {
435 31
        $this->useInlineStylesBlock = $on;
436
437 31
        return $this;
438
    }
439
440
    /**
441
     * Remove id and class attributes.
442
     *
443
     * @param \DOMXPath $xPath the DOMXPath for the entire document
444
     *
445
     * @return void
446
     */
447 3
    private function cleanupHTML(\DOMXPath $xPath)
448
    {
449
        /** @var \DOMAttr[]|\DOMNodeList<\DOMAttr>|false $nodes */
0 ignored issues
show
Documentation introduced by
The doc-type \DOMAttr[]|\DOMNodeList<\DOMAttr>|false could not be parsed: Expected "|" or "end of type", but got "<" at position 23. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
450 3
        $nodes = $xPath->query('//@class | //@id');
451 3
        if ($nodes !== false) {
452 3
            foreach ($nodes as $node) {
453 3
                $node->ownerElement->removeAttributeNode($node);
454
            }
455
        }
456 3
    }
457
458
    /**
459
     * @param \DOMElement $element
460
     * @param array       $ruleProperties
461
     *
462
     * @return string
463
     */
464 40
    private function createPropertyChunks(\DOMElement $element, array $ruleProperties): string
465
    {
466
        // init var
467 40
        $properties = [];
468
469
        // get current styles
470
        /** @var \DOMAttr|null $stylesAttribute */
471 40
        $stylesAttribute = $element->attributes->getNamedItem('style');
472
473
        // any styles defined before?
474 40
        if ($stylesAttribute !== null) {
475
            // get value for the styles attribute
476
            /** @noinspection PhpUndefinedFieldInspection */
477 40
            $definedStyles = (string) $stylesAttribute->value;
478
479
            // split into properties
480 40
            $definedProperties = $this->splitIntoProperties($definedStyles);
481
482 40
            $properties = $this->splitStyleIntoChunks($definedProperties);
483
        }
484
485
        // add new properties into the list
486 40
        foreach ($ruleProperties as $key => $value) {
487
            // If one of the rules is already set and is !important, don't apply it,
488
            // except if the new rule is also important.
489
            if (
490 40
                !isset($properties[$key])
491
                ||
492 12
                \stripos($properties[$key], '!important') === false
493
                ||
494 40
                \stripos(\implode('', (array) $value), '!important') !== false
495
            ) {
496 40
                unset($properties[$key]);
497 40
                $properties[$key] = $value;
498
            }
499
        }
500
501
        // build string
502 40
        $propertyChunks = [];
503
504
        // build chunks
505 40
        foreach ($properties as $key => $values) {
506 40
            foreach ((array) $values as $value) {
507 40
                $propertyChunks[] = $key . ': ' . $value . ';';
508
            }
509
        }
510
511 40
        return \implode(' ', $propertyChunks);
512
    }
513
514
    /**
515
     * create XPath
516
     *
517
     * @param \DOMDocument $document
518
     * @param array        $cssRules
519
     *
520
     * @return \DOMXPath
521
     */
522 55
    private function createXPath(\DOMDocument $document, array $cssRules): \DOMXPath
523
    {
524
        /** @var \DOMElement[]|\SplObjectStorage $propertyStorage */
525 55
        $propertyStorage = new \SplObjectStorage();
526 55
        $xPath = new \DOMXPath($document);
527
528
        // any rules?
529 55
        if (\count($cssRules) !== 0) {
530
            // loop rules
531 42
            foreach ($cssRules as $rule) {
532 42
                $ruleSelector = $rule['selector'];
533 42
                $ruleProperties = $rule['properties'];
534
535 42
                if (!$ruleSelector || !$ruleProperties) {
536 4
                    continue;
537
                }
538
539
                try {
540 41
                    $query = $this->cssConverter->toXPath($ruleSelector);
541 5
                } catch (ExceptionInterface $e) {
542 5
                    $query = null;
543
                }
544
545
                // validate query
546 41
                if ($query === null) {
547 5
                    continue;
548
                }
549
550
                // search elements
551
                /** @var \DOMElement[]|\DOMNodeList<\DOMElement>|false $elements */
0 ignored issues
show
Documentation introduced by
The doc-type \DOMElement[]|\DOMNodeList<\DOMElement>|false could not be parsed: Expected "|" or "end of type", but got "<" at position 26. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
552 40
                $elements = $xPath->query($query);
553
554
                // validate elements
555 40
                if ($elements === false) {
556
                    continue;
557
                }
558
559
                // loop found elements
560 40
                foreach ($elements as $element) {
561
                    if (
562 40
                        $ruleSelector === '*'
563
                        &&
564
                        (
565 2
                            $element->tagName === 'html'
566 2
                            || $element->tagName === 'title'
567 2
                            || $element->tagName === 'meta'
568 2
                            || $element->tagName === 'head'
569 2
                            || $element->tagName === 'style'
570 2
                            || $element->tagName === 'script'
571 40
                            || $element->tagName === 'link'
572
                        )
573
                    ) {
574 2
                        continue;
575
                    }
576
577
                    // no styles stored?
578 40
                    if (!isset($propertyStorage[$element])) {
579
580
                        // init var
581
                        /** @var \DOMAttr|null $originalStyle */
582 40
                        $originalStyle = $element->attributes->getNamedItem('style');
583
584 40
                        if ($originalStyle !== null) {
585 12
                            $originalStyle = (string) $originalStyle->value;
586
                        } else {
587 38
                            $originalStyle = '';
588
                        }
589
590
                        // store original styles
591 40
                        $propertyStorage->attach($element, $originalStyle);
592
593
                        // clear the styles
594 40
                        $element->setAttribute('style', '');
595
                    }
596
597
                    // set attribute
598 40
                    $propertiesString = $this->createPropertyChunks($element, $ruleProperties);
599 40
                    if ($propertiesString) {
600 40
                        $element->setAttribute('style', $propertiesString);
601
                    }
602
                }
603
            }
604
605 42
            foreach ($propertyStorage as $element) {
606 40
                $originalStyle = $propertyStorage->getInfo();
607 40
                if ($originalStyle) {
608 12
                    $originalStyles = $this->splitIntoProperties($originalStyle);
609 12
                    $originalProperties = $this->splitStyleIntoChunks($originalStyles);
610
611
                    // set attribute
612 12
                    $propertiesString = $this->createPropertyChunks($element, $originalProperties);
613 12
                    if ($propertiesString) {
614 40
                        $element->setAttribute('style', $propertiesString);
615
                    }
616
                }
617
            }
618
        }
619
620 55
        return $xPath;
621
    }
622
623
    /**
624
     * @param string $css
625
     *
626
     * @return string
627
     */
628 55
    private function doCleanup($css): string
629
    {
630
        // remove newlines & replace double quotes by single quotes
631 55
        $css = \str_replace(
632 55
            ["\r", "\n", '"'],
633 55
            ['', '', '\''],
634 55
            $css
635
        );
636
637
        // remove comments
638 55
        $css = (string) \preg_replace(self::$styleCommentRegEx, '', $css);
639
640
        // remove spaces
641 55
        $css = (string) \preg_replace('/\s\s+/u', ' ', $css);
642
643
        // remove css charset
644 55
        if ($this->excludeCssCharset === true) {
645 55
            $css = $this->stripeCharsetInCss($css);
646
        }
647
648
        // remove css media queries
649 55
        if ($this->excludeMediaQueries === true) {
650 53
            $css = $this->stripeMediaQueries($css);
651
        }
652
653 55
        return (string) $css;
654
    }
655
656
    /**
657
     * get css media queries from the string
658
     *
659
     * @param string $css
660
     *
661
     * @return string
662
     */
663 51
    private function getMediaQueries($css): string
664
    {
665
        // remove comments previously to matching media queries
666 51
        $css = (string) \preg_replace(self::$styleCommentRegEx, '', $css);
667
668 51
        \preg_match_all(self::$cssMediaQueriesRegEx, $css, $matches);
669
670 51
        return \implode("\n", $matches[0]);
671
    }
672
673
    /**
674
     * Process the loaded CSS
675
     *
676
     * @param string $css
677
     *
678
     * @return array
679
     */
680 55
    private function processCSS($css): array
681
    {
682
        //reset current set of rules
683 55
        $cssRules = [];
684
685
        // init vars
686 55
        $css = (string) $css;
687
688 55
        $css = $this->doCleanup($css);
689
690
        // rules are splitted by }
691 55
        $rules = \explode('}', $css);
692
693
        // init var
694 55
        $i = 1;
695
696
        // loop rules
697 55
        foreach ($rules as $rule) {
698
            // split into chunks
699 55
            $chunks = \explode('{', $rule);
700
701
            // invalid rule?
702 55
            if (!isset($chunks[1])) {
703 55
                continue;
704
            }
705
706
            // set the selectors
707 42
            $selectors = \trim($chunks[0]);
708
709
            // get css-properties
710 42
            $cssProperties = \trim($chunks[1]);
711
712
            // split multiple selectors
713 42
            $selectors = \explode(',', $selectors);
714
715
            // loop selectors
716 42
            foreach ($selectors as $selector) {
717
                // cleanup
718 42
                $selector = \trim($selector);
719
720
                // build an array for each selector
721 42
                $ruleSet = [];
722
723
                // store selector
724 42
                $ruleSet['selector'] = $selector;
725
726
                // process the properties
727 42
                $ruleSet['properties'] = $this->processCSSProperties($cssProperties);
728
729
                // calculate specificity
730 42
                $ruleSet['specificity'] = Specificity::fromSelector($selector);
731
732
                // remember the order in which the rules appear
733 42
                $ruleSet['order'] = $i;
734
735
                // add into rules
736 42
                $cssRules[] = $ruleSet;
737
738
                // increment
739 42
                ++$i;
740
            }
741
        }
742
743
        // sort based on specificity
744 55
        if (\count($cssRules) !== 0) {
745 42
            \usort($cssRules, [__CLASS__, 'sortOnSpecificity']);
746
        }
747
748 55
        return $cssRules;
749
    }
750
751
    /**
752
     * Process the CSS-properties
753
     *
754
     * @param string $propertyString the CSS-properties
755
     *
756
     * @return array
757
     */
758 42
    private function processCSSProperties($propertyString): array
759
    {
760
        // split into chunks
761 42
        $properties = $this->splitIntoProperties($propertyString);
762
763
        // init var
764 42
        $pairs = [];
765
766
        // loop properties
767 42
        foreach ($properties as $property) {
768
            // split into chunks
769 42
            $chunks = \explode(':', $property, 2);
770
771
            // validate
772 42
            if (!isset($chunks[1])) {
773 36
                continue;
774
            }
775
776
            // cleanup
777 41
            $chunks[0] = \trim($chunks[0]);
778 41
            $chunks[1] = \trim($chunks[1]);
779
780
            // add to pairs array
781
            if (
782 41
                !isset($pairs[$chunks[0]])
783
                ||
784 41
                !\in_array($chunks[1], $pairs[$chunks[0]], true)
785
            ) {
786 41
                $pairs[$chunks[0]][] = $chunks[1];
787
            }
788
        }
789
790
        // sort the pairs
791 42
        \ksort($pairs);
792
793
        // return
794 42
        return $pairs;
795
    }
796
797
    /**
798
     * Sort an array on the specificity element in an ascending way.
799
     *
800
     * INFO: Lower specificity will be sorted to the beginning of the array.
801
     *
802
     * @param array $e1 the first element
803
     * @param array $e2 the second element
804
     *
805
     * @return int
806
     *
807
     * @psalm-param array<specificity: Specificity, order: int> $e1
808
     * @psalm-param array<specificity: Specificity, order: int> $e2
809
     */
810 24
    private static function sortOnSpecificity(array $e1, array $e2): int
811
    {
812
        // Compare the specificity
813 24
        $value = $e1['specificity']->compareTo($e2['specificity']);
814
815
        // if the specificity is the same, use the order in which the element appeared
816 24
        if ($value === 0) {
817 19
            $value = $e1['order'] - $e2['order'];
818
        }
819
820 24
        return $value;
821
    }
822
823
    /**
824
     * Split a style string into an array of properties.
825
     * The returned array can contain empty strings.
826
     *
827
     * @param string $styles ex: 'color:blue;font-size:12px;'
828
     *
829
     * @return array an array of strings containing css property ex: array('color:blue','font-size:12px')
830
     */
831 42
    private function splitIntoProperties($styles): array
832
    {
833 42
        $properties = \explode(';', $styles);
834 42
        $propertiesCount = \count($properties);
835
836
        /** @noinspection ForeachInvariantsInspection */
837 42
        for ($i = 0; $i < $propertiesCount; ++$i) {
838
            // If next property begins with base64,
839
            // Then the ';' was part of this property (and we should not have split on it).
840
            if (
841 42
                isset($properties[$i + 1])
842
                &&
843 42
                \strpos($properties[$i + 1], 'base64,') !== false
844
            ) {
845 1
                $properties[$i] .= ';' . $properties[$i + 1];
846 1
                $properties[$i + 1] = '';
847 1
                ++$i;
848
            }
849
        }
850
851 42
        return $properties;
852
    }
853
854
    /**
855
     * @param array $definedProperties
856
     *
857
     * @return array
858
     */
859 40
    private function splitStyleIntoChunks(array $definedProperties): array
860
    {
861
        // init var
862 40
        $properties = [];
863
864
        // loop properties
865 40
        foreach ($definedProperties as $property) {
866
            // validate property
867
            if (
868 40
                !$property
869
                ||
870 40
                \strpos($property, ':') === false
871
            ) {
872 40
                continue;
873
            }
874
875
            // split into chunks
876 20
            $chunks = \explode(':', \trim($property), 2);
877
878
            // validate
879 20
            if (!isset($chunks[1])) {
880
                continue;
881
            }
882
883
            // loop chunks
884 20
            $properties[$chunks[0]] = \trim($chunks[1]);
885
        }
886
887 40
        return $properties;
888
    }
889
890
    /**
891
     * Strip style tags into the generated HTML.
892
     *
893
     * @param \DOMXPath $xPath the DOMXPath for the entire document
894
     *
895
     * @return void
896
     */
897 14
    private function stripOriginalStyleTags(\DOMXPath $xPath)
898
    {
899
        // get all style tags
900
        /** @var \DOMElement[]|\DOMNodeList<\DOMElement>|false $nodes */
0 ignored issues
show
Documentation introduced by
The doc-type \DOMElement[]|\DOMNodeList<\DOMElement>|false could not be parsed: Expected "|" or "end of type", but got "<" at position 26. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
901 14
        $nodes = $xPath->query('descendant-or-self::style');
902 14
        if ($nodes !== false) {
903 14
            foreach ($nodes as $node) {
904 13
                if ($this->excludeMediaQueries === true) {
905
906
                    // remove comments previously to matching media queries
907 12
                    $nodeValueTmp = (string) \preg_replace(self::$styleCommentRegEx, '', $node->nodeValue);
908
909
                    // search for Media Queries
910 12
                    \preg_match_all(self::$cssMediaQueriesRegEx, $nodeValueTmp, $mqs);
911
912
                    // replace the nodeValue with just the Media Queries
913 12
                    $node->nodeValue = \implode("\n", $mqs[0]);
914
                } else {
915
                    // remove the entire style tag
916 1
                    if ($node->parentNode !== null) {
917 13
                        $node->parentNode->removeChild($node);
918
                    }
919
                }
920
            }
921
        }
922 14
    }
923
924
    /**
925
     * remove charset from the string
926
     *
927
     * @param string $css
928
     *
929
     * @return string
930
     */
931 55
    private function stripeCharsetInCss($css): string
932
    {
933 55
        return (string) \preg_replace(self::$cssCharsetRegEx, '', $css);
934
    }
935
936
    /**
937
     * remove css media queries from the string
938
     *
939
     * @param string $css
940
     *
941
     * @return string
942
     */
943 53
    private function stripeMediaQueries($css): string
944
    {
945
        // remove comments previously to matching media queries
946 53
        $css = (string) \preg_replace(self::$styleCommentRegEx, '', $css);
947
948 53
        return (string) \preg_replace(self::$cssMediaQueriesRegEx, '', $css);
949
    }
950
}
951