Completed
Pull Request — master (#159)
by
unknown
06:50
created

CssToInlineStyles::inlineCssOnElement()   B

Complexity

Conditions 6
Paths 13

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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