CssToInlineStyles   F
last analyzed

Complexity

Total Complexity 70

Size/Duplication

Total Lines 688
Duplicated Lines 7.85 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 54
loc 688
rs 2.712
c 0
b 0
f 0
wmc 70
lcom 1
cbo 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 3
B buildXPathQuery() 0 58 1
A calculateCSSSpecifity() 0 26 4
A cleanupHTML() 0 11 1
F convert() 54 265 38
A getEncoding() 0 4 1
B processCSS() 0 74 5
A processCSSProperties() 0 33 5
A setCleanup() 0 4 1
A setCSS() 0 4 1
A setEncoding() 0 4 1
A setHTML() 0 4 1
A setUseInlineStylesBlock() 0 4 1
A setStripOriginalStyleTags() 0 4 1
A stripOriginalStyleTags() 0 4 1
A sortOnSpecifity() 0 14 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CssToInlineStyles often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CssToInlineStyles, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace TijsVerkoyen\CssToInlineStyles;
4
5
/**
6
 * CSS to Inline Styles class
7
 *
8
 * @author		Tijs Verkoyen <[email protected]>
9
 * @version		1.1.0
10
 * @copyright	Copyright (c), Tijs Verkoyen. All rights reserved.
11
 * @license		BSD License
12
 */
13
class CssToInlineStyles
14
{
15
    /**
16
     * The CSS to use
17
     *
18
     * @var	string
19
     */
20
    private $css;
21
22
    /**
23
     * The processed CSS rules
24
     *
25
     * @var	array
26
     */
27
    private $cssRules;
28
29
    /**
30
     * Should the generated HTML be cleaned
31
     *
32
     * @var	bool
33
     */
34
    private $cleanup = false;
35
36
    /**
37
     * The encoding to use.
38
     *
39
     * @var	string
40
     */
41
    private $encoding = 'UTF-8';
42
43
    /**
44
     * The HTML to process
45
     *
46
     * @var	string
47
     */
48
    private $html;
49
50
    /**
51
     * Use inline-styles block as CSS
52
     *
53
     * @var	bool
54
     */
55
    private $useInlineStylesBlock = false;
56
57
    /*
58
     * Strip original style tags
59
     *
60
     * @var bool
61
     */
62
    private $stripOriginalStyleTags = false;
63
64
    /**
65
     * Creates an instance, you could set the HTML and CSS here, or load it
66
     * later.
67
     *
68
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
69
     * @param  string[optional] $html The HTML to process.
70
     * @param  string[optional] $css  The CSS to use.
71
     */
72
    public function __construct($html = null, $css = null)
73
    {
74
        if($html !== null) $this->setHTML($html);
75
        if($css !== null) $this->setCSS($css);
76
    }
77
78
    /**
79
     * Convert a CSS-selector into an xPath-query
80
     *
81
     * @return string
82
     * @param  string $selector The CSS-selector.
83
     */
84
    private function buildXPathQuery($selector)
85
    {
86
        // redefine
87
        $selector = (string) $selector;
88
89
        // the CSS selector
90
        $cssSelector = array(
91
            // E F, Matches any F element that is a descendant of an E element
92
            '/(\w)\s+(\w)/',
93
            // E > F, Matches any F element that is a child of an element E
94
            '/(\w)\s*>\s*(\w)/',
95
            // E:first-child, Matches element E when E is the first child of its parent
96
            '/(\w):first-child/',
97
            // E + F, Matches any F element immediately preceded by an element
98
            '/(\w)\s*\+\s*(\w)/',
99
            // E[foo], Matches any E element with the "foo" attribute set (whatever the value)
100
            '/(\w)\[([\w\-]+)]/',
101
            // E[foo="warning"], Matches any E element whose "foo" attribute value is exactly equal to "warning"
102
            '/(\w)\[([\w\-]+)\=\"(.*)\"]/',
103
            // div.warning, HTML only. The same as DIV[class~="warning"]
104
            '/(\w+|\*)+\.([\w\-]+)+/',
105
            // .warning, HTML only. The same as *[class~="warning"]
106
            '/\.([\w\-]+)/',
107
            // E#myid, Matches any E element with id-attribute equal to "myid"
108
            '/(\w+)+\#([\w\-]+)/',
109
            // #myid, Matches any element with id-attribute equal to "myid"
110
            '/\#([\w\-]+)/'
111
        );
112
113
        // the xPath-equivalent
114
        $xPathQuery = array(
115
            // E F, Matches any F element that is a descendant of an E element
116
            '\1//\2',
117
            // E > F, Matches any F element that is a child of an element E
118
            '\1/\2',
119
            // E:first-child, Matches element E when E is the first child of its parent
120
            '*[1]/self::\1',
121
            // E + F, Matches any F element immediately preceded by an element
122
            '\1/following-sibling::*[1]/self::\2',
123
            // E[foo], Matches any E element with the "foo" attribute set (whatever the value)
124
            '\1 [ @\2 ]',
125
            // E[foo="warning"], Matches any E element whose "foo" attribute value is exactly equal to "warning"
126
            '\1[ contains( concat( " ", @\2, " " ), concat( " ", "\3", " " ) ) ]',
127
            // div.warning, HTML only. The same as DIV[class~="warning"]
128
            '\1[ contains( concat( " ", @class, " " ), concat( " ", "\2", " " ) ) ]',
129
            // .warning, HTML only. The same as *[class~="warning"]
130
            '*[ contains( concat( " ", @class, " " ), concat( " ", "\1", " " ) ) ]',
131
            // E#myid, Matches any E element with id-attribute equal to "myid"
132
            '\1[ @id = "\2" ]',
133
            // #myid, Matches any element with id-attribute equal to "myid"
134
            '*[ @id = "\1" ]'
135
        );
136
137
        // return
138
        $xPath = (string) '//' . preg_replace($cssSelector, $xPathQuery, $selector);
139
140
        return str_replace('] *', ']//*', $xPath);
141
    }
142
143
    /**
144
     * Calculate the specifity for the CSS-selector
145
     *
146
     * @return int
147
     * @param  string $selector The selector to calculate the specifity for.
148
     */
149
    private function calculateCSSSpecifity($selector)
150
    {
151
        // cleanup selector
152
        $selector = str_replace(array('>', '+'), array(' > ', ' + '), $selector);
153
154
        // init var
155
        $specifity = 0;
156
157
        // split the selector into chunks based on spaces
158
        $chunks = explode(' ', $selector);
159
160
        // loop chunks
161
        foreach ($chunks as $chunk) {
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[optional] $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 Exception('No HTML provided.');
209
210
        // should we use inline style-block
211
        if ($this->useInlineStylesBlock) {
212
            // init var
213
            $matches = array();
214
215
            // match the style blocks
216
            preg_match_all('|<style(.*)>(.*)</style>|isU', $this->html, $matches);
217
218
            // any style-blocks found?
219
            if (!empty($matches[2])) {
220
                // add
221
                foreach($matches[2] as $match) $this->css .= trim($match) ."\n";
222
            }
223
        }
224
225
        // process css
226
        $this->processCSS();
227
228
        // create new DOMDocument
229
        $document = new \DOMDocument('1.0', $this->getEncoding());
230
231
        // set error level
232
        libxml_use_internal_errors(true);
233
234
        // load HTML
235
//        $document->loadHTML($this->html);
236
        $document->loadHTML('<'.'?xml version="1.0" encoding="'.$this->getEncoding().'"?'.'><head><meta http-equiv="Content-Type" content="text/html; charset='.$this->getEncoding().'"></head>'.$this->html);
237
238
        // create new XPath
239
        $xPath = new \DOMXPath($document);
240
241
        // any rules?
242
        if (!empty($this->cssRules)) {
243
            // loop rules
244
            foreach ($this->cssRules as $rule) {
245
                // init var
246
                $query = $this->buildXPathQuery($rule['selector']);
247
248
                // validate query
249
                if($query === false) continue;
250
251
                // search elements
252
                $elements = $xPath->query($query);
253
254
                // validate elements
255
                if($elements === false) continue;
256
257
                // loop found elements
258
                foreach ($elements as $element) {
259
                    // no styles stored?
260
                    if ($element->attributes->getNamedItem(
261
                        'data-css-to-inline-styles-original-styles'
262
                    ) == null) {
263
                        // init var
264
                        $originalStyle = '';
265
                        if ($element->attributes->getNamedItem('style') !== null) {
266
                            $originalStyle = $element->attributes->getNamedItem('style')->value;
267
                        }
268
269
                        // store original styles
270
                        $element->setAttribute(
271
                            'data-css-to-inline-styles-original-styles',
272
                            $originalStyle
273
                        );
274
275
                        // clear the styles
276
                        $element->setAttribute('style', '');
277
                    }
278
279
                    // init var
280
                    $properties = array();
281
282
                    // get current styles
283
                    $stylesAttribute = $element->attributes->getNamedItem('style');
284
285
                    // any styles defined before?
286 View Code Duplication
                    if ($stylesAttribute !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
287
                        // get value for the styles attribute
288
                        $definedStyles = (string) $stylesAttribute->value;
289
290
                        // split into properties
291
                        $definedProperties = (array) explode(';', $definedStyles);
292
293
                        // loop properties
294
                        foreach ($definedProperties as $property) {
295
                            // validate property
296
                            if($property == '') continue;
297
298
                            // split into chunks
299
                            $chunks = (array) explode(':', trim($property), 2);
300
301
                            // validate
302
                            if(!isset($chunks[1])) continue;
303
304
                            // loop chunks
305
                            $properties[$chunks[0]] = trim($chunks[1]);
306
                        }
307
                    }
308
309
                    // add new properties into the list
310
                    foreach ($rule['properties'] as $key => $value) {
311
                        $properties[$key] = $value;
312
                    }
313
314
                    // build string
315
                    $propertyChunks = array();
316
317
                    // build chunks
318 View Code Duplication
                    foreach ($properties as $key => $values) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
319
                        foreach ((array) $values as $value) {
320
                            $propertyChunks[] = $key . ': ' . $value . ';';
321
                        }
322
                    }
323
324
                    // build properties string
325
                    $propertiesString = implode(' ', $propertyChunks);
326
327
                    // set attribute
328
                    if ($propertiesString != '') {
329
                        $element->setAttribute('style', $propertiesString);
330
                    }
331
                }
332
            }
333
334
            // reapply original styles
335
            $query = $this->buildXPathQuery(
336
                '*[@data-css-to-inline-styles-original-styles]'
337
            );
338
339
            // validate query
340
            if($query === false) return;
341
342
            // search elements
343
            $elements = $xPath->query($query);
344
345
            // loop found elements
346
            foreach ($elements as $element) {
347
                // get the original styles
348
                $originalStyle = $element->attributes->getNamedItem(
349
                    'data-css-to-inline-styles-original-styles'
350
                )->value;
351
352
                if ($originalStyle != '') {
353
                    $originalProperties = array();
354
                    $originalStyles = (array) explode(';', $originalStyle);
355
356
                    foreach ($originalStyles as $property) {
357
                        // validate property
358
                        if($property == '') continue;
359
360
                        // split into chunks
361
                        $chunks = (array) explode(':', trim($property), 2);
362
363
                        // validate
364
                        if(!isset($chunks[1])) continue;
365
366
                        // loop chunks
367
                        $originalProperties[$chunks[0]] = trim($chunks[1]);
368
                    }
369
370
                    // get current styles
371
                    $stylesAttribute = $element->attributes->getNamedItem('style');
372
                    $properties = array();
373
374
                    // any styles defined before?
375 View Code Duplication
                    if ($stylesAttribute !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
376
                        // get value for the styles attribute
377
                        $definedStyles = (string) $stylesAttribute->value;
378
379
                        // split into properties
380
                        $definedProperties = (array) explode(';', $definedStyles);
381
382
                        // loop properties
383
                        foreach ($definedProperties as $property) {
384
                            // validate property
385
                            if($property == '') continue;
386
387
                            // split into chunks
388
                            $chunks = (array) explode(':', trim($property), 2);
389
390
                            // validate
391
                            if(!isset($chunks[1])) continue;
392
393
                            // loop chunks
394
                            $properties[$chunks[0]] = trim($chunks[1]);
395
                        }
396
                    }
397
398
                    // add new properties into the list
399
                    foreach ($originalProperties as $key => $value) {
400
                        $properties[$key] = $value;
401
                    }
402
403
                    // build string
404
                    $propertyChunks = array();
405
406
                    // build chunks
407 View Code Duplication
                    foreach ($properties as $key => $values) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
408
                        foreach ((array) $values as $value) {
409
                            $propertyChunks[] = $key . ': ' . $value . ';';
410
                        }
411
                    }
412
413
                    // build properties string
414
                    $propertiesString = implode(' ', $propertyChunks);
415
416
                    // set attribute
417
                    if($propertiesString != '') $element->setAttribute(
418
                        'style', $propertiesString
419
                    );
420
                }
421
422
                // remove placeholder
423
                $element->removeAttribute(
424
                    'data-css-to-inline-styles-original-styles'
425
                );
426
            }
427
        }
428
429
        // should we output XHTML?
430
        if ($outputXHTML) {
431
            // set formating
432
            $document->formatOutput = true;
433
434
            // get the HTML as XML
435
            $html = $document->saveXML(null, LIBXML_NOEMPTYTAG);
436
437
            // get start of the XML-declaration
438
            $startPosition = strpos($html, '<?xml');
439
440
            // valid start position?
441
            if ($startPosition !== false) {
442
                // get end of the xml-declaration
443
                $endPosition = strpos($html, '?>', $startPosition);
444
445
                // remove the XML-header
446
                $html = ltrim(substr($html, $endPosition + 1));
447
            }
448
        }
449
450
        // just regular HTML 4.01 as it should be used in newsletters
451
        else {
452
            // get the HTML
453
            $html = $document->saveHTML();
454
        }
455
456
        // cleanup the HTML if we need to
457
        if($this->cleanup) $html = $this->cleanupHTML($html);
458
459
        // strip original style tags if we need to
460
        if ($this->stripOriginalStyleTags) {
461
            $html = $this->stripOriginalStyleTags($html);
462
        }
463
464
        // return
465
        return $html;
466
    }
467
468
469
    /**
470
     * Get the encoding to use
471
     *
472
     * @return string
473
     */
474
    private function getEncoding()
475
    {
476
        return $this->encoding;
477
    }
478
479
480
    /**
481
     * Process the loaded CSS
482
     *
483
     * @return void
484
     */
485
    private function processCSS()
486
    {
487
        // init vars
488
        $css = (string) $this->css;
489
490
        // remove newlines
491
        $css = str_replace(array("\r", "\n"), '', $css);
492
493
        // replace double quotes by single quotes
494
        $css = str_replace('"', '\'', $css);
495
496
        // remove comments
497
        $css = preg_replace('|/\*.*?\*/|', '', $css);
498
499
        // remove spaces
500
        $css = preg_replace('/\s\s+/', ' ', $css);
501
502
        // rules are splitted by }
503
        $rules = (array) explode('}', $css);
504
505
        // init var
506
        $i = 1;
507
508
        // loop rules
509
        foreach ($rules as $rule) {
510
            // split into chunks
511
            $chunks = explode('{', $rule);
512
513
            // invalid rule?
514
            if(!isset($chunks[1])) continue;
515
516
            // set the selectors
517
            $selectors = trim($chunks[0]);
518
519
            // get cssProperties
520
            $cssProperties = trim($chunks[1]);
521
522
            // split multiple selectors
523
            $selectors = (array) explode(',', $selectors);
524
525
            // loop selectors
526
            foreach ($selectors as $selector) {
527
                // cleanup
528
                $selector = trim($selector);
529
530
                // build an array for each selector
531
                $ruleSet = array();
532
533
                // store selector
534
                $ruleSet['selector'] = $selector;
535
536
                // process the properties
537
                $ruleSet['properties'] = $this->processCSSProperties(
538
                    $cssProperties
539
                );
540
541
                // calculate specifity
542
                $ruleSet['specifity'] = $this->calculateCSSSpecifity(
543
                    $selector
544
                ) + $i;
545
546
                // add into global rules
547
                $this->cssRules[] = $ruleSet;
548
            }
549
550
            // increment
551
            $i++;
552
        }
553
554
        // sort based on specifity
555
        if (!empty($this->cssRules)) {
556
            usort($this->cssRules, array(__CLASS__, 'sortOnSpecifity'));
557
        }
558
    }
