Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.24% covered (success)
97.24%
141 / 145
89.47% covered (warning)
89.47%
17 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Less_Visitor_toCSS
97.24% covered (success)
97.24%
141 / 145
89.47% covered (warning)
89.47%
17 / 19
80
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 run
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitDeclaration
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 visitMixinDefinition
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitExtend
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitComment
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 visitMedia
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 visitAtRule
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
10
 checkPropertiesInRoot
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
8.83
 visitRuleset
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
10
 visitAnonymous
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 visitImport
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 visitRulesetRoot
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 visitRulesetPaths
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 _removeDuplicateRules
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 _mergeRules
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
11
 toExpression
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 toValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 hasVisibleChild
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2declare( strict_types = 1 );
3
4/**
5 * @private
6 */
7class Less_Visitor_toCSS extends Less_VisitorReplacing {
8
9    /** @var bool|null */
10    private $charset;
11
12    public function __construct() {
13        parent::__construct();
14    }
15
16    /**
17     * @param Less_Tree_Ruleset $root
18     */
19    public function run( $root ) {
20        return $this->visitObj( $root );
21    }
22
23    public function visitDeclaration( $declNode ) {
24        if ( $declNode->variable ) {
25            return [];
26        }
27        return $declNode;
28    }
29
30    public function visitMixinDefinition( $mixinNode ) {
31        // mixin definitions do not get eval'd - this means they keep state
32        // so we have to clear that state here so it isn't used if toCSS is called twice
33        $mixinNode->frames = [];
34        return [];
35    }
36
37    public function visitExtend() {
38        return [];
39    }
40
41    public function visitComment( $commentNode ) {
42        if ( $commentNode->isSilent() ) {
43            return [];
44        }
45        return $commentNode;
46    }
47
48    public function visitMedia( $mediaNode, &$visitDeeper ) {
49        $mediaNode->accept( $this );
50        $visitDeeper = false;
51
52        if ( !$mediaNode->rules ) {
53            return [];
54        }
55        return $mediaNode;
56    }
57
58    public function visitAtRule( $atRuleNode, &$visitDeeper ) {
59        if ( $atRuleNode->name === '@charset' ) {
60            if ( !$atRuleNode->getIsReferenced() ) {
61                return;
62            }
63            if ( isset( $this->charset ) && $this->charset ) {
64                // NOTE: Skip debugInfo handling (not implemented)
65                return;
66            }
67            $this->charset = true;
68        }
69
70        if ( $atRuleNode->rules ) {
71            self::_mergeRules( $atRuleNode->rules[0]->rules );
72            // process childs
73            $atRuleNode->accept( $this );
74            $visitDeeper = false;
75
76            // the directive was directly referenced and therefore needs to be shown in the output
77            if ( $atRuleNode->getIsReferenced() ) {
78                return $atRuleNode;
79            }
80
81            if ( !$atRuleNode->rules ) {
82                return;
83            }
84            if ( $this->hasVisibleChild( $atRuleNode ) ) {
85                // marking as referenced in case the directive is stored inside another directive
86                $atRuleNode->markReferenced();
87                return $atRuleNode;
88            }
89                // The directive was not directly referenced and does not contain anything that
90                //was referenced. Therefore it must not be shown in output.
91                return;
92        } else {
93            if ( !$atRuleNode->getIsReferenced() ) {
94                return;
95            }
96        }
97
98        return $atRuleNode;
99    }
100
101    public function checkPropertiesInRoot( $rulesetNode ) {
102        if ( !$rulesetNode->firstRoot ) {
103            return;
104        }
105
106        foreach ( $rulesetNode->rules as $ruleNode ) {
107            if ( $ruleNode instanceof Less_Tree_Declaration && !$ruleNode->variable ) {
108                $msg = "properties must be inside selector blocks, they cannot be in the root. Index " . $ruleNode->index .
109                    ( $ruleNode->currentFileInfo ? ' Filename: ' . $ruleNode->currentFileInfo['filename'] : null );
110                throw new Less_Exception_Compiler( $msg );
111            }
112        }
113    }
114
115    public function visitRuleset( $rulesetNode, &$visitDeeper ) {
116        $visitDeeper = false;
117
118        $this->checkPropertiesInRoot( $rulesetNode );
119
120        if ( $rulesetNode->root ) {
121            return $this->visitRulesetRoot( $rulesetNode );
122        }
123
124        $rulesets = [];
125        $rulesetNode->paths = $this->visitRulesetPaths( $rulesetNode );
126
127        // Compile rules and rulesets
128        $nodeRuleCnt = $rulesetNode->rules ? count( $rulesetNode->rules ) : 0;
129        for ( $i = 0; $i < $nodeRuleCnt; ) {
130            $rule = $rulesetNode->rules[$i];
131
132            if ( property_exists( $rule, 'rules' ) ) {
133                // visit because we are moving them out from being a child
134                $rulesets[] = $this->visitObj( $rule );
135                array_splice( $rulesetNode->rules, $i, 1 );
136                $nodeRuleCnt--;
137                continue;
138            }
139            $i++;
140        }
141
142        // accept the visitor to remove rules and refactor itself
143        // then we can decide now whether we want it or not
144        if ( $nodeRuleCnt > 0 ) {
145            $rulesetNode->accept( $this );
146
147            if ( $rulesetNode->rules ) {
148
149                if ( count( $rulesetNode->rules ) > 1 ) {
150                    self::_mergeRules( $rulesetNode->rules );
151                    $this->_removeDuplicateRules( $rulesetNode->rules );
152                }
153
154                // now decide whether we keep the ruleset
155                if ( $rulesetNode->paths ) {
156                    // array_unshift($rulesets, $rulesetNode);
157                    array_splice( $rulesets, 0, 0, [ $rulesetNode ] );
158                }
159            }
160
161        }
162
163        if ( count( $rulesets ) === 1 ) {
164            return $rulesets[0];
165        }
166        return $rulesets;
167    }
168
169    public function visitAnonymous( $anonymousNode ) {
170        if ( !$anonymousNode->getIsReferenced() ) {
171            return;
172        }
173
174        $anonymousNode->accept( $this );
175        return $anonymousNode;
176    }
177
178    public function visitImport( $importNode ) {
179        if ( isset( $importNode->path->currentFileInfo["reference"] ) && $importNode->css ) {
180            return;
181        }
182        return $importNode;
183    }
184
185    /**
186     * Helper function for visitiRuleset
187     *
188     * return array|Less_Tree_Ruleset
189     */
190    private function visitRulesetRoot( $rulesetNode ) {
191        $rulesetNode->accept( $this );
192        if ( $rulesetNode->firstRoot || $rulesetNode->rules ) {
193            return $rulesetNode;
194        }
195        return [];
196    }
197
198    /**
199     * Helper function for visitRuleset()
200     *
201     * @return array
202     */
203    private function visitRulesetPaths( $rulesetNode ) {
204        $paths = [];
205        foreach ( $rulesetNode->paths as $p ) {
206            if ( $p[0]->elements[0]->combinator === ' ' ) {
207                $p[0]->elements[0]->combinator = '';
208            }
209
210            foreach ( $p as $pi ) {
211                if ( $pi->getIsReferenced() && $pi->getIsOutput() ) {
212                    $paths[] = $p;
213                    break;
214                }
215            }
216        }
217
218        return $paths;
219    }
220
221    protected function _removeDuplicateRules( &$rules ) {
222        // remove duplicates
223        $ruleCache = [];
224        for ( $i = count( $rules ) - 1; $i >= 0; $i-- ) {
225            $rule = $rules[$i];
226            if ( $rule instanceof Less_Tree_Declaration || $rule instanceof Less_Tree_NameValue ) {
227
228                if ( !isset( $ruleCache[$rule->name] ) ) {
229                    $ruleCache[$rule->name] = $rule;
230                } else {
231                    $ruleList =& $ruleCache[$rule->name];
232
233                    if ( $ruleList instanceof Less_Tree_Declaration || $ruleList instanceof Less_Tree_NameValue ) {
234                        $ruleList = $ruleCache[$rule->name] = [ $ruleCache[$rule->name]->toCSS() ];
235                    }
236
237                    $ruleCSS = $rule->toCSS();
238                    if ( in_array( $ruleCSS, $ruleList ) ) {
239                        array_splice( $rules, $i, 1 );
240                    } else {
241                        $ruleList[] = $ruleCSS;
242                    }
243                }
244            }
245        }
246    }
247
248    public static function _mergeRules( &$rules ) {
249        $groups = [];
250
251        // obj($rules);
252
253        $rules_len = count( $rules );
254        for ( $i = 0; $i < $rules_len; $i++ ) {
255            $rule = $rules[$i];
256
257            if ( ( $rule instanceof Less_Tree_Declaration ) && $rule->merge ) {
258
259                $key = $rule->name;
260                if ( $rule->important ) {
261                    $key .= ',!';
262                }
263
264                if ( !isset( $groups[$key] ) ) {
265                    $groups[$key] = [];
266                } else {
267                    array_splice( $rules, $i--, 1 );
268                    $rules_len--;
269                }
270
271                $groups[$key][] = $rule;
272            }
273        }
274
275        foreach ( $groups as $parts ) {
276
277            if ( count( $parts ) > 1 ) {
278                $rule = $parts[0];
279                $spacedGroups = [];
280                $lastSpacedGroup = [];
281                $parts_mapped = [];
282                foreach ( $parts as $p ) {
283                    if ( $p->merge === '+' ) {
284                        if ( $lastSpacedGroup ) {
285                            $spacedGroups[] = self::toExpression( $lastSpacedGroup );
286                        }
287                        $lastSpacedGroup = [];
288                    }
289                    $lastSpacedGroup[] = $p;
290                }
291
292                $spacedGroups[] = self::toExpression( $lastSpacedGroup );
293                $rule->value = self::toValue( $spacedGroups );
294            }
295        }
296    }
297
298    public static function toExpression( $values ) {
299        $mapped = [];
300        foreach ( $values as $p ) {
301            $mapped[] = $p->value;
302        }
303        return new Less_Tree_Expression( $mapped );
304    }
305
306    public static function toValue( $values ) {
307        // return new Less_Tree_Value($values); ??
308
309        $mapped = [];
310        foreach ( $values as $p ) {
311            $mapped[] = $p;
312        }
313        return new Less_Tree_Value( $mapped );
314    }
315
316    public function hasVisibleChild( $atRuleNode ) {
317        // prepare list of childs
318        $rule = $bodyRules = $atRuleNode->rules;
319        // if there is only one nested ruleset and that one has no path, then it is
320        //just fake ruleset that got not replaced and we need to look inside it to
321        //get real childs
322        if ( count( $bodyRules ) === 1 && ( !$bodyRules[0]->paths || count( $bodyRules[0]->paths ) === 0 ) ) {
323            $bodyRules = $bodyRules[0]->rules;
324        }
325        foreach ( $bodyRules as $rule ) {
326            if ( method_exists( $rule, 'getIsReferenced' ) && $rule->getIsReferenced() ) {
327                // the directive contains something that was referenced (likely by extend)
328                //therefore it needs to be shown in output too
329                return true;
330            }
331        }
332        return false;
333    }
334}