RelevantSelectorParser   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 32
lcom 1
cbo 5
dl 0
loc 288
rs 9.6
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
A extract() 0 13 2
B populateSelectors() 0 26 4
A storeDeclaration() 0 4 1
A prepareSelectorArray() 0 12 3
A setStylesheetReader() 0 17 3
A getStylesheetReader() 0 4 1
A setCharset() 0 6 1
A getCharset() 0 4 1
A isPseudoClassAllowed() 0 12 3
A getRelevantStyleRules() 0 19 4
A getRelevantMediaRule() 0 17 4
A isAllowedMediaRule() 0 7 3
1
<?php namespace Luminaire\Premailer\Parser;
2
3
/**
4
 * Created by Sublime Text 3
5
 *
6
 * @user     Kevin Tanjung
7
 * @website  http://kevintanjung.github.io
8
 * @email    [email protected]
9
 * @date     04/08/2016
10
 * @time     09:09
11
 */
12
13
use Crossjoin\Css\Reader\CssString as StylesheetReader;
14
use Crossjoin\Css\Format\Rule\AtMedia\MediaQuery;
15
use Crossjoin\Css\Format\Rule\AtMedia\MediaRule;
16
use Crossjoin\Css\Format\Rule\Style\StyleRuleSet;
17
use Crossjoin\Css\Format\Rule\Style\StyleSelector;
18
use Illuminate\Support\Arr;
19
use InvalidArgumentException;
20
21
/**
22
 * Retrieve relevant CSS selector from a given CSS rules
23
 *
24
 * @package  \Luminaire\Poseidon\Parser
25
 */
