Completed
Push — master ( 716332...ea404f )
by Lars
05:22
created

CssToInlineStyles::setEncoding()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
crap 1
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
 * @author     Tijs Verkoyen <[email protected]>
15
 */
16
class CssToInlineStyles
17
{
18
19
  /**
20
   * @var CssSelectorConverter
21
   */
22
  private $cssConverter;
23
24
  /**
25
   * regular expression: css media queries
26
   *
27
   * @var string
28
   */
29
  private static $cssMediaQueriesRegEx = '#@media\\s+(?:only\\s)?(?:[\\s{\\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misU';
30
31
  /**
32
   * regular expression: css charset
33
   *
34
   * @var string
35
   */
36
  private static $cssCharsetRegEx = '/@charset [\'"][^\'"]+[\'"];/i';
37
38
  /**
39
   * regular expression: conditional inline style tags
40
   *
41
   * @var string
42
   */
43
  private static $excludeConditionalInlineStylesBlockRegEx = '/<!--\[if.*<style.*-->/isU';
44
45
  /**
46
   * regular expression: inline style tags
47
   *
48
   * @var string
49
   */
50
  private static $styleTagRegEx = '|<style(?:\s.*)?>(.*)</style>|isU';
51
52
  /**
53
   * regular expression: html-comments without conditional comments
54
   *
55
   * @var string
56
   */
57
  private static $htmlCommentWithoutConditionalCommentRegEx = '|<!--(?!\[if).*-->|isU';
58
59
  /**
60
   * regular expression: style-tag with 'cleanup'-css-class
61
   *
62
   * @var string
63
   */
64
  private static $styleTagWithCleanupClassRegEx = '|<style[^>]+class="cleanup"[^>]*>.*</style>|isU';
65
66
  /**
67
   * regular expression: css-comments
68
   *
69
   * @var string
70
   */
71
  private static $styleCommentRegEx = '/\\/\\*.*\\*\\//sU';
72
73
  /**
74
   * The CSS to use.
75
   *
76
   * @var string
77
   */
78
  private $css;
79
80
  /**
81
   * The CSS-Media-Queries to use.
82
   *
83
   * @var string
84
   */
85
  private $css_media_queries;
86
87
  /**
88
   * Should the generated HTML be cleaned.
89
   *
90
   * @var bool
91
   */
92
  private $cleanup = false;
93
94
  /**
95
   * The encoding to use.
96
   *
97
   * @var string
98
   */
99
  private $encoding = 'UTF-8';
100
101
  /**
102
   * The HTML to process.
103
   *
104
   * @var string
105
   */
106
  private $html;
107
108
  /**
109
   * Use inline-styles block as CSS.
110
   *
111
   * @var bool
112
   */
113
  private $useInlineStylesBlock = false;
114
115
  /**
116
   * Use link block reference as CSS.
117
   *
118
   * @var bool
119
   */
120
  private $loadCSSFromHTML = false;
121
122
  /**
123
   * Strip original style tags.
124
   *
125
   * @var bool
126
   */
127
  private $stripOriginalStyleTags = false;
128
129
  /**
130
   * Exclude conditional inline-style blocks.
131
   *
132
   * @var bool
133
   */
134
  private $excludeConditionalInlineStylesBlock = true;
135
136
  /**
137
   * Exclude media queries from "$this->css" and keep media queries for inline-styles blocks.
138
   *
139
   * @var bool
140
   */
141
  private $excludeMediaQueries = true;
142
143
  /**
144
   * Exclude media queries from "$this->css" and keep media queries for inline-styles blocks.
145
   *
146
   * @var bool
147
   */
148
  private $excludeCssCharset = true;
149
150
  /**
151
   * Creates an instance, you could set the HTML and CSS here, or load it later.
152
   *
153
   * @param  null|string $html The HTML to process.
154
   * @param  null|string $css  The CSS to use.
155
   */
156 58
  public function __construct(string $html = null, string $css = null)
157
  {
158 58
    if (null !== $html) {
159 2
      $this->setHTML($html);
160
    }
161
162 58
    if (null !== $css) {
163 2
      $this->setCSS($css);
164
    }
165
166 58
    if (\class_exists(CssSelectorConverter::class)) {
167 58
      $this->cssConverter = new CssSelectorConverter();
168
    }
169 58
  }
170
171
  /**
172
   * Set HTML to process.
173
   *
174
   * @param string $html <p>The HTML to process.</p>
175
   *
176
   * @return $this
177
   */
178 56
  public function setHTML(string $html)
179
  {
180
    // strip style definitions, if we use css-class "cleanup" on a style-element
181 56
    $this->html = (string)\preg_replace(self::$styleTagWithCleanupClassRegEx, ' ', $html);
182
183 56
    return $this;
184
  }
185
186
  /**
187
   * Set CSS to use.
188
   *
189
   * @param string $css <p>The CSS to use.</p>
190
   *
191
   * @return $this
192
   */
193 51
  public function setCSS(string $css)
194
  {
195 51
    $this->css = $css;
196
197 51
    $this->css_media_queries = $this->getMediaQueries($css);
198
199 51
    return $this;
200
  }
201
202
  /**
203
   * Sort an array on the specificity element in an ascending way.
204
   *
205
   * INFO: Lower specificity will be sorted to the beginning of the array.
206
   *
207
   * @param Specificity[] $e1 The first element.
208
   * @param Specificity[] $e2 The second element.
209
   *
210
   * @return int
211
   */
212 24
  private static function sortOnSpecificity(array $e1, array $e2): int
213
  {
214
    // Compare the specificity
215 24
    $value = $e1['specificity']->compareTo($e2['specificity']);
0 ignored issues
show
Documentation introduced by
$e2['specificity'] is of type object<voku\CssToInlineStyles\Specificity>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
216
217
    // if the specificity is the same, use the order in which the element appeared
218 24
    if (0 === $value) {
219 19
      $value = $e1['order'] - $e2['order'];
220
    }
221
222 24
    return $value;
223
  }
224
225
  /**
226
   * Converts the loaded HTML into an HTML-string with inline styles based on the loaded CSS.
227
   *
228
   * @param bool        $outputXHTML         [optional] <p>Should we output valid XHTML?</p>
229
   * @param int|null    $libXMLExtraOptions  [optional] <p>$libXMLExtraOptions Since PHP 5.4.0 and Libxml 2.6.0,
230
   *                                         you may also use the options parameter to specify additional
231
   *                                         Libxml parameters.
232
   *                                         </p>
233
   * @param string|null $path                [optional] <p>Set the path to your external css-files.</p>
234
   *
235
   * @return string
236
   *
237
   * @throws Exception
238
   */
239 56
  public function convert(bool $outputXHTML = false, int $libXMLExtraOptions = null, $path = null): string
240
  {
241
    // validate
242 56
    if (!$this->html) {
243 1
      throw new Exception('No HTML provided.');
244
    }
245
246
    // use local variables
247 55
    $css = $this->css;
248
249
    // create new HtmlDomParser
250 55
    $dom = HtmlDomParser::str_get_html($this->html, $libXMLExtraOptions);
0 ignored issues
show
Unused Code introduced by
The call to HtmlDomParser::str_get_html() has too many arguments starting with $this->html.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
251
252
    // check if there is some link css reference
253 55
    if ($this->loadCSSFromHTML) {
254 1
      foreach ($dom->find('link') as $node) {
0 ignored issues
show
Bug introduced by
The expression $dom->find('link') of type array<integer,object<vok...leHtmlDomNodeInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
255
256 1
        $file = ($path ?: __DIR__) . '/' . $node->getAttribute('href');
257
258 1
        if (\file_exists($file)) {
259 1
          $css .= \file_get_contents($file);
260
261
          // converting to inline css because we don't need/want to load css files, so remove the link
262 1
          $node->outertext = '';
263
        }
264
      }
265
    }
266
267
    // should we use inline style-block
268 55
    if ($this->useInlineStylesBlock) {
269
270 31
      if (true === $this->excludeConditionalInlineStylesBlock) {
271 27
        $this->html = \preg_replace(self::$excludeConditionalInlineStylesBlockRegEx, '', $this->html);
272
      }
273
274 31
      $css .= $this->getCssFromInlineHtmlStyleBlock($this->html);
275
    }
276
277
    // process css
278 55
    $cssRules = $this->processCSS($css);
279
280
    // create new XPath
281 55
    $xPath = $this->createXPath($dom->getDocument(), $cssRules);
282
283
    // strip original style tags if we need to
284 55
    if ($this->stripOriginalStyleTags === true) {
285 14
      $this->stripOriginalStyleTags($xPath);
286
    }
287
288
    // cleanup the HTML if we need to
289 55
    if (true === $this->cleanup) {
290 3
      $this->cleanupHTML($xPath);
291
    }
292
293
    // should we output XHTML?
294 55
    if (true === $outputXHTML) {
295 5
      return $dom->xml();
296
    }
297
298
    // just regular HTML 4.01 as it should be used in newsletters
299 51
    $html = $dom->html();
300
301
    // add css media queries from "$this->setCSS()"
302
    if (
303 51
        $this->stripOriginalStyleTags === false
304
        &&
305 51
        $this->css_media_queries
306
    ) {
307 3
      $html = \str_ireplace('</head>', "\n" . '<style type="text/css">' . "\n" . $this->css_media_queries . "\n" . '</style>' . "\n" . '</head>', $html);
308
    }
309
310 51
    return $html;
311
  }
312
313
  /**
314
   * get css from inline-html style-block
315
   *
316
   * @param string $html
317
   *
318
   * @return string
319
   */
320 33
  public function getCssFromInlineHtmlStyleBlock($html): string
321
  {
322
    // init var
323 33
    $css = '';
324 33
    $matches = [];
325
326 33
    $htmlNoComments = \preg_replace(self::$htmlCommentWithoutConditionalCommentRegEx, '', $html);
327
328
    // match the style blocks
329 33
    \preg_match_all(self::$styleTagRegEx, $htmlNoComments, $matches);
330
331
    // any style-blocks found?
332 33
    if (!empty($matches[1])) {
333
      // add
334 32
      foreach ($matches[1] as $match) {
335 32
        $css .= \trim($match) . "\n";
336
      }
337
    }
338
339 33
    return $css;
340
  }
341
342
  /**
343
   * Process the loaded CSS
344
   *
345
   * @param string $css
346
   *
347
   * @return array
348
   */
349 55
  private function processCSS($css): array
350
  {
351
    //reset current set of rules
352 55
    $cssRules = [];
353
354
    // init vars
355 55
    $css = (string)$css;
356
357 55
    $css = $this->doCleanup($css);
358
359
    // rules are splitted by }
360 55
    $rules = \explode('}', $css);
361
362
    // init var
363 55
    $i = 1;
364
365
    // loop rules
366 55
    foreach ($rules as $rule) {
367
      // split into chunks
368 55
      $chunks = \explode('{', $rule);
369
370
      // invalid rule?
371 55
      if (!isset($chunks[1])) {
372 55
        continue;
373
      }
374
375
      // set the selectors
376 42
      $selectors = \trim($chunks[0]);
377
378
      // get css-properties
379 42
      $cssProperties = \trim($chunks[1]);
380
381
      // split multiple selectors
382 42
      $selectors = \explode(',', $selectors);
383
384
      // loop selectors
385 42
      foreach ($selectors as $selector) {
386
        // cleanup
387 42
        $selector = \trim($selector);
388
389
        // build an array for each selector
390 42
        $ruleSet = [];
391
392
        // store selector
393 42
        $ruleSet['selector'] = $selector;
394
395
        // process the properties
396 42
        $ruleSet['properties'] = $this->processCSSProperties($cssProperties);
397
398
        // calculate specificity
399 42
        $ruleSet['specificity'] = Specificity::fromSelector($selector);
400
401
        // remember the order in which the rules appear
402 42
        $ruleSet['order'] = $i;
403
404
        // add into rules
405 42
        $cssRules[] = $ruleSet;
406
407
        // increment
408 42
        $i++;
409
      }
410
    }
411
412
    // sort based on specificity
413 55
    if (0 !== \count($cssRules)) {
414 42
      \usort($cssRules, [__CLASS__, 'sortOnSpecificity']);
415
    }
416
417 55
    return $cssRules;
418
  }
419
420
  /**
421
   * @param string $css
422
   *
423
   * @return string
424
   */
425 55
  private function doCleanup($css): string
426
  {
427
    // remove newlines & replace double quotes by single quotes
428 55
    $css = \str_replace(
429 55
        ["\r", "\n", '"'],
430 55
        ['', '', '\''],
431 55
        $css
432
    );
433
434
    // remove comments
435 55
    $css = \preg_replace(self::$styleCommentRegEx, '', $css);
436
437
    // remove spaces
438 55
    $css = \preg_replace('/\s\s+/', ' ', $css);
439
440
    // remove css charset
441 55
    if (true === $this->excludeCssCharset) {
442 55
      $css = $this->stripeCharsetInCss($css);
443
    }
444
445
    // remove css media queries
446 55
    if (true === $this->excludeMediaQueries) {
447 53
      $css = $this->stripeMediaQueries($css);
448
    }
449
450 55
    return (string)$css;
451
  }
452
453
  /**
454
   * remove css media queries from the string
455
   *
456
   * @param string $css
457
   *
458
   * @return string
459
   */
460 53
  private function stripeMediaQueries($css): string
461
  {
462
    // remove comments previously to matching media queries
463 53
    $css = \preg_replace(self::$styleCommentRegEx, '', $css);
464
465 53
    return (string)\preg_replace(self::$cssMediaQueriesRegEx, '', $css);
466
  }
467
468
  /**
469
   * get css media queries from the string
470
   *
471
   * @param string $css
472
   *
473
   * @return string
474
   */
475 51
  private function getMediaQueries($css): string
476
  {
477
    // remove comments previously to matching media queries
478 51
    $css = \preg_replace(self::$styleCommentRegEx, '', $css);
479
480 51
    \preg_match_all(self::$cssMediaQueriesRegEx, $css, $matches);
481
482 51
    return \implode("\n", $matches[0]);
483
  }
484
485
  /**
486
   * remove charset from the string
487
   *
488
   * @param string $css
489
   *
490
   * @return string
491
   */
492 55
  private function stripeCharsetInCss($css): string
493
  {
494 55
    return (string)\preg_replace(self::$cssCharsetRegEx, '', $css);
495
  }
496
497
  /**
498
   * Process the CSS-properties
499
   *
500
   * @return array
501
   *
502
   * @param  string $propertyString The CSS-properties.
503
   */
504 42
  private function processCSSProperties($propertyString): array
505
  {
506
    // split into chunks
507 42
    $properties = $this->splitIntoProperties($propertyString);
508
509
    // init var
510 42
    $pairs = [];
511
512
    // loop properties
513 42
    foreach ($properties as $property) {
514
      // split into chunks
515 42
      $chunks = \explode(':', $property, 2);
516
517
      // validate
518 42
      if (!isset($chunks[1])) {
519 36
        continue;
520
      }
521
522
      // cleanup
523 41
      $chunks[0] = \trim($chunks[0]);
524 41
      $chunks[1] = \trim($chunks[1]);
525
526
      // add to pairs array
527
      if (
528 41
          !isset($pairs[$chunks[0]])
529
          ||
530 41
          !\in_array($chunks[1], $pairs[$chunks[0]], true)
531
      ) {
532 41
        $pairs[$chunks[0]][] = $chunks[1];
533
      }
534
    }
535
536
    // sort the pairs
537 42
    \ksort($pairs);
538
539
    // return
540 42
    return $pairs;
541
  }
542
543
  /**
544
   * Split a style string into an array of properties.
545
   * The returned array can contain empty strings.
546
   *
547
   * @param string $styles ex: 'color:blue;font-size:12px;'
548
   *
549
   * @return array an array of strings containing css property ex: array('color:blue','font-size:12px')
550
   */
551 42
  private function splitIntoProperties($styles): array
552
  {
553 42
    $properties = \explode(';', $styles);
554 42
    $propertiesCount = \count($properties);
555
556
    /** @noinspection ForeachInvariantsInspection */
557 42
    for ($i = 0; $i < $propertiesCount; $i++) {
558
      // If next property begins with base64,
559
      // Then the ';' was part of this property (and we should not have split on it).
560
      if (
561 42
          isset($properties[$i + 1])
562
          &&
563 42
          \strpos($properties[$i + 1], 'base64,') !== false
564
      ) {
565 1
        $properties[$i] .= ';' . $properties[$i + 1];
566 1
        $properties[$i + 1] = '';
567 1
        ++$i;
568
      }
569
    }
570
571 42
    return $properties;
572
  }
573
574
  /**
575
   * create XPath
576
   *
577
   * @param \DOMDocument $document
578
   * @param array        $cssRules
579
   *
580
   * @return \DOMXPath
581
   */
582 55
  private function createXPath(\DOMDocument $document, array $cssRules): \DOMXPath
583
  {
584 55
    $propertyStorage = new \SplObjectStorage();
585 55
    $xPath = new \DOMXPath($document);
586
587
    // any rules?
588 55
    if (0 !== \count($cssRules)) {
589
      // loop rules
590 42
      foreach ($cssRules as $rule) {
591
592 42
        $ruleSelector = $rule['selector'];
593 42
        $ruleProperties = $rule['properties'];
594
595 42
        if (!$ruleSelector || !$ruleProperties) {
596 4
          continue;
597
        }
598
599
        try {
600 41
          $query = $this->cssConverter->toXPath($ruleSelector);
601 5
        } catch (ExceptionInterface $e) {
602 5
          $query = null;
603
        }
604
605
        // validate query
606 41
        if (null === $query) {
607 5
          continue;
608
        }
609
610
        // search elements
611 40
        $elements = $xPath->query($query);
612
613
        // validate elements
614 40
        if (false === $elements) {
615
          continue;
616
        }
617
618
        // loop found elements
619 40
        foreach ($elements as $element) {
620
621
          /**
622
           * @var $element \DOMElement
623
           */
624
625
          if (
626 40
              $ruleSelector == '*'
627
              &&
628
              (
629 2
                  $element->tagName == 'html'
630 2
                  || $element->tagName === 'title'
631 2
                  || $element->tagName == 'meta'
632 2
                  || $element->tagName == 'head'
633 2
                  || $element->tagName == 'style'
634 2
                  || $element->tagName == 'script'
635 40
                  || $element->tagName == 'link'
636
              )
637
          ) {
638 2
            continue;
639
          }
640
641
          // no styles stored?
642 40
          if (!isset($propertyStorage[$element])) {
643
644
            // init var
645 40
            $originalStyle = $element->attributes->getNamedItem('style');
646
647 40
            if ($originalStyle) {
648
              /** @noinspection PhpUndefinedFieldInspection */
649 12
              $originalStyle = (string)$originalStyle->value;
0 ignored issues
show
Bug introduced by
The property value does not seem to exist in DOMNode.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
650
            } else {
651 38
              $originalStyle = '';
652
            }
653
654
            // store original styles
655 40
            $propertyStorage->attach($element, $originalStyle);
656
657
            // clear the styles
658 40
            $element->setAttribute('style', '');
659
          }
660
661
          // set attribute
662 40
          $propertiesString = $this->createPropertyChunks($element, $ruleProperties);
663 40
          if ($propertiesString) {
664 40
            $element->setAttribute('style', $propertiesString);
665
          }
666
        }
667
      }
668
669 42
      foreach ($propertyStorage as $element) {
670 40
        $originalStyle = $propertyStorage->getInfo();
671 40
        if ($originalStyle) {
672 12
          $originalStyles = $this->splitIntoProperties($originalStyle);
673 12
          $originalProperties = $this->splitStyleIntoChunks($originalStyles);
674
675
          // set attribute
676 12
          $propertiesString = $this->createPropertyChunks($element, $originalProperties);
677 12
          if ($propertiesString) {
678 40
            $element->setAttribute('style', $propertiesString);
679
          }
680
        }
681
      }
682
    }
683
684 55
    return $xPath;
685
  }
686
687
  /**
688
   * @param \DOMElement $element
689
   * @param array       $ruleProperties
690
   *
691
   * @return string
692
   */
693 40
  private function createPropertyChunks(\DOMElement $element, array $ruleProperties): string
694
  {
695
    // init var
696 40
    $properties = [];
697
698
    // get current styles
699 40
    $stylesAttribute = $element->attributes->getNamedItem('style');
700
701
    // any styles defined before?
702 40
    if (null !== $stylesAttribute) {
703
      // get value for the styles attribute
704
      /** @noinspection PhpUndefinedFieldInspection */
705 40
      $definedStyles = (string)$stylesAttribute->value;
0 ignored issues
show
Bug introduced by
The property value does not seem to exist in DOMNode.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
706
707
      // split into properties
708 40
      $definedProperties = $this->splitIntoProperties($definedStyles);
709
710 40
      $properties = $this->splitStyleIntoChunks($definedProperties);
711
    }
712
713
    // add new properties into the list
714 40
    foreach ($ruleProperties as $key => $value) {
715
      // If one of the rules is already set and is !important, don't apply it,
716
      // except if the new rule is also important.
717
      if (
718 40
          !isset($properties[$key])
719
          ||
720 12
          false === \stripos($properties[$key], '!important')
721
          ||
722 40
          false !== \stripos(\implode('', (array)$value), '!important')
723
      ) {
724 40
        unset($properties[$key]);
725 40
        $properties[$key] = $value;
726
      }
727
    }
728
729
    // build string
730 40
    $propertyChunks = [];
731
732
    // build chunks
733 40
    foreach ($properties as $key => $values) {
734 40
      foreach ((array)$values as $value) {
735 40
        $propertyChunks[] = $key . ': ' . $value . ';';
736
      }
737
    }
738
739 40
    return \implode(' ', $propertyChunks);
740
  }
741
742
  /**
743
   * @param array $definedProperties
744
   *
745
   * @return array
746
   */
747 40
  private function splitStyleIntoChunks(array $definedProperties): array
748
  {
749
    // init var
750 40
    $properties = [];
751
752
    // loop properties
753 40
    foreach ($definedProperties as $property) {
754
      // validate property
755
      if (
756 40
          !$property
757
          ||
758 40
          \strpos($property, ':') === false
759
      ) {
760 40
        continue;
761
      }
762
763
      // split into chunks
764 20
      $chunks = \explode(':', \trim($property), 2);
765
766
      // validate
767 20
      if (!isset($chunks[1])) {
768
        continue;
769
      }
770
771
      // loop chunks
772 20
      $properties[$chunks[0]] = \trim($chunks[1]);
773
    }
774
775 40
    return $properties;
776
  }
777
778
  /**
779
   * Strip style tags into the generated HTML.
780
   *
781
   * @param  \DOMXPath $xPath The DOMXPath for the entire document.
782
   */
783 14
  private function stripOriginalStyleTags(\DOMXPath $xPath)
784
  {
785
    // get all style tags
786 14
    $nodes = $xPath->query('descendant-or-self::style');
787 14
    foreach ($nodes as $node) {
788 13
      if ($this->excludeMediaQueries === true) {
789
790
        // remove comments previously to matching media queries
791 12
        $node->nodeValue = \preg_replace(self::$styleCommentRegEx, '', $node->nodeValue);
792
793
        // search for Media Queries
794 12
        \preg_match_all(self::$cssMediaQueriesRegEx, $node->nodeValue, $mqs);
795
796
        // replace the nodeValue with just the Media Queries
797 12
        $node->nodeValue = \implode("\n", $mqs[0]);
798
799
      } else {
800
        // remove the entire style tag
801 13
        $node->parentNode->removeChild($node);
802
      }
803
    }
804 14
  }
805
806
  /**
807
   * Remove id and class attributes.
808
   *
809
   * @param  \DOMXPath $xPath The DOMXPath for the entire document.
810
   */
811 3
  private function cleanupHTML(\DOMXPath $xPath)
812
  {
813 3
    $nodes = $xPath->query('//@class | //@id');
814 3
    foreach ($nodes as $node) {
815 3
      $node->ownerElement->removeAttributeNode($node);
816
    }
817 3
  }
818
819
  /**
820
   * Should the IDs and classes be removed?
821
   *
822
   * @param  bool $on Should we enable cleanup?
823
   *
824
   * @return $this
825
   */
826 3
  public function setCleanup(bool $on = true)
827
  {
828 3
    $this->cleanup = $on;
829
830 3
    return $this;
831
  }
832
833
  /**
834
   * Set the encoding to use with the DOMDocument.
835
   *
836
   * @param  string $encoding The encoding to use.
837
   *
838
   * @return $this
839
   *
840
   * @deprecated Doesn't have any effect
841
   */
842 2
  public function setEncoding(string $encoding)
843
  {
844 2
    $this->encoding = $encoding;
845
846 2
    return $this;
847
  }
848
849
  /**
850
   * Set use of inline styles block.
851
   *
852
   * Info: If this is enabled the class will use the style-block in the HTML.
853
   *
854
   * @param  bool $on Should we process inline styles?
855
   *
856
   * @return $this
857
   */
858 31
  public function setUseInlineStylesBlock(bool $on = true)
859
  {
860 31
    $this->useInlineStylesBlock = $on;
861
862 31
    return $this;
863
  }
864
865
  /**
866
   * Set use of inline link block.
867
   *
868
   * Info: If this is enabled the class will use the links reference in the HTML.
869
   *
870
   * @param  bool [optional] $on Should we process link styles?
871
   *
872
   * @return $this
873
   */
874 2
  public function setLoadCSSFromHTML(bool $on = true)
875
  {
876 2
    $this->loadCSSFromHTML = $on;
877
878 2
    return $this;
879
  }
880
881
  /**
882
   * Set strip original style tags.
883
   *
884
   * Info: If this is enabled the class will remove all style tags in the HTML.
885
   *
886
   * @param  bool $on Should we process inline styles?
887
   *
888
   * @return $this
889
   */
890 18
  public function setStripOriginalStyleTags(bool $on = true)
891
  {
892 18
    $this->stripOriginalStyleTags = $on;
893
894 18
    return $this;
895
  }
896
897
  /**
898
   * Set exclude media queries.
899
   *
900
   * Info: If this is enabled the media queries will be removed before inlining the rules.
901
   *
902
   * WARNING: If you use inline styles block "<style>" the this option will keep the media queries.
903
   *
904
   * @param bool $on
905
   *
906
   * @return $this
907
   */
908 16
  public function setExcludeMediaQueries(bool $on = true)
909
  {
910 16
    $this->excludeMediaQueries = $on;
911
912 16
    return $this;
913
  }
914
915
  /**
916
   * Set exclude charset.
917
   *
918
   * @param bool $on
919
   *
920
   * @return $this
921
   */
922 1
  public function setExcludeCssCharset(bool $on = true)
923
  {
924 1
    $this->excludeCssCharset = $on;
925
926 1
    return $this;
927
  }
928
929
  /**
930
   * Set exclude conditional inline-style blocks.
931
   *
932
   * e.g.: <!--[if gte mso 9]><style>.foo { bar } </style><![endif]-->
933
   *
934
   * @param bool $on
935
   *
936
   * @return $this
937
   */
938 6
  public function setExcludeConditionalInlineStylesBlock(bool $on = true)
939
  {
940 6
    $this->excludeConditionalInlineStylesBlock = $on;
941
942 6
    return $this;
943
  }
944
}
945