1<?php
2/**
3 * Tokenizes CSS code.
4 *
5 * PHP version 5
6 *
7 * @category  PHP
8 * @package   PHP_CodeSniffer
9 * @author    Greg Sherwood <gsherwood@squiz.net>
10 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
11 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
12 * @link      http://pear.php.net/package/PHP_CodeSniffer
13 */
14
15if (class_exists('PHP_CodeSniffer_Tokenizers_PHP', true) === false) {
16    throw new Exception('Class PHP_CodeSniffer_Tokenizers_PHP not found');
17}
18
19/**
20 * Tokenizes CSS code.
21 *
22 * @category  PHP
23 * @package   PHP_CodeSniffer
24 * @author    Greg Sherwood <gsherwood@squiz.net>
25 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
26 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
27 * @version   Release: @package_version@
28 * @link      http://pear.php.net/package/PHP_CodeSniffer
29 */
30class PHP_CodeSniffer_Tokenizers_CSS extends PHP_CodeSniffer_Tokenizers_PHP
31{
32
33    /**
34     * If TRUE, files that appear to be minified will not be processed.
35     *
36     * @var boolean
37     */
38    public $skipMinified = true;
39
40
41    /**
42     * Creates an array of tokens when given some CSS code.
43     *
44     * Uses the PHP tokenizer to do all the tricky work
45     *
46     * @param string $string  The string to tokenize.
47     * @param string $eolChar The EOL character to use for splitting strings.
48     *
49     * @return array
50     */
51    public function tokenizeString($string, $eolChar='\n')
52    {
53        if (PHP_CODESNIFFER_VERBOSITY > 1) {
54            echo "\t*** START CSS TOKENIZING 1ST PASS ***".PHP_EOL;
55        }
56
57        // If the content doesn't have an EOL char on the end, add one so
58        // the open and close tags we add are parsed correctly.
59        $eolAdded = false;
60        if (substr($string, (strlen($eolChar) * -1)) !== $eolChar) {
61            $string  .= $eolChar;
62            $eolAdded = true;
63        }
64
65        $string = str_replace('<?php', '^PHPCS_CSS_T_OPEN_TAG^', $string);
66        $string = str_replace('?>', '^PHPCS_CSS_T_CLOSE_TAG^', $string);
67        $tokens = parent::tokenizeString('<?php '.$string.'?>', $eolChar);
68
69        $finalTokens    = array();
70        $finalTokens[0] = array(
71                           'code'    => T_OPEN_TAG,
72                           'type'    => 'T_OPEN_TAG',
73                           'content' => '',
74                          );
75
76        $newStackPtr      = 1;
77        $numTokens        = count($tokens);
78        $multiLineComment = false;
79        for ($stackPtr = 1; $stackPtr < $numTokens; $stackPtr++) {
80            $token = $tokens[$stackPtr];
81
82            // CSS files don't have lists, breaks etc, so convert these to
83            // standard strings early so they can be converted into T_STYLE
84            // tokens and joined with other strings if needed.
85            if ($token['code'] === T_BREAK
86                || $token['code'] === T_LIST
87                || $token['code'] === T_DEFAULT
88                || $token['code'] === T_SWITCH
89                || $token['code'] === T_FOR
90                || $token['code'] === T_FOREACH
91                || $token['code'] === T_WHILE
92                || $token['code'] === T_DEC
93            ) {
94                $token['type'] = 'T_STRING';
95                $token['code'] = T_STRING;
96            }
97
98            if (PHP_CODESNIFFER_VERBOSITY > 1) {
99                $type    = $token['type'];
100                $content = PHP_CodeSniffer::prepareForOutput($token['content']);
101                echo "\tProcess token $stackPtr: $type => $content".PHP_EOL;
102            }
103
104            if ($token['code'] === T_BITWISE_XOR
105                && $tokens[($stackPtr + 1)]['content'] === 'PHPCS_CSS_T_OPEN_TAG'
106            ) {
107                $content = '<?php';
108                for ($stackPtr = ($stackPtr + 3); $stackPtr < $numTokens; $stackPtr++) {
109                    if ($tokens[$stackPtr]['code'] === T_BITWISE_XOR
110                        && $tokens[($stackPtr + 1)]['content'] === 'PHPCS_CSS_T_CLOSE_TAG'
111                    ) {
112                        // Add the end tag and ignore the * we put at the end.
113                        $content  .= '?>';
114                        $stackPtr += 2;
115                        break;
116                    } else {
117                        $content .= $tokens[$stackPtr]['content'];
118                    }
119                }
120
121                if (PHP_CODESNIFFER_VERBOSITY > 1) {
122                    echo "\t\t=> Found embedded PHP code: ";
123                    $cleanContent = PHP_CodeSniffer::prepareForOutput($content);
124                    echo $cleanContent.PHP_EOL;
125                }
126
127                $finalTokens[$newStackPtr] = array(
128                                              'type'    => 'T_EMBEDDED_PHP',
129                                              'code'    => T_EMBEDDED_PHP,
130                                              'content' => $content,
131                                             );
132
133                $newStackPtr++;
134                continue;
135            }//end if
136
137            if ($token['code'] === T_GOTO_LABEL) {
138                // Convert these back to T_STRING followed by T_COLON so we can
139                // more easily process style definitions.
140                $finalTokens[$newStackPtr] = array(
141                                              'type'    => 'T_STRING',
142                                              'code'    => T_STRING,
143                                              'content' => substr($token['content'], 0, -1),
144                                             );
145                $newStackPtr++;
146                $finalTokens[$newStackPtr] = array(
147                                              'type'    => 'T_COLON',
148                                              'code'    => T_COLON,
149                                              'content' => ':',
150                                             );
151                $newStackPtr++;
152                continue;
153            }
154
155            if ($token['code'] === T_FUNCTION) {
156                // There are no functions in CSS, so convert this to a string.
157                $finalTokens[$newStackPtr] = array(
158                                              'type'    => 'T_STRING',
159                                              'code'    => T_STRING,
160                                              'content' => $token['content'],
161                                             );
162
163                $newStackPtr++;
164                continue;
165            }
166
167            if ($token['code'] === T_COMMENT
168                && substr($token['content'], 0, 2) === '/*'
169            ) {
170                // Multi-line comment. Record it so we can ignore other
171                // comment tags until we get out of this one.
172                $multiLineComment = true;
173            }
174
175            if ($token['code'] === T_COMMENT
176                && $multiLineComment === false
177                && (substr($token['content'], 0, 2) === '//'
178                || $token['content']{0} === '#')
179            ) {
180                $content = ltrim($token['content'], '#/');
181
182                // Guard against PHP7+ syntax errors by stripping
183                // leading zeros so the content doesn't look like an invalid int.
184                $leadingZero = false;
185                if ($content{0} === '0') {
186                    $content     = '1'.$content;
187                    $leadingZero = true;
188                }
189
190                $commentTokens = parent::tokenizeString('<?php '.$content.'?>', $eolChar);
191
192                // The first and last tokens are the open/close tags.
193                array_shift($commentTokens);
194                array_pop($commentTokens);
195
196                if ($leadingZero === true) {
197                    $commentTokens[0]['content'] = substr($commentTokens[0]['content'], 1);
198                    $content = substr($content, 1);
199                }
200
201                if ($token['content']{0} === '#') {
202                    // The # character is not a comment in CSS files, so
203                    // determine what it means in this context.
204                    $firstContent = $commentTokens[0]['content'];
205
206                    // If the first content is just a number, it is probably a
207                    // colour like 8FB7DB, which PHP splits into 8 and FB7DB.
208                    if (($commentTokens[0]['code'] === T_LNUMBER
209                        || $commentTokens[0]['code'] === T_DNUMBER)
210                        && $commentTokens[1]['code'] === T_STRING
211                    ) {
212                        $firstContent .= $commentTokens[1]['content'];
213                        array_shift($commentTokens);
214                    }
215
216                    // If the first content looks like a colour and not a class
217                    // definition, join the tokens together.
218                    if (preg_match('/^[ABCDEF0-9]+$/i', $firstContent) === 1
219                        && $commentTokens[1]['content'] !== '-'
220                    ) {
221                        array_shift($commentTokens);
222                        // Work out what we trimmed off above and remember to re-add it.
223                        $trimmed = substr($token['content'], 0, (strlen($token['content']) - strlen($content)));
224                        $finalTokens[$newStackPtr] = array(
225                                                      'type'    => 'T_COLOUR',
226                                                      'code'    => T_COLOUR,
227                                                      'content' => $trimmed.$firstContent,
228                                                     );
229                    } else {
230                        $finalTokens[$newStackPtr] = array(
231                                                      'type'    => 'T_HASH',
232                                                      'code'    => T_HASH,
233                                                      'content' => '#',
234                                                     );
235                    }
236                } else {
237                    $finalTokens[$newStackPtr] = array(
238                                                  'type'    => 'T_STRING',
239                                                  'code'    => T_STRING,
240                                                  'content' => '//',
241                                                 );
242                }//end if
243
244                $newStackPtr++;
245
246                array_splice($tokens, $stackPtr, 1, $commentTokens);
247                $numTokens = count($tokens);
248                $stackPtr--;
249                continue;
250            }//end if
251
252            if ($token['code'] === T_COMMENT
253                && substr($token['content'], -2) === '*/'
254            ) {
255                // Multi-line comment is done.
256                $multiLineComment = false;
257            }
258
259            $finalTokens[$newStackPtr] = $token;
260            $newStackPtr++;
261        }//end for
262
263        if (PHP_CODESNIFFER_VERBOSITY > 1) {
264            echo "\t*** END CSS TOKENIZING 1ST PASS ***".PHP_EOL;
265            echo "\t*** START CSS TOKENIZING 2ND PASS ***".PHP_EOL;
266        }
267
268        // A flag to indicate if we are inside a style definition,
269        // which is defined using curly braces.
270        $inStyleDef = false;
271
272        // A flag to indicate if an At-rule like "@media" is used, which will result
273        // in nested curly brackets.
274        $asperandStart = false;
275
276        $numTokens = count($finalTokens);
277        for ($stackPtr = 0; $stackPtr < $numTokens; $stackPtr++) {
278            $token = $finalTokens[$stackPtr];
279
280            if (PHP_CODESNIFFER_VERBOSITY > 1) {
281                $type    = $token['type'];
282                $content = PHP_CodeSniffer::prepareForOutput($token['content']);
283                echo "\tProcess token $stackPtr: $type => $content".PHP_EOL;
284            }
285
286            switch ($token['code']) {
287            case T_OPEN_CURLY_BRACKET:
288                // Opening curly brackets for an At-rule do not start a style
289                // definition. We also reset the asperand flag here because the next
290                // opening curly bracket could be indeed the start of a style
291                // definition.
292                if ($asperandStart === true) {
293                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
294                        if ($inStyleDef === true) {
295                            echo "\t\t* style definition closed *".PHP_EOL;
296                        }
297
298                        if ($asperandStart === true) {
299                            echo "\t\t* at-rule definition closed *".PHP_EOL;
300                        }
301                    }
302
303                    $inStyleDef    = false;
304                    $asperandStart = false;
305                } else {
306                    $inStyleDef = true;
307                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
308                        echo "\t\t* style definition opened *".PHP_EOL;
309                    }
310                }
311                break;
312            case T_CLOSE_CURLY_BRACKET:
313                if (PHP_CODESNIFFER_VERBOSITY > 1) {
314                    if ($inStyleDef === true) {
315                        echo "\t\t* style definition closed *".PHP_EOL;
316                    }
317
318                    if ($asperandStart === true) {
319                        echo "\t\t* at-rule definition closed *".PHP_EOL;
320                    }
321                }
322
323                $inStyleDef    = false;
324                $asperandStart = false;
325                break;
326            case T_MINUS:
327                // Minus signs are often used instead of spaces inside
328                // class names, IDs and styles.
329                if ($finalTokens[($stackPtr + 1)]['code'] === T_STRING) {
330                    if ($finalTokens[($stackPtr - 1)]['code'] === T_STRING) {
331                        $newContent = $finalTokens[($stackPtr - 1)]['content'].'-'.$finalTokens[($stackPtr + 1)]['content'];
332
333                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
334                            echo "\t\t* token is a string joiner; ignoring this and previous token".PHP_EOL;
335                            $old = PHP_CodeSniffer::prepareForOutput($finalTokens[($stackPtr + 1)]['content']);
336                            $new = PHP_CodeSniffer::prepareForOutput($newContent);
337                            echo "\t\t=> token ".($stackPtr + 1)." content changed from \"$old\" to \"$new\"".PHP_EOL;
338                        }
339
340                        $finalTokens[($stackPtr + 1)]['content'] = $newContent;
341                        unset($finalTokens[$stackPtr]);
342                        unset($finalTokens[($stackPtr - 1)]);
343                    } else {
344                        $newContent = '-'.$finalTokens[($stackPtr + 1)]['content'];
345
346                        $finalTokens[($stackPtr + 1)]['content'] = $newContent;
347                        unset($finalTokens[$stackPtr]);
348                    }
349                } else if ($finalTokens[($stackPtr + 1)]['code'] === T_LNUMBER) {
350                    // They can also be used to provide negative numbers.
351                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
352                        echo "\t\t* token is part of a negative number; adding content to next token and ignoring *".PHP_EOL;
353                        $content = PHP_CodeSniffer::prepareForOutput($finalTokens[($stackPtr + 1)]['content']);
354                        echo "\t\t=> token ".($stackPtr + 1)." content changed from \"$content\" to \"-$content\"".PHP_EOL;
355                    }
356
357                    $finalTokens[($stackPtr + 1)]['content'] = '-'.$finalTokens[($stackPtr + 1)]['content'];
358                    unset($finalTokens[$stackPtr]);
359                }//end if
360
361                break;
362            case T_COLON:
363                // Only interested in colons that are defining styles.
364                if ($inStyleDef === false) {
365                    break;
366                }
367
368                for ($x = ($stackPtr - 1); $x >= 0; $x--) {
369                    if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$finalTokens[$x]['code']]) === false) {
370                        break;
371                    }
372                }
373
374                if (PHP_CODESNIFFER_VERBOSITY > 1) {
375                    $type = $finalTokens[$x]['type'];
376                    echo "\t\t=> token $x changed from $type to T_STYLE".PHP_EOL;
377                }
378
379                $finalTokens[$x]['type'] = 'T_STYLE';
380                $finalTokens[$x]['code'] = T_STYLE;
381                break;
382            case T_STRING:
383                if (strtolower($token['content']) === 'url') {
384                    // Find the next content.
385                    for ($x = ($stackPtr + 1); $x < $numTokens; $x++) {
386                        if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$finalTokens[$x]['code']]) === false) {
387                            break;
388                        }
389                    }
390
391                    // Needs to be in the format "url(" for it to be a URL.
392                    if ($finalTokens[$x]['code'] !== T_OPEN_PARENTHESIS) {
393                        continue;
394                    }
395
396                    // Make sure the content isn't empty.
397                    for ($y = ($x + 1); $y < $numTokens; $y++) {
398                        if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$finalTokens[$y]['code']]) === false) {
399                            break;
400                        }
401                    }
402
403                    if ($finalTokens[$y]['code'] === T_CLOSE_PARENTHESIS) {
404                        continue;
405                    }
406
407                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
408                        for ($i = ($stackPtr + 1); $i <= $y; $i++) {
409                            $type    = $finalTokens[$i]['type'];
410                            $content = PHP_CodeSniffer::prepareForOutput($finalTokens[$i]['content']);
411                            echo "\tProcess token $i: $type => $content".PHP_EOL;
412                        }
413
414                        echo "\t\t* token starts a URL *".PHP_EOL;
415                    }
416
417                    // Join all the content together inside the url() statement.
418                    $newContent = '';
419                    for ($i = ($x + 2); $i < $numTokens; $i++) {
420                        if ($finalTokens[$i]['code'] === T_CLOSE_PARENTHESIS) {
421                            break;
422                        }
423
424                        $newContent .= $finalTokens[$i]['content'];
425                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
426                            $content = PHP_CodeSniffer::prepareForOutput($finalTokens[$i]['content']);
427                            echo "\t\t=> token $i added to URL string and ignored: $content".PHP_EOL;
428                        }
429
430                        unset($finalTokens[$i]);
431                    }
432
433                    $stackPtr = $i;
434
435                    // If the content inside the "url()" is in double quotes
436                    // there will only be one token and so we don't have to do
437                    // anything except change its type. If it is not empty,
438                    // we need to do some token merging.
439                    $finalTokens[($x + 1)]['type'] = 'T_URL';
440                    $finalTokens[($x + 1)]['code'] = T_URL;
441
442                    if ($newContent !== '') {
443                        $finalTokens[($x + 1)]['content'] .= $newContent;
444                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
445                            $content = PHP_CodeSniffer::prepareForOutput($finalTokens[($x + 1)]['content']);
446                            echo "\t\t=> token content changed to: $content".PHP_EOL;
447                        }
448                    }
449                } else if ($finalTokens[$stackPtr]['content'][0] === '-'
450                    && $finalTokens[($stackPtr + 1)]['code'] === T_STRING
451                ) {
452                    if (isset($finalTokens[($stackPtr - 1)]) === true
453                        && $finalTokens[($stackPtr - 1)]['code'] === T_STRING
454                    ) {
455                        $newContent = $finalTokens[($stackPtr - 1)]['content'].$finalTokens[$stackPtr]['content'].$finalTokens[($stackPtr + 1)]['content'];
456
457                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
458                            echo "\t\t* token is a string joiner; ignoring this and previous token".PHP_EOL;
459                            $old = PHP_CodeSniffer::prepareForOutput($finalTokens[($stackPtr + 1)]['content']);
460                            $new = PHP_CodeSniffer::prepareForOutput($newContent);
461                            echo "\t\t=> token ".($stackPtr + 1)." content changed from \"$old\" to \"$new\"".PHP_EOL;
462                        }
463
464                        $finalTokens[($stackPtr + 1)]['content'] = $newContent;
465                        unset($finalTokens[$stackPtr]);
466                        unset($finalTokens[($stackPtr - 1)]);
467                    } else {
468                        $newContent = $finalTokens[$stackPtr]['content'].$finalTokens[($stackPtr + 1)]['content'];
469
470                        $finalTokens[($stackPtr + 1)]['content'] = $newContent;
471                        unset($finalTokens[$stackPtr]);
472                    }
473                }//end if
474
475                break;
476            case T_ASPERAND:
477                $asperandStart = true;
478                if (PHP_CODESNIFFER_VERBOSITY > 1) {
479                    echo "\t\t* at-rule definition opened *".PHP_EOL;
480                }
481                break;
482            default:
483                // Nothing special to be done with this token.
484                break;
485            }//end switch
486        }//end for
487
488        // Reset the array keys to avoid gaps.
489        $finalTokens = array_values($finalTokens);
490        $numTokens   = count($finalTokens);
491
492        // Blank out the content of the end tag.
493        $finalTokens[($numTokens - 1)]['content'] = '';
494
495        if ($eolAdded === true) {
496            // Strip off the extra EOL char we added for tokenizing.
497            $finalTokens[($numTokens - 2)]['content'] = substr(
498                $finalTokens[($numTokens - 2)]['content'],
499                0,
500                (strlen($eolChar) * -1)
501            );
502
503            if ($finalTokens[($numTokens - 2)]['content'] === '') {
504                unset($finalTokens[($numTokens - 2)]);
505                $finalTokens = array_values($finalTokens);
506                $numTokens   = count($finalTokens);
507            }
508        }
509
510        if (PHP_CODESNIFFER_VERBOSITY > 1) {
511            echo "\t*** END CSS TOKENIZING 2ND PASS ***".PHP_EOL;
512        }
513
514        return $finalTokens;
515
516    }//end tokenizeString()
517
518
519    /**
520     * Performs additional processing after main tokenizing.
521     *
522     * @param array  $tokens  The array of tokens to process.
523     * @param string $eolChar The EOL character to use for splitting strings.
524     *
525     * @return void
526     */
527    public function processAdditional(&$tokens, $eolChar)
528    {
529        /*
530            We override this method because we don't want the PHP version to
531            run during CSS processing because it is wasted processing time.
532        */
533
534    }//end processAdditional()
535
536
537}//end class
538