Completed
Push — master ( bf7d5b...58773d )
by Lars
02:51
created

putReplacedBackToPreserveHtmlEntities()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
cc 1
eloc 5
nc 1
nop 1
crap 1
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 47
  public function __construct($html = null, $css = null)
149
  {
150 47
    if (null !== $html) {
151 2
      $this->setHTML($html);
152 2
    }
153
154 47
    if (null !== $css) {
155 2
      $this->setCSS($css);
156 2
    }
157 47
  }
158
159
  /**
160
   * Set HTML to process
161
   *
162
   * @param  string $html The HTML to process.
163
   */
164 45
  public function setHTML($html)
165
  {
166
    // strip style definitions, if we use css-class "cleanup" on a style-element
167 45
    $this->html = (string)preg_replace('/<style[^>]+class="cleanup"[^>]*>.*<\/style>/Usi', ' ', $html);
168 45
  }
169
170
  /**
171
   * Set CSS to use
172
   *
173
   * @param  string $css The CSS to use.
174
   */
175 43
  public function setCSS($css)
176
  {
177 43
    $this->css = (string)$css;
178 43
  }
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 16
  private static function sortOnSpecificity($e1, $e2)
189
  {
190
    // Compare the specificity
191 16
    $value = $e1['specificity']->compareTo($e2['specificity']);
192
193
    // if the specificity is the same, use the order in which the element appeared
194 16
    if (0 === $value) {
195 11
      $value = $e1['order'] - $e2['order'];
196 11
    }
197
198 16
    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 45
  public function convert($outputXHTML = false, $libXMLOptions = 0, $path = false)
216
  {
217
    // redefine
218 45
    $outputXHTML = (bool)$outputXHTML;
219
220
    // validate
221 45
    if (!$this->html) {
222 1
      throw new Exception('No HTML provided.');
223
    }
224
225
    // use local variables
226 44
    $css = $this->css;
227
228
    // create new DOMDocument
229 44
    $document = $this->createDOMDocument($this->html, $libXMLOptions);
230
231
    // check if there is some link css reference
232 44
    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 44
    if ($this->useInlineStylesBlock) {
249
250 26
      if (true === $this->excludeConditionalInlineStylesBlock) {
251 22
        $this->html = preg_replace(self::$excludeConditionalInlineStylesBlockRegEx, '', $this->html);
252 22
      }
253
254 26
      $css .= $this->getCssFromInlineHtmlStyleBlock($this->html);
255 26
    }
256
257
    // process css
258 44
    $cssRules = $this->processCSS($css);
259
260
    // create new XPath
261 44
    $xPath = $this->createXPath($document, $cssRules);
262
263
    // strip original style tags if we need to
264 44
    if ($this->stripOriginalStyleTags === true) {
265 13
      $this->stripOriginalStyleTags($xPath);
266 13
    }
267
268
    // cleanup the HTML if we need to
269 44
    if (true === $this->cleanup) {
270 3
      $this->cleanupHTML($xPath);
271 3
    }
272
273
    // should we output XHTML?
274 44
    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 39
    $html = $document->saveHTML();
288 39
    $html = $this->putReplacedBackToPreserveHtmlEntities($html);
289
290 39
    return $html;
291
  }
292
293
  /**
294
   * get css from inline-html style-block
295
   *
296
   * @param string $html
297
   *
298
   * @return string
299
   */
300 28
  public function getCssFromInlineHtmlStyleBlock($html)
301
  {
302
    // init var
303 28
    $css = '';
304 28
    $matches = array();
305
306
    // match the style blocks
307 28
    preg_match_all(self::$styleTagRegEx, $html, $matches);
308
309
    // any style-blocks found?
310 28
    if (!empty($matches[2])) {
311
      // add
312 27
      foreach ($matches[2] as $match) {
313 27
        $css .= trim($match) . "\n";
314 27
      }
315 27
    }
316
317 28
    return $css;
318
  }
319
320
  /**
321
   * Process the loaded CSS
322
   *
323
   * @param $css
324
   *
325
   * @return array
326
   */
327 44
  private function processCSS($css)
328
  {
329
    //reset current set of rules
330 44
    $cssRules = array();
331
332
    // init vars
333 44
    $css = (string)$css;
334
335 44
    $css = $this->doCleanup($css);
336
337
    // rules are splitted by }
338 44
    $rules = (array)explode('}', $css);
339
340
    // init var
341 44
    $i = 1;
342
343
    // loop rules
344 44
    foreach ($rules as $rule) {
345
      // split into chunks
346 44
      $chunks = explode('{', $rule);
347
348
      // invalid rule?
349 44
      if (!isset($chunks[1])) {
350 44
        continue;
351
      }
352
353
      // set the selectors
354 33
      $selectors = trim($chunks[0]);
355
356
      // get cssProperties
357 33
      $cssProperties = trim($chunks[1]);
358
359
      // split multiple selectors
360 33
      $selectors = (array)explode(',', $selectors);
361
362
      // loop selectors
363 33
      foreach ($selectors as $selector) {
364
        // cleanup
365 33
        $selector = trim($selector);
366
367
        // build an array for each selector
368 33
        $ruleSet = array();
369
370
        // store selector
371 33
        $ruleSet['selector'] = $selector;
372
373
        // process the properties
374 33
        $ruleSet['properties'] = $this->processCSSProperties($cssProperties);
375
376
377
        // calculate specificity
378 33
        $ruleSet['specificity'] = Specificity::fromSelector($selector);
379
380
        // remember the order in which the rules appear
381 33
        $ruleSet['order'] = $i;
382
383
        // add into rules
384 33
        $cssRules[] = $ruleSet;
385
386
        // increment
387 33
        $i++;
388 33
      }
389 44
    }
390
391
    // sort based on specificity
392 44
    if (0 !== count($cssRules)) {
393 33
      usort($cssRules, array(__CLASS__, 'sortOnSpecificity'));
394 33
    }
395
396 44
    return $cssRules;
397
  }
398
399
  /**
400
   * @param string $css
401
   *
402
   * @return string
403
   */
404 44
  private function doCleanup($css)
405
  {
406
    // remove newlines & replace double quotes by single quotes
407 44
    $css = str_replace(
408 44
        array("\r", "\n", '"'),
409 44
        array('', '', '\''),
410
        $css
411 44
    );
412
413
    // remove comments
414 44
    $css = preg_replace(self::$styleCommentRegEx, '', $css);
415
416
    // remove spaces
417 44
    $css = preg_replace('/\s\s+/', ' ', $css);
418
419
    // remove css charset
420 44
    if (true === $this->excludeCssCharset) {
421 44
      $css = $this->stripeCharsetInCss($css);
422 44
    }
423
424
    // remove css media queries
425 44
    if (true === $this->excludeMediaQueries) {
426 43
      $css = $this->stripeMediaQueries($css);
427 43
    }
428
429 44
    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 43
  private function stripeMediaQueries($css)
440
  {
441
    // remove comments previously to matching media queries
442 43
    $css = preg_replace(self::$styleCommentRegEx, '', $css);
443
444 43
    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 44
  private function stripeCharsetInCss($css)
455
  {
456 44
    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 33
  private function processCSSProperties($propertyString)
467
  {
468
    // split into chunks
469 33
    $properties = $this->splitIntoProperties($propertyString);
470
471
    // init var
472 33
    $pairs = array();
473
474
    // loop properties
475 33
    foreach ($properties as $property) {
476
      // split into chunks
477 33
      $chunks = (array)explode(':', $property, 2);
478
479
      // validate
480 33
      if (!isset($chunks[1])) {
481 27
        continue;
482
      }
483
484
      // cleanup
485 32
      $chunks[0] = trim($chunks[0]);
486 32
      $chunks[1] = trim($chunks[1]);
487
488
      // add to pairs array
489
      if (
490 32
          !isset($pairs[$chunks[0]])
491 32
          ||
492 3
          !in_array($chunks[1], $pairs[$chunks[0]], true)
493 32
      ) {
494 32
        $pairs[$chunks[0]][] = $chunks[1];
495 32
      }
496 33
    }
497
498
    // sort the pairs
499 33
    ksort($pairs);
500
501
    // return
502 33
    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 33
  private function splitIntoProperties($styles)
514
  {
515 33
    $properties = (array)explode(';', $styles);
516 33
    $propertiesCount = count($properties);
517
518
    /** @noinspection ForeachInvariantsInspection */
519 33
    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 33
          isset($properties[$i + 1])
524 33
          &&
525 26
          strpos($properties[$i + 1], 'base64,') !== false
526 33
      ) {
527 1
        $properties[$i] .= ';' . $properties[$i + 1];
528 1
        $properties[$i + 1] = '';
529 1
        ++$i;
530 1
      }
531 33
    }
532
533 33
    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 44
  private function createDOMDocument($html, $libXMLOptions = 0)
545
  {
546
    // create new DOMDocument
547 44
    $document = new \DOMDocument('1.0', $this->getEncoding());
548
549
    // DOMDocument settings
550 44
    $document->preserveWhiteSpace = false;
551 44
    $document->formatOutput = true;
552
553
    // set error level
554 44
    $internalErrors = libxml_use_internal_errors(true);
555
556 44
    $html = $this->replaceToPreserveHtmlEntities($html);
557
558
    // UTF-8 hack: http://php.net/manual/en/domdocument.loadhtml.php#95251
559 44
    $html = trim($html);
560 44
    $xmlHackUsed = false;
561 44
    if (stripos('<?xml', $html) !== 0) {
562 44
      $xmlHackUsed = true;
563 44
      $html = '<?xml encoding="' . $this->getEncoding() . '" ?>' . $html;
564 44
    }
565
566
    // load HTML
567 44
    if ($libXMLOptions !== 0) {
568
      $document->loadHTML($html, $libXMLOptions);
569
    } else {
570 44
      $document->loadHTML($html);
571
    }
572
573
    // remove the "xml-encoding" hack
574 44
    if ($xmlHackUsed === true) {
575 44
      foreach ($document->childNodes as $child) {
576 44
        if ($child->nodeType == XML_PI_NODE) {
577 44
          $document->removeChild($child);
578 44
        }
579 44
      }
580 44
    }
581
582
    // set encoding
583 44
    $document->encoding = $this->getEncoding();
584
585
    // restore error level
586 44
    libxml_use_internal_errors($internalErrors);
587
588 44
    return $document;
589
  }
590
591
  /**
592
   * Get the encoding to use
593
   *
594
   * @return string
595
   */
596 44
  private function getEncoding()
597
  {
598 44
    return $this->encoding;
599
  }
600
601
  /**
602
   * create XPath
603
   *
604
   * @param \DOMDocument $document
605
   * @param array        $cssRules
606
   *
607
   * @return \DOMXPath
608
   */
609 44
  private function createXPath(\DOMDocument $document, array $cssRules)
610
  {
611 44
    $xPath = new \DOMXPath($document);
612
613
    // any rules?
614 44
    if (0 !== count($cssRules)) {
615
      // loop rules
616 33
      foreach ($cssRules as $rule) {
617
618
        try {
619 33
          $converter = new CssSelectorConverter();
620 33
          $query = $converter->toXPath($rule['selector']);
621 33
        } catch (ExceptionInterface $e) {
622 4
          $query = null;
623
        }
624 33
        $converter = null;
625
626
        // validate query
627 33
        if (null === $query) {
628 4
          continue;
629
        }
630
631
        // search elements
632 31
        $elements = $xPath->query($query);
633
634
        // validate elements
635 31
        if (false === $elements) {
636
          continue;
637
        }
638
639
        // loop found elements
640 31
        foreach ($elements as $element) {
641
642
          /**
643
           * @var $element \DOMElement
644
           */
645
646
          // no styles stored?
647 31
          if (null === $element->attributes->getNamedItem('data-css-to-inline-styles-original-styles')) {
648
649
            // init var
650 31
            $originalStyle = '';
651
652 31
            if (null !== $element->attributes->getNamedItem('style')) {
653
              /** @noinspection PhpUndefinedFieldInspection */
654 8
              $originalStyle = $element->attributes->getNamedItem('style')->value;
655 8
            }
656
657
            // store original styles
658 31
            $element->setAttribute('data-css-to-inline-styles-original-styles', $originalStyle);
659
660
            // clear the styles
661 31
            $element->setAttribute('style', '');
662 31
          }
663
664 31
          $propertiesString = $this->createPropertyChunks($element, $rule['properties']);
665
666
          // set attribute
667 31
          if ('' != $propertiesString) {
668 31
            $element->setAttribute('style', $propertiesString);
669 31
          }
670 31
        }
671 33
      }
672
673
      // reapply original styles
674
      // search elements
675 33
      $elements = $xPath->query('//*[@data-css-to-inline-styles-original-styles]');
676
677
      // loop found elements
678 33
      foreach ($elements as $element) {
679
        // get the original styles
680
        /** @noinspection PhpUndefinedFieldInspection */
681 31
        $originalStyle = $element->attributes->getNamedItem('data-css-to-inline-styles-original-styles')->value;
682
683 31
        if ('' != $originalStyle) {
684 8
          $originalStyles = $this->splitIntoProperties($originalStyle);
685
686 8
          $originalProperties = $this->splitStyleIntoChunks($originalStyles);
687
688 8
          $propertiesString = $this->createPropertyChunks($element, $originalProperties);
689
690
          // set attribute
691 8
          if ('' != $propertiesString) {
692 8
            $element->setAttribute('style', $propertiesString);
693 8
          }
694 8
        }
695
696
        // remove placeholder
697 31
        $element->removeAttribute('data-css-to-inline-styles-original-styles');
698 33
      }
699 33
    }
700
701 44
    return $xPath;
702
  }
703
704
  /**
705
   * @param \DOMElement $element
706
   * @param array       $ruleProperties
707
   *
708
   * @return array
709
   */
710 31
  private function createPropertyChunks(\DOMElement $element, array $ruleProperties)
711
  {
712
    // init var
713 31
    $properties = array();
714
715
    // get current styles
716 31
    $stylesAttribute = $element->attributes->getNamedItem('style');
717
718
    // any styles defined before?
719 31
    if (null !== $stylesAttribute) {
720
      // get value for the styles attribute
721
      /** @noinspection PhpUndefinedFieldInspection */
722 31
      $definedStyles = (string)$stylesAttribute->value;
723
724
      // split into properties
725 31
      $definedProperties = $this->splitIntoProperties($definedStyles);
726
727 31
      $properties = $this->splitStyleIntoChunks($definedProperties);
728 31
    }
729
730
    // add new properties into the list
731 31
    foreach ($ruleProperties as $key => $value) {
732
      // If one of the rules is already set and is !important, don't apply it,
733
      // except if the new rule is also important.
734
      if (
735 31
          !isset($properties[$key])
736 31
          ||
737 9
          false === stripos($properties[$key], '!important')
738 9
          ||
739 4
          false !== stripos(implode('', (array)$value), '!important')
740 31
      ) {
741 31
        $properties[$key] = $value;
742 31
      }
743 31
    }
744
745
    // build string
746 31
    $propertyChunks = array();
747
748
    // build chunks
749 31
    foreach ($properties as $key => $values) {
750 31
      foreach ((array)$values as $value) {
751 31
        $propertyChunks[] = $key . ': ' . $value . ';';
752 31
      }
753 31
    }
754
755 31
    return implode(' ', $propertyChunks);
756
  }
757
758
  /**
759
   * @param array $definedProperties
760
   *
761
   * @return array
762
   */
763 31
  private function splitStyleIntoChunks(array $definedProperties)
764
  {
765
    // init var
766 31
    $properties = array();
767
768
    // loop properties
769 31
    foreach ($definedProperties as $property) {
770
      // validate property
771
      if (
772
          !$property
773 31
          ||
774 16
          strpos($property, ':') === false
775 31
      ) {
776 31
        continue;
777
      }
778
779
      // split into chunks
780 16
      $chunks = (array)explode(':', trim($property), 2);
781
782
      // validate
783 16
      if (!isset($chunks[1])) {
784
        continue;
785
      }
786
787
      // loop chunks
788 16
      $properties[$chunks[0]] = trim($chunks[1]);
789 31
    }
790
791 31
    return $properties;
792
  }
793
794
  /**
795
   * Strip style tags into the generated HTML
796
   *
797
   * @param  \DOMXPath $xPath The DOMXPath for the entire document.
798
   *
799
   * @return string
800
   */
801 13
  private function stripOriginalStyleTags(\DOMXPath $xPath)
802
  {
803
    // get all style tags
804 13
    $nodes = $xPath->query('descendant-or-self::style');
805 13
    foreach ($nodes as $node) {
806 12
      if ($this->excludeMediaQueries === true) {
807
808
        // remove comments previously to matching media queries
809 11
        $node->nodeValue = preg_replace(self::$styleCommentRegEx, '', $node->nodeValue);
810
811
        // search for Media Queries
812 11
        preg_match_all(self::$cssMediaQueriesRegEx, $node->nodeValue, $mqs);
813
814
        // replace the nodeValue with just the Media Queries
815 11
        $node->nodeValue = implode("\n", $mqs[0]);
816
817 11
      } else {
818
        // remove the entire style tag
819 1
        $node->parentNode->removeChild($node);
820
      }
821 13
    }
822 13
  }
823
824
  /**
825
   * Remove id and class attributes.
826
   *
827
   * @param  \DOMXPath $xPath The DOMXPath for the entire document.
828
   *
829
   * @return string
830
   */
831 3
  private function cleanupHTML(\DOMXPath $xPath)
832
  {
833 3
    $nodes = $xPath->query('//@class | //@id');
834 3
    foreach ($nodes as $node) {
835 3
      $node->ownerElement->removeAttributeNode($node);
836 3
    }
837 3
  }
838
839
  /**
840
   * Should the IDs and classes be removed?
841
   *
842
   * @param  bool $on Should we enable cleanup?
843
   */
844 3
  public function setCleanup($on = true)
845
  {
846 3
    $this->cleanup = (bool)$on;
847 3
  }
848
849
  /**
850
   * Set the encoding to use with the DOMDocument
851
   *
852
   * @param  string $encoding The encoding to use.
853
   *
854
   * @deprecated Doesn't have any effect
855
   */
856
  public function setEncoding($encoding)
857
  {
858
    $this->encoding = (string)$encoding;
859
  }
860
861
  /**
862
   * Set use of inline styles block
863
   * If this is enabled the class will use the style-block in the HTML.
864
   *
865
   * @param  bool $on Should we process inline styles?
866
   */
867 26
  public function setUseInlineStylesBlock($on = true)
868
  {
869 26
    $this->useInlineStylesBlock = (bool)$on;
870 26
  }
871
872
  /**
873
   * Set use of inline link block
874
   * If this is enabled the class will use the links reference in the HTML.
875
   *
876
   * @return void
877
   *
878
   * @param  bool [optional] $on Should we process link styles?
879
   */
880 2
  public function setLoadCSSFromHTML($on = true)
881
  {
882 2
    $this->loadCSSFromHTML = (bool)$on;
883 2
  }
884
885
  /**
886
   * Set strip original style tags
887
   * If this is enabled the class will remove all style tags in the HTML.
888
   *
889
   * @param  bool $on Should we process inline styles?
890
   */
891 17
  public function setStripOriginalStyleTags($on = true)
892
  {
893 17
    $this->stripOriginalStyleTags = (bool)$on;
894 17
  }
895
896
  /**
897
   * Set exclude media queries
898
   *
899
   * If this is enabled the media queries will be removed before inlining the rules.
900
   *
901
   * WARNING: If you use inline styles block "<style>" the this option will keep the media queries.
902
   *
903
   * @param bool $on
904
   */
905 14
  public function setExcludeMediaQueries($on = true)
906
  {
907 14
    $this->excludeMediaQueries = (bool)$on;
908 14
  }
909
910
  /**
911
   * Set exclude charset
912
   *
913
   * @param bool $on
914
   */
915 1
  public function setExcludeCssCharset($on = true)
916
  {
917 1
    $this->excludeCssCharset = (bool)$on;
918 1
  }
919
920
  /**
921
   * Set exclude conditional inline-style blocks e.g.: <!--[if gte mso 9]><style>.foo { bar } </style><![endif]-->
922
   *
923
   * @param bool $on
924
   */
925 6
  public function setExcludeConditionalInlineStylesBlock($on = true)
926
  {
927 6
    $this->excludeConditionalInlineStylesBlock = (bool)$on;
928 6
  }
929
930
  /**
931
   * @param string $html
932
   *
933
   * @return string
934
   */
935 44
  private function replaceToPreserveHtmlEntities($html)
936
  {
937 44
    preg_match_all("/(\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|[^[:punct:]\s]|\/|\}|\]))/i", $html, $linksOld);
938
939 44
    $linksNew = array();
940 44
    if (!empty($linksOld[1])) {
941 5
      $linksOld = $linksOld[1];
942 5
      foreach ($linksOld as $linkKey => $linkOld) {
943 5
        $linksNew[$linkKey] = str_replace(
944 5
            self::$domLinkReplaceHelper['orig'],
945 5
            self::$domLinkReplaceHelper['tmp'],
946
            $linkOld
947 5
        );
948 5
      }
949 5
    }
950
951 44
    $linksNewCount = count($linksNew);
952 44
    if ($linksNewCount > 0 && count($linksOld) === $linksNewCount) {
953 5
      $search = array_merge($linksOld, self::$domReplaceHelper['orig']);
954 5
      $replace = array_merge($linksNew, self::$domReplaceHelper['tmp']);
955 5
    } else {
956 39
      $search = self::$domReplaceHelper['orig'];
957 39
      $replace = self::$domReplaceHelper['tmp'];
958
    }
959
960 44
    return str_replace($search, $replace, $html);
961
  }
962
963
  /**
964
   * @param string $html
965
   *
966
   * @return string
967
   */
968 44
  private function putReplacedBackToPreserveHtmlEntities($html)
969
  {
970 44
    return str_replace(
971 44
        array_merge(self::$domLinkReplaceHelper['tmp'], self::$domReplaceHelper['tmp'], array('&#13;')),
972 44
        array_merge(self::$domLinkReplaceHelper['orig'], self::$domReplaceHelper['orig'], array('')),
973
        $html
974 44
    );
975
  }
976
}
977