Completed
Push — master ( 9753fc...bb535b )
by Tijs
03:33
created

CssToInlineStyles::processCSS()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 84
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 9
Bugs 1 Features 1
Metric Value
c 9
b 1
f 1
dl 0
loc 84
rs 8.3501
cc 6
eloc 31
nc 16
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace TijsVerkoyen\CssToInlineStyles;
4
5
use Symfony\Component\CssSelector\CssSelector;
6
use Symfony\Component\CssSelector\CssSelectorConverter;
7
use Symfony\Component\CssSelector\Exception\ExceptionInterface;
8
use TijsVerkoyen\CssToInlineStyles\Css\Processor;
9
use TijsVerkoyen\CssToInlineStyles\Css\Property\Processor as PropertyProcessor;
10
use TijsVerkoyen\CssToInlineStyles\Css\Rule\Rule;
11
12
class CssToInlineStyles
13
{
14
    /**
15
     * Will inline the $css into the given $html
16
     *
17
     * Remark: if the html contains <style>-tags those will be used, the rules
18
     * in $css will be appended.
19
     *
20
     * @param string $html
21
     * @param string $css
22
     * @return string
23
     */
24
    public function convert($html, $css = null)
25
    {
26
        $document = $this->createDomDocumentFromHtml($html);
27
        $processor = new Processor();
28
29
        // get all styles from the style-tags
30
        $rules = $processor->getRules(
31
            $processor->getCssFromStyleTags($html)
32
        );
33
34
        if ($css !== null) {
35
            $rules = $processor->getRules($css, $rules);
36
        }
37
38
        $document = $this->inline($document, $rules);
39
40
        return $this->getHtmlFromDocument($document);
41
    }
42
43
    /**
44
     * Inle the given properties on an given DOMElement
45
     *
46
     * @param \DOMElement             $element
47
     * @param Css\Property\Property[] $properties
48
     * @return \DOMElement
49
     */
50
    public function inlineCssOnElement(\DOMElement $element, array $properties)
51
    {
52
        if (empty($properties)) {
53
            return $element;
54
        }
55
56
        $cssProperties = array();
57
        $inlineProperties = $this->getInlineStyles($element);
58
59
        if (!empty($inlineProperties)) {
60
            foreach ($inlineProperties as $property) {
61
                $cssProperties[$property->getName()] = $property;
62
            }
63
        }
64
65
        foreach ($properties as $property) {
66
            if (!isset($cssProperties[$property->getName()])) {
67
                $cssProperties[$property->getName()] = $property;
68
            }
69
        }
70
71
        $rules = array();
72
        foreach ($cssProperties as $property) {
73
            $rules[] = $property->toString();
74
        }
75
        $element->setAttribute('style', implode(' ', $rules));
76
77
        return $element;
78
    }
79
80
    /**
81
     * Get the current inline styles for a given DOMElement
82
     *
83
     * @param \DOMElement $element
84
     * @return Css\Property\Property[]
85
     */
86
    public function getInlineStyles(\DOMElement $element)
87
    {
88
        $processor = new PropertyProcessor();
89
90
        return $processor->convertArrayToObjects(
91
            $processor->splitIntoSeparateProperties(
92
                $element->getAttribute('style')
93
            )
94
        );
95
    }
96
97
    /**
98
     * @param string $html
99
     * @return \DOMDocument
100
     */
101
    protected function createDomDocumentFromHtml($html)
102
    {
103
        $document = new \DOMDocument('1.0', 'UTF-8');
104
        $internalErrors = libxml_use_internal_errors(true);
105
        $document->loadHTML($html);
106
        libxml_use_internal_errors($internalErrors);
107
        $document->formatOutput = true;
108
109
        return $document;
110
    }
111
112
    /**
113
     * @param \DOMDocument $document
114
     * @return string
115
     */
116
    protected function getHtmlFromDocument(\DOMDocument $document)
117
    {
118
        $xml = $document->saveXML(null, LIBXML_NOEMPTYTAG);
119
120
        $html = preg_replace(
121
            '|<\?xml (.*)\?>|',
122
            '',
123
            $xml
124
        );
125
126
        return ltrim($html);
127
    }
128
129
    /**
130
     * @param \DOMDocument    $document
131
     * @param Css\Rule\Rule[] $rules
132
     * @return \DOMDocument
133
     */
134
    protected function inline(\DOMDocument $document, array $rules)
135
    {
136
        if (empty($rules)) {
137
            return $document;
138
        }
139
140
        $xPath = new \DOMXPath($document);
141
        foreach ($rules as $rule) {
142
            try {
143
                if (class_exists('Symfony\Component\CssSelector\CssSelectorConverter')) {
144
                    $converter = new CssSelectorConverter();
145
                    $expression = $converter->toXPath($rule->getSelector());
146
                } else {
147
                    $expression = CssSelector::toXPath($rule->getSelector());
148
                }
149
            } catch (ExceptionInterface $e) {
150
                continue;
151
            }
152
153
            $elements = $xPath->query($expression);
154
155
            if ($elements === false) {
156
                continue;
157
            }
158
159
            foreach ($elements as $element) {
160
                $this->calculatePropertiesToBeApplied($element, $rule->getProperties());
161
            }
162
        }
163
164
        $elements = $xPath->query('//*[@data-css-to-inline-styles]');
165
166
        foreach ($elements as $element) {
167
            $propertiesToBeApplied = $element->attributes->getNamedItem('data-css-to-inline-styles');
168
            $element->removeAttribute('data-css-to-inline-styles');
169
170
            if ($propertiesToBeApplied !== null) {
171
                $properties = unserialize(base64_decode($propertiesToBeApplied->value));
172
                $this->inlineCssOnElement($element, $properties);
173
            }
174
        }
175
176
        return $document;
177
    }
178
179
    /**
180
     * Store the calculated values in a temporary data-attribute
181
     *
182
     * @param \DOMElement             $element
183
     * @param Css\Property\Property[] $properties
184
     * @return \DOMElement
185
     */
186
    private function calculatePropertiesToBeApplied(
187
        \DOMElement $element,
188
        array $properties
189
    ) {
190
        if (empty($properties)) {
191
            return $element;
192
        }
193
194
        $cssProperties = array();
195
        $currentStyles = $element->attributes->getNamedItem('data-css-to-inline-styles');
196
197
        if ($currentStyles !== null) {
198
            $currentProperties = unserialize(
199
                base64_decode(
200
                    $currentStyles->value
201
                )
202
            );
203
204
            foreach ($currentProperties as $property) {
205
                $cssProperties[$property->getName()] = $property;
206
            }
207
        }
208
209
        foreach ($properties as $property) {
210
            if (isset($cssProperties[$property->getName()])) {
211
                $existingProperty = $cssProperties[$property->getName()];
212
213
                if (
214
                    ($existingProperty->isImportant() && $property->isImportant()) &&
215
                    ($existingProperty->getOriginalSpecificity()->compareTo($property->getOriginalSpecificity()) <= 0)
216
                ) {
217
                    // if both the properties are important we should use the specificity
218
                    $cssProperties[$property->getName()] = $property;
219
                } elseif (!$existingProperty->isImportant() && $property->isImportant()) {
220
                    // if the existing property is not important but the new one is, it should be overruled
221
                    $cssProperties[$property->getName()] = $property;
222
                } elseif (
223
                    !$existingProperty->isImportant() &&
224
                    ($existingProperty->getOriginalSpecificity()->compareTo($property->getOriginalSpecificity()) <= 0)
225
                ) {
226
                    // if the existing property is not important we should check the specificity
227
                    $cssProperties[$property->getName()] = $property;
228
                }
229
            } else {
230
                $cssProperties[$property->getName()] = $property;
231
            }
232
        }
233
234
        $element->setAttribute(
235
            'data-css-to-inline-styles',
236
            base64_encode(
237
                serialize(
238
                    array_values($cssProperties)
239
                )
240
            )
241
        );
242
243
        return $element;
244
    }
245
}
246