Completed
Push — master ( bb379f...a58099 )
by Tijs
02:45
created

CssToInlineStyles::getHtmlFromDocument()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 10
nc 2
nop 1
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
     * Inline 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
        $propertyStorage = new \SplObjectStorage();
158
159
        $xPath = new \DOMXPath($document);
160
        foreach ($rules as $rule) {
161
            try {
162
                if (null !== $this->cssConverter) {
163
                    $expression = $this->cssConverter->toXPath($rule->getSelector());
164
                } else {
165
                    // Compatibility layer for Symfony 2.7 and older
166
                    $expression = CssSelector::toXPath($rule->getSelector());
167
                }
168
            } catch (ExceptionInterface $e) {
169
                continue;
170
            }
171
172
            $elements = $xPath->query($expression);
173
174
            if ($elements === false) {
175
                continue;
176
            }
177
178
            foreach ($elements as $element) {
179
                $propertyStorage[$element] = $this->calculatePropertiesToBeApplied(
180
                    $rule->getProperties(),
181
                    $propertyStorage->contains($element) ? $propertyStorage[$element] : array()
182
                );
183
            }
184
        }
185
186
        foreach ($propertyStorage as $element) {
187
            $this->inlineCssOnElement($element, $propertyStorage[$element]);
188
        }
189
190
        return $document;
191
    }
192
193
    /**
194
     * Merge the CSS rules to determine the applied properties.
195
     *
196
     * @param Css\Property\Property[] $properties
197
     * @param Css\Property\Property[] $cssProperties existing applied properties indexed by name
198
     *
199
     * @return Css\Property\Property[] updated properties, indexed by name
200
     */
201
    private function calculatePropertiesToBeApplied(array $properties, array $cssProperties)
202
    {
203
        if (empty($properties)) {
204
            return $cssProperties;
205
        }
206
207
        foreach ($properties as $property) {
208
            if (isset($cssProperties[$property->getName()])) {
209
                $existingProperty = $cssProperties[$property->getName()];
210
211
                if (
212
                    ($existingProperty->isImportant() && $property->isImportant()) &&
213
                    ($existingProperty->getOriginalSpecificity()->compareTo($property->getOriginalSpecificity()) <= 0)
214
                ) {
215
                    // if both the properties are important we should use the specificity
216
                    $cssProperties[$property->getName()] = $property;
217
                } elseif (!$existingProperty->isImportant() && $property->isImportant()) {
218
                    // if the existing property is not important but the new one is, it should be overruled
219
                    $cssProperties[$property->getName()] = $property;
220
                } elseif (
221
                    !$existingProperty->isImportant() &&
222
                    ($existingProperty->getOriginalSpecificity()->compareTo($property->getOriginalSpecificity()) <= 0)
223
                ) {
224
                    // if the existing property is not important we should check the specificity
225
                    $cssProperties[$property->getName()] = $property;
226
                }
227
            } else {
228
                $cssProperties[$property->getName()] = $property;
229
            }
230
        }
231
232
        return $cssProperties;
233
    }
234
}
235