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