PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.8
4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
← All changes | inc/libraries/class-emogrifier.php +792 -792 4.3.94.4.8 View file →
@@ -1,792 +1,792 @@
1 -<?php
2 -/**
3 - * This class provides functions for converting CSS styles into inline style attributes in your HTML code.
4 - *
5 - * For more information, please see the README.md file.
6 - *
7 - * @author Cameron Brooks
8 - * @author Jaime Prado
9 - * @author Roman O�ana <ozana@omdesign.cz>
10 - * @deprecated v4.2.9.1
11 - */
12 -if(!class_exists('Emogrifier')) {
13 - class Emogrifier {
14 - /**
15 - * @var string
16 - */
17 - const ENCODING = 'UTF-8';
18 -
19 - /**
20 - * @var integer
21 - */
22 - const CACHE_KEY_CSS = 0;
23 -
24 - /**
25 - * @var integer
26 - */
27 - const CACHE_KEY_SELECTOR = 1;
28 -
29 - /**
30 - * @var integer
31 - */
32 - const CACHE_KEY_XPATH = 2;
33 -
34 - /**
35 - * @var integer
36 - */
37 - const CACHE_KEY_CSS_DECLARATION_BLOCK = 3;
38 -
39 - /**
40 - * for calculating nth-of-type and nth-child selectors
41 - *
42 - * @var integer
43 - */
44 - const INDEX = 0;
45 -
46 - /**
47 - * for calculating nth-of-type and nth-child selectors
48 - *
49 - * @var integer
50 - */
51 - const MULTIPLIER = 1;
52 -
53 - /**
54 - * @var string
55 - */
56 - const ID_ATTRIBUTE_MATCHER = '/(\\w+)?\\#([\\w\\-]+)/';
57 -
58 - /**
59 - * @var string
60 - */
61 - const CLASS_ATTRIBUTE_MATCHER = '/(\\w+|[\\*\\]])?((\\.[\\w\\-]+)+)/';
62 -
63 - /**
64 - * @var string
65 - */
66 - private $html = '';
67 -
68 - /**
69 - * @var string
70 - */
71 - private $css = '';
72 -
73 - /**
74 - * @var array<string>
75 - */
76 - private $unprocessableHtmlTags = array( 'wbr' );
77 -
78 - /**
79 - * @var array<array>
80 - */
81 - private $caches = array(
82 - self::CACHE_KEY_CSS => array(),
83 - self::CACHE_KEY_SELECTOR => array(),
84 - self::CACHE_KEY_XPATH => array(),
85 - self::CACHE_KEY_CSS_DECLARATION_BLOCK => array(),
86 - );
87 -
88 - /**
89 - * the visited nodes with the XPath paths as array keys
90 - *
91 - * @var array<\DOMNode>
92 - */
93 - private $visitedNodes = array();
94 -
95 - /**
96 - * the styles to apply to the nodes with the XPath paths as array keys for the outer array and the attribute names/values
97 - * as key/value pairs for the inner array
98 - *
99 - * @var array<array><string>
100 - */
101 - private $styleAttributesForNodes = array();
102 -
103 - /**
104 - * This attribute applies to the case where you want to preserve your original text encoding.
105 - *
106 - * By default, emogrifier translates your text into HTML entities for two reasons:
107 - *
108 - * 1. Because of client incompatibilities, it is better practice to send out HTML entities rather than unicode over email.
109 - *
110 - * 2. It translates any illegal XML characters that DOMDocument cannot work with.
111 - *
112 - * If you would like to preserve your original encoding, set this attribute to TRUE.
113 - *
114 - * @var boolean
115 - */
116 - public $preserveEncoding = false;
117 -
118 - public static $_media = '';
119 -
120 - /**
121 - * The constructor.
122 - *
123 - * @param string $html the HTML to emogrify, must be UTF-8-encoded
124 - * @param string $css the CSS to merge, must be UTF-8-encoded
125 - */
126 - public function __construct( $html = '', $css = '' ) {
127 - $this->setHtml( $html );
128 - $this->setCss( $css );
129 - }
130 -
131 - /**
132 - * The destructor.
133 - */
134 - public function __destruct() {
135 - $this->purgeVisitedNodes();
136 - }
137 -
138 - /**
139 - * Sets the HTML to emogrify.
140 - *
141 - * @param string $html the HTML to emogrify, must be UTF-8-encoded
142 - */
143 - public function setHtml( $html = '' ) {
144 - $this->html = $html;
145 - }
146 -
147 - /**
148 - * Sets the CSS to merge with the HTML.
149 - *
150 - * @param string $css the CSS to merge, must be UTF-8-encoded
151 - */
152 - public function setCss( $css = '' ) {
153 - $this->css = $css;
154 - }
155 -
156 - /**
157 - * Clears all caches.
158 - */
159 - private function clearAllCaches() {
160 - $this->clearCache( self::CACHE_KEY_CSS );
161 - $this->clearCache( self::CACHE_KEY_SELECTOR );
162 - $this->clearCache( self::CACHE_KEY_XPATH );
163 - $this->clearCache( self::CACHE_KEY_CSS_DECLARATION_BLOCK );
164 - }
165 -
166 - /**
167 - * Clears a single cache by key.
168 - *
169 - * @param integer $key the cache key, must be CACHE_KEY_CSS, CACHE_KEY_SELECTOR, CACHE_KEY_XPATH or CACHE_KEY_CSS_DECLARATION_BLOCK
170 - *
171 - * @throws InvalidArgumentException
172 - */
173 - private function clearCache( $key ) {
174 - $allowedCacheKeys = array( self::CACHE_KEY_CSS, self::CACHE_KEY_SELECTOR, self::CACHE_KEY_XPATH, self::CACHE_KEY_CSS_DECLARATION_BLOCK );
175 - if ( !in_array( $key, $allowedCacheKeys, true ) ) {
176 - throw new InvalidArgumentException( 'Invalid cache key: ' . $key, 1391822035 );
177 - }
178 -
179 - $this->caches[$key] = array();
180 - }
181 -
182 - /**
183 - * Purges the visited nodes.
184 - */
185 - private function purgeVisitedNodes() {
186 - $this->visitedNodes = array();
187 - $this->styleAttributesForNodes = array();
188 - }
189 -
190 - /**
191 - * Marks a tag for removal.
192 - *
193 - * There are some HTML tags that DOMDocument cannot process, and it will throw an error if it encounters them.
194 - * In particular, DOMDocument will complain if you try to use HTML5 tags in an XHTML document.
195 - *
196 - * Note: The tags will not be removed if they have any content.
197 - *
198 - * @param string $tagName the tag name, e.g., "p"
199 - */
200 - public function addUnprocessableHtmlTag( $tagName ) {
201 - $this->unprocessableHtmlTags[] = $tagName;
202 - }
203 -
204 - /**
205 - * Drops a tag from the removal list.
206 - *
207 - * @param string $tagName the tag name, e.g., "p"
208 - */
209 - public function removeUnprocessableHtmlTag( $tagName ) {
210 - $key = array_search( $tagName, $this->unprocessableHtmlTags, true );
211 - if ( $key !== false ) {
212 - unset( $this->unprocessableHtmlTags[$key] );
213 - }
214 - }
215 -
216 - /**
217 - * Applies the CSS you submit to the HTML you submit.
218 - *
219 - * This method places the CSS inline.
220 - *
221 - * @return string
222 - *
223 - * @throws BadMethodCallException
224 - */
225 - public function emogrify() {
226 - if ( $this->html === '' ) {
227 - throw new BadMethodCallException( 'Please set some HTML first before calling emogrify.', 1390393096 );
228 - }
229 -
230 - $xmlDocument = $this->createXmlDocument();
231 - $xpath = new DOMXPath( $xmlDocument );
232 - $this->clearAllCaches();
233 -
234 - // before be begin processing the CSS file, parse the document and normalize all existing CSS attributes (changes 'DISPLAY: none' to 'display: none');
235 - // we wouldn't have to do this if DOMXPath supported XPath 2.0.
236 - // also store a reference of nodes with existing inline styles so we don't overwrite them
237 - $this->purgeVisitedNodes();
238 -
239 - $nodesWithStyleAttributes = $xpath->query( '//*[@style]' );
240 - if ( $nodesWithStyleAttributes !== false ) {
241 - /** @var $nodeWithStyleAttribute DOMNode */
242 - foreach ( $nodesWithStyleAttributes as $node ) {
243 - $normalizedOriginalStyle = preg_replace_callback( '/[A-z\\-]+(?=\\:)/S', array( $this, 'strtolower' ), $node->getAttribute( 'style' ) );
244 -
245 - // in order to not overwrite existing style attributes in the HTML, we have to save the original HTML styles
246 - $nodePath = $node->getNodePath();
247 - if ( !isset( $this->styleAttributesForNodes[$nodePath] ) ) {
248 - $this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationBlock( $normalizedOriginalStyle );
249 - $this->visitedNodes[$nodePath] = $node;
250 - }
251 -
252 - $node->setAttribute( 'style', $normalizedOriginalStyle );
253 - }
254 - }
255 -
256 - // grab any existing style blocks from the html and append them to the existing CSS
257 - // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS)
258 - $allCss = $this->css;
259 -
260 - $allCss .= $this->getCssFromAllStyleNodes( $xpath );
261 -
262 - $cssParts = $this->splitCssAndMediaQuery( $allCss );
263 - self::$_media = ''; // reset
264 -
265 - $cssKey = md5( $cssParts['css'] );
266 - if ( !isset( $this->caches[self::CACHE_KEY_CSS][$cssKey] ) ) {
267 - // process the CSS file for selectors and definitions
268 - preg_match_all( '/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mis', $cssParts['css'], $matches, PREG_SET_ORDER );
269 -
270 - $allSelectors = array();
271 - foreach ( $matches as $key => $selectorString ) {
272 - // if there is a blank definition, skip
273 - if ( !strlen( trim( $selectorString[2] ) ) ) {
274 - continue;
275 - }
276 -
277 - // else split by commas and duplicate attributes so we can sort by selector precedence
278 - $selectors = explode( ',', $selectorString[1] );
279 - foreach ( $selectors as $selector ) {
280 - // don't process pseudo-elements and behavioral (dynamic) pseudo-classes; ONLY allow structural pseudo-classes
281 - if ( strpos( $selector, ':' ) !== false && !preg_match( '/:\\S+\\-(child|type)\\(/i', $selector ) ) {
282 - continue;
283 - }
284 -
285 - $allSelectors[] = array( 'selector' => trim( $selector ),
286 - 'attributes' => trim( $selectorString[2] ),
287 - // keep track of where it appears in the file, since order is important
288 - 'line' => $key,
289 - );
290 - }
291 - }
292 -
293 - // now sort the selectors by precedence
294 - usort( $allSelectors, array( $this, 'sortBySelectorPrecedence' ) );
295 -
296 - $this->caches[self::CACHE_KEY_CSS][$cssKey] = $allSelectors;
297 - }
298 -
299 - foreach ( $this->caches[self::CACHE_KEY_CSS][$cssKey] as $value ) {
300 - // query the body for the xpath selector
301 - $nodesMatchingCssSelectors = $xpath->query( $this->translateCssToXpath( $value['selector'] ) );
302 -
303 - /** @var $node \DOMNode */
304 - foreach ( $nodesMatchingCssSelectors as $node ) {
305 - // if it has a style attribute, get it, process it, and append (overwrite) new stuff
306 - if ( $node->hasAttribute( 'style' ) ) {
307 - // break it up into an associative array
308 - $oldStyleDeclarations = $this->parseCssDeclarationBlock( $node->getAttribute( 'style' ) );
309 - } else {
310 - $oldStyleDeclarations = array();
311 - }
312 - $newStyleDeclarations = $this->parseCssDeclarationBlock( $value['attributes'] );
313 - $node->setAttribute( 'style', $this->generateStyleStringFromDeclarationsArrays( $oldStyleDeclarations, $newStyleDeclarations ) );
314 - }
315 - }
316 -
317 - // now iterate through the nodes that contained inline styles in the original HTML
318 - foreach ( $this->styleAttributesForNodes as $nodePath => $styleAttributesForNode ) {
319 - $node = $this->visitedNodes[$nodePath];
320 - $currentStyleAttributes = $this->parseCssDeclarationBlock( $node->getAttribute( 'style' ) );
321 - $node->setAttribute( 'style', $this->generateStyleStringFromDeclarationsArrays( $currentStyleAttributes, $styleAttributesForNode ) );
322 - }
323 -
324 - // This removes styles from your email that contain display:none.
325 - // We need to look for display:none, but we need to do a case-insensitive search. Since DOMDocument only supports XPath 1.0,
326 - // lower-case() isn't available to us. We've thus far only set attributes to lowercase, not attribute values. Consequently, we need
327 - // to translate() the letters that would be in 'NONE' ("NOE") to lowercase.
328 - $nodesWithStyleDisplayNone = $xpath->query( '//*[contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")]' );
329 - // The checks on parentNode and is_callable below ensure that if we've deleted the parent node,
330 - // we don't try to call removeChild on a nonexistent child node
331 - if ( $nodesWithStyleDisplayNone->length > 0 ) {
332 - /** @var $node \DOMNode */
333 - foreach ( $nodesWithStyleDisplayNone as $node ) {
334 - if ( $node->parentNode && is_callable( array( $node->parentNode, 'removeChild' ) ) ) {
335 - $node->parentNode->removeChild( $node );
336 - }
337 - }
338 - }
339 -
340 - $this->copyCssWithMediaToStyleNode( $cssParts, $xmlDocument );
341 -
342 - if ( $this->preserveEncoding ) {
343 - if ( function_exists( 'mb_convert_encoding' ) ) {
344 - return mb_convert_encoding( $xmlDocument->saveHTML(), self::ENCODING, 'HTML-ENTITIES' );
345 - } else {
346 - return htmlspecialchars_decode( utf8_encode( html_entity_decode( $xmlDocument->saveHTML(), ENT_COMPAT, self::ENCODING ) ) );
347 - }
348 - } else {
349 - return $xmlDocument->saveHTML();
350 - }
351 - }
352 -
353 - public function strtolower( array $m ) {
354 - return strtolower( $m[0] );
355 - }
356 -
357 -
358 - /**
359 - * This method merges old or existing name/value array with new name/value array
360 - * and then generates a string of the combined style suitable for placing inline.
361 - * This becomes the single point for CSS string generation allowing for consistent
362 - * CSS output no matter where the CSS originally came from.
363 - *
364 - * @param array $oldStyles
365 - * @param array $newStyles
366 - *
367 - * @return string
368 - */
369 - private function generateStyleStringFromDeclarationsArrays( array $oldStyles, array $newStyles ) {
370 - $combinedStyles = array_merge( $oldStyles, $newStyles );
371 - $style = '';
372 - foreach ( $combinedStyles as $attributeName => $attributeValue ) {
373 - $style .= ( strtolower( trim( $attributeName ) ) . ': ' . trim( $attributeValue ) . '; ' );
374 - }
375 - return trim( $style );
376 - }
377 -
378 -
379 - /**
380 - * Copies the media part from CSS array parts to $xmlDocument.
381 - *
382 - * @param array $cssParts
383 - * @param DOMDocument $xmlDocument
384 - */
385 - public function copyCssWithMediaToStyleNode( array $cssParts, DOMDocument $xmlDocument ) {
386 - if ( isset( $cssParts['media'] ) && $cssParts['media'] !== '' ) {
387 - $this->addStyleElementToDocument( $xmlDocument, $cssParts['media'] );
388 - }
389 - }
390 -
391 - /**
392 - * Returns CSS content.
393 - *
394 - * @param DOMXPath $xpath
395 - *
396 - * @return string
397 - */
398 - private function getCssFromAllStyleNodes( DOMXPath $xpath ) {
399 - $styleNodes = $xpath->query( '//style' );
400 -
401 - if ( $styleNodes === false ) {
402 - return '';
403 - }
404 -
405 - $css = '';
406 - /** @var $styleNode DOMNode */
407 - foreach ( $styleNodes as $styleNode ) {
408 - $css .= "\n\n" . $styleNode->nodeValue;
409 - $styleNode->parentNode->removeChild( $styleNode );
410 - }
411 -
412 - return $css;
413 - }
414 -
415 - /**
416 - * Adds a style element with $css to $document.
417 - *
418 - * @param DOMDocument $document
419 - * @param string $css
420 - */
421 - private function addStyleElementToDocument( DOMDocument $document, $css ) {
422 - $styleElement = $document->createElement( 'style', $css );
423 - $styleAttribute = $document->createAttribute( 'type' );
424 - $styleAttribute->value = 'text/css';
425 - $styleElement->appendChild( $styleAttribute );
426 -
427 - $head = $this->getOrCreateHeadElement( $document );
428 - $head->appendChild( $styleElement );
429 - }
430 -
431 - /**
432 - * Returns the existing or creates a new head element in $document.
433 - *
434 - * @param DOMDocument $document
435 - *
436 - * @return DOMNode the head element
437 - */
438 - private function getOrCreateHeadElement( DOMDocument $document ) {
439 - $head = $document->getElementsByTagName( 'head' )->item( 0 );
440 -
441 - if ( $head === null ) {
442 - $head = $document->createElement( 'head' );
443 - $html = $document->getElementsByTagName( 'html' )->item( 0 );
444 - $html->insertBefore( $head, $document->getElementsByTagName( 'body' )->item( 0 ) );
445 - }
446 -
447 - return $head;
448 - }
449 -
450 - /**
451 - * Splits input CSS code to an array where:
452 - *
453 - * - key "css" will be contains clean CSS code
454 - * - key "media" will be contains all valuable media queries
455 - *
456 - * Example:
457 - *
458 - * The CSS code
459 - *
460 - * "@import "file.css"; h1 { color:red; } @media { h1 {}} @media tv { h1 {}}"
461 - *
462 - * will be parsed into the following array:
463 - *
464 - * "css" => "h1 { color:red; }"
465 - * "media" => "@media { h1 {}}"
466 - *
467 - * @param string $css
468 - *
469 - * @return array
470 - */
471 - private function splitCssAndMediaQuery( $css ) {
472 - $css = preg_replace_callback( '#@media\\s+(?:only\\s)?(?:[\\s{\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misU', array( $this, '_media_concat' ), $css );
473 -
474 - // filter the CSS
475 - $search = array(
476 - // get rid of css comment code
477 - '/\\/\\*.*\\*\\//sU',
478 - // strip out any import directives
479 - '/^\\s*@import\\s[^;]+;/misU',
480 - // strip remains media enclosures
481 - '/^\\s*@media\\s[^{]+{(.*)}\\s*}\\s/misU',
482 - );
483 -
484 - $replace = array(
485 - '',
486 - '',
487 - '',
488 - );
489 -
490 - // clean CSS before output
491 - $css = preg_replace( $search, $replace, $css );
492 -
493 - return array( 'css' => $css, 'media' => self::$_media );
494 - }
495 -
496 - private function _media_concat( $matches ) {
497 - self::$_media .= $matches[0];
498 - }
499 -
500 - /**
501 - * Creates a DOMDocument instance with the current HTML.
502 - *
503 - * @return DOMDocument
504 - */
505 - private function createXmlDocument() {
506 - $xmlDocument = new DOMDocument;
507 - $xmlDocument->encoding = self::ENCODING;
508 - $xmlDocument->strictErrorChecking = false;
509 - $xmlDocument->formatOutput = true;
510 - $libXmlState = libxml_use_internal_errors( true );
511 - $xmlDocument->loadHTML( $this->getUnifiedHtml() );
512 - libxml_clear_errors();
513 - libxml_use_internal_errors( $libXmlState );
514 - $xmlDocument->normalizeDocument();
515 -
516 - return $xmlDocument;
517 - }
518 -
519 - /**
520 - * Returns the HTML with the non-ASCII characters converts into HTML entities and the unprocessable HTML tags removed.
521 - *
522 - * @return string the unified HTML
523 - *
524 - * @throws BadMethodCallException
525 - */
526 - private function getUnifiedHtml() {
527 - if ( !empty( $this->unprocessableHtmlTags ) ) {
528 - $unprocessableHtmlTags = implode( '|', $this->unprocessableHtmlTags );
529 - $bodyWithoutUnprocessableTags = preg_replace( '/<\\/?(' . $unprocessableHtmlTags . ')[^>]*>/i', '', $this->html );
530 - } else {
531 - $bodyWithoutUnprocessableTags = $this->html;
532 - }
533 -
534 - return htmlspecialchars_decode( utf8_decode( htmlentities( $bodyWithoutUnprocessableTags, ENT_COMPAT, self::ENCODING, false ) ) );
535 - }
536 -
537 - /**
538 - * @param array $a
539 - * @param array $b
540 - *
541 - * @return integer
542 - */
543 - private function sortBySelectorPrecedence( array $a, array $b ) {
544 - $precedenceA = $this->getCssSelectorPrecedence( $a['selector'] );
545 - $precedenceB = $this->getCssSelectorPrecedence( $b['selector'] );
546 -
547 - // We want these sorted in ascending order so selectors with lesser precedence get processed first and
548 - // selectors with greater precedence get sorted last.
549 - // The parenthesis around the -1 are necessary to avoid a PHP_CodeSniffer warning about missing spaces around
550 - // arithmetic operators.
551 - // @see http://forge.typo3.org/issues/55605
552 - $precedenceForEquals = ( $a['line'] < $b['line'] ? ( - 1 ) : 1 );
553 - $precedenceForNotEquals = ( $precedenceA < $precedenceB ? ( - 1 ) : 1 );
554 - return ( $precedenceA === $precedenceB ) ? $precedenceForEquals : $precedenceForNotEquals;
555 - }
556 -
557 - /**
558 - * @param string $selector
559 - *
560 - * @return integer
561 - */
562 - private function getCssSelectorPrecedence( $selector ) {
563 - $selectorKey = md5( $selector );
564 - if ( !isset( $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] ) ) {
565 - $precedence = 0;
566 - $value = 100;
567 - // ids: worth 100, classes: worth 10, elements: worth 1
568 - $search = array( '\\#', '\\.', '' );
569 -
570 - foreach ( $search as $s ) {
571 - if ( trim( $selector == '' ) ) {
572 - break;
573 - }
574 - $number = 0;
575 - $selector = preg_replace( '/' . $s . '\\w+/', '', $selector, - 1, $number );
576 - $precedence += ( $value * $number );
577 - $value /= 10;
578 - }
579 - $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence;
580 - }
581 -
582 - return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey];
583 - }
584 -
585 - /**
586 - * Right now, we support all CSS 1 selectors and most CSS2/3 selectors.
587 - *
588 - * @see http://plasmasturm.org/log/444/
589 - *
590 - * @param string $paramCssSelector
591 - *
592 - * @return string
593 - */
594 - private function translateCssToXpath( $paramCssSelector ) {
595 - $cssSelector = ' ' . $paramCssSelector . ' ';
596 - $cssSelector = preg_replace_callback( '/\s+\w+\s+/', array( $this, 'strtolower' ), $cssSelector );
597 - $cssSelector = trim( $cssSelector );
598 - $xpathKey = md5( $cssSelector );
599 - if ( !isset( $this->caches[self::CACHE_KEY_XPATH][$xpathKey] ) ) {
600 - // returns an Xpath selector
601 - $search = array(
602 - // Matches any element that is a child of parent.
603 - '/\\s+>\\s+/',
604 - // Matches any element that is an adjacent sibling.
605 - '/\\s+\\+\\s+/',
606 - // Matches any element that is a descendant of an parent element element.
607 - '/\\s+/',
608 - // first-child pseudo-selector
609 - '/([^\\/]+):first-child/i',
610 - // last-child pseudo-selector
611 - '/([^\\/]+):last-child/i',
612 - // Matches attribute only selector
613 - '/^\\[(\\w+)\\]/',
614 - // Matches element with attribute
615 - '/(\\w)\\[(\\w+)\\]/',
616 - // Matches element with EXACT attribute
617 - '/(\\w)\\[(\\w+)\\=[\'"]?(\\w+)[\'"]?\\]/',
618 - );
619 - $replace = array(
620 - '/',
621 - '/following-sibling::*[1]/self::',
622 - '//',
623 - '*[1]/self::\\1',
624 - '*[last()]/self::\\1',
625 - '*[@\\1]',
626 - '\\1[@\\2]',
627 - '\\1[@\\2="\\3"]',
628 - );
629 -
630 - $cssSelector = '//' . preg_replace( $search, $replace, $cssSelector );
631 -
632 - $cssSelector = preg_replace_callback( self::ID_ATTRIBUTE_MATCHER, array( $this, 'matchIdAttributes' ), $cssSelector );
633 - $cssSelector = preg_replace_callback( self::CLASS_ATTRIBUTE_MATCHER, array( $this, 'matchClassAttributes' ), $cssSelector );
634 -
635 - // Advanced selectors are going to require a bit more advanced emogrification.
636 - // When we required PHP 5.3, we could do this with closures.
637 - $cssSelector = preg_replace_callback(
638 - '/([^\\/]+):nth-child\\(\s*(odd|even|[+\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
639 - array( $this, 'translateNthChild' ), $cssSelector
640 - );
641 - $cssSelector = preg_replace_callback(
642 - '/([^\\/]+):nth-of-type\\(\s*(odd|even|[+\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
643 - array( $this, 'translateNthOfType' ), $cssSelector
644 - );
645 -
646 - $this->caches[self::CACHE_KEY_SELECTOR][$xpathKey] = $cssSelector;
647 - }
648 - return $this->caches[self::CACHE_KEY_SELECTOR][$xpathKey];
649 - }
650 -
651 - /**
652 - * @param array $match
653 - *
654 - * @return string
655 - */
656 - private function matchIdAttributes( array $match ) {
657 - return ( strlen( $match[1] ) ? $match[1] : '*' ) . '[@id="' . $match[2] . '"]';
658 - }
659 -
660 - /**
661 - * @param array $match
662 - *
663 - * @return string
664 - */
665 - private function matchClassAttributes( array $match ) {
666 - return ( strlen( $match[1] ) ? $match[1] : '*' ) . '[contains(concat(" ",@class," "),concat(" ","' .
667 - implode(
668 - '"," "))][contains(concat(" ",@class," "),concat(" ","',
669 - explode( '.', substr( $match[2], 1 ) )
670 - ) . '"," "))]';
671 - }
672 -
673 - /**
674 - * @param array $match
675 - *
676 - * @return string
677 - */
678 - private function translateNthChild( array $match ) {
679 - $result = $this->parseNth( $match );
680 -
681 - if ( isset( $result[self::MULTIPLIER] ) ) {
682 - if ( $result[self::MULTIPLIER] < 0 ) {
683 - $result[self::MULTIPLIER] = abs( $result[self::MULTIPLIER] );
684 - return sprintf( '*[(last() - position()) mod %u = %u]/self::%s', $result[self::MULTIPLIER], $result[self::INDEX], $match[1] );
685 - } else {
686 - return sprintf( '*[position() mod %u = %u]/self::%s', $result[self::MULTIPLIER], $result[self::INDEX], $match[1] );
687 - }
688 - } else {
689 - return sprintf( '*[%u]/self::%s', $result[self::INDEX], $match[1] );
690 - }
691 - }
692 -
693 - /**
694 - * @param array $match
695 - *
696 - * @return string
697 - */
698 - private function translateNthOfType( array $match ) {
699 - $result = $this->parseNth( $match );
700 -
701 - if ( isset( $result[self::MULTIPLIER] ) ) {
702 - if ( $result[self::MULTIPLIER] < 0 ) {
703 - $result[self::MULTIPLIER] = abs( $result[self::MULTIPLIER] );
704 - return sprintf( '%s[(last() - position()) mod %u = %u]', $match[1], $result[self::MULTIPLIER], $result[self::INDEX] );
705 - } else {
706 - return sprintf( '%s[position() mod %u = %u]', $match[1], $result[self::MULTIPLIER], $result[self::INDEX] );
707 - }
708 - } else {
709 - return sprintf( '%s[%u]', $match[1], $result[self::INDEX] );
710 - }
711 - }
712 -
713 - /**
714 - * @param array $match
715 - *
716 - * @return array
717 - */
718 - private function parseNth( array $match ) {
719 - if ( in_array( strtolower( $match[2] ), array( 'even', 'odd' ) ) ) {
720 - $index = strtolower( $match[2] ) == 'even' ? 0 : 1;
721 - return array( self::MULTIPLIER => 2, self::INDEX => $index );
722 - } elseif ( stripos( $match[2], 'n' ) === false ) {
723 - // if there is a multiplier
724 - $index = intval( str_replace( ' ', '', $match[2] ) );
725 - return array( self::INDEX => $index );
726 - } else {
727 - if ( isset( $match[3] ) ) {
728 - $multipleTerm = str_replace( $match[3], '', $match[2] );
729 - $index = intval( str_replace( ' ', '', $match[3] ) );
730 - } else {
731 - $multipleTerm = $match[2];
732 - $index = 0;
733 - }
734 -
735 - $multiplier = str_ireplace( 'n', '', $multipleTerm );
736 -
737 - if ( !strlen( $multiplier ) ) {
738 - $multiplier = 1;
739 - } elseif ( $multiplier == 0 ) {
740 - return array( self::INDEX => $index );
741 - } else {
742 - $multiplier = intval( $multiplier );
743 - }
744 -
745 - while ( $index < 0 ) {
746 - $index += abs( $multiplier );
747 - }
748 -
749 - return array( self::MULTIPLIER => $multiplier, self::INDEX => $index );
750 - }
751 - }
752 -
753 - /**
754 - * Parses a CSS declaration block into property name/value pairs.
755 - *
756 - * Example:
757 - *
758 - * The declaration block
759 - *
760 - * "color: #000; font-weight: bold;"
761 - *
762 - * will be parsed into the following array:
763 - *
764 - * "color" => "#000"
765 - * "font-weight" => "bold"
766 - *
767 - * @param string $cssDeclarationBlock the CSS declaration block without the curly braces, may be empty
768 - *
769 - * @return array the CSS declarations with the property names as array keys and the property values as array values
770 - */
771 - private function parseCssDeclarationBlock( $cssDeclarationBlock ) {
772 - if ( isset( $this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock] ) ) {
773 - return $this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock];
774 - }
775 -
776 - $properties = array();
777 - $declarations = explode( ';', $cssDeclarationBlock );
778 - foreach ( $declarations as $declaration ) {
779 - $matches = array();
780 - if ( !preg_match( '/ *([A-Za-z\\-]+) *: *([^;]+) */', $declaration, $matches ) ) {
781 - continue;
782 - }
783 - $propertyName = strtolower( $matches[1] );
784 - $propertyValue = $matches[2];
785 - $properties[$propertyName] = $propertyValue;
786 - }
787 - $this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock] = $properties;
788 -
789 - return $properties;
790 - }
791 - }
792 -}
1 +<?php
2 +/**
3 + * This class provides functions for converting CSS styles into inline style attributes in your HTML code.
4 + *
5 + * For more information, please see the README.md file.
6 + *
7 + * @author Cameron Brooks
8 + * @author Jaime Prado
9 + * @author Roman O�ana <ozana@omdesign.cz>
10 + * @deprecated v4.2.9.1
11 + */
12 +if(!class_exists('Emogrifier')) {
13 + class Emogrifier {
14 + /**
15 + * @var string
16 + */
17 + const ENCODING = 'UTF-8';
18 +
19 + /**
20 + * @var integer
21 + */
22 + const CACHE_KEY_CSS = 0;
23 +
24 + /**
25 + * @var integer
26 + */
27 + const CACHE_KEY_SELECTOR = 1;
28 +
29 + /**
30 + * @var integer
31 + */
32 + const CACHE_KEY_XPATH = 2;
33 +
34 + /**
35 + * @var integer
36 + */
37 + const CACHE_KEY_CSS_DECLARATION_BLOCK = 3;
38 +
39 + /**
40 + * for calculating nth-of-type and nth-child selectors
41 + *
42 + * @var integer
43 + */
44 + const INDEX = 0;
45 +
46 + /**
47 + * for calculating nth-of-type and nth-child selectors
48 + *
49 + * @var integer
50 + */
51 + const MULTIPLIER = 1;
52 +
53 + /**
54 + * @var string
55 + */
56 + const ID_ATTRIBUTE_MATCHER = '/(\\w+)?\\#([\\w\\-]+)/';
57 +
58 + /**
59 + * @var string
60 + */
61 + const CLASS_ATTRIBUTE_MATCHER = '/(\\w+|[\\*\\]])?((\\.[\\w\\-]+)+)/';
62 +
63 + /**
64 + * @var string
65 + */
66 + private $html = '';
67 +
68 + /**
69 + * @var string
70 + */
71 + private $css = '';
72 +
73 + /**
74 + * @var array<string>
75 + */
76 + private $unprocessableHtmlTags = array( 'wbr' );
77 +
78 + /**
79 + * @var array<array>
80 + */
81 + private $caches = array(
82 + self::CACHE_KEY_CSS => array(),
83 + self::CACHE_KEY_SELECTOR => array(),
84 + self::CACHE_KEY_XPATH => array(),
85 + self::CACHE_KEY_CSS_DECLARATION_BLOCK => array(),
86 + );
87 +
88 + /**
89 + * the visited nodes with the XPath paths as array keys
90 + *
91 + * @var array<\DOMNode>
92 + */
93 + private $visitedNodes = array();
94 +
95 + /**
96 + * the styles to apply to the nodes with the XPath paths as array keys for the outer array and the attribute names/values
97 + * as key/value pairs for the inner array
98 + *
99 + * @var array<array><string>
100 + */
101 + private $styleAttributesForNodes = array();
102 +
103 + /**
104 + * This attribute applies to the case where you want to preserve your original text encoding.
105 + *
106 + * By default, emogrifier translates your text into HTML entities for two reasons:
107 + *
108 + * 1. Because of client incompatibilities, it is better practice to send out HTML entities rather than unicode over email.
109 + *
110 + * 2. It translates any illegal XML characters that DOMDocument cannot work with.
111 + *
112 + * If you would like to preserve your original encoding, set this attribute to TRUE.
113 + *
114 + * @var boolean
115 + */
116 + public $preserveEncoding = false;
117 +
118 + public static $_media = '';
119 +
120 + /**
121 + * The constructor.
122 + *
123 + * @param string $html the HTML to emogrify, must be UTF-8-encoded
124 + * @param string $css the CSS to merge, must be UTF-8-encoded
125 + */
126 + public function __construct( $html = '', $css = '' ) {
127 + $this->setHtml( $html );
128 + $this->setCss( $css );
129 + }
130 +
131 + /**
132 + * The destructor.
133 + */
134 + public function __destruct() {
135 + $this->purgeVisitedNodes();
136 + }
137 +
138 + /**
139 + * Sets the HTML to emogrify.
140 + *
141 + * @param string $html the HTML to emogrify, must be UTF-8-encoded
142 + */
143 + public function setHtml( $html = '' ) {
144 + $this->html = $html;
145 + }
146 +
147 + /**
148 + * Sets the CSS to merge with the HTML.
149 + *
150 + * @param string $css the CSS to merge, must be UTF-8-encoded
151 + */
152 + public function setCss( $css = '' ) {
153 + $this->css = $css;
154 + }
155 +
156 + /**
157 + * Clears all caches.
158 + */
159 + private function clearAllCaches() {
160 + $this->clearCache( self::CACHE_KEY_CSS );
161 + $this->clearCache( self::CACHE_KEY_SELECTOR );
162 + $this->clearCache( self::CACHE_KEY_XPATH );
163 + $this->clearCache( self::CACHE_KEY_CSS_DECLARATION_BLOCK );
164 + }
165 +
166 + /**
167 + * Clears a single cache by key.
168 + *
169 + * @param integer $key the cache key, must be CACHE_KEY_CSS, CACHE_KEY_SELECTOR, CACHE_KEY_XPATH or CACHE_KEY_CSS_DECLARATION_BLOCK
170 + *
171 + * @throws InvalidArgumentException
172 + */
173 + private function clearCache( $key ) {
174 + $allowedCacheKeys = array( self::CACHE_KEY_CSS, self::CACHE_KEY_SELECTOR, self::CACHE_KEY_XPATH, self::CACHE_KEY_CSS_DECLARATION_BLOCK );
175 + if ( !in_array( $key, $allowedCacheKeys, true ) ) {
176 + throw new InvalidArgumentException( 'Invalid cache key: ' . $key, 1391822035 );
177 + }
178 +
179 + $this->caches[$key] = array();
180 + }
181 +
182 + /**
183 + * Purges the visited nodes.
184 + */
185 + private function purgeVisitedNodes() {
186 + $this->visitedNodes = array();
187 + $this->styleAttributesForNodes = array();
188 + }
189 +
190 + /**
191 + * Marks a tag for removal.
192 + *
193 + * There are some HTML tags that DOMDocument cannot process, and it will throw an error if it encounters them.
194 + * In particular, DOMDocument will complain if you try to use HTML5 tags in an XHTML document.
195 + *
196 + * Note: The tags will not be removed if they have any content.
197 + *
198 + * @param string $tagName the tag name, e.g., "p"
199 + */
200 + public function addUnprocessableHtmlTag( $tagName ) {
201 + $this->unprocessableHtmlTags[] = $tagName;
202 + }
203 +
204 + /**
205 + * Drops a tag from the removal list.
206 + *
207 + * @param string $tagName the tag name, e.g., "p"
208 + */
209 + public function removeUnprocessableHtmlTag( $tagName ) {
210 + $key = array_search( $tagName, $this->unprocessableHtmlTags, true );
211 + if ( $key !== false ) {
212 + unset( $this->unprocessableHtmlTags[$key] );
213 + }
214 + }
215 +
216 + /**
217 + * Applies the CSS you submit to the HTML you submit.
218 + *
219 + * This method places the CSS inline.
220 + *
221 + * @return string
222 + *
223 + * @throws BadMethodCallException
224 + */
225 + public function emogrify() {
226 + if ( $this->html === '' ) {
227 + throw new BadMethodCallException( 'Please set some HTML first before calling emogrify.', 1390393096 );
228 + }
229 +
230 + $xmlDocument = $this->createXmlDocument();
231 + $xpath = new DOMXPath( $xmlDocument );
232 + $this->clearAllCaches();
233 +
234 + // before be begin processing the CSS file, parse the document and normalize all existing CSS attributes (changes 'DISPLAY: none' to 'display: none');
235 + // we wouldn't have to do this if DOMXPath supported XPath 2.0.
236 + // also store a reference of nodes with existing inline styles so we don't overwrite them
237 + $this->purgeVisitedNodes();
238 +
239 + $nodesWithStyleAttributes = $xpath->query( '//*[@style]' );
240 + if ( $nodesWithStyleAttributes !== false ) {
241 + /** @var $nodeWithStyleAttribute DOMNode */
242 + foreach ( $nodesWithStyleAttributes as $node ) {
243 + $normalizedOriginalStyle = preg_replace_callback( '/[A-z\\-]+(?=\\:)/S', array( $this, 'strtolower' ), $node->getAttribute( 'style' ) );
244 +
245 + // in order to not overwrite existing style attributes in the HTML, we have to save the original HTML styles
246 + $nodePath = $node->getNodePath();
247 + if ( !isset( $this->styleAttributesForNodes[$nodePath] ) ) {
248 + $this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationBlock( $normalizedOriginalStyle );
249 + $this->visitedNodes[$nodePath] = $node;
250 + }
251 +
252 + $node->setAttribute( 'style', $normalizedOriginalStyle );
253 + }
254 + }
255 +
256 + // grab any existing style blocks from the html and append them to the existing CSS
257 + // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS)
258 + $allCss = $this->css;
259 +
260 + $allCss .= $this->getCssFromAllStyleNodes( $xpath );
261 +
262 + $cssParts = $this->splitCssAndMediaQuery( $allCss );
263 + self::$_media = ''; // reset
264 +
265 + $cssKey = md5( $cssParts['css'] );
266 + if ( !isset( $this->caches[self::CACHE_KEY_CSS][$cssKey] ) ) {
267 + // process the CSS file for selectors and definitions
268 + preg_match_all( '/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mis', $cssParts['css'], $matches, PREG_SET_ORDER );
269 +
270 + $allSelectors = array();
271 + foreach ( $matches as $key => $selectorString ) {
272 + // if there is a blank definition, skip
273 + if ( !strlen( trim( $selectorString[2] ) ) ) {
274 + continue;
275 + }
276 +
277 + // else split by commas and duplicate attributes so we can sort by selector precedence
278 + $selectors = explode( ',', $selectorString[1] );
279 + foreach ( $selectors as $selector ) {
280 + // don't process pseudo-elements and behavioral (dynamic) pseudo-classes; ONLY allow structural pseudo-classes
281 + if ( strpos( $selector, ':' ) !== false && !preg_match( '/:\\S+\\-(child|type)\\(/i', $selector ) ) {
282 + continue;
283 + }
284 +
285 + $allSelectors[] = array( 'selector' => trim( $selector ),
286 + 'attributes' => trim( $selectorString[2] ),
287 + // keep track of where it appears in the file, since order is important
288 + 'line' => $key,
289 + );
290 + }
291 + }
292 +
293 + // now sort the selectors by precedence
294 + usort( $allSelectors, array( $this, 'sortBySelectorPrecedence' ) );
295 +
296 + $this->caches[self::CACHE_KEY_CSS][$cssKey] = $allSelectors;
297 + }
298 +
299 + foreach ( $this->caches[self::CACHE_KEY_CSS][$cssKey] as $value ) {
300 + // query the body for the xpath selector
301 + $nodesMatchingCssSelectors = $xpath->query( $this->translateCssToXpath( $value['selector'] ) );
302 +
303 + /** @var $node \DOMNode */
304 + foreach ( $nodesMatchingCssSelectors as $node ) {
305 + // if it has a style attribute, get it, process it, and append (overwrite) new stuff
306 + if ( $node->hasAttribute( 'style' ) ) {
307 + // break it up into an associative array
308 + $oldStyleDeclarations = $this->parseCssDeclarationBlock( $node->getAttribute( 'style' ) );
309 + } else {
310 + $oldStyleDeclarations = array();
311 + }
312 + $newStyleDeclarations = $this->parseCssDeclarationBlock( $value['attributes'] );
313 + $node->setAttribute( 'style', $this->generateStyleStringFromDeclarationsArrays( $oldStyleDeclarations, $newStyleDeclarations ) );
314 + }
315 + }
316 +
317 + // now iterate through the nodes that contained inline styles in the original HTML
318 + foreach ( $this->styleAttributesForNodes as $nodePath => $styleAttributesForNode ) {
319 + $node = $this->visitedNodes[$nodePath];
320 + $currentStyleAttributes = $this->parseCssDeclarationBlock( $node->getAttribute( 'style' ) );
321 + $node->setAttribute( 'style', $this->generateStyleStringFromDeclarationsArrays( $currentStyleAttributes, $styleAttributesForNode ) );
322 + }
323 +
324 + // This removes styles from your email that contain display:none.
325 + // We need to look for display:none, but we need to do a case-insensitive search. Since DOMDocument only supports XPath 1.0,
326 + // lower-case() isn't available to us. We've thus far only set attributes to lowercase, not attribute values. Consequently, we need
327 + // to translate() the letters that would be in 'NONE' ("NOE") to lowercase.
328 + $nodesWithStyleDisplayNone = $xpath->query( '//*[contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")]' );
329 + // The checks on parentNode and is_callable below ensure that if we've deleted the parent node,
330 + // we don't try to call removeChild on a nonexistent child node
331 + if ( $nodesWithStyleDisplayNone->length > 0 ) {
332 + /** @var $node \DOMNode */
333 + foreach ( $nodesWithStyleDisplayNone as $node ) {
334 + if ( $node->parentNode && is_callable( array( $node->parentNode, 'removeChild' ) ) ) {
335 + $node->parentNode->removeChild( $node );
336 + }
337 + }
338 + }
339 +
340 + $this->copyCssWithMediaToStyleNode( $cssParts, $xmlDocument );
341 +
342 + if ( $this->preserveEncoding ) {
343 + if ( function_exists( 'mb_convert_encoding' ) ) {
344 + return mb_convert_encoding( $xmlDocument->saveHTML(), self::ENCODING, 'HTML-ENTITIES' );
345 + } else {
346 + return htmlspecialchars_decode( utf8_encode( html_entity_decode( $xmlDocument->saveHTML(), ENT_COMPAT, self::ENCODING ) ) );
347 + }
348 + } else {
349 + return $xmlDocument->saveHTML();
350 + }
351 + }
352 +
353 + public function strtolower( array $m ) {
354 + return strtolower( $m[0] );
355 + }
356 +
357 +
358 + /**
359 + * This method merges old or existing name/value array with new name/value array
360 + * and then generates a string of the combined style suitable for placing inline.
361 + * This becomes the single point for CSS string generation allowing for consistent
362 + * CSS output no matter where the CSS originally came from.
363 + *
364 + * @param array $oldStyles
365 + * @param array $newStyles
366 + *
367 + * @return string
368 + */
369 + private function generateStyleStringFromDeclarationsArrays( array $oldStyles, array $newStyles ) {
370 + $combinedStyles = array_merge( $oldStyles, $newStyles );
371 + $style = '';
372 + foreach ( $combinedStyles as $attributeName => $attributeValue ) {
373 + $style .= ( strtolower( trim( $attributeName ) ) . ': ' . trim( $attributeValue ) . '; ' );
374 + }
375 + return trim( $style );
376 + }
377 +
378 +
379 + /**
380 + * Copies the media part from CSS array parts to $xmlDocument.
381 + *
382 + * @param array $cssParts
383 + * @param DOMDocument $xmlDocument
384 + */
385 + public function copyCssWithMediaToStyleNode( array $cssParts, DOMDocument $xmlDocument ) {
386 + if ( isset( $cssParts['media'] ) && $cssParts['media'] !== '' ) {
387 + $this->addStyleElementToDocument( $xmlDocument, $cssParts['media'] );
388 + }
389 + }
390 +
391 + /**
392 + * Returns CSS content.
393 + *
394 + * @param DOMXPath $xpath
395 + *
396 + * @return string
397 + */
398 + private function getCssFromAllStyleNodes( DOMXPath $xpath ) {
399 + $styleNodes = $xpath->query( '//style' );
400 +
401 + if ( $styleNodes === false ) {
402 + return '';
403 + }
404 +
405 + $css = '';
406 + /** @var $styleNode DOMNode */
407 + foreach ( $styleNodes as $styleNode ) {
408 + $css .= "\n\n" . $styleNode->nodeValue;
409 + $styleNode->parentNode->removeChild( $styleNode );
410 + }
411 +
412 + return $css;
413 + }
414 +
415 + /**
416 + * Adds a style element with $css to $document.
417 + *
418 + * @param DOMDocument $document
419 + * @param string $css
420 + */
421 + private function addStyleElementToDocument( DOMDocument $document, $css ) {
422 + $styleElement = $document->createElement( 'style', $css );
423 + $styleAttribute = $document->createAttribute( 'type' );
424 + $styleAttribute->value = 'text/css';
425 + $styleElement->appendChild( $styleAttribute );
426 +
427 + $head = $this->getOrCreateHeadElement( $document );
428 + $head->appendChild( $styleElement );
429 + }
430 +
431 + /**
432 + * Returns the existing or creates a new head element in $document.
433 + *
434 + * @param DOMDocument $document
435 + *
436 + * @return DOMNode the head element
437 + */
438 + private function getOrCreateHeadElement( DOMDocument $document ) {
439 + $head = $document->getElementsByTagName( 'head' )->item( 0 );
440 +
441 + if ( $head === null ) {
442 + $head = $document->createElement( 'head' );
443 + $html = $document->getElementsByTagName( 'html' )->item( 0 );
444 + $html->insertBefore( $head, $document->getElementsByTagName( 'body' )->item( 0 ) );
445 + }
446 +
447 + return $head;
448 + }
449 +
450 + /**
451 + * Splits input CSS code to an array where:
452 + *
453 + * - key "css" will be contains clean CSS code
454 + * - key "media" will be contains all valuable media queries
455 + *
456 + * Example:
457 + *
458 + * The CSS code
459 + *
460 + * "@import "file.css"; h1 { color:red; } @media { h1 {}} @media tv { h1 {}}"
461 + *
462 + * will be parsed into the following array:
463 + *
464 + * "css" => "h1 { color:red; }"
465 + * "media" => "@media { h1 {}}"
466 + *
467 + * @param string $css
468 + *
469 + * @return array
470 + */
471 + private function splitCssAndMediaQuery( $css ) {
472 + $css = preg_replace_callback( '#@media\\s+(?:only\\s)?(?:[\\s{\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misU', array( $this, '_media_concat' ), $css );
473 +
474 + // filter the CSS
475 + $search = array(
476 + // get rid of css comment code
477 + '/\\/\\*.*\\*\\//sU',
478 + // strip out any import directives
479 + '/^\\s*@import\\s[^;]+;/misU',
480 + // strip remains media enclosures
481 + '/^\\s*@media\\s[^{]+{(.*)}\\s*}\\s/misU',
482 + );
483 +
484 + $replace = array(
485 + '',
486 + '',
487 + '',
488 + );
489 +
490 + // clean CSS before output
491 + $css = preg_replace( $search, $replace, $css );
492 +
493 + return array( 'css' => $css, 'media' => self::$_media );
494 + }
495 +
496 + private function _media_concat( $matches ) {
497 + self::$_media .= $matches[0];
498 + }
499 +
500 + /**
501 + * Creates a DOMDocument instance with the current HTML.
502 + *
503 + * @return DOMDocument
504 + */
505 + private function createXmlDocument() {
506 + $xmlDocument = new DOMDocument;
507 + $xmlDocument->encoding = self::ENCODING;
508 + $xmlDocument->strictErrorChecking = false;
509 + $xmlDocument->formatOutput = true;
510 + $libXmlState = libxml_use_internal_errors( true );
511 + $xmlDocument->loadHTML( $this->getUnifiedHtml() );
512 + libxml_clear_errors();
513 + libxml_use_internal_errors( $libXmlState );
514 + $xmlDocument->normalizeDocument();
515 +
516 + return $xmlDocument;
517 + }
518 +
519 + /**
520 + * Returns the HTML with the non-ASCII characters converts into HTML entities and the unprocessable HTML tags removed.
521 + *
522 + * @return string the unified HTML
523 + *
524 + * @throws BadMethodCallException
525 + */
526 + private function getUnifiedHtml() {
527 + if ( !empty( $this->unprocessableHtmlTags ) ) {
528 + $unprocessableHtmlTags = implode( '|', $this->unprocessableHtmlTags );
529 + $bodyWithoutUnprocessableTags = preg_replace( '/<\\/?(' . $unprocessableHtmlTags . ')[^>]*>/i', '', $this->html );
530 + } else {
531 + $bodyWithoutUnprocessableTags = $this->html;
532 + }
533 +
534 + return htmlspecialchars_decode( utf8_decode( htmlentities( $bodyWithoutUnprocessableTags, ENT_COMPAT, self::ENCODING, false ) ) );
535 + }
536 +
537 + /**
538 + * @param array $a
539 + * @param array $b
540 + *
541 + * @return integer
542 + */
543 + private function sortBySelectorPrecedence( array $a, array $b ) {
544 + $precedenceA = $this->getCssSelectorPrecedence( $a['selector'] );
545 + $precedenceB = $this->getCssSelectorPrecedence( $b['selector'] );
546 +
547 + // We want these sorted in ascending order so selectors with lesser precedence get processed first and
548 + // selectors with greater precedence get sorted last.
549 + // The parenthesis around the -1 are necessary to avoid a PHP_CodeSniffer warning about missing spaces around
550 + // arithmetic operators.
551 + // @see http://forge.typo3.org/issues/55605
552 + $precedenceForEquals = ( $a['line'] < $b['line'] ? ( - 1 ) : 1 );
553 + $precedenceForNotEquals = ( $precedenceA < $precedenceB ? ( - 1 ) : 1 );
554 + return ( $precedenceA === $precedenceB ) ? $precedenceForEquals : $precedenceForNotEquals;
555 + }
556 +
557 + /**
558 + * @param string $selector
559 + *
560 + * @return integer
561 + */
562 + private function getCssSelectorPrecedence( $selector ) {
563 + $selectorKey = md5( $selector );
564 + if ( !isset( $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] ) ) {
565 + $precedence = 0;
566 + $value = 100;
567 + // ids: worth 100, classes: worth 10, elements: worth 1
568 + $search = array( '\\#', '\\.', '' );
569 +
570 + foreach ( $search as $s ) {
571 + if ( trim( $selector == '' ) ) {
572 + break;
573 + }
574 + $number = 0;
575 + $selector = preg_replace( '/' . $s . '\\w+/', '', $selector, - 1, $number );
576 + $precedence += ( $value * $number );
577 + $value /= 10;
578 + }
579 + $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence;
580 + }
581 +
582 + return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey];
583 + }
584 +
585 + /**
586 + * Right now, we support all CSS 1 selectors and most CSS2/3 selectors.
587 + *
588 + * @see http://plasmasturm.org/log/444/
589 + *
590 + * @param string $paramCssSelector
591 + *
592 + * @return string
593 + */
594 + private function translateCssToXpath( $paramCssSelector ) {
595 + $cssSelector = ' ' . $paramCssSelector . ' ';
596 + $cssSelector = preg_replace_callback( '/\s+\w+\s+/', array( $this, 'strtolower' ), $cssSelector );
597 + $cssSelector = trim( $cssSelector );
598 + $xpathKey = md5( $cssSelector );
599 + if ( !isset( $this->caches[self::CACHE_KEY_XPATH][$xpathKey] ) ) {
600 + // returns an Xpath selector
601 + $search = array(
602 + // Matches any element that is a child of parent.
603 + '/\\s+>\\s+/',
604 + // Matches any element that is an adjacent sibling.
605 + '/\\s+\\+\\s+/',
606 + // Matches any element that is a descendant of an parent element element.
607 + '/\\s+/',
608 + // first-child pseudo-selector
609 + '/([^\\/]+):first-child/i',
610 + // last-child pseudo-selector
611 + '/([^\\/]+):last-child/i',
612 + // Matches attribute only selector
613 + '/^\\[(\\w+)\\]/',
614 + // Matches element with attribute
615 + '/(\\w)\\[(\\w+)\\]/',
616 + // Matches element with EXACT attribute
617 + '/(\\w)\\[(\\w+)\\=[\'"]?(\\w+)[\'"]?\\]/',
618 + );
619 + $replace = array(
620 + '/',
621 + '/following-sibling::*[1]/self::',
622 + '//',
623 + '*[1]/self::\\1',
624 + '*[last()]/self::\\1',
625 + '*[@\\1]',
626 + '\\1[@\\2]',
627 + '\\1[@\\2="\\3"]',
628 + );
629 +
630 + $cssSelector = '//' . preg_replace( $search, $replace, $cssSelector );
631 +
632 + $cssSelector = preg_replace_callback( self::ID_ATTRIBUTE_MATCHER, array( $this, 'matchIdAttributes' ), $cssSelector );
633 + $cssSelector = preg_replace_callback( self::CLASS_ATTRIBUTE_MATCHER, array( $this, 'matchClassAttributes' ), $cssSelector );
634 +
635 + // Advanced selectors are going to require a bit more advanced emogrification.
636 + // When we required PHP 5.3, we could do this with closures.
637 + $cssSelector = preg_replace_callback(
638 + '/([^\\/]+):nth-child\\(\s*(odd|even|[+\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
639 + array( $this, 'translateNthChild' ), $cssSelector
640 + );
641 + $cssSelector = preg_replace_callback(
642 + '/([^\\/]+):nth-of-type\\(\s*(odd|even|[+\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
643 + array( $this, 'translateNthOfType' ), $cssSelector
644 + );
645 +
646 + $this->caches[self::CACHE_KEY_SELECTOR][$xpathKey] = $cssSelector;
647 + }
648 + return $this->caches[self::CACHE_KEY_SELECTOR][$xpathKey];
649 + }
650 +
651 + /**
652 + * @param array $match
653 + *
654 + * @return string
655 + */
656 + private function matchIdAttributes( array $match ) {
657 + return ( strlen( $match[1] ) ? $match[1] : '*' ) . '[@id="' . $match[2] . '"]';
658 + }
659 +
660 + /**
661 + * @param array $match
662 + *
663 + * @return string
664 + */
665 + private function matchClassAttributes( array $match ) {
666 + return ( strlen( $match[1] ) ? $match[1] : '*' ) . '[contains(concat(" ",@class," "),concat(" ","' .
667 + implode(
668 + '"," "))][contains(concat(" ",@class," "),concat(" ","',
669 + explode( '.', substr( $match[2], 1 ) )
670 + ) . '"," "))]';
671 + }
672 +
673 + /**
674 + * @param array $match
675 + *
676 + * @return string
677 + */
678 + private function translateNthChild( array $match ) {
679 + $result = $this->parseNth( $match );
680 +
681 + if ( isset( $result[self::MULTIPLIER] ) ) {
682 + if ( $result[self::MULTIPLIER] < 0 ) {
683 + $result[self::MULTIPLIER] = abs( $result[self::MULTIPLIER] );
684 + return sprintf( '*[(last() - position()) mod %u = %u]/self::%s', $result[self::MULTIPLIER], $result[self::INDEX], $match[1] );
685 + } else {
686 + return sprintf( '*[position() mod %u = %u]/self::%s', $result[self::MULTIPLIER], $result[self::INDEX], $match[1] );
687 + }
688 + } else {
689 + return sprintf( '*[%u]/self::%s', $result[self::INDEX], $match[1] );
690 + }
691 + }
692 +
693 + /**
694 + * @param array $match
695 + *
696 + * @return string
697 + */
698 + private function translateNthOfType( array $match ) {
699 + $result = $this->parseNth( $match );
700 +
701 + if ( isset( $result[self::MULTIPLIER] ) ) {
702 + if ( $result[self::MULTIPLIER] < 0 ) {
703 + $result[self::MULTIPLIER] = abs( $result[self::MULTIPLIER] );
704 + return sprintf( '%s[(last() - position()) mod %u = %u]', $match[1], $result[self::MULTIPLIER], $result[self::INDEX] );
705 + } else {
706 + return sprintf( '%s[position() mod %u = %u]', $match[1], $result[self::MULTIPLIER], $result[self::INDEX] );
707 + }
708 + } else {
709 + return sprintf( '%s[%u]', $match[1], $result[self::INDEX] );
710 + }
711 + }
712 +
713 + /**
714 + * @param array $match
715 + *
716 + * @return array
717 + */
718 + private function parseNth( array $match ) {
719 + if ( in_array( strtolower( $match[2] ), array( 'even', 'odd' ) ) ) {
720 + $index = strtolower( $match[2] ) == 'even' ? 0 : 1;
721 + return array( self::MULTIPLIER => 2, self::INDEX => $index );
722 + } elseif ( stripos( $match[2], 'n' ) === false ) {
723 + // if there is a multiplier
724 + $index = intval( str_replace( ' ', '', $match[2] ) );
725 + return array( self::INDEX => $index );
726 + } else {
727 + if ( isset( $match[3] ) ) {
728 + $multipleTerm = str_replace( $match[3], '', $match[2] );
729 + $index = intval( str_replace( ' ', '', $match[3] ) );
730 + } else {
731 + $multipleTerm = $match[2];
732 + $index = 0;
733 + }
734 +
735 + $multiplier = str_ireplace( 'n', '', $multipleTerm );
736 +
737 + if ( !strlen( $multiplier ) ) {
738 + $multiplier = 1;
739 + } elseif ( $multiplier == 0 ) {
740 + return array( self::INDEX => $index );
741 + } else {
742 + $multiplier = intval( $multiplier );
743 + }
744 +
745 + while ( $index < 0 ) {
746 + $index += abs( $multiplier );
747 + }
748 +
749 + return array( self::MULTIPLIER => $multiplier, self::INDEX => $index );
750 + }
751 + }
752 +
753 + /**
754 + * Parses a CSS declaration block into property name/value pairs.
755 + *
756 + * Example:
757 + *
758 + * The declaration block
759 + *
760 + * "color: #000; font-weight: bold;"
761 + *
762 + * will be parsed into the following array:
763 + *
764 + * "color" => "#000"
765 + * "font-weight" => "bold"
766 + *
767 + * @param string $cssDeclarationBlock the CSS declaration block without the curly braces, may be empty
768 + *
769 + * @return array the CSS declarations with the property names as array keys and the property values as array values
770 + */
771 + private function parseCssDeclarationBlock( $cssDeclarationBlock ) {
772 + if ( isset( $this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock] ) ) {
773 + return $this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock];
774 + }
775 +
776 + $properties = array();
777 + $declarations = explode( ';', $cssDeclarationBlock );
778 + foreach ( $declarations as $declaration ) {
779 + $matches = array();
780 + if ( !preg_match( '/ *([A-Za-z\\-]+) *: *([^;]+) */', $declaration, $matches ) ) {
781 + continue;
782 + }
783 + $propertyName = strtolower( $matches[1] );
784 + $propertyValue = $matches[2];
785 + $properties[$propertyName] = $propertyValue;
786 + }
787 + $this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock] = $properties;
788 +
789 + return $properties;
790 + }
791 + }
792 +}