26
class RelevantSelectorParser
27
{
28
29
    /**
30
     * The pseudo classes that can be set in a style attribute and that are
31
     * supported by the Symfony CssSelector (doesn't support CSS4 yet).
32
     *
33
     * @var array
34
     */
35
    protected $allowed_pseudo_classes = [
36
        StyleSelector::PSEUDO_CLASS_FIRST_CHILD,
37
        StyleSelector::PSEUDO_CLASS_ROOT,
38
        StyleSelector::PSEUDO_CLASS_NTH_CHILD,
39
        StyleSelector::PSEUDO_CLASS_NTH_LAST_CHILD,
40
        StyleSelector::PSEUDO_CLASS_NTH_OF_TYPE,
41
        StyleSelector::PSEUDO_CLASS_NTH_LAST_OF_TYPE,
42
        StyleSelector::PSEUDO_CLASS_LAST_CHILD,
43
        StyleSelector::PSEUDO_CLASS_FIRST_OF_TYPE,
44
        StyleSelector::PSEUDO_CLASS_LAST_OF_TYPE,
45
        StyleSelector::PSEUDO_CLASS_ONLY_CHILD,
46
        StyleSelector::PSEUDO_CLASS_ONLY_OF_TYPE,
47
        StyleSelector::PSEUDO_CLASS_EMPTY,
48
        StyleSelector::PSEUDO_CLASS_NOT,
49
    ];
50
51
    /**
52
     * The stylesheet reader instance
53
     *
54
     * @var \Crossjoin\Css\Reader\CssString
55
     */
56
    protected $reader;
57
58
    /**
59
     * The charset of the stylesheet
60
     *
61
     * @var string
62
     */
63
    protected $charset;
64
65
    /**
66
     * Create a new instance of "Relevant Selector Parser"
67
     *
68
     * @param  \Crossjoin\Css\Reader\CssString|string|null  $stylesheet
69
     * @param  string                                       $charset
70
     */
71
    public function __construct($stylesheet = null, $charset = 'UTF-8')
72
    {
73
        $this->charset = $charset;
74
75
        if ( ! is_null($stylesheet))
76
        {
77
            $this->setStylesheetReader($stylesheet);
78
        }
79
    }
80
81
    /**
82
     * Get the relevant selectors
83
     *
84
     * @return array
85
     */
86
    public function extract()
87
    {
88
        $selectors = [];
89
        $rules     = $this->reader->getStyleSheet()->getRules();
90
        $relevant  = $this->getRelevantStyleRules($rules);
91
92
        foreach ($relevant as $rule)
93
        {
94
            $this->populateSelectors($selectors, $rule);
0 ignored issues
show
Documentation introduced by
$rule is of type object<Crossjoin\Css\For...ule\Style\StyleRuleSet>, but the function expects a array.

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...
95
        }
96
97
        return $selectors;
98
    }
99
100
    /**
101
     * Store the selectors from the rule to the tank
102
     *
103
     * @param  array   &$tank
104
     * @param  array   $rule
105
     * @return void
106
     */
107
    protected function populateSelectors(array &$tank, $rule)
108
    {
109
        foreach ($rule->getSelectors() as $selector)
0 ignored issues
show
Bug introduced by
The method getSelectors cannot be called on $rule (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
110
        {
111
            if ( ! $this->isPseudoClassAllowed($selector))
112
            {
113
                continue;
114
            }
115
116
            $this->prepareSelectorArray(
117
                $tank,
118
                $selector->getSpecificity(),
119
                $selector->getValue()
120
            );
121
122
            foreach ($rule->getDeclarations() as $declaration)
0 ignored issues
show
Bug introduced by
The method getDeclarations cannot be called on $rule (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
123
            {
124
                $this->storeDeclaration(
125
                    $tank,
126
                    $declaration,
127
                    $selector->getSpecificity(),
128
                    $selector->getValue()
129
                );
130
            }
131
        }
132
    }
133
134
    /**
135
     * Store the declaration in the selector tank
136
     *
137
     * @param  array                                              &$tank
138
     * @param  \Crossjoin\Css\Format\Rule\Style\StyleDeclaration  $declaration
139
     * @param  string                                             $specifity
140
     * @param  string                                             $name
141
     * @return void
142
     */
143
    protected function storeDeclaration(array &$tank, $declaration, $specifity, $name)
144
    {
145
        $tank[$specifity][$name][] = $declaration;
146
    }
147
148
    /**
149
     * Before we build the dictionary of style declaration, we will need to make
150
     * sure, there is an array to be inserted.
151
     *
152
     * @param  array   &$selectors
153
     * @param  string  $specifity
154
     * @param  string  $name
155
     * @return void
156
     */
157
    protected function prepareSelectorArray(array &$selectors, $specifity, $name)
158
    {
159
        if ( ! isset($selectors[$specifity]))
160
        {
161
            $selectors[$specifity] = [];
162
        }
163
164
        if ( ! isset($selectors[$specifity][$name]))
165
        {
166
            $selectors[$specifity][$name] = [];
167
        }
168
    }
169
170
    /**
171
     * Set the stylesheet reader instance
172
     *
173
     * @param  \Crossjoin\Css\Reader\CssString|string  $stylesheet
174
     * @return $this
175
     *
176
     * @throws \InvalidArgumentException
177
     */
178
    public function setStylesheetReader($stylesheet)
179
    {
180
        if (is_string($stylesheet))
181
        {
182
            $stylesheet = new StylesheetReader($stylesheet);
183
        }
184
185
        if ( ! $stylesheet instanceof StylesheetReader)
186
        {
187
            throw new InvalidArgumentException('The argument 0 of the [setStylesheetReader] method expects to be a string of CSS or a [Crossjoin\Css\Reader\CssString]');
188
        }
189
190
        $this->reader = $stylesheet;
191
        $this->reader->setEnvironmentEncoding($this->getCharset());
192
193
        return $this;
194
    }
195
196
    /**
197
     * Get the stylesheet reader instance
198
     *
199
     * @return \Crossjoin\Css\Reader\CssString|null
200
     */
201
    public function getStylesheetReader()
202
    {
203
        return $this->reader;
204
    }
205
206
    /**
207
     * Set the charset of the stylesheet
208
     *
209
     * @param  string  $charset
210
     * @return $this
211
     */
212
    public function setCharset($charset)
213
    {
214
        $this->charset = $charset;
215
216
        return $this;
217
    }
218
219
    /**
220
     * Get the charset of the stylesheet
221
     *
222
     * @return string
223
     */
224
    public function getCharset()
225
    {
226
        return $this->charset;
227
    }
228
229
    /**
230
     * Check if a Selector has a valid Pseudo Class
231
     *
232
     * @param  \Crossjoin\Css\Format\Rule\Style\StyleSelector  $selector
233
     * @return bool
234
     */
235
    public function isPseudoClassAllowed(StyleSelector $selector)
236
    {
237
        foreach ($selector->getPseudoClasses() as $pseudo_class)
238
        {
239
            if ( ! in_array($pseudo_class, $this->allowed_pseudo_classes))
240
            {
241
                return false;
242
            }
243
        }
244
245
        return true;
246
    }
247
248
    /**
249
     * Gets all generally relevant style rules
250
     *
251
     * @param  \Crossjoin\Css\Format\Rule\RuleAbstract[]  $rules
252
     * @return \Crossjoin\Css\Format\Rule\Style\StyleRuleSet[]
253
     */
254
    protected function getRelevantStyleRules(array $rules)
255
    {
256
        $relevants = [];
257
258
        foreach ($rules as $rule)
259
        {
260
            if ($rule instanceof StyleRuleSet)
261
            {
262
                $relevants[] = $rule;
263
            }
264
265
            if ($rule instanceof MediaRule)
266
            {
267
                $this->getRelevantMediaRule($rule, $relevants);
268
            }
269
        }
270
271
        return $relevants;
272
    }
273
274
    /**
275
     * Gets the relevant style rules from a media rule
276
     *
277
     * @param  \Crossjoin\Css\Format\Rule\AtMedia\MediaRule  $rule
278
     * @param  array                                         &$collection
279
     * @return void
280
     */
281
    protected function getRelevantMediaRule(MediaRule $rule, &$collection)
282
    {
283
        foreach ($rule->getQueries() as $media_query)
284
        {
285
            if ( ! $this->isAllowedMediaRule($media_query))
286
            {
287
                continue;
288
            }
289
290
            foreach ($this->getRelevantStyleRules($rule->getRules()) as $style_rule)
291
            {
292
                $collection[] = $style_rule;
293
            }
294
295
            break;
296
        }
297
    }
298
299
    /**
300
     * Check if the media rule should be included
301
     *
302
     * @param  \Crossjoin\Css\Format\Rule\AtMedia\MediaQuery  $media_query
303
     * @return bool
304
     */
305
    protected function isAllowedMediaRule(MediaQuery $media_query)
306
    {
307
        $type      = $media_query->getType();
308
        $condition = count($media_query->getConditions());
309
310
        return ($type === MediaQuery::TYPE_ALL || $type === MediaQuery::TYPE_SCREEN) && $condition === 0;
311
    }
312
313
}
314