Completed
Pull Request — master (#192)
by
unknown
01:50
created

CssToInlineStyles   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 260
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 5
dl 0
loc 260
rs 9.52
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 2
A convert() 0 23 2
B inlineCssOnElement() 0 27 6
A getInlineStyles() 0 10 1
A createDomDocumentFromHtml() 0 10 1
A getHtmlFromDocument() 0 20 2
B inline() 0 44 9
A appendMediaQueries() 0 17 4
B calculatePropertiesToBeApplied() 0 32 9
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\Processor as RuleProcessor;
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. Media queries if available will be filtered out
28
     * and appended to the body in their own <style> tag.
29
     *
30
     * @param string $html
31
     * @param string $css
32
     *
33
     * @return string
34
     */
35
    public function convert($html, $css = null)
36
    {
37
        $document  = $this->createDomDocumentFromHtml($html);
38
        $processor = new Processor();
39
40
        // get all styles from the style-tags
41
        $styleTagCss = $processor->getCssFromStyleTags($html);
42
43
        // proccess style tag css
44
        $mediaQueries = $processor->getMediaQueries($styleTagCss);
45
        $rules        = $processor->getRules($styleTagCss);
46
47
        // process extra css included as string argument to this function
48
        if ( $css !== null ) {
49
            $mediaQueries = $processor->getMediaQueries($css, $mediaQueries);
50
            $rules        = $processor->getRules($css, $rules);
51
        }
52
53
        $document = $this->inline($document, $rules);
54
        $document = $this->appendMediaQueries($document, $mediaQueries);
55
56
        return $this->getHtmlFromDocument($document);
57
    }
58
59
    /**
60
     * Inline the given properties on an given DOMElement
61
     *
62
     * @param \DOMElement             $element
63
     * @param Css\Property\Property[] $properties
64
     *
65
     * @return \DOMElement
66
     */
67
    public function inlineCssOnElement(\DOMElement $element, array $properties)
68
    {
69
        if (empty($properties)) {
70
            return $element;
71
        }
72
73
        $cssProperties = array();
74
        $inlineProperties = array();
75
76
        foreach ($this->getInlineStyles($element) as $property) {
77
            $inlineProperties[$property->getName()] = $property;
78
        }
79
80
        foreach ($properties as $property) {
81
            if (!isset($inlineProperties[$property->getName()])) {
82
                $cssProperties[$property->getName()] = $property;
83
            }
84
        }
85
86
        $rules = array();
87
        foreach (array_merge($cssProperties, $inlineProperties) as $property) {
88
            $rules[] = $property->toString();
89
        }
90
        $element->setAttribute('style', implode(' ', $rules));
91
92
        return $element;
93
    }
94
95
    /**
96
     * Get the current inline styles for a given DOMElement
97
     *
98
     * @param \DOMElement $element
99
     *
100
     * @return Css\Property\Property[]
101
     */
102
    public function getInlineStyles(\DOMElement $element)
103
    {
104
        $processor = new PropertyProcessor();
105
106
        return $processor->convertArrayToObjects(
107
            $processor->splitIntoSeparateProperties(
108
                $element->getAttribute('style')
109
            )
110
        );
111
    }
112
113
    /**
114
     * @param string $html
115
     *
116
     * @return \DOMDocument
117
     */
118
    protected function createDomDocumentFromHtml($html)
119
    {
120
        $document = new \DOMDocument('1.0', 'UTF-8');
121
        $internalErrors = libxml_use_internal_errors(true);
122
        $document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
123
        libxml_use_internal_errors($internalErrors);
124
        $document->formatOutput = true;
125
126
        return $document;
127
    }
128
129
    /**
130
     * @param \DOMDocument $document
131
     *
132
     * @return string
133
     */
134
    protected function getHtmlFromDocument(\DOMDocument $document)
135
    {
136
        // retrieve the document element
137
        // we do it this way to preserve the utf-8 encoding
138
        $htmlElement = $document->documentElement;
139
        $html = $document->saveHTML($htmlElement);
140
        $html = trim($html);
141
142
        // retrieve the doctype
143
        $document->removeChild($htmlElement);
144
        $doctype = $document->saveHTML();
145
        $doctype = trim($doctype);
146
147
        // if it is the html5 doctype convert it to lowercase
148
        if ($doctype === '<!DOCTYPE html>') {
149
            $doctype = strtolower($doctype);
150
        }
151
152
        return $doctype."\n".$html;
153
    }
154
155
    /**
156
     * @param \DOMDocument    $document
157
     * @param Css\Rule\Rule[] $rules
158
     *
159
     * @return \DOMDocument
160
     */
161
    protected function inline(\DOMDocument $document, array $rules)
162
    {
163
        if (empty($rules)) {
164
            return $document;
165
        }
166
167
        $propertyStorage = new \SplObjectStorage();
168
169
        $xPath = new \DOMXPath($document);
170
171
        usort($rules, array(RuleProcessor::class, 'sortOnSpecificity'));
172
173
        foreach ($rules as $rule) {
174
            try {
175
                if (null !== $this->cssConverter) {
176
                    $expression = $this->cssConverter->toXPath($rule->getSelector());
177
                } else {
178
                    // Compatibility layer for Symfony 2.7 and older
179
                    $expression = CssSelector::toXPath($rule->getSelector());
180
                }
181
            } catch (ExceptionInterface $e) {
182
                continue;
183
            }
184
185
            $elements = $xPath->query($expression);
186
187
            if ($elements === false) {
188
                continue;
189
            }
190
191
            foreach ($elements as $element) {
192
                $propertyStorage[$element] = $this->calculatePropertiesToBeApplied(
193
                    $rule->getProperties(),
194
                    $propertyStorage->contains($element) ? $propertyStorage[$element] : array()
195
                );
196
            }
197
        }
198
199
        foreach ($propertyStorage as $element) {
200
            $this->inlineCssOnElement($element, $propertyStorage[$element]);
201
        }
202
203
        return $document;
204
    }
205
206
    /**
207
     * Appends a style tag to the body element containing all media queries.
208
     *
209
     * @param $document
210
     * @param $mediaQueries
211
     * @return \DOMDocument
212
     */
213
    protected function appendMediaQueries(\DOMDocument $document, array $mediaQueries) {
214
        if ( ! sizeof($mediaQueries) ) return $document;
215
        $style = $document->createElement('style', implode($mediaQueries));
216
        if ( $style ) {
217
            $head = $document->getElementsByTagName('head')->item(0);
218
            // Partials aren't supported (missing head tag). But lets play nice.
219
            if ( $head !== null ) {
220
                $head->appendChild($style);
221
            } else {
222
                $docNode = $document->documentElement;
223
                $head = $document->createElement('head');
224
                $head->appendChild($style);
225
                $docNode->appendChild($head);
226
            }
227
        }
228
        return $document;
229
    }
230
231
    /**
232
     * Merge the CSS rules to determine the applied properties.
233
     *
234
     * @param Css\Property\Property[] $properties
235
     * @param Css\Property\Property[] $cssProperties existing applied properties indexed by name
236
     *
237
     * @return Css\Property\Property[] updated properties, indexed by name
238
     */
239
    private function calculatePropertiesToBeApplied(array $properties, array $cssProperties)
240
    {
241
        if (empty($properties)) {
242
            return $cssProperties;
243
        }
244
245
        foreach ($properties as $property) {
246
            if (isset($cssProperties[$property->getName()])) {
247
                $existingProperty = $cssProperties[$property->getName()];
248
249
                //skip check to overrule if existing property is important and current is not
250
                if ($existingProperty->isImportant() && !$property->isImportant()) {
251
                    continue;
252
                }
253
254
                //overrule if current property is important and existing is not, else check specificity
255
                $overrule = !$existingProperty->isImportant() && $property->isImportant();
256
                if (!$overrule) {
257
                    $overrule = $existingProperty->getOriginalSpecificity()->compareTo($property->getOriginalSpecificity()) <= 0;
0 ignored issues
show
Documentation introduced by
$property->getOriginalSpecificity() is of type object<Symfony\Component...ector\Node\Specificity>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
258
                }
259
260
                if ($overrule) {
261
                    unset($cssProperties[$property->getName()]);
262
                    $cssProperties[$property->getName()] = $property;
263
                }
264
            } else {
265
                $cssProperties[$property->getName()] = $property;
266
            }
267
        }
268
269
        return $cssProperties;
270
    }
271
}
272