Completed
Push — master ( 547304...966d0e )
by Kevin
11:14
created

RelevantSelectorParser::setCharset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
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);
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   $selectors
0 ignored issues
show
Bug introduced by
There is no parameter named $selectors. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
105
     * @return void
106
     */
107
    protected function populateSelectors(array &$tank, $rule)
108
    {
109
        foreach ($rule->getSelectors() as $selector)
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)
123
            {
124
                $this->storeDeclaration(
125
                    $tank,
126
                    $declaration,
127
                    $selector->getSpecificity(),
0 ignored issues
show
Unused Code introduced by
The call to RelevantSelectorParser::storeDeclaration() has too many arguments starting with $selector->getSpecificity().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

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