Completed
Push — master ( b729a9...e4ba26 )
by Tijs
05:00
created

CssToInlineStyles::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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