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

CssToInlineStyles::appendMediaQueries()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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