Completed
Push — master ( 0c36ae...01c66d )
by Lars
06:07
created

CssToInlineStyles::createXPath()   D

Complexity

Conditions 23
Paths 2

Size

Total Lines 117
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 58
CRAP Score 23.0025

Importance

Changes 13
Bugs 4 Features 0
Metric Value
c 13
b 4
f 0
dl 0
loc 117
ccs 58
cts 59
cp 0.9831
rs 4.6303
cc 23
eloc 51
nc 2
nop 2
crap 23.0025

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace voku\CssToInlineStyles;
3
4
use Symfony\Component\CssSelector\CssSelectorConverter;
5
use Symfony\Component\CssSelector\Exception\ExceptionInterface;
6
7
/**
8
 * CSS to Inline Styles class
9
 *
10
 * @author     Tijs Verkoyen <[email protected]>
11
 */
12
class CssToInlineStyles
13
{
14
15
  /**
16
   * regular expression: css media queries
17
   *
18
   * @var string
19
   */
20
  private static $cssMediaQueriesRegEx = '#@media\\s+(?:only\\s)?(?:[\\s{\\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misU';
21
22
  /**
23
   * regular expression: css charset
24
   *
25
   * @var string
26
   */
27
  private static $cssCharsetRegEx = '/@charset [\'"][^\'"]+[\'"];/i';
28
29
  /**
30
   * regular expression: conditional inline style tags
31
   *
32
   * @var string
33
   */
34
  private static $excludeConditionalInlineStylesBlockRegEx = '/<!--.*<style.*-->/isU';
35
36
  /**
37
   * regular expression: inline style tags
38
   *
39
   * @var string
40
   */
41
  private static $styleTagRegEx = '|<style(.*)>(.*)</style>|isU';
42
43
  /**
44
   * @var array
45
   */
46
  private static $domLinkReplaceHelper = array(
47
      'orig' => array('[', ']', '{', '}',),
48
      'tmp'  => array(
49
          '!!!!SQUARE_BRACKET_LEFT!!!!',
50
          '!!!!SQUARE_BRACKET_RIGHT!!!!',
51
          '!!!!BRACKET_LEFT!!!!',
52
          '!!!!BRACKET_RIGHT!!!!',
53
      ),
54
  );
55
56
  /**
57
   * @var array
58
   */
59
  protected static $domReplaceHelper = array(
60
      'orig' => array('&'),
61
      'tmp'  => array('!!!!AMP!!!!'),
62
  );
63
64
  /**
65
   * regular expression: css-comments
66
   *
67
   * @var string
68
   */
69
  private static $styleCommentRegEx = '/\\/\\*.*\\*\\//sU';
70
71
  /**
72
   * The CSS to use
73
   *
74
   * @var  string
75
   */
76
  private $css;
77
78
  /**
79
   * Should the generated HTML be cleaned
80
   *
81
   * @var  bool
82
   */
83
  private $cleanup = false;
84
85
  /**
86
   * The encoding to use.
87
   *
88
   * @var  string
89
   */
90
  private $encoding = 'UTF-8';
91
92
  /**
93
   * The HTML to process
94
   *
95
   * @var  string
96
   */
97
  private $html;
98
99
  /**
100
   * Use inline-styles block as CSS
101
   *
102
   * @var bool
103
   */
104
  private $useInlineStylesBlock = false;
105
106
  /**
107
   * Use link block reference as CSS
108
   *
109
   * @var bool
110
   */
111
  private $loadCSSFromHTML = false;
112
113
  /**
114
   * Strip original style tags
115
   *
116
   * @var bool
117
   */
118
  private $stripOriginalStyleTags = false;
119
120
  /**
121
   * Exclude conditional inline-style blocks
122
   *
123
   * @var bool
124
   */
125
  private $excludeConditionalInlineStylesBlock = true;
126
127
  /**
128
   * Exclude media queries from "$this->css" and keep media queries for inline-styles blocks
129
   *
130
   * @var bool
131
   */
132
  private $excludeMediaQueries = true;
133
134
  /**
135
   * Exclude media queries from "$this->css" and keep media queries for inline-styles blocks
136
   *
137
   * @var bool
138
   */
139
  private $excludeCssCharset = true;
140
141
  /**
142
   * Creates an instance, you could set the HTML and CSS here, or load it
143
   * later.
144
   *
145
   * @param  null|string $html The HTML to process.
146
   * @param  null|string $css  The CSS to use.
147
   */
148 48
  public function __construct($html = null, $css = null)
149
  {
150 48
    if (null !== $html) {
151 2
      $this->setHTML($html);
152 2
    }
153
154 48
    if (null !== $css) {
155 2
      $this->setCSS($css);
156 2
    }
157 48
  }
158
159
  /**
160
   * Set HTML to process
161
   *
162
   * @param  string $html The HTML to process.
163
   */
164 46
  public function setHTML($html)
165
  {
166
    // strip style definitions, if we use css-class "cleanup" on a style-element
167 46
    $this->html = (string)preg_replace('/<style[^>]+class="cleanup"[^>]*>.*<\/style>/Usi', ' ', $html);
168 46
  }
169
170
  /**
171
   * Set CSS to use
172
   *
173
   * @param  string $css The CSS to use.
174
   */
175 44
  public function setCSS($css)
176
  {
177 44
    $this->css = (string)$css;
178 44
  }
179
180
  /**
181
   * Sort an array on the specificity element
182
   *
183
   * @return int
184
   *
185
   * @param Specificity[] $e1 The first element.
186
   * @param Specificity[] $e2 The second element.
187
   */
188 17
  private static function sortOnSpecificity($e1, $e2)
189
  {
190
    // Compare the specificity
191 17
    $value = $e1['specificity']->compareTo($e2['specificity']);
192
193
    // if the specificity is the same, use the order in which the element appeared
194 17
    if (0 === $value) {
195 12
      $value = $e1['order'] - $e2['order'];
196 12
    }
197
198 17
    return $value;
199
  }
200
201
  /**
202
   * Converts the loaded HTML into an HTML-string with inline styles based on the loaded CSS
203
   *
204
   * @param bool $outputXHTML                             [optional] Should we output valid XHTML?
205
   * @param int  $libXMLOptions                           [optional] $libXMLOptions Since PHP 5.4.0 and Libxml 2.6.0,
206
   *                                                      you may also use the options parameter to specify additional
207
   *                                                      Libxml parameters. Recommend these options:
208
   *                                                      LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
209
   * @param bool $path                                    [optional] Set the path to your external css-files.
210
   *
211
   * @return string
212
   *
213
   * @throws Exception
214
   */
215 46
  public function convert($outputXHTML = false, $libXMLOptions = 0, $path = false)
216
  {
217
    // redefine
218 46
    $outputXHTML = (bool)$outputXHTML;
219
220
    // validate
221 46
    if (!$this->html) {
222 1
      throw new Exception('No HTML provided.');
223
    }
224
225
    // use local variables
226 45
    $css = $this->css;
227
228
    // create new DOMDocument
229 45
    $document = $this->createDOMDocument($this->html, $libXMLOptions);
230
231
    // check if there is some link css reference
232 45
    if ($this->loadCSSFromHTML) {
233 1
      foreach ($document->getElementsByTagName('link') as $node) {
234
235
        /** @noinspection PhpUndefinedMethodInspection */
236 1
        $file = ($path ?: __DIR__) . '/' . $node->getAttribute('href');
237
238 1
        if (file_exists($file)) {
239 1
          $css .= file_get_contents($file);
240
241
          // converting to inline css because we don't need/want to load css files, so remove the link
242 1
          $node->parentNode->removeChild($node);
243 1
        }
244 1
      }
245 1
    }
246
247
    // should we use inline style-block
248 45
    if ($this->useInlineStylesBlock) {
249
250 27
      if (true === $this->excludeConditionalInlineStylesBlock) {
251 23
        $this->html = preg_replace(self::$excludeConditionalInlineStylesBlockRegEx, '', $this->html);
252 23
      }
253
254 27
      $css .= $this->getCssFromInlineHtmlStyleBlock($this->html);
255 27
    }
256
257
    // process css
258 45
    $cssRules = $this->processCSS($css);
259
260
    // create new XPath
261 45
    $xPath = $this->createXPath($document, $cssRules);
262
263
    // strip original style tags if we need to
264 45
    if ($this->stripOriginalStyleTags === true) {
265 13
      $this->stripOriginalStyleTags($xPath);
266 13
    }
267
268
    // cleanup the HTML if we need to
269 45
    if (true === $this->cleanup) {
270 3
      $this->cleanupHTML($xPath);
271 3
    }
272
273
    // should we output XHTML?
274 45
    if (true === $outputXHTML) {
275
      // set formatting
276 6
      $document->formatOutput = true;
277
278
      // get the HTML as XML
279 6
      $xml = $document->saveXML(null, LIBXML_NOEMPTYTAG);
280 6
      $xml = $this->putReplacedBackToPreserveHtmlEntities($xml);
281
282
      // remove the XML-header
283 6
      return ltrim(preg_replace('/<\?xml.*\?>/', '', $xml));
284
    }
285
286
    // just regular HTML 4.01 as it should be used in newsletters
287 40
    $html = $document->saveHTML();
288 40
    $html = $this->putReplacedBackToPreserveHtmlEntities($html);
289
290 40
    return $html;
291
  }
292
293
  /**
294
   * get css from inline-html style-block
295
   *
296
   * @param string $html
297
   *
298
   * @return string
299
   */
300 29
  public function getCssFromInlineHtmlStyleBlock($html)
301
  {
302
    // init var
303 29
    $css = '';
304 29
    $matches = array();
305
306
    // match the style blocks
307 29
    preg_match_all(self::$styleTagRegEx, $html, $matches);
308
309
    // any style-blocks found?
310 29
    if (!empty($matches[2])) {
311
      // add
312 28
      foreach ($matches[2] as $match) {
313 28
        $css .= trim($match) . "\n";
314 28
      }
315 28
    }
316
317 29
    return $css;
318
  }
319
320
  /**
321
   * Process the loaded CSS
322
   *
323
   * @param $css
324
   *
325
   * @return array
326
   */
327 45
  private function processCSS($css)
328
  {
329
    //reset current set of rules
330 45
    $cssRules = array();
331
332
    // init vars
333 45
    $css = (string)$css;
334
335 45
    $css = $this->doCleanup($css);
336
337
    // rules are splitted by }
338 45
    $rules = (array)explode('}', $css);
339
340
    // init var
341 45
    $i = 1;
342
343
    // loop rules
344 45
    foreach ($rules as $rule) {
345
      // split into chunks
346 45
      $chunks = explode('{', $rule);
347
348
      // invalid rule?
349 45
      if (!isset($chunks[1])) {
350 45
        continue;
351
      }
352
353
      // set the selectors
354 34
      $selectors = trim($chunks[0]);
355
356
      // get cssProperties
357 34
      $cssProperties = trim($chunks[1]);
358
359
      // split multiple selectors
360 34
      $selectors = (array)explode(',', $selectors);
361
362
      // loop selectors
363 34
      foreach ($selectors as $selector) {
364
        // cleanup
365 34
        $selector = trim($selector);
366
367
        // build an array for each selector
368 34
        $ruleSet = array();
369
370
        // store selector
371 34
        $ruleSet['selector'] = $selector;
372
373
        // process the properties
374 34
        $ruleSet['properties'] = $this->processCSSProperties($cssProperties);
375
376
377
        // calculate specificity
378 34
        $ruleSet['specificity'] = Specificity::fromSelector($selector);
379
380
        // remember the order in which the rules appear
381 34
        $ruleSet['order'] = $i;
382
383
        // add into rules
384 34
        $cssRules[] = $ruleSet;
385
386
        // increment
387 34
        $i++;
388 34
      }
389 45
    }
390
391
    // sort based on specificity
392 45
    if (0 !== count($cssRules)) {
393 34
      usort($cssRules, array(__CLASS__, 'sortOnSpecificity'));
394 34
    }
395
396 45
    return $cssRules;
397
  }
398
399
  /**
400
   * @param string $css
401
   *
402
   * @return string
403
   */
404 45
  private function doCleanup($css)
405
  {
406
    // remove newlines & replace double quotes by single quotes
407 45
    $css = str_replace(
408 45
        array("\r", "\n", '"'),
409 45
        array('', '', '\''),
410
        $css
411 45
    );
412
413
    // remove comments
414 45
    $css = preg_replace(self::$styleCommentRegEx, '', $css);
415
416
    // remove spaces
417 45
    $css = preg_replace('/\s\s+/', ' ', $css);
418
419
    // remove css charset
420 45
    if (true === $this->excludeCssCharset) {
421 45
      $css = $this->stripeCharsetInCss($css);
422 45
    }
423
424
    // remove css media queries
425 45
    if (true === $this->excludeMediaQueries) {
426 44
      $css = $this->stripeMediaQueries($css);
427 44
    }
428
429 45
    return (string)$css;
430
  }
431
432
  /**
433
   * remove css media queries from the string
434
   *
435
   * @param string $css
436
   *
437
   * @return string
438
   */
439 44
  private function stripeMediaQueries($css)
440
  {
441
    // remove comments previously to matching media queries
442 44
    $css = preg_replace(self::$styleCommentRegEx, '', $css);
443
444 44
    return (string)preg_replace(self::$cssMediaQueriesRegEx, '', $css);
445
  }
446
447
  /**
448
   * remove charset from the string
449
   *
450
   * @param $css
451
   *
452
   * @return string
453
   */
454 45
  private function stripeCharsetInCss($css)
455
  {
456 45
    return (string)preg_replace(self::$cssCharsetRegEx, '', $css);
457
  }
458
459
  /**
460
   * Process the CSS-properties
461
   *
462
   * @return array
463
   *
464
   * @param  string $propertyString The CSS-properties.
465
   */
466 34
  private function processCSSProperties($propertyString)
467
  {
468
    // split into chunks
469 34
    $properties = $this->splitIntoProperties($propertyString);
470
471
    // init var
472 34
    $pairs = array();
473
474
    // loop properties
475 34
    foreach ($properties as $property) {
476
      // split into chunks
477 34
      $chunks = (array)explode(':', $property, 2);
478
479
      // validate
480 34
      if (!isset($chunks[1])) {
481 28
        continue;
482
      }
483
484
      // cleanup
485 33
      $chunks[0] = trim($chunks[0]);
486 33
      $chunks[1] = trim($chunks[1]);
487
488
      // add to pairs array
489
      if (
490 33
          !isset($pairs[$chunks[0]])
491 33
          ||
492 3
          !in_array($chunks[1], $pairs[$chunks[0]], true)
493 33
      ) {
494 33
        $pairs[$chunks[0]][] = $chunks[1];
495 33
      }
496 34
    }
497
498
    // sort the pairs
499 34
    ksort($pairs);
500
501
    // return
502 34
    return $pairs;
503
  }
504
505
  /**
506
   * Split a style string into an array of properties.
507
   * The returned array can contain empty strings.
508
   *
509
   * @param string $styles ex: 'color:blue;font-size:12px;'
510
   *
511
   * @return array an array of strings containing css property ex: array('color:blue','font-size:12px')
512
   */
513 34
  private function splitIntoProperties($styles)
514
  {
515 34
    $properties = (array)explode(';', $styles);
516 34
    $propertiesCount = count($properties);
517
518
    /** @noinspection ForeachInvariantsInspection */
519 34
    for ($i = 0; $i < $propertiesCount; $i++) {
520
      // If next property begins with base64,
521
      // Then the ';' was part of this property (and we should not have split on it).
522
      if (
523 34
          isset($properties[$i + 1])
524 34
          &&
525 27
          strpos($properties[$i + 1], 'base64,') !== false
526 34
      ) {
527 1
        $properties[$i] .= ';' . $properties[$i + 1];
528 1
        $properties[$i + 1] = '';
529 1
        ++$i;
530 1
      }
531 34
    }
532
533 34
    return $properties;
534
  }
535
536
  /**
537
   * create DOMDocument from HTML
538
   *
539
   * @param string $html
540
   * @param int    $libXMLOptions
541
   *
542
   * @return \DOMDocument
543
   */
544 45
  private function createDOMDocument($html, $libXMLOptions = 0)
545
  {
546
    // create new DOMDocument
547 45
    $document = new \DOMDocument('1.0', $this->getEncoding());
548
549
    // DOMDocument settings
550 45
    $document->preserveWhiteSpace = false;
551 45
    $document->formatOutput = true;
552
553
    // set error level
554 45
    $internalErrors = libxml_use_internal_errors(true);
555
556 45
    $html = $this->replaceToPreserveHtmlEntities($html);
557
558
    // UTF-8 hack: http://php.net/manual/en/domdocument.loadhtml.php#95251
559 45
    $html = trim($html);
560 45
    $xmlHackUsed = false;
561 45
    if (stripos('<?xml', $html) !== 0) {
562 45
      $xmlHackUsed = true;
563 45
      $html = '<?xml encoding="' . $this->getEncoding() . '" ?>' . $html;
564 45
    }
565
566
    // load HTML
567 45
    if ($libXMLOptions !== 0) {
568
      $document->loadHTML($html, $libXMLOptions);
569
    } else {
570 45
      $document->loadHTML($html);
571
    }
572
573
    // remove the "xml-encoding" hack
574 45
    if ($xmlHackUsed === true) {
575 45
      foreach ($document->childNodes as $child) {
576 45
        if ($child->nodeType == XML_PI_NODE) {
577 45
          $document->removeChild($child);
578 45
        }
579 45
      }
580 45
    }
581
582
    // set encoding
583 45
    $document->encoding = $this->getEncoding();
584
585
    // restore error level
586 45
    libxml_use_internal_errors($internalErrors);
587
588 45
    return $document;
589
  }
590
591
  /**
592
   * Get the encoding to use
593
   *
594
   * @return string
595
   */
596 45
  private function getEncoding()
597
  {
598 45
    return $this->encoding;
599
  }
600
601
  /**
602
   * create XPath
603
   *
604
   * @param \DOMDocument $document
605
   * @param array        $cssRules
606
   *
607
   * @return \DOMXPath
608
   */
609 45
  private function createXPath(\DOMDocument $document, array $cssRules)
610
  {
611 45
    $xPath = new \DOMXPath($document);
612
613
    // any rules?
614 45
    if (0 !== count($cssRules)) {
615
      // loop rules
616 34
      foreach ($cssRules as $rule) {
617
618 34
        $ruleSelector = $rule['selector'];
619 34
        $ruleProperties = $rule['properties'];
620
621 34
        if (!$ruleSelector || !$ruleProperties) {
622 3
          continue;
623
        }
624
625
        try {
626 33
          $converter = new CssSelectorConverter();
627 33
          $query = $converter->toXPath($ruleSelector);
628 33
        } catch (ExceptionInterface $e) {
629 3
          $query = null;
630
        }
631 33
        $converter = null;
632
633
        // validate query
634 33
        if (null === $query) {
635 3
          continue;
636
        }
637
638
        // search elements
639 32
        $elements = $xPath->query($query);
640
641
        // validate elements
642 32
        if (false === $elements) {
643
          continue;
644
        }
645
646
        // loop found elements
647 32
        foreach ($elements as $element) {
648
649
          /**
650
           * @var $element \DOMElement
651
           */
652
653
          if (
654
              $ruleSelector == '*'
655 32
              &&
656
              (
657 1
                $element->tagName == 'html'
658 1
                || $element->tagName === 'title'
659 1
                || $element->tagName == 'meta'
660 1
                || $element->tagName == 'head'
661 1
                || $element->tagName == 'style'
662 1
                || $element->tagName == 'script'
663 1
                || $element->tagName == 'link'
664 1
              )
665 32
          ) {
666 1
            continue;
667
          }
668
669
          // no styles stored?
670 32
          if (null === $element->attributes->getNamedItem('data-css-to-inline-styles-original-styles')) {
671
672
            // init var
673 32
            $originalStyle = '';
674
675 32
            if (null !== $element->attributes->getNamedItem('style')) {
676
              /** @noinspection PhpUndefinedFieldInspection */
677 9
              $originalStyle = $element->attributes->getNamedItem('style')->value;
678 9
            }
679
680
            // store original styles
681 32
            $element->setAttribute('data-css-to-inline-styles-original-styles', $originalStyle);
682
683
            // clear the styles
684 32
            $element->setAttribute('style', '');
685 32
          }
686
687 32
          $propertiesString = $this->createPropertyChunks($element, $ruleProperties);
688
689
          // set attribute
690 32
          if ('' != $propertiesString) {
691 32
            $element->setAttribute('style', $propertiesString);
692 32
          }
693 32
        }
694 34
      }
695
696
      // reapply original styles
697
      // search elements
698 34
      $elements = $xPath->query('//*[@data-css-to-inline-styles-original-styles]');
699
700
      // loop found elements
701 34
      foreach ($elements as $element) {
702
        // get the original styles
703
        /** @noinspection PhpUndefinedFieldInspection */
704 32
        $originalStyle = $element->attributes->getNamedItem('data-css-to-inline-styles-original-styles')->value;
705
706 32
        if ('' != $originalStyle) {
707 9
          $originalStyles = $this->splitIntoProperties($originalStyle);
708
709 9
          $originalProperties = $this->splitStyleIntoChunks($originalStyles);
710
711 9
          $propertiesString = $this->createPropertyChunks($element, $originalProperties);
712
713
          // set attribute
714 9
          if ('' != $propertiesString) {
715 9
            $element->setAttribute('style', $propertiesString);
716 9
          }
717 9
        }
718
719
        // remove placeholder
720 32
        $element->removeAttribute('data-css-to-inline-styles-original-styles');
721 34
      }
722 34
    }
723
724 45
    return $xPath;
725
  }
726
727
  /**
728
   * @param \DOMElement $element
729
   * @param array       $ruleProperties
730
   *
731
   * @return array
732
   */
733 32
  private function createPropertyChunks(\DOMElement $element, array $ruleProperties)
734
  {
735
    // init var
736 32
    $properties = array();
737
738
    // get current styles
739 32
    $stylesAttribute = $element->attributes->getNamedItem('style');
740
741
    // any styles defined before?
742 32
    if (null !== $stylesAttribute) {
743
      // get value for the styles attribute
744
      /** @noinspection PhpUndefinedFieldInspection */
745 32
      $definedStyles = (string)$stylesAttribute->value;
746
747
      // split into properties
748 32
      $definedProperties = $this->splitIntoProperties($definedStyles);
749
750 32
      $properties = $this->splitStyleIntoChunks($definedProperties);
751 32
    }
752
753
    // add new properties into the list
754 32
    foreach ($ruleProperties as $key => $value) {
755
      // If one of the rules is already set and is !important, don't apply it,
756
      // except if the new rule is also important.
757
      if (
758 32
          !isset($properties[$key])
759 32
          ||
760 10
          false === stripos($properties[$key], '!important')
761 10
          ||
762 5
          false !== stripos(implode('', (array)$value), '!important')
763 32
      ) {
764 32
        $properties[$key] = $value;
765 32
      }
766 32
    }
767
768
    // build string
769 32
    $propertyChunks = array();
770
771
    // build chunks
772 32
    foreach ($properties as $key => $values) {
773 32
      foreach ((array)$values as $value) {
774 32
        $propertyChunks[] = $key . ': ' . $value . ';';
775 32
      }
776 32
    }
777
778 32
    return implode(' ', $propertyChunks);
779
  }
780
781
  /**
782
   * @param array $definedProperties
783
   *
784
   * @return array
785
   */
786 32
  private function splitStyleIntoChunks(array $definedProperties)
787
  {
788
    // init var
789 32
    $properties = array();
790
791
    // loop properties
792 32
    foreach ($definedProperties as $property) {
793
      // validate property
794
      if (
795
          !$property
796 32
          ||
797 17
          strpos($property, ':') === false
798 32
      ) {
799 32
        continue;
800
      }
801
802
      // split into chunks
803 17
      $chunks = (array)explode(':', trim($property), 2);
804
805
      // validate
806 17
      if (!isset($chunks[1])) {
807
        continue;
808
      }
809
810
      // loop chunks
811 17
      $properties[$chunks[0]] = trim($chunks[1]);
812 32
    }
813
814 32
    return $properties;
815
  }
816
817
  /**
818
   * Strip style tags into the generated HTML
819
   *
820
   * @param  \DOMXPath $xPath The DOMXPath for the entire document.
821
   *
822
   * @return string
823
   */
824 13
  private function stripOriginalStyleTags(\DOMXPath $xPath)
825
  {
826
    // get all style tags
827 13
    $nodes = $xPath->query('descendant-or-self::style');
828 13
    foreach ($nodes as $node) {
829 12
      if ($this->excludeMediaQueries === true) {
830
831
        // remove comments previously to matching media queries
832 11
        $node->nodeValue = preg_replace(self::$styleCommentRegEx, '', $node->nodeValue);
833
834
        // search for Media Queries
835 11
        preg_match_all(self::$cssMediaQueriesRegEx, $node->nodeValue, $mqs);
836
837
        // replace the nodeValue with just the Media Queries
838 11
        $node->nodeValue = implode("\n", $mqs[0]);
839
840 11
      } else {
841
        // remove the entire style tag
842 1
        $node->parentNode->removeChild($node);
843
      }
844 13
    }
845 13
  }
846
847
  /**
848
   * Remove id and class attributes.
849
   *
850
   * @param  \DOMXPath $xPath The DOMXPath for the entire document.
851
   *
852
   * @return string
853
   */
854 3
  private function cleanupHTML(\DOMXPath $xPath)
855
  {
856 3
    $nodes = $xPath->query('//@class | //@id');
857 3
    foreach ($nodes as $node) {
858 3
      $node->ownerElement->removeAttributeNode($node);
859 3
    }
860 3
  }
861
862
  /**
863
   * Should the IDs and classes be removed?
864
   *
865
   * @param  bool $on Should we enable cleanup?
866
   */
867 3
  public function setCleanup($on = true)
868
  {
869 3
    $this->cleanup = (bool)$on;
870 3
  }
871
872
  /**
873
   * Set the encoding to use with the DOMDocument
874
   *
875
   * @param  string $encoding The encoding to use.
876
   *
877
   * @deprecated Doesn't have any effect
878
   */
879
  public function setEncoding($encoding)
880
  {
881
    $this->encoding = (string)$encoding;
882
  }
883
884
  /**
885
   * Set use of inline styles block
886
   * If this is enabled the class will use the style-block in the HTML.
887
   *
888
   * @param  bool $on Should we process inline styles?
889
   */
890 27
  public function setUseInlineStylesBlock($on = true)
891
  {
892 27
    $this->useInlineStylesBlock = (bool)$on;
893 27
  }
894
895
  /**
896
   * Set use of inline link block
897
   * If this is enabled the class will use the links reference in the HTML.
898
   *
899
   * @return void
900
   *
901
   * @param  bool [optional] $on Should we process link styles?
902
   */
903 2
  public function setLoadCSSFromHTML($on = true)
904
  {
905 2
    $this->loadCSSFromHTML = (bool)$on;
906 2
  }
907
908
  /**
909
   * Set strip original style tags
910
   * If this is enabled the class will remove all style tags in the HTML.
911
   *
912
   * @param  bool $on Should we process inline styles?
913
   */
914 17
  public function setStripOriginalStyleTags($on = true)
915
  {
916 17
    $this->stripOriginalStyleTags = (bool)$on;
917 17
  }
918
919
  /**
920
   * Set exclude media queries
921
   *
922
   * If this is enabled the media queries will be removed before inlining the rules.
923
   *
924
   * WARNING: If you use inline styles block "<style>" the this option will keep the media queries.
925
   *
926
   * @param bool $on
927
   */
928 14
  public function setExcludeMediaQueries($on = true)
929
  {
930 14
    $this->excludeMediaQueries = (bool)$on;
931 14
  }
932
933
  /**
934
   * Set exclude charset
935
   *
936
   * @param bool $on
937
   */
938 1
  public function setExcludeCssCharset($on = true)
939
  {
940 1
    $this->excludeCssCharset = (bool)$on;
941 1
  }
942
943
  /**
944
   * Set exclude conditional inline-style blocks e.g.: <!--[if gte mso 9]><style>.foo { bar } </style><![endif]-->
945
   *
946
   * @param bool $on
947
   */
948 6
  public function setExcludeConditionalInlineStylesBlock($on = true)
949
  {
950 6
    $this->excludeConditionalInlineStylesBlock = (bool)$on;
951 6
  }
952
953
  /**
954
   * @param string $html
955
   *
956
   * @return string
957
   */
958 45
  private function replaceToPreserveHtmlEntities($html)
959
  {
960 45
    preg_match_all("/(\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|[^[:punct:]\s]|\/|\}|\]))/i", $html, $linksOld);
961
962 45
    $linksNew = array();
963 45
    if (!empty($linksOld[1])) {
964 6
      $linksOld = $linksOld[1];
965 6
      foreach ($linksOld as $linkKey => $linkOld) {
966 6
        $linksNew[$linkKey] = str_replace(
967 6
            self::$domLinkReplaceHelper['orig'],
968 6
            self::$domLinkReplaceHelper['tmp'],
969
            $linkOld
970 6
        );
971 6
      }
972 6
    }
973
974 45
    $linksNewCount = count($linksNew);
975 45
    if ($linksNewCount > 0 && count($linksOld) === $linksNewCount) {
976 6
      $search = array_merge($linksOld, self::$domReplaceHelper['orig']);
977 6
      $replace = array_merge($linksNew, self::$domReplaceHelper['tmp']);
978 6
    } else {
979 39
      $search = self::$domReplaceHelper['orig'];
980 39
      $replace = self::$domReplaceHelper['tmp'];
981
    }
982
983 45
    return str_replace($search, $replace, $html);
984
  }
985
986
  /**
987
   * @param string $html
988
   *
989
   * @return string
990
   */
991 45
  private function putReplacedBackToPreserveHtmlEntities($html)
992
  {
993 45
    return str_replace(
994 45
        array_merge(self::$domLinkReplaceHelper['tmp'], self::$domReplaceHelper['tmp'], array('&#13;')),
995 45
        array_merge(self::$domLinkReplaceHelper['orig'], self::$domReplaceHelper['orig'], array('')),
996
        $html
997 45
    );
998
  }
999
}
1000