[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 3 require_once( dirname(__FILE__).'/Cache.php'); 4 5 /** 6 * Class for parsing and compiling less files into css 7 * 8 * @package Less 9 * @subpackage parser 10 * 11 */ 12 class Less_Parser{ 13 14 15 /** 16 * Default parser options 17 */ 18 public static $default_options = array( 19 'compress' => false, // option - whether to compress 20 'strictUnits' => false, // whether units need to evaluate correctly 21 'strictMath' => false, // whether math has to be within parenthesis 22 'relativeUrls' => true, // option - whether to adjust URL's to be relative 23 'urlArgs' => '', // whether to add args into url tokens 24 'numPrecision' => 8, 25 26 'import_dirs' => array(), 27 'import_callback' => null, 28 'cache_dir' => null, 29 'cache_method' => 'php', // false, 'serialize', 'php', 'var_export', 'callback'; 30 'cache_callback_get' => null, 31 'cache_callback_set' => null, 32 33 'sourceMap' => false, // whether to output a source map 34 'sourceMapBasepath' => null, 35 'sourceMapWriteTo' => null, 36 'sourceMapURL' => null, 37 38 'indentation' => ' ', 39 40 'plugins' => array(), 41 42 ); 43 44 public static $options = array(); 45 46 47 private $input; // Less input string 48 private $input_len; // input string length 49 private $pos; // current index in `input` 50 private $saveStack = array(); // holds state for backtracking 51 private $furthest; 52 private $mb_internal_encoding = ''; // for remember exists value of mbstring.internal_encoding 53 54 /** 55 * @var Less_Environment 56 */ 57 private $env; 58 59 protected $rules = array(); 60 61 private static $imports = array(); 62 63 public static $has_extends = false; 64 65 public static $next_id = 0; 66 67 /** 68 * Filename to contents of all parsed the files 69 * 70 * @var array 71 */ 72 public static $contentsMap = array(); 73 74 75 /** 76 * @param Less_Environment|array|null $env 77 */ 78 public function __construct( $env = null ){ 79 80 // Top parser on an import tree must be sure there is one "env" 81 // which will then be passed around by reference. 82 if( $env instanceof Less_Environment ){ 83 $this->env = $env; 84 }else{ 85 $this->SetOptions(Less_Parser::$default_options); 86 $this->Reset( $env ); 87 } 88 89 // mbstring.func_overload > 1 bugfix 90 // The encoding value must be set for each source file, 91 // therefore, to conserve resources and improve the speed of this design is taken here 92 if (ini_get('mbstring.func_overload')) { 93 $this->mb_internal_encoding = ini_get('mbstring.internal_encoding'); 94 @ini_set('mbstring.internal_encoding', 'ascii'); 95 } 96 97 } 98 99 100 /** 101 * Reset the parser state completely 102 * 103 */ 104 public function Reset( $options = null ){ 105 $this->rules = array(); 106 self::$imports = array(); 107 self::$has_extends = false; 108 self::$imports = array(); 109 self::$contentsMap = array(); 110 111 $this->env = new Less_Environment($options); 112 $this->env->Init(); 113 114 //set new options 115 if( is_array($options) ){ 116 $this->SetOptions(Less_Parser::$default_options); 117 $this->SetOptions($options); 118 } 119 } 120 121 /** 122 * Set one or more compiler options 123 * options: import_dirs, cache_dir, cache_method 124 * 125 */ 126 public function SetOptions( $options ){ 127 foreach($options as $option => $value){ 128 $this->SetOption($option,$value); 129 } 130 } 131 132 /** 133 * Set one compiler option 134 * 135 */ 136 public function SetOption($option,$value){ 137 138 switch($option){ 139 140 case 'import_dirs': 141 $this->SetImportDirs($value); 142 return; 143 144 case 'cache_dir': 145 if( is_string($value) ){ 146 Less_Cache::SetCacheDir($value); 147 Less_Cache::CheckCacheDir(); 148 } 149 return; 150 } 151 152 Less_Parser::$options[$option] = $value; 153 } 154 155 /** 156 * Registers a new custom function 157 * 158 * @param string $name function name 159 * @param callable $callback callback 160 */ 161 public function registerFunction($name, $callback) { 162 $this->env->functions[$name] = $callback; 163 } 164 165 /** 166 * Removed an already registered function 167 * 168 * @param string $name function name 169 */ 170 public function unregisterFunction($name) { 171 if( isset($this->env->functions[$name]) ) 172 unset($this->env->functions[$name]); 173 } 174 175 176 /** 177 * Get the current css buffer 178 * 179 * @return string 180 */ 181 public function getCss(){ 182 183 $precision = ini_get('precision'); 184 @ini_set('precision',16); 185 $locale = setlocale(LC_NUMERIC, 0); 186 setlocale(LC_NUMERIC, "C"); 187 188 try { 189 190 $root = new Less_Tree_Ruleset(array(), $this->rules ); 191 $root->root = true; 192 $root->firstRoot = true; 193 194 195 $this->PreVisitors($root); 196 197 self::$has_extends = false; 198 $evaldRoot = $root->compile($this->env); 199 200 201 202 $this->PostVisitors($evaldRoot); 203 204 if( Less_Parser::$options['sourceMap'] ){ 205 $generator = new Less_SourceMap_Generator($evaldRoot, Less_Parser::$contentsMap, Less_Parser::$options ); 206 // will also save file 207 // FIXME: should happen somewhere else? 208 $css = $generator->generateCSS(); 209 }else{ 210 $css = $evaldRoot->toCSS(); 211 } 212 213 if( Less_Parser::$options['compress'] ){ 214 $css = preg_replace('/(^(\s)+)|((\s)+$)/', '', $css); 215 } 216 217 } catch (Exception $exc) { 218 // Intentional fall-through so we can reset environment 219 } 220 221 //reset php settings 222 @ini_set('precision',$precision); 223 setlocale(LC_NUMERIC, $locale); 224 225 // If you previously defined $this->mb_internal_encoding 226 // is required to return the encoding as it was before 227 if ($this->mb_internal_encoding != '') { 228 @ini_set("mbstring.internal_encoding", $this->mb_internal_encoding); 229 $this->mb_internal_encoding = ''; 230 } 231 232 // Rethrow exception after we handled resetting the environment 233 if (!empty($exc)) { 234 throw $exc; 235 } 236 237 238 239 return $css; 240 } 241 242 /** 243 * Run pre-compile visitors 244 * 245 */ 246 private function PreVisitors($root){ 247 248 if( Less_Parser::$options['plugins'] ){ 249 foreach(Less_Parser::$options['plugins'] as $plugin){ 250 if( !empty($plugin->isPreEvalVisitor) ){ 251 $plugin->run($root); 252 } 253 } 254 } 255 } 256 257 258 /** 259 * Run post-compile visitors 260 * 261 */ 262 private function PostVisitors($evaldRoot){ 263 264 $visitors = array(); 265 $visitors[] = new Less_Visitor_joinSelector(); 266 if( self::$has_extends ){ 267 $visitors[] = new Less_Visitor_processExtends(); 268 } 269 $visitors[] = new Less_Visitor_toCSS(); 270 271 272 if( Less_Parser::$options['plugins'] ){ 273 foreach(Less_Parser::$options['plugins'] as $plugin){ 274 if( property_exists($plugin,'isPreEvalVisitor') && $plugin->isPreEvalVisitor ){ 275 continue; 276 } 277 278 if( property_exists($plugin,'isPreVisitor') && $plugin->isPreVisitor ){ 279 array_unshift( $visitors, $plugin); 280 }else{ 281 $visitors[] = $plugin; 282 } 283 } 284 } 285 286 287 for($i = 0; $i < count($visitors); $i++ ){ 288 $visitors[$i]->run($evaldRoot); 289 } 290 291 } 292 293 294 /** 295 * Parse a Less string into css 296 * 297 * @param string $str The string to convert 298 * @param string $uri_root The url of the file 299 * @return Less_Tree_Ruleset|Less_Parser 300 */ 301 public function parse( $str, $file_uri = null ){ 302 303 if( !$file_uri ){ 304 $uri_root = ''; 305 $filename = 'anonymous-file-'.Less_Parser::$next_id++.'.less'; 306 }else{ 307 $file_uri = self::WinPath($file_uri); 308 $filename = $file_uri; 309 $uri_root = dirname($file_uri); 310 } 311 312 $previousFileInfo = $this->env->currentFileInfo; 313 $uri_root = self::WinPath($uri_root); 314 $this->SetFileInfo($filename, $uri_root); 315 316 $this->input = $str; 317 $this->_parse(); 318 319 if( $previousFileInfo ){ 320 $this->env->currentFileInfo = $previousFileInfo; 321 } 322 323 return $this; 324 } 325 326 327 /** 328 * Parse a Less string from a given file 329 * 330 * @throws Less_Exception_Parser 331 * @param string $filename The file to parse 332 * @param string $uri_root The url of the file 333 * @param bool $returnRoot Indicates whether the return value should be a css string a root node 334 * @return Less_Tree_Ruleset|Less_Parser 335 */ 336 public function parseFile( $filename, $uri_root = '', $returnRoot = false){ 337 338 if( !file_exists($filename) ){ 339 $this->Error(sprintf('File `%s` not found.', $filename)); 340 } 341 342 343 // fix uri_root? 344 // Instead of The mixture of file path for the first argument and directory path for the second argument has bee 345 if( !$returnRoot && !empty($uri_root) && basename($uri_root) == basename($filename) ){ 346 $uri_root = dirname($uri_root); 347 } 348 349 350 $previousFileInfo = $this->env->currentFileInfo; 351 352 353 if( $filename ){ 354 $filename = self::WinPath(realpath($filename)); 355 } 356 $uri_root = self::WinPath($uri_root); 357 358 $this->SetFileInfo($filename, $uri_root); 359 360 self::AddParsedFile($filename); 361 362 if( $returnRoot ){ 363 $rules = $this->GetRules( $filename ); 364 $return = new Less_Tree_Ruleset(array(), $rules ); 365 }else{ 366 $this->_parse( $filename ); 367 $return = $this; 368 } 369 370 if( $previousFileInfo ){ 371 $this->env->currentFileInfo = $previousFileInfo; 372 } 373 374 return $return; 375 } 376 377 378 /** 379 * Allows a user to set variables values 380 * @param array $vars 381 * @return Less_Parser 382 */ 383 public function ModifyVars( $vars ){ 384 385 $this->input = Less_Parser::serializeVars( $vars ); 386 $this->_parse(); 387 388 return $this; 389 } 390 391 392 /** 393 * @param string $filename 394 */ 395 public function SetFileInfo( $filename, $uri_root = ''){ 396 397 $filename = Less_Environment::normalizePath($filename); 398 $dirname = preg_replace('/[^\/\\\\]*$/','',$filename); 399 400 if( !empty($uri_root) ){ 401 $uri_root = rtrim($uri_root,'/').'/'; 402 } 403 404 $currentFileInfo = array(); 405 406 //entry info 407 if( isset($this->env->currentFileInfo) ){ 408 $currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath']; 409 $currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri']; 410 $currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath']; 411 412 }else{ 413 $currentFileInfo['entryPath'] = $dirname; 414 $currentFileInfo['entryUri'] = $uri_root; 415 $currentFileInfo['rootpath'] = $dirname; 416 } 417 418 $currentFileInfo['currentDirectory'] = $dirname; 419 $currentFileInfo['currentUri'] = $uri_root.basename($filename); 420 $currentFileInfo['filename'] = $filename; 421 $currentFileInfo['uri_root'] = $uri_root; 422 423 424 //inherit reference 425 if( isset($this->env->currentFileInfo['reference']) && $this->env->currentFileInfo['reference'] ){ 426 $currentFileInfo['reference'] = true; 427 } 428 429 $this->env->currentFileInfo = $currentFileInfo; 430 } 431 432 433 /** 434 * @deprecated 1.5.1.2 435 * 436 */ 437 public function SetCacheDir( $dir ){ 438 439 if( !file_exists($dir) ){ 440 if( mkdir($dir) ){ 441 return true; 442 } 443 throw new Less_Exception_Parser('Less.php cache directory couldn\'t be created: '.$dir); 444 445 }elseif( !is_dir($dir) ){ 446 throw new Less_Exception_Parser('Less.php cache directory doesn\'t exist: '.$dir); 447 448 }elseif( !is_writable($dir) ){ 449 throw new Less_Exception_Parser('Less.php cache directory isn\'t writable: '.$dir); 450 451 }else{ 452 $dir = self::WinPath($dir); 453 Less_Cache::$cache_dir = rtrim($dir,'/').'/'; 454 return true; 455 } 456 } 457 458 459 /** 460 * Set a list of directories or callbacks the parser should use for determining import paths 461 * 462 * @param array $dirs 463 */ 464 public function SetImportDirs( $dirs ){ 465 Less_Parser::$options['import_dirs'] = array(); 466 467 foreach($dirs as $path => $uri_root){ 468 469 $path = self::WinPath($path); 470 if( !empty($path) ){ 471 $path = rtrim($path,'/').'/'; 472 } 473 474 if ( !is_callable($uri_root) ){ 475 $uri_root = self::WinPath($uri_root); 476 if( !empty($uri_root) ){ 477 $uri_root = rtrim($uri_root,'/').'/'; 478 } 479 } 480 481 Less_Parser::$options['import_dirs'][$path] = $uri_root; 482 } 483 } 484 485 /** 486 * @param string $file_path 487 */ 488 private function _parse( $file_path = null ){ 489 $this->rules = array_merge($this->rules, $this->GetRules( $file_path )); 490 } 491 492 493 /** 494 * Return the results of parsePrimary for $file_path 495 * Use cache and save cached results if possible 496 * 497 * @param string|null $file_path 498 */ 499 private function GetRules( $file_path ){ 500 501 $this->SetInput($file_path); 502 503 $cache_file = $this->CacheFile( $file_path ); 504 if( $cache_file ){ 505 if( Less_Parser::$options['cache_method'] == 'callback' ){ 506 if( is_callable(Less_Parser::$options['cache_callback_get']) ){ 507 $cache = call_user_func_array( 508 Less_Parser::$options['cache_callback_get'], 509 array($this, $file_path, $cache_file) 510 ); 511 512 if( $cache ){ 513 $this->UnsetInput(); 514 return $cache; 515 } 516 } 517 518 }elseif( file_exists($cache_file) ){ 519 switch(Less_Parser::$options['cache_method']){ 520 521 // Using serialize 522 // Faster but uses more memory 523 case 'serialize': 524 $cache = unserialize(file_get_contents($cache_file)); 525 if( $cache ){ 526 touch($cache_file); 527 $this->UnsetInput(); 528 return $cache; 529 } 530 break; 531 532 533 // Using generated php code 534 case 'var_export': 535 case 'php': 536 $this->UnsetInput(); 537 return include($cache_file); 538 } 539 } 540 } 541 542 $rules = $this->parsePrimary(); 543 544 if( $this->pos < $this->input_len ){ 545 throw new Less_Exception_Chunk($this->input, null, $this->furthest, $this->env->currentFileInfo); 546 } 547 548 $this->UnsetInput(); 549 550 551 //save the cache 552 if( $cache_file ){ 553 if( Less_Parser::$options['cache_method'] == 'callback' ){ 554 if( is_callable(Less_Parser::$options['cache_callback_set']) ){ 555 call_user_func_array( 556 Less_Parser::$options['cache_callback_set'], 557 array($this, $file_path, $cache_file, $rules) 558 ); 559 } 560 561 }else{ 562 //msg('write cache file'); 563 switch(Less_Parser::$options['cache_method']){ 564 case 'serialize': 565 file_put_contents( $cache_file, serialize($rules) ); 566 break; 567 case 'php': 568 file_put_contents( $cache_file, '<?php return '.self::ArgString($rules).'; ?>' ); 569 break; 570 case 'var_export': 571 //Requires __set_state() 572 file_put_contents( $cache_file, '<?php return '.var_export($rules,true).'; ?>' ); 573 break; 574 } 575 576 Less_Cache::CleanCache(); 577 } 578 } 579 580 return $rules; 581 } 582 583 584 /** 585 * Set up the input buffer 586 * 587 */ 588 public function SetInput( $file_path ){ 589 590 if( $file_path ){ 591 $this->input = file_get_contents( $file_path ); 592 } 593 594 $this->pos = $this->furthest = 0; 595 596 // Remove potential UTF Byte Order Mark 597 $this->input = preg_replace('/\\G\xEF\xBB\xBF/', '', $this->input); 598 $this->input_len = strlen($this->input); 599 600 601 if( Less_Parser::$options['sourceMap'] && $this->env->currentFileInfo ){ 602 $uri = $this->env->currentFileInfo['currentUri']; 603 Less_Parser::$contentsMap[$uri] = $this->input; 604 } 605 606 } 607 608 609 /** 610 * Free up some memory 611 * 612 */ 613 public function UnsetInput(){ 614 unset($this->input, $this->pos, $this->input_len, $this->furthest); 615 $this->saveStack = array(); 616 } 617 618 619 public function CacheFile( $file_path ){ 620 621 if( $file_path && $this->CacheEnabled() ){ 622 623 $env = get_object_vars($this->env); 624 unset($env['frames']); 625 626 $parts = array(); 627 $parts[] = $file_path; 628 $parts[] = filesize( $file_path ); 629 $parts[] = filemtime( $file_path ); 630 $parts[] = $env; 631 $parts[] = Less_Version::cache_version; 632 $parts[] = Less_Parser::$options['cache_method']; 633 return Less_Cache::$cache_dir . Less_Cache::$prefix . base_convert( sha1(json_encode($parts) ), 16, 36) . '.lesscache'; 634 } 635 } 636 637 638 static function AddParsedFile($file){ 639 self::$imports[] = $file; 640 } 641 642 static function AllParsedFiles(){ 643 return self::$imports; 644 } 645 646 /** 647 * @param string $file 648 */ 649 static function FileParsed($file){ 650 return in_array($file,self::$imports); 651 } 652 653 654 function save() { 655 $this->saveStack[] = $this->pos; 656 } 657 658 private function restore() { 659 $this->pos = array_pop($this->saveStack); 660 } 661 662 private function forget(){ 663 array_pop($this->saveStack); 664 } 665 666 667 private function isWhitespace($offset = 0) { 668 return preg_match('/\s/',$this->input[ $this->pos + $offset]); 669 } 670 671 /** 672 * Parse from a token, regexp or string, and move forward if match 673 * 674 * @param array $toks 675 * @return array 676 */ 677 private function match($toks){ 678 679 // The match is confirmed, add the match length to `this::pos`, 680 // and consume any extra white-space characters (' ' || '\n') 681 // which come after that. The reason for this is that LeSS's 682 // grammar is mostly white-space insensitive. 683 // 684 685 foreach($toks as $tok){ 686 687 $char = $tok[0]; 688 689 if( $char === '/' ){ 690 $match = $this->MatchReg($tok); 691 692 if( $match ){ 693 return count($match) === 1 ? $match[0] : $match; 694 } 695 696 }elseif( $char === '#' ){ 697 $match = $this->MatchChar($tok[1]); 698 699 }else{ 700 // Non-terminal, match using a function call 701 $match = $this->$tok(); 702 703 } 704 705 if( $match ){ 706 return $match; 707 } 708 } 709 } 710 711 /** 712 * @param string[] $toks 713 * 714 * @return string 715 */ 716 private function MatchFuncs($toks){ 717 718 if( $this->pos < $this->input_len ){ 719 foreach($toks as $tok){ 720 $match = $this->$tok(); 721 if( $match ){ 722 return $match; 723 } 724 } 725 } 726 727 } 728 729 // Match a single character in the input, 730 private function MatchChar($tok){ 731 if( ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok) ){ 732 $this->skipWhitespace(1); 733 return $tok; 734 } 735 } 736 737 // Match a regexp from the current start point 738 private function MatchReg($tok){ 739 740 if( preg_match($tok, $this->input, $match, 0, $this->pos) ){ 741 $this->skipWhitespace(strlen($match[0])); 742 return $match; 743 } 744 } 745 746 747 /** 748 * Same as match(), but don't change the state of the parser, 749 * just return the match. 750 * 751 * @param string $tok 752 * @return integer 753 */ 754 public function PeekReg($tok){ 755 return preg_match($tok, $this->input, $match, 0, $this->pos); 756 } 757 758 /** 759 * @param string $tok 760 */ 761 public function PeekChar($tok){ 762 //return ($this->input[$this->pos] === $tok ); 763 return ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok ); 764 } 765 766 767 /** 768 * @param integer $length 769 */ 770 public function skipWhitespace($length){ 771 772 $this->pos += $length; 773 774 for(; $this->pos < $this->input_len; $this->pos++ ){ 775 $c = $this->input[$this->pos]; 776 777 if( ($c !== "\n") && ($c !== "\r") && ($c !== "\t") && ($c !== ' ') ){ 778 break; 779 } 780 } 781 } 782 783 784 /** 785 * @param string $tok 786 * @param string|null $msg 787 */ 788 public function expect($tok, $msg = NULL) { 789 $result = $this->match( array($tok) ); 790 if (!$result) { 791 $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg ); 792 } else { 793 return $result; 794 } 795 } 796 797 /** 798 * @param string $tok 799 */ 800 public function expectChar($tok, $msg = null ){ 801 $result = $this->MatchChar($tok); 802 if( !$result ){ 803 $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg ); 804 }else{ 805 return $result; 806 } 807 } 808 809 // 810 // Here in, the parsing rules/functions 811 // 812 // The basic structure of the syntax tree generated is as follows: 813 // 814 // Ruleset -> Rule -> Value -> Expression -> Entity 815 // 816 // Here's some LESS code: 817 // 818 // .class { 819 // color: #fff; 820 // border: 1px solid #000; 821 // width: @w + 4px; 822 // > .child {...} 823 // } 824 // 825 // And here's what the parse tree might look like: 826 // 827 // Ruleset (Selector '.class', [ 828 // Rule ("color", Value ([Expression [Color #fff]])) 829 // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]])) 830 // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]])) 831 // Ruleset (Selector [Element '>', '.child'], [...]) 832 // ]) 833 // 834 // In general, most rules will try to parse a token with the `$()` function, and if the return 835 // value is truly, will return a new node, of the relevant type. Sometimes, we need to check 836 // first, before parsing, that's when we use `peek()`. 837 // 838 839 // 840 // The `primary` rule is the *entry* and *exit* point of the parser. 841 // The rules here can appear at any level of the parse tree. 842 // 843 // The recursive nature of the grammar is an interplay between the `block` 844 // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, 845 // as represented by this simplified grammar: 846 // 847 // primary → (ruleset | rule)+ 848 // ruleset → selector+ block 849 // block → '{' primary '}' 850 // 851 // Only at one point is the primary rule not called from the 852 // block rule: at the root level. 853 // 854 private function parsePrimary(){ 855 $root = array(); 856 857 while( true ){ 858 859 if( $this->pos >= $this->input_len ){ 860 break; 861 } 862 863 $node = $this->parseExtend(true); 864 if( $node ){ 865 $root = array_merge($root,$node); 866 continue; 867 } 868 869 //$node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseDirective')); 870 $node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseNameValue', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseRulesetCall', 'parseDirective')); 871 872 if( $node ){ 873 $root[] = $node; 874 }elseif( !$this->MatchReg('/\\G[\s\n;]+/') ){ 875 break; 876 } 877 878 if( $this->PeekChar('}') ){ 879 break; 880 } 881 } 882 883 return $root; 884 } 885 886 887 888 // We create a Comment node for CSS comments `/* */`, 889 // but keep the LeSS comments `//` silent, by just skipping 890 // over them. 891 private function parseComment(){ 892 893 if( $this->input[$this->pos] !== '/' ){ 894 return; 895 } 896 897 if( $this->input[$this->pos+1] === '/' ){ 898 $match = $this->MatchReg('/\\G\/\/.*/'); 899 return $this->NewObj4('Less_Tree_Comment',array($match[0], true, $this->pos, $this->env->currentFileInfo)); 900 } 901 902 //$comment = $this->MatchReg('/\\G\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/'); 903 $comment = $this->MatchReg('/\\G\/\*(?s).*?\*+\/\n?/');//not the same as less.js to prevent fatal errors 904 if( $comment ){ 905 return $this->NewObj4('Less_Tree_Comment',array($comment[0], false, $this->pos, $this->env->currentFileInfo)); 906 } 907 } 908 909 private function parseComments(){ 910 $comments = array(); 911 912 while( $this->pos < $this->input_len ){ 913 $comment = $this->parseComment(); 914 if( !$comment ){ 915 break; 916 } 917 918 $comments[] = $comment; 919 } 920 921 return $comments; 922 } 923 924 925 926 // 927 // A string, which supports escaping " and ' 928 // 929 // "milky way" 'he\'s the one!' 930 // 931 private function parseEntitiesQuoted() { 932 $j = $this->pos; 933 $e = false; 934 $index = $this->pos; 935 936 if( $this->input[$this->pos] === '~' ){ 937 $j++; 938 $e = true; // Escaped strings 939 } 940 941 if( $this->input[$j] != '"' && $this->input[$j] !== "'" ){ 942 return; 943 } 944 945 if ($e) { 946 $this->MatchChar('~'); 947 } 948 949 // Fix for #124: match escaped newlines 950 //$str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.)*)"|\'((?:[^\'\\\\\r\n]|\\\\.)*)\'/'); 951 $str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)"|\'((?:[^\'\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)\'/'); 952 953 if( $str ){ 954 $result = $str[0][0] == '"' ? $str[1] : $str[2]; 955 return $this->NewObj5('Less_Tree_Quoted',array($str[0], $result, $e, $index, $this->env->currentFileInfo) ); 956 } 957 return; 958 } 959 960 961 // 962 // A catch-all word, such as: 963 // 964 // black border-collapse 965 // 966 private function parseEntitiesKeyword(){ 967 968 //$k = $this->MatchReg('/\\G[_A-Za-z-][_A-Za-z0-9-]*/'); 969 $k = $this->MatchReg('/\\G%|\\G[_A-Za-z-][_A-Za-z0-9-]*/'); 970 if( $k ){ 971 $k = $k[0]; 972 $color = $this->fromKeyword($k); 973 if( $color ){ 974 return $color; 975 } 976 return $this->NewObj1('Less_Tree_Keyword',$k); 977 } 978 } 979 980 // duplicate of Less_Tree_Color::FromKeyword 981 private function FromKeyword( $keyword ){ 982 $keyword = strtolower($keyword); 983 984 if( Less_Colors::hasOwnProperty($keyword) ){ 985 // detect named color 986 return $this->NewObj1('Less_Tree_Color',substr(Less_Colors::color($keyword), 1)); 987 } 988 989 if( $keyword === 'transparent' ){ 990 return $this->NewObj3('Less_Tree_Color', array( array(0, 0, 0), 0, true)); 991 } 992 } 993 994 // 995 // A function call 996 // 997 // rgb(255, 0, 255) 998 // 999 // We also try to catch IE's `alpha()`, but let the `alpha` parser 1000 // deal with the details. 1001 // 1002 // The arguments are parsed with the `entities.arguments` parser. 1003 // 1004 private function parseEntitiesCall(){ 1005 $index = $this->pos; 1006 1007 if( !preg_match('/\\G([\w-]+|%|progid:[\w\.]+)\(/', $this->input, $name,0,$this->pos) ){ 1008 return; 1009 } 1010 $name = $name[1]; 1011 $nameLC = strtolower($name); 1012 1013 if ($nameLC === 'url') { 1014 return null; 1015 } 1016 1017 $this->pos += strlen($name); 1018 1019 if( $nameLC === 'alpha' ){ 1020 $alpha_ret = $this->parseAlpha(); 1021 if( $alpha_ret ){ 1022 return $alpha_ret; 1023 } 1024 } 1025 1026 $this->MatchChar('('); // Parse the '(' and consume whitespace. 1027 1028 $args = $this->parseEntitiesArguments(); 1029 1030 if( !$this->MatchChar(')') ){ 1031 return; 1032 } 1033 1034 if ($name) { 1035 return $this->NewObj4('Less_Tree_Call',array($name, $args, $index, $this->env->currentFileInfo) ); 1036 } 1037 } 1038 1039 /** 1040 * Parse a list of arguments 1041 * 1042 * @return array 1043 */ 1044 private function parseEntitiesArguments(){ 1045 1046 $args = array(); 1047 while( true ){ 1048 $arg = $this->MatchFuncs( array('parseEntitiesAssignment','parseExpression') ); 1049 if( !$arg ){ 1050 break; 1051 } 1052 1053 $args[] = $arg; 1054 if( !$this->MatchChar(',') ){ 1055 break; 1056 } 1057 } 1058 return $args; 1059 } 1060 1061 private function parseEntitiesLiteral(){ 1062 return $this->MatchFuncs( array('parseEntitiesDimension','parseEntitiesColor','parseEntitiesQuoted','parseUnicodeDescriptor') ); 1063 } 1064 1065 // Assignments are argument entities for calls. 1066 // They are present in ie filter properties as shown below. 1067 // 1068 // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) 1069 // 1070 private function parseEntitiesAssignment() { 1071 1072 $key = $this->MatchReg('/\\G\w+(?=\s?=)/'); 1073 if( !$key ){ 1074 return; 1075 } 1076 1077 if( !$this->MatchChar('=') ){ 1078 return; 1079 } 1080 1081 $value = $this->parseEntity(); 1082 if( $value ){ 1083 return $this->NewObj2('Less_Tree_Assignment',array($key[0], $value)); 1084 } 1085 } 1086 1087 // 1088 // Parse url() tokens 1089 // 1090 // We use a specific rule for urls, because they don't really behave like 1091 // standard function calls. The difference is that the argument doesn't have 1092 // to be enclosed within a string, so it can't be parsed as an Expression. 1093 // 1094 private function parseEntitiesUrl(){ 1095 1096 1097 if( $this->input[$this->pos] !== 'u' || !$this->matchReg('/\\Gurl\(/') ){ 1098 return; 1099 } 1100 1101 $value = $this->match( array('parseEntitiesQuoted','parseEntitiesVariable','/\\Gdata\:.*?[^\)]+/','/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/') ); 1102 if( !$value ){ 1103 $value = ''; 1104 } 1105 1106 1107 $this->expectChar(')'); 1108 1109 1110 if( isset($value->value) || $value instanceof Less_Tree_Variable ){ 1111 return $this->NewObj2('Less_Tree_Url',array($value, $this->env->currentFileInfo)); 1112 } 1113 1114 return $this->NewObj2('Less_Tree_Url', array( $this->NewObj1('Less_Tree_Anonymous',$value), $this->env->currentFileInfo) ); 1115 } 1116 1117 1118 // 1119 // A Variable entity, such as `@fink`, in 1120 // 1121 // width: @fink + 2px 1122 // 1123 // We use a different parser for variable definitions, 1124 // see `parsers.variable`. 1125 // 1126 private function parseEntitiesVariable(){ 1127 $index = $this->pos; 1128 if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G@@?[\w-]+/'))) { 1129 return $this->NewObj3('Less_Tree_Variable', array( $name[0], $index, $this->env->currentFileInfo)); 1130 } 1131 } 1132 1133 1134 // A variable entity useing the protective {} e.g. @{var} 1135 private function parseEntitiesVariableCurly() { 1136 $index = $this->pos; 1137 1138 if( $this->input_len > ($this->pos+1) && $this->input[$this->pos] === '@' && ($curly = $this->MatchReg('/\\G@\{([\w-]+)\}/')) ){ 1139 return $this->NewObj3('Less_Tree_Variable',array('@'.$curly[1], $index, $this->env->currentFileInfo)); 1140 } 1141 } 1142 1143 // 1144 // A Hexadecimal color 1145 // 1146 // #4F3C2F 1147 // 1148 // `rgb` and `hsl` colors are parsed through the `entities.call` parser. 1149 // 1150 private function parseEntitiesColor(){ 1151 if ($this->PeekChar('#') && ($rgb = $this->MatchReg('/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/'))) { 1152 return $this->NewObj1('Less_Tree_Color',$rgb[1]); 1153 } 1154 } 1155 1156 // 1157 // A Dimension, that is, a number and a unit 1158 // 1159 // 0.5em 95% 1160 // 1161 private function parseEntitiesDimension(){ 1162 1163 $c = @ord($this->input[$this->pos]); 1164 1165 //Is the first char of the dimension 0-9, '.', '+' or '-' 1166 if (($c > 57 || $c < 43) || $c === 47 || $c == 44){ 1167 return; 1168 } 1169 1170 $value = $this->MatchReg('/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/'); 1171 if( $value ){ 1172 1173 if( isset($value[2]) ){ 1174 return $this->NewObj2('Less_Tree_Dimension', array($value[1],$value[2])); 1175 } 1176 return $this->NewObj1('Less_Tree_Dimension',$value[1]); 1177 } 1178 } 1179 1180 1181 // 1182 // A unicode descriptor, as is used in unicode-range 1183 // 1184 // U+0?? or U+00A1-00A9 1185 // 1186 function parseUnicodeDescriptor() { 1187 $ud = $this->MatchReg('/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/'); 1188 if( $ud ){ 1189 return $this->NewObj1('Less_Tree_UnicodeDescriptor', $ud[0]); 1190 } 1191 } 1192 1193 1194 // 1195 // JavaScript code to be evaluated 1196 // 1197 // `window.location.href` 1198 // 1199 private function parseEntitiesJavascript(){ 1200 $e = false; 1201 $j = $this->pos; 1202 if( $this->input[$j] === '~' ){ 1203 $j++; 1204 $e = true; 1205 } 1206 if( $this->input[$j] !== '`' ){ 1207 return; 1208 } 1209 if( $e ){ 1210 $this->MatchChar('~'); 1211 } 1212 $str = $this->MatchReg('/\\G`([^`]*)`/'); 1213 if( $str ){ 1214 return $this->NewObj3('Less_Tree_Javascript', array($str[1], $this->pos, $e)); 1215 } 1216 } 1217 1218 1219 // 1220 // The variable part of a variable definition. Used in the `rule` parser 1221 // 1222 // @fink: 1223 // 1224 private function parseVariable(){ 1225 if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*:/'))) { 1226 return $name[1]; 1227 } 1228 } 1229 1230 1231 // 1232 // The variable part of a variable definition. Used in the `rule` parser 1233 // 1234 // @fink(); 1235 // 1236 private function parseRulesetCall(){ 1237 1238 if( $this->input[$this->pos] === '@' && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*\(\s*\)\s*;/')) ){ 1239 return $this->NewObj1('Less_Tree_RulesetCall', $name[1] ); 1240 } 1241 } 1242 1243 1244 // 1245 // extend syntax - used to extend selectors 1246 // 1247 function parseExtend($isRule = false){ 1248 1249 $index = $this->pos; 1250 $extendList = array(); 1251 1252 1253 if( !$this->MatchReg( $isRule ? '/\\G&:extend\(/' : '/\\G:extend\(/' ) ){ return; } 1254 1255 do{ 1256 $option = null; 1257 $elements = array(); 1258 while( true ){ 1259 $option = $this->MatchReg('/\\G(all)(?=\s*(\)|,))/'); 1260 if( $option ){ break; } 1261 $e = $this->parseElement(); 1262 if( !$e ){ break; } 1263 $elements[] = $e; 1264 } 1265 1266 if( $option ){ 1267 $option = $option[1]; 1268 } 1269 1270 $extendList[] = $this->NewObj3('Less_Tree_Extend', array( $this->NewObj1('Less_Tree_Selector',$elements), $option, $index )); 1271 1272 }while( $this->MatchChar(",") ); 1273 1274 $this->expect('/\\G\)/'); 1275 1276 if( $isRule ){ 1277 $this->expect('/\\G;/'); 1278 } 1279 1280 return $extendList; 1281 } 1282 1283 1284 // 1285 // A Mixin call, with an optional argument list 1286 // 1287 // #mixins > .square(#fff); 1288 // .rounded(4px, black); 1289 // .button; 1290 // 1291 // The `while` loop is there because mixins can be 1292 // namespaced, but we only support the child and descendant 1293 // selector for now. 1294 // 1295 private function parseMixinCall(){ 1296 1297 $char = $this->input[$this->pos]; 1298 if( $char !== '.' && $char !== '#' ){ 1299 return; 1300 } 1301 1302 $index = $this->pos; 1303 $this->save(); // stop us absorbing part of an invalid selector 1304 1305 $elements = $this->parseMixinCallElements(); 1306 1307 if( $elements ){ 1308 1309 if( $this->MatchChar('(') ){ 1310 $returned = $this->parseMixinArgs(true); 1311 $args = $returned['args']; 1312 $this->expectChar(')'); 1313 }else{ 1314 $args = array(); 1315 } 1316 1317 $important = $this->parseImportant(); 1318 1319 if( $this->parseEnd() ){ 1320 $this->forget(); 1321 return $this->NewObj5('Less_Tree_Mixin_Call', array( $elements, $args, $index, $this->env->currentFileInfo, $important)); 1322 } 1323 } 1324 1325 $this->restore(); 1326 } 1327 1328 1329 private function parseMixinCallElements(){ 1330 $elements = array(); 1331 $c = null; 1332 1333 while( true ){ 1334 $elemIndex = $this->pos; 1335 $e = $this->MatchReg('/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/'); 1336 if( !$e ){ 1337 break; 1338 } 1339 $elements[] = $this->NewObj4('Less_Tree_Element', array($c, $e[0], $elemIndex, $this->env->currentFileInfo)); 1340 $c = $this->MatchChar('>'); 1341 } 1342 1343 return $elements; 1344 } 1345 1346 1347 1348 /** 1349 * @param boolean $isCall 1350 */ 1351 private function parseMixinArgs( $isCall ){ 1352 $expressions = array(); 1353 $argsSemiColon = array(); 1354 $isSemiColonSeperated = null; 1355 $argsComma = array(); 1356 $expressionContainsNamed = null; 1357 $name = null; 1358 $returner = array('args'=>array(), 'variadic'=> false); 1359 1360 $this->save(); 1361 1362 while( true ){ 1363 if( $isCall ){ 1364 $arg = $this->MatchFuncs( array( 'parseDetachedRuleset','parseExpression' ) ); 1365 } else { 1366 $this->parseComments(); 1367 if( $this->input[ $this->pos ] === '.' && $this->MatchReg('/\\G\.{3}/') ){ 1368 $returner['variadic'] = true; 1369 if( $this->MatchChar(";") && !$isSemiColonSeperated ){ 1370 $isSemiColonSeperated = true; 1371 } 1372 1373 if( $isSemiColonSeperated ){ 1374 $argsSemiColon[] = array('variadic'=>true); 1375 }else{ 1376 $argsComma[] = array('variadic'=>true); 1377 } 1378 break; 1379 } 1380 $arg = $this->MatchFuncs( array('parseEntitiesVariable','parseEntitiesLiteral','parseEntitiesKeyword') ); 1381 } 1382 1383 if( !$arg ){ 1384 break; 1385 } 1386 1387 1388 $nameLoop = null; 1389 if( $arg instanceof Less_Tree_Expression ){ 1390 $arg->throwAwayComments(); 1391 } 1392 $value = $arg; 1393 $val = null; 1394 1395 if( $isCall ){ 1396 // Variable 1397 if( property_exists($arg,'value') && count($arg->value) == 1 ){ 1398 $val = $arg->value[0]; 1399 } 1400 } else { 1401 $val = $arg; 1402 } 1403 1404 1405 if( $val instanceof Less_Tree_Variable ){ 1406 1407 if( $this->MatchChar(':') ){ 1408 if( $expressions ){ 1409 if( $isSemiColonSeperated ){ 1410 $this->Error('Cannot mix ; and , as delimiter types'); 1411 } 1412 $expressionContainsNamed = true; 1413 } 1414 1415 // we do not support setting a ruleset as a default variable - it doesn't make sense 1416 // However if we do want to add it, there is nothing blocking it, just don't error 1417 // and remove isCall dependency below 1418 $value = null; 1419 if( $isCall ){ 1420 $value = $this->parseDetachedRuleset(); 1421 } 1422 if( !$value ){ 1423 $value = $this->parseExpression(); 1424 } 1425 1426 if( !$value ){ 1427 if( $isCall ){ 1428 $this->Error('could not understand value for named argument'); 1429 } else { 1430 $this->restore(); 1431 $returner['args'] = array(); 1432 return $returner; 1433 } 1434 } 1435 1436 $nameLoop = ($name = $val->name); 1437 }elseif( !$isCall && $this->MatchReg('/\\G\.{3}/') ){ 1438 $returner['variadic'] = true; 1439 if( $this->MatchChar(";") && !$isSemiColonSeperated ){ 1440 $isSemiColonSeperated = true; 1441 } 1442 if( $isSemiColonSeperated ){ 1443 $argsSemiColon[] = array('name'=> $arg->name, 'variadic' => true); 1444 }else{ 1445 $argsComma[] = array('name'=> $arg->name, 'variadic' => true); 1446 } 1447 break; 1448 }elseif( !$isCall ){ 1449 $name = $nameLoop = $val->name; 1450 $value = null; 1451 } 1452 } 1453 1454 if( $value ){ 1455 $expressions[] = $value; 1456 } 1457 1458 $argsComma[] = array('name'=>$nameLoop, 'value'=>$value ); 1459 1460 if( $this->MatchChar(',') ){ 1461 continue; 1462 } 1463 1464 if( $this->MatchChar(';') || $isSemiColonSeperated ){ 1465 1466 if( $expressionContainsNamed ){ 1467 $this->Error('Cannot mix ; and , as delimiter types'); 1468 } 1469 1470 $isSemiColonSeperated = true; 1471 1472 if( count($expressions) > 1 ){ 1473 $value = $this->NewObj1('Less_Tree_Value', $expressions); 1474 } 1475 $argsSemiColon[] = array('name'=>$name, 'value'=>$value ); 1476 1477 $name = null; 1478 $expressions = array(); 1479 $expressionContainsNamed = false; 1480 } 1481 } 1482 1483 $this->forget(); 1484 $returner['args'] = ($isSemiColonSeperated ? $argsSemiColon : $argsComma); 1485 return $returner; 1486 } 1487 1488 1489 1490 // 1491 // A Mixin definition, with a list of parameters 1492 // 1493 // .rounded (@radius: 2px, @color) { 1494 // ... 1495 // } 1496 // 1497 // Until we have a finer grained state-machine, we have to 1498 // do a look-ahead, to make sure we don't have a mixin call. 1499 // See the `rule` function for more information. 1500 // 1501 // We start by matching `.rounded (`, and then proceed on to 1502 // the argument list, which has optional default values. 1503 // We store the parameters in `params`, with a `value` key, 1504 // if there is a value, such as in the case of `@radius`. 1505 // 1506 // Once we've got our params list, and a closing `)`, we parse 1507 // the `{...}` block. 1508 // 1509 private function parseMixinDefinition(){ 1510 $cond = null; 1511 1512 $char = $this->input[$this->pos]; 1513 if( ($char !== '.' && $char !== '#') || ($char === '{' && $this->PeekReg('/\\G[^{]*\}/')) ){ 1514 return; 1515 } 1516 1517 $this->save(); 1518 1519 $match = $this->MatchReg('/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/'); 1520 if( $match ){ 1521 $name = $match[1]; 1522 1523 $argInfo = $this->parseMixinArgs( false ); 1524 $params = $argInfo['args']; 1525 $variadic = $argInfo['variadic']; 1526 1527 1528 // .mixincall("@{a}"); 1529 // looks a bit like a mixin definition.. 1530 // also 1531 // .mixincall(@a: {rule: set;}); 1532 // so we have to be nice and restore 1533 if( !$this->MatchChar(')') ){ 1534 $this->furthest = $this->pos; 1535 $this->restore(); 1536 return; 1537 } 1538 1539 1540 $this->parseComments(); 1541 1542 if ($this->MatchReg('/\\Gwhen/')) { // Guard 1543 $cond = $this->expect('parseConditions', 'Expected conditions'); 1544 } 1545 1546 $ruleset = $this->parseBlock(); 1547 1548 if( is_array($ruleset) ){ 1549 $this->forget(); 1550 return $this->NewObj5('Less_Tree_Mixin_Definition', array( $name, $params, $ruleset, $cond, $variadic)); 1551 } 1552 1553 $this->restore(); 1554 }else{ 1555 $this->forget(); 1556 } 1557 } 1558 1559 // 1560 // Entities are the smallest recognized token, 1561 // and can be found inside a rule's value. 1562 // 1563 private function parseEntity(){ 1564 1565 return $this->MatchFuncs( array('parseEntitiesLiteral','parseEntitiesVariable','parseEntitiesUrl','parseEntitiesCall','parseEntitiesKeyword','parseEntitiesJavascript','parseComment') ); 1566 } 1567 1568 // 1569 // A Rule terminator. Note that we use `peek()` to check for '}', 1570 // because the `block` rule will be expecting it, but we still need to make sure 1571 // it's there, if ';' was ommitted. 1572 // 1573 private function parseEnd(){ 1574 return $this->MatchChar(';') || $this->PeekChar('}'); 1575 } 1576 1577 // 1578 // IE's alpha function 1579 // 1580 // alpha(opacity=88) 1581 // 1582 private function parseAlpha(){ 1583 1584 if ( ! $this->MatchReg('/\\G\(opacity=/i')) { 1585 return; 1586 } 1587 1588 $value = $this->MatchReg('/\\G[0-9]+/'); 1589 if( $value ){ 1590 $value = $value[0]; 1591 }else{ 1592 $value = $this->parseEntitiesVariable(); 1593 if( !$value ){ 1594 return; 1595 } 1596 } 1597 1598 $this->expectChar(')'); 1599 return $this->NewObj1('Less_Tree_Alpha',$value); 1600 } 1601 1602 1603 // 1604 // A Selector Element 1605 // 1606 // div 1607 // + h1 1608 // #socks 1609 // input[type="text"] 1610 // 1611 // Elements are the building blocks for Selectors, 1612 // they are made out of a `Combinator` (see combinator rule), 1613 // and an element name, such as a tag a class, or `*`. 1614 // 1615 private function parseElement(){ 1616 $c = $this->parseCombinator(); 1617 $index = $this->pos; 1618 1619 $e = $this->match( array('/\\G(?:\d+\.\d+|\d+)%/', '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/', 1620 '#*', '#&', 'parseAttribute', '/\\G\([^()@]+\)/', '/\\G[\.#](?=@)/', 'parseEntitiesVariableCurly') ); 1621 1622 if( is_null($e) ){ 1623 $this->save(); 1624 if( $this->MatchChar('(') ){ 1625 if( ($v = $this->parseSelector()) && $this->MatchChar(')') ){ 1626 $e = $this->NewObj1('Less_Tree_Paren',$v); 1627 $this->forget(); 1628 }else{ 1629 $this->restore(); 1630 } 1631 }else{ 1632 $this->forget(); 1633 } 1634 } 1635 1636 if( !is_null($e) ){ 1637 return $this->NewObj4('Less_Tree_Element',array( $c, $e, $index, $this->env->currentFileInfo)); 1638 } 1639 } 1640 1641 // 1642 // Combinators combine elements together, in a Selector. 1643 // 1644 // Because our parser isn't white-space sensitive, special care 1645 // has to be taken, when parsing the descendant combinator, ` `, 1646 // as it's an empty space. We have to check the previous character 1647 // in the input, to see if it's a ` ` character. 1648 // 1649 private function parseCombinator(){ 1650 if( $this->pos < $this->input_len ){ 1651 $c = $this->input[$this->pos]; 1652 if ($c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ){ 1653 1654 $this->pos++; 1655 if( $this->input[$this->pos] === '^' ){ 1656 $c = '^^'; 1657 $this->pos++; 1658 } 1659 1660 $this->skipWhitespace(0); 1661 1662 return $c; 1663 } 1664 1665 if( $this->pos > 0 && $this->isWhitespace(-1) ){ 1666 return ' '; 1667 } 1668 } 1669 } 1670 1671 // 1672 // A CSS selector (see selector below) 1673 // with less extensions e.g. the ability to extend and guard 1674 // 1675 private function parseLessSelector(){ 1676 return $this->parseSelector(true); 1677 } 1678 1679 // 1680 // A CSS Selector 1681 // 1682 // .class > div + h1 1683 // li a:hover 1684 // 1685 // Selectors are made out of one or more Elements, see above. 1686 // 1687 private function parseSelector( $isLess = false ){ 1688 $elements = array(); 1689 $extendList = array(); 1690 $condition = null; 1691 $when = false; 1692 $extend = false; 1693 $e = null; 1694 $c = null; 1695 $index = $this->pos; 1696 1697 while( ($isLess && ($extend = $this->parseExtend())) || ($isLess && ($when = $this->MatchReg('/\\Gwhen/') )) || ($e = $this->parseElement()) ){ 1698 if( $when ){ 1699 $condition = $this->expect('parseConditions', 'expected condition'); 1700 }elseif( $condition ){ 1701 //error("CSS guard can only be used at the end of selector"); 1702 }elseif( $extend ){ 1703 $extendList = array_merge($extendList,$extend); 1704 }else{ 1705 //if( count($extendList) ){ 1706 //error("Extend can only be used at the end of selector"); 1707 //} 1708 if( $this->pos < $this->input_len ){ 1709 $c = $this->input[ $this->pos ]; 1710 } 1711 $elements[] = $e; 1712 $e = null; 1713 } 1714 1715 if( $c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')') { break; } 1716 } 1717 1718 if( $elements ){ 1719 return $this->NewObj5('Less_Tree_Selector',array($elements, $extendList, $condition, $index, $this->env->currentFileInfo)); 1720 } 1721 if( $extendList ) { 1722 $this->Error('Extend must be used to extend a selector, it cannot be used on its own'); 1723 } 1724 } 1725 1726 private function parseTag(){ 1727 return ( $tag = $this->MatchReg('/\\G[A-Za-z][A-Za-z-]*[0-9]?/') ) ? $tag : $this->MatchChar('*'); 1728 } 1729 1730 private function parseAttribute(){ 1731 1732 $val = null; 1733 1734 if( !$this->MatchChar('[') ){ 1735 return; 1736 } 1737 1738 $key = $this->parseEntitiesVariableCurly(); 1739 if( !$key ){ 1740 $key = $this->expect('/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/'); 1741 } 1742 1743 $op = $this->MatchReg('/\\G[|~*$^]?=/'); 1744 if( $op ){ 1745 $val = $this->match( array('parseEntitiesQuoted','/\\G[0-9]+%/','/\\G[\w-]+/','parseEntitiesVariableCurly') ); 1746 } 1747 1748 $this->expectChar(']'); 1749 1750 return $this->NewObj3('Less_Tree_Attribute',array( $key, $op[0], $val)); 1751 } 1752 1753 // 1754 // The `block` rule is used by `ruleset` and `mixin.definition`. 1755 // It's a wrapper around the `primary` rule, with added `{}`. 1756 // 1757 private function parseBlock(){ 1758 if( $this->MatchChar('{') ){ 1759 $content = $this->parsePrimary(); 1760 if( $this->MatchChar('}') ){ 1761 return $content; 1762 } 1763 } 1764 } 1765 1766 private function parseBlockRuleset(){ 1767 $block = $this->parseBlock(); 1768 1769 if( $block ){ 1770 $block = $this->NewObj2('Less_Tree_Ruleset',array( null, $block)); 1771 } 1772 1773 return $block; 1774 } 1775 1776 private function parseDetachedRuleset(){ 1777 $blockRuleset = $this->parseBlockRuleset(); 1778 if( $blockRuleset ){ 1779 return $this->NewObj1('Less_Tree_DetachedRuleset',$blockRuleset); 1780 } 1781 } 1782 1783 // 1784 // div, .class, body > p {...} 1785 // 1786 private function parseRuleset(){ 1787 $selectors = array(); 1788 1789 $this->save(); 1790 1791 while( true ){ 1792 $s = $this->parseLessSelector(); 1793 if( !$s ){ 1794 break; 1795 } 1796 $selectors[] = $s; 1797 $this->parseComments(); 1798 1799 if( $s->condition && count($selectors) > 1 ){ 1800 $this->Error('Guards are only currently allowed on a single selector.'); 1801 } 1802 1803 if( !$this->MatchChar(',') ){ 1804 break; 1805 } 1806 if( $s->condition ){ 1807 $this->Error('Guards are only currently allowed on a single selector.'); 1808 } 1809 $this->parseComments(); 1810 } 1811 1812 1813 if( $selectors ){ 1814 $rules = $this->parseBlock(); 1815 if( is_array($rules) ){ 1816 $this->forget(); 1817 return $this->NewObj2('Less_Tree_Ruleset',array( $selectors, $rules)); //Less_Environment::$strictImports 1818 } 1819 } 1820 1821 // Backtrack 1822 $this->furthest = $this->pos; 1823 $this->restore(); 1824 } 1825 1826 /** 1827 * Custom less.php parse function for finding simple name-value css pairs 1828 * ex: width:100px; 1829 * 1830 */ 1831 private function parseNameValue(){ 1832 1833 $index = $this->pos; 1834 $this->save(); 1835 1836 1837 //$match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*((?:\'")?[a-zA-Z0-9\-% \.,!]+?(?:\'")?)\s*([;}])/'); 1838 $match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?) *(! *important)?\s*([;}])/'); 1839 if( $match ){ 1840 1841 if( $match[4] == '}' ){ 1842 $this->pos = $index + strlen($match[0])-1; 1843 } 1844 1845 if( $match[3] ){ 1846 $match[2] .= ' !important'; 1847 } 1848 1849 return $this->NewObj4('Less_Tree_NameValue',array( $match[1], $match[2], $index, $this->env->currentFileInfo)); 1850 } 1851 1852 $this->restore(); 1853 } 1854 1855 1856 private function parseRule( $tryAnonymous = null ){ 1857 1858 $merge = false; 1859 $startOfRule = $this->pos; 1860 1861 $c = $this->input[$this->pos]; 1862 if( $c === '.' || $c === '#' || $c === '&' ){ 1863 return; 1864 } 1865 1866 $this->save(); 1867 $name = $this->MatchFuncs( array('parseVariable','parseRuleProperty')); 1868 1869 if( $name ){ 1870 1871 $isVariable = is_string($name); 1872 1873 $value = null; 1874 if( $isVariable ){ 1875 $value = $this->parseDetachedRuleset(); 1876 } 1877 1878 $important = null; 1879 if( !$value ){ 1880 1881 // prefer to try to parse first if its a variable or we are compressing 1882 // but always fallback on the other one 1883 //if( !$tryAnonymous && is_string($name) && $name[0] === '@' ){ 1884 if( !$tryAnonymous && (Less_Parser::$options['compress'] || $isVariable) ){ 1885 $value = $this->MatchFuncs( array('parseValue','parseAnonymousValue')); 1886 }else{ 1887 $value = $this->MatchFuncs( array('parseAnonymousValue','parseValue')); 1888 } 1889 1890 $important = $this->parseImportant(); 1891 1892 // a name returned by this.ruleProperty() is always an array of the form: 1893 // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"] 1894 // where each item is a tree.Keyword or tree.Variable 1895 if( !$isVariable && is_array($name) ){ 1896 $nm = array_pop($name); 1897 if( $nm->value ){ 1898 $merge = $nm->value; 1899 } 1900 } 1901 } 1902 1903 1904 if( $value && $this->parseEnd() ){ 1905 $this->forget(); 1906 return $this->NewObj6('Less_Tree_Rule',array( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo)); 1907 }else{ 1908 $this->furthest = $this->pos; 1909 $this->restore(); 1910 if( $value && !$tryAnonymous ){ 1911 return $this->parseRule(true); 1912 } 1913 } 1914 }else{ 1915 $this->forget(); 1916 } 1917 } 1918 1919 function parseAnonymousValue(){ 1920 1921 if( preg_match('/\\G([^@+\/\'"*`(;{}-]*);/',$this->input, $match, 0, $this->pos) ){ 1922 $this->pos += strlen($match[1]); 1923 return $this->NewObj1('Less_Tree_Anonymous',$match[1]); 1924 } 1925 } 1926 1927 // 1928 // An @import directive 1929 // 1930 // @import "lib"; 1931 // 1932 // Depending on our environment, importing is done differently: 1933 // In the browser, it's an XHR request, in Node, it would be a 1934 // file-system operation. The function used for importing is 1935 // stored in `import`, which we pass to the Import constructor. 1936 // 1937 private function parseImport(){ 1938 1939 $this->save(); 1940 1941 $dir = $this->MatchReg('/\\G@import?\s+/'); 1942 1943 if( $dir ){ 1944 $options = $this->parseImportOptions(); 1945 $path = $this->MatchFuncs( array('parseEntitiesQuoted','parseEntitiesUrl')); 1946 1947 if( $path ){ 1948 $features = $this->parseMediaFeatures(); 1949 if( $this->MatchChar(';') ){ 1950 if( $features ){ 1951 $features = $this->NewObj1('Less_Tree_Value',$features); 1952 } 1953 1954 $this->forget(); 1955 return $this->NewObj5('Less_Tree_Import',array( $path, $features, $options, $this->pos, $this->env->currentFileInfo)); 1956 } 1957 } 1958 } 1959 1960 $this->restore(); 1961 } 1962 1963 private function parseImportOptions(){ 1964 1965 $options = array(); 1966 1967 // list of options, surrounded by parens 1968 if( !$this->MatchChar('(') ){ 1969 return $options; 1970 } 1971 do{ 1972 $optionName = $this->parseImportOption(); 1973 if( $optionName ){ 1974 $value = true; 1975 switch( $optionName ){ 1976 case "css": 1977 $optionName = "less"; 1978 $value = false; 1979 break; 1980 case "once": 1981 $optionName = "multiple"; 1982 $value = false; 1983 break; 1984 } 1985 $options[$optionName] = $value; 1986 if( !$this->MatchChar(',') ){ break; } 1987 } 1988 }while( $optionName ); 1989 $this->expectChar(')'); 1990 return $options; 1991 } 1992 1993 private function parseImportOption(){ 1994 $opt = $this->MatchReg('/\\G(less|css|multiple|once|inline|reference|optional)/'); 1995 if( $opt ){ 1996 return $opt[1]; 1997 } 1998 } 1999 2000 private function parseMediaFeature() { 2001 $nodes = array(); 2002 2003 do{ 2004 $e = $this->MatchFuncs(array('parseEntitiesKeyword','parseEntitiesVariable')); 2005 if( $e ){ 2006 $nodes[] = $e; 2007 } elseif ($this->MatchChar('(')) { 2008 $p = $this->parseProperty(); 2009 $e = $this->parseValue(); 2010 if ($this->MatchChar(')')) { 2011 if ($p && $e) { 2012 $r = $this->NewObj7('Less_Tree_Rule', array( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true)); 2013 $nodes[] = $this->NewObj1('Less_Tree_Paren',$r); 2014 } elseif ($e) { 2015 $nodes[] = $this->NewObj1('Less_Tree_Paren',$e); 2016 } else { 2017 return null; 2018 } 2019 } else 2020 return null; 2021 } 2022 } while ($e); 2023 2024 if ($nodes) { 2025 return $this->NewObj1('Less_Tree_Expression',$nodes); 2026 } 2027 } 2028 2029 private function parseMediaFeatures() { 2030 $features = array(); 2031 2032 do{ 2033 $e = $this->parseMediaFeature(); 2034 if( $e ){ 2035 $features[] = $e; 2036 if (!$this->MatchChar(',')) break; 2037 }else{ 2038 $e = $this->parseEntitiesVariable(); 2039 if( $e ){ 2040 $features[] = $e; 2041 if (!$this->MatchChar(',')) break; 2042 } 2043 } 2044 } while ($e); 2045 2046 return $features ? $features : null; 2047 } 2048 2049 private function parseMedia() { 2050 if( $this->MatchReg('/\\G@media/') ){ 2051 $features = $this->parseMediaFeatures(); 2052 $rules = $this->parseBlock(); 2053 2054 if( is_array($rules) ){ 2055 return $this->NewObj4('Less_Tree_Media',array( $rules, $features, $this->pos, $this->env->currentFileInfo)); 2056 } 2057 } 2058 } 2059 2060 2061 // 2062 // A CSS Directive 2063 // 2064 // @charset "utf-8"; 2065 // 2066 private function parseDirective(){ 2067 2068 if( !$this->PeekChar('@') ){ 2069 return; 2070 } 2071 2072 $rules = null; 2073 $index = $this->pos; 2074 $hasBlock = true; 2075 $hasIdentifier = false; 2076 $hasExpression = false; 2077 $hasUnknown = false; 2078 2079 2080 $value = $this->MatchFuncs(array('parseImport','parseMedia')); 2081 if( $value ){ 2082 return $value; 2083 } 2084 2085 $this->save(); 2086 2087 $name = $this->MatchReg('/\\G@[a-z-]+/'); 2088 2089 if( !$name ) return; 2090 $name = $name[0]; 2091 2092 2093 $nonVendorSpecificName = $name; 2094 $pos = strpos($name,'-', 2); 2095 if( $name[1] == '-' && $pos > 0 ){ 2096 $nonVendorSpecificName = "@" . substr($name, $pos + 1); 2097 } 2098 2099 2100 switch( $nonVendorSpecificName ){ 2101 /* 2102 case "@font-face": 2103 case "@viewport": 2104 case "@top-left": 2105 case "@top-left-corner": 2106 case "@top-center": 2107 case "@top-right": 2108 case "@top-right-corner": 2109 case "@bottom-left": 2110 case "@bottom-left-corner": 2111 case "@bottom-center": 2112 case "@bottom-right": 2113 case "@bottom-right-corner": 2114 case "@left-top": 2115 case "@left-middle": 2116 case "@left-bottom": 2117 case "@right-top": 2118 case "@right-middle": 2119 case "@right-bottom": 2120 hasBlock = true; 2121 break; 2122 */ 2123 case "@charset": 2124 $hasIdentifier = true; 2125 $hasBlock = false; 2126 break; 2127 case "@namespace": 2128 $hasExpression = true; 2129 $hasBlock = false; 2130 break; 2131 case "@keyframes": 2132 $hasIdentifier = true; 2133 break; 2134 case "@host": 2135 case "@page": 2136 case "@document": 2137 case "@supports": 2138 $hasUnknown = true; 2139 break; 2140 } 2141 2142 if( $hasIdentifier ){ 2143 $value = $this->parseEntity(); 2144 if( !$value ){ 2145 $this->error("expected " . $name . " identifier"); 2146 } 2147 } else if( $hasExpression ){ 2148 $value = $this->parseExpression(); 2149 if( !$value ){ 2150 $this->error("expected " . $name. " expression"); 2151 } 2152 } else if ($hasUnknown) { 2153 2154 $value = $this->MatchReg('/\\G[^{;]+/'); 2155 if( $value ){ 2156 $value = $this->NewObj1('Less_Tree_Anonymous',trim($value[0])); 2157 } 2158 } 2159 2160 if( $hasBlock ){ 2161 $rules = $this->parseBlockRuleset(); 2162 } 2163 2164 if( $rules || (!$hasBlock && $value && $this->MatchChar(';'))) { 2165 $this->forget(); 2166 return $this->NewObj5('Less_Tree_Directive',array($name, $value, $rules, $index, $this->env->currentFileInfo)); 2167 } 2168 2169 $this->restore(); 2170 } 2171 2172 2173 // 2174 // A Value is a comma-delimited list of Expressions 2175 // 2176 // font-family: Baskerville, Georgia, serif; 2177 // 2178 // In a Rule, a Value represents everything after the `:`, 2179 // and before the `;`. 2180 // 2181 private function parseValue(){ 2182 $expressions = array(); 2183 2184 do{ 2185 $e = $this->parseExpression(); 2186 if( $e ){ 2187 $expressions[] = $e; 2188 if (! $this->MatchChar(',')) { 2189 break; 2190 } 2191 } 2192 }while($e); 2193 2194 if( $expressions ){ 2195 return $this->NewObj1('Less_Tree_Value',$expressions); 2196 } 2197 } 2198 2199 private function parseImportant (){ 2200 if( $this->PeekChar('!') && $this->MatchReg('/\\G! *important/') ){ 2201 return ' !important'; 2202 } 2203 } 2204 2205 private function parseSub (){ 2206 2207 if( $this->MatchChar('(') ){ 2208 $a = $this->parseAddition(); 2209 if( $a ){ 2210 $this->expectChar(')'); 2211 return $this->NewObj2('Less_Tree_Expression',array( array($a), true) ); //instead of $e->parens = true so the value is cached 2212 } 2213 } 2214 } 2215 2216 2217 /** 2218 * Parses multiplication operation 2219 * 2220 * @return Less_Tree_Operation|null 2221 */ 2222 function parseMultiplication(){ 2223 2224 $return = $m = $this->parseOperand(); 2225 if( $return ){ 2226 while( true ){ 2227 2228 $isSpaced = $this->isWhitespace( -1 ); 2229 2230 if( $this->PeekReg('/\\G\/[*\/]/') ){ 2231 break; 2232 } 2233 2234 $op = $this->MatchChar('/'); 2235 if( !$op ){ 2236 $op = $this->MatchChar('*'); 2237 if( !$op ){ 2238 break; 2239 } 2240 } 2241 2242 $a = $this->parseOperand(); 2243 2244 if(!$a) { break; } 2245 2246 $m->parensInOp = true; 2247 $a->parensInOp = true; 2248 $return = $this->NewObj3('Less_Tree_Operation',array( $op, array( $return, $a ), $isSpaced) ); 2249 } 2250 } 2251 return $return; 2252 2253 } 2254 2255 2256 /** 2257 * Parses an addition operation 2258 * 2259 * @return Less_Tree_Operation|null 2260 */ 2261 private function parseAddition (){ 2262 2263 $return = $m = $this->parseMultiplication(); 2264 if( $return ){ 2265 while( true ){ 2266 2267 $isSpaced = $this->isWhitespace( -1 ); 2268 2269 $op = $this->MatchReg('/\\G[-+]\s+/'); 2270 if( $op ){ 2271 $op = $op[0]; 2272 }else{ 2273 if( !$isSpaced ){ 2274 $op = $this->match(array('#+','#-')); 2275 } 2276 if( !$op ){ 2277 break; 2278 } 2279 } 2280 2281 $a = $this->parseMultiplication(); 2282 if( !$a ){ 2283 break; 2284 } 2285 2286 $m->parensInOp = true; 2287 $a->parensInOp = true; 2288 $return = $this->NewObj3('Less_Tree_Operation',array($op, array($return, $a), $isSpaced)); 2289 } 2290 } 2291 2292 return $return; 2293 } 2294 2295 2296 /** 2297 * Parses the conditions 2298 * 2299 * @return Less_Tree_Condition|null 2300 */ 2301 private function parseConditions() { 2302 $index = $this->pos; 2303 $return = $a = $this->parseCondition(); 2304 if( $a ){ 2305 while( true ){ 2306 if( !$this->PeekReg('/\\G,\s*(not\s*)?\(/') || !$this->MatchChar(',') ){ 2307 break; 2308 } 2309 $b = $this->parseCondition(); 2310 if( !$b ){ 2311 break; 2312 } 2313 2314 $return = $this->NewObj4('Less_Tree_Condition',array('or', $return, $b, $index)); 2315 } 2316 return $return; 2317 } 2318 } 2319 2320 private function parseCondition() { 2321 $index = $this->pos; 2322 $negate = false; 2323 $c = null; 2324 2325 if ($this->MatchReg('/\\Gnot/')) $negate = true; 2326 $this->expectChar('('); 2327 $a = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted')); 2328 2329 if( $a ){ 2330 $op = $this->MatchReg('/\\G(?:>=|<=|=<|[<=>])/'); 2331 if( $op ){ 2332 $b = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted')); 2333 if( $b ){ 2334 $c = $this->NewObj5('Less_Tree_Condition',array($op[0], $a, $b, $index, $negate)); 2335 } else { 2336 $this->Error('Unexpected expression'); 2337 } 2338 } else { 2339 $k = $this->NewObj1('Less_Tree_Keyword','true'); 2340 $c = $this->NewObj5('Less_Tree_Condition',array('=', $a, $k, $index, $negate)); 2341 } 2342 $this->expectChar(')'); 2343 return $this->MatchReg('/\\Gand/') ? $this->NewObj3('Less_Tree_Condition',array('and', $c, $this->parseCondition())) : $c; 2344 } 2345 } 2346 2347 /** 2348 * An operand is anything that can be part of an operation, 2349 * such as a Color, or a Variable 2350 * 2351 */ 2352 private function parseOperand (){ 2353 2354 $negate = false; 2355 $offset = $this->pos+1; 2356 if( $offset >= $this->input_len ){ 2357 return; 2358 } 2359 $char = $this->input[$offset]; 2360 if( $char === '@' || $char === '(' ){ 2361 $negate = $this->MatchChar('-'); 2362 } 2363 2364 $o = $this->MatchFuncs(array('parseSub','parseEntitiesDimension','parseEntitiesColor','parseEntitiesVariable','parseEntitiesCall')); 2365 2366 if( $negate ){ 2367 $o->parensInOp = true; 2368 $o = $this->NewObj1('Less_Tree_Negative',$o); 2369 } 2370 2371 return $o; 2372 } 2373 2374 2375 /** 2376 * Expressions either represent mathematical operations, 2377 * or white-space delimited Entities. 2378 * 2379 * 1px solid black 2380 * @var * 2 2381 * 2382 * @return Less_Tree_Expression|null 2383 */ 2384 private function parseExpression (){ 2385 $entities = array(); 2386 2387 do{ 2388 $e = $this->MatchFuncs(array('parseAddition','parseEntity')); 2389 if( $e ){ 2390 $entities[] = $e; 2391 // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here 2392 if( !$this->PeekReg('/\\G\/[\/*]/') ){ 2393 $delim = $this->MatchChar('/'); 2394 if( $delim ){ 2395 $entities[] = $this->NewObj1('Less_Tree_Anonymous',$delim); 2396 } 2397 } 2398 } 2399 }while($e); 2400 2401 if( $entities ){ 2402 return $this->NewObj1('Less_Tree_Expression',$entities); 2403 } 2404 } 2405 2406 2407 /** 2408 * Parse a property 2409 * eg: 'min-width', 'orientation', etc 2410 * 2411 * @return string 2412 */ 2413 private function parseProperty (){ 2414 $name = $this->MatchReg('/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/'); 2415 if( $name ){ 2416 return $name[1]; 2417 } 2418 } 2419 2420 2421 /** 2422 * Parse a rule property 2423 * eg: 'color', 'width', 'height', etc 2424 * 2425 * @return string 2426 */ 2427 private function parseRuleProperty(){ 2428 $offset = $this->pos; 2429 $name = array(); 2430 $index = array(); 2431 $length = 0; 2432 2433 2434 $this->rulePropertyMatch('/\\G(\*?)/', $offset, $length, $index, $name ); 2435 while( $this->rulePropertyMatch('/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/', $offset, $length, $index, $name )); // ! 2436 2437 if( (count($name) > 1) && $this->rulePropertyMatch('/\\G\s*((?:\+_|\+)?)\s*:/', $offset, $length, $index, $name) ){ 2438 // at last, we have the complete match now. move forward, 2439 // convert name particles to tree objects and return: 2440 $this->skipWhitespace($length); 2441 2442 if( $name[0] === '' ){ 2443 array_shift($name); 2444 array_shift($index); 2445 } 2446 foreach($name as $k => $s ){ 2447 if( !$s || $s[0] !== '@' ){ 2448 $name[$k] = $this->NewObj1('Less_Tree_Keyword',$s); 2449 }else{ 2450 $name[$k] = $this->NewObj3('Less_Tree_Variable',array('@' . substr($s,2,-1), $index[$k], $this->env->currentFileInfo)); 2451 } 2452 } 2453 return $name; 2454 } 2455 2456 2457 } 2458 2459 private function rulePropertyMatch( $re, &$offset, &$length, &$index, &$name ){ 2460 preg_match($re, $this->input, $a, 0, $offset); 2461 if( $a ){ 2462 $index[] = $this->pos + $length; 2463 $length += strlen($a[0]); 2464 $offset += strlen($a[0]); 2465 $name[] = $a[1]; 2466 return true; 2467 } 2468 } 2469 2470 public static function serializeVars( $vars ){ 2471 $s = ''; 2472 2473 foreach($vars as $name => $value){ 2474 $s .= (($name[0] === '@') ? '' : '@') . $name .': '. $value . ((substr($value,-1) === ';') ? '' : ';'); 2475 } 2476 2477 return $s; 2478 } 2479 2480 2481 /** 2482 * Some versions of php have trouble with method_exists($a,$b) if $a is not an object 2483 * 2484 * @param string $b 2485 */ 2486 public static function is_method($a,$b){ 2487 return is_object($a) && method_exists($a,$b); 2488 } 2489 2490 2491 /** 2492 * Round numbers similarly to javascript 2493 * eg: 1.499999 to 1 instead of 2 2494 * 2495 */ 2496 public static function round($i, $precision = 0){ 2497 2498 $precision = pow(10,$precision); 2499 $i = $i*$precision; 2500 2501 $ceil = ceil($i); 2502 $floor = floor($i); 2503 if( ($ceil - $i) <= ($i - $floor) ){ 2504 return $ceil/$precision; 2505 }else{ 2506 return $floor/$precision; 2507 } 2508 } 2509 2510 2511 /** 2512 * Create Less_Tree_* objects and optionally generate a cache string 2513 * 2514 * @return mixed 2515 */ 2516 public function NewObj0($class){ 2517 $obj = new $class(); 2518 if( $this->CacheEnabled() ){ 2519 $obj->cache_string = ' new '.$class.'()'; 2520 } 2521 return $obj; 2522 } 2523 2524 public function NewObj1($class, $arg){ 2525 $obj = new $class( $arg ); 2526 if( $this->CacheEnabled() ){ 2527 $obj->cache_string = ' new '.$class.'('.Less_Parser::ArgString($arg).')'; 2528 } 2529 return $obj; 2530 } 2531 2532 public function NewObj2($class, $args){ 2533 $obj = new $class( $args[0], $args[1] ); 2534 if( $this->CacheEnabled() ){ 2535 $this->ObjCache( $obj, $class, $args); 2536 } 2537 return $obj; 2538 } 2539 2540 public function NewObj3($class, $args){ 2541 $obj = new $class( $args[0], $args[1], $args[2] ); 2542 if( $this->CacheEnabled() ){ 2543 $this->ObjCache( $obj, $class, $args); 2544 } 2545 return $obj; 2546 } 2547 2548 public function NewObj4($class, $args){ 2549 $obj = new $class( $args[0], $args[1], $args[2], $args[3] ); 2550 if( $this->CacheEnabled() ){ 2551 $this->ObjCache( $obj, $class, $args); 2552 } 2553 return $obj; 2554 } 2555 2556 public function NewObj5($class, $args){ 2557 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4] ); 2558 if( $this->CacheEnabled() ){ 2559 $this->ObjCache( $obj, $class, $args); 2560 } 2561 return $obj; 2562 } 2563 2564 public function NewObj6($class, $args){ 2565 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5] ); 2566 if( $this->CacheEnabled() ){ 2567 $this->ObjCache( $obj, $class, $args); 2568 } 2569 return $obj; 2570 } 2571 2572 public function NewObj7($class, $args){ 2573 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6] ); 2574 if( $this->CacheEnabled() ){ 2575 $this->ObjCache( $obj, $class, $args); 2576 } 2577 return $obj; 2578 } 2579 2580 //caching 2581 public function ObjCache($obj, $class, $args=array()){ 2582 $obj->cache_string = ' new '.$class.'('. self::ArgCache($args).')'; 2583 } 2584 2585 public function ArgCache($args){ 2586 return implode(',',array_map( array('Less_Parser','ArgString'),$args)); 2587 } 2588 2589 2590 /** 2591 * Convert an argument to a string for use in the parser cache 2592 * 2593 * @return string 2594 */ 2595 public static function ArgString($arg){ 2596 2597 $type = gettype($arg); 2598 2599 if( $type === 'object'){ 2600 $string = $arg->cache_string; 2601 unset($arg->cache_string); 2602 return $string; 2603 2604 }elseif( $type === 'array' ){ 2605 $string = ' Array('; 2606 foreach($arg as $k => $a){ 2607 $string .= var_export($k,true).' => '.self::ArgString($a).','; 2608 } 2609 return $string . ')'; 2610 } 2611 2612 return var_export($arg,true); 2613 } 2614 2615 public function Error($msg){ 2616 throw new Less_Exception_Parser($msg, null, $this->furthest, $this->env->currentFileInfo); 2617 } 2618 2619 public static function WinPath($path){ 2620 return str_replace('\\', '/', $path); 2621 } 2622 2623 public function CacheEnabled(){ 2624 return (Less_Parser::$options['cache_method'] && (Less_Cache::$cache_dir || (Less_Parser::$options['cache_method'] == 'callback'))); 2625 } 2626 2627 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |