ToCSSVisitor::visitMedia()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 2
1
<?php
2
3
/*
4
 * This file is part of the ILess
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace ILess\Visitor;
11
12
use ILess\Context;
13
use ILess\Exception\CompilerException;
14
use ILess\Node;
15
use ILess\Node\AnonymousNode;
16
use ILess\Node\CombinatorNode;
17
use ILess\Node\CommentNode;
18
use ILess\Node\DirectiveNode;
19
use ILess\Node\ExpressionNode;
20
use ILess\Node\ExtendNode;
21
use ILess\Node\ImportNode;
22
use ILess\Node\MediaNode;
23
use ILess\Node\MixinDefinitionNode;
24
use ILess\Node\ReferencedInterface;
25
use ILess\Node\RuleNode;
26
use ILess\Node\RulesetNode;
27
use ILess\Node\SelectorNode;
28
use ILess\Node\ValueNode;
29
30
/**
31
 * To CSS visitor.
32
 */
33
class ToCSSVisitor extends Visitor
34
{
35
    /**
36
     * The context.
37
     *
38
     * @var Context
39
     */
40
    protected $context;
41
42
    /**
43
     * Is replacing flag.
44
     *
45
     * @var bool
46
     */
47
    protected $isReplacing = true;
48
49
    /**
50
     * Charset flag.
51
     *
52
     * @var bool
53
     */
54
    public $charset = false;
55
56
    /**
57
     * Constructor.
58
     *
59
     * @param Context $context
60
     */
61
    public function __construct(Context $context)
62
    {
63
        parent::__construct();
64
        $this->context = $context;
65
    }
66
67
    /**
68
     * Returns the context.
69
     *
70
     * @return Context
71
     */
72
    public function getContext()
73
    {
74
        return $this->context;
75
    }
76
77
    /**
78
     * Visits a rule node.
79
     *
80
     * @param RuleNode $node The node
81
     * @param VisitorArguments $arguments The arguments
82
     *
83
     * @return array|RuleNode
84
     */
85
    public function visitRule(RuleNode $node, VisitorArguments $arguments)
86
    {
87
        if ($node->variable) {
88
            return;
89
        }
90
91
        return $node;
92
    }
93
94
    /**
95
     * Visits a mixin definition.
96
     *
97
     * @param MixinDefinitionNode $node The node
98
     * @param VisitorArguments $arguments The arguments
99
     *
100
     * @return array
101
     */
102
    public function visitMixinDefinition(MixinDefinitionNode $node, VisitorArguments $arguments)
103
    {
104
        // mixin definitions do not get compiled - this means they keep state
105
        // so we have to clear that state here so it isn't used if toCSS is called twice
106
        $node->frames = [];
107
    }
108
109
    /**
110
     * Visits a extend node.
111
     *
112
     * @param ExtendNode $node The node
113
     * @param VisitorArguments $arguments The arguments
114
     */
115
    public function visitExtend(ExtendNode $node, VisitorArguments $arguments)
116
    {
117
    }
118
119
    /**
120
     * Visits a comment node.
121
     *
122
     * @param CommentNode $node The node
123
     * @param VisitorArguments $arguments The arguments
124
     *
125
     * @return CommentNode|null
126
     */
127
    public function visitComment(CommentNode $node, VisitorArguments $arguments)
128
    {
129
        if ($node->isSilent($this->getContext())) {
130
            return;
131
        }
132
133
        return $node;
134
    }
135
136
    /**
137
     * Visits a media node.
138
     *
139
     * @param MediaNode $node The node
140
     * @param VisitorArguments $arguments The arguments
141
     *
142
     * @return MediaNode|null
143
     */
144
    public function visitMedia(MediaNode $node, VisitorArguments $arguments)
145
    {
146
        $node->accept($this);
147
        $arguments->visitDeeper = false;
148
149
        if (!count($node->rules)) {
150
            return;
151
        }
152
153
        return $node;
154
    }
155
156
    /**
157
     * Visits a import node.
158
     *
159
     * @param ImportNode $node The node
160
     * @param VisitorArguments $arguments The arguments
161
     *
162
     * @return ImportNode|null
163
     */
164
    public function visitImport(ImportNode $node, VisitorArguments $arguments)
165
    {
166
        if ($node->path->currentFileInfo->reference !== false && $node->css) {
167
            return;
168
        }
169
170
        return $node;
171
    }
172
173
    /**
174
     * Visits a directive node.
175
     *
176
     * @param DirectiveNode $node The node
177
     * @param VisitorArguments $arguments The arguments
178
     *
179
     * @return DirectiveNode|null
180
     */
181
    public function visitDirective(DirectiveNode $node, VisitorArguments $arguments)
182
    {
183
        if ($node->name === '@charset') {
184
            if (!$node->getIsReferenced()) {
185
                return;
186
            }
187
            // Only output the debug info together with subsequent @charset definitions
188
            // a comment (or @media statement) before the actual @charset directive would
189
            // be considered illegal css as it has to be on the first line
190
            if ($this->charset) {
191
                if ($node->debugInfo) {
192
                    $comment = new CommentNode(sprintf("/* %s */\n",
193
                        str_replace("\n", '', $node->toCSS($this->getContext()))));
194
                    $comment->debugInfo = $node->debugInfo;
195
196
                    return $this->visit($comment);
197
                }
198
199
                return;
200
            }
201
            $this->charset = true;
202
        }
203
204
        if (count($node->rules)) {
205
            // it is still true that it is only one ruleset in array
206
            // this is last such moment
207
            $this->mergeRules($node->rules[0]->rules);
208
209
            $node->accept($this);
210
            $arguments->visitDeeper = false;
211
            // the directive was directly referenced and therefore needs to be shown in the output
212
            if ($node->getIsReferenced()) {
213
                return $node;
214
            }
215
216
            if (!count($node->rules)) {
217
                return;
218
            }
219
220
            // the directive was not directly referenced - we need to check whether some of its children
221
            // was referenced
222
            if ($this->hasVisibleChild($node)) {
223
                // marking as referenced in case the directive is stored inside another directive
224
                $node->markReferenced();
225
226
                return $node;
227
            }
228
229
            // The directive was not directly referenced and does not contain anything that
230
            // was referenced. Therefore it must not be shown in output.
231
            return;
232
        } else {
233
            if (!$node->getIsReferenced()) {
234
                return;
235
            }
236
        }
237
238
        return $node;
239
    }
240
241
    /**
242
     * @param DirectiveNode $node
243
     *
244
     * @return bool
245
     */
246
    private function hasVisibleChild(DirectiveNode $node)
247
    {
248
        $bodyRules = &$node->rules;
249
250
        // if there is only one nested ruleset and that one has no path, then it is
251
        // just fake ruleset that got not replaced and we need to look inside it to
252
        // get real children
253
        if (count($bodyRules) === 1 && (count($bodyRules[0]->paths) === 0)) {
254
            $bodyRules = $bodyRules[0]->rules;
255
        }
256
257
        for ($r = 0; $r < count($bodyRules); ++$r) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
258
            $rule = $bodyRules[$r];
259
            if ($rule instanceof ReferencedInterface && $rule->getIsReferenced()) {
260
                // the directive contains something that was referenced (likely by extend)
261
                // therefore it needs to be shown in output too
262
                return true;
263
            }
264
        }
265
266
        return false;
267
    }
