LessRuleList::parseSelectors()   B
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 15
cts 15
cp 1
rs 8.7624
c 0
b 0
f 0
cc 5
eloc 14
nc 4
nop 2
crap 5
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 16
    public function addRule(LessRule $rule)
18
    {
19 16
        $this->list[] = $rule;
20 16
    }
21
22
    /**
23
     * Build and returns a tree for the CSS input
24
     * @return array
25
     */
26 16
    protected function getTree()
27
    {
28 16
        $output = array();
29
30 16
        foreach ($this->list as $ruleSet) {
31 16
            $selectors = $ruleSet->getSelectors();
32
33 16
            foreach ($ruleSet->getTokens() as $token) {
34 16
                $this->parseTreeNode($output, $selectors, $token);
35 16
            }
36 16
        }
37 16
        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 16
    protected function parseDirectDescendants($selector)
49
    {
50 16
        $selector = str_replace('> ', '>', $selector);
51 16
        $selector = str_replace('>', ' >', $selector);
52 16
        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 16
    protected function parsePseudoClasses($selector)
64
    {
65 16
        $nestedPseudo = false;
66 16
        $lastCharacterColon = false;
67 16
        $selectorOut = '';
68 16
        for ($i = 0; $i < strlen($selector); $i++) {
69 16
            $c = $selector{$i};
70
71
            // Don't parse anything between (..) and [..]
72 16
            $nestedPseudo = ($c === '(' || $c === '[') || $nestedPseudo;
73 16
            $nestedPseudo = !($c === ')' || $c === ']') && $nestedPseudo;
74
75 16
            if ($nestedPseudo === false && $c === ':' && $lastCharacterColon === false && $i > 0) {
76 3
                $selectorOut .= ' &';
77 3
                $lastCharacterColon = true;
78 3
            }
79
            else {
80 16
                $lastCharacterColon = false;
81
            }
82
83 16
            $selectorOut .= $c;
84 16
        }
85 16
        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 16
    protected function parseSelectors($selector, array $characters)
96
    {
97 16
        $selectorOut = '';
98 16
        $selectorFound = false;
99 16
        for ($i = 0; $i < strlen($selector); $i++) {
100 16
            $c = $selector{$i};
101 16
            if ($c == ' ' && $selectorFound) {
102 3
                continue;
103
            }
104
            else {
105 16
                $selectorFound = false;
106
            }
107 16
            if (in_array($c, $characters)) {
108 3
                $selectorOut .= '&';
109 3
                $selectorFound = true;
110 3
            }
111 16
            $selectorOut .= $c;
112 16
        }
113
114 16
        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 16
    protected function parseTreeNode(&$output, $selectors, $token)
124
    {
125
        // we don't parse comments
126 16
        if ($token instanceof \CssCommentToken) {
127 1
            return;
128
        }
129 16
        foreach ($token->MediaTypes as $mediaType) {
130
            // make sure we're aware of our media type
131 16
            if (!array_key_exists($mediaType, $output)) {
132 16
                $output[$mediaType] = array();
133 16
            }
134
135 16
            foreach ($selectors as $selector) {
136
                // add declaration token to output for each selector
137 16
                $currentNode = &$output[$mediaType];
138
139 16
                $selector = $this->parseDirectDescendants($selector);
140 16
                $selector = $this->parsePseudoClasses($selector);
141 16
                $selector = $this->parseSelectors($selector, ['+', '~']);
142
143
                // selectors like "html body" must be split into an array so we can
144
                // easily nest them
145 16
                $selectorPath = $this->splitSelector($selector);
146 16
                foreach ($selectorPath as $selectorPathItem) {
147 16
                    if (!array_key_exists($selectorPathItem, $currentNode)) {
148 16
                        $currentNode[$selectorPathItem] = array();
149 16
                    }
150 16
                    $currentNode = &$currentNode[$selectorPathItem];
151 16
                }
152
153 16
                $currentNode['@rules'][] = $this->formatTokenAsLess($token);
154 16
            }
155 16
        }
156 16
    }
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 16
    protected function splitSelector($selector)
167
    {
168 16
        $selectors = [];
169
170 16
        $currentSelector = '';
171 16
        $quoteFound = false;
172 16
        for ($i = 0; $i < strlen($selector); $i++) {
173 16
            $c = $selector{$i};
174
175 16
            if ($c === ' ' && !$quoteFound) {
176 12
                if (trim($currentSelector) != '') {
177 12
                    $selectors[] = trim($currentSelector);
178 12
                    $currentSelector = '';
179 12
                }
180 12
            }
181
182 16
            if ($quoteFound && in_array($c, ['"', '\''])) {
183 2
                $quoteFound = false;
184 2
            }
185 16
            elseif (!$quoteFound && in_array($c, ['"', '\''])) {
186 2
                $quoteFound = true;
187 2
            }
188
189 16
            $currentSelector .= $c;
190 16
        }
191 16
        if ($currentSelector != '') {
192 16
            if (trim($currentSelector) != '') {
193 16
                $selectors[] = trim($currentSelector);
194 16
            }
195 16
        }
196
197 16
        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 16
    public function formatTokenAsLess(\aCssToken $token, $level = 0)
207
    {
208 16
        $indentation = str_repeat("\t", $level);
209
210 16
        if ($token instanceof \CssRulesetDeclarationToken) {
211 16
            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 1
        } elseif ($token instanceof \CssCommentToken) {
227 1
            return;
228
        } else {
229 1
            return $indentation . $token;
230
        }
231
    }
232
233 16
    protected function formatAsLess($selector, $level = 0)
234
    {
235 16
        $return = '';
236 16
        $indentation = str_repeat("\t", $level);
237 16
        foreach ($selector as $nodeKey => $node) {
238 16
            $return .= $indentation . "{$nodeKey} {\n";
239
240 16
            foreach ($node as $subNodeKey => $subNodes) {
241 16
                if ($subNodeKey === '@rules') {
242 16
                    foreach ($subNodes as $subNode) {
243 16
                        $return .= $indentation . "\t" . $subNode . "\n";
244 16
                    }
245 16
                } else {
246 12
                    $return .= $this->formatAsLess(array($subNodeKey => $subNodes), $level + 1);
247
                }
248 16
            }
249
250 16
            $return .= $indentation . "}\n";
251
252 16
        }
253 16
        return $return;
254
    }
255
256 16
    public function lessify()
257
    {
258 16
        $tree = $this->getTree();
259 16
        $return = '';
260
261 16
        foreach ($tree as $mediaType => $node) {
262 16
            if ($mediaType == 'all') {
263 16
                $return .= $this->formatAsLess($node);
264 16
            } else {
265 1
                $return .= "@media {$mediaType} {\n";
266 1
                $return .= $this->formatAsLess($node, 1);
267 1
                $return .= "}\n";
268
            }
269 16
        }
270
271 16
        return $return;
272
    }
273
}
274