Completed
Push — master ( f301aa...0a6a3c )
by Remo
03:56
created

LessRuleList   C

Complexity

Total Complexity 60

Size/Duplication

Total Lines 263
Duplicated Lines 2.28 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 100%

Importance

Changes 18
Bugs 9 Features 4
Metric Value
wmc 60
c 18
b 9
f 4
lcom 1
cbo 6
dl 6
loc 263
ccs 143
cts 143
cp 1
rs 6.0975

10 Methods

Rating   Name   Duplication   Size   Complexity  
A addRule() 0 4 1
A getTree() 0 13 3
A parseDirectDescendants() 0 6 1
C parsePseudoClasses() 0 24 9
B parseSelectors() 0 21 5
C parseTreeNode() 0 34 7
C splitSelector() 0 33 11
C formatTokenAsLess() 6 24 15
B formatAsLess() 0 22 5
A lessify() 0 17 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like LessRuleList often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LessRuleList, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Ortic\Css2Less\tokens;
4
5
/**
6
 * Class LessRuleList
7
 * @package Ortic\Css2Less\tokens
8
 */
9
class LessRuleList
10
{
11
    private $list = array();
12
13
    /**
14
     * Add a new rule object to our list
15
     * @param LessRule $rule
16
     */
17 15
    public function addRule(LessRule $rule)
18
    {
19 15
        $this->list[] = $rule;
20 15
    }
21
22
    /**
23
     * Build and returns a tree for the CSS input
24
     * @return array
25
     */
26 15
    protected function getTree()
27
    {
28 15
        $output = array();
29
30 15
        foreach ($this->list as $ruleSet) {
31 15
            $selectors = $ruleSet->getSelectors();
32
33 15
            foreach ($ruleSet->getTokens() as $token) {
34 15
                $this->parseTreeNode($output, $selectors, $token);
35 15
            }
36 15
        }
37 15
        return $output;
38
    }
39
40
    /**
41
     * Add support for direct descendants operator by aligning the spaces properly.
42
     * the code below supports "html >p" since we split by spaces. A selector "html > p" would cause an
43
     * additional tree level, we therefore normalize them with the two lines below.
44
     *
45
     * @param string $selector
46
     * @return string
47
     */
48 15
    protected function parseDirectDescendants($selector)
49
    {
50 15
        $selector = str_replace('> ', '>', $selector);
51 15
        $selector = str_replace('>', ' >', $selector);
52 15
        return $selector;
53
    }
54
55
    /**
56
     * Support for pseudo classes by adding a space before ":" and also "&" to let less know that there
57
     * shouldn't be a space when concatenating the nested selectors to a single css rule. We have to
58
     * ignore every colon if it's wrapped by :not(...) as we don't nest this in LESS.
59
     *
60
     * @param string $selector
61
     * @return string
62
     */
63 15
    protected function parsePseudoClasses($selector)
64
    {
65 15
        $nestedPseudo = false;
66 15
        $lastCharacterColon = false;
67 15
        $selectorOut = '';
68 15
        for ($i = 0; $i < strlen($selector); $i++) {
69 15
            $c = $selector{$i};
70
71
            // Don't parse anything between (..) and [..]
72 15
            $nestedPseudo = ($c === '(' || $c === '[') || $nestedPseudo;
73 15
            $nestedPseudo = !($c === ')' || $c === ']') && $nestedPseudo;
74
75 15
            if ($nestedPseudo === false && $c === ':' && $lastCharacterColon === false) {
76 3
                $selectorOut .= ' &';
77 3
                $lastCharacterColon = true;
78 3
            }
79
            else {
80 15
                $lastCharacterColon = false;
81
            }
82
83 15
            $selectorOut .= $c;
84 15
        }
85 15
        return $selectorOut;
86
    }
87
88
    /**
89
     * Ensures that operators like "+" are properly combined with a "&"
90
     * 
91
     * @param $selector
92
     * @param array $characters
93
     * @return string
94
     */
95 15
    protected function parseSelectors($selector, array $characters)
96
    {
97 15
        $selectorOut = '';
98 15
        $selectorFound = false;
99 15
        for ($i = 0; $i < strlen($selector); $i++) {
100 15
            $c = $selector{$i};
101 15
            if ($c == ' ' && $selectorFound) {
102 3
                continue;
103
            }
104
            else {
105 15
                $selectorFound = false;
106
            }
107 15
            if (in_array($c, $characters)) {
108 3
                $selectorOut .= '&';
109 3
                $selectorFound = true;
110 3
            }
111 15
            $selectorOut .= $c;
112 15
        }
113
114 15
        return $selectorOut;
115
    }
116
117
    /**
118
     * Parse CSS input part into a LESS node
119
     * @param $output
120
     * @param $selectors
121
     * @param $token
122
     */
123 15
    protected function parseTreeNode(&$output, $selectors, $token)
124
    {
125
        // we don't parse comments
126 15
        if ($token instanceof \CssCommentToken) {
127 1
            return;
128
        }
129 15
        foreach ($token->MediaTypes as $mediaType) {
130
            // make sure we're aware of our media type
131 15
            if (!array_key_exists($mediaType, $output)) {
132 15
                $output[$mediaType] = array();
133 15
            }
134
135 15
            foreach ($selectors as $selector) {
136
                // add declaration token to output for each selector
137 15
                $currentNode = &$output[$mediaType];
138
139 15
                $selector = $this->parseDirectDescendants($selector);
140 15
                $selector = $this->parsePseudoClasses($selector);
141 15
                $selector = $this->parseSelectors($selector, ['+', '~']);
142
143
                // selectors like "html body" must be split into an array so we can
144
                // easily nest them
145 15
                $selectorPath = $this->splitSelector($selector);
146 15
                foreach ($selectorPath as $selectorPathItem) {
147 15
                    if (!array_key_exists($selectorPathItem, $currentNode)) {
148 15
                        $currentNode[$selectorPathItem] = array();
149 15
                    }
150 15
                    $currentNode = &$currentNode[$selectorPathItem];
151 15
                }
152
153 15
                $currentNode['@rules'][] = $this->formatTokenAsLess($token);
154 15
            }
155 15
        }
156 15
    }
157
158
    /**
159
     * Splits CSS selectors into an array, but only where it makes sense to create a new nested level in LESS/SCSS.
160
     * We split "body div" into array(body, div), but we don't split "a[title='hello world']" and thus create
161
     * array([title='hello world'])
162
     *
163
     * @param string $selector
164
     * @return array
165
     */
166 15
    protected function splitSelector($selector)
167
    {
168 15
        $selectors = [];
169
170 15
        $currentSelector = '';
171 15
        $quoteFound = false;
172 15
        for ($i = 0; $i < strlen($selector); $i++) {
173 15
            $c = $selector{$i};
174
175 15
            if ($c === ' ' && !$quoteFound) {
176 12
                if (trim($currentSelector) != '') {
177 12
                    $selectors[] = trim($currentSelector);
178 12
                    $currentSelector = '';
179 12
                }
180 12
            }
181
182 15
            if ($quoteFound && in_array($c, ['"', '\''])) {
183 2
                $quoteFound = false;
184 2
            }
185 15
            elseif (!$quoteFound && in_array($c, ['"', '\''])) {
186 2
                $quoteFound = true;
187 2
            }
188
189 15
            $currentSelector .= $c;
190 15
        }
191 15
        if ($currentSelector != '') {
192 15
            if (trim($currentSelector) != '') {
193 15
                $selectors[] = trim($currentSelector);
194 15
            }
195 15
        }
196
197 15
        return $selectors;
198
    }
199
200
    /**
201
     * Format LESS nodes in a nicer way with indentation and proper brackets
202
     * @param $token
203
     * @param int $level
204
     * @return string
205
     */
206 15
    public function formatTokenAsLess(\aCssToken $token, $level = 0)
207
    {
208 15
        $indentation = str_repeat("\t", $level);
209
210 15
        if ($token instanceof \CssRulesetDeclarationToken) {
211 15
            return $indentation . $token->Property . ": " . $token->Value . ($token->IsImportant ? " !important" : "") . ($token->IsLast ? "" : ";");
212 1
        } elseif ($token instanceof \CssAtKeyframesStartToken) {
213 1
            return $indentation . "@" . $token->AtRuleName . " \"" . $token->Name . "\" {";
214 1
        } elseif ($token instanceof \CssAtKeyframesRulesetStartToken) {
215 1
            return $indentation . "\t" . implode(",", $token->Selectors) . " {";
216 1
        } elseif ($token instanceof \CssAtKeyframesRulesetEndToken) {
217 1
            return $indentation . "\t" . "}";
218 1 View Code Duplication
        } elseif ($token instanceof \CssAtKeyframesRulesetDeclarationToken) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
219 1
            return $indentation . "\t\t" . $token->Property . ": " . $token->Value . ($token->IsImportant ? " !important" : "") . ($token->IsLast ? "" : ";");
220 1
        } elseif ($token instanceof \CssAtCharsetToken) {
221 1
            return $indentation . "@charset " . $token->Charset . ";";
222 1
        } elseif ($token instanceof \CssAtFontFaceStartToken) {
223 1
            return "@font-face {";
224 1 View Code Duplication
        } elseif ($token instanceof \CssAtFontFaceDeclarationToken) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
225 1
            return $indentation . "\t" . $token->Property . ": " . $token->Value . ($token->IsImportant ? " !important" : "") . ($token->IsLast ? "" : ";");
226
        } else {
227 1
            return $indentation . $token;
228
        }