559
560
    /**
561
     * Process the CSS-properties
562
     *
563
     * @return array
564
     * @param  string $propertyString The CSS-properties.
565
     */
566
    private function processCSSProperties($propertyString)
567
    {
568
        // split into chunks
569
        $properties = (array) explode(';', $propertyString);
570
571
        // init var
572
        $pairs = array();
573
574
        // loop properties
575
        foreach ($properties as $property) {
576
            // split into chunks
577
            $chunks = (array) explode(':', $property, 2);
578
579
            // validate
580
            if(!isset($chunks[1])) continue;
581
582
            // cleanup
583
            $chunks[0] = trim($chunks[0]);
584
            $chunks[1] = trim($chunks[1]);
585
586
            // add to pairs array
587
            if(!isset($pairs[$chunks[0]]) ||
588
               !in_array($chunks[1], $pairs[$chunks[0]])) {
589
                $pairs[$chunks[0]][] = $chunks[1];
590
            }
591
        }
592
593
        // sort the pairs
594
        ksort($pairs);
595
596
        // return
597
        return $pairs;
598
    }
599
600
    /**
601
     * Should the IDs and classes be removed?
602
     *
603
     * @return void
604
     * @param  bool[optional] $on Should we enable cleanup?
605
     */
606
    public function setCleanup($on = true)
