Lib_Pelago - Version 1.9.1.0

Version Notes

1.9.1.0

Download this release

Release Info

Developer Magento Core Team
Extension Lib_Pelago
Version 1.9.1.0
Comparing to
See all releases


Version 1.9.1.0

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