229
    }
230
231 15
    protected function formatAsLess($selector, $level = 0)
232
    {
233 15
        $return = '';
234 15
        $indentation = str_repeat("\t", $level);
235 15
        foreach ($selector as $nodeKey => $node) {
236 15
            $return .= $indentation . "{$nodeKey} {\n";
237
238 15
            foreach ($node as $subNodeKey => $subNodes) {
239 15
                if ($subNodeKey === '@rules') {
240 15
                    foreach ($subNodes as $subNode) {
241 15
                        $return .= $indentation . "\t" . $subNode . "\n";
242 15
                    }
243 15
                } else {
244 12
                    $return .= $this->formatAsLess(array($subNodeKey => $subNodes), $level + 1);
245
                }
246 15
            }
247
248 15
            $return .= $indentation . "}\n";
249
250 15
        }
251 15
        return $return;
252
    }
253
254 15
    public function lessify()
255
    {
256 15
        $tree = $this->getTree();
257 15
        $return = '';
258
259 15
        foreach ($tree as $mediaType => $node) {
260 15
            if ($mediaType == 'all') {
261 15
                $return .= $this->formatAsLess($node);
262 15
            } else {
263 1
                $return .= "@media {$mediaType} {\n";
264 1
                $return .= $this->formatAsLess($node, 1);
265 1
                $return .= "}\n";
266
            }
267 15
        }
268
269 15
        return $return;
270
    }
271
}
272