607
    {
608
        $this->cleanup = (bool) $on;
609
    }
610
611
    /**
612
     * Set CSS to use
613
     *
614
     * @return void
615
     * @param  string $css The CSS to use.
616
     */
617
    public function setCSS($css)
618
    {
619
        $this->css = (string) $css;
620
    }
621
622
    /**
623
     * Set the encoding to use with the DOMDocument
624
     *
625
     * @return void
626
     * @param  string $encoding The encoding to use.
627
     */
628
    public function setEncoding($encoding)
629
    {
630
        $this->encoding = (string) $encoding;
631
    }
632
633
    /**
634
     * Set HTML to process
635
     *
636
     * @return void
637
     * @param  string $html The HTML to process.
638
     */
639
    public function setHTML($html)
640
    {
641
        $this->html = (string) $html;
642
    }
643
644
    /**
645
     * Set use of inline styles block
646
     * If this is enabled the class will use the style-block in the HTML.
647
     *
648
     * @return void
649
     * @param  bool[optional] $on Should we process inline styles?
650
     */
651
    public function setUseInlineStylesBlock($on = true)
652
    {
653
        $this->useInlineStylesBlock = (bool) $on;
654
    }
655
656
    /**
657
     * Set strip original style tags
658
     * If this is enabled the class will remove all style tags in the HTML.
659
     *
660
     * @return void
661
     * @param  bool[optional] $onShould we process inline styles?
0 ignored issues
show
Bug introduced by
There is no parameter named $onShould. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
662
     */
663
    public function setStripOriginalStyleTags($on = true)
664
    {
665
        $this->stripOriginalStyleTags = (bool) $on;
666
    }
667
668
    /**
669
     * Strip style tags into the generated HTML
670
     *
671
     * @return string
672
     * @param  string $html The HTML to strip style tags.
673
     */
674
    private function stripOriginalStyleTags($html)
675
    {
676
        return preg_replace('|<style(.*)>(.*)</style>|isU', '', $html);
677
    }
678
679
    /**
680
     * Sort an array on the specifity element
681
     *
682
     * @return int
683
     * @param  array $e1 The first element.
684
     * @param  array $e2 The second element.
685
     */
686
    private static function sortOnSpecifity($e1, $e2)
687
    {
688
        // validate
689
        if(!isset($e1['specifity']) || !isset($e2['specifity'])) return 0;
690
691
        // lower
692
        if($e1['specifity'] < $e2['specifity']) return -1;
693
694
        // higher
695
        if($e1['specifity'] > $e2['specifity']) return 1;
696
697
        // fallback
698
        return 0;
699
    }
700
}
701