268
269
    /**
270
     * Visits a ruleset node.
271
     *
272
     * @param RulesetNode $node The node
273
     * @param VisitorArguments $arguments The arguments
274
     *
275
     * @return array|RulesetNode
276
     */
277
    public function visitRuleset(RulesetNode $node, VisitorArguments $arguments)
278
    {
279
        if ($node->firstRoot) {
280
            $this->checkPropertiesInRoot($node->rules);
281
        }
282
283
        $rulesets = [];
284
285
        if (!$node->root) {
286
            $paths = [];
287
            foreach ($node->paths as $p) {
288
                if ($p[0]->elements[0]->combinator->value === ' ') {
289
                    $p[0]->elements[0]->combinator = new CombinatorNode('');
290
                }
291
                foreach ($p as $pi) {
292
                    /* @var $pi SelectorNode */
293
                    if ($pi->getIsReferenced() && $pi->getIsOutput()) {
294
                        $paths[] = $p;
295
                        break;
296
                    }
297
                }
298
            }
299
300
            $node->paths = $paths;
301
302
            // Compile rules and rulesets
303
            for ($i = 0, $count = count($node->rules); $i < $count;) {
304
                $rule = $node->rules[$i];
305
                //if ($rule instanceof ILess\ILess\Node\RuleNode || $rule instanceof ILess\ILess\Node\RulesetNode) {
306
                //if ($rule instanceof ILess\ILess\Node\RulesetNode) {
307
                if (Node::propertyExists($rule, 'rules')) {
308
                    // visit because we are moving them out from being a child
309
                    $rulesets[] = $this->visit($rule);
310
                    array_splice($node->rules, $i, 1);
311
                    --$count;
312
                    continue;
313
                }
314
                ++$i;
315
            }
316
317
            // accept the visitor to remove rules and refactor itself
318
            // then we can decide now whether we want it or not
319
            if ($count > 0) {
320
                $node->accept($this);
321
            } else {
322
                $node->rules = [];
323
            }
324
325
            $arguments->visitDeeper = false;
326
327
            // accept the visitor to remove rules and refactor itself
328
            // then we can decide now whether we want it or not
329
            if ($node->rules) {
330
                // passed by reference
331
                $this->mergeRules($node->rules);
332
            }
333
334
            if ($node->rules) {
335
                // passed by reference
336
                $this->removeDuplicateRules($node->rules);
337
            }
338
339
            // now decide whether we keep the ruleset
340
            if ($node->rules && $node->paths) {
341
                array_splice($rulesets, 0, 0, [$node]);
342
            }
343
        } else {
344
            $node->accept($this);
345
            $arguments->visitDeeper = false;
346
            if ($node->firstRoot || count($node->rules) > 0) {
347
                array_splice($rulesets, 0, 0, [$node]);
348
            }
349
        }
350
351
        if (count($rulesets) === 1) {
352
            return $rulesets[0];
353
        }
354
355
        return $rulesets;
356
    }
