source: trunk/library/CssToInlineStyles/css_to_inline_styles.php @ 5476

Revision 5476, 13.8 KB checked in by cristiano, 12 years ago (diff)

Ticket #2480 - Adequações no parser de e-mail - Tratamento do HTML

Line 
1<?php
2
3/**
4 * CSS to Inline Styles class
5 *
6 * This source file can be used to convert HTML with CSS into HTML with inline styles
7 *
8 * Known issues:
9 * - no support for pseudo selectors
10 *
11 * The class is documented in the file itself. If you find any bugs help me out and report them. Reporting can be done by sending an email to php-css-to-inline-styles-bugs[at]verkoyen[dot]eu.
12 * If you report a bug, make sure you give me enough information (include your code).
13 *
14 * Changelog since 1.0.2
15 * - .class are matched from now on.
16 * - fixed issue with #id
17 * - new beta-feature: added a way to output valid XHTML (thx to Matt Hornsby)
18 *
19 * Changelog since 1.0.1
20 * - fixed some stuff on specifity
21 *
22 * Changelog since 1.0.0
23 * - rewrote the buildXPathQuery-method
24 * - fixed some stuff on specifity
25 * - added a way to use inline style-blocks
26 *
27 * License
28 * Copyright (c) 2010, Tijs Verkoyen. All rights reserved.
29 *
30 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
31 *
32 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
33 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
34 * 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.
35 *
36 * This software is provided by the author "as is" and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the author be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
37 *
38 * @author              Tijs Verkoyen <php-css-to-inline-styles@verkoyen.eu>
39 * @version             1.0.3
40 *
41 * @copyright   Copyright (c) 2010, Tijs Verkoyen. All rights reserved.
42 * @license             BSD License
43 */
44class CSSToInlineStyles
45{
46        /**
47         * The CSS to use
48         *
49         * @var string
50         */
51        private $css;
52
53
54        /**
55         * The processed CSS rules
56         *
57         * @var array
58         */
59        private $cssRules;
60
61
62        /**
63         * Should the generated HTML be cleaned
64         *
65         * @var bool
66         */
67        private $cleanup = false;
68
69
70        /**
71         * The HTML to process
72         *
73         * @var string
74         */
75        private $html;
76
77
78        /**
79         * Use inline-styles block as CSS
80         *
81         * @var bool
82         */
83        private $useInlineStylesBlock = false;
84
85
86        /**
87         * Creates an instance, you could set the HTML and CSS here, or load it later.
88         *
89         * @return      void
90         * @param       string[optional] $html  The HTML to process
91         * @param       string[optional] $css   The CSS to use
92         */
93        public function __construct($html = null, $css = null)
94        {
95                if($html !== null) $this->setHTML($html);
96                if($css !== null) $this->setCSS($css);
97        }
98
99
100        /**
101         * Convert a CSS-selector into an xPath-query
102         *
103         * @return      string
104         * @param       string $selector        The CSS-selector
105         */
106        private function buildXPathQuery($selector)
107        {
108                // redefine
109                $selector = (string) $selector;
110
111                // the CSS selector
112                $cssSelector = array(   '/(\w)\s+(\w)/',                                // E F                          Matches any F element that is a descendant of an E element
113                                                                '/(\w)\s*>\s*(\w)/',                    // E > F                        Matches any F element that is a child of an element E
114                                                                '/(\w):first-child/',                   // E:first-child        Matches element E when E is the first child of its parent
115                                                                '/(\w)\s*\+\s*(\w)/',                   // E + F                        Matches any F element immediately preceded by an element
116                                                                '/(\w)\[([\w\-]+)]/',                   // E[foo]                       Matches any E element with the "foo" attribute set (whatever the value)
117                                                                '/(\w)\[([\w\-]+)\=\"(.*)\"]/', // E[foo="warning"]     Matches any E element whose "foo" attribute value is exactly equal to "warning"
118                                                                '/(\w+|\*)+\.([\w\-]+)+/',              // div.warning          HTML only. The same as DIV[class~="warning"]
119                                                                '/\.([\w\-]+)/',                                // .warning                     HTML only. The same as *[class~="warning"]
120                                                                '/(\w+)+\#([\w\-]+)/',                  // E#myid                       Matches any E element with id-attribute equal to "myid"
121                                                                '/\#([\w\-]+)/'                                 // #myid                        Matches any element with id-attribute equal to "myid"
122                                                        );
123
124                // the xPath-equivalent
125                $xPathQuery = array(    '\1//\2',                                                                                                                                               // E F                          Matches any F element that is a descendant of an E element
126                                                                '\1/\2',                                                                                                                                                // E > F                        Matches any F element that is a child of an element E
127                                                                '*[1]/self::\1',                                                                                                                                // E:first-child        Matches element E when E is the first child of its parent
128                                                                '\1/following-sibling::*[1]/self::\2',                                                                                  // E + F                        Matches any F element immediately preceded by an element
129                                                                '\1 [ @\2 ]',                                                                                                                                   // E[foo]                       Matches any E element with the "foo" attribute set (whatever the value)
130                                                                '\1[ contains( concat( " ", @\2, " " ), concat( " ", "\3", " " ) ) ]',                  // E[foo="warning"]     Matches any E element whose "foo" attribute value is exactly equal to "warning"
131                                                                '\1[ contains( concat( " ", @class, " " ), concat( " ", "\2", " " ) ) ]',               // div.warning          HTML only. The same as DIV[class~="warning"]
132                                                                '*[ contains( concat( " ", @class, " " ), concat( " ", "\1", " " ) ) ]',                // .warning                     HTML only. The same as *[class~="warning"]
133                                                                '\1[ @id = "\2" ]',                                                                                                                             // E#myid                       Matches any E element with id-attribute equal to "myid"
134                                                                '*[ @id = "\1" ]'                                                                                                                               // #myid                        Matches any element with id-attribute equal to "myid"
135                                                        );
136
137                // return
138                return (string) '//'. preg_replace($cssSelector, $xPathQuery, $selector);
139        }
140
141
142        /**
143         * Calculate the specifity for the CSS-selector
144         *
145         * @return      int
146         * @param       string $selector
147         */
148        private function calculateCSSSpecifity($selector)
149        {
150                // cleanup selector
151                $selector = str_replace(array('>', '+'), array(' > ', ' + '), $selector);
152
153                // init var
154                $specifity = 0;
155
156                // split the selector into chunks based on spaces
157                $chunks = explode(' ', $selector);
158
159                // loop chunks
160                foreach($chunks as $chunk)
161                {
162                        // an ID is important, so give it a high specifity
163                        if(strstr($chunk, '#') !== false) $specifity += 100;
164
165                        // classes are more important than a tag, but less important then an ID
166                        elseif(strstr($chunk, '.')) $specifity += 10;
167
168                        // anything else isn't that important
169                        else $specifity += 1;
170                }
171
172                // return
173                return $specifity;
174        }
175
176
177        /**
178         * Cleanup the generated HTML
179         *
180         * @return      string
181         * @param       string $html    The HTML to cleanup
182         */
183        private function cleanupHTML($html)
184        {
185                // remove classes
186                $html = preg_replace('/(\s)+class="(.*)"(\s)+/U', ' ', $html);
187
188                // remove IDs
189                $html = preg_replace('/(\s)+id="(.*)"(\s)+/U', ' ', $html);
190
191                // return
192                return $html;
193        }
194
195
196        /**
197         * Converts the loaded HTML into an HTML-string with inline styles based on the loaded CSS
198         *
199         * @return      string
200         * @param       bool $outputXHTML       Should we output valid XHTML?
201         */
202        public function convert($outputXHTML = false)
203        {
204                // redefine
205                $outputXHTML = (bool) $outputXHTML;
206
207                // validate
208                if($this->html == null) throw new CSSToInlineStylesException('No HTML provided.');
209
210                // should we use inline style-block
211                if($this->useInlineStylesBlock)
212                {
213                        // init var
214                        $matches = array();
215
216                        // match the style blocks
217                        preg_match_all('|<style(.*)>(.*)</style>|isU', $this->html, $matches);
218
219                        // any style-blocks found?
220                        if(!empty($matches[2]))
221                        {
222                                // add
223                                foreach($matches[2] as $match) $this->css .= trim($match) ."\n";
224                        }
225                }
226
227                // process css
228                $this->processCSS();
229
230                // create new DOMDocument
231                $document = new DOMDocument();
232
233                // set error level
234                libxml_use_internal_errors(true);
235
236                // load HTML
237                $document->loadHTML($this->html);
238
239                // create new XPath
240                $xPath = new DOMXPath($document);
241
242                // any rules?
243                if(!empty($this->cssRules))
244                {
245                        // loop rules
246                        foreach($this->cssRules as $rule)
247                        {
248                                // init var
249                                $query = $this->buildXPathQuery($rule['selector']);
250
251                                // validate query
252                                if($query === false) continue;
253
254                                // search elements
255                                $elements = $xPath->query($query);
256
257                                // validate elements
258                                if($elements === false) continue;
259
260                                // loop found elements
261                                foreach($elements as $element)
262                                {
263                                        // init var
264                                        $properties = array();
265
266                                        // get current styles
267                                        $stylesAttribute = $element->attributes->getNamedItem('style');
268                                       
269                                        // add new properties into the list
270                                        foreach($rule['properties'] as $key => $value) $properties[$key] = $value;
271                                       
272                                        // any styles defined before?
273                                        if($stylesAttribute !== null)
274                                        {
275                                                // get value for the styles attribute
276                                                $definedStyles = (string) $stylesAttribute->value;
277
278                                                // split into properties
279                                                $definedProperties = (array) explode(';', $definedStyles);
280   
281                                                // loop properties
282                                                foreach($definedProperties as $property)
283                                                {
284                                                        // validate property
285                                                        if($property == '') continue;
286
287                                                        // split into chunks
288                                                        $chunks = (array) explode(':', trim($property), 2);
289
290                                                        // validate
291                                                        if(!isset($chunks[1])) continue;
292
293                                                        // loop chunks
294                                                        $properties[$chunks[0]] = trim($chunks[1]);
295                                                }
296                                        }                                     
297
298
299                                        // build string
300                                        $propertyChunks = array();
301
302                                        // build chunks
303                                        foreach($properties as $key => $value) $propertyChunks[] = $key .': '. $value .';';
304
305                                        // build properties string
306                                        $propertiesString = implode(' ', $propertyChunks);
307
308                                        // set attribute
309                                        if($propertiesString != '') $element->setAttribute('style', $propertiesString);
310                                }
311                        }
312                }
313
314                // should we output XHTML?
315                if($outputXHTML)
316                {
317                        // set formating
318                        $document->formatOutput = true;
319
320                        // get the HTML as XML
321                        $html = $document->saveXML(null, LIBXML_NOEMPTYTAG);
322
323                        // remove the XML-header
324                        $html = str_replace('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n", '', $html);
325                }
326
327                // just regular HTML 4.01 as it should be used in newsletters
328                else
329                {
330                        // get the HTML
331                        $html = $document->saveHTML();
332                }
333
334                // cleanup the HTML if we need to
335                if($this->cleanup) $html = $this->cleanupHTML($html);
336
337                // return
338                return $html;
339        }
340
341
342        /**
343         * Process the loaded CSS
344         *
345         * @return      void
346         */
347        private function processCSS()
348        {
349                // init vars
350                $css = (string) $this->css;
351
352                // remove newlines
353                $css = str_replace(array("\r", "\n"), '', $css);
354
355                // replace double quotes by single quotes
356                $css = str_replace('"', '\'', $css);
357
358                // remove comments
359                $css = preg_replace('|/\*.*?\*/|', '', $css);
360
361                // remove spaces
362                $css = preg_replace('/\s\s+/', ' ', $css);
363
364                // rules are splitted by }
365                $rules = (array) explode('}', $css);
366
367                // init var
368                $i = 1;
369
370                // loop rules
371                foreach($rules as $rule)
372                {
373                        // split into chunks
374                        $chunks = explode('{', $rule);
375
376                        // invalid rule?
377                        if(!isset($chunks[1])) continue;
378
379                        // set the selectors
380                        $selectors = trim($chunks[0]);
381
382                        // get cssProperties
383                        $cssProperties = trim($chunks[1]);
384
385                        // split multiple selectors
386                        $selectors = (array) explode(',', $selectors);
387
388                        // loop selectors
389                        foreach($selectors as $selector)
390                        {
391                                // cleanup
392                                $selector = trim($selector);
393
394                                // build an array for each selector
395                                $ruleSet = array();
396
397                                // store selector
398                                $ruleSet['selector'] = $selector;
399
400                                // process the properties
401                                $ruleSet['properties'] = $this->processCSSProperties($cssProperties);
402
403                                // calculate specifity
404                                $ruleSet['specifity'] = $this->calculateCSSSpecifity($selector);
405
406                                // add into global rules
407                                $this->cssRules[] = $ruleSet;
408                        }
409
410                        // increment
411                        $i++;
412                }
413
414                // sort based on specifity
415                if(!empty($this->cssRules)) usort($this->cssRules, array('CSSToInlineStyles', 'sortOnSpecifity'));
416        }
417
418
419        /**
420         * Process the CSS-properties
421         *
422         * @return      array
423         * @param       string $propertyString
424         */
425        private function processCSSProperties($propertyString)
426        {
427                // split into chunks
428                $properties = (array) explode(';', $propertyString);
429
430                // init var
431                $pairs = array();
432
433                // loop properties
434                foreach($properties as $property)
435                {
436                        // split into chunks
437                        $chunks = (array) explode(':', $property, 2);
438
439                        // validate
440                        if(!isset($chunks[1])) continue;
441
442                        // add to pairs array
443                        $pairs[trim($chunks[0])] = trim($chunks[1]);
444                }
445
446                // sort the pairs
447                ksort($pairs);
448
449                // return
450                return $pairs;
451        }
452
453
454        /**
455         * Should the IDs and classes be removed?
456         *
457         * @return      void
458         * @param       bool[optional] $on
459         */
460        public function setCleanup($on = true)
461        {
462                $this->cleanup = (bool) $on;
463        }
464
465
466        /**
467         * Set CSS to use
468         *
469         * @return      void
470         * @param       string $css             The CSS to use
471         */
472        public function setCSS($css)
473        {
474                $this->css = (string) $css;
475        }
476
477
478        /**
479         * Set HTML to process
480         *
481         * @return      void
482         * @param       string $html
483         */
484        public function setHTML($html)
485        {
486                $this->html = (string) $html;
487        }
488
489
490        /**
491         * Set use of inline styles block
492         * If this is enabled the class will use the style-block in the HTML.
493         *
494         * @param       bool[optional] $on
495         */
496        public function setUseInlineStylesBlock($on = true)
497        {
498                $this->useInlineStylesBlock = (bool) $on;
499        }
500
501
502        /**
503         * Sort an array on the specifity element
504         *
505         * @return      int
506         * @param       array $e1       The first element
507         * @param       array $e2       The second element
508         */
509        private static function sortOnSpecifity($e1, $e2)
510        {
511                // validate
512                if(!isset($e1['specifity']) || !isset($e2['specifity'])) return 0;
513
514                // lower
515                if($e1['specifity'] < $e2['specifity']) return -1;
516
517                // higher
518                if($e1['specifity'] > $e2['specifity']) return 1;
519
520                // fallback
521                return 0;
522        }
523}
524
525
526/**
527 * CSSToInlineStyles Exception class
528 *
529 * @author      Tijs Verkoyen <php-css-to-inline-styles@verkoyen.eu>
530 */
531class CSSToInlineStylesException extends Exception
532{
533}
534
535?>
Note: See TracBrowser for help on using the repository browser.