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

LessRuleList::splitSelector()   C

Complexity

Conditions 11
Paths 30

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 11

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 33
ccs 26
cts 26
cp 1
rs 5.2653
cc 11
eloc 19
nc 30
nop 1
crap 11

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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