357
358
    /**
359
     * Visits anonymous node.
360
     *
361
     * @param AnonymousNode $node
362
     * @param VisitorArguments $arguments
363
     */
364
    public function visitAnonymous(AnonymousNode $node, VisitorArguments $arguments)
365
    {
366
        if (!$node->getIsReferenced()) {
367
            return;
368
        }
369
370
        $node->accept($this);
371
372
        return $node;
373
    }
374
375
    /**
376
     * Checks properties for presence in selector blocks.
377
     *
378
     * @param array $rules
379
     *
380
     * @throws CompilerException
381
     */
382
    private function checkPropertiesInRoot($rules)
383
    {
384
        for ($i = 0, $count = count($rules); $i < $count; ++$i) {
385
            $ruleNode = $rules[$i];
386
            if ($ruleNode instanceof RuleNode && !$ruleNode->variable) {
387
                throw new CompilerException(
388
                    'Properties must be inside selector blocks, they cannot be in the root.',
389
                    $ruleNode->index,
390
                    $ruleNode->currentFileInfo
391
                );
392
            }
393
        }
394
    }
395
396
    /**
397
     * Merges rules.
398
     *
399
     * @param array $rules
400
     */
401
    private function mergeRules(array &$rules)
402
    {
403
        $groups = [];
404
        for ($i = 0, $rulesCount = count($rules); $i < $rulesCount; ++$i) {
405
            $rule = $rules[$i];
406
            if (($rule instanceof RuleNode) && $rule->merge) {
407
                $key = $rule->name;
408
                if ($rule->important) {
409
                    $key .= ',!';
410
                }
411
                if (!isset($groups[$key])) {
412
                    $groups[$key] = [];
413
                } else {
414
                    array_splice($rules, $i--, 1);
415
                    --$rulesCount;
416
                }
417
                $groups[$key][] = $rule;
418
            }
419
        }
420
421
        foreach ($groups as $parts) {
422
            if (count($parts) > 1) {
423
                $rule = $parts[0];
424
                $spacedGroups = [];
425
                $lastSpacedGroup = [];
426
                foreach ($parts as $p) {
427
                    if ($p->merge === '+') {
428
                        if (count($lastSpacedGroup) > 0) {
429
                            $spacedGroups[] = $this->toExpression($lastSpacedGroup);
430
                        }
431
                        $lastSpacedGroup = [];
432
                    }
433
                    $lastSpacedGroup[] = $p;
434
                }
435
                $spacedGroups[] = $this->toExpression($lastSpacedGroup);
436
                $rule->value = $this->toValue($spacedGroups);
437
            }
438
        }
439
    }
440
441
    /**
442
     * Removes duplicates.
443
     *
444
     * @param array $rules
445
     */
446
    private function removeDuplicateRules(array &$rules)
447
    {
448
        // remove duplicates
449
        $ruleCache = [];
450
        for ($i = count($rules) - 1; $i >= 0; --$i) {
451
            $rule = $rules[$i];
452
            if ($rule instanceof RuleNode) {
453
                $key = serialize($rule->name);
454
                if (!isset($ruleCache[$key])) {
455
                    $ruleCache[$key] = $rule;
456
                } else {
457
                    $ruleList = &$ruleCache[$key];
458
                    if ($ruleList instanceof RuleNode) {
459
                        $ruleList = $ruleCache[$key] = [$ruleCache[$key]->toCSS($this->getContext())];
460
                    }
461
                    $ruleCSS = $rule->toCSS($this->getContext());
462
                    if (array_search($ruleCSS, $ruleList) !== false) {
463
                        array_splice($rules, $i, 1);
464
                    } else {
465
                        $ruleList[] = $ruleCSS;
466
                    }
467
                }
468
            }
469
        }
470
    }
471
472
    /**
473
     * @param $values
474
     *
475
     * @return ExpressionNode
476
     */
477
    private function toExpression($values)
478
    {
479
        $mapped = [];
480
        foreach ($values as $p) {
481
            $mapped[] = $p->value;
482
        }
483
484
        return new ExpressionNode($mapped);
485
    }
486
487
    /**
488
     * @param $values
489
     *
490
     * @return ValueNode
491
     */
492
    private function toValue($values)
493
    {
494
        return new ValueNode($values);
495
    }
496
}
497