Less.php (260594B)
1 <?php 2 3 /** 4 * Class for parsing and compiling less files into css 5 * 6 * @package Less 7 * @subpackage parser 8 * 9 */ 10 class Less_Parser{ 11 12 13 /** 14 * Default parser options 15 */ 16 public static $default_options = array( 17 'compress' => false, // option - whether to compress 18 'strictUnits' => false, // whether units need to evaluate correctly 19 'strictMath' => false, // whether math has to be within parenthesis 20 'relativeUrls' => true, // option - whether to adjust URL's to be relative 21 'urlArgs' => '', // whether to add args into url tokens 22 'numPrecision' => 8, 23 24 'import_dirs' => array(), 25 'import_callback' => null, 26 'cache_dir' => null, 27 'cache_method' => 'php', // false, 'serialize', 'php', 'var_export', 'callback'; 28 'cache_callback_get' => null, 29 'cache_callback_set' => null, 30 31 'sourceMap' => false, // whether to output a source map 32 'sourceMapBasepath' => null, 33 'sourceMapWriteTo' => null, 34 'sourceMapURL' => null, 35 36 'indentation' => ' ', 37 38 'plugins' => array(), 39 40 ); 41 42 public static $options = array(); 43 44 45 private $input; // Less input string 46 private $input_len; // input string length 47 private $pos; // current index in `input` 48 private $saveStack = array(); // holds state for backtracking 49 private $furthest; 50 private $mb_internal_encoding = ''; // for remember exists value of mbstring.internal_encoding 51 52 /** 53 * @var Less_Environment 54 */ 55 private $env; 56 57 protected $rules = array(); 58 59 private static $imports = array(); 60 61 public static $has_extends = false; 62 63 public static $next_id = 0; 64 65 /** 66 * Filename to contents of all parsed the files 67 * 68 * @var array 69 */ 70 public static $contentsMap = array(); 71 72 73 /** 74 * @param Less_Environment|array|null $env 75 */ 76 public function __construct( $env = null ){ 77 78 // Top parser on an import tree must be sure there is one "env" 79 // which will then be passed around by reference. 80 if( $env instanceof Less_Environment ){ 81 $this->env = $env; 82 }else{ 83 $this->SetOptions(Less_Parser::$default_options); 84 $this->Reset( $env ); 85 } 86 87 // mbstring.func_overload > 1 bugfix 88 // The encoding value must be set for each source file, 89 // therefore, to conserve resources and improve the speed of this design is taken here 90 if (ini_get('mbstring.func_overload')) { 91 $this->mb_internal_encoding = ini_get('mbstring.internal_encoding'); 92 @ini_set('mbstring.internal_encoding', 'ascii'); 93 } 94 95 } 96 97 98 /** 99 * Reset the parser state completely 100 * 101 */ 102 public function Reset( $options = null ){ 103 $this->rules = array(); 104 self::$imports = array(); 105 self::$has_extends = false; 106 self::$imports = array(); 107 self::$contentsMap = array(); 108 109 $this->env = new Less_Environment($options); 110 $this->env->Init(); 111 112 //set new options 113 if( is_array($options) ){ 114 $this->SetOptions(Less_Parser::$default_options); 115 $this->SetOptions($options); 116 } 117 } 118 119 /** 120 * Set one or more compiler options 121 * options: import_dirs, cache_dir, cache_method 122 * 123 */ 124 public function SetOptions( $options ){ 125 foreach($options as $option => $value){ 126 $this->SetOption($option,$value); 127 } 128 } 129 130 /** 131 * Set one compiler option 132 * 133 */ 134 public function SetOption($option,$value){ 135 136 switch($option){ 137 138 case 'import_dirs': 139 $this->SetImportDirs($value); 140 return; 141 142 case 'cache_dir': 143 if( is_string($value) ){ 144 Less_Cache::SetCacheDir($value); 145 Less_Cache::CheckCacheDir(); 146 } 147 return; 148 } 149 150 Less_Parser::$options[$option] = $value; 151 } 152 153 /** 154 * Registers a new custom function 155 * 156 * @param string $name function name 157 * @param callable $callback callback 158 */ 159 public function registerFunction($name, $callback) { 160 $this->env->functions[$name] = $callback; 161 } 162 163 /** 164 * Removed an already registered function 165 * 166 * @param string $name function name 167 */ 168 public function unregisterFunction($name) { 169 if( isset($this->env->functions[$name]) ) 170 unset($this->env->functions[$name]); 171 } 172 173 174 /** 175 * Get the current css buffer 176 * 177 * @return string 178 */ 179 public function getCss(){ 180 181 $precision = ini_get('precision'); 182 @ini_set('precision',16); 183 $locale = setlocale(LC_NUMERIC, 0); 184 setlocale(LC_NUMERIC, "C"); 185 186 try { 187 188 $root = new Less_Tree_Ruleset(array(), $this->rules ); 189 $root->root = true; 190 $root->firstRoot = true; 191 192 193 $this->PreVisitors($root); 194 195 self::$has_extends = false; 196 $evaldRoot = $root->compile($this->env); 197 198 199 200 $this->PostVisitors($evaldRoot); 201 202 if( Less_Parser::$options['sourceMap'] ){ 203 $generator = new Less_SourceMap_Generator($evaldRoot, Less_Parser::$contentsMap, Less_Parser::$options ); 204 // will also save file 205 // FIXME: should happen somewhere else? 206 $css = $generator->generateCSS(); 207 }else{ 208 $css = $evaldRoot->toCSS(); 209 } 210 211 if( Less_Parser::$options['compress'] ){ 212 $css = preg_replace('/(^(\s)+)|((\s)+$)/', '', $css); 213 } 214 215 } catch (Exception $exc) { 216 // Intentional fall-through so we can reset environment 217 } 218 219 //reset php settings 220 @ini_set('precision',$precision); 221 setlocale(LC_NUMERIC, $locale); 222 223 // If you previously defined $this->mb_internal_encoding 224 // is required to return the encoding as it was before 225 if ($this->mb_internal_encoding != '') { 226 @ini_set("mbstring.internal_encoding", $this->mb_internal_encoding); 227 $this->mb_internal_encoding = ''; 228 } 229 230 // Rethrow exception after we handled resetting the environment 231 if (!empty($exc)) { 232 throw $exc; 233 } 234 235 236 237 return $css; 238 } 239 240 /** 241 * Run pre-compile visitors 242 * 243 */ 244 private function PreVisitors($root){ 245 246 if( Less_Parser::$options['plugins'] ){ 247 foreach(Less_Parser::$options['plugins'] as $plugin){ 248 if( !empty($plugin->isPreEvalVisitor) ){ 249 $plugin->run($root); 250 } 251 } 252 } 253 } 254 255 256 /** 257 * Run post-compile visitors 258 * 259 */ 260 private function PostVisitors($evaldRoot){ 261 262 $visitors = array(); 263 $visitors[] = new Less_Visitor_joinSelector(); 264 if( self::$has_extends ){ 265 $visitors[] = new Less_Visitor_processExtends(); 266 } 267 $visitors[] = new Less_Visitor_toCSS(); 268 269 270 if( Less_Parser::$options['plugins'] ){ 271 foreach(Less_Parser::$options['plugins'] as $plugin){ 272 if( property_exists($plugin,'isPreEvalVisitor') && $plugin->isPreEvalVisitor ){ 273 continue; 274 } 275 276 if( property_exists($plugin,'isPreVisitor') && $plugin->isPreVisitor ){ 277 array_unshift( $visitors, $plugin); 278 }else{ 279 $visitors[] = $plugin; 280 } 281 } 282 } 283 284 285 for($i = 0; $i < count($visitors); $i++ ){ 286 $visitors[$i]->run($evaldRoot); 287 } 288 289 } 290 291 292 /** 293 * Parse a Less string into css 294 * 295 * @param string $str The string to convert 296 * @param string $uri_root The url of the file 297 * @return Less_Tree_Ruleset|Less_Parser 298 */ 299 public function parse( $str, $file_uri = null ){ 300 301 if( !$file_uri ){ 302 $uri_root = ''; 303 $filename = 'anonymous-file-'.Less_Parser::$next_id++.'.less'; 304 }else{ 305 $file_uri = self::WinPath($file_uri); 306 $filename = $file_uri; 307 $uri_root = dirname($file_uri); 308 } 309 310 $previousFileInfo = $this->env->currentFileInfo; 311 $uri_root = self::WinPath($uri_root); 312 $this->SetFileInfo($filename, $uri_root); 313 314 $this->input = $str; 315 $this->_parse(); 316 317 if( $previousFileInfo ){ 318 $this->env->currentFileInfo = $previousFileInfo; 319 } 320 321 return $this; 322 } 323 324 325 /** 326 * Parse a Less string from a given file 327 * 328 * @throws Less_Exception_Parser 329 * @param string $filename The file to parse 330 * @param string $uri_root The url of the file 331 * @param bool $returnRoot Indicates whether the return value should be a css string a root node 332 * @return Less_Tree_Ruleset|Less_Parser 333 */ 334 public function parseFile( $filename, $uri_root = '', $returnRoot = false){ 335 336 if( !file_exists($filename) ){ 337 $this->Error(sprintf('File `%s` not found.', $filename)); 338 } 339 340 341 // fix uri_root? 342 // Instead of The mixture of file path for the first argument and directory path for the second argument has bee 343 if( !$returnRoot && !empty($uri_root) && basename($uri_root) == basename($filename) ){ 344 $uri_root = dirname($uri_root); 345 } 346 347 348 $previousFileInfo = $this->env->currentFileInfo; 349 350 351 if( $filename ){ 352 $filename = self::WinPath(realpath($filename)); 353 } 354 $uri_root = self::WinPath($uri_root); 355 356 $this->SetFileInfo($filename, $uri_root); 357 358 self::AddParsedFile($filename); 359 360 if( $returnRoot ){ 361 $rules = $this->GetRules( $filename ); 362 $return = new Less_Tree_Ruleset(array(), $rules ); 363 }else{ 364 $this->_parse( $filename ); 365 $return = $this; 366 } 367 368 if( $previousFileInfo ){ 369 $this->env->currentFileInfo = $previousFileInfo; 370 } 371 372 return $return; 373 } 374 375 376 /** 377 * Allows a user to set variables values 378 * @param array $vars 379 * @return Less_Parser 380 */ 381 public function ModifyVars( $vars ){ 382 383 $this->input = Less_Parser::serializeVars( $vars ); 384 $this->_parse(); 385 386 return $this; 387 } 388 389 390 /** 391 * @param string $filename 392 */ 393 public function SetFileInfo( $filename, $uri_root = ''){ 394 395 $filename = Less_Environment::normalizePath($filename); 396 $dirname = preg_replace('/[^\/\\\\]*$/','',$filename); 397 398 if( !empty($uri_root) ){ 399 $uri_root = rtrim($uri_root,'/').'/'; 400 } 401 402 $currentFileInfo = array(); 403 404 //entry info 405 if( isset($this->env->currentFileInfo) ){ 406 $currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath']; 407 $currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri']; 408 $currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath']; 409 410 }else{ 411 $currentFileInfo['entryPath'] = $dirname; 412 $currentFileInfo['entryUri'] = $uri_root; 413 $currentFileInfo['rootpath'] = $dirname; 414 } 415 416 $currentFileInfo['currentDirectory'] = $dirname; 417 $currentFileInfo['currentUri'] = $uri_root.basename($filename); 418 $currentFileInfo['filename'] = $filename; 419 $currentFileInfo['uri_root'] = $uri_root; 420 421 422 //inherit reference 423 if( isset($this->env->currentFileInfo['reference']) && $this->env->currentFileInfo['reference'] ){ 424 $currentFileInfo['reference'] = true; 425 } 426 427 $this->env->currentFileInfo = $currentFileInfo; 428 } 429 430 431 /** 432 * @deprecated 1.5.1.2 433 * 434 */ 435 public function SetCacheDir( $dir ){ 436 437 if( !file_exists($dir) ){ 438 if( mkdir($dir) ){ 439 return true; 440 } 441 throw new Less_Exception_Parser('Less.php cache directory couldn\'t be created: '.$dir); 442 443 }elseif( !is_dir($dir) ){ 444 throw new Less_Exception_Parser('Less.php cache directory doesn\'t exist: '.$dir); 445 446 }elseif( !is_writable($dir) ){ 447 throw new Less_Exception_Parser('Less.php cache directory isn\'t writable: '.$dir); 448 449 }else{ 450 $dir = self::WinPath($dir); 451 Less_Cache::$cache_dir = rtrim($dir,'/').'/'; 452 return true; 453 } 454 } 455 456 457 /** 458 * Set a list of directories or callbacks the parser should use for determining import paths 459 * 460 * @param array $dirs 461 */ 462 public function SetImportDirs( $dirs ){ 463 Less_Parser::$options['import_dirs'] = array(); 464 465 foreach($dirs as $path => $uri_root){ 466 467 $path = self::WinPath($path); 468 if( !empty($path) ){ 469 $path = rtrim($path,'/').'/'; 470 } 471 472 if ( !is_callable($uri_root) ){ 473 $uri_root = self::WinPath($uri_root); 474 if( !empty($uri_root) ){ 475 $uri_root = rtrim($uri_root,'/').'/'; 476 } 477 } 478 479 Less_Parser::$options['import_dirs'][$path] = $uri_root; 480 } 481 } 482 483 /** 484 * @param string $file_path 485 */ 486 private function _parse( $file_path = null ){ 487 $this->rules = array_merge($this->rules, $this->GetRules( $file_path )); 488 } 489 490 491 /** 492 * Return the results of parsePrimary for $file_path 493 * Use cache and save cached results if possible 494 * 495 * @param string|null $file_path 496 */ 497 private function GetRules( $file_path ){ 498 499 $this->SetInput($file_path); 500 501 $cache_file = $this->CacheFile( $file_path ); 502 if( $cache_file ){ 503 if( Less_Parser::$options['cache_method'] == 'callback' ){ 504 if( is_callable(Less_Parser::$options['cache_callback_get']) ){ 505 $cache = call_user_func_array( 506 Less_Parser::$options['cache_callback_get'], 507 array($this, $file_path, $cache_file) 508 ); 509 510 if( $cache ){ 511 $this->UnsetInput(); 512 return $cache; 513 } 514 } 515 516 }elseif( file_exists($cache_file) ){ 517 switch(Less_Parser::$options['cache_method']){ 518 519 // Using serialize 520 // Faster but uses more memory 521 case 'serialize': 522 $cache = unserialize(file_get_contents($cache_file)); 523 if( $cache ){ 524 touch($cache_file); 525 $this->UnsetInput(); 526 return $cache; 527 } 528 break; 529 530 531 // Using generated php code 532 case 'var_export': 533 case 'php': 534 $this->UnsetInput(); 535 return include($cache_file); 536 } 537 } 538 } 539 540 $rules = $this->parsePrimary(); 541 542 if( $this->pos < $this->input_len ){ 543 throw new Less_Exception_Chunk($this->input, null, $this->furthest, $this->env->currentFileInfo); 544 } 545 546 $this->UnsetInput(); 547 548 549 //save the cache 550 if( $cache_file ){ 551 if( Less_Parser::$options['cache_method'] == 'callback' ){ 552 if( is_callable(Less_Parser::$options['cache_callback_set']) ){ 553 call_user_func_array( 554 Less_Parser::$options['cache_callback_set'], 555 array($this, $file_path, $cache_file, $rules) 556 ); 557 } 558 559 }else{ 560 //msg('write cache file'); 561 switch(Less_Parser::$options['cache_method']){ 562 case 'serialize': 563 file_put_contents( $cache_file, serialize($rules) ); 564 break; 565 case 'php': 566 file_put_contents( $cache_file, '<?php return '.self::ArgString($rules).'; ?>' ); 567 break; 568 case 'var_export': 569 //Requires __set_state() 570 file_put_contents( $cache_file, '<?php return '.var_export($rules,true).'; ?>' ); 571 break; 572 } 573 574 Less_Cache::CleanCache(); 575 } 576 } 577 578 return $rules; 579 } 580 581 582 /** 583 * Set up the input buffer 584 * 585 */ 586 public function SetInput( $file_path ){ 587 588 if( $file_path ){ 589 $this->input = file_get_contents( $file_path ); 590 } 591 592 $this->pos = $this->furthest = 0; 593 594 // Remove potential UTF Byte Order Mark 595 $this->input = preg_replace('/\\G\xEF\xBB\xBF/', '', $this->input); 596 $this->input_len = strlen($this->input); 597 598 599 if( Less_Parser::$options['sourceMap'] && $this->env->currentFileInfo ){ 600 $uri = $this->env->currentFileInfo['currentUri']; 601 Less_Parser::$contentsMap[$uri] = $this->input; 602 } 603 604 } 605 606 607 /** 608 * Free up some memory 609 * 610 */ 611 public function UnsetInput(){ 612 unset($this->input, $this->pos, $this->input_len, $this->furthest); 613 $this->saveStack = array(); 614 } 615 616 617 public function CacheFile( $file_path ){ 618 619 if( $file_path && $this->CacheEnabled() ){ 620 621 $env = get_object_vars($this->env); 622 unset($env['frames']); 623 624 $parts = array(); 625 $parts[] = $file_path; 626 $parts[] = filesize( $file_path ); 627 $parts[] = filemtime( $file_path ); 628 $parts[] = $env; 629 $parts[] = Less_Version::cache_version; 630 $parts[] = Less_Parser::$options['cache_method']; 631 return Less_Cache::$cache_dir . Less_Cache::$prefix . base_convert( sha1(json_encode($parts) ), 16, 36) . '.lesscache'; 632 } 633 } 634 635 636 static function AddParsedFile($file){ 637 self::$imports[] = $file; 638 } 639 640 static function AllParsedFiles(){ 641 return self::$imports; 642 } 643 644 /** 645 * @param string $file 646 */ 647 static function FileParsed($file){ 648 return in_array($file,self::$imports); 649 } 650 651 652 function save() { 653 $this->saveStack[] = $this->pos; 654 } 655 656 private function restore() { 657 $this->pos = array_pop($this->saveStack); 658 } 659 660 private function forget(){ 661 array_pop($this->saveStack); 662 } 663 664 665 private function isWhitespace($offset = 0) { 666 return preg_match('/\s/',$this->input[ $this->pos + $offset]); 667 } 668 669 /** 670 * Parse from a token, regexp or string, and move forward if match 671 * 672 * @param array $toks 673 * @return array 674 */ 675 private function match($toks){ 676 677 // The match is confirmed, add the match length to `this::pos`, 678 // and consume any extra white-space characters (' ' || '\n') 679 // which come after that. The reason for this is that LeSS's 680 // grammar is mostly white-space insensitive. 681 // 682 683 foreach($toks as $tok){ 684 685 $char = $tok[0]; 686 687 if( $char === '/' ){ 688 $match = $this->MatchReg($tok); 689 690 if( $match ){ 691 return count($match) === 1 ? $match[0] : $match; 692 } 693 694 }elseif( $char === '#' ){ 695 $match = $this->MatchChar($tok[1]); 696 697 }else{ 698 // Non-terminal, match using a function call 699 $match = $this->$tok(); 700 701 } 702 703 if( $match ){ 704 return $match; 705 } 706 } 707 } 708 709 /** 710 * @param string[] $toks 711 * 712 * @return string 713 */ 714 private function MatchFuncs($toks){ 715 716 if( $this->pos < $this->input_len ){ 717 foreach($toks as $tok){ 718 $match = $this->$tok(); 719 if( $match ){ 720 return $match; 721 } 722 } 723 } 724 725 } 726 727 // Match a single character in the input, 728 private function MatchChar($tok){ 729 if( ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok) ){ 730 $this->skipWhitespace(1); 731 return $tok; 732 } 733 } 734 735 // Match a regexp from the current start point 736 private function MatchReg($tok){ 737 738 if( preg_match($tok, $this->input, $match, 0, $this->pos) ){ 739 $this->skipWhitespace(strlen($match[0])); 740 return $match; 741 } 742 } 743 744 745 /** 746 * Same as match(), but don't change the state of the parser, 747 * just return the match. 748 * 749 * @param string $tok 750 * @return integer 751 */ 752 public function PeekReg($tok){ 753 return preg_match($tok, $this->input, $match, 0, $this->pos); 754 } 755 756 /** 757 * @param string $tok 758 */ 759 public function PeekChar($tok){ 760 //return ($this->input[$this->pos] === $tok ); 761 return ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok ); 762 } 763 764 765 /** 766 * @param integer $length 767 */ 768 public function skipWhitespace($length){ 769 770 $this->pos += $length; 771 772 for(; $this->pos < $this->input_len; $this->pos++ ){ 773 $c = $this->input[$this->pos]; 774 775 if( ($c !== "\n") && ($c !== "\r") && ($c !== "\t") && ($c !== ' ') ){ 776 break; 777 } 778 } 779 } 780 781 782 /** 783 * @param string $tok 784 * @param string|null $msg 785 */ 786 public function expect($tok, $msg = NULL) { 787 $result = $this->match( array($tok) ); 788 if (!$result) { 789 $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg ); 790 } else { 791 return $result; 792 } 793 } 794 795 /** 796 * @param string $tok 797 */ 798 public function expectChar($tok, $msg = null ){ 799 $result = $this->MatchChar($tok); 800 if( !$result ){ 801 $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg ); 802 }else{ 803 return $result; 804 } 805 } 806 807 // 808 // Here in, the parsing rules/functions 809 // 810 // The basic structure of the syntax tree generated is as follows: 811 // 812 // Ruleset -> Rule -> Value -> Expression -> Entity 813 // 814 // Here's some LESS code: 815 // 816 // .class { 817 // color: #fff; 818 // border: 1px solid #000; 819 // width: @w + 4px; 820 // > .child {...} 821 // } 822 // 823 // And here's what the parse tree might look like: 824 // 825 // Ruleset (Selector '.class', [ 826 // Rule ("color", Value ([Expression [Color #fff]])) 827 // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]])) 828 // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]])) 829 // Ruleset (Selector [Element '>', '.child'], [...]) 830 // ]) 831 // 832 // In general, most rules will try to parse a token with the `$()` function, and if the return 833 // value is truly, will return a new node, of the relevant type. Sometimes, we need to check 834 // first, before parsing, that's when we use `peek()`. 835 // 836 837 // 838 // The `primary` rule is the *entry* and *exit* point of the parser. 839 // The rules here can appear at any level of the parse tree. 840 // 841 // The recursive nature of the grammar is an interplay between the `block` 842 // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, 843 // as represented by this simplified grammar: 844 // 845 // primary → (ruleset | rule)+ 846 // ruleset → selector+ block 847 // block → '{' primary '}' 848 // 849 // Only at one point is the primary rule not called from the 850 // block rule: at the root level. 851 // 852 private function parsePrimary(){ 853 $root = array(); 854 855 while( true ){ 856 857 if( $this->pos >= $this->input_len ){ 858 break; 859 } 860 861 $node = $this->parseExtend(true); 862 if( $node ){ 863 $root = array_merge($root,$node); 864 continue; 865 } 866 867 //$node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseDirective')); 868 $node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseNameValue', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseRulesetCall', 'parseDirective')); 869 870 if( $node ){ 871 $root[] = $node; 872 }elseif( !$this->MatchReg('/\\G[\s\n;]+/') ){ 873 break; 874 } 875 876 if( $this->PeekChar('}') ){ 877 break; 878 } 879 } 880 881 return $root; 882 } 883 884 885 886 // We create a Comment node for CSS comments `/* */`, 887 // but keep the LeSS comments `//` silent, by just skipping 888 // over them. 889 private function parseComment(){ 890 891 if( $this->input[$this->pos] !== '/' ){ 892 return; 893 } 894 895 if( $this->input[$this->pos+1] === '/' ){ 896 $match = $this->MatchReg('/\\G\/\/.*/'); 897 return $this->NewObj4('Less_Tree_Comment',array($match[0], true, $this->pos, $this->env->currentFileInfo)); 898 } 899 900 //$comment = $this->MatchReg('/\\G\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/'); 901 $comment = $this->MatchReg('/\\G\/\*(?s).*?\*+\/\n?/');//not the same as less.js to prevent fatal errors 902 if( $comment ){ 903 return $this->NewObj4('Less_Tree_Comment',array($comment[0], false, $this->pos, $this->env->currentFileInfo)); 904 } 905 } 906 907 private function parseComments(){ 908 $comments = array(); 909 910 while( $this->pos < $this->input_len ){ 911 $comment = $this->parseComment(); 912 if( !$comment ){ 913 break; 914 } 915 916 $comments[] = $comment; 917 } 918 919 return $comments; 920 } 921 922 923 924 // 925 // A string, which supports escaping " and ' 926 // 927 // "milky way" 'he\'s the one!' 928 // 929 private function parseEntitiesQuoted() { 930 $j = $this->pos; 931 $e = false; 932 $index = $this->pos; 933 934 if( $this->input[$this->pos] === '~' ){ 935 $j++; 936 $e = true; // Escaped strings 937 } 938 939 if( $this->input[$j] != '"' && $this->input[$j] !== "'" ){ 940 return; 941 } 942 943 if ($e) { 944 $this->MatchChar('~'); 945 } 946 947 // Fix for #124: match escaped newlines 948 //$str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.)*)"|\'((?:[^\'\\\\\r\n]|\\\\.)*)\'/'); 949 $str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)"|\'((?:[^\'\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)\'/'); 950 951 if( $str ){ 952 $result = $str[0][0] == '"' ? $str[1] : $str[2]; 953 return $this->NewObj5('Less_Tree_Quoted',array($str[0], $result, $e, $index, $this->env->currentFileInfo) ); 954 } 955 return; 956 } 957 958 959 // 960 // A catch-all word, such as: 961 // 962 // black border-collapse 963 // 964 private function parseEntitiesKeyword(){ 965 966 //$k = $this->MatchReg('/\\G[_A-Za-z-][_A-Za-z0-9-]*/'); 967 $k = $this->MatchReg('/\\G%|\\G[_A-Za-z-][_A-Za-z0-9-]*/'); 968 if( $k ){ 969 $k = $k[0]; 970 $color = $this->fromKeyword($k); 971 if( $color ){ 972 return $color; 973 } 974 return $this->NewObj1('Less_Tree_Keyword',$k); 975 } 976 } 977 978 // duplicate of Less_Tree_Color::FromKeyword 979 private function FromKeyword( $keyword ){ 980 $keyword = strtolower($keyword); 981 982 if( Less_Colors::hasOwnProperty($keyword) ){ 983 // detect named color 984 return $this->NewObj1('Less_Tree_Color',substr(Less_Colors::color($keyword), 1)); 985 } 986 987 if( $keyword === 'transparent' ){ 988 return $this->NewObj3('Less_Tree_Color', array( array(0, 0, 0), 0, true)); 989 } 990 } 991 992 // 993 // A function call 994 // 995 // rgb(255, 0, 255) 996 // 997 // We also try to catch IE's `alpha()`, but let the `alpha` parser 998 // deal with the details. 999 // 1000 // The arguments are parsed with the `entities.arguments` parser. 1001 // 1002 private function parseEntitiesCall(){ 1003 $index = $this->pos; 1004 1005 if( !preg_match('/\\G([\w-]+|%|progid:[\w\.]+)\(/', $this->input, $name,0,$this->pos) ){ 1006 return; 1007 } 1008 $name = $name[1]; 1009 $nameLC = strtolower($name); 1010 1011 if ($nameLC === 'url') { 1012 return null; 1013 } 1014 1015 $this->pos += strlen($name); 1016 1017 if( $nameLC === 'alpha' ){ 1018 $alpha_ret = $this->parseAlpha(); 1019 if( $alpha_ret ){ 1020 return $alpha_ret; 1021 } 1022 } 1023 1024 $this->MatchChar('('); // Parse the '(' and consume whitespace. 1025 1026 $args = $this->parseEntitiesArguments(); 1027 1028 if( !$this->MatchChar(')') ){ 1029 return; 1030 } 1031 1032 if ($name) { 1033 return $this->NewObj4('Less_Tree_Call',array($name, $args, $index, $this->env->currentFileInfo) ); 1034 } 1035 } 1036 1037 /** 1038 * Parse a list of arguments 1039 * 1040 * @return array 1041 */ 1042 private function parseEntitiesArguments(){ 1043 1044 $args = array(); 1045 while( true ){ 1046 $arg = $this->MatchFuncs( array('parseEntitiesAssignment','parseExpression') ); 1047 if( !$arg ){ 1048 break; 1049 } 1050 1051 $args[] = $arg; 1052 if( !$this->MatchChar(',') ){ 1053 break; 1054 } 1055 } 1056 return $args; 1057 } 1058 1059 private function parseEntitiesLiteral(){ 1060 return $this->MatchFuncs( array('parseEntitiesDimension','parseEntitiesColor','parseEntitiesQuoted','parseUnicodeDescriptor') ); 1061 } 1062 1063 // Assignments are argument entities for calls. 1064 // They are present in ie filter properties as shown below. 1065 // 1066 // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) 1067 // 1068 private function parseEntitiesAssignment() { 1069 1070 $key = $this->MatchReg('/\\G\w+(?=\s?=)/'); 1071 if( !$key ){ 1072 return; 1073 } 1074 1075 if( !$this->MatchChar('=') ){ 1076 return; 1077 } 1078 1079 $value = $this->parseEntity(); 1080 if( $value ){ 1081 return $this->NewObj2('Less_Tree_Assignment',array($key[0], $value)); 1082 } 1083 } 1084 1085 // 1086 // Parse url() tokens 1087 // 1088 // We use a specific rule for urls, because they don't really behave like 1089 // standard function calls. The difference is that the argument doesn't have 1090 // to be enclosed within a string, so it can't be parsed as an Expression. 1091 // 1092 private function parseEntitiesUrl(){ 1093 1094 1095 if( $this->input[$this->pos] !== 'u' || !$this->matchReg('/\\Gurl\(/') ){ 1096 return; 1097 } 1098 1099 $value = $this->match( array('parseEntitiesQuoted','parseEntitiesVariable','/\\Gdata\:.*?[^\)]+/','/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/') ); 1100 if( !$value ){ 1101 $value = ''; 1102 } 1103 1104 1105 $this->expectChar(')'); 1106 1107 1108 if( isset($value->value) || $value instanceof Less_Tree_Variable ){ 1109 return $this->NewObj2('Less_Tree_Url',array($value, $this->env->currentFileInfo)); 1110 } 1111 1112 return $this->NewObj2('Less_Tree_Url', array( $this->NewObj1('Less_Tree_Anonymous',$value), $this->env->currentFileInfo) ); 1113 } 1114 1115 1116 // 1117 // A Variable entity, such as `@fink`, in 1118 // 1119 // width: @fink + 2px 1120 // 1121 // We use a different parser for variable definitions, 1122 // see `parsers.variable`. 1123 // 1124 private function parseEntitiesVariable(){ 1125 $index = $this->pos; 1126 if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G@@?[\w-]+/'))) { 1127 return $this->NewObj3('Less_Tree_Variable', array( $name[0], $index, $this->env->currentFileInfo)); 1128 } 1129 } 1130 1131 1132 // A variable entity useing the protective {} e.g. @{var} 1133 private function parseEntitiesVariableCurly() { 1134 $index = $this->pos; 1135 1136 if( $this->input_len > ($this->pos+1) && $this->input[$this->pos] === '@' && ($curly = $this->MatchReg('/\\G@\{([\w-]+)\}/')) ){ 1137 return $this->NewObj3('Less_Tree_Variable',array('@'.$curly[1], $index, $this->env->currentFileInfo)); 1138 } 1139 } 1140 1141 // 1142 // A Hexadecimal color 1143 // 1144 // #4F3C2F 1145 // 1146 // `rgb` and `hsl` colors are parsed through the `entities.call` parser. 1147 // 1148 private function parseEntitiesColor(){ 1149 if ($this->PeekChar('#') && ($rgb = $this->MatchReg('/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/'))) { 1150 return $this->NewObj1('Less_Tree_Color',$rgb[1]); 1151 } 1152 } 1153 1154 // 1155 // A Dimension, that is, a number and a unit 1156 // 1157 // 0.5em 95% 1158 // 1159 private function parseEntitiesDimension(){ 1160 1161 $c = @ord($this->input[$this->pos]); 1162 1163 //Is the first char of the dimension 0-9, '.', '+' or '-' 1164 if (($c > 57 || $c < 43) || $c === 47 || $c == 44){ 1165 return; 1166 } 1167 1168 $value = $this->MatchReg('/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/'); 1169 if( $value ){ 1170 1171 if( isset($value[2]) ){ 1172 return $this->NewObj2('Less_Tree_Dimension', array($value[1],$value[2])); 1173 } 1174 return $this->NewObj1('Less_Tree_Dimension',$value[1]); 1175 } 1176 } 1177 1178 1179 // 1180 // A unicode descriptor, as is used in unicode-range 1181 // 1182 // U+0?? or U+00A1-00A9 1183 // 1184 function parseUnicodeDescriptor() { 1185 $ud = $this->MatchReg('/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/'); 1186 if( $ud ){ 1187 return $this->NewObj1('Less_Tree_UnicodeDescriptor', $ud[0]); 1188 } 1189 } 1190 1191 1192 // 1193 // JavaScript code to be evaluated 1194 // 1195 // `window.location.href` 1196 // 1197 private function parseEntitiesJavascript(){ 1198 $e = false; 1199 $j = $this->pos; 1200 if( $this->input[$j] === '~' ){ 1201 $j++; 1202 $e = true; 1203 } 1204 if( $this->input[$j] !== '`' ){ 1205 return; 1206 } 1207 if( $e ){ 1208 $this->MatchChar('~'); 1209 } 1210 $str = $this->MatchReg('/\\G`([^`]*)`/'); 1211 if( $str ){ 1212 return $this->NewObj3('Less_Tree_Javascript', array($str[1], $this->pos, $e)); 1213 } 1214 } 1215 1216 1217 // 1218 // The variable part of a variable definition. Used in the `rule` parser 1219 // 1220 // @fink: 1221 // 1222 private function parseVariable(){ 1223 if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*:/'))) { 1224 return $name[1]; 1225 } 1226 } 1227 1228 1229 // 1230 // The variable part of a variable definition. Used in the `rule` parser 1231 // 1232 // @fink(); 1233 // 1234 private function parseRulesetCall(){ 1235 1236 if( $this->input[$this->pos] === '@' && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*\(\s*\)\s*;/')) ){ 1237 return $this->NewObj1('Less_Tree_RulesetCall', $name[1] ); 1238 } 1239 } 1240 1241 1242 // 1243 // extend syntax - used to extend selectors 1244 // 1245 function parseExtend($isRule = false){ 1246 1247 $index = $this->pos; 1248 $extendList = array(); 1249 1250 1251 if( !$this->MatchReg( $isRule ? '/\\G&:extend\(/' : '/\\G:extend\(/' ) ){ return; } 1252 1253 do{ 1254 $option = null; 1255 $elements = array(); 1256 while( true ){ 1257 $option = $this->MatchReg('/\\G(all)(?=\s*(\)|,))/'); 1258 if( $option ){ break; } 1259 $e = $this->parseElement(); 1260 if( !$e ){ break; } 1261 $elements[] = $e; 1262 } 1263 1264 if( $option ){ 1265 $option = $option[1]; 1266 } 1267 1268 $extendList[] = $this->NewObj3('Less_Tree_Extend', array( $this->NewObj1('Less_Tree_Selector',$elements), $option, $index )); 1269 1270 }while( $this->MatchChar(",") ); 1271 1272 $this->expect('/\\G\)/'); 1273 1274 if( $isRule ){ 1275 $this->expect('/\\G;/'); 1276 } 1277 1278 return $extendList; 1279 } 1280 1281 1282 // 1283 // A Mixin call, with an optional argument list 1284 // 1285 // #mixins > .square(#fff); 1286 // .rounded(4px, black); 1287 // .button; 1288 // 1289 // The `while` loop is there because mixins can be 1290 // namespaced, but we only support the child and descendant 1291 // selector for now. 1292 // 1293 private function parseMixinCall(){ 1294 1295 $char = $this->input[$this->pos]; 1296 if( $char !== '.' && $char !== '#' ){ 1297 return; 1298 } 1299 1300 $index = $this->pos; 1301 $this->save(); // stop us absorbing part of an invalid selector 1302 1303 $elements = $this->parseMixinCallElements(); 1304 1305 if( $elements ){ 1306 1307 if( $this->MatchChar('(') ){ 1308 $returned = $this->parseMixinArgs(true); 1309 $args = $returned['args']; 1310 $this->expectChar(')'); 1311 }else{ 1312 $args = array(); 1313 } 1314 1315 $important = $this->parseImportant(); 1316 1317 if( $this->parseEnd() ){ 1318 $this->forget(); 1319 return $this->NewObj5('Less_Tree_Mixin_Call', array( $elements, $args, $index, $this->env->currentFileInfo, $important)); 1320 } 1321 } 1322 1323 $this->restore(); 1324 } 1325 1326 1327 private function parseMixinCallElements(){ 1328 $elements = array(); 1329 $c = null; 1330 1331 while( true ){ 1332 $elemIndex = $this->pos; 1333 $e = $this->MatchReg('/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/'); 1334 if( !$e ){ 1335 break; 1336 } 1337 $elements[] = $this->NewObj4('Less_Tree_Element', array($c, $e[0], $elemIndex, $this->env->currentFileInfo)); 1338 $c = $this->MatchChar('>'); 1339 } 1340 1341 return $elements; 1342 } 1343 1344 1345 1346 /** 1347 * @param boolean $isCall 1348 */ 1349 private function parseMixinArgs( $isCall ){ 1350 $expressions = array(); 1351 $argsSemiColon = array(); 1352 $isSemiColonSeperated = null; 1353 $argsComma = array(); 1354 $expressionContainsNamed = null; 1355 $name = null; 1356 $returner = array('args'=>array(), 'variadic'=> false); 1357 1358 $this->save(); 1359 1360 while( true ){ 1361 if( $isCall ){ 1362 $arg = $this->MatchFuncs( array( 'parseDetachedRuleset','parseExpression' ) ); 1363 } else { 1364 $this->parseComments(); 1365 if( $this->input[ $this->pos ] === '.' && $this->MatchReg('/\\G\.{3}/') ){ 1366 $returner['variadic'] = true; 1367 if( $this->MatchChar(";") && !$isSemiColonSeperated ){ 1368 $isSemiColonSeperated = true; 1369 } 1370 1371 if( $isSemiColonSeperated ){ 1372 $argsSemiColon[] = array('variadic'=>true); 1373 }else{ 1374 $argsComma[] = array('variadic'=>true); 1375 } 1376 break; 1377 } 1378 $arg = $this->MatchFuncs( array('parseEntitiesVariable','parseEntitiesLiteral','parseEntitiesKeyword') ); 1379 } 1380 1381 if( !$arg ){ 1382 break; 1383 } 1384 1385 1386 $nameLoop = null; 1387 if( $arg instanceof Less_Tree_Expression ){ 1388 $arg->throwAwayComments(); 1389 } 1390 $value = $arg; 1391 $val = null; 1392 1393 if( $isCall ){ 1394 // Variable 1395 if( property_exists($arg,'value') && count($arg->value) == 1 ){ 1396 $val = $arg->value[0]; 1397 } 1398 } else { 1399 $val = $arg; 1400 } 1401 1402 1403 if( $val instanceof Less_Tree_Variable ){ 1404 1405 if( $this->MatchChar(':') ){ 1406 if( $expressions ){ 1407 if( $isSemiColonSeperated ){ 1408 $this->Error('Cannot mix ; and , as delimiter types'); 1409 } 1410 $expressionContainsNamed = true; 1411 } 1412 1413 // we do not support setting a ruleset as a default variable - it doesn't make sense 1414 // However if we do want to add it, there is nothing blocking it, just don't error 1415 // and remove isCall dependency below 1416 $value = null; 1417 if( $isCall ){ 1418 $value = $this->parseDetachedRuleset(); 1419 } 1420 if( !$value ){ 1421 $value = $this->parseExpression(); 1422 } 1423 1424 if( !$value ){ 1425 if( $isCall ){ 1426 $this->Error('could not understand value for named argument'); 1427 } else { 1428 $this->restore(); 1429 $returner['args'] = array(); 1430 return $returner; 1431 } 1432 } 1433 1434 $nameLoop = ($name = $val->name); 1435 }elseif( !$isCall && $this->MatchReg('/\\G\.{3}/') ){ 1436 $returner['variadic'] = true; 1437 if( $this->MatchChar(";") && !$isSemiColonSeperated ){ 1438 $isSemiColonSeperated = true; 1439 } 1440 if( $isSemiColonSeperated ){ 1441 $argsSemiColon[] = array('name'=> $arg->name, 'variadic' => true); 1442 }else{ 1443 $argsComma[] = array('name'=> $arg->name, 'variadic' => true); 1444 } 1445 break; 1446 }elseif( !$isCall ){ 1447 $name = $nameLoop = $val->name; 1448 $value = null; 1449 } 1450 } 1451 1452 if( $value ){ 1453 $expressions[] = $value; 1454 } 1455 1456 $argsComma[] = array('name'=>$nameLoop, 'value'=>$value ); 1457 1458 if( $this->MatchChar(',') ){ 1459 continue; 1460 } 1461 1462 if( $this->MatchChar(';') || $isSemiColonSeperated ){ 1463 1464 if( $expressionContainsNamed ){ 1465 $this->Error('Cannot mix ; and , as delimiter types'); 1466 } 1467 1468 $isSemiColonSeperated = true; 1469 1470 if( count($expressions) > 1 ){ 1471 $value = $this->NewObj1('Less_Tree_Value', $expressions); 1472 } 1473 $argsSemiColon[] = array('name'=>$name, 'value'=>$value ); 1474 1475 $name = null; 1476 $expressions = array(); 1477 $expressionContainsNamed = false; 1478 } 1479 } 1480 1481 $this->forget(); 1482 $returner['args'] = ($isSemiColonSeperated ? $argsSemiColon : $argsComma); 1483 return $returner; 1484 } 1485 1486 1487 1488 // 1489 // A Mixin definition, with a list of parameters 1490 // 1491 // .rounded (@radius: 2px, @color) { 1492 // ... 1493 // } 1494 // 1495 // Until we have a finer grained state-machine, we have to 1496 // do a look-ahead, to make sure we don't have a mixin call. 1497 // See the `rule` function for more information. 1498 // 1499 // We start by matching `.rounded (`, and then proceed on to 1500 // the argument list, which has optional default values. 1501 // We store the parameters in `params`, with a `value` key, 1502 // if there is a value, such as in the case of `@radius`. 1503 // 1504 // Once we've got our params list, and a closing `)`, we parse 1505 // the `{...}` block. 1506 // 1507 private function parseMixinDefinition(){ 1508 $cond = null; 1509 1510 $char = $this->input[$this->pos]; 1511 if( ($char !== '.' && $char !== '#') || ($char === '{' && $this->PeekReg('/\\G[^{]*\}/')) ){ 1512 return; 1513 } 1514 1515 $this->save(); 1516 1517 $match = $this->MatchReg('/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/'); 1518 if( $match ){ 1519 $name = $match[1]; 1520 1521 $argInfo = $this->parseMixinArgs( false ); 1522 $params = $argInfo['args']; 1523 $variadic = $argInfo['variadic']; 1524 1525 1526 // .mixincall("@{a}"); 1527 // looks a bit like a mixin definition.. 1528 // also 1529 // .mixincall(@a: {rule: set;}); 1530 // so we have to be nice and restore 1531 if( !$this->MatchChar(')') ){ 1532 $this->furthest = $this->pos; 1533 $this->restore(); 1534 return; 1535 } 1536 1537 1538 $this->parseComments(); 1539 1540 if ($this->MatchReg('/\\Gwhen/')) { // Guard 1541 $cond = $this->expect('parseConditions', 'Expected conditions'); 1542 } 1543 1544 $ruleset = $this->parseBlock(); 1545 1546 if( is_array($ruleset) ){ 1547 $this->forget(); 1548 return $this->NewObj5('Less_Tree_Mixin_Definition', array( $name, $params, $ruleset, $cond, $variadic)); 1549 } 1550 1551 $this->restore(); 1552 }else{ 1553 $this->forget(); 1554 } 1555 } 1556 1557 // 1558 // Entities are the smallest recognized token, 1559 // and can be found inside a rule's value. 1560 // 1561 private function parseEntity(){ 1562 1563 return $this->MatchFuncs( array('parseEntitiesLiteral','parseEntitiesVariable','parseEntitiesUrl','parseEntitiesCall','parseEntitiesKeyword','parseEntitiesJavascript','parseComment') ); 1564 } 1565 1566 // 1567 // A Rule terminator. Note that we use `peek()` to check for '}', 1568 // because the `block` rule will be expecting it, but we still need to make sure 1569 // it's there, if ';' was ommitted. 1570 // 1571 private function parseEnd(){ 1572 return $this->MatchChar(';') || $this->PeekChar('}'); 1573 } 1574 1575 // 1576 // IE's alpha function 1577 // 1578 // alpha(opacity=88) 1579 // 1580 private function parseAlpha(){ 1581 1582 if ( ! $this->MatchReg('/\\G\(opacity=/i')) { 1583 return; 1584 } 1585 1586 $value = $this->MatchReg('/\\G[0-9]+/'); 1587 if( $value ){ 1588 $value = $value[0]; 1589 }else{ 1590 $value = $this->parseEntitiesVariable(); 1591 if( !$value ){ 1592 return; 1593 } 1594 } 1595 1596 $this->expectChar(')'); 1597 return $this->NewObj1('Less_Tree_Alpha',$value); 1598 } 1599 1600 1601 // 1602 // A Selector Element 1603 // 1604 // div 1605 // + h1 1606 // #socks 1607 // input[type="text"] 1608 // 1609 // Elements are the building blocks for Selectors, 1610 // they are made out of a `Combinator` (see combinator rule), 1611 // and an element name, such as a tag a class, or `*`. 1612 // 1613 private function parseElement(){ 1614 $c = $this->parseCombinator(); 1615 $index = $this->pos; 1616 1617 $e = $this->match( array('/\\G(?:\d+\.\d+|\d+)%/', '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/', 1618 '#*', '#&', 'parseAttribute', '/\\G\([^()@]+\)/', '/\\G[\.#](?=@)/', 'parseEntitiesVariableCurly') ); 1619 1620 if( is_null($e) ){ 1621 $this->save(); 1622 if( $this->MatchChar('(') ){ 1623 if( ($v = $this->parseSelector()) && $this->MatchChar(')') ){ 1624 $e = $this->NewObj1('Less_Tree_Paren',$v); 1625 $this->forget(); 1626 }else{ 1627 $this->restore(); 1628 } 1629 }else{ 1630 $this->forget(); 1631 } 1632 } 1633 1634 if( !is_null($e) ){ 1635 return $this->NewObj4('Less_Tree_Element',array( $c, $e, $index, $this->env->currentFileInfo)); 1636 } 1637 } 1638 1639 // 1640 // Combinators combine elements together, in a Selector. 1641 // 1642 // Because our parser isn't white-space sensitive, special care 1643 // has to be taken, when parsing the descendant combinator, ` `, 1644 // as it's an empty space. We have to check the previous character 1645 // in the input, to see if it's a ` ` character. 1646 // 1647 private function parseCombinator(){ 1648 if( $this->pos < $this->input_len ){ 1649 $c = $this->input[$this->pos]; 1650 if ($c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ){ 1651 1652 $this->pos++; 1653 if( $this->input[$this->pos] === '^' ){ 1654 $c = '^^'; 1655 $this->pos++; 1656 } 1657 1658 $this->skipWhitespace(0); 1659 1660 return $c; 1661 } 1662 1663 if( $this->pos > 0 && $this->isWhitespace(-1) ){ 1664 return ' '; 1665 } 1666 } 1667 } 1668 1669 // 1670 // A CSS selector (see selector below) 1671 // with less extensions e.g. the ability to extend and guard 1672 // 1673 private function parseLessSelector(){ 1674 return $this->parseSelector(true); 1675 } 1676 1677 // 1678 // A CSS Selector 1679 // 1680 // .class > div + h1 1681 // li a:hover 1682 // 1683 // Selectors are made out of one or more Elements, see above. 1684 // 1685 private function parseSelector( $isLess = false ){ 1686 $elements = array(); 1687 $extendList = array(); 1688 $condition = null; 1689 $when = false; 1690 $extend = false; 1691 $e = null; 1692 $c = null; 1693 $index = $this->pos; 1694 1695 while( ($isLess && ($extend = $this->parseExtend())) || ($isLess && ($when = $this->MatchReg('/\\Gwhen/') )) || ($e = $this->parseElement()) ){ 1696 if( $when ){ 1697 $condition = $this->expect('parseConditions', 'expected condition'); 1698 }elseif( $condition ){ 1699 //error("CSS guard can only be used at the end of selector"); 1700 }elseif( $extend ){ 1701 $extendList = array_merge($extendList,$extend); 1702 }else{ 1703 //if( count($extendList) ){ 1704 //error("Extend can only be used at the end of selector"); 1705 //} 1706 if( $this->pos < $this->input_len ){ 1707 $c = $this->input[ $this->pos ]; 1708 } 1709 $elements[] = $e; 1710 $e = null; 1711 } 1712 1713 if( $c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')') { break; } 1714 } 1715 1716 if( $elements ){ 1717 return $this->NewObj5('Less_Tree_Selector',array($elements, $extendList, $condition, $index, $this->env->currentFileInfo)); 1718 } 1719 if( $extendList ) { 1720 $this->Error('Extend must be used to extend a selector, it cannot be used on its own'); 1721 } 1722 } 1723 1724 private function parseTag(){ 1725 return ( $tag = $this->MatchReg('/\\G[A-Za-z][A-Za-z-]*[0-9]?/') ) ? $tag : $this->MatchChar('*'); 1726 } 1727 1728 private function parseAttribute(){ 1729 1730 $val = null; 1731 1732 if( !$this->MatchChar('[') ){ 1733 return; 1734 } 1735 1736 $key = $this->parseEntitiesVariableCurly(); 1737 if( !$key ){ 1738 $key = $this->expect('/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/'); 1739 } 1740 1741 $op = $this->MatchReg('/\\G[|~*$^]?=/'); 1742 if( $op ){ 1743 $val = $this->match( array('parseEntitiesQuoted','/\\G[0-9]+%/','/\\G[\w-]+/','parseEntitiesVariableCurly') ); 1744 } 1745 1746 $this->expectChar(']'); 1747 1748 return $this->NewObj3('Less_Tree_Attribute',array( $key, $op[0], $val)); 1749 } 1750 1751 // 1752 // The `block` rule is used by `ruleset` and `mixin.definition`. 1753 // It's a wrapper around the `primary` rule, with added `{}`. 1754 // 1755 private function parseBlock(){ 1756 if( $this->MatchChar('{') ){ 1757 $content = $this->parsePrimary(); 1758 if( $this->MatchChar('}') ){ 1759 return $content; 1760 } 1761 } 1762 } 1763 1764 private function parseBlockRuleset(){ 1765 $block = $this->parseBlock(); 1766 1767 if( $block ){ 1768 $block = $this->NewObj2('Less_Tree_Ruleset',array( null, $block)); 1769 } 1770 1771 return $block; 1772 } 1773 1774 private function parseDetachedRuleset(){ 1775 $blockRuleset = $this->parseBlockRuleset(); 1776 if( $blockRuleset ){ 1777 return $this->NewObj1('Less_Tree_DetachedRuleset',$blockRuleset); 1778 } 1779 } 1780 1781 // 1782 // div, .class, body > p {...} 1783 // 1784 private function parseRuleset(){ 1785 $selectors = array(); 1786 1787 $this->save(); 1788 1789 while( true ){ 1790 $s = $this->parseLessSelector(); 1791 if( !$s ){ 1792 break; 1793 } 1794 $selectors[] = $s; 1795 $this->parseComments(); 1796 1797 if( $s->condition && count($selectors) > 1 ){ 1798 $this->Error('Guards are only currently allowed on a single selector.'); 1799 } 1800 1801 if( !$this->MatchChar(',') ){ 1802 break; 1803 } 1804 if( $s->condition ){ 1805 $this->Error('Guards are only currently allowed on a single selector.'); 1806 } 1807 $this->parseComments(); 1808 } 1809 1810 1811 if( $selectors ){ 1812 $rules = $this->parseBlock(); 1813 if( is_array($rules) ){ 1814 $this->forget(); 1815 return $this->NewObj2('Less_Tree_Ruleset',array( $selectors, $rules)); //Less_Environment::$strictImports 1816 } 1817 } 1818 1819 // Backtrack 1820 $this->furthest = $this->pos; 1821 $this->restore(); 1822 } 1823 1824 /** 1825 * Custom less.php parse function for finding simple name-value css pairs 1826 * ex: width:100px; 1827 * 1828 */ 1829 private function parseNameValue(){ 1830 1831 $index = $this->pos; 1832 $this->save(); 1833 1834 1835 //$match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*((?:\'")?[a-zA-Z0-9\-% \.,!]+?(?:\'")?)\s*([;}])/'); 1836 $match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?) *(! *important)?\s*([;}])/'); 1837 if( $match ){ 1838 1839 if( $match[4] == '}' ){ 1840 $this->pos = $index + strlen($match[0])-1; 1841 } 1842 1843 if( $match[3] ){ 1844 $match[2] .= ' !important'; 1845 } 1846 1847 return $this->NewObj4('Less_Tree_NameValue',array( $match[1], $match[2], $index, $this->env->currentFileInfo)); 1848 } 1849 1850 $this->restore(); 1851 } 1852 1853 1854 private function parseRule( $tryAnonymous = null ){ 1855 1856 $merge = false; 1857 $startOfRule = $this->pos; 1858 1859 $c = $this->input[$this->pos]; 1860 if( $c === '.' || $c === '#' || $c === '&' ){ 1861 return; 1862 } 1863 1864 $this->save(); 1865 $name = $this->MatchFuncs( array('parseVariable','parseRuleProperty')); 1866 1867 if( $name ){ 1868 1869 $isVariable = is_string($name); 1870 1871 $value = null; 1872 if( $isVariable ){ 1873 $value = $this->parseDetachedRuleset(); 1874 } 1875 1876 $important = null; 1877 if( !$value ){ 1878 1879 // prefer to try to parse first if its a variable or we are compressing 1880 // but always fallback on the other one 1881 //if( !$tryAnonymous && is_string($name) && $name[0] === '@' ){ 1882 if( !$tryAnonymous && (Less_Parser::$options['compress'] || $isVariable) ){ 1883 $value = $this->MatchFuncs( array('parseValue','parseAnonymousValue')); 1884 }else{ 1885 $value = $this->MatchFuncs( array('parseAnonymousValue','parseValue')); 1886 } 1887 1888 $important = $this->parseImportant(); 1889 1890 // a name returned by this.ruleProperty() is always an array of the form: 1891 // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"] 1892 // where each item is a tree.Keyword or tree.Variable 1893 if( !$isVariable && is_array($name) ){ 1894 $nm = array_pop($name); 1895 if( $nm->value ){ 1896 $merge = $nm->value; 1897 } 1898 } 1899 } 1900 1901 1902 if( $value && $this->parseEnd() ){ 1903 $this->forget(); 1904 return $this->NewObj6('Less_Tree_Rule',array( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo)); 1905 }else{ 1906 $this->furthest = $this->pos; 1907 $this->restore(); 1908 if( $value && !$tryAnonymous ){ 1909 return $this->parseRule(true); 1910 } 1911 } 1912 }else{ 1913 $this->forget(); 1914 } 1915 } 1916 1917 function parseAnonymousValue(){ 1918 1919 if( preg_match('/\\G([^@+\/\'"*`(;{}-]*);/',$this->input, $match, 0, $this->pos) ){ 1920 $this->pos += strlen($match[1]); 1921 return $this->NewObj1('Less_Tree_Anonymous',$match[1]); 1922 } 1923 } 1924 1925 // 1926 // An @import directive 1927 // 1928 // @import "lib"; 1929 // 1930 // Depending on our environment, importing is done differently: 1931 // In the browser, it's an XHR request, in Node, it would be a 1932 // file-system operation. The function used for importing is 1933 // stored in `import`, which we pass to the Import constructor. 1934 // 1935 private function parseImport(){ 1936 1937 $this->save(); 1938 1939 $dir = $this->MatchReg('/\\G@import?\s+/'); 1940 1941 if( $dir ){ 1942 $options = $this->parseImportOptions(); 1943 $path = $this->MatchFuncs( array('parseEntitiesQuoted','parseEntitiesUrl')); 1944 1945 if( $path ){ 1946 $features = $this->parseMediaFeatures(); 1947 if( $this->MatchChar(';') ){ 1948 if( $features ){ 1949 $features = $this->NewObj1('Less_Tree_Value',$features); 1950 } 1951 1952 $this->forget(); 1953 return $this->NewObj5('Less_Tree_Import',array( $path, $features, $options, $this->pos, $this->env->currentFileInfo)); 1954 } 1955 } 1956 } 1957 1958 $this->restore(); 1959 } 1960 1961 private function parseImportOptions(){ 1962 1963 $options = array(); 1964 1965 // list of options, surrounded by parens 1966 if( !$this->MatchChar('(') ){ 1967 return $options; 1968 } 1969 do{ 1970 $optionName = $this->parseImportOption(); 1971 if( $optionName ){ 1972 $value = true; 1973 switch( $optionName ){ 1974 case "css": 1975 $optionName = "less"; 1976 $value = false; 1977 break; 1978 case "once": 1979 $optionName = "multiple"; 1980 $value = false; 1981 break; 1982 } 1983 $options[$optionName] = $value; 1984 if( !$this->MatchChar(',') ){ break; } 1985 } 1986 }while( $optionName ); 1987 $this->expectChar(')'); 1988 return $options; 1989 } 1990 1991 private function parseImportOption(){ 1992 $opt = $this->MatchReg('/\\G(less|css|multiple|once|inline|reference|optional)/'); 1993 if( $opt ){ 1994 return $opt[1]; 1995 } 1996 } 1997 1998 private function parseMediaFeature() { 1999 $nodes = array(); 2000 2001 do{ 2002 $e = $this->MatchFuncs(array('parseEntitiesKeyword','parseEntitiesVariable')); 2003 if( $e ){ 2004 $nodes[] = $e; 2005 } elseif ($this->MatchChar('(')) { 2006 $p = $this->parseProperty(); 2007 $e = $this->parseValue(); 2008 if ($this->MatchChar(')')) { 2009 if ($p && $e) { 2010 $r = $this->NewObj7('Less_Tree_Rule', array( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true)); 2011 $nodes[] = $this->NewObj1('Less_Tree_Paren',$r); 2012 } elseif ($e) { 2013 $nodes[] = $this->NewObj1('Less_Tree_Paren',$e); 2014 } else { 2015 return null; 2016 } 2017 } else 2018 return null; 2019 } 2020 } while ($e); 2021 2022 if ($nodes) { 2023 return $this->NewObj1('Less_Tree_Expression',$nodes); 2024 } 2025 } 2026 2027 private function parseMediaFeatures() { 2028 $features = array(); 2029 2030 do{ 2031 $e = $this->parseMediaFeature(); 2032 if( $e ){ 2033 $features[] = $e; 2034 if (!$this->MatchChar(',')) break; 2035 }else{ 2036 $e = $this->parseEntitiesVariable(); 2037 if( $e ){ 2038 $features[] = $e; 2039 if (!$this->MatchChar(',')) break; 2040 } 2041 } 2042 } while ($e); 2043 2044 return $features ? $features : null; 2045 } 2046 2047 private function parseMedia() { 2048 if( $this->MatchReg('/\\G@media/') ){ 2049 $features = $this->parseMediaFeatures(); 2050 $rules = $this->parseBlock(); 2051 2052 if( is_array($rules) ){ 2053 return $this->NewObj4('Less_Tree_Media',array( $rules, $features, $this->pos, $this->env->currentFileInfo)); 2054 } 2055 } 2056 } 2057 2058 2059 // 2060 // A CSS Directive 2061 // 2062 // @charset "utf-8"; 2063 // 2064 private function parseDirective(){ 2065 2066 if( !$this->PeekChar('@') ){ 2067 return; 2068 } 2069 2070 $rules = null; 2071 $index = $this->pos; 2072 $hasBlock = true; 2073 $hasIdentifier = false; 2074 $hasExpression = false; 2075 $hasUnknown = false; 2076 2077 2078 $value = $this->MatchFuncs(array('parseImport','parseMedia')); 2079 if( $value ){ 2080 return $value; 2081 } 2082 2083 $this->save(); 2084 2085 $name = $this->MatchReg('/\\G@[a-z-]+/'); 2086 2087 if( !$name ) return; 2088 $name = $name[0]; 2089 2090 2091 $nonVendorSpecificName = $name; 2092 $pos = strpos($name,'-', 2); 2093 if( $name[1] == '-' && $pos > 0 ){ 2094 $nonVendorSpecificName = "@" . substr($name, $pos + 1); 2095 } 2096 2097 2098 switch( $nonVendorSpecificName ){ 2099 /* 2100 case "@font-face": 2101 case "@viewport": 2102 case "@top-left": 2103 case "@top-left-corner": 2104 case "@top-center": 2105 case "@top-right": 2106 case "@top-right-corner": 2107 case "@bottom-left": 2108 case "@bottom-left-corner": 2109 case "@bottom-center": 2110 case "@bottom-right": 2111 case "@bottom-right-corner": 2112 case "@left-top": 2113 case "@left-middle": 2114 case "@left-bottom": 2115 case "@right-top": 2116 case "@right-middle": 2117 case "@right-bottom": 2118 hasBlock = true; 2119 break; 2120 */ 2121 case "@charset": 2122 $hasIdentifier = true; 2123 $hasBlock = false; 2124 break; 2125 case "@namespace": 2126 $hasExpression = true; 2127 $hasBlock = false; 2128 break; 2129 case "@keyframes": 2130 $hasIdentifier = true; 2131 break; 2132 case "@host": 2133 case "@page": 2134 case "@document": 2135 case "@supports": 2136 $hasUnknown = true; 2137 break; 2138 } 2139 2140 if( $hasIdentifier ){ 2141 $value = $this->parseEntity(); 2142 if( !$value ){ 2143 $this->error("expected " . $name . " identifier"); 2144 } 2145 } else if( $hasExpression ){ 2146 $value = $this->parseExpression(); 2147 if( !$value ){ 2148 $this->error("expected " . $name. " expression"); 2149 } 2150 } else if ($hasUnknown) { 2151 2152 $value = $this->MatchReg('/\\G[^{;]+/'); 2153 if( $value ){ 2154 $value = $this->NewObj1('Less_Tree_Anonymous',trim($value[0])); 2155 } 2156 } 2157 2158 if( $hasBlock ){ 2159 $rules = $this->parseBlockRuleset(); 2160 } 2161 2162 if( $rules || (!$hasBlock && $value && $this->MatchChar(';'))) { 2163 $this->forget(); 2164 return $this->NewObj5('Less_Tree_Directive',array($name, $value, $rules, $index, $this->env->currentFileInfo)); 2165 } 2166 2167 $this->restore(); 2168 } 2169 2170 2171 // 2172 // A Value is a comma-delimited list of Expressions 2173 // 2174 // font-family: Baskerville, Georgia, serif; 2175 // 2176 // In a Rule, a Value represents everything after the `:`, 2177 // and before the `;`. 2178 // 2179 private function parseValue(){ 2180 $expressions = array(); 2181 2182 do{ 2183 $e = $this->parseExpression(); 2184 if( $e ){ 2185 $expressions[] = $e; 2186 if (! $this->MatchChar(',')) { 2187 break; 2188 } 2189 } 2190 }while($e); 2191 2192 if( $expressions ){ 2193 return $this->NewObj1('Less_Tree_Value',$expressions); 2194 } 2195 } 2196 2197 private function parseImportant (){ 2198 if( $this->PeekChar('!') && $this->MatchReg('/\\G! *important/') ){ 2199 return ' !important'; 2200 } 2201 } 2202 2203 private function parseSub (){ 2204 2205 if( $this->MatchChar('(') ){ 2206 $a = $this->parseAddition(); 2207 if( $a ){ 2208 $this->expectChar(')'); 2209 return $this->NewObj2('Less_Tree_Expression',array( array($a), true) ); //instead of $e->parens = true so the value is cached 2210 } 2211 } 2212 } 2213 2214 2215 /** 2216 * Parses multiplication operation 2217 * 2218 * @return Less_Tree_Operation|null 2219 */ 2220 function parseMultiplication(){ 2221 2222 $return = $m = $this->parseOperand(); 2223 if( $return ){ 2224 while( true ){ 2225 2226 $isSpaced = $this->isWhitespace( -1 ); 2227 2228 if( $this->PeekReg('/\\G\/[*\/]/') ){ 2229 break; 2230 } 2231 2232 $op = $this->MatchChar('/'); 2233 if( !$op ){ 2234 $op = $this->MatchChar('*'); 2235 if( !$op ){ 2236 break; 2237 } 2238 } 2239 2240 $a = $this->parseOperand(); 2241 2242 if(!$a) { break; } 2243 2244 $m->parensInOp = true; 2245 $a->parensInOp = true; 2246 $return = $this->NewObj3('Less_Tree_Operation',array( $op, array( $return, $a ), $isSpaced) ); 2247 } 2248 } 2249 return $return; 2250 2251 } 2252 2253 2254 /** 2255 * Parses an addition operation 2256 * 2257 * @return Less_Tree_Operation|null 2258 */ 2259 private function parseAddition (){ 2260 2261 $return = $m = $this->parseMultiplication(); 2262 if( $return ){ 2263 while( true ){ 2264 2265 $isSpaced = $this->isWhitespace( -1 ); 2266 2267 $op = $this->MatchReg('/\\G[-+]\s+/'); 2268 if( $op ){ 2269 $op = $op[0]; 2270 }else{ 2271 if( !$isSpaced ){ 2272 $op = $this->match(array('#+','#-')); 2273 } 2274 if( !$op ){ 2275 break; 2276 } 2277 } 2278 2279 $a = $this->parseMultiplication(); 2280 if( !$a ){ 2281 break; 2282 } 2283 2284 $m->parensInOp = true; 2285 $a->parensInOp = true; 2286 $return = $this->NewObj3('Less_Tree_Operation',array($op, array($return, $a), $isSpaced)); 2287 } 2288 } 2289 2290 return $return; 2291 } 2292 2293 2294 /** 2295 * Parses the conditions 2296 * 2297 * @return Less_Tree_Condition|null 2298 */ 2299 private function parseConditions() { 2300 $index = $this->pos; 2301 $return = $a = $this->parseCondition(); 2302 if( $a ){ 2303 while( true ){ 2304 if( !$this->PeekReg('/\\G,\s*(not\s*)?\(/') || !$this->MatchChar(',') ){ 2305 break; 2306 } 2307 $b = $this->parseCondition(); 2308 if( !$b ){ 2309 break; 2310 } 2311 2312 $return = $this->NewObj4('Less_Tree_Condition',array('or', $return, $b, $index)); 2313 } 2314 return $return; 2315 } 2316 } 2317 2318 private function parseCondition() { 2319 $index = $this->pos; 2320 $negate = false; 2321 $c = null; 2322 2323 if ($this->MatchReg('/\\Gnot/')) $negate = true; 2324 $this->expectChar('('); 2325 $a = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted')); 2326 2327 if( $a ){ 2328 $op = $this->MatchReg('/\\G(?:>=|<=|=<|[<=>])/'); 2329 if( $op ){ 2330 $b = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted')); 2331 if( $b ){ 2332 $c = $this->NewObj5('Less_Tree_Condition',array($op[0], $a, $b, $index, $negate)); 2333 } else { 2334 $this->Error('Unexpected expression'); 2335 } 2336 } else { 2337 $k = $this->NewObj1('Less_Tree_Keyword','true'); 2338 $c = $this->NewObj5('Less_Tree_Condition',array('=', $a, $k, $index, $negate)); 2339 } 2340 $this->expectChar(')'); 2341 return $this->MatchReg('/\\Gand/') ? $this->NewObj3('Less_Tree_Condition',array('and', $c, $this->parseCondition())) : $c; 2342 } 2343 } 2344 2345 /** 2346 * An operand is anything that can be part of an operation, 2347 * such as a Color, or a Variable 2348 * 2349 */ 2350 private function parseOperand (){ 2351 2352 $negate = false; 2353 $offset = $this->pos+1; 2354 if( $offset >= $this->input_len ){ 2355 return; 2356 } 2357 $char = $this->input[$offset]; 2358 if( $char === '@' || $char === '(' ){ 2359 $negate = $this->MatchChar('-'); 2360 } 2361 2362 $o = $this->MatchFuncs(array('parseSub','parseEntitiesDimension','parseEntitiesColor','parseEntitiesVariable','parseEntitiesCall')); 2363 2364 if( $negate ){ 2365 $o->parensInOp = true; 2366 $o = $this->NewObj1('Less_Tree_Negative',$o); 2367 } 2368 2369 return $o; 2370 } 2371 2372 2373 /** 2374 * Expressions either represent mathematical operations, 2375 * or white-space delimited Entities. 2376 * 2377 * 1px solid black 2378 * @var * 2 2379 * 2380 * @return Less_Tree_Expression|null 2381 */ 2382 private function parseExpression (){ 2383 $entities = array(); 2384 2385 do{ 2386 $e = $this->MatchFuncs(array('parseAddition','parseEntity')); 2387 if( $e ){ 2388 $entities[] = $e; 2389 // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here 2390 if( !$this->PeekReg('/\\G\/[\/*]/') ){ 2391 $delim = $this->MatchChar('/'); 2392 if( $delim ){ 2393 $entities[] = $this->NewObj1('Less_Tree_Anonymous',$delim); 2394 } 2395 } 2396 } 2397 }while($e); 2398 2399 if( $entities ){ 2400 return $this->NewObj1('Less_Tree_Expression',$entities); 2401 } 2402 } 2403 2404 2405 /** 2406 * Parse a property 2407 * eg: 'min-width', 'orientation', etc 2408 * 2409 * @return string 2410 */ 2411 private function parseProperty (){ 2412 $name = $this->MatchReg('/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/'); 2413 if( $name ){ 2414 return $name[1]; 2415 } 2416 } 2417 2418 2419 /** 2420 * Parse a rule property 2421 * eg: 'color', 'width', 'height', etc 2422 * 2423 * @return string 2424 */ 2425 private function parseRuleProperty(){ 2426 $offset = $this->pos; 2427 $name = array(); 2428 $index = array(); 2429 $length = 0; 2430 2431 2432 $this->rulePropertyMatch('/\\G(\*?)/', $offset, $length, $index, $name ); 2433 while( $this->rulePropertyMatch('/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/', $offset, $length, $index, $name )); // ! 2434 2435 if( (count($name) > 1) && $this->rulePropertyMatch('/\\G\s*((?:\+_|\+)?)\s*:/', $offset, $length, $index, $name) ){ 2436 // at last, we have the complete match now. move forward, 2437 // convert name particles to tree objects and return: 2438 $this->skipWhitespace($length); 2439 2440 if( $name[0] === '' ){ 2441 array_shift($name); 2442 array_shift($index); 2443 } 2444 foreach($name as $k => $s ){ 2445 if( !$s || $s[0] !== '@' ){ 2446 $name[$k] = $this->NewObj1('Less_Tree_Keyword',$s); 2447 }else{ 2448 $name[$k] = $this->NewObj3('Less_Tree_Variable',array('@' . substr($s,2,-1), $index[$k], $this->env->currentFileInfo)); 2449 } 2450 } 2451 return $name; 2452 } 2453 2454 2455 } 2456 2457 private function rulePropertyMatch( $re, &$offset, &$length, &$index, &$name ){ 2458 preg_match($re, $this->input, $a, 0, $offset); 2459 if( $a ){ 2460 $index[] = $this->pos + $length; 2461 $length += strlen($a[0]); 2462 $offset += strlen($a[0]); 2463 $name[] = $a[1]; 2464 return true; 2465 } 2466 } 2467 2468 public static function serializeVars( $vars ){ 2469 $s = ''; 2470 2471 foreach($vars as $name => $value){ 2472 $s .= (($name[0] === '@') ? '' : '@') . $name .': '. $value . ((substr($value,-1) === ';') ? '' : ';'); 2473 } 2474 2475 return $s; 2476 } 2477 2478 2479 /** 2480 * Some versions of php have trouble with method_exists($a,$b) if $a is not an object 2481 * 2482 * @param string $b 2483 */ 2484 public static function is_method($a,$b){ 2485 return is_object($a) && method_exists($a,$b); 2486 } 2487 2488 2489 /** 2490 * Round numbers similarly to javascript 2491 * eg: 1.499999 to 1 instead of 2 2492 * 2493 */ 2494 public static function round($i, $precision = 0){ 2495 2496 $precision = pow(10,$precision); 2497 $i = $i*$precision; 2498 2499 $ceil = ceil($i); 2500 $floor = floor($i); 2501 if( ($ceil - $i) <= ($i - $floor) ){ 2502 return $ceil/$precision; 2503 }else{ 2504 return $floor/$precision; 2505 } 2506 } 2507 2508 2509 /** 2510 * Create Less_Tree_* objects and optionally generate a cache string 2511 * 2512 * @return mixed 2513 */ 2514 public function NewObj0($class){ 2515 $obj = new $class(); 2516 if( $this->CacheEnabled() ){ 2517 $obj->cache_string = ' new '.$class.'()'; 2518 } 2519 return $obj; 2520 } 2521 2522 public function NewObj1($class, $arg){ 2523 $obj = new $class( $arg ); 2524 if( $this->CacheEnabled() ){ 2525 $obj->cache_string = ' new '.$class.'('.Less_Parser::ArgString($arg).')'; 2526 } 2527 return $obj; 2528 } 2529 2530 public function NewObj2($class, $args){ 2531 $obj = new $class( $args[0], $args[1] ); 2532 if( $this->CacheEnabled() ){ 2533 $this->ObjCache( $obj, $class, $args); 2534 } 2535 return $obj; 2536 } 2537 2538 public function NewObj3($class, $args){ 2539 $obj = new $class( $args[0], $args[1], $args[2] ); 2540 if( $this->CacheEnabled() ){ 2541 $this->ObjCache( $obj, $class, $args); 2542 } 2543 return $obj; 2544 } 2545 2546 public function NewObj4($class, $args){ 2547 $obj = new $class( $args[0], $args[1], $args[2], $args[3] ); 2548 if( $this->CacheEnabled() ){ 2549 $this->ObjCache( $obj, $class, $args); 2550 } 2551 return $obj; 2552 } 2553 2554 public function NewObj5($class, $args){ 2555 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4] ); 2556 if( $this->CacheEnabled() ){ 2557 $this->ObjCache( $obj, $class, $args); 2558 } 2559 return $obj; 2560 } 2561 2562 public function NewObj6($class, $args){ 2563 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5] ); 2564 if( $this->CacheEnabled() ){ 2565 $this->ObjCache( $obj, $class, $args); 2566 } 2567 return $obj; 2568 } 2569 2570 public function NewObj7($class, $args){ 2571 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6] ); 2572 if( $this->CacheEnabled() ){ 2573 $this->ObjCache( $obj, $class, $args); 2574 } 2575 return $obj; 2576 } 2577 2578 //caching 2579 public function ObjCache($obj, $class, $args=array()){ 2580 $obj->cache_string = ' new '.$class.'('. self::ArgCache($args).')'; 2581 } 2582 2583 public function ArgCache($args){ 2584 return implode(',',array_map( array('Less_Parser','ArgString'),$args)); 2585 } 2586 2587 2588 /** 2589 * Convert an argument to a string for use in the parser cache 2590 * 2591 * @return string 2592 */ 2593 public static function ArgString($arg){ 2594 2595 $type = gettype($arg); 2596 2597 if( $type === 'object'){ 2598 $string = $arg->cache_string; 2599 unset($arg->cache_string); 2600 return $string; 2601 2602 }elseif( $type === 'array' ){ 2603 $string = ' Array('; 2604 foreach($arg as $k => $a){ 2605 $string .= var_export($k,true).' => '.self::ArgString($a).','; 2606 } 2607 return $string . ')'; 2608 } 2609 2610 return var_export($arg,true); 2611 } 2612 2613 public function Error($msg){ 2614 throw new Less_Exception_Parser($msg, null, $this->furthest, $this->env->currentFileInfo); 2615 } 2616 2617 public static function WinPath($path){ 2618 return str_replace('\\', '/', $path); 2619 } 2620 2621 public function CacheEnabled(){ 2622 return false; 2623 //return (Less_Parser::$options['cache_method'] && (Less_Cache::$cache_dir || (Less_Parser::$options['cache_method'] == 'callback'))); 2624 } 2625 2626 } 2627 2628 2629 /** 2630 * Utility for css colors 2631 * 2632 * @package Less 2633 * @subpackage color 2634 */ 2635 class Less_Colors { 2636 2637 public static $colors = array( 2638 'aliceblue'=>'#f0f8ff', 2639 'antiquewhite'=>'#faebd7', 2640 'aqua'=>'#00ffff', 2641 'aquamarine'=>'#7fffd4', 2642 'azure'=>'#f0ffff', 2643 'beige'=>'#f5f5dc', 2644 'bisque'=>'#ffe4c4', 2645 'black'=>'#000000', 2646 'blanchedalmond'=>'#ffebcd', 2647 'blue'=>'#0000ff', 2648 'blueviolet'=>'#8a2be2', 2649 'brown'=>'#a52a2a', 2650 'burlywood'=>'#deb887', 2651 'cadetblue'=>'#5f9ea0', 2652 'chartreuse'=>'#7fff00', 2653 'chocolate'=>'#d2691e', 2654 'coral'=>'#ff7f50', 2655 'cornflowerblue'=>'#6495ed', 2656 'cornsilk'=>'#fff8dc', 2657 'crimson'=>'#dc143c', 2658 'cyan'=>'#00ffff', 2659 'darkblue'=>'#00008b', 2660 'darkcyan'=>'#008b8b', 2661 'darkgoldenrod'=>'#b8860b', 2662 'darkgray'=>'#a9a9a9', 2663 'darkgrey'=>'#a9a9a9', 2664 'darkgreen'=>'#006400', 2665 'darkkhaki'=>'#bdb76b', 2666 'darkmagenta'=>'#8b008b', 2667 'darkolivegreen'=>'#556b2f', 2668 'darkorange'=>'#ff8c00', 2669 'darkorchid'=>'#9932cc', 2670 'darkred'=>'#8b0000', 2671 'darksalmon'=>'#e9967a', 2672 'darkseagreen'=>'#8fbc8f', 2673 'darkslateblue'=>'#483d8b', 2674 'darkslategray'=>'#2f4f4f', 2675 'darkslategrey'=>'#2f4f4f', 2676 'darkturquoise'=>'#00ced1', 2677 'darkviolet'=>'#9400d3', 2678 'deeppink'=>'#ff1493', 2679 'deepskyblue'=>'#00bfff', 2680 'dimgray'=>'#696969', 2681 'dimgrey'=>'#696969', 2682 'dodgerblue'=>'#1e90ff', 2683 'firebrick'=>'#b22222', 2684 'floralwhite'=>'#fffaf0', 2685 'forestgreen'=>'#228b22', 2686 'fuchsia'=>'#ff00ff', 2687 'gainsboro'=>'#dcdcdc', 2688 'ghostwhite'=>'#f8f8ff', 2689 'gold'=>'#ffd700', 2690 'goldenrod'=>'#daa520', 2691 'gray'=>'#808080', 2692 'grey'=>'#808080', 2693 'green'=>'#008000', 2694 'greenyellow'=>'#adff2f', 2695 'honeydew'=>'#f0fff0', 2696 'hotpink'=>'#ff69b4', 2697 'indianred'=>'#cd5c5c', 2698 'indigo'=>'#4b0082', 2699 'ivory'=>'#fffff0', 2700 'khaki'=>'#f0e68c', 2701 'lavender'=>'#e6e6fa', 2702 'lavenderblush'=>'#fff0f5', 2703 'lawngreen'=>'#7cfc00', 2704 'lemonchiffon'=>'#fffacd', 2705 'lightblue'=>'#add8e6', 2706 'lightcoral'=>'#f08080', 2707 'lightcyan'=>'#e0ffff', 2708 'lightgoldenrodyellow'=>'#fafad2', 2709 'lightgray'=>'#d3d3d3', 2710 'lightgrey'=>'#d3d3d3', 2711 'lightgreen'=>'#90ee90', 2712 'lightpink'=>'#ffb6c1', 2713 'lightsalmon'=>'#ffa07a', 2714 'lightseagreen'=>'#20b2aa', 2715 'lightskyblue'=>'#87cefa', 2716 'lightslategray'=>'#778899', 2717 'lightslategrey'=>'#778899', 2718 'lightsteelblue'=>'#b0c4de', 2719 'lightyellow'=>'#ffffe0', 2720 'lime'=>'#00ff00', 2721 'limegreen'=>'#32cd32', 2722 'linen'=>'#faf0e6', 2723 'magenta'=>'#ff00ff', 2724 'maroon'=>'#800000', 2725 'mediumaquamarine'=>'#66cdaa', 2726 'mediumblue'=>'#0000cd', 2727 'mediumorchid'=>'#ba55d3', 2728 'mediumpurple'=>'#9370d8', 2729 'mediumseagreen'=>'#3cb371', 2730 'mediumslateblue'=>'#7b68ee', 2731 'mediumspringgreen'=>'#00fa9a', 2732 'mediumturquoise'=>'#48d1cc', 2733 'mediumvioletred'=>'#c71585', 2734 'midnightblue'=>'#191970', 2735 'mintcream'=>'#f5fffa', 2736 'mistyrose'=>'#ffe4e1', 2737 'moccasin'=>'#ffe4b5', 2738 'navajowhite'=>'#ffdead', 2739 'navy'=>'#000080', 2740 'oldlace'=>'#fdf5e6', 2741 'olive'=>'#808000', 2742 'olivedrab'=>'#6b8e23', 2743 'orange'=>'#ffa500', 2744 'orangered'=>'#ff4500', 2745 'orchid'=>'#da70d6', 2746 'palegoldenrod'=>'#eee8aa', 2747 'palegreen'=>'#98fb98', 2748 'paleturquoise'=>'#afeeee', 2749 'palevioletred'=>'#d87093', 2750 'papayawhip'=>'#ffefd5', 2751 'peachpuff'=>'#ffdab9', 2752 'peru'=>'#cd853f', 2753 'pink'=>'#ffc0cb', 2754 'plum'=>'#dda0dd', 2755 'powderblue'=>'#b0e0e6', 2756 'purple'=>'#800080', 2757 'red'=>'#ff0000', 2758 'rosybrown'=>'#bc8f8f', 2759 'royalblue'=>'#4169e1', 2760 'saddlebrown'=>'#8b4513', 2761 'salmon'=>'#fa8072', 2762 'sandybrown'=>'#f4a460', 2763 'seagreen'=>'#2e8b57', 2764 'seashell'=>'#fff5ee', 2765 'sienna'=>'#a0522d', 2766 'silver'=>'#c0c0c0', 2767 'skyblue'=>'#87ceeb', 2768 'slateblue'=>'#6a5acd', 2769 'slategray'=>'#708090', 2770 'slategrey'=>'#708090', 2771 'snow'=>'#fffafa', 2772 'springgreen'=>'#00ff7f', 2773 'steelblue'=>'#4682b4', 2774 'tan'=>'#d2b48c', 2775 'teal'=>'#008080', 2776 'thistle'=>'#d8bfd8', 2777 'tomato'=>'#ff6347', 2778 'turquoise'=>'#40e0d0', 2779 'violet'=>'#ee82ee', 2780 'wheat'=>'#f5deb3', 2781 'white'=>'#ffffff', 2782 'whitesmoke'=>'#f5f5f5', 2783 'yellow'=>'#ffff00', 2784 'yellowgreen'=>'#9acd32' 2785 ); 2786 2787 public static function hasOwnProperty($color) { 2788 return isset(self::$colors[$color]); 2789 } 2790 2791 2792 public static function color($color) { 2793 return self::$colors[$color]; 2794 } 2795 2796 } 2797 2798 2799 2800 /** 2801 * Environment 2802 * 2803 * @package Less 2804 * @subpackage environment 2805 */ 2806 class Less_Environment{ 2807 2808 //public $paths = array(); // option - unmodified - paths to search for imports on 2809 //public static $files = array(); // list of files that have been imported, used for import-once 2810 //public $rootpath; // option - rootpath to append to URL's 2811 //public static $strictImports = null; // option - 2812 //public $insecure; // option - whether to allow imports from insecure ssl hosts 2813 //public $processImports; // option - whether to process imports. if false then imports will not be imported 2814 //public $javascriptEnabled; // option - whether JavaScript is enabled. if undefined, defaults to true 2815 //public $useFileCache; // browser only - whether to use the per file session cache 2816 public $currentFileInfo; // information about the current file - for error reporting and importing and making urls relative etc. 2817 2818 public $importMultiple = false; // whether we are currently importing multiple copies 2819 2820 2821 /** 2822 * @var array 2823 */ 2824 public $frames = array(); 2825 2826 /** 2827 * @var array 2828 */ 2829 public $mediaBlocks = array(); 2830 2831 /** 2832 * @var array 2833 */ 2834 public $mediaPath = array(); 2835 2836 public static $parensStack = 0; 2837 2838 public static $tabLevel = 0; 2839 2840 public static $lastRule = false; 2841 2842 public static $_outputMap; 2843 2844 public static $mixin_stack = 0; 2845 2846 /** 2847 * @var array 2848 */ 2849 public $functions = array(); 2850 2851 2852 public function Init(){ 2853 2854 self::$parensStack = 0; 2855 self::$tabLevel = 0; 2856 self::$lastRule = false; 2857 self::$mixin_stack = 0; 2858 2859 if( Less_Parser::$options['compress'] ){ 2860 2861 Less_Environment::$_outputMap = array( 2862 ',' => ',', 2863 ': ' => ':', 2864 '' => '', 2865 ' ' => ' ', 2866 ':' => ' :', 2867 '+' => '+', 2868 '~' => '~', 2869 '>' => '>', 2870 '|' => '|', 2871 '^' => '^', 2872 '^^' => '^^' 2873 ); 2874 2875 }else{ 2876 2877 Less_Environment::$_outputMap = array( 2878 ',' => ', ', 2879 ': ' => ': ', 2880 '' => '', 2881 ' ' => ' ', 2882 ':' => ' :', 2883 '+' => ' + ', 2884 '~' => ' ~ ', 2885 '>' => ' > ', 2886 '|' => '|', 2887 '^' => ' ^ ', 2888 '^^' => ' ^^ ' 2889 ); 2890 2891 } 2892 } 2893 2894 2895 public function copyEvalEnv($frames = array() ){ 2896 $new_env = new Less_Environment(); 2897 $new_env->frames = $frames; 2898 return $new_env; 2899 } 2900 2901 2902 public static function isMathOn(){ 2903 return !Less_Parser::$options['strictMath'] || Less_Environment::$parensStack; 2904 } 2905 2906 public static function isPathRelative($path){ 2907 return !preg_match('/^(?:[a-z-]+:|\/)/',$path); 2908 } 2909 2910 2911 /** 2912 * Canonicalize a path by resolving references to '/./', '/../' 2913 * Does not remove leading "../" 2914 * @param string path or url 2915 * @return string Canonicalized path 2916 * 2917 */ 2918 public static function normalizePath($path){ 2919 2920 $segments = explode('/',$path); 2921 $segments = array_reverse($segments); 2922 2923 $path = array(); 2924 $path_len = 0; 2925 2926 while( $segments ){ 2927 $segment = array_pop($segments); 2928 switch( $segment ) { 2929 2930 case '.': 2931 break; 2932 2933 case '..': 2934 if( !$path_len || ( $path[$path_len-1] === '..') ){ 2935 $path[] = $segment; 2936 $path_len++; 2937 }else{ 2938 array_pop($path); 2939 $path_len--; 2940 } 2941 break; 2942 2943 default: 2944 $path[] = $segment; 2945 $path_len++; 2946 break; 2947 } 2948 } 2949 2950 return implode('/',$path); 2951 } 2952 2953 2954 public function unshiftFrame($frame){ 2955 array_unshift($this->frames, $frame); 2956 } 2957 2958 public function shiftFrame(){ 2959 return array_shift($this->frames); 2960 } 2961 2962 } 2963 2964 2965 /** 2966 * Builtin functions 2967 * 2968 * @package Less 2969 * @subpackage function 2970 * @see http://lesscss.org/functions/ 2971 */ 2972 class Less_Functions{ 2973 2974 public $env; 2975 public $currentFileInfo; 2976 2977 function __construct($env, $currentFileInfo = null ){ 2978 $this->env = $env; 2979 $this->currentFileInfo = $currentFileInfo; 2980 } 2981 2982 /** 2983 * @param string $op 2984 */ 2985 public static function operate( $op, $a, $b ){ 2986 switch ($op) { 2987 case '+': return $a + $b; 2988 case '-': return $a - $b; 2989 case '*': return $a * $b; 2990 case '/': return $a / $b; 2991 } 2992 } 2993 2994 public static function clamp($val, $max = 1){ 2995 return min( max($val, 0), $max); 2996 } 2997 2998 public static function fround( $value ){ 2999 3000 if( $value === 0 ){ 3001 return $value; 3002 } 3003 3004 if( Less_Parser::$options['numPrecision'] ){ 3005 $p = pow(10, Less_Parser::$options['numPrecision']); 3006 return round( $value * $p) / $p; 3007 } 3008 return $value; 3009 } 3010 3011 public static function number($n){ 3012 3013 if ($n instanceof Less_Tree_Dimension) { 3014 return floatval( $n->unit->is('%') ? $n->value / 100 : $n->value); 3015 } else if (is_numeric($n)) { 3016 return $n; 3017 } else { 3018 throw new Less_Exception_Compiler("color functions take numbers as parameters"); 3019 } 3020 } 3021 3022 public static function scaled($n, $size = 255 ){ 3023 if( $n instanceof Less_Tree_Dimension && $n->unit->is('%') ){ 3024 return (float)$n->value * $size / 100; 3025 } else { 3026 return Less_Functions::number($n); 3027 } 3028 } 3029 3030 public function rgb ($r = null, $g = null, $b = null){ 3031 if (is_null($r) || is_null($g) || is_null($b)) { 3032 throw new Less_Exception_Compiler("rgb expects three parameters"); 3033 } 3034 return $this->rgba($r, $g, $b, 1.0); 3035 } 3036 3037 public function rgba($r = null, $g = null, $b = null, $a = null){ 3038 $rgb = array($r, $g, $b); 3039 $rgb = array_map(array('Less_Functions','scaled'),$rgb); 3040 3041 $a = self::number($a); 3042 return new Less_Tree_Color($rgb, $a); 3043 } 3044 3045 public function hsl($h, $s, $l){ 3046 return $this->hsla($h, $s, $l, 1.0); 3047 } 3048 3049 public function hsla($h, $s, $l, $a){ 3050 3051 $h = fmod(self::number($h), 360) / 360; // Classic % operator will change float to int 3052 $s = self::clamp(self::number($s)); 3053 $l = self::clamp(self::number($l)); 3054 $a = self::clamp(self::number($a)); 3055 3056 $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s; 3057 3058 $m1 = $l * 2 - $m2; 3059 3060 return $this->rgba( self::hsla_hue($h + 1/3, $m1, $m2) * 255, 3061 self::hsla_hue($h, $m1, $m2) * 255, 3062 self::hsla_hue($h - 1/3, $m1, $m2) * 255, 3063 $a); 3064 } 3065 3066 /** 3067 * @param double $h 3068 */ 3069 public function hsla_hue($h, $m1, $m2){ 3070 $h = $h < 0 ? $h + 1 : ($h > 1 ? $h - 1 : $h); 3071 if ($h * 6 < 1) return $m1 + ($m2 - $m1) * $h * 6; 3072 else if ($h * 2 < 1) return $m2; 3073 else if ($h * 3 < 2) return $m1 + ($m2 - $m1) * (2/3 - $h) * 6; 3074 else return $m1; 3075 } 3076 3077 public function hsv($h, $s, $v) { 3078 return $this->hsva($h, $s, $v, 1.0); 3079 } 3080 3081 /** 3082 * @param double $a 3083 */ 3084 public function hsva($h, $s, $v, $a) { 3085 $h = ((Less_Functions::number($h) % 360) / 360 ) * 360; 3086 $s = Less_Functions::number($s); 3087 $v = Less_Functions::number($v); 3088 $a = Less_Functions::number($a); 3089 3090 $i = floor(($h / 60) % 6); 3091 $f = ($h / 60) - $i; 3092 3093 $vs = array( $v, 3094 $v * (1 - $s), 3095 $v * (1 - $f * $s), 3096 $v * (1 - (1 - $f) * $s)); 3097 3098 $perm = array(array(0, 3, 1), 3099 array(2, 0, 1), 3100 array(1, 0, 3), 3101 array(1, 2, 0), 3102 array(3, 1, 0), 3103 array(0, 1, 2)); 3104 3105 return $this->rgba($vs[$perm[$i][0]] * 255, 3106 $vs[$perm[$i][1]] * 255, 3107 $vs[$perm[$i][2]] * 255, 3108 $a); 3109 } 3110 3111 public function hue($color = null){ 3112 if (!$color instanceof Less_Tree_Color) { 3113 throw new Less_Exception_Compiler('The first argument to hue must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3114 } 3115 3116 $c = $color->toHSL(); 3117 return new Less_Tree_Dimension(Less_Parser::round($c['h'])); 3118 } 3119 3120 public function saturation($color = null){ 3121 if (!$color instanceof Less_Tree_Color) { 3122 throw new Less_Exception_Compiler('The first argument to saturation must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3123 } 3124 3125 $c = $color->toHSL(); 3126 return new Less_Tree_Dimension(Less_Parser::round($c['s'] * 100), '%'); 3127 } 3128 3129 public function lightness($color = null){ 3130 if (!$color instanceof Less_Tree_Color) { 3131 throw new Less_Exception_Compiler('The first argument to lightness must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3132 } 3133 3134 $c = $color->toHSL(); 3135 return new Less_Tree_Dimension(Less_Parser::round($c['l'] * 100), '%'); 3136 } 3137 3138 public function hsvhue( $color = null ){ 3139 if (!$color instanceof Less_Tree_Color) { 3140 throw new Less_Exception_Compiler('The first argument to hsvhue must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3141 } 3142 3143 $hsv = $color->toHSV(); 3144 return new Less_Tree_Dimension( Less_Parser::round($hsv['h']) ); 3145 } 3146 3147 3148 public function hsvsaturation( $color = null ){ 3149 if (!$color instanceof Less_Tree_Color) { 3150 throw new Less_Exception_Compiler('The first argument to hsvsaturation must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3151 } 3152 3153 $hsv = $color->toHSV(); 3154 return new Less_Tree_Dimension( Less_Parser::round($hsv['s'] * 100), '%' ); 3155 } 3156 3157 public function hsvvalue( $color = null ){ 3158 if (!$color instanceof Less_Tree_Color) { 3159 throw new Less_Exception_Compiler('The first argument to hsvvalue must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3160 } 3161 3162 $hsv = $color->toHSV(); 3163 return new Less_Tree_Dimension( Less_Parser::round($hsv['v'] * 100), '%' ); 3164 } 3165 3166 public function red($color = null) { 3167 if (!$color instanceof Less_Tree_Color) { 3168 throw new Less_Exception_Compiler('The first argument to red must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3169 } 3170 3171 return new Less_Tree_Dimension( $color->rgb[0] ); 3172 } 3173 3174 public function green($color = null) { 3175 if (!$color instanceof Less_Tree_Color) { 3176 throw new Less_Exception_Compiler('The first argument to green must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3177 } 3178 3179 return new Less_Tree_Dimension( $color->rgb[1] ); 3180 } 3181 3182 public function blue($color = null) { 3183 if (!$color instanceof Less_Tree_Color) { 3184 throw new Less_Exception_Compiler('The first argument to blue must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3185 } 3186 3187 return new Less_Tree_Dimension( $color->rgb[2] ); 3188 } 3189 3190 public function alpha($color = null){ 3191 if (!$color instanceof Less_Tree_Color) { 3192 throw new Less_Exception_Compiler('The first argument to alpha must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3193 } 3194 3195 $c = $color->toHSL(); 3196 return new Less_Tree_Dimension($c['a']); 3197 } 3198 3199 public function luma ($color = null) { 3200 if (!$color instanceof Less_Tree_Color) { 3201 throw new Less_Exception_Compiler('The first argument to luma must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3202 } 3203 3204 return new Less_Tree_Dimension(Less_Parser::round( $color->luma() * $color->alpha * 100), '%'); 3205 } 3206 3207 public function luminance( $color = null ){ 3208 if (!$color instanceof Less_Tree_Color) { 3209 throw new Less_Exception_Compiler('The first argument to luminance must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3210 } 3211 3212 $luminance = 3213 (0.2126 * $color->rgb[0] / 255) 3214 + (0.7152 * $color->rgb[1] / 255) 3215 + (0.0722 * $color->rgb[2] / 255); 3216 3217 return new Less_Tree_Dimension(Less_Parser::round( $luminance * $color->alpha * 100), '%'); 3218 } 3219 3220 public function saturate($color = null, $amount = null){ 3221 // filter: saturate(3.2); 3222 // should be kept as is, so check for color 3223 if ($color instanceof Less_Tree_Dimension) { 3224 return null; 3225 } 3226 3227 if (!$color instanceof Less_Tree_Color) { 3228 throw new Less_Exception_Compiler('The first argument to saturate must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3229 } 3230 if (!$amount instanceof Less_Tree_Dimension) { 3231 throw new Less_Exception_Compiler('The second argument to saturate must be a percentage' . ($amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3232 } 3233 3234 $hsl = $color->toHSL(); 3235 3236 $hsl['s'] += $amount->value / 100; 3237 $hsl['s'] = self::clamp($hsl['s']); 3238 3239 return $this->hsla($hsl['h'], $hsl['s'], $hsl['l'], $hsl['a']); 3240 } 3241 3242 /** 3243 * @param Less_Tree_Dimension $amount 3244 */ 3245 public function desaturate($color = null, $amount = null){ 3246 if (!$color instanceof Less_Tree_Color) { 3247 throw new Less_Exception_Compiler('The first argument to desaturate must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3248 } 3249 if (!$amount instanceof Less_Tree_Dimension) { 3250 throw new Less_Exception_Compiler('The second argument to desaturate must be a percentage' . ($amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3251 } 3252 3253 $hsl = $color->toHSL(); 3254 3255 $hsl['s'] -= $amount->value / 100; 3256 $hsl['s'] = self::clamp($hsl['s']); 3257 3258 return $this->hsla($hsl['h'], $hsl['s'], $hsl['l'], $hsl['a']); 3259 } 3260 3261 3262 3263 public function lighten($color = null, $amount=null){ 3264 if (!$color instanceof Less_Tree_Color) { 3265 throw new Less_Exception_Compiler('The first argument to lighten must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3266 } 3267 if (!$amount instanceof Less_Tree_Dimension) { 3268 throw new Less_Exception_Compiler('The second argument to lighten must be a percentage' . ($amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3269 } 3270 3271 $hsl = $color->toHSL(); 3272 3273 $hsl['l'] += $amount->value / 100; 3274 $hsl['l'] = self::clamp($hsl['l']); 3275 3276 return $this->hsla($hsl['h'], $hsl['s'], $hsl['l'], $hsl['a']); 3277 } 3278 3279 public function darken($color = null, $amount = null){ 3280 if (!$color instanceof Less_Tree_Color) { 3281 throw new Less_Exception_Compiler('The first argument to darken must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3282 } 3283 if (!$amount instanceof Less_Tree_Dimension) { 3284 throw new Less_Exception_Compiler('The second argument to darken must be a percentage' . ($amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3285 } 3286 3287 $hsl = $color->toHSL(); 3288 $hsl['l'] -= $amount->value / 100; 3289 $hsl['l'] = self::clamp($hsl['l']); 3290 3291 return $this->hsla($hsl['h'], $hsl['s'], $hsl['l'], $hsl['a']); 3292 } 3293 3294 public function fadein($color = null, $amount = null){ 3295 if (!$color instanceof Less_Tree_Color) { 3296 throw new Less_Exception_Compiler('The first argument to fadein must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3297 } 3298 if (!$amount instanceof Less_Tree_Dimension) { 3299 throw new Less_Exception_Compiler('The second argument to fadein must be a percentage' . ($amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3300 } 3301 3302 $hsl = $color->toHSL(); 3303 $hsl['a'] += $amount->value / 100; 3304 $hsl['a'] = self::clamp($hsl['a']); 3305 return $this->hsla($hsl['h'], $hsl['s'], $hsl['l'], $hsl['a']); 3306 } 3307 3308 public function fadeout($color = null, $amount = null){ 3309 if (!$color instanceof Less_Tree_Color) { 3310 throw new Less_Exception_Compiler('The first argument to fadeout must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3311 } 3312 if (!$amount instanceof Less_Tree_Dimension) { 3313 throw new Less_Exception_Compiler('The second argument to fadeout must be a percentage' . ($amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3314 } 3315 3316 $hsl = $color->toHSL(); 3317 $hsl['a'] -= $amount->value / 100; 3318 $hsl['a'] = self::clamp($hsl['a']); 3319 return $this->hsla($hsl['h'], $hsl['s'], $hsl['l'], $hsl['a']); 3320 } 3321 3322 public function fade($color = null, $amount = null){ 3323 if (!$color instanceof Less_Tree_Color) { 3324 throw new Less_Exception_Compiler('The first argument to fade must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3325 } 3326 if (!$amount instanceof Less_Tree_Dimension) { 3327 throw new Less_Exception_Compiler('The second argument to fade must be a percentage' . ($amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3328 } 3329 3330 $hsl = $color->toHSL(); 3331 3332 $hsl['a'] = $amount->value / 100; 3333 $hsl['a'] = self::clamp($hsl['a']); 3334 return $this->hsla($hsl['h'], $hsl['s'], $hsl['l'], $hsl['a']); 3335 } 3336 3337 3338 3339 public function spin($color = null, $amount = null){ 3340 if (!$color instanceof Less_Tree_Color) { 3341 throw new Less_Exception_Compiler('The first argument to spin must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3342 } 3343 if (!$amount instanceof Less_Tree_Dimension) { 3344 throw new Less_Exception_Compiler('The second argument to spin must be a number' . ($amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3345 } 3346 3347 $hsl = $color->toHSL(); 3348 $hue = fmod($hsl['h'] + $amount->value, 360); 3349 3350 $hsl['h'] = $hue < 0 ? 360 + $hue : $hue; 3351 3352 return $this->hsla($hsl['h'], $hsl['s'], $hsl['l'], $hsl['a']); 3353 } 3354 3355 // 3356 // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein 3357 // http://sass-lang.com 3358 // 3359 3360 /** 3361 * @param Less_Tree_Color $color1 3362 */ 3363 public function mix($color1 = null, $color2 = null, $weight = null){ 3364 if (!$color1 instanceof Less_Tree_Color) { 3365 throw new Less_Exception_Compiler('The first argument to mix must be a color' . ($color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3366 } 3367 if (!$color2 instanceof Less_Tree_Color) { 3368 throw new Less_Exception_Compiler('The second argument to mix must be a color' . ($color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3369 } 3370 if (!$weight) { 3371 $weight = new Less_Tree_Dimension('50', '%'); 3372 } 3373 if (!$weight instanceof Less_Tree_Dimension) { 3374 throw new Less_Exception_Compiler('The third argument to contrast must be a percentage' . ($weight instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3375 } 3376 3377 $p = $weight->value / 100.0; 3378 $w = $p * 2 - 1; 3379 $hsl1 = $color1->toHSL(); 3380 $hsl2 = $color2->toHSL(); 3381 $a = $hsl1['a'] - $hsl2['a']; 3382 3383 $w1 = (((($w * $a) == -1) ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2; 3384 $w2 = 1 - $w1; 3385 3386 $rgb = array($color1->rgb[0] * $w1 + $color2->rgb[0] * $w2, 3387 $color1->rgb[1] * $w1 + $color2->rgb[1] * $w2, 3388 $color1->rgb[2] * $w1 + $color2->rgb[2] * $w2); 3389 3390 $alpha = $color1->alpha * $p + $color2->alpha * (1 - $p); 3391 3392 return new Less_Tree_Color($rgb, $alpha); 3393 } 3394 3395 public function greyscale($color){ 3396 return $this->desaturate($color, new Less_Tree_Dimension(100,'%')); 3397 } 3398 3399 3400 public function contrast( $color, $dark = null, $light = null, $threshold = null){ 3401 // filter: contrast(3.2); 3402 // should be kept as is, so check for color 3403 if (!$color instanceof Less_Tree_Color) { 3404 return null; 3405 } 3406 if( !$light ){ 3407 $light = $this->rgba(255, 255, 255, 1.0); 3408 } 3409 if( !$dark ){ 3410 $dark = $this->rgba(0, 0, 0, 1.0); 3411 } 3412 3413 if (!$dark instanceof Less_Tree_Color) { 3414 throw new Less_Exception_Compiler('The second argument to contrast must be a color' . ($dark instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3415 } 3416 if (!$light instanceof Less_Tree_Color) { 3417 throw new Less_Exception_Compiler('The third argument to contrast must be a color' . ($light instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3418 } 3419 3420 //Figure out which is actually light and dark! 3421 if( $dark->luma() > $light->luma() ){ 3422 $t = $light; 3423 $light = $dark; 3424 $dark = $t; 3425 } 3426 if( !$threshold ){ 3427 $threshold = 0.43; 3428 } else { 3429 $threshold = Less_Functions::number($threshold); 3430 } 3431 3432 if( $color->luma() < $threshold ){ 3433 return $light; 3434 } else { 3435 return $dark; 3436 } 3437 } 3438 3439 public function e ($str){ 3440 if( is_string($str) ){ 3441 return new Less_Tree_Anonymous($str); 3442 } 3443 return new Less_Tree_Anonymous($str instanceof Less_Tree_JavaScript ? $str->expression : $str->value); 3444 } 3445 3446 public function escape ($str){ 3447 3448 $revert = array('%21'=>'!', '%2A'=>'*', '%27'=>"'",'%3F'=>'?','%26'=>'&','%2C'=>',','%2F'=>'/','%40'=>'@','%2B'=>'+','%24'=>'$'); 3449 3450 return new Less_Tree_Anonymous(strtr(rawurlencode($str->value), $revert)); 3451 } 3452 3453 3454 /** 3455 * todo: This function will need some additional work to make it work the same as less.js 3456 * 3457 */ 3458 public function replace( $string, $pattern, $replacement, $flags = null ){ 3459 $result = $string->value; 3460 3461 $expr = '/'.str_replace('/','\\/',$pattern->value).'/'; 3462 if( $flags && $flags->value){ 3463 $expr .= self::replace_flags($flags->value); 3464 } 3465 3466 $result = preg_replace($expr,$replacement->value,$result); 3467 3468 3469 if( property_exists($string,'quote') ){ 3470 return new Less_Tree_Quoted( $string->quote, $result, $string->escaped); 3471 } 3472 return new Less_Tree_Quoted( '', $result ); 3473 } 3474 3475 public static function replace_flags($flags){ 3476 $flags = str_split($flags,1); 3477 $new_flags = ''; 3478 3479 foreach($flags as $flag){ 3480 switch($flag){ 3481 case 'e': 3482 case 'g': 3483 break; 3484 3485 default: 3486 $new_flags .= $flag; 3487 break; 3488 } 3489 } 3490 3491 return $new_flags; 3492 } 3493 3494 public function _percent(){ 3495 $string = func_get_arg(0); 3496 3497 $args = func_get_args(); 3498 array_shift($args); 3499 $result = $string->value; 3500 3501 foreach($args as $arg){ 3502 if( preg_match('/%[sda]/i',$result, $token) ){ 3503 $token = $token[0]; 3504 $value = stristr($token, 's') ? $arg->value : $arg->toCSS(); 3505 $value = preg_match('/[A-Z]$/', $token) ? urlencode($value) : $value; 3506 $result = preg_replace('/%[sda]/i',$value, $result, 1); 3507 } 3508 } 3509 $result = str_replace('%%', '%', $result); 3510 3511 return new Less_Tree_Quoted( $string->quote , $result, $string->escaped); 3512 } 3513 3514 public function unit( $val, $unit = null) { 3515 if( !($val instanceof Less_Tree_Dimension) ){ 3516 throw new Less_Exception_Compiler('The first argument to unit must be a number' . ($val instanceof Less_Tree_Operation ? '. Have you forgotten parenthesis?' : '.') ); 3517 } 3518 3519 if( $unit ){ 3520 if( $unit instanceof Less_Tree_Keyword ){ 3521 $unit = $unit->value; 3522 } else { 3523 $unit = $unit->toCSS(); 3524 } 3525 } else { 3526 $unit = ""; 3527 } 3528 return new Less_Tree_Dimension($val->value, $unit ); 3529 } 3530 3531 public function convert($val, $unit){ 3532 return $val->convertTo($unit->value); 3533 } 3534 3535 public function round($n, $f = false) { 3536 3537 $fraction = 0; 3538 if( $f !== false ){ 3539 $fraction = $f->value; 3540 } 3541 3542 return $this->_math('Less_Parser::round',null, $n, $fraction); 3543 } 3544 3545 public function pi(){ 3546 return new Less_Tree_Dimension(M_PI); 3547 } 3548 3549 public function mod($a, $b) { 3550 return new Less_Tree_Dimension( $a->value % $b->value, $a->unit); 3551 } 3552 3553 3554 3555 public function pow($x, $y) { 3556 if( is_numeric($x) && is_numeric($y) ){ 3557 $x = new Less_Tree_Dimension($x); 3558 $y = new Less_Tree_Dimension($y); 3559 }elseif( !($x instanceof Less_Tree_Dimension) || !($y instanceof Less_Tree_Dimension) ){ 3560 throw new Less_Exception_Compiler('Arguments must be numbers'); 3561 } 3562 3563 return new Less_Tree_Dimension( pow($x->value, $y->value), $x->unit ); 3564 } 3565 3566 // var mathFunctions = [{name:"ce ... 3567 public function ceil( $n ){ return $this->_math('ceil', null, $n); } 3568 public function floor( $n ){ return $this->_math('floor', null, $n); } 3569 public function sqrt( $n ){ return $this->_math('sqrt', null, $n); } 3570 public function abs( $n ){ return $this->_math('abs', null, $n); } 3571 3572 public function tan( $n ){ return $this->_math('tan', '', $n); } 3573 public function sin( $n ){ return $this->_math('sin', '', $n); } 3574 public function cos( $n ){ return $this->_math('cos', '', $n); } 3575 3576 public function atan( $n ){ return $this->_math('atan', 'rad', $n); } 3577 public function asin( $n ){ return $this->_math('asin', 'rad', $n); } 3578 public function acos( $n ){ return $this->_math('acos', 'rad', $n); } 3579 3580 private function _math() { 3581 $args = func_get_args(); 3582 $fn = array_shift($args); 3583 $unit = array_shift($args); 3584 3585 if ($args[0] instanceof Less_Tree_Dimension) { 3586 3587 if( $unit === null ){ 3588 $unit = $args[0]->unit; 3589 }else{ 3590 $args[0] = $args[0]->unify(); 3591 } 3592 $args[0] = (float)$args[0]->value; 3593 return new Less_Tree_Dimension( call_user_func_array($fn, $args), $unit); 3594 } else if (is_numeric($args[0])) { 3595 return call_user_func_array($fn,$args); 3596 } else { 3597 throw new Less_Exception_Compiler("math functions take numbers as parameters"); 3598 } 3599 } 3600 3601 /** 3602 * @param boolean $isMin 3603 */ 3604 private function _minmax( $isMin, $args ){ 3605 3606 $arg_count = count($args); 3607 3608 if( $arg_count < 1 ){ 3609 throw new Less_Exception_Compiler( 'one or more arguments required'); 3610 } 3611 3612 $j = null; 3613 $unitClone = null; 3614 $unitStatic = null; 3615 3616 3617 $order = array(); // elems only contains original argument values. 3618 $values = array(); // key is the unit.toString() for unified tree.Dimension values, 3619 // value is the index into the order array. 3620 3621 3622 for( $i = 0; $i < $arg_count; $i++ ){ 3623 $current = $args[$i]; 3624 if( !($current instanceof Less_Tree_Dimension) ){ 3625 if( is_array($args[$i]->value) ){ 3626 $args[] = $args[$i]->value; 3627 } 3628 continue; 3629 } 3630 3631 if( $current->unit->toString() === '' && !$unitClone ){ 3632 $temp = new Less_Tree_Dimension($current->value, $unitClone); 3633 $currentUnified = $temp->unify(); 3634 }else{ 3635 $currentUnified = $current->unify(); 3636 } 3637 3638 if( $currentUnified->unit->toString() === "" && !$unitStatic ){ 3639 $unit = $unitStatic; 3640 }else{ 3641 $unit = $currentUnified->unit->toString(); 3642 } 3643 3644 if( $unit !== '' && !$unitStatic || $unit !== '' && $order[0]->unify()->unit->toString() === "" ){ 3645 $unitStatic = $unit; 3646 } 3647 3648 if( $unit != '' && !$unitClone ){ 3649 $unitClone = $current->unit->toString(); 3650 } 3651 3652 if( isset($values['']) && $unit !== '' && $unit === $unitStatic ){ 3653 $j = $values['']; 3654 }elseif( isset($values[$unit]) ){ 3655 $j = $values[$unit]; 3656 }else{ 3657 3658 if( $unitStatic && $unit !== $unitStatic ){ 3659 throw new Less_Exception_Compiler( 'incompatible types'); 3660 } 3661 $values[$unit] = count($order); 3662 $order[] = $current; 3663 continue; 3664 } 3665 3666 3667 if( $order[$j]->unit->toString() === "" && $unitClone ){ 3668 $temp = new Less_Tree_Dimension( $order[$j]->value, $unitClone); 3669 $referenceUnified = $temp->unify(); 3670 }else{ 3671 $referenceUnified = $order[$j]->unify(); 3672 } 3673 if( ($isMin && $currentUnified->value < $referenceUnified->value) || (!$isMin && $currentUnified->value > $referenceUnified->value) ){ 3674 $order[$j] = $current; 3675 } 3676 } 3677 3678 if( count($order) == 1 ){ 3679 return $order[0]; 3680 } 3681 $args = array(); 3682 foreach($order as $a){ 3683 $args[] = $a->toCSS($this->env); 3684 } 3685 return new Less_Tree_Anonymous( ($isMin?'min(':'max(') . implode(Less_Environment::$_outputMap[','],$args).')'); 3686 } 3687 3688 public function min(){ 3689 $args = func_get_args(); 3690 return $this->_minmax( true, $args ); 3691 } 3692 3693 public function max(){ 3694 $args = func_get_args(); 3695 return $this->_minmax( false, $args ); 3696 } 3697 3698 public function getunit($n){ 3699 return new Less_Tree_Anonymous($n->unit); 3700 } 3701 3702 public function argb($color) { 3703 if (!$color instanceof Less_Tree_Color) { 3704 throw new Less_Exception_Compiler('The first argument to argb must be a color' . ($color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 3705 } 3706 3707 return new Less_Tree_Anonymous($color->toARGB()); 3708 } 3709 3710 public function percentage($n) { 3711 return new Less_Tree_Dimension($n->value * 100, '%'); 3712 } 3713 3714 public function color($n) { 3715 3716 if( $n instanceof Less_Tree_Quoted ){ 3717 $colorCandidate = $n->value; 3718 $returnColor = Less_Tree_Color::fromKeyword($colorCandidate); 3719 if( $returnColor ){ 3720 return $returnColor; 3721 } 3722 if( preg_match('/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/',$colorCandidate) ){ 3723 return new Less_Tree_Color(substr($colorCandidate, 1)); 3724 } 3725 throw new Less_Exception_Compiler("argument must be a color keyword or 3/6 digit hex e.g. #FFF"); 3726 } else { 3727 throw new Less_Exception_Compiler("argument must be a string"); 3728 } 3729 } 3730 3731 3732 public function iscolor($n) { 3733 return $this->_isa($n, 'Less_Tree_Color'); 3734 } 3735 3736 public function isnumber($n) { 3737 return $this->_isa($n, 'Less_Tree_Dimension'); 3738 } 3739 3740 public function isstring($n) { 3741 return $this->_isa($n, 'Less_Tree_Quoted'); 3742 } 3743 3744 public function iskeyword($n) { 3745 return $this->_isa($n, 'Less_Tree_Keyword'); 3746 } 3747 3748 public function isurl($n) { 3749 return $this->_isa($n, 'Less_Tree_Url'); 3750 } 3751 3752 public function ispixel($n) { 3753 return $this->isunit($n, 'px'); 3754 } 3755 3756 public function ispercentage($n) { 3757 return $this->isunit($n, '%'); 3758 } 3759 3760 public function isem($n) { 3761 return $this->isunit($n, 'em'); 3762 } 3763 3764 /** 3765 * @param string $unit 3766 */ 3767 public function isunit( $n, $unit ){ 3768 return ($n instanceof Less_Tree_Dimension) && $n->unit->is( ( property_exists($unit,'value') ? $unit->value : $unit) ) ? new Less_Tree_Keyword('true') : new Less_Tree_Keyword('false'); 3769 } 3770 3771 /** 3772 * @param string $type 3773 */ 3774 private function _isa($n, $type) { 3775 return is_a($n, $type) ? new Less_Tree_Keyword('true') : new Less_Tree_Keyword('false'); 3776 } 3777 3778 public function tint($color, $amount) { 3779 return $this->mix( $this->rgb(255,255,255), $color, $amount); 3780 } 3781 3782 public function shade($color, $amount) { 3783 return $this->mix($this->rgb(0, 0, 0), $color, $amount); 3784 } 3785 3786 public function extract($values, $index ){ 3787 $index = (int)$index->value - 1; // (1-based index) 3788 // handle non-array values as an array of length 1 3789 // return 'undefined' if index is invalid 3790 if( property_exists($values,'value') && is_array($values->value) ){ 3791 if( isset($values->value[$index]) ){ 3792 return $values->value[$index]; 3793 } 3794 return null; 3795 3796 }elseif( (int)$index === 0 ){ 3797 return $values; 3798 } 3799 3800 return null; 3801 } 3802 3803 public function length($values){ 3804 $n = (property_exists($values,'value') && is_array($values->value)) ? count($values->value) : 1; 3805 return new Less_Tree_Dimension($n); 3806 } 3807 3808 public function datauri($mimetypeNode, $filePathNode = null ) { 3809 3810 $filePath = ( $filePathNode ? $filePathNode->value : null ); 3811 $mimetype = $mimetypeNode->value; 3812 3813 $args = 2; 3814 if( !$filePath ){ 3815 $filePath = $mimetype; 3816 $args = 1; 3817 } 3818 3819 $filePath = str_replace('\\','/',$filePath); 3820 if( Less_Environment::isPathRelative($filePath) ){ 3821 3822 if( Less_Parser::$options['relativeUrls'] ){ 3823 $temp = $this->currentFileInfo['currentDirectory']; 3824 } else { 3825 $temp = $this->currentFileInfo['entryPath']; 3826 } 3827 3828 if( !empty($temp) ){ 3829 $filePath = Less_Environment::normalizePath(rtrim($temp,'/').'/'.$filePath); 3830 } 3831 3832 } 3833 3834 3835 // detect the mimetype if not given 3836 if( $args < 2 ){ 3837 3838 /* incomplete 3839 $mime = require('mime'); 3840 mimetype = mime.lookup(path); 3841 3842 // use base 64 unless it's an ASCII or UTF-8 format 3843 var charset = mime.charsets.lookup(mimetype); 3844 useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0; 3845 if (useBase64) mimetype += ';base64'; 3846 */ 3847 3848 $mimetype = Less_Mime::lookup($filePath); 3849 3850 $charset = Less_Mime::charsets_lookup($mimetype); 3851 $useBase64 = !in_array($charset,array('US-ASCII', 'UTF-8')); 3852 if( $useBase64 ){ $mimetype .= ';base64'; } 3853 3854 }else{ 3855 $useBase64 = preg_match('/;base64$/',$mimetype); 3856 } 3857 3858 3859 if( file_exists($filePath) ){ 3860 $buf = @file_get_contents($filePath); 3861 }else{ 3862 $buf = false; 3863 } 3864 3865 3866 // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded 3867 // and the --ieCompat flag is enabled, return a normal url() instead. 3868 $DATA_URI_MAX_KB = 32; 3869 $fileSizeInKB = round( strlen($buf) / 1024 ); 3870 if( $fileSizeInKB >= $DATA_URI_MAX_KB ){ 3871 $url = new Less_Tree_Url( ($filePathNode ? $filePathNode : $mimetypeNode), $this->currentFileInfo); 3872 return $url->compile($this); 3873 } 3874 3875 if( $buf ){ 3876 $buf = $useBase64 ? base64_encode($buf) : rawurlencode($buf); 3877 $filePath = '"data:' . $mimetype . ',' . $buf . '"'; 3878 } 3879 3880 return new Less_Tree_Url( new Less_Tree_Anonymous($filePath) ); 3881 } 3882 3883 //svg-gradient 3884 public function svggradient( $direction ){ 3885 3886 $throw_message = 'svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]'; 3887 $arguments = func_get_args(); 3888 3889 if( count($arguments) < 3 ){ 3890 throw new Less_Exception_Compiler( $throw_message ); 3891 } 3892 3893 $stops = array_slice($arguments,1); 3894 $gradientType = 'linear'; 3895 $rectangleDimension = 'x="0" y="0" width="1" height="1"'; 3896 $useBase64 = true; 3897 $directionValue = $direction->toCSS(); 3898 3899 3900 switch( $directionValue ){ 3901 case "to bottom": 3902 $gradientDirectionSvg = 'x1="0%" y1="0%" x2="0%" y2="100%"'; 3903 break; 3904 case "to right": 3905 $gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="0%"'; 3906 break; 3907 case "to bottom right": 3908 $gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="100%"'; 3909 break; 3910 case "to top right": 3911 $gradientDirectionSvg = 'x1="0%" y1="100%" x2="100%" y2="0%"'; 3912 break; 3913 case "ellipse": 3914 case "ellipse at center": 3915 $gradientType = "radial"; 3916 $gradientDirectionSvg = 'cx="50%" cy="50%" r="75%"'; 3917 $rectangleDimension = 'x="-50" y="-50" width="101" height="101"'; 3918 break; 3919 default: 3920 throw new Less_Exception_Compiler( "svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center'" ); 3921 } 3922 3923 $returner = '<?xml version="1.0" ?>' . 3924 '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100%" height="100%" viewBox="0 0 1 1" preserveAspectRatio="none">' . 3925 '<' . $gradientType . 'Gradient id="gradient" gradientUnits="userSpaceOnUse" ' . $gradientDirectionSvg . '>'; 3926 3927 for( $i = 0; $i < count($stops); $i++ ){ 3928 if( is_object($stops[$i]) && property_exists($stops[$i],'value') ){ 3929 $color = $stops[$i]->value[0]; 3930 $position = $stops[$i]->value[1]; 3931 }else{ 3932 $color = $stops[$i]; 3933 $position = null; 3934 } 3935 3936 if( !($color instanceof Less_Tree_Color) || (!(($i === 0 || $i+1 === count($stops)) && $position === null) && !($position instanceof Less_Tree_Dimension)) ){ 3937 throw new Less_Exception_Compiler( $throw_message ); 3938 } 3939 if( $position ){ 3940 $positionValue = $position->toCSS(); 3941 }elseif( $i === 0 ){ 3942 $positionValue = '0%'; 3943 }else{ 3944 $positionValue = '100%'; 3945 } 3946 $alpha = $color->alpha; 3947 $returner .= '<stop offset="' . $positionValue . '" stop-color="' . $color->toRGB() . '"' . ($alpha < 1 ? ' stop-opacity="' . $alpha . '"' : '') . '/>'; 3948 } 3949 3950 $returner .= '</' . $gradientType . 'Gradient><rect ' . $rectangleDimension . ' fill="url(#gradient)" /></svg>'; 3951 3952 3953 if( $useBase64 ){ 3954 $returner = "'data:image/svg+xml;base64,".base64_encode($returner)."'"; 3955 }else{ 3956 $returner = "'data:image/svg+xml,".$returner."'"; 3957 } 3958 3959 return new Less_Tree_URL( new Less_Tree_Anonymous( $returner ) ); 3960 } 3961 3962 3963 /** 3964 * Php version of javascript's `encodeURIComponent` function 3965 * 3966 * @param string $string The string to encode 3967 * @return string The encoded string 3968 */ 3969 public static function encodeURIComponent($string){ 3970 $revert = array('%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')'); 3971 return strtr(rawurlencode($string), $revert); 3972 } 3973 3974 3975 // Color Blending 3976 // ref: http://www.w3.org/TR/compositing-1 3977 3978 public function colorBlend( $mode, $color1, $color2 ){ 3979 $ab = $color1->alpha; // backdrop 3980 $as = $color2->alpha; // source 3981 $r = array(); // result 3982 3983 $ar = $as + $ab * (1 - $as); 3984 for( $i = 0; $i < 3; $i++ ){ 3985 $cb = $color1->rgb[$i] / 255; 3986 $cs = $color2->rgb[$i] / 255; 3987 $cr = call_user_func( $mode, $cb, $cs ); 3988 if( $ar ){ 3989 $cr = ($as * $cs + $ab * ($cb - $as * ($cb + $cs - $cr))) / $ar; 3990 } 3991 $r[$i] = $cr * 255; 3992 } 3993 3994 return new Less_Tree_Color($r, $ar); 3995 } 3996 3997 public function multiply($color1 = null, $color2 = null ){ 3998 if (!$color1 instanceof Less_Tree_Color) { 3999 throw new Less_Exception_Compiler('The first argument to multiply must be a color' . ($color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4000 } 4001 if (!$color2 instanceof Less_Tree_Color) { 4002 throw new Less_Exception_Compiler('The second argument to multiply must be a color' . ($color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4003 } 4004 4005 return $this->colorBlend( array($this,'colorBlendMultiply'), $color1, $color2 ); 4006 } 4007 4008 private function colorBlendMultiply($cb, $cs){ 4009 return $cb * $cs; 4010 } 4011 4012 public function screen($color1 = null, $color2 = null ){ 4013 if (!$color1 instanceof Less_Tree_Color) { 4014 throw new Less_Exception_Compiler('The first argument to screen must be a color' . ($color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4015 } 4016 if (!$color2 instanceof Less_Tree_Color) { 4017 throw new Less_Exception_Compiler('The second argument to screen must be a color' . ($color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4018 } 4019 4020 return $this->colorBlend( array($this,'colorBlendScreen'), $color1, $color2 ); 4021 } 4022 4023 private function colorBlendScreen( $cb, $cs){ 4024 return $cb + $cs - $cb * $cs; 4025 } 4026 4027 public function overlay($color1 = null, $color2 = null){ 4028 if (!$color1 instanceof Less_Tree_Color) { 4029 throw new Less_Exception_Compiler('The first argument to overlay must be a color' . ($color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4030 } 4031 if (!$color2 instanceof Less_Tree_Color) { 4032 throw new Less_Exception_Compiler('The second argument to overlay must be a color' . ($color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4033 } 4034 4035 return $this->colorBlend( array($this,'colorBlendOverlay'), $color1, $color2 ); 4036 } 4037 4038 private function colorBlendOverlay($cb, $cs ){ 4039 $cb *= 2; 4040 return ($cb <= 1) 4041 ? $this->colorBlendMultiply($cb, $cs) 4042 : $this->colorBlendScreen($cb - 1, $cs); 4043 } 4044 4045 public function softlight($color1 = null, $color2 = null){ 4046 if (!$color1 instanceof Less_Tree_Color) { 4047 throw new Less_Exception_Compiler('The first argument to softlight must be a color' . ($color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4048 } 4049 if (!$color2 instanceof Less_Tree_Color) { 4050 throw new Less_Exception_Compiler('The second argument to softlight must be a color' . ($color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4051 } 4052 4053 return $this->colorBlend( array($this,'colorBlendSoftlight'), $color1, $color2 ); 4054 } 4055 4056 private function colorBlendSoftlight($cb, $cs ){ 4057 $d = 1; 4058 $e = $cb; 4059 if( $cs > 0.5 ){ 4060 $e = 1; 4061 $d = ($cb > 0.25) ? sqrt($cb) 4062 : ((16 * $cb - 12) * $cb + 4) * $cb; 4063 } 4064 return $cb - (1 - 2 * $cs) * $e * ($d - $cb); 4065 } 4066 4067 public function hardlight($color1 = null, $color2 = null){ 4068 if (!$color1 instanceof Less_Tree_Color) { 4069 throw new Less_Exception_Compiler('The first argument to hardlight must be a color' . ($color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4070 } 4071 if (!$color2 instanceof Less_Tree_Color) { 4072 throw new Less_Exception_Compiler('The second argument to hardlight must be a color' . ($color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4073 } 4074 4075 return $this->colorBlend( array($this,'colorBlendHardlight'), $color1, $color2 ); 4076 } 4077 4078 private function colorBlendHardlight( $cb, $cs ){ 4079 return $this->colorBlendOverlay($cs, $cb); 4080 } 4081 4082 public function difference($color1 = null, $color2 = null) { 4083 if (!$color1 instanceof Less_Tree_Color) { 4084 throw new Less_Exception_Compiler('The first argument to difference must be a color' . ($color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4085 } 4086 if (!$color2 instanceof Less_Tree_Color) { 4087 throw new Less_Exception_Compiler('The second argument to difference must be a color' . ($color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4088 } 4089 4090 return $this->colorBlend( array($this,'colorBlendDifference'), $color1, $color2 ); 4091 } 4092 4093 private function colorBlendDifference( $cb, $cs ){ 4094 return abs($cb - $cs); 4095 } 4096 4097 public function exclusion( $color1 = null, $color2 = null ){ 4098 if (!$color1 instanceof Less_Tree_Color) { 4099 throw new Less_Exception_Compiler('The first argument to exclusion must be a color' . ($color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4100 } 4101 if (!$color2 instanceof Less_Tree_Color) { 4102 throw new Less_Exception_Compiler('The second argument to exclusion must be a color' . ($color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4103 } 4104 4105 return $this->colorBlend( array($this,'colorBlendExclusion'), $color1, $color2 ); 4106 } 4107 4108 private function colorBlendExclusion( $cb, $cs ){ 4109 return $cb + $cs - 2 * $cb * $cs; 4110 } 4111 4112 public function average($color1 = null, $color2 = null){ 4113 if (!$color1 instanceof Less_Tree_Color) { 4114 throw new Less_Exception_Compiler('The first argument to average must be a color' . ($color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4115 } 4116 if (!$color2 instanceof Less_Tree_Color) { 4117 throw new Less_Exception_Compiler('The second argument to average must be a color' . ($color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4118 } 4119 4120 return $this->colorBlend( array($this,'colorBlendAverage'), $color1, $color2 ); 4121 } 4122 4123 // non-w3c functions: 4124 public function colorBlendAverage($cb, $cs ){ 4125 return ($cb + $cs) / 2; 4126 } 4127 4128 public function negation($color1 = null, $color2 = null ){ 4129 if (!$color1 instanceof Less_Tree_Color) { 4130 throw new Less_Exception_Compiler('The first argument to negation must be a color' . ($color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4131 } 4132 if (!$color2 instanceof Less_Tree_Color) { 4133 throw new Less_Exception_Compiler('The second argument to negation must be a color' . ($color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '') ); 4134 } 4135 4136 return $this->colorBlend( array($this,'colorBlendNegation'), $color1, $color2 ); 4137 } 4138 4139 public function colorBlendNegation($cb, $cs){ 4140 return 1 - abs($cb + $cs - 1); 4141 } 4142 4143 // ~ End of Color Blending 4144 4145 } 4146 4147 4148 /** 4149 * Mime lookup 4150 * 4151 * @package Less 4152 * @subpackage node 4153 */ 4154 class Less_Mime{ 4155 4156 // this map is intentionally incomplete 4157 // if you want more, install 'mime' dep 4158 static $_types = array( 4159 '.htm' => 'text/html', 4160 '.html'=> 'text/html', 4161 '.gif' => 'image/gif', 4162 '.jpg' => 'image/jpeg', 4163 '.jpeg'=> 'image/jpeg', 4164 '.png' => 'image/png', 4165 '.ttf' => 'application/x-font-ttf', 4166 '.otf' => 'application/x-font-otf', 4167 '.eot' => 'application/vnd.ms-fontobject', 4168 '.woff' => 'application/x-font-woff', 4169 '.svg' => 'image/svg+xml', 4170 ); 4171 4172 public static function lookup( $filepath ){ 4173 $parts = explode('.',$filepath); 4174 $ext = '.'.strtolower(array_pop($parts)); 4175 4176 if( !isset(self::$_types[$ext]) ){ 4177 return null; 4178 } 4179 return self::$_types[$ext]; 4180 } 4181 4182 public static function charsets_lookup( $type = null ){ 4183 // assumes all text types are UTF-8 4184 return $type && preg_match('/^text\//',$type) ? 'UTF-8' : ''; 4185 } 4186 } 4187 4188 4189 /** 4190 * Tree 4191 * 4192 * @package Less 4193 * @subpackage tree 4194 */ 4195 class Less_Tree{ 4196 4197 public $cache_string; 4198 4199 public function toCSS(){ 4200 $output = new Less_Output(); 4201 $this->genCSS($output); 4202 return $output->toString(); 4203 } 4204 4205 4206 /** 4207 * Generate CSS by adding it to the output object 4208 * 4209 * @param Less_Output $output The output 4210 * @return void 4211 */ 4212 public function genCSS($output){} 4213 4214 4215 /** 4216 * @param Less_Tree_Ruleset[] $rules 4217 */ 4218 public static function outputRuleset( $output, $rules ){ 4219 4220 $ruleCnt = count($rules); 4221 Less_Environment::$tabLevel++; 4222 4223 4224 // Compressed 4225 if( Less_Parser::$options['compress'] ){ 4226 $output->add('{'); 4227 for( $i = 0; $i < $ruleCnt; $i++ ){ 4228 $rules[$i]->genCSS( $output ); 4229 } 4230 4231 $output->add( '}' ); 4232 Less_Environment::$tabLevel--; 4233 return; 4234 } 4235 4236 4237 // Non-compressed 4238 $tabSetStr = "\n".str_repeat( Less_Parser::$options['indentation'] , Less_Environment::$tabLevel-1 ); 4239 $tabRuleStr = $tabSetStr.Less_Parser::$options['indentation']; 4240 4241 $output->add( " {" ); 4242 for($i = 0; $i < $ruleCnt; $i++ ){ 4243 $output->add( $tabRuleStr ); 4244 $rules[$i]->genCSS( $output ); 4245 } 4246 Less_Environment::$tabLevel--; 4247 $output->add( $tabSetStr.'}' ); 4248 4249 } 4250 4251 public function accept($visitor){} 4252 4253 4254 public static function ReferencedArray($rules){ 4255 foreach($rules as $rule){ 4256 if( method_exists($rule, 'markReferenced') ){ 4257 $rule->markReferenced(); 4258 } 4259 } 4260 } 4261 4262 4263 /** 4264 * Requires php 5.3+ 4265 */ 4266 public static function __set_state($args){ 4267 4268 $class = get_called_class(); 4269 $obj = new $class(null,null,null,null); 4270 foreach($args as $key => $val){ 4271 $obj->$key = $val; 4272 } 4273 return $obj; 4274 } 4275 4276 } 4277 4278 4279 /** 4280 * Parser output 4281 * 4282 * @package Less 4283 * @subpackage output 4284 */ 4285 class Less_Output{ 4286 4287 /** 4288 * Output holder 4289 * 4290 * @var string 4291 */ 4292 protected $strs = array(); 4293 4294 /** 4295 * Adds a chunk to the stack 4296 * 4297 * @param string $chunk The chunk to output 4298 * @param Less_FileInfo $fileInfo The file information 4299 * @param integer $index The index 4300 * @param mixed $mapLines 4301 */ 4302 public function add($chunk, $fileInfo = null, $index = 0, $mapLines = null){ 4303 $this->strs[] = $chunk; 4304 } 4305 4306 /** 4307 * Is the output empty? 4308 * 4309 * @return boolean 4310 */ 4311 public function isEmpty(){ 4312 return count($this->strs) === 0; 4313 } 4314 4315 4316 /** 4317 * Converts the output to string 4318 * 4319 * @return string 4320 */ 4321 public function toString(){ 4322 return implode('',$this->strs); 4323 } 4324 4325 } 4326 4327 /** 4328 * Visitor 4329 * 4330 * @package Less 4331 * @subpackage visitor 4332 */ 4333 class Less_Visitor{ 4334 4335 protected $methods = array(); 4336 protected $_visitFnCache = array(); 4337 4338 public function __construct(){ 4339 $this->_visitFnCache = get_class_methods(get_class($this)); 4340 $this->_visitFnCache = array_flip($this->_visitFnCache); 4341 } 4342 4343 public function visitObj( $node ){ 4344 4345 $funcName = 'visit'.$node->type; 4346 if( isset($this->_visitFnCache[$funcName]) ){ 4347 4348 $visitDeeper = true; 4349 $this->$funcName( $node, $visitDeeper ); 4350 4351 if( $visitDeeper ){ 4352 $node->accept($this); 4353 } 4354 4355 $funcName = $funcName . "Out"; 4356 if( isset($this->_visitFnCache[$funcName]) ){ 4357 $this->$funcName( $node ); 4358 } 4359 4360 }else{ 4361 $node->accept($this); 4362 } 4363 4364 return $node; 4365 } 4366 4367 public function visitArray( $nodes ){ 4368 4369 array_map( array($this,'visitObj'), $nodes); 4370 return $nodes; 4371 } 4372 } 4373 4374 4375 4376 /** 4377 * Replacing Visitor 4378 * 4379 * @package Less 4380 * @subpackage visitor 4381 */ 4382 class Less_VisitorReplacing extends Less_Visitor{ 4383 4384 public function visitObj( $node ){ 4385 4386 $funcName = 'visit'.$node->type; 4387 if( isset($this->_visitFnCache[$funcName]) ){ 4388 4389 $visitDeeper = true; 4390 $node = $this->$funcName( $node, $visitDeeper ); 4391 4392 if( $node ){ 4393 if( $visitDeeper && is_object($node) ){ 4394 $node->accept($this); 4395 } 4396 4397 $funcName = $funcName . "Out"; 4398 if( isset($this->_visitFnCache[$funcName]) ){ 4399 $this->$funcName( $node ); 4400 } 4401 } 4402 4403 }else{ 4404 $node->accept($this); 4405 } 4406 4407 return $node; 4408 } 4409 4410 public function visitArray( $nodes ){ 4411 4412 $newNodes = array(); 4413 foreach($nodes as $node){ 4414 $evald = $this->visitObj($node); 4415 if( $evald ){ 4416 if( is_array($evald) ){ 4417 self::flatten($evald,$newNodes); 4418 }else{ 4419 $newNodes[] = $evald; 4420 } 4421 } 4422 } 4423 return $newNodes; 4424 } 4425 4426 public function flatten( $arr, &$out ){ 4427 4428 foreach($arr as $item){ 4429 if( !is_array($item) ){ 4430 $out[] = $item; 4431 continue; 4432 } 4433 4434 foreach($item as $nestedItem){ 4435 if( is_array($nestedItem) ){ 4436 self::flatten( $nestedItem, $out); 4437 }else{ 4438 $out[] = $nestedItem; 4439 } 4440 } 4441 } 4442 4443 return $out; 4444 } 4445 4446 } 4447 4448 4449 4450 4451 /** 4452 * Configurable 4453 * 4454 * @package Less 4455 * @subpackage Core 4456 */ 4457 abstract class Less_Configurable { 4458 4459 /** 4460 * Array of options 4461 * 4462 * @var array 4463 */ 4464 protected $options = array(); 4465 4466 /** 4467 * Array of default options 4468 * 4469 * @var array 4470 */ 4471 protected $defaultOptions = array(); 4472 4473 4474 /** 4475 * Set options 4476 * 4477 * If $options is an object it will be converted into an array by called 4478 * it's toArray method. 4479 * 4480 * @throws Exception 4481 * @param array|object $options 4482 * 4483 */ 4484 public function setOptions($options){ 4485 $options = array_intersect_key($options,$this->defaultOptions); 4486 $this->options = array_merge($this->defaultOptions, $this->options, $options); 4487 } 4488 4489 4490 /** 4491 * Get an option value by name 4492 * 4493 * If the option is empty or not set a NULL value will be returned. 4494 * 4495 * @param string $name 4496 * @param mixed $default Default value if confiuration of $name is not present 4497 * @return mixed 4498 */ 4499 public function getOption($name, $default = null){ 4500 if(isset($this->options[$name])){ 4501 return $this->options[$name]; 4502 } 4503 return $default; 4504 } 4505 4506 4507 /** 4508 * Set an option 4509 * 4510 * @param string $name 4511 * @param mixed $value 4512 */ 4513 public function setOption($name, $value){ 4514 $this->options[$name] = $value; 4515 } 4516 4517 } 4518 4519 /** 4520 * Alpha 4521 * 4522 * @package Less 4523 * @subpackage tree 4524 */ 4525 class Less_Tree_Alpha extends Less_Tree{ 4526 public $value; 4527 public $type = 'Alpha'; 4528 4529 public function __construct($val){ 4530 $this->value = $val; 4531 } 4532 4533 //function accept( $visitor ){ 4534 // $this->value = $visitor->visit( $this->value ); 4535 //} 4536 4537 public function compile($env){ 4538 4539 if( is_object($this->value) ){ 4540 $this->value = $this->value->compile($env); 4541 } 4542 4543 return $this; 4544 } 4545 4546 /** 4547 * @see Less_Tree::genCSS 4548 */ 4549 public function genCSS( $output ){ 4550 4551 $output->add( "alpha(opacity=" ); 4552 4553 if( is_string($this->value) ){ 4554 $output->add( $this->value ); 4555 }else{ 4556 $this->value->genCSS( $output); 4557 } 4558 4559 $output->add( ')' ); 4560 } 4561 4562 public function toCSS(){ 4563 return "alpha(opacity=" . (is_string($this->value) ? $this->value : $this->value->toCSS()) . ")"; 4564 } 4565 4566 4567 } 4568 4569 /** 4570 * Anonymous 4571 * 4572 * @package Less 4573 * @subpackage tree 4574 */ 4575 class Less_Tree_Anonymous extends Less_Tree{ 4576 public $value; 4577 public $quote; 4578 public $index; 4579 public $mapLines; 4580 public $currentFileInfo; 4581 public $type = 'Anonymous'; 4582 4583 /** 4584 * @param integer $index 4585 * @param boolean $mapLines 4586 */ 4587 public function __construct($value, $index = null, $currentFileInfo = null, $mapLines = null ){ 4588 $this->value = $value; 4589 $this->index = $index; 4590 $this->mapLines = $mapLines; 4591 $this->currentFileInfo = $currentFileInfo; 4592 } 4593 4594 public function compile(){ 4595 return new Less_Tree_Anonymous($this->value, $this->index, $this->currentFileInfo, $this->mapLines); 4596 } 4597 4598 public function compare($x){ 4599 if( !is_object($x) ){ 4600 return -1; 4601 } 4602 4603 $left = $this->toCSS(); 4604 $right = $x->toCSS(); 4605 4606 if( $left === $right ){ 4607 return 0; 4608 } 4609 4610 return $left < $right ? -1 : 1; 4611 } 4612 4613 /** 4614 * @see Less_Tree::genCSS 4615 */ 4616 public function genCSS( $output ){ 4617 $output->add( $this->value, $this->currentFileInfo, $this->index, $this->mapLines ); 4618 } 4619 4620 public function toCSS(){ 4621 return $this->value; 4622 } 4623 4624 } 4625 4626 4627 /** 4628 * Assignment 4629 * 4630 * @package Less 4631 * @subpackage tree 4632 */ 4633 class Less_Tree_Assignment extends Less_Tree{ 4634 4635 public $key; 4636 public $value; 4637 public $type = 'Assignment'; 4638 4639 public function __construct($key, $val) { 4640 $this->key = $key; 4641 $this->value = $val; 4642 } 4643 4644 public function accept( $visitor ){ 4645 $this->value = $visitor->visitObj( $this->value ); 4646 } 4647 4648 public function compile($env) { 4649 return new Less_Tree_Assignment( $this->key, $this->value->compile($env)); 4650 } 4651 4652 /** 4653 * @see Less_Tree::genCSS 4654 */ 4655 public function genCSS( $output ){ 4656 $output->add( $this->key . '=' ); 4657 $this->value->genCSS( $output ); 4658 } 4659 4660 public function toCss(){ 4661 return $this->key . '=' . $this->value->toCSS(); 4662 } 4663 } 4664 4665 4666 /** 4667 * Attribute 4668 * 4669 * @package Less 4670 * @subpackage tree 4671 */ 4672 class Less_Tree_Attribute extends Less_Tree{ 4673 4674 public $key; 4675 public $op; 4676 public $value; 4677 public $type = 'Attribute'; 4678 4679 public function __construct($key, $op, $value){ 4680 $this->key = $key; 4681 $this->op = $op; 4682 $this->value = $value; 4683 } 4684 4685 public function compile($env){ 4686 4687 $key_obj = is_object($this->key); 4688 $val_obj = is_object($this->value); 4689 4690 if( !$key_obj && !$val_obj ){ 4691 return $this; 4692 } 4693 4694 return new Less_Tree_Attribute( 4695 $key_obj ? $this->key->compile($env) : $this->key , 4696 $this->op, 4697 $val_obj ? $this->value->compile($env) : $this->value); 4698 } 4699 4700 /** 4701 * @see Less_Tree::genCSS 4702 */ 4703 public function genCSS( $output ){ 4704 $output->add( $this->toCSS() ); 4705 } 4706 4707 public function toCSS(){ 4708 $value = $this->key; 4709 4710 if( $this->op ){ 4711 $value .= $this->op; 4712 $value .= (is_object($this->value) ? $this->value->toCSS() : $this->value); 4713 } 4714 4715 return '[' . $value . ']'; 4716 } 4717 } 4718 4719 4720 /** 4721 * Call 4722 * 4723 * @package Less 4724 * @subpackage tree 4725 */ 4726 class Less_Tree_Call extends Less_Tree{ 4727 public $value; 4728 4729 protected $name; 4730 protected $args; 4731 protected $index; 4732 protected $currentFileInfo; 4733 public $type = 'Call'; 4734 4735 public function __construct($name, $args, $index, $currentFileInfo = null ){ 4736 $this->name = $name; 4737 $this->args = $args; 4738 $this->index = $index; 4739 $this->currentFileInfo = $currentFileInfo; 4740 } 4741 4742 public function accept( $visitor ){ 4743 $this->args = $visitor->visitArray( $this->args ); 4744 } 4745 4746 // 4747 // When evaluating a function call, 4748 // we either find the function in `tree.functions` [1], 4749 // in which case we call it, passing the evaluated arguments, 4750 // or we simply print it out as it appeared originally [2]. 4751 // 4752 // The *functions.js* file contains the built-in functions. 4753 // 4754 // The reason why we evaluate the arguments, is in the case where 4755 // we try to pass a variable to a function, like: `saturate(@color)`. 4756 // The function should receive the value, not the variable. 4757 // 4758 public function compile($env=null){ 4759 $args = array(); 4760 foreach($this->args as $a){ 4761 $args[] = $a->compile($env); 4762 } 4763 4764 $nameLC = strtolower($this->name); 4765 switch($nameLC){ 4766 case '%': 4767 $nameLC = '_percent'; 4768 break; 4769 4770 case 'get-unit': 4771 $nameLC = 'getunit'; 4772 break; 4773 4774 case 'data-uri': 4775 $nameLC = 'datauri'; 4776 break; 4777 4778 case 'svg-gradient': 4779 $nameLC = 'svggradient'; 4780 break; 4781 } 4782 4783 $result = null; 4784 if( $nameLC === 'default' ){ 4785 $result = Less_Tree_DefaultFunc::compile(); 4786 4787 }else{ 4788 4789 if( method_exists('Less_Functions',$nameLC) ){ // 1. 4790 try { 4791 4792 $func = new Less_Functions($env, $this->currentFileInfo); 4793 $result = call_user_func_array( array($func,$nameLC),$args); 4794 4795 } catch (Exception $e) { 4796 throw new Less_Exception_Compiler('error evaluating function `' . $this->name . '` '.$e->getMessage().' index: '. $this->index); 4797 } 4798 } elseif( isset( $env->functions[$nameLC] ) && is_callable( $env->functions[$nameLC] ) ) { 4799 try { 4800 $result = call_user_func_array( $env->functions[$nameLC], $args ); 4801 } catch (Exception $e) { 4802 throw new Less_Exception_Compiler('error evaluating function `' . $this->name . '` '.$e->getMessage().' index: '. $this->index); 4803 } 4804 } 4805 } 4806 4807 if( $result !== null ){ 4808 return $result; 4809 } 4810 4811 4812 return new Less_Tree_Call( $this->name, $args, $this->index, $this->currentFileInfo ); 4813 } 4814 4815 /** 4816 * @see Less_Tree::genCSS 4817 */ 4818 public function genCSS( $output ){ 4819 4820 $output->add( $this->name . '(', $this->currentFileInfo, $this->index ); 4821 $args_len = count($this->args); 4822 for($i = 0; $i < $args_len; $i++ ){ 4823 $this->args[$i]->genCSS( $output ); 4824 if( $i + 1 < $args_len ){ 4825 $output->add( ', ' ); 4826 } 4827 } 4828 4829 $output->add( ')' ); 4830 } 4831 4832 4833 //public function toCSS(){ 4834 // return $this->compile()->toCSS(); 4835 //} 4836 4837 } 4838 4839 4840 /** 4841 * Color 4842 * 4843 * @package Less 4844 * @subpackage tree 4845 */ 4846 class Less_Tree_Color extends Less_Tree{ 4847 public $rgb; 4848 public $alpha; 4849 public $isTransparentKeyword; 4850 public $type = 'Color'; 4851 4852 public function __construct($rgb, $a = 1, $isTransparentKeyword = null ){ 4853 4854 if( $isTransparentKeyword ){ 4855 $this->rgb = $rgb; 4856 $this->alpha = $a; 4857 $this->isTransparentKeyword = true; 4858 return; 4859 } 4860 4861 $this->rgb = array(); 4862 if( is_array($rgb) ){ 4863 $this->rgb = $rgb; 4864 }else if( strlen($rgb) == 6 ){ 4865 foreach(str_split($rgb, 2) as $c){ 4866 $this->rgb[] = hexdec($c); 4867 } 4868 }else{ 4869 foreach(str_split($rgb, 1) as $c){ 4870 $this->rgb[] = hexdec($c.$c); 4871 } 4872 } 4873 $this->alpha = is_numeric($a) ? $a : 1; 4874 } 4875 4876 public function compile(){ 4877 return $this; 4878 } 4879 4880 public function luma(){ 4881 $r = $this->rgb[0] / 255; 4882 $g = $this->rgb[1] / 255; 4883 $b = $this->rgb[2] / 255; 4884 4885 $r = ($r <= 0.03928) ? $r / 12.92 : pow((($r + 0.055) / 1.055), 2.4); 4886 $g = ($g <= 0.03928) ? $g / 12.92 : pow((($g + 0.055) / 1.055), 2.4); 4887 $b = ($b <= 0.03928) ? $b / 12.92 : pow((($b + 0.055) / 1.055), 2.4); 4888 4889 return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b; 4890 } 4891 4892 /** 4893 * @see Less_Tree::genCSS 4894 */ 4895 public function genCSS( $output ){ 4896 $output->add( $this->toCSS() ); 4897 } 4898 4899 public function toCSS( $doNotCompress = false ){ 4900 $compress = Less_Parser::$options['compress'] && !$doNotCompress; 4901 $alpha = Less_Functions::fround( $this->alpha ); 4902 4903 4904 // 4905 // If we have some transparency, the only way to represent it 4906 // is via `rgba`. Otherwise, we use the hex representation, 4907 // which has better compatibility with older browsers. 4908 // Values are capped between `0` and `255`, rounded and zero-padded. 4909 // 4910 if( $alpha < 1 ){ 4911 if( ( $alpha === 0 || $alpha === 0.0 ) && isset($this->isTransparentKeyword) && $this->isTransparentKeyword ){ 4912 return 'transparent'; 4913 } 4914 4915 $values = array(); 4916 foreach($this->rgb as $c){ 4917 $values[] = Less_Functions::clamp( round($c), 255); 4918 } 4919 $values[] = $alpha; 4920 4921 $glue = ($compress ? ',' : ', '); 4922 return "rgba(" . implode($glue, $values) . ")"; 4923 }else{ 4924 4925 $color = $this->toRGB(); 4926 4927 if( $compress ){ 4928 4929 // Convert color to short format 4930 if( $color[1] === $color[2] && $color[3] === $color[4] && $color[5] === $color[6]) { 4931 $color = '#'.$color[1] . $color[3] . $color[5]; 4932 } 4933 } 4934 4935 return $color; 4936 } 4937 } 4938 4939 // 4940 // Operations have to be done per-channel, if not, 4941 // channels will spill onto each other. Once we have 4942 // our result, in the form of an integer triplet, 4943 // we create a new Color node to hold the result. 4944 // 4945 4946 /** 4947 * @param string $op 4948 */ 4949 public function operate( $op, $other) { 4950 $rgb = array(); 4951 $alpha = $this->alpha * (1 - $other->alpha) + $other->alpha; 4952 for ($c = 0; $c < 3; $c++) { 4953 $rgb[$c] = Less_Functions::operate( $op, $this->rgb[$c], $other->rgb[$c]); 4954 } 4955 return new Less_Tree_Color($rgb, $alpha); 4956 } 4957 4958 public function toRGB(){ 4959 return $this->toHex($this->rgb); 4960 } 4961 4962 public function toHSL(){ 4963 $r = $this->rgb[0] / 255; 4964 $g = $this->rgb[1] / 255; 4965 $b = $this->rgb[2] / 255; 4966 $a = $this->alpha; 4967 4968 $max = max($r, $g, $b); 4969 $min = min($r, $g, $b); 4970 $l = ($max + $min) / 2; 4971 $d = $max - $min; 4972 4973 $h = $s = 0; 4974 if( $max !== $min ){ 4975 $s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min); 4976 4977 switch ($max) { 4978 case $r: $h = ($g - $b) / $d + ($g < $b ? 6 : 0); break; 4979 case $g: $h = ($b - $r) / $d + 2; break; 4980 case $b: $h = ($r - $g) / $d + 4; break; 4981 } 4982 $h /= 6; 4983 } 4984 return array('h' => $h * 360, 's' => $s, 'l' => $l, 'a' => $a ); 4985 } 4986 4987 //Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript 4988 public function toHSV() { 4989 $r = $this->rgb[0] / 255; 4990 $g = $this->rgb[1] / 255; 4991 $b = $this->rgb[2] / 255; 4992 $a = $this->alpha; 4993 4994 $max = max($r, $g, $b); 4995 $min = min($r, $g, $b); 4996 4997 $v = $max; 4998 4999 $d = $max - $min; 5000 if ($max === 0) { 5001 $s = 0; 5002 } else { 5003 $s = $d / $max; 5004 } 5005 5006 $h = 0; 5007 if( $max !== $min ){ 5008 switch($max){ 5009 case $r: $h = ($g - $b) / $d + ($g < $b ? 6 : 0); break; 5010 case $g: $h = ($b - $r) / $d + 2; break; 5011 case $b: $h = ($r - $g) / $d + 4; break; 5012 } 5013 $h /= 6; 5014 } 5015 return array('h'=> $h * 360, 's'=> $s, 'v'=> $v, 'a' => $a ); 5016 } 5017 5018 public function toARGB(){ 5019 $argb = array_merge( (array) Less_Parser::round($this->alpha * 255), $this->rgb); 5020 return $this->toHex( $argb ); 5021 } 5022 5023 public function compare($x){ 5024 5025 if( !property_exists( $x, 'rgb' ) ){ 5026 return -1; 5027 } 5028 5029 5030 return ($x->rgb[0] === $this->rgb[0] && 5031 $x->rgb[1] === $this->rgb[1] && 5032 $x->rgb[2] === $this->rgb[2] && 5033 $x->alpha === $this->alpha) ? 0 : -1; 5034 } 5035 5036 public function toHex( $v ){ 5037 5038 $ret = '#'; 5039 foreach($v as $c){ 5040 $c = Less_Functions::clamp( Less_Parser::round($c), 255); 5041 if( $c < 16 ){ 5042 $ret .= '0'; 5043 } 5044 $ret .= dechex($c); 5045 } 5046 5047 return $ret; 5048 } 5049 5050 5051 /** 5052 * @param string $keyword 5053 */ 5054 public static function fromKeyword( $keyword ){ 5055 $keyword = strtolower($keyword); 5056 5057 if( Less_Colors::hasOwnProperty($keyword) ){ 5058 // detect named color 5059 return new Less_Tree_Color(substr(Less_Colors::color($keyword), 1)); 5060 } 5061 5062 if( $keyword === 'transparent' ){ 5063 return new Less_Tree_Color( array(0, 0, 0), 0, true); 5064 } 5065 } 5066 5067 } 5068 5069 5070 /** 5071 * Comment 5072 * 5073 * @package Less 5074 * @subpackage tree 5075 */ 5076 class Less_Tree_Comment extends Less_Tree{ 5077 5078 public $value; 5079 public $silent; 5080 public $isReferenced; 5081 public $currentFileInfo; 5082 public $type = 'Comment'; 5083 5084 public function __construct($value, $silent, $index = null, $currentFileInfo = null ){ 5085 $this->value = $value; 5086 $this->silent = !! $silent; 5087 $this->currentFileInfo = $currentFileInfo; 5088 } 5089 5090 /** 5091 * @see Less_Tree::genCSS 5092 */ 5093 public function genCSS( $output ){ 5094 //if( $this->debugInfo ){ 5095 //$output->add( tree.debugInfo($env, $this), $this->currentFileInfo, $this->index); 5096 //} 5097 $output->add( trim($this->value) );//TODO shouldn't need to trim, we shouldn't grab the \n 5098 } 5099 5100 public function toCSS(){ 5101 return Less_Parser::$options['compress'] ? '' : $this->value; 5102 } 5103 5104 public function isSilent(){ 5105 $isReference = ($this->currentFileInfo && isset($this->currentFileInfo['reference']) && (!isset($this->isReferenced) || !$this->isReferenced) ); 5106 $isCompressed = Less_Parser::$options['compress'] && !preg_match('/^\/\*!/', $this->value); 5107 return $this->silent || $isReference || $isCompressed; 5108 } 5109 5110 public function compile(){ 5111 return $this; 5112 } 5113 5114 public function markReferenced(){ 5115 $this->isReferenced = true; 5116 } 5117 5118 } 5119 5120 5121 /** 5122 * Condition 5123 * 5124 * @package Less 5125 * @subpackage tree 5126 */ 5127 class Less_Tree_Condition extends Less_Tree{ 5128 5129 public $op; 5130 public $lvalue; 5131 public $rvalue; 5132 public $index; 5133 public $negate; 5134 public $type = 'Condition'; 5135 5136 public function __construct($op, $l, $r, $i = 0, $negate = false) { 5137 $this->op = trim($op); 5138 $this->lvalue = $l; 5139 $this->rvalue = $r; 5140 $this->index = $i; 5141 $this->negate = $negate; 5142 } 5143 5144 public function accept($visitor){ 5145 $this->lvalue = $visitor->visitObj( $this->lvalue ); 5146 $this->rvalue = $visitor->visitObj( $this->rvalue ); 5147 } 5148 5149 public function compile($env) { 5150 $a = $this->lvalue->compile($env); 5151 $b = $this->rvalue->compile($env); 5152 5153 switch( $this->op ){ 5154 case 'and': 5155 $result = $a && $b; 5156 break; 5157 5158 case 'or': 5159 $result = $a || $b; 5160 break; 5161 5162 default: 5163 if( Less_Parser::is_method($a, 'compare') ){ 5164 $result = $a->compare($b); 5165 }elseif( Less_Parser::is_method($b, 'compare') ){ 5166 $result = $b->compare($a); 5167 }else{ 5168 throw new Less_Exception_Compiler('Unable to perform comparison', null, $this->index); 5169 } 5170 5171 switch ($result) { 5172 case -1: 5173 $result = $this->op === '<' || $this->op === '=<' || $this->op === '<='; 5174 break; 5175 5176 case 0: 5177 $result = $this->op === '=' || $this->op === '>=' || $this->op === '=<' || $this->op === '<='; 5178 break; 5179 5180 case 1: 5181 $result = $this->op === '>' || $this->op === '>='; 5182 break; 5183 } 5184 break; 5185 } 5186 5187 return $this->negate ? !$result : $result; 5188 } 5189 5190 } 5191 5192 5193 /** 5194 * DefaultFunc 5195 * 5196 * @package Less 5197 * @subpackage tree 5198 */ 5199 class Less_Tree_DefaultFunc{ 5200 5201 static $error_; 5202 static $value_; 5203 5204 public static function compile(){ 5205 if( self::$error_ ){ 5206 throw new Exception(self::$error_); 5207 } 5208 if( self::$value_ !== null ){ 5209 return self::$value_ ? new Less_Tree_Keyword('true') : new Less_Tree_Keyword('false'); 5210 } 5211 } 5212 5213 public static function value( $v ){ 5214 self::$value_ = $v; 5215 } 5216 5217 public static function error( $e ){ 5218 self::$error_ = $e; 5219 } 5220 5221 public static function reset(){ 5222 self::$value_ = self::$error_ = null; 5223 } 5224 } 5225 5226 /** 5227 * DetachedRuleset 5228 * 5229 * @package Less 5230 * @subpackage tree 5231 */ 5232 class Less_Tree_DetachedRuleset extends Less_Tree{ 5233 5234 public $ruleset; 5235 public $frames; 5236 public $type = 'DetachedRuleset'; 5237 5238 public function __construct( $ruleset, $frames = null ){ 5239 $this->ruleset = $ruleset; 5240 $this->frames = $frames; 5241 } 5242 5243 public function accept($visitor) { 5244 $this->ruleset = $visitor->visitObj($this->ruleset); 5245 } 5246 5247 public function compile($env){ 5248 if( $this->frames ){ 5249 $frames = $this->frames; 5250 }else{ 5251 $frames = $env->frames; 5252 } 5253 return new Less_Tree_DetachedRuleset($this->ruleset, $frames); 5254 } 5255 5256 public function callEval($env) { 5257 if( $this->frames ){ 5258 return $this->ruleset->compile( $env->copyEvalEnv( array_merge($this->frames,$env->frames) ) ); 5259 } 5260 return $this->ruleset->compile( $env ); 5261 } 5262 } 5263 5264 5265 5266 /** 5267 * Dimension 5268 * 5269 * @package Less 5270 * @subpackage tree 5271 */ 5272 class Less_Tree_Dimension extends Less_Tree{ 5273 5274 public $value; 5275 public $unit; 5276 public $type = 'Dimension'; 5277 5278 public function __construct($value, $unit = null){ 5279 $this->value = floatval($value); 5280 5281 if( $unit && ($unit instanceof Less_Tree_Unit) ){ 5282 $this->unit = $unit; 5283 }elseif( $unit ){ 5284 $this->unit = new Less_Tree_Unit( array($unit) ); 5285 }else{ 5286 $this->unit = new Less_Tree_Unit( ); 5287 } 5288 } 5289 5290 public function accept( $visitor ){ 5291 $this->unit = $visitor->visitObj( $this->unit ); 5292 } 5293 5294 public function compile(){ 5295 return $this; 5296 } 5297 5298 public function toColor() { 5299 return new Less_Tree_Color(array($this->value, $this->value, $this->value)); 5300 } 5301 5302 /** 5303 * @see Less_Tree::genCSS 5304 */ 5305 public function genCSS( $output ){ 5306 5307 if( Less_Parser::$options['strictUnits'] && !$this->unit->isSingular() ){ 5308 throw new Less_Exception_Compiler("Multiple units in dimension. Correct the units or use the unit function. Bad unit: ".$this->unit->toString()); 5309 } 5310 5311 $value = Less_Functions::fround( $this->value ); 5312 $strValue = (string)$value; 5313 5314 if( $value !== 0 && $value < 0.000001 && $value > -0.000001 ){ 5315 // would be output 1e-6 etc. 5316 $strValue = number_format($strValue,10); 5317 $strValue = preg_replace('/\.?0+$/','', $strValue); 5318 } 5319 5320 if( Less_Parser::$options['compress'] ){ 5321 // Zero values doesn't need a unit 5322 if( $value === 0 && $this->unit->isLength() ){ 5323 $output->add( $strValue ); 5324 return $strValue; 5325 } 5326 5327 // Float values doesn't need a leading zero 5328 if( $value > 0 && $value < 1 && $strValue[0] === '0' ){ 5329 $strValue = substr($strValue,1); 5330 } 5331 } 5332 5333 $output->add( $strValue ); 5334 $this->unit->genCSS( $output ); 5335 } 5336 5337 public function __toString(){ 5338 return $this->toCSS(); 5339 } 5340 5341 // In an operation between two Dimensions, 5342 // we default to the first Dimension's unit, 5343 // so `1px + 2em` will yield `3px`. 5344 5345 /** 5346 * @param string $op 5347 */ 5348 public function operate( $op, $other){ 5349 5350 $value = Less_Functions::operate( $op, $this->value, $other->value); 5351 $unit = clone $this->unit; 5352 5353 if( $op === '+' || $op === '-' ){ 5354 5355 if( !$unit->numerator && !$unit->denominator ){ 5356 $unit->numerator = $other->unit->numerator; 5357 $unit->denominator = $other->unit->denominator; 5358 }elseif( !$other->unit->numerator && !$other->unit->denominator ){ 5359 // do nothing 5360 }else{ 5361 $other = $other->convertTo( $this->unit->usedUnits()); 5362 5363 if( Less_Parser::$options['strictUnits'] && $other->unit->toString() !== $unit->toCSS() ){ 5364 throw new Less_Exception_Compiler("Incompatible units. Change the units or use the unit function. Bad units: '" . $unit->toString() . "' and " . $other->unit->toString() . "'."); 5365 } 5366 5367 $value = Less_Functions::operate( $op, $this->value, $other->value); 5368 } 5369 }elseif( $op === '*' ){ 5370 $unit->numerator = array_merge($unit->numerator, $other->unit->numerator); 5371 $unit->denominator = array_merge($unit->denominator, $other->unit->denominator); 5372 sort($unit->numerator); 5373 sort($unit->denominator); 5374 $unit->cancel(); 5375 }elseif( $op === '/' ){ 5376 $unit->numerator = array_merge($unit->numerator, $other->unit->denominator); 5377 $unit->denominator = array_merge($unit->denominator, $other->unit->numerator); 5378 sort($unit->numerator); 5379 sort($unit->denominator); 5380 $unit->cancel(); 5381 } 5382 return new Less_Tree_Dimension( $value, $unit); 5383 } 5384 5385 public function compare($other) { 5386 if ($other instanceof Less_Tree_Dimension) { 5387 5388 if( $this->unit->isEmpty() || $other->unit->isEmpty() ){ 5389 $a = $this; 5390 $b = $other; 5391 } else { 5392 $a = $this->unify(); 5393 $b = $other->unify(); 5394 if( $a->unit->compare($b->unit) !== 0 ){ 5395 return -1; 5396 } 5397 } 5398 $aValue = $a->value; 5399 $bValue = $b->value; 5400 5401 if ($bValue > $aValue) { 5402 return -1; 5403 } elseif ($bValue < $aValue) { 5404 return 1; 5405 } else { 5406 return 0; 5407 } 5408 } else { 5409 return -1; 5410 } 5411 } 5412 5413 public function unify() { 5414 return $this->convertTo(array('length'=> 'px', 'duration'=> 's', 'angle' => 'rad' )); 5415 } 5416 5417 public function convertTo($conversions) { 5418 $value = $this->value; 5419 $unit = clone $this->unit; 5420 5421 if( is_string($conversions) ){ 5422 $derivedConversions = array(); 5423 foreach( Less_Tree_UnitConversions::$groups as $i ){ 5424 if( isset(Less_Tree_UnitConversions::${$i}[$conversions]) ){ 5425 $derivedConversions = array( $i => $conversions); 5426 } 5427 } 5428 $conversions = $derivedConversions; 5429 } 5430 5431 5432 foreach($conversions as $groupName => $targetUnit){ 5433 $group = Less_Tree_UnitConversions::${$groupName}; 5434 5435 //numerator 5436 foreach($unit->numerator as $i => $atomicUnit){ 5437 $atomicUnit = $unit->numerator[$i]; 5438 if( !isset($group[$atomicUnit]) ){ 5439 continue; 5440 } 5441 5442 $value = $value * ($group[$atomicUnit] / $group[$targetUnit]); 5443 5444 $unit->numerator[$i] = $targetUnit; 5445 } 5446 5447 //denominator 5448 foreach($unit->denominator as $i => $atomicUnit){ 5449 $atomicUnit = $unit->denominator[$i]; 5450 if( !isset($group[$atomicUnit]) ){ 5451 continue; 5452 } 5453 5454 $value = $value / ($group[$atomicUnit] / $group[$targetUnit]); 5455 5456 $unit->denominator[$i] = $targetUnit; 5457 } 5458 } 5459 5460 $unit->cancel(); 5461 5462 return new Less_Tree_Dimension( $value, $unit); 5463 } 5464 } 5465 5466 5467 /** 5468 * Directive 5469 * 5470 * @package Less 5471 * @subpackage tree 5472 */ 5473 class Less_Tree_Directive extends Less_Tree{ 5474 5475 public $name; 5476 public $value; 5477 public $rules; 5478 public $index; 5479 public $isReferenced; 5480 public $currentFileInfo; 5481 public $debugInfo; 5482 public $type = 'Directive'; 5483 5484 public function __construct($name, $value = null, $rules, $index = null, $currentFileInfo = null, $debugInfo = null ){ 5485 $this->name = $name; 5486 $this->value = $value; 5487 if( $rules ){ 5488 $this->rules = $rules; 5489 $this->rules->allowImports = true; 5490 } 5491 5492 $this->index = $index; 5493 $this->currentFileInfo = $currentFileInfo; 5494 $this->debugInfo = $debugInfo; 5495 } 5496 5497 5498 public function accept( $visitor ){ 5499 if( $this->rules ){ 5500 $this->rules = $visitor->visitObj( $this->rules ); 5501 } 5502 if( $this->value ){ 5503 $this->value = $visitor->visitObj( $this->value ); 5504 } 5505 } 5506 5507 5508 /** 5509 * @see Less_Tree::genCSS 5510 */ 5511 public function genCSS( $output ){ 5512 $value = $this->value; 5513 $rules = $this->rules; 5514 $output->add( $this->name, $this->currentFileInfo, $this->index ); 5515 if( $this->value ){ 5516 $output->add(' '); 5517 $this->value->genCSS($output); 5518 } 5519 if( $this->rules ){ 5520 Less_Tree::outputRuleset( $output, array($this->rules)); 5521 } else { 5522 $output->add(';'); 5523 } 5524 } 5525 5526 public function compile($env){ 5527 5528 $value = $this->value; 5529 $rules = $this->rules; 5530 if( $value ){ 5531 $value = $value->compile($env); 5532 } 5533 5534 if( $rules ){ 5535 $rules = $rules->compile($env); 5536 $rules->root = true; 5537 } 5538 5539 return new Less_Tree_Directive( $this->name, $value, $rules, $this->index, $this->currentFileInfo, $this->debugInfo ); 5540 } 5541 5542 5543 public function variable($name){ 5544 if( $this->rules ){ 5545 return $this->rules->variable($name); 5546 } 5547 } 5548 5549 public function find($selector){ 5550 if( $this->rules ){ 5551 return $this->rules->find($selector, $this); 5552 } 5553 } 5554 5555 //rulesets: function () { if (this.rules) return tree.Ruleset.prototype.rulesets.apply(this.rules); }, 5556 5557 public function markReferenced(){ 5558 $this->isReferenced = true; 5559 if( $this->rules ){ 5560 Less_Tree::ReferencedArray($this->rules->rules); 5561 } 5562 } 5563 5564 } 5565 5566 5567 /** 5568 * Element 5569 * 5570 * @package Less 5571 * @subpackage tree 5572 */ 5573 class Less_Tree_Element extends Less_Tree{ 5574 5575 public $combinator = ''; 5576 public $value = ''; 5577 public $index; 5578 public $currentFileInfo; 5579 public $type = 'Element'; 5580 5581 public $value_is_object = false; 5582 5583 public function __construct($combinator, $value, $index = null, $currentFileInfo = null ){ 5584 5585 $this->value = $value; 5586 $this->value_is_object = is_object($value); 5587 5588 if( $combinator ){ 5589 $this->combinator = $combinator; 5590 } 5591 5592 $this->index = $index; 5593 $this->currentFileInfo = $currentFileInfo; 5594 } 5595 5596 public function accept( $visitor ){ 5597 if( $this->value_is_object ){ //object or string 5598 $this->value = $visitor->visitObj( $this->value ); 5599 } 5600 } 5601 5602 public function compile($env){ 5603 5604 if( Less_Environment::$mixin_stack ){ 5605 return new Less_Tree_Element($this->combinator, ($this->value_is_object ? $this->value->compile($env) : $this->value), $this->index, $this->currentFileInfo ); 5606 } 5607 5608 if( $this->value_is_object ){ 5609 $this->value = $this->value->compile($env); 5610 } 5611 5612 return $this; 5613 } 5614 5615 /** 5616 * @see Less_Tree::genCSS 5617 */ 5618 public function genCSS( $output ){ 5619 $output->add( $this->toCSS(), $this->currentFileInfo, $this->index ); 5620 } 5621 5622 public function toCSS(){ 5623 5624 if( $this->value_is_object ){ 5625 $value = $this->value->toCSS(); 5626 }else{ 5627 $value = $this->value; 5628 } 5629 5630 5631 if( $value === '' && $this->combinator && $this->combinator === '&' ){ 5632 return ''; 5633 } 5634 5635 5636 return Less_Environment::$_outputMap[$this->combinator] . $value; 5637 } 5638 5639 } 5640 5641 5642 /** 5643 * Expression 5644 * 5645 * @package Less 5646 * @subpackage tree 5647 */ 5648 class Less_Tree_Expression extends Less_Tree{ 5649 5650 public $value = array(); 5651 public $parens = false; 5652 public $parensInOp = false; 5653 public $type = 'Expression'; 5654 5655 public function __construct( $value, $parens = null ){ 5656 $this->value = $value; 5657 $this->parens = $parens; 5658 } 5659 5660 public function accept( $visitor ){ 5661 $this->value = $visitor->visitArray( $this->value ); 5662 } 5663 5664 public function compile($env) { 5665 5666 $doubleParen = false; 5667 5668 if( $this->parens && !$this->parensInOp ){ 5669 Less_Environment::$parensStack++; 5670 } 5671 5672 $returnValue = null; 5673 if( $this->value ){ 5674 5675 $count = count($this->value); 5676 5677 if( $count > 1 ){ 5678 5679 $ret = array(); 5680 foreach($this->value as $e){ 5681 $ret[] = $e->compile($env); 5682 } 5683 $returnValue = new Less_Tree_Expression($ret); 5684 5685 }else{ 5686 5687 if( ($this->value[0] instanceof Less_Tree_Expression) && $this->value[0]->parens && !$this->value[0]->parensInOp ){ 5688 $doubleParen = true; 5689 } 5690 5691 $returnValue = $this->value[0]->compile($env); 5692 } 5693 5694 } else { 5695 $returnValue = $this; 5696 } 5697 5698 if( $this->parens ){ 5699 if( !$this->parensInOp ){ 5700 Less_Environment::$parensStack--; 5701 5702 }elseif( !Less_Environment::isMathOn() && !$doubleParen ){ 5703 $returnValue = new Less_Tree_Paren($returnValue); 5704 5705 } 5706 } 5707 return $returnValue; 5708 } 5709 5710 /** 5711 * @see Less_Tree::genCSS 5712 */ 5713 public function genCSS( $output ){ 5714 $val_len = count($this->value); 5715 for( $i = 0; $i < $val_len; $i++ ){ 5716 $this->value[$i]->genCSS( $output ); 5717 if( $i + 1 < $val_len ){ 5718 $output->add( ' ' ); 5719 } 5720 } 5721 } 5722 5723 public function throwAwayComments() { 5724 5725 if( is_array($this->value) ){ 5726 $new_value = array(); 5727 foreach($this->value as $v){ 5728 if( $v instanceof Less_Tree_Comment ){ 5729 continue; 5730 } 5731 $new_value[] = $v; 5732 } 5733 $this->value = $new_value; 5734 } 5735 } 5736 } 5737 5738 5739 /** 5740 * Extend 5741 * 5742 * @package Less 5743 * @subpackage tree 5744 */ 5745 class Less_Tree_Extend extends Less_Tree{ 5746 5747 public $selector; 5748 public $option; 5749 public $index; 5750 public $selfSelectors = array(); 5751 public $allowBefore; 5752 public $allowAfter; 5753 public $firstExtendOnThisSelectorPath; 5754 public $type = 'Extend'; 5755 public $ruleset; 5756 5757 5758 public $object_id; 5759 public $parent_ids = array(); 5760 5761 /** 5762 * @param integer $index 5763 */ 5764 public function __construct($selector, $option, $index){ 5765 static $i = 0; 5766 $this->selector = $selector; 5767 $this->option = $option; 5768 $this->index = $index; 5769 5770 switch($option){ 5771 case "all": 5772 $this->allowBefore = true; 5773 $this->allowAfter = true; 5774 break; 5775 default: 5776 $this->allowBefore = false; 5777 $this->allowAfter = false; 5778 break; 5779 } 5780 5781 $this->object_id = $i++; 5782 $this->parent_ids = array($this->object_id); 5783 } 5784 5785 public function accept( $visitor ){ 5786 $this->selector = $visitor->visitObj( $this->selector ); 5787 } 5788 5789 public function compile( $env ){ 5790 Less_Parser::$has_extends = true; 5791 $this->selector = $this->selector->compile($env); 5792 return $this; 5793 //return new Less_Tree_Extend( $this->selector->compile($env), $this->option, $this->index); 5794 } 5795 5796 public function findSelfSelectors( $selectors ){ 5797 $selfElements = array(); 5798 5799 5800 for( $i = 0, $selectors_len = count($selectors); $i < $selectors_len; $i++ ){ 5801 $selectorElements = $selectors[$i]->elements; 5802 // duplicate the logic in genCSS function inside the selector node. 5803 // future TODO - move both logics into the selector joiner visitor 5804 if( $i && $selectorElements && $selectorElements[0]->combinator === "") { 5805 $selectorElements[0]->combinator = ' '; 5806 } 5807 $selfElements = array_merge( $selfElements, $selectors[$i]->elements ); 5808 } 5809 5810 $this->selfSelectors = array(new Less_Tree_Selector($selfElements)); 5811 } 5812 5813 } 5814 5815 /** 5816 * CSS @import node 5817 * 5818 * The general strategy here is that we don't want to wait 5819 * for the parsing to be completed, before we start importing 5820 * the file. That's because in the context of a browser, 5821 * most of the time will be spent waiting for the server to respond. 5822 * 5823 * On creation, we push the import path to our import queue, though 5824 * `import,push`, we also pass it a callback, which it'll call once 5825 * the file has been fetched, and parsed. 5826 * 5827 * @package Less 5828 * @subpackage tree 5829 */ 5830 class Less_Tree_Import extends Less_Tree{ 5831 5832 public $options; 5833 public $index; 5834 public $path; 5835 public $features; 5836 public $currentFileInfo; 5837 public $css; 5838 public $skip; 5839 public $root; 5840 public $type = 'Import'; 5841 5842 public function __construct($path, $features, $options, $index, $currentFileInfo = null ){ 5843 $this->options = $options; 5844 $this->index = $index; 5845 $this->path = $path; 5846 $this->features = $features; 5847 $this->currentFileInfo = $currentFileInfo; 5848 5849 if( is_array($options) ){ 5850 $this->options += array('inline'=>false); 5851 5852 if( isset($this->options['less']) || $this->options['inline'] ){ 5853 $this->css = !isset($this->options['less']) || !$this->options['less'] || $this->options['inline']; 5854 } else { 5855 $pathValue = $this->getPath(); 5856 if( $pathValue && preg_match('/css([\?;].*)?$/',$pathValue) ){ 5857 $this->css = true; 5858 } 5859 } 5860 } 5861 } 5862 5863 // 5864 // The actual import node doesn't return anything, when converted to CSS. 5865 // The reason is that it's used at the evaluation stage, so that the rules 5866 // it imports can be treated like any other rules. 5867 // 5868 // In `eval`, we make sure all Import nodes get evaluated, recursively, so 5869 // we end up with a flat structure, which can easily be imported in the parent 5870 // ruleset. 5871 // 5872 5873 public function accept($visitor){ 5874 5875 if( $this->features ){ 5876 $this->features = $visitor->visitObj($this->features); 5877 } 5878 $this->path = $visitor->visitObj($this->path); 5879 5880 if( !$this->options['inline'] && $this->root ){ 5881 $this->root = $visitor->visit($this->root); 5882 } 5883 } 5884 5885 /** 5886 * @see Less_Tree::genCSS 5887 */ 5888 public function genCSS( $output ){ 5889 if( $this->css ){ 5890 5891 $output->add( '@import ', $this->currentFileInfo, $this->index ); 5892 5893 $this->path->genCSS( $output ); 5894 if( $this->features ){ 5895 $output->add( ' ' ); 5896 $this->features->genCSS( $output ); 5897 } 5898 $output->add( ';' ); 5899 } 5900 } 5901 5902 public function toCSS(){ 5903 $features = $this->features ? ' ' . $this->features->toCSS() : ''; 5904 5905 if ($this->css) { 5906 return "@import " . $this->path->toCSS() . $features . ";\n"; 5907 } else { 5908 return ""; 5909 } 5910 } 5911 5912 /** 5913 * @return string 5914 */ 5915 public function getPath(){ 5916 if ($this->path instanceof Less_Tree_Quoted) { 5917 $path = $this->path->value; 5918 $path = ( isset($this->css) || preg_match('/(\.[a-z]*$)|([\?;].*)$/',$path)) ? $path : $path . '.less'; 5919 } else if ($this->path instanceof Less_Tree_URL) { 5920 $path = $this->path->value->value; 5921 }else{ 5922 return null; 5923 } 5924 5925 //remove query string and fragment 5926 return preg_replace('/[\?#][^\?]*$/','',$path); 5927 } 5928 5929 public function compileForImport( $env ){ 5930 return new Less_Tree_Import( $this->path->compile($env), $this->features, $this->options, $this->index, $this->currentFileInfo); 5931 } 5932 5933 public function compilePath($env) { 5934 $path = $this->path->compile($env); 5935 $rootpath = ''; 5936 if( $this->currentFileInfo && $this->currentFileInfo['rootpath'] ){ 5937 $rootpath = $this->currentFileInfo['rootpath']; 5938 } 5939 5940 5941 if( !($path instanceof Less_Tree_URL) ){ 5942 if( $rootpath ){ 5943 $pathValue = $path->value; 5944 // Add the base path if the import is relative 5945 if( $pathValue && Less_Environment::isPathRelative($pathValue) ){ 5946 $path->value = $this->currentFileInfo['uri_root'].$pathValue; 5947 } 5948 } 5949 $path->value = Less_Environment::normalizePath($path->value); 5950 } 5951 5952 5953 5954 return $path; 5955 } 5956 5957 public function compile( $env ){ 5958 5959 $evald = $this->compileForImport($env); 5960 5961 //get path & uri 5962 $path_and_uri = null; 5963 if( is_callable(Less_Parser::$options['import_callback']) ){ 5964 $path_and_uri = call_user_func(Less_Parser::$options['import_callback'],$evald); 5965 } 5966 5967 if( !$path_and_uri ){ 5968 $path_and_uri = $evald->PathAndUri(); 5969 } 5970 5971 if( $path_and_uri ){ 5972 list($full_path, $uri) = $path_and_uri; 5973 }else{ 5974 $full_path = $uri = $evald->getPath(); 5975 } 5976 5977 5978 //import once 5979 if( $evald->skip( $full_path, $env) ){ 5980 return array(); 5981 } 5982 5983 if( $this->options['inline'] ){ 5984 //todo needs to reference css file not import 5985 //$contents = new Less_Tree_Anonymous($this->root, 0, array('filename'=>$this->importedFilename), true ); 5986 5987 Less_Parser::AddParsedFile($full_path); 5988 $contents = new Less_Tree_Anonymous( file_get_contents($full_path), 0, array(), true ); 5989 5990 if( $this->features ){ 5991 return new Less_Tree_Media( array($contents), $this->features->value ); 5992 } 5993 5994 return array( $contents ); 5995 } 5996 5997 // optional (need to be before "CSS" to support optional CSS imports. CSS should be checked only if empty($this->currentFileInfo)) 5998 if( isset($this->options['optional']) && $this->options['optional'] && !file_exists($full_path) && (!$evald->css || !empty($this->currentFileInfo))) { 5999 return array(); 6000 } 6001 6002 6003 // css ? 6004 if( $evald->css ){ 6005 $features = ( $evald->features ? $evald->features->compile($env) : null ); 6006 return new Less_Tree_Import( $this->compilePath( $env), $features, $this->options, $this->index); 6007 } 6008 6009 6010 return $this->ParseImport( $full_path, $uri, $env ); 6011 } 6012 6013 6014 /** 6015 * Using the import directories, get the full absolute path and uri of the import 6016 * 6017 * @param Less_Tree_Import $evald 6018 */ 6019 public function PathAndUri(){ 6020 6021 $evald_path = $this->getPath(); 6022 6023 if( $evald_path ){ 6024 6025 $import_dirs = array(); 6026 6027 if( Less_Environment::isPathRelative($evald_path) ){ 6028 //if the path is relative, the file should be in the current directory 6029 $import_dirs[ $this->currentFileInfo['currentDirectory'] ] = $this->currentFileInfo['uri_root']; 6030 6031 }else{ 6032 //otherwise, the file should be relative to the server root 6033 $import_dirs[ $this->currentFileInfo['entryPath'] ] = $this->currentFileInfo['entryUri']; 6034 6035 //if the user supplied entryPath isn't the actual root 6036 $import_dirs[ $_SERVER['DOCUMENT_ROOT'] ] = ''; 6037 6038 } 6039 6040 // always look in user supplied import directories 6041 $import_dirs = array_merge( $import_dirs, Less_Parser::$options['import_dirs'] ); 6042 6043 6044 foreach( $import_dirs as $rootpath => $rooturi){ 6045 if( is_callable($rooturi) ){ 6046 list($path, $uri) = call_user_func($rooturi, $evald_path); 6047 if( is_string($path) ){ 6048 $full_path = $path; 6049 return array( $full_path, $uri ); 6050 } 6051 }elseif( !empty($rootpath) ){ 6052 6053 6054 if( $rooturi ){ 6055 if( strpos($evald_path,$rooturi) === 0 ){ 6056 $evald_path = substr( $evald_path, strlen($rooturi) ); 6057 } 6058 } 6059 6060 $path = rtrim($rootpath,'/\\').'/'.ltrim($evald_path,'/\\'); 6061 6062 if( file_exists($path) ){ 6063 $full_path = Less_Environment::normalizePath($path); 6064 $uri = Less_Environment::normalizePath(dirname($rooturi.$evald_path)); 6065 return array( $full_path, $uri ); 6066 } elseif( file_exists($path.'.less') ){ 6067 $full_path = Less_Environment::normalizePath($path.'.less'); 6068 $uri = Less_Environment::normalizePath(dirname($rooturi.$evald_path.'.less')); 6069 return array( $full_path, $uri ); 6070 } 6071 } 6072 } 6073 } 6074 } 6075 6076 6077 /** 6078 * Parse the import url and return the rules 6079 * 6080 * @return Less_Tree_Media|array 6081 */ 6082 public function ParseImport( $full_path, $uri, $env ){ 6083 6084 $import_env = clone $env; 6085 if( (isset($this->options['reference']) && $this->options['reference']) || isset($this->currentFileInfo['reference']) ){ 6086 $import_env->currentFileInfo['reference'] = true; 6087 } 6088 6089 if( (isset($this->options['multiple']) && $this->options['multiple']) ){ 6090 $import_env->importMultiple = true; 6091 } 6092 6093 $parser = new Less_Parser($import_env); 6094 $root = $parser->parseFile($full_path, $uri, true); 6095 6096 6097 $ruleset = new Less_Tree_Ruleset(array(), $root->rules ); 6098 $ruleset->evalImports($import_env); 6099 6100 return $this->features ? new Less_Tree_Media($ruleset->rules, $this->features->value) : $ruleset->rules; 6101 } 6102 6103 6104 /** 6105 * Should the import be skipped? 6106 * 6107 * @return boolean|null 6108 */ 6109 private function Skip($path, $env){ 6110 6111 $path = Less_Parser::winPath(realpath($path)); 6112 6113 if( $path && Less_Parser::FileParsed($path) ){ 6114 6115 if( isset($this->currentFileInfo['reference']) ){ 6116 return true; 6117 } 6118 6119 return !isset($this->options['multiple']) && !$env->importMultiple; 6120 } 6121 6122 } 6123 } 6124 6125 6126 /** 6127 * Javascript 6128 * 6129 * @package Less 6130 * @subpackage tree 6131 */ 6132 class Less_Tree_Javascript extends Less_Tree{ 6133 6134 public $type = 'Javascript'; 6135 public $escaped; 6136 public $expression; 6137 public $index; 6138 6139 /** 6140 * @param boolean $index 6141 * @param boolean $escaped 6142 */ 6143 public function __construct($string, $index, $escaped){ 6144 $this->escaped = $escaped; 6145 $this->expression = $string; 6146 $this->index = $index; 6147 } 6148 6149 public function compile(){ 6150 return new Less_Tree_Anonymous('/* Sorry, can not do JavaScript evaluation in PHP... :( */'); 6151 } 6152 6153 } 6154 6155 6156 /** 6157 * Keyword 6158 * 6159 * @package Less 6160 * @subpackage tree 6161 */ 6162 class Less_Tree_Keyword extends Less_Tree{ 6163 6164 public $value; 6165 public $type = 'Keyword'; 6166 6167 /** 6168 * @param string $value 6169 */ 6170 public function __construct($value){ 6171 $this->value = $value; 6172 } 6173 6174 public function compile(){ 6175 return $this; 6176 } 6177 6178 /** 6179 * @see Less_Tree::genCSS 6180 */ 6181 public function genCSS( $output ){ 6182 6183 if( $this->value === '%') { 6184 throw new Less_Exception_Compiler("Invalid % without number"); 6185 } 6186 6187 $output->add( $this->value ); 6188 } 6189 6190 public function compare($other) { 6191 if ($other instanceof Less_Tree_Keyword) { 6192 return $other->value === $this->value ? 0 : 1; 6193 } else { 6194 return -1; 6195 } 6196 } 6197 } 6198 6199 6200 /** 6201 * Media 6202 * 6203 * @package Less 6204 * @subpackage tree 6205 */ 6206 class Less_Tree_Media extends Less_Tree{ 6207 6208 public $features; 6209 public $rules; 6210 public $index; 6211 public $currentFileInfo; 6212 public $isReferenced; 6213 public $type = 'Media'; 6214 6215 public function __construct($value = array(), $features = array(), $index = null, $currentFileInfo = null ){ 6216 6217 $this->index = $index; 6218 $this->currentFileInfo = $currentFileInfo; 6219 6220 $selectors = $this->emptySelectors(); 6221 6222 $this->features = new Less_Tree_Value($features); 6223 6224 $this->rules = array(new Less_Tree_Ruleset($selectors, $value)); 6225 $this->rules[0]->allowImports = true; 6226 } 6227 6228 public function accept( $visitor ){ 6229 $this->features = $visitor->visitObj($this->features); 6230 $this->rules = $visitor->visitArray($this->rules); 6231 } 6232 6233 /** 6234 * @see Less_Tree::genCSS 6235 */ 6236 public function genCSS( $output ){ 6237 6238 $output->add( '@media ', $this->currentFileInfo, $this->index ); 6239 $this->features->genCSS( $output ); 6240 Less_Tree::outputRuleset( $output, $this->rules); 6241 6242 } 6243 6244 public function compile($env) { 6245 6246 $media = new Less_Tree_Media(array(), array(), $this->index, $this->currentFileInfo ); 6247 6248 $strictMathBypass = false; 6249 if( Less_Parser::$options['strictMath'] === false) { 6250 $strictMathBypass = true; 6251 Less_Parser::$options['strictMath'] = true; 6252 } 6253 6254 $media->features = $this->features->compile($env); 6255 6256 if( $strictMathBypass ){ 6257 Less_Parser::$options['strictMath'] = false; 6258 } 6259 6260 $env->mediaPath[] = $media; 6261 $env->mediaBlocks[] = $media; 6262 6263 array_unshift($env->frames, $this->rules[0]); 6264 $media->rules = array($this->rules[0]->compile($env)); 6265 array_shift($env->frames); 6266 6267 array_pop($env->mediaPath); 6268 6269 return !$env->mediaPath ? $media->compileTop($env) : $media->compileNested($env); 6270 } 6271 6272 public function variable($name) { 6273 return $this->rules[0]->variable($name); 6274 } 6275 6276 public function find($selector) { 6277 return $this->rules[0]->find($selector, $this); 6278 } 6279 6280 public function emptySelectors(){ 6281 $el = new Less_Tree_Element('','&', $this->index, $this->currentFileInfo ); 6282 $sels = array( new Less_Tree_Selector(array($el), array(), null, $this->index, $this->currentFileInfo) ); 6283 $sels[0]->mediaEmpty = true; 6284 return $sels; 6285 } 6286 6287 public function markReferenced(){ 6288 $this->rules[0]->markReferenced(); 6289 $this->isReferenced = true; 6290 Less_Tree::ReferencedArray($this->rules[0]->rules); 6291 } 6292 6293 // evaltop 6294 public function compileTop($env) { 6295 $result = $this; 6296 6297 if (count($env->mediaBlocks) > 1) { 6298 $selectors = $this->emptySelectors(); 6299 $result = new Less_Tree_Ruleset($selectors, $env->mediaBlocks); 6300 $result->multiMedia = true; 6301 } 6302 6303 $env->mediaBlocks = array(); 6304 $env->mediaPath = array(); 6305 6306 return $result; 6307 } 6308 6309 public function compileNested($env) { 6310 $path = array_merge($env->mediaPath, array($this)); 6311 6312 // Extract the media-query conditions separated with `,` (OR). 6313 foreach ($path as $key => $p) { 6314 $value = $p->features instanceof Less_Tree_Value ? $p->features->value : $p->features; 6315 $path[$key] = is_array($value) ? $value : array($value); 6316 } 6317 6318 // Trace all permutations to generate the resulting media-query. 6319 // 6320 // (a, b and c) with nested (d, e) -> 6321 // a and d 6322 // a and e 6323 // b and c and d 6324 // b and c and e 6325 6326 $permuted = $this->permute($path); 6327 $expressions = array(); 6328 foreach($permuted as $path){ 6329 6330 for( $i=0, $len=count($path); $i < $len; $i++){ 6331 $path[$i] = Less_Parser::is_method($path[$i], 'toCSS') ? $path[$i] : new Less_Tree_Anonymous($path[$i]); 6332 } 6333 6334 for( $i = count($path) - 1; $i > 0; $i-- ){ 6335 array_splice($path, $i, 0, array(new Less_Tree_Anonymous('and'))); 6336 } 6337 6338 $expressions[] = new Less_Tree_Expression($path); 6339 } 6340 $this->features = new Less_Tree_Value($expressions); 6341 6342 6343 6344 // Fake a tree-node that doesn't output anything. 6345 return new Less_Tree_Ruleset(array(), array()); 6346 } 6347 6348 public function permute($arr) { 6349 if (!$arr) 6350 return array(); 6351 6352 if (count($arr) == 1) 6353 return $arr[0]; 6354 6355 $result = array(); 6356 $rest = $this->permute(array_slice($arr, 1)); 6357 foreach ($rest as $r) { 6358 foreach ($arr[0] as $a) { 6359 $result[] = array_merge( 6360 is_array($a) ? $a : array($a), 6361 is_array($r) ? $r : array($r) 6362 ); 6363 } 6364 } 6365 6366 return $result; 6367 } 6368 6369 public function bubbleSelectors($selectors) { 6370 6371 if( !$selectors) return; 6372 6373 $this->rules = array(new Less_Tree_Ruleset( $selectors, array($this->rules[0]))); 6374 } 6375 6376 } 6377 6378 6379 /** 6380 * A simple css name-value pair 6381 * ex: width:100px; 6382 * 6383 * In bootstrap, there are about 600-1,000 simple name-value pairs (depending on how forgiving the match is) -vs- 6,020 dynamic rules (Less_Tree_Rule) 6384 * Using the name-value object can speed up bootstrap compilation slightly, but it breaks color keyword interpretation: color:red -> color:#FF0000; 6385 * 6386 * @package Less 6387 * @subpackage tree 6388 */ 6389 class Less_Tree_NameValue extends Less_Tree{ 6390 6391 public $name; 6392 public $value; 6393 public $index; 6394 public $currentFileInfo; 6395 public $type = 'NameValue'; 6396 public $important = ''; 6397 6398 public function __construct($name, $value = null, $index = null, $currentFileInfo = null ){ 6399 $this->name = $name; 6400 $this->value = $value; 6401 $this->index = $index; 6402 $this->currentFileInfo = $currentFileInfo; 6403 } 6404 6405 public function genCSS( $output ){ 6406 6407 $output->add( 6408 $this->name 6409 . Less_Environment::$_outputMap[': '] 6410 . $this->value 6411 . $this->important 6412 . (((Less_Environment::$lastRule && Less_Parser::$options['compress'])) ? "" : ";") 6413 , $this->currentFileInfo, $this->index); 6414 } 6415 6416 public function compile ($env){ 6417 return $this; 6418 } 6419 6420 public function makeImportant(){ 6421 $new = new Less_Tree_NameValue($this->name, $this->value, $this->index, $this->currentFileInfo); 6422 $new->important = ' !important'; 6423 return $new; 6424 } 6425 6426 6427 } 6428 6429 6430 /** 6431 * Negative 6432 * 6433 * @package Less 6434 * @subpackage tree 6435 */ 6436 class Less_Tree_Negative extends Less_Tree{ 6437 6438 public $value; 6439 public $type = 'Negative'; 6440 6441 public function __construct($node){ 6442 $this->value = $node; 6443 } 6444 6445 //function accept($visitor) { 6446 // $this->value = $visitor->visit($this->value); 6447 //} 6448 6449 /** 6450 * @see Less_Tree::genCSS 6451 */ 6452 public function genCSS( $output ){ 6453 $output->add( '-' ); 6454 $this->value->genCSS( $output ); 6455 } 6456 6457 public function compile($env) { 6458 if( Less_Environment::isMathOn() ){ 6459 $ret = new Less_Tree_Operation('*', array( new Less_Tree_Dimension(-1), $this->value ) ); 6460 return $ret->compile($env); 6461 } 6462 return new Less_Tree_Negative( $this->value->compile($env) ); 6463 } 6464 } 6465 6466 /** 6467 * Operation 6468 * 6469 * @package Less 6470 * @subpackage tree 6471 */ 6472 class Less_Tree_Operation extends Less_Tree{ 6473 6474 public $op; 6475 public $operands; 6476 public $isSpaced; 6477 public $type = 'Operation'; 6478 6479 /** 6480 * @param string $op 6481 */ 6482 public function __construct($op, $operands, $isSpaced = false){ 6483 $this->op = trim($op); 6484 $this->operands = $operands; 6485 $this->isSpaced = $isSpaced; 6486 } 6487 6488 public function accept($visitor) { 6489 $this->operands = $visitor->visitArray($this->operands); 6490 } 6491 6492 public function compile($env){ 6493 $a = $this->operands[0]->compile($env); 6494 $b = $this->operands[1]->compile($env); 6495 6496 6497 if( Less_Environment::isMathOn() ){ 6498 6499 if( $a instanceof Less_Tree_Dimension && $b instanceof Less_Tree_Color ){ 6500 $a = $a->toColor(); 6501 6502 }elseif( $b instanceof Less_Tree_Dimension && $a instanceof Less_Tree_Color ){ 6503 $b = $b->toColor(); 6504 6505 } 6506 6507 if( !method_exists($a,'operate') ){ 6508 throw new Less_Exception_Compiler("Operation on an invalid type"); 6509 } 6510 6511 return $a->operate( $this->op, $b); 6512 } 6513 6514 return new Less_Tree_Operation($this->op, array($a, $b), $this->isSpaced ); 6515 } 6516 6517 6518 /** 6519 * @see Less_Tree::genCSS 6520 */ 6521 public function genCSS( $output ){ 6522 $this->operands[0]->genCSS( $output ); 6523 if( $this->isSpaced ){ 6524 $output->add( " " ); 6525 } 6526 $output->add( $this->op ); 6527 if( $this->isSpaced ){ 6528 $output->add( ' ' ); 6529 } 6530 $this->operands[1]->genCSS( $output ); 6531 } 6532 6533 } 6534 6535 6536 /** 6537 * Paren 6538 * 6539 * @package Less 6540 * @subpackage tree 6541 */ 6542 class Less_Tree_Paren extends Less_Tree{ 6543 6544 public $value; 6545 public $type = 'Paren'; 6546 6547 public function __construct($value) { 6548 $this->value = $value; 6549 } 6550 6551 public function accept($visitor){ 6552 $this->value = $visitor->visitObj($this->value); 6553 } 6554 6555 /** 6556 * @see Less_Tree::genCSS 6557 */ 6558 public function genCSS( $output ){ 6559 $output->add( '(' ); 6560 $this->value->genCSS( $output ); 6561 $output->add( ')' ); 6562 } 6563 6564 public function compile($env) { 6565 return new Less_Tree_Paren($this->value->compile($env)); 6566 } 6567 6568 } 6569 6570 6571 /** 6572 * Quoted 6573 * 6574 * @package Less 6575 * @subpackage tree 6576 */ 6577 class Less_Tree_Quoted extends Less_Tree{ 6578 public $escaped; 6579 public $value; 6580 public $quote; 6581 public $index; 6582 public $currentFileInfo; 6583 public $type = 'Quoted'; 6584 6585 /** 6586 * @param string $str 6587 */ 6588 public function __construct($str, $content = '', $escaped = false, $index = false, $currentFileInfo = null ){ 6589 $this->escaped = $escaped; 6590 $this->value = $content; 6591 if( $str ){ 6592 $this->quote = $str[0]; 6593 } 6594 $this->index = $index; 6595 $this->currentFileInfo = $currentFileInfo; 6596 } 6597 6598 /** 6599 * @see Less_Tree::genCSS 6600 */ 6601 public function genCSS( $output ){ 6602 if( !$this->escaped ){ 6603 $output->add( $this->quote, $this->currentFileInfo, $this->index ); 6604 } 6605 $output->add( $this->value ); 6606 if( !$this->escaped ){ 6607 $output->add( $this->quote ); 6608 } 6609 } 6610 6611 public function compile($env){ 6612 6613 $value = $this->value; 6614 if( preg_match_all('/`([^`]+)`/', $this->value, $matches) ){ 6615 foreach($matches as $i => $match){ 6616 $js = new Less_Tree_JavaScript($matches[1], $this->index, true); 6617 $js = $js->compile()->value; 6618 $value = str_replace($matches[0][$i], $js, $value); 6619 } 6620 } 6621 6622 if( preg_match_all('/@\{([\w-]+)\}/',$value,$matches) ){ 6623 foreach($matches[1] as $i => $match){ 6624 $v = new Less_Tree_Variable('@' . $match, $this->index, $this->currentFileInfo ); 6625 $v = $v->compile($env); 6626 $v = ($v instanceof Less_Tree_Quoted) ? $v->value : $v->toCSS(); 6627 $value = str_replace($matches[0][$i], $v, $value); 6628 } 6629 } 6630 6631 return new Less_Tree_Quoted($this->quote . $value . $this->quote, $value, $this->escaped, $this->index, $this->currentFileInfo); 6632 } 6633 6634 public function compare($x) { 6635 6636 if( !Less_Parser::is_method($x, 'toCSS') ){ 6637 return -1; 6638 } 6639 6640 $left = $this->toCSS(); 6641 $right = $x->toCSS(); 6642 6643 if ($left === $right) { 6644 return 0; 6645 } 6646 6647 return $left < $right ? -1 : 1; 6648 } 6649 } 6650 6651 6652 /** 6653 * Rule 6654 * 6655 * @package Less 6656 * @subpackage tree 6657 */ 6658 class Less_Tree_Rule extends Less_Tree{ 6659 6660 public $name; 6661 public $value; 6662 public $important; 6663 public $merge; 6664 public $index; 6665 public $inline; 6666 public $variable; 6667 public $currentFileInfo; 6668 public $type = 'Rule'; 6669 6670 /** 6671 * @param string $important 6672 */ 6673 public function __construct($name, $value = null, $important = null, $merge = null, $index = null, $currentFileInfo = null, $inline = false){ 6674 $this->name = $name; 6675 $this->value = ($value instanceof Less_Tree_Value || $value instanceof Less_Tree_Ruleset) ? $value : new Less_Tree_Value(array($value)); 6676 $this->important = $important ? ' ' . trim($important) : ''; 6677 $this->merge = $merge; 6678 $this->index = $index; 6679 $this->currentFileInfo = $currentFileInfo; 6680 $this->inline = $inline; 6681 $this->variable = ( is_string($name) && $name[0] === '@'); 6682 } 6683 6684 public function accept($visitor) { 6685 $this->value = $visitor->visitObj( $this->value ); 6686 } 6687 6688 /** 6689 * @see Less_Tree::genCSS 6690 */ 6691 public function genCSS( $output ){ 6692 6693 $output->add( $this->name . Less_Environment::$_outputMap[': '], $this->currentFileInfo, $this->index); 6694 try{ 6695 $this->value->genCSS( $output); 6696 6697 }catch( Less_Exception_Parser $e ){ 6698 $e->index = $this->index; 6699 $e->currentFile = $this->currentFileInfo; 6700 throw $e; 6701 } 6702 $output->add( $this->important . (($this->inline || (Less_Environment::$lastRule && Less_Parser::$options['compress'])) ? "" : ";"), $this->currentFileInfo, $this->index); 6703 } 6704 6705 public function compile ($env){ 6706 6707 $name = $this->name; 6708 if( is_array($name) ){ 6709 // expand 'primitive' name directly to get 6710 // things faster (~10% for benchmark.less): 6711 if( count($name) === 1 && $name[0] instanceof Less_Tree_Keyword ){ 6712 $name = $name[0]->value; 6713 }else{ 6714 $name = $this->CompileName($env,$name); 6715 } 6716 } 6717 6718 $strictMathBypass = Less_Parser::$options['strictMath']; 6719 if( $name === "font" && !Less_Parser::$options['strictMath'] ){ 6720 Less_Parser::$options['strictMath'] = true; 6721 } 6722 6723 try { 6724 $evaldValue = $this->value->compile($env); 6725 6726 if( !$this->variable && $evaldValue->type === "DetachedRuleset") { 6727 throw new Less_Exception_Compiler("Rulesets cannot be evaluated on a property.", null, $this->index, $this->currentFileInfo); 6728 } 6729 6730 if( Less_Environment::$mixin_stack ){ 6731 $return = new Less_Tree_Rule($name, $evaldValue, $this->important, $this->merge, $this->index, $this->currentFileInfo, $this->inline); 6732 }else{ 6733 $this->name = $name; 6734 $this->value = $evaldValue; 6735 $return = $this; 6736 } 6737 6738 }catch( Less_Exception_Parser $e ){ 6739 if( !is_numeric($e->index) ){ 6740 $e->index = $this->index; 6741 $e->currentFile = $this->currentFileInfo; 6742 } 6743 throw $e; 6744 } 6745 6746 Less_Parser::$options['strictMath'] = $strictMathBypass; 6747 6748 return $return; 6749 } 6750 6751 6752 public function CompileName( $env, $name ){ 6753 $output = new Less_Output(); 6754 foreach($name as $n){ 6755 $n->compile($env)->genCSS($output); 6756 } 6757 return $output->toString(); 6758 } 6759 6760 public function makeImportant(){ 6761 return new Less_Tree_Rule($this->name, $this->value, '!important', $this->merge, $this->index, $this->currentFileInfo, $this->inline); 6762 } 6763 6764 } 6765 6766 6767 /** 6768 * Ruleset 6769 * 6770 * @package Less 6771 * @subpackage tree 6772 */ 6773 class Less_Tree_Ruleset extends Less_Tree{ 6774 6775 protected $lookups; 6776 public $_variables; 6777 public $_rulesets; 6778 6779 public $strictImports; 6780 6781 public $selectors; 6782 public $rules; 6783 public $root; 6784 public $allowImports; 6785 public $paths; 6786 public $firstRoot; 6787 public $type = 'Ruleset'; 6788 public $multiMedia; 6789 public $allExtends; 6790 6791 public $ruleset_id; 6792 public $originalRuleset; 6793 6794 public $first_oelements; 6795 6796 public function SetRulesetIndex(){ 6797 $this->ruleset_id = Less_Parser::$next_id++; 6798 $this->originalRuleset = $this->ruleset_id; 6799 6800 if( $this->selectors ){ 6801 foreach($this->selectors as $sel){ 6802 if( $sel->_oelements ){ 6803 $this->first_oelements[$sel->_oelements[0]] = true; 6804 } 6805 } 6806 } 6807 } 6808 6809 public function __construct($selectors, $rules, $strictImports = null){ 6810 $this->selectors = $selectors; 6811 $this->rules = $rules; 6812 $this->lookups = array(); 6813 $this->strictImports = $strictImports; 6814 $this->SetRulesetIndex(); 6815 } 6816 6817 public function accept( $visitor ){ 6818 if( $this->paths ){ 6819 $paths_len = count($this->paths); 6820 for($i = 0,$paths_len; $i < $paths_len; $i++ ){ 6821 $this->paths[$i] = $visitor->visitArray($this->paths[$i]); 6822 } 6823 }elseif( $this->selectors ){ 6824 $this->selectors = $visitor->visitArray($this->selectors); 6825 } 6826 6827 if( $this->rules ){ 6828 $this->rules = $visitor->visitArray($this->rules); 6829 } 6830 } 6831 6832 public function compile($env){ 6833 6834 $ruleset = $this->PrepareRuleset($env); 6835 6836 6837 // Store the frames around mixin definitions, 6838 // so they can be evaluated like closures when the time comes. 6839 $rsRuleCnt = count($ruleset->rules); 6840 for( $i = 0; $i < $rsRuleCnt; $i++ ){ 6841 if( $ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset ){ 6842 $ruleset->rules[$i] = $ruleset->rules[$i]->compile($env); 6843 } 6844 } 6845 6846 $mediaBlockCount = 0; 6847 if( $env instanceof Less_Environment ){ 6848 $mediaBlockCount = count($env->mediaBlocks); 6849 } 6850 6851 // Evaluate mixin calls. 6852 $this->EvalMixinCalls( $ruleset, $env, $rsRuleCnt ); 6853 6854 6855 // Evaluate everything else 6856 for( $i=0; $i<$rsRuleCnt; $i++ ){ 6857 if(! ($ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset) ){ 6858 $ruleset->rules[$i] = $ruleset->rules[$i]->compile($env); 6859 } 6860 } 6861 6862 // Evaluate everything else 6863 for( $i=0; $i<$rsRuleCnt; $i++ ){ 6864 $rule = $ruleset->rules[$i]; 6865 6866 // for rulesets, check if it is a css guard and can be removed 6867 if( $rule instanceof Less_Tree_Ruleset && $rule->selectors && count($rule->selectors) === 1 ){ 6868 6869 // check if it can be folded in (e.g. & where) 6870 if( $rule->selectors[0]->isJustParentSelector() ){ 6871 array_splice($ruleset->rules,$i--,1); 6872 $rsRuleCnt--; 6873 6874 for($j = 0; $j < count($rule->rules); $j++ ){ 6875 $subRule = $rule->rules[$j]; 6876 if( !($subRule instanceof Less_Tree_Rule) || !$subRule->variable ){ 6877 array_splice($ruleset->rules, ++$i, 0, array($subRule)); 6878 $rsRuleCnt++; 6879 } 6880 } 6881 6882 } 6883 } 6884 } 6885 6886 6887 // Pop the stack 6888 $env->shiftFrame(); 6889 6890 if ($mediaBlockCount) { 6891 $len = count($env->mediaBlocks); 6892 for($i = $mediaBlockCount; $i < $len; $i++ ){ 6893 $env->mediaBlocks[$i]->bubbleSelectors($ruleset->selectors); 6894 } 6895 } 6896 6897 return $ruleset; 6898 } 6899 6900 /** 6901 * Compile Less_Tree_Mixin_Call objects 6902 * 6903 * @param Less_Tree_Ruleset $ruleset 6904 * @param integer $rsRuleCnt 6905 */ 6906 private function EvalMixinCalls( $ruleset, $env, &$rsRuleCnt ){ 6907 for($i=0; $i < $rsRuleCnt; $i++){ 6908 $rule = $ruleset->rules[$i]; 6909 6910 if( $rule instanceof Less_Tree_Mixin_Call ){ 6911 $rule = $rule->compile($env); 6912 6913 $temp = array(); 6914 foreach($rule as $r){ 6915 if( ($r instanceof Less_Tree_Rule) && $r->variable ){ 6916 // do not pollute the scope if the variable is 6917 // already there. consider returning false here 6918 // but we need a way to "return" variable from mixins 6919 if( !$ruleset->variable($r->name) ){ 6920 $temp[] = $r; 6921 } 6922 }else{ 6923 $temp[] = $r; 6924 } 6925 } 6926 $temp_count = count($temp)-1; 6927 array_splice($ruleset->rules, $i, 1, $temp); 6928 $rsRuleCnt += $temp_count; 6929 $i += $temp_count; 6930 $ruleset->resetCache(); 6931 6932 }elseif( $rule instanceof Less_Tree_RulesetCall ){ 6933 6934 $rule = $rule->compile($env); 6935 $rules = array(); 6936 foreach($rule->rules as $r){ 6937 if( ($r instanceof Less_Tree_Rule) && $r->variable ){ 6938 continue; 6939 } 6940 $rules[] = $r; 6941 } 6942 6943 array_splice($ruleset->rules, $i, 1, $rules); 6944 $temp_count = count($rules); 6945 $rsRuleCnt += $temp_count - 1; 6946 $i += $temp_count-1; 6947 $ruleset->resetCache(); 6948 } 6949 6950 } 6951 } 6952 6953 6954 /** 6955 * Compile the selectors and create a new ruleset object for the compile() method 6956 * 6957 */ 6958 private function PrepareRuleset($env){ 6959 6960 $hasOnePassingSelector = false; 6961 $selectors = array(); 6962 if( $this->selectors ){ 6963 Less_Tree_DefaultFunc::error("it is currently only allowed in parametric mixin guards,"); 6964 6965 foreach($this->selectors as $s){ 6966 $selector = $s->compile($env); 6967 $selectors[] = $selector; 6968 if( $selector->evaldCondition ){ 6969 $hasOnePassingSelector = true; 6970 } 6971 } 6972 6973 Less_Tree_DefaultFunc::reset(); 6974 } else { 6975 $hasOnePassingSelector = true; 6976 } 6977 6978 if( $this->rules && $hasOnePassingSelector ){ 6979 $rules = $this->rules; 6980 }else{ 6981 $rules = array(); 6982 } 6983 6984 $ruleset = new Less_Tree_Ruleset($selectors, $rules, $this->strictImports); 6985 6986 $ruleset->originalRuleset = $this->ruleset_id; 6987 6988 $ruleset->root = $this->root; 6989 $ruleset->firstRoot = $this->firstRoot; 6990 $ruleset->allowImports = $this->allowImports; 6991 6992 6993 // push the current ruleset to the frames stack 6994 $env->unshiftFrame($ruleset); 6995 6996 6997 // Evaluate imports 6998 if( $ruleset->root || $ruleset->allowImports || !$ruleset->strictImports ){ 6999 $ruleset->evalImports($env); 7000 } 7001 7002 return $ruleset; 7003 } 7004 7005 function evalImports($env) { 7006 7007 $rules_len = count($this->rules); 7008 for($i=0; $i < $rules_len; $i++){ 7009 $rule = $this->rules[$i]; 7010 7011 if( $rule instanceof Less_Tree_Import ){ 7012 $rules = $rule->compile($env); 7013 if( is_array($rules) ){ 7014 array_splice($this->rules, $i, 1, $rules); 7015 $temp_count = count($rules)-1; 7016 $i += $temp_count; 7017 $rules_len += $temp_count; 7018 }else{ 7019 array_splice($this->rules, $i, 1, array($rules)); 7020 } 7021 7022 $this->resetCache(); 7023 } 7024 } 7025 } 7026 7027 function makeImportant(){ 7028 7029 $important_rules = array(); 7030 foreach($this->rules as $rule){ 7031 if( $rule instanceof Less_Tree_Rule || $rule instanceof Less_Tree_Ruleset || $rule instanceof Less_Tree_NameValue ){ 7032 $important_rules[] = $rule->makeImportant(); 7033 }else{ 7034 $important_rules[] = $rule; 7035 } 7036 } 7037 7038 return new Less_Tree_Ruleset($this->selectors, $important_rules, $this->strictImports ); 7039 } 7040 7041 public function matchArgs($args){ 7042 return !$args; 7043 } 7044 7045 // lets you call a css selector with a guard 7046 public function matchCondition( $args, $env ){ 7047 $lastSelector = end($this->selectors); 7048 7049 if( !$lastSelector->evaldCondition ){ 7050 return false; 7051 } 7052 if( $lastSelector->condition && !$lastSelector->condition->compile( $env->copyEvalEnv( $env->frames ) ) ){ 7053 return false; 7054 } 7055 return true; 7056 } 7057 7058 function resetCache(){ 7059 $this->_rulesets = null; 7060 $this->_variables = null; 7061 $this->lookups = array(); 7062 } 7063 7064 public function variables(){ 7065 $this->_variables = array(); 7066 foreach( $this->rules as $r){ 7067 if ($r instanceof Less_Tree_Rule && $r->variable === true) { 7068 $this->_variables[$r->name] = $r; 7069 } 7070 } 7071 } 7072 7073 public function variable($name){ 7074 7075 if( is_null($this->_variables) ){ 7076 $this->variables(); 7077 } 7078 return isset($this->_variables[$name]) ? $this->_variables[$name] : null; 7079 } 7080 7081 public function find( $selector, $self = null ){ 7082 7083 $key = implode(' ',$selector->_oelements); 7084 7085 if( !isset($this->lookups[$key]) ){ 7086 7087 if( !$self ){ 7088 $self = $this->ruleset_id; 7089 } 7090 7091 $this->lookups[$key] = array(); 7092 7093 $first_oelement = $selector->_oelements[0]; 7094 7095 foreach($this->rules as $rule){ 7096 if( $rule instanceof Less_Tree_Ruleset && $rule->ruleset_id != $self ){ 7097 7098 if( isset($rule->first_oelements[$first_oelement]) ){ 7099 7100 foreach( $rule->selectors as $ruleSelector ){ 7101 $match = $selector->match($ruleSelector); 7102 if( $match ){ 7103 if( $selector->elements_len > $match ){ 7104 $this->lookups[$key] = array_merge($this->lookups[$key], $rule->find( new Less_Tree_Selector(array_slice($selector->elements, $match)), $self)); 7105 } else { 7106 $this->lookups[$key][] = $rule; 7107 } 7108 break; 7109 } 7110 } 7111 } 7112 } 7113 } 7114 } 7115 7116 return $this->lookups[$key]; 7117 } 7118 7119 7120 /** 7121 * @see Less_Tree::genCSS 7122 */ 7123 public function genCSS( $output ){ 7124 7125 if( !$this->root ){ 7126 Less_Environment::$tabLevel++; 7127 } 7128 7129 $tabRuleStr = $tabSetStr = ''; 7130 if( !Less_Parser::$options['compress'] ){ 7131 if( Less_Environment::$tabLevel ){ 7132 $tabRuleStr = "\n".str_repeat( Less_Parser::$options['indentation'] , Less_Environment::$tabLevel ); 7133 $tabSetStr = "\n".str_repeat( Less_Parser::$options['indentation'] , Less_Environment::$tabLevel-1 ); 7134 }else{ 7135 $tabSetStr = $tabRuleStr = "\n"; 7136 } 7137 } 7138 7139 7140 $ruleNodes = array(); 7141 $rulesetNodes = array(); 7142 foreach($this->rules as $rule){ 7143 7144 $class = get_class($rule); 7145 if( ($class === 'Less_Tree_Media') || ($class === 'Less_Tree_Directive') || ($this->root && $class === 'Less_Tree_Comment') || ($class === 'Less_Tree_Ruleset' && $rule->rules) ){ 7146 $rulesetNodes[] = $rule; 7147 }else{ 7148 $ruleNodes[] = $rule; 7149 } 7150 } 7151 7152 // If this is the root node, we don't render 7153 // a selector, or {}. 7154 if( !$this->root ){ 7155 7156 /* 7157 debugInfo = tree.debugInfo(env, this, tabSetStr); 7158 7159 if (debugInfo) { 7160 output.add(debugInfo); 7161 output.add(tabSetStr); 7162 } 7163 */ 7164 7165 $paths_len = count($this->paths); 7166 for( $i = 0; $i < $paths_len; $i++ ){ 7167 $path = $this->paths[$i]; 7168 $firstSelector = true; 7169 7170 foreach($path as $p){ 7171 $p->genCSS( $output, $firstSelector ); 7172 $firstSelector = false; 7173 } 7174 7175 if( $i + 1 < $paths_len ){ 7176 $output->add( ',' . $tabSetStr ); 7177 } 7178 } 7179 7180 $output->add( (Less_Parser::$options['compress'] ? '{' : " {") . $tabRuleStr ); 7181 } 7182 7183 // Compile rules and rulesets 7184 $ruleNodes_len = count($ruleNodes); 7185 $rulesetNodes_len = count($rulesetNodes); 7186 for( $i = 0; $i < $ruleNodes_len; $i++ ){ 7187 $rule = $ruleNodes[$i]; 7188 7189 // @page{ directive ends up with root elements inside it, a mix of rules and rulesets 7190 // In this instance we do not know whether it is the last property 7191 if( $i + 1 === $ruleNodes_len && (!$this->root || $rulesetNodes_len === 0 || $this->firstRoot ) ){ 7192 Less_Environment::$lastRule = true; 7193 } 7194 7195 $rule->genCSS( $output ); 7196 7197 if( !Less_Environment::$lastRule ){ 7198 $output->add( $tabRuleStr ); 7199 }else{ 7200 Less_Environment::$lastRule = false; 7201 } 7202 } 7203 7204 if( !$this->root ){ 7205 $output->add( $tabSetStr . '}' ); 7206 Less_Environment::$tabLevel--; 7207 } 7208 7209 $firstRuleset = true; 7210 $space = ($this->root ? $tabRuleStr : $tabSetStr); 7211 for( $i = 0; $i < $rulesetNodes_len; $i++ ){ 7212 7213 if( $ruleNodes_len && $firstRuleset ){ 7214 $output->add( $space ); 7215 }elseif( !$firstRuleset ){ 7216 $output->add( $space ); 7217 } 7218 $firstRuleset = false; 7219 $rulesetNodes[$i]->genCSS( $output); 7220 } 7221 7222 if( !Less_Parser::$options['compress'] && $this->firstRoot ){ 7223 $output->add( "\n" ); 7224 } 7225 7226 } 7227 7228 7229 function markReferenced(){ 7230 if( !$this->selectors ){ 7231 return; 7232 } 7233 foreach($this->selectors as $selector){ 7234 $selector->markReferenced(); 7235 } 7236 } 7237 7238 public function joinSelectors( $context, $selectors ){ 7239 $paths = array(); 7240 if( is_array($selectors) ){ 7241 foreach($selectors as $selector) { 7242 $this->joinSelector( $paths, $context, $selector); 7243 } 7244 } 7245 return $paths; 7246 } 7247 7248 public function joinSelector( &$paths, $context, $selector){ 7249 7250 $hasParentSelector = false; 7251 7252 foreach($selector->elements as $el) { 7253 if( $el->value === '&') { 7254 $hasParentSelector = true; 7255 } 7256 } 7257 7258 if( !$hasParentSelector ){ 7259 if( $context ){ 7260 foreach($context as $context_el){ 7261 $paths[] = array_merge($context_el, array($selector) ); 7262 } 7263 }else { 7264 $paths[] = array($selector); 7265 } 7266 return; 7267 } 7268 7269 7270 // The paths are [[Selector]] 7271 // The first list is a list of comma seperated selectors 7272 // The inner list is a list of inheritance seperated selectors 7273 // e.g. 7274 // .a, .b { 7275 // .c { 7276 // } 7277 // } 7278 // == [[.a] [.c]] [[.b] [.c]] 7279 // 7280 7281 // the elements from the current selector so far 7282 $currentElements = array(); 7283 // the current list of new selectors to add to the path. 7284 // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors 7285 // by the parents 7286 $newSelectors = array(array()); 7287 7288 7289 foreach( $selector->elements as $el){ 7290 7291 // non parent reference elements just get added 7292 if( $el->value !== '&' ){ 7293 $currentElements[] = $el; 7294 } else { 7295 // the new list of selectors to add 7296 $selectorsMultiplied = array(); 7297 7298 // merge the current list of non parent selector elements 7299 // on to the current list of selectors to add 7300 if( $currentElements ){ 7301 $this->mergeElementsOnToSelectors( $currentElements, $newSelectors); 7302 } 7303 7304 // loop through our current selectors 7305 foreach($newSelectors as $sel){ 7306 7307 // if we don't have any parent paths, the & might be in a mixin so that it can be used 7308 // whether there are parents or not 7309 if( !$context ){ 7310 // the combinator used on el should now be applied to the next element instead so that 7311 // it is not lost 7312 if( $sel ){ 7313 $sel[0]->elements = array_slice($sel[0]->elements,0); 7314 $sel[0]->elements[] = new Less_Tree_Element($el->combinator, '', $el->index, $el->currentFileInfo ); 7315 } 7316 $selectorsMultiplied[] = $sel; 7317 }else { 7318 7319 // and the parent selectors 7320 foreach($context as $parentSel){ 7321 // We need to put the current selectors 7322 // then join the last selector's elements on to the parents selectors 7323 7324 // our new selector path 7325 $newSelectorPath = array(); 7326 // selectors from the parent after the join 7327 $afterParentJoin = array(); 7328 $newJoinedSelectorEmpty = true; 7329 7330 //construct the joined selector - if & is the first thing this will be empty, 7331 // if not newJoinedSelector will be the last set of elements in the selector 7332 if( $sel ){ 7333 $newSelectorPath = $sel; 7334 $lastSelector = array_pop($newSelectorPath); 7335 $newJoinedSelector = $selector->createDerived( array_slice($lastSelector->elements,0) ); 7336 $newJoinedSelectorEmpty = false; 7337 } 7338 else { 7339 $newJoinedSelector = $selector->createDerived(array()); 7340 } 7341 7342 //put together the parent selectors after the join 7343 if ( count($parentSel) > 1) { 7344 $afterParentJoin = array_merge($afterParentJoin, array_slice($parentSel,1) ); 7345 } 7346 7347 if ( $parentSel ){ 7348 $newJoinedSelectorEmpty = false; 7349 7350 // join the elements so far with the first part of the parent 7351 $newJoinedSelector->elements[] = new Less_Tree_Element( $el->combinator, $parentSel[0]->elements[0]->value, $el->index, $el->currentFileInfo); 7352 7353 $newJoinedSelector->elements = array_merge( $newJoinedSelector->elements, array_slice($parentSel[0]->elements, 1) ); 7354 } 7355 7356 if (!$newJoinedSelectorEmpty) { 7357 // now add the joined selector 7358 $newSelectorPath[] = $newJoinedSelector; 7359 } 7360 7361 // and the rest of the parent 7362 $newSelectorPath = array_merge($newSelectorPath, $afterParentJoin); 7363 7364 // add that to our new set of selectors 7365 $selectorsMultiplied[] = $newSelectorPath; 7366 } 7367 } 7368 } 7369 7370 // our new selectors has been multiplied, so reset the state 7371 $newSelectors = $selectorsMultiplied; 7372 $currentElements = array(); 7373 } 7374 } 7375 7376 // if we have any elements left over (e.g. .a& .b == .b) 7377 // add them on to all the current selectors 7378 if( $currentElements ){ 7379 $this->mergeElementsOnToSelectors($currentElements, $newSelectors); 7380 } 7381 foreach( $newSelectors as $new_sel){ 7382 if( $new_sel ){ 7383 $paths[] = $new_sel; 7384 } 7385 } 7386 } 7387 7388 function mergeElementsOnToSelectors( $elements, &$selectors){ 7389 7390 if( !$selectors ){ 7391 $selectors[] = array( new Less_Tree_Selector($elements) ); 7392 return; 7393 } 7394 7395 7396 foreach( $selectors as &$sel){ 7397 7398 // if the previous thing in sel is a parent this needs to join on to it 7399 if( $sel ){ 7400 $last = count($sel)-1; 7401 $sel[$last] = $sel[$last]->createDerived( array_merge($sel[$last]->elements, $elements) ); 7402 }else{ 7403 $sel[] = new Less_Tree_Selector( $elements ); 7404 } 7405 } 7406 } 7407 } 7408 7409 7410 /** 7411 * RulesetCall 7412 * 7413 * @package Less 7414 * @subpackage tree 7415 */ 7416 class Less_Tree_RulesetCall extends Less_Tree{ 7417 7418 public $variable; 7419 public $type = "RulesetCall"; 7420 7421 public function __construct($variable){ 7422 $this->variable = $variable; 7423 } 7424 7425 public function accept($visitor) {} 7426 7427 public function compile( $env ){ 7428 $variable = new Less_Tree_Variable($this->variable); 7429 $detachedRuleset = $variable->compile($env); 7430 return $detachedRuleset->callEval($env); 7431 } 7432 } 7433 7434 7435 7436 /** 7437 * Selector 7438 * 7439 * @package Less 7440 * @subpackage tree 7441 */ 7442 class Less_Tree_Selector extends Less_Tree{ 7443 7444 public $elements; 7445 public $condition; 7446 public $extendList = array(); 7447 public $_css; 7448 public $index; 7449 public $evaldCondition = false; 7450 public $type = 'Selector'; 7451 public $currentFileInfo = array(); 7452 public $isReferenced; 7453 public $mediaEmpty; 7454 7455 public $elements_len = 0; 7456 7457 public $_oelements; 7458 public $_oelements_len; 7459 public $cacheable = true; 7460 7461 /** 7462 * @param boolean $isReferenced 7463 */ 7464 public function __construct( $elements, $extendList = array() , $condition = null, $index=null, $currentFileInfo=null, $isReferenced=null ){ 7465 7466 $this->elements = $elements; 7467 $this->elements_len = count($elements); 7468 $this->extendList = $extendList; 7469 $this->condition = $condition; 7470 if( $currentFileInfo ){ 7471 $this->currentFileInfo = $currentFileInfo; 7472 } 7473 $this->isReferenced = $isReferenced; 7474 if( !$condition ){ 7475 $this->evaldCondition = true; 7476 } 7477 7478 $this->CacheElements(); 7479 } 7480 7481 public function accept($visitor) { 7482 $this->elements = $visitor->visitArray($this->elements); 7483 $this->extendList = $visitor->visitArray($this->extendList); 7484 if( $this->condition ){ 7485 $this->condition = $visitor->visitObj($this->condition); 7486 } 7487 7488 if( $visitor instanceof Less_Visitor_extendFinder ){ 7489 $this->CacheElements(); 7490 } 7491 } 7492 7493 public function createDerived( $elements, $extendList = null, $evaldCondition = null ){ 7494 $newSelector = new Less_Tree_Selector( $elements, ($extendList ? $extendList : $this->extendList), null, $this->index, $this->currentFileInfo, $this->isReferenced); 7495 $newSelector->evaldCondition = $evaldCondition ? $evaldCondition : $this->evaldCondition; 7496 return $newSelector; 7497 } 7498 7499 7500 public function match( $other ){ 7501 7502 if( !$other->_oelements || ($this->elements_len < $other->_oelements_len) ){ 7503 return 0; 7504 } 7505 7506 for( $i = 0; $i < $other->_oelements_len; $i++ ){ 7507 if( $this->elements[$i]->value !== $other->_oelements[$i]) { 7508 return 0; 7509 } 7510 } 7511 7512 return $other->_oelements_len; // return number of matched elements 7513 } 7514 7515 7516 public function CacheElements(){ 7517 7518 $this->_oelements = array(); 7519 $css = ''; 7520 7521 foreach($this->elements as $v){ 7522 7523 $css .= $v->combinator; 7524 if( !$v->value_is_object ){ 7525 $css .= $v->value; 7526 continue; 7527 } 7528 7529 if( !property_exists($v->value,'value') || !is_string($v->value->value) ){ 7530 $this->cacheable = false; 7531 return; 7532 } 7533 $css .= $v->value->value; 7534 } 7535 7536 $this->_oelements_len = preg_match_all('/[,&#\.\w-](?:[\w-]|(?:\\\\.))*/', $css, $matches); 7537 if( $this->_oelements_len ){ 7538 $this->_oelements = $matches[0]; 7539 7540 if( $this->_oelements[0] === '&' ){ 7541 array_shift($this->_oelements); 7542 $this->_oelements_len--; 7543 } 7544 } 7545 } 7546 7547 public function isJustParentSelector(){ 7548 return !$this->mediaEmpty && 7549 count($this->elements) === 1 && 7550 $this->elements[0]->value === '&' && 7551 ($this->elements[0]->combinator === ' ' || $this->elements[0]->combinator === ''); 7552 } 7553 7554 public function compile($env) { 7555 7556 $elements = array(); 7557 foreach($this->elements as $el){ 7558 $elements[] = $el->compile($env); 7559 } 7560 7561 $extendList = array(); 7562 foreach($this->extendList as $el){ 7563 $extendList[] = $el->compile($el); 7564 } 7565 7566 $evaldCondition = false; 7567 if( $this->condition ){ 7568 $evaldCondition = $this->condition->compile($env); 7569 } 7570 7571 return $this->createDerived( $elements, $extendList, $evaldCondition ); 7572 } 7573 7574 7575 /** 7576 * @see Less_Tree::genCSS 7577 */ 7578 public function genCSS( $output, $firstSelector = true ){ 7579 7580 if( !$firstSelector && $this->elements[0]->combinator === "" ){ 7581 $output->add(' ', $this->currentFileInfo, $this->index); 7582 } 7583 7584 foreach($this->elements as $element){ 7585 $element->genCSS( $output ); 7586 } 7587 } 7588 7589 public function markReferenced(){ 7590 $this->isReferenced = true; 7591 } 7592 7593 public function getIsReferenced(){ 7594 return !isset($this->currentFileInfo['reference']) || !$this->currentFileInfo['reference'] || $this->isReferenced; 7595 } 7596 7597 public function getIsOutput(){ 7598 return $this->evaldCondition; 7599 } 7600 7601 } 7602 7603 7604 /** 7605 * UnicodeDescriptor 7606 * 7607 * @package Less 7608 * @subpackage tree 7609 */ 7610 class Less_Tree_UnicodeDescriptor extends Less_Tree{ 7611 7612 public $value; 7613 public $type = 'UnicodeDescriptor'; 7614 7615 public function __construct($value){ 7616 $this->value = $value; 7617 } 7618 7619 /** 7620 * @see Less_Tree::genCSS 7621 */ 7622 public function genCSS( $output ){ 7623 $output->add( $this->value ); 7624 } 7625 7626 public function compile(){ 7627 return $this; 7628 } 7629 } 7630 7631 7632 7633 /** 7634 * Unit 7635 * 7636 * @package Less 7637 * @subpackage tree 7638 */ 7639 class Less_Tree_Unit extends Less_Tree{ 7640 7641 var $numerator = array(); 7642 var $denominator = array(); 7643 public $backupUnit; 7644 public $type = 'Unit'; 7645 7646 public function __construct($numerator = array(), $denominator = array(), $backupUnit = null ){ 7647 $this->numerator = $numerator; 7648 $this->denominator = $denominator; 7649 $this->backupUnit = $backupUnit; 7650 } 7651 7652 public function __clone(){ 7653 } 7654 7655 /** 7656 * @see Less_Tree::genCSS 7657 */ 7658 public function genCSS( $output ){ 7659 7660 if( $this->numerator ){ 7661 $output->add( $this->numerator[0] ); 7662 }elseif( $this->denominator ){ 7663 $output->add( $this->denominator[0] ); 7664 }elseif( !Less_Parser::$options['strictUnits'] && $this->backupUnit ){ 7665 $output->add( $this->backupUnit ); 7666 return ; 7667 } 7668 } 7669 7670 public function toString(){ 7671 $returnStr = implode('*',$this->numerator); 7672 foreach($this->denominator as $d){ 7673 $returnStr .= '/'.$d; 7674 } 7675 return $returnStr; 7676 } 7677 7678 public function __toString(){ 7679 return $this->toString(); 7680 } 7681 7682 7683 /** 7684 * @param Less_Tree_Unit $other 7685 */ 7686 public function compare($other) { 7687 return $this->is( $other->toString() ) ? 0 : -1; 7688 } 7689 7690 public function is($unitString){ 7691 return $this->toString() === $unitString; 7692 } 7693 7694 public function isLength(){ 7695 $css = $this->toCSS(); 7696 return !!preg_match('/px|em|%|in|cm|mm|pc|pt|ex/',$css); 7697 } 7698 7699 public function isAngle() { 7700 return isset( Less_Tree_UnitConversions::$angle[$this->toCSS()] ); 7701 } 7702 7703 public function isEmpty(){ 7704 return !$this->numerator && !$this->denominator; 7705 } 7706 7707 public function isSingular() { 7708 return count($this->numerator) <= 1 && !$this->denominator; 7709 } 7710 7711 7712 public function usedUnits(){ 7713 $result = array(); 7714 7715 foreach(Less_Tree_UnitConversions::$groups as $groupName){ 7716 $group = Less_Tree_UnitConversions::${$groupName}; 7717 7718 foreach($this->numerator as $atomicUnit){ 7719 if( isset($group[$atomicUnit]) && !isset($result[$groupName]) ){ 7720 $result[$groupName] = $atomicUnit; 7721 } 7722 } 7723 7724 foreach($this->denominator as $atomicUnit){ 7725 if( isset($group[$atomicUnit]) && !isset($result[$groupName]) ){ 7726 $result[$groupName] = $atomicUnit; 7727 } 7728 } 7729 } 7730 7731 return $result; 7732 } 7733 7734 public function cancel(){ 7735 $counter = array(); 7736 $backup = null; 7737 7738 foreach($this->numerator as $atomicUnit){ 7739 if( !$backup ){ 7740 $backup = $atomicUnit; 7741 } 7742 $counter[$atomicUnit] = ( isset($counter[$atomicUnit]) ? $counter[$atomicUnit] : 0) + 1; 7743 } 7744 7745 foreach($this->denominator as $atomicUnit){ 7746 if( !$backup ){ 7747 $backup = $atomicUnit; 7748 } 7749 $counter[$atomicUnit] = ( isset($counter[$atomicUnit]) ? $counter[$atomicUnit] : 0) - 1; 7750 } 7751 7752 $this->numerator = array(); 7753 $this->denominator = array(); 7754 7755 foreach($counter as $atomicUnit => $count){ 7756 if( $count > 0 ){ 7757 for( $i = 0; $i < $count; $i++ ){ 7758 $this->numerator[] = $atomicUnit; 7759 } 7760 }elseif( $count < 0 ){ 7761 for( $i = 0; $i < -$count; $i++ ){ 7762 $this->denominator[] = $atomicUnit; 7763 } 7764 } 7765 } 7766 7767 if( !$this->numerator && !$this->denominator && $backup ){ 7768 $this->backupUnit = $backup; 7769 } 7770 7771 sort($this->numerator); 7772 sort($this->denominator); 7773 } 7774 7775 7776 } 7777 7778 7779 7780 /** 7781 * UnitConversions 7782 * 7783 * @package Less 7784 * @subpackage tree 7785 */ 7786 class Less_Tree_UnitConversions{ 7787 7788 public static $groups = array('length','duration','angle'); 7789 7790 public static $length = array( 7791 'm'=> 1, 7792 'cm'=> 0.01, 7793 'mm'=> 0.001, 7794 'in'=> 0.0254, 7795 'px'=> 0.000264583, // 0.0254 / 96, 7796 'pt'=> 0.000352778, // 0.0254 / 72, 7797 'pc'=> 0.004233333, // 0.0254 / 72 * 12 7798 ); 7799 7800 public static $duration = array( 7801 's'=> 1, 7802 'ms'=> 0.001 7803 ); 7804 7805 public static $angle = array( 7806 'rad' => 0.1591549430919, // 1/(2*M_PI), 7807 'deg' => 0.002777778, // 1/360, 7808 'grad'=> 0.0025, // 1/400, 7809 'turn'=> 1 7810 ); 7811 7812 } 7813 7814 /** 7815 * Url 7816 * 7817 * @package Less 7818 * @subpackage tree 7819 */ 7820 class Less_Tree_Url extends Less_Tree{ 7821 7822 public $attrs; 7823 public $value; 7824 public $currentFileInfo; 7825 public $isEvald; 7826 public $type = 'Url'; 7827 7828 public function __construct($value, $currentFileInfo = null, $isEvald = null){ 7829 $this->value = $value; 7830 $this->currentFileInfo = $currentFileInfo; 7831 $this->isEvald = $isEvald; 7832 } 7833 7834 public function accept( $visitor ){ 7835 $this->value = $visitor->visitObj($this->value); 7836 } 7837 7838 /** 7839 * @see Less_Tree::genCSS 7840 */ 7841 public function genCSS( $output ){ 7842 $output->add( 'url(' ); 7843 $this->value->genCSS( $output ); 7844 $output->add( ')' ); 7845 } 7846 7847 /** 7848 * @param Less_Functions $ctx 7849 */ 7850 public function compile($ctx){ 7851 $val = $this->value->compile($ctx); 7852 7853 if( !$this->isEvald ){ 7854 // Add the base path if the URL is relative 7855 if( Less_Parser::$options['relativeUrls'] 7856 && $this->currentFileInfo 7857 && is_string($val->value) 7858 && Less_Environment::isPathRelative($val->value) 7859 ){ 7860 $rootpath = $this->currentFileInfo['uri_root']; 7861 if ( !$val->quote ){ 7862 $rootpath = preg_replace('/[\(\)\'"\s]/', '\\$1', $rootpath ); 7863 } 7864 $val->value = $rootpath . $val->value; 7865 } 7866 7867 $val->value = Less_Environment::normalizePath( $val->value); 7868 } 7869 7870 // Add cache buster if enabled 7871 if( Less_Parser::$options['urlArgs'] ){ 7872 if( !preg_match('/^\s*data:/',$val->value) ){ 7873 $delimiter = strpos($val->value,'?') === false ? '?' : '&'; 7874 $urlArgs = $delimiter . Less_Parser::$options['urlArgs']; 7875 $hash_pos = strpos($val->value,'#'); 7876 if( $hash_pos !== false ){ 7877 $val->value = substr_replace($val->value,$urlArgs, $hash_pos, 0); 7878 } else { 7879 $val->value .= $urlArgs; 7880 } 7881 } 7882 } 7883 7884 return new Less_Tree_URL($val, $this->currentFileInfo, true); 7885 } 7886 7887 } 7888 7889 7890 /** 7891 * Value 7892 * 7893 * @package Less 7894 * @subpackage tree 7895 */ 7896 class Less_Tree_Value extends Less_Tree{ 7897 7898 public $type = 'Value'; 7899 public $value; 7900 7901 public function __construct($value){ 7902 $this->value = $value; 7903 } 7904 7905 public function accept($visitor) { 7906 $this->value = $visitor->visitArray($this->value); 7907 } 7908 7909 public function compile($env){ 7910 7911 $ret = array(); 7912 $i = 0; 7913 foreach($this->value as $i => $v){ 7914 $ret[] = $v->compile($env); 7915 } 7916 if( $i > 0 ){ 7917 return new Less_Tree_Value($ret); 7918 } 7919 return $ret[0]; 7920 } 7921 7922 /** 7923 * @see Less_Tree::genCSS 7924 */ 7925 function genCSS( $output ){ 7926 $len = count($this->value); 7927 for($i = 0; $i < $len; $i++ ){ 7928 $this->value[$i]->genCSS( $output ); 7929 if( $i+1 < $len ){ 7930 $output->add( Less_Environment::$_outputMap[','] ); 7931 } 7932 } 7933 } 7934 7935 } 7936 7937 7938 /** 7939 * Variable 7940 * 7941 * @package Less 7942 * @subpackage tree 7943 */ 7944 class Less_Tree_Variable extends Less_Tree{ 7945 7946 public $name; 7947 public $index; 7948 public $currentFileInfo; 7949 public $evaluating = false; 7950 public $type = 'Variable'; 7951 7952 /** 7953 * @param string $name 7954 */ 7955 public function __construct($name, $index = null, $currentFileInfo = null) { 7956 $this->name = $name; 7957 $this->index = $index; 7958 $this->currentFileInfo = $currentFileInfo; 7959 } 7960 7961 public function compile($env) { 7962 7963 if( $this->name[1] === '@' ){ 7964 $v = new Less_Tree_Variable(substr($this->name, 1), $this->index + 1, $this->currentFileInfo); 7965 $name = '@' . $v->compile($env)->value; 7966 }else{ 7967 $name = $this->name; 7968 } 7969 7970 if ($this->evaluating) { 7971 throw new Less_Exception_Compiler("Recursive variable definition for " . $name, null, $this->index, $this->currentFileInfo); 7972 } 7973 7974 $this->evaluating = true; 7975 7976 foreach($env->frames as $frame){ 7977 if( $v = $frame->variable($name) ){ 7978 $r = $v->value->compile($env); 7979 $this->evaluating = false; 7980 return $r; 7981 } 7982 } 7983 7984 throw new Less_Exception_Compiler("variable " . $name . " is undefined in file ".$this->currentFileInfo["filename"], null, $this->index, $this->currentFileInfo); 7985 } 7986 7987 } 7988 7989 7990 7991 class Less_Tree_Mixin_Call extends Less_Tree{ 7992 7993 public $selector; 7994 public $arguments; 7995 public $index; 7996 public $currentFileInfo; 7997 7998 public $important; 7999 public $type = 'MixinCall'; 8000 8001 /** 8002 * less.js: tree.mixin.Call 8003 * 8004 */ 8005 public function __construct($elements, $args, $index, $currentFileInfo, $important = false){ 8006 $this->selector = new Less_Tree_Selector($elements); 8007 $this->arguments = $args; 8008 $this->index = $index; 8009 $this->currentFileInfo = $currentFileInfo; 8010 $this->important = $important; 8011 } 8012 8013 //function accept($visitor){ 8014 // $this->selector = $visitor->visit($this->selector); 8015 // $this->arguments = $visitor->visit($this->arguments); 8016 //} 8017 8018 8019 public function compile($env){ 8020 8021 $rules = array(); 8022 $match = false; 8023 $isOneFound = false; 8024 $candidates = array(); 8025 $defaultUsed = false; 8026 $conditionResult = array(); 8027 8028 $args = array(); 8029 foreach($this->arguments as $a){ 8030 $args[] = array('name'=> $a['name'], 'value' => $a['value']->compile($env) ); 8031 } 8032 8033 foreach($env->frames as $frame){ 8034 8035 $mixins = $frame->find($this->selector); 8036 8037 if( !$mixins ){ 8038 continue; 8039 } 8040 8041 $isOneFound = true; 8042 $defNone = 0; 8043 $defTrue = 1; 8044 $defFalse = 2; 8045 8046 // To make `default()` function independent of definition order we have two "subpasses" here. 8047 // At first we evaluate each guard *twice* (with `default() == true` and `default() == false`), 8048 // and build candidate list with corresponding flags. Then, when we know all possible matches, 8049 // we make a final decision. 8050 8051 $mixins_len = count($mixins); 8052 for( $m = 0; $m < $mixins_len; $m++ ){ 8053 $mixin = $mixins[$m]; 8054 8055 if( $this->IsRecursive( $env, $mixin ) ){ 8056 continue; 8057 } 8058 8059 if( $mixin->matchArgs($args, $env) ){ 8060 8061 $candidate = array('mixin' => $mixin, 'group' => $defNone); 8062 8063 if( $mixin instanceof Less_Tree_Ruleset ){ 8064 8065 for( $f = 0; $f < 2; $f++ ){ 8066 Less_Tree_DefaultFunc::value($f); 8067 $conditionResult[$f] = $mixin->matchCondition( $args, $env); 8068 } 8069 if( $conditionResult[0] || $conditionResult[1] ){ 8070 if( $conditionResult[0] != $conditionResult[1] ){ 8071 $candidate['group'] = $conditionResult[1] ? $defTrue : $defFalse; 8072 } 8073 8074 $candidates[] = $candidate; 8075 } 8076 }else{ 8077 $candidates[] = $candidate; 8078 } 8079 8080 $match = true; 8081 } 8082 } 8083 8084 Less_Tree_DefaultFunc::reset(); 8085 8086 8087 $count = array(0, 0, 0); 8088 for( $m = 0; $m < count($candidates); $m++ ){ 8089 $count[ $candidates[$m]['group'] ]++; 8090 } 8091 8092 if( $count[$defNone] > 0 ){ 8093 $defaultResult = $defFalse; 8094 } else { 8095 $defaultResult = $defTrue; 8096 if( ($count[$defTrue] + $count[$defFalse]) > 1 ){ 8097 throw new Exception( 'Ambiguous use of `default()` found when matching for `' . $this->format($args) . '`' ); 8098 } 8099 } 8100 8101 8102 $candidates_length = count($candidates); 8103 $length_1 = ($candidates_length == 1); 8104 8105 for( $m = 0; $m < $candidates_length; $m++){ 8106 $candidate = $candidates[$m]['group']; 8107 if( ($candidate === $defNone) || ($candidate === $defaultResult) ){ 8108 try{ 8109 $mixin = $candidates[$m]['mixin']; 8110 if( !($mixin instanceof Less_Tree_Mixin_Definition) ){ 8111 $mixin = new Less_Tree_Mixin_Definition('', array(), $mixin->rules, null, false); 8112 $mixin->originalRuleset = $mixins[$m]->originalRuleset; 8113 } 8114 $rules = array_merge($rules, $mixin->evalCall($env, $args, $this->important)->rules); 8115 } catch (Exception $e) { 8116 //throw new Less_Exception_Compiler($e->getMessage(), $e->index, null, $this->currentFileInfo['filename']); 8117 throw new Less_Exception_Compiler($e->getMessage(), null, null, $this->currentFileInfo); 8118 } 8119 } 8120 } 8121 8122 if( $match ){ 8123 if( !$this->currentFileInfo || !isset($this->currentFileInfo['reference']) || !$this->currentFileInfo['reference'] ){ 8124 Less_Tree::ReferencedArray($rules); 8125 } 8126 8127 return $rules; 8128 } 8129 } 8130 8131 if( $isOneFound ){ 8132 throw new Less_Exception_Compiler('No matching definition was found for `'.$this->Format( $args ).'`', null, $this->index, $this->currentFileInfo); 8133 8134 }else{ 8135 throw new Less_Exception_Compiler(trim($this->selector->toCSS()) . " is undefined in ".$this->currentFileInfo['filename'], null, $this->index); 8136 } 8137 8138 } 8139 8140 /** 8141 * Format the args for use in exception messages 8142 * 8143 */ 8144 private function Format($args){ 8145 $message = array(); 8146 if( $args ){ 8147 foreach($args as $a){ 8148 $argValue = ''; 8149 if( $a['name'] ){ 8150 $argValue .= $a['name'] . ':'; 8151 } 8152 if( is_object($a['value']) ){ 8153 $argValue .= $a['value']->toCSS(); 8154 }else{ 8155 $argValue .= '???'; 8156 } 8157 $message[] = $argValue; 8158 } 8159 } 8160 return implode(', ',$message); 8161 } 8162 8163 8164 /** 8165 * Are we in a recursive mixin call? 8166 * 8167 * @return bool 8168 */ 8169 private function IsRecursive( $env, $mixin ){ 8170 8171 foreach($env->frames as $recur_frame){ 8172 if( !($mixin instanceof Less_Tree_Mixin_Definition) ){ 8173 8174 if( $mixin === $recur_frame ){ 8175 return true; 8176 } 8177 8178 if( isset($recur_frame->originalRuleset) && $mixin->ruleset_id === $recur_frame->originalRuleset ){ 8179 return true; 8180 } 8181 } 8182 } 8183 8184 return false; 8185 } 8186 8187 } 8188 8189 8190 8191 8192 class Less_Tree_Mixin_Definition extends Less_Tree_Ruleset{ 8193 public $name; 8194 public $selectors; 8195 public $params; 8196 public $arity = 0; 8197 public $rules; 8198 public $lookups = array(); 8199 public $required = 0; 8200 public $frames = array(); 8201 public $condition; 8202 public $variadic; 8203 public $type = 'MixinDefinition'; 8204 8205 8206 // less.js : /lib/less/tree/mixin.js : tree.mixin.Definition 8207 public function __construct($name, $params, $rules, $condition, $variadic = false, $frames = array() ){ 8208 $this->name = $name; 8209 $this->selectors = array(new Less_Tree_Selector(array( new Less_Tree_Element(null, $name)))); 8210 8211 $this->params = $params; 8212 $this->condition = $condition; 8213 $this->variadic = $variadic; 8214 $this->rules = $rules; 8215 8216 if( $params ){ 8217 $this->arity = count($params); 8218 foreach( $params as $p ){ 8219 if (! isset($p['name']) || ($p['name'] && !isset($p['value']))) { 8220 $this->required++; 8221 } 8222 } 8223 } 8224 8225 $this->frames = $frames; 8226 $this->SetRulesetIndex(); 8227 } 8228 8229 8230 8231 //function accept( $visitor ){ 8232 // $this->params = $visitor->visit($this->params); 8233 // $this->rules = $visitor->visit($this->rules); 8234 // $this->condition = $visitor->visit($this->condition); 8235 //} 8236 8237 8238 public function toCSS(){ 8239 return ''; 8240 } 8241 8242 // less.js : /lib/less/tree/mixin.js : tree.mixin.Definition.evalParams 8243 public function compileParams($env, $mixinFrames, $args = array() , &$evaldArguments = array() ){ 8244 $frame = new Less_Tree_Ruleset(null, array()); 8245 $params = $this->params; 8246 $mixinEnv = null; 8247 $argsLength = 0; 8248 8249 if( $args ){ 8250 $argsLength = count($args); 8251 for($i = 0; $i < $argsLength; $i++ ){ 8252 $arg = $args[$i]; 8253 8254 if( $arg && $arg['name'] ){ 8255 $isNamedFound = false; 8256 8257 foreach($params as $j => $param){ 8258 if( !isset($evaldArguments[$j]) && $arg['name'] === $params[$j]['name']) { 8259 $evaldArguments[$j] = $arg['value']->compile($env); 8260 array_unshift($frame->rules, new Less_Tree_Rule( $arg['name'], $arg['value']->compile($env) ) ); 8261 $isNamedFound = true; 8262 break; 8263 } 8264 } 8265 if ($isNamedFound) { 8266 array_splice($args, $i, 1); 8267 $i--; 8268 $argsLength--; 8269 continue; 8270 } else { 8271 throw new Less_Exception_Compiler("Named argument for " . $this->name .' '.$args[$i]['name'] . ' not found'); 8272 } 8273 } 8274 } 8275 } 8276 8277 $argIndex = 0; 8278 foreach($params as $i => $param){ 8279 8280 if ( isset($evaldArguments[$i]) ){ continue; } 8281 8282 $arg = null; 8283 if( isset($args[$argIndex]) ){ 8284 $arg = $args[$argIndex]; 8285 } 8286 8287 if (isset($param['name']) && $param['name']) { 8288 8289 if( isset($param['variadic']) ){ 8290 $varargs = array(); 8291 for ($j = $argIndex; $j < $argsLength; $j++) { 8292 $varargs[] = $args[$j]['value']->compile($env); 8293 } 8294 $expression = new Less_Tree_Expression($varargs); 8295 array_unshift($frame->rules, new Less_Tree_Rule($param['name'], $expression->compile($env))); 8296 }else{ 8297 $val = ($arg && $arg['value']) ? $arg['value'] : false; 8298 8299 if ($val) { 8300 $val = $val->compile($env); 8301 } else if ( isset($param['value']) ) { 8302 8303 if( !$mixinEnv ){ 8304 $mixinEnv = new Less_Environment(); 8305 $mixinEnv->frames = array_merge( array($frame), $mixinFrames); 8306 } 8307 8308 $val = $param['value']->compile($mixinEnv); 8309 $frame->resetCache(); 8310 } else { 8311 throw new Less_Exception_Compiler("Wrong number of arguments for " . $this->name . " (" . $argsLength . ' for ' . $this->arity . ")"); 8312 } 8313 8314 array_unshift($frame->rules, new Less_Tree_Rule($param['name'], $val)); 8315 $evaldArguments[$i] = $val; 8316 } 8317 } 8318 8319 if ( isset($param['variadic']) && $args) { 8320 for ($j = $argIndex; $j < $argsLength; $j++) { 8321 $evaldArguments[$j] = $args[$j]['value']->compile($env); 8322 } 8323 } 8324 $argIndex++; 8325 } 8326 8327 ksort($evaldArguments); 8328 $evaldArguments = array_values($evaldArguments); 8329 8330 return $frame; 8331 } 8332 8333 public function compile($env) { 8334 if( $this->frames ){ 8335 return new Less_Tree_Mixin_Definition($this->name, $this->params, $this->rules, $this->condition, $this->variadic, $this->frames ); 8336 } 8337 return new Less_Tree_Mixin_Definition($this->name, $this->params, $this->rules, $this->condition, $this->variadic, $env->frames ); 8338 } 8339 8340 public function evalCall($env, $args = NULL, $important = NULL) { 8341 8342 Less_Environment::$mixin_stack++; 8343 8344 $_arguments = array(); 8345 8346 if( $this->frames ){ 8347 $mixinFrames = array_merge($this->frames, $env->frames); 8348 }else{ 8349 $mixinFrames = $env->frames; 8350 } 8351 8352 $frame = $this->compileParams($env, $mixinFrames, $args, $_arguments); 8353 8354 $ex = new Less_Tree_Expression($_arguments); 8355 array_unshift($frame->rules, new Less_Tree_Rule('@arguments', $ex->compile($env))); 8356 8357 8358 $ruleset = new Less_Tree_Ruleset(null, $this->rules); 8359 $ruleset->originalRuleset = $this->ruleset_id; 8360 8361 8362 $ruleSetEnv = new Less_Environment(); 8363 $ruleSetEnv->frames = array_merge( array($this, $frame), $mixinFrames ); 8364 $ruleset = $ruleset->compile( $ruleSetEnv ); 8365 8366 if( $important ){ 8367 $ruleset = $ruleset->makeImportant(); 8368 } 8369 8370 Less_Environment::$mixin_stack--; 8371 8372 return $ruleset; 8373 } 8374 8375 8376 public function matchCondition($args, $env) { 8377 8378 if( !$this->condition ){ 8379 return true; 8380 } 8381 8382 // set array to prevent error on array_merge 8383 if(!is_array($this->frames)) { 8384 $this->frames = array(); 8385 } 8386 8387 $frame = $this->compileParams($env, array_merge($this->frames,$env->frames), $args ); 8388 8389 $compile_env = new Less_Environment(); 8390 $compile_env->frames = array_merge( 8391 array($frame) // the parameter variables 8392 , $this->frames // the parent namespace/mixin frames 8393 , $env->frames // the current environment frames 8394 ); 8395 8396 $compile_env->functions = $env->functions; 8397 8398 return (bool)$this->condition->compile($compile_env); 8399 } 8400 8401 public function matchArgs($args, $env = NULL){ 8402 $argsLength = count($args); 8403 8404 if( !$this->variadic ){ 8405 if( $argsLength < $this->required ){ 8406 return false; 8407 } 8408 if( $argsLength > count($this->params) ){ 8409 return false; 8410 } 8411 }else{ 8412 if( $argsLength < ($this->required - 1)){ 8413 return false; 8414 } 8415 } 8416 8417 $len = min($argsLength, $this->arity); 8418 8419 for( $i = 0; $i < $len; $i++ ){ 8420 if( !isset($this->params[$i]['name']) && !isset($this->params[$i]['variadic']) ){ 8421 if( $args[$i]['value']->compile($env)->toCSS() != $this->params[$i]['value']->compile($env)->toCSS() ){ 8422 return false; 8423 } 8424 } 8425 } 8426 8427 return true; 8428 } 8429 8430 } 8431 8432 8433 /** 8434 * Extend Finder Visitor 8435 * 8436 * @package Less 8437 * @subpackage visitor 8438 */ 8439 class Less_Visitor_extendFinder extends Less_Visitor{ 8440 8441 public $contexts = array(); 8442 public $allExtendsStack; 8443 public $foundExtends; 8444 8445 public function __construct(){ 8446 $this->contexts = array(); 8447 $this->allExtendsStack = array(array()); 8448 parent::__construct(); 8449 } 8450 8451 /** 8452 * @param Less_Tree_Ruleset $root 8453 */ 8454 public function run($root){ 8455 $root = $this->visitObj($root); 8456 $root->allExtends =& $this->allExtendsStack[0]; 8457 return $root; 8458 } 8459 8460 public function visitRule($ruleNode, &$visitDeeper ){ 8461 $visitDeeper = false; 8462 } 8463 8464 public function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ){ 8465 $visitDeeper = false; 8466 } 8467 8468 public function visitRuleset($rulesetNode){ 8469 8470 if( $rulesetNode->root ){ 8471 return; 8472 } 8473 8474 $allSelectorsExtendList = array(); 8475 8476 // get &:extend(.a); rules which apply to all selectors in this ruleset 8477 if( $rulesetNode->rules ){ 8478 foreach($rulesetNode->rules as $rule){ 8479 if( $rule instanceof Less_Tree_Extend ){ 8480 $allSelectorsExtendList[] = $rule; 8481 $rulesetNode->extendOnEveryPath = true; 8482 } 8483 } 8484 } 8485 8486 8487 // now find every selector and apply the extends that apply to all extends 8488 // and the ones which apply to an individual extend 8489 foreach($rulesetNode->paths as $selectorPath){ 8490 $selector = end($selectorPath); //$selectorPath[ count($selectorPath)-1]; 8491 8492 $j = 0; 8493 foreach($selector->extendList as $extend){ 8494 $this->allExtendsStackPush($rulesetNode, $selectorPath, $extend, $j); 8495 } 8496 foreach($allSelectorsExtendList as $extend){ 8497 $this->allExtendsStackPush($rulesetNode, $selectorPath, $extend, $j); 8498 } 8499 } 8500 8501 $this->contexts[] = $rulesetNode->selectors; 8502 } 8503 8504 public function allExtendsStackPush($rulesetNode, $selectorPath, $extend, &$j){ 8505 $this->foundExtends = true; 8506 $extend = clone $extend; 8507 $extend->findSelfSelectors( $selectorPath ); 8508 $extend->ruleset = $rulesetNode; 8509 if( $j === 0 ){ 8510 $extend->firstExtendOnThisSelectorPath = true; 8511 } 8512 8513 $end_key = count($this->allExtendsStack)-1; 8514 $this->allExtendsStack[$end_key][] = $extend; 8515 $j++; 8516 } 8517 8518 8519 public function visitRulesetOut( $rulesetNode ){ 8520 if( !is_object($rulesetNode) || !$rulesetNode->root ){ 8521 array_pop($this->contexts); 8522 } 8523 } 8524 8525 public function visitMedia( $mediaNode ){ 8526 $mediaNode->allExtends = array(); 8527 $this->allExtendsStack[] =& $mediaNode->allExtends; 8528 } 8529 8530 public function visitMediaOut(){ 8531 array_pop($this->allExtendsStack); 8532 } 8533 8534 public function visitDirective( $directiveNode ){ 8535 $directiveNode->allExtends = array(); 8536 $this->allExtendsStack[] =& $directiveNode->allExtends; 8537 } 8538 8539 public function visitDirectiveOut(){ 8540 array_pop($this->allExtendsStack); 8541 } 8542 } 8543 8544 8545 8546 8547 /* 8548 class Less_Visitor_import extends Less_VisitorReplacing{ 8549 8550 public $_visitor; 8551 public $_importer; 8552 public $importCount; 8553 8554 function __construct( $evalEnv ){ 8555 $this->env = $evalEnv; 8556 $this->importCount = 0; 8557 parent::__construct(); 8558 } 8559 8560 8561 function run( $root ){ 8562 $root = $this->visitObj($root); 8563 $this->isFinished = true; 8564 8565 //if( $this->importCount === 0) { 8566 // $this->_finish(); 8567 //} 8568 } 8569 8570 function visitImport($importNode, &$visitDeeper ){ 8571 $importVisitor = $this; 8572 $inlineCSS = $importNode->options['inline']; 8573 8574 if( !$importNode->css || $inlineCSS ){ 8575 $evaldImportNode = $importNode->compileForImport($this->env); 8576 8577 if( $evaldImportNode && (!$evaldImportNode->css || $inlineCSS) ){ 8578 $importNode = $evaldImportNode; 8579 $this->importCount++; 8580 $env = clone $this->env; 8581 8582 if( (isset($importNode->options['multiple']) && $importNode->options['multiple']) ){ 8583 $env->importMultiple = true; 8584 } 8585 8586 //get path & uri 8587 $path_and_uri = null; 8588 if( is_callable(Less_Parser::$options['import_callback']) ){ 8589 $path_and_uri = call_user_func(Less_Parser::$options['import_callback'],$importNode); 8590 } 8591 8592 if( !$path_and_uri ){ 8593 $path_and_uri = $importNode->PathAndUri(); 8594 } 8595 8596 if( $path_and_uri ){ 8597 list($full_path, $uri) = $path_and_uri; 8598 }else{ 8599 $full_path = $uri = $importNode->getPath(); 8600 } 8601 8602 8603 //import once 8604 if( $importNode->skip( $full_path, $env) ){ 8605 return array(); 8606 } 8607 8608 if( $importNode->options['inline'] ){ 8609 //todo needs to reference css file not import 8610 //$contents = new Less_Tree_Anonymous($importNode->root, 0, array('filename'=>$importNode->importedFilename), true ); 8611 8612 Less_Parser::AddParsedFile($full_path); 8613 $contents = new Less_Tree_Anonymous( file_get_contents($full_path), 0, array(), true ); 8614 8615 if( $importNode->features ){ 8616 return new Less_Tree_Media( array($contents), $importNode->features->value ); 8617 } 8618 8619 return array( $contents ); 8620 } 8621 8622 8623 // css ? 8624 if( $importNode->css ){ 8625 $features = ( $importNode->features ? $importNode->features->compile($env) : null ); 8626 return new Less_Tree_Import( $importNode->compilePath( $env), $features, $importNode->options, $this->index); 8627 } 8628 8629 return $importNode->ParseImport( $full_path, $uri, $env ); 8630 } 8631 8632 } 8633 8634 $visitDeeper = false; 8635 return $importNode; 8636 } 8637 8638 8639 function visitRule( $ruleNode, &$visitDeeper ){ 8640 $visitDeeper = false; 8641 return $ruleNode; 8642 } 8643 8644 function visitDirective($directiveNode, $visitArgs){ 8645 array_unshift($this->env->frames,$directiveNode); 8646 return $directiveNode; 8647 } 8648 8649 function visitDirectiveOut($directiveNode) { 8650 array_shift($this->env->frames); 8651 } 8652 8653 function visitMixinDefinition($mixinDefinitionNode, $visitArgs) { 8654 array_unshift($this->env->frames,$mixinDefinitionNode); 8655 return $mixinDefinitionNode; 8656 } 8657 8658 function visitMixinDefinitionOut($mixinDefinitionNode) { 8659 array_shift($this->env->frames); 8660 } 8661 8662 function visitRuleset($rulesetNode, $visitArgs) { 8663 array_unshift($this->env->frames,$rulesetNode); 8664 return $rulesetNode; 8665 } 8666 8667 function visitRulesetOut($rulesetNode) { 8668 array_shift($this->env->frames); 8669 } 8670 8671 function visitMedia($mediaNode, $visitArgs) { 8672 array_unshift($this->env->frames, $mediaNode->ruleset); 8673 return $mediaNode; 8674 } 8675 8676 function visitMediaOut($mediaNode) { 8677 array_shift($this->env->frames); 8678 } 8679 8680 } 8681 */ 8682 8683 8684 8685 8686 /** 8687 * Join Selector Visitor 8688 * 8689 * @package Less 8690 * @subpackage visitor 8691 */ 8692 class Less_Visitor_joinSelector extends Less_Visitor{ 8693 8694 public $contexts = array( array() ); 8695 8696 /** 8697 * @param Less_Tree_Ruleset $root 8698 */ 8699 public function run( $root ){ 8700 return $this->visitObj($root); 8701 } 8702 8703 public function visitRule( $ruleNode, &$visitDeeper ){ 8704 $visitDeeper = false; 8705 } 8706 8707 public function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ){ 8708 $visitDeeper = false; 8709 } 8710 8711 public function visitRuleset( $rulesetNode ){ 8712 8713 $paths = array(); 8714 8715 if( !$rulesetNode->root ){ 8716 $selectors = array(); 8717 8718 if( $rulesetNode->selectors && $rulesetNode->selectors ){ 8719 foreach($rulesetNode->selectors as $selector){ 8720 if( $selector->getIsOutput() ){ 8721 $selectors[] = $selector; 8722 } 8723 } 8724 } 8725 8726 if( !$selectors ){ 8727 $rulesetNode->selectors = null; 8728 $rulesetNode->rules = null; 8729 }else{ 8730 $context = end($this->contexts); //$context = $this->contexts[ count($this->contexts) - 1]; 8731 $paths = $rulesetNode->joinSelectors( $context, $selectors); 8732 } 8733 8734 $rulesetNode->paths = $paths; 8735 } 8736 8737 $this->contexts[] = $paths; //different from less.js. Placed after joinSelectors() so that $this->contexts will get correct $paths 8738 } 8739 8740 public function visitRulesetOut(){ 8741 array_pop($this->contexts); 8742 } 8743 8744 public function visitMedia($mediaNode) { 8745 $context = end($this->contexts); //$context = $this->contexts[ count($this->contexts) - 1]; 8746 8747 if( !count($context) || (is_object($context[0]) && $context[0]->multiMedia) ){ 8748 $mediaNode->rules[0]->root = true; 8749 } 8750 } 8751 8752 } 8753 8754 8755 8756 /** 8757 * Process Extends Visitor 8758 * 8759 * @package Less 8760 * @subpackage visitor 8761 */ 8762 class Less_Visitor_processExtends extends Less_Visitor{ 8763 8764 public $allExtendsStack; 8765 8766 /** 8767 * @param Less_Tree_Ruleset $root 8768 */ 8769 public function run( $root ){ 8770 $extendFinder = new Less_Visitor_extendFinder(); 8771 $extendFinder->run( $root ); 8772 if( !$extendFinder->foundExtends){ 8773 return $root; 8774 } 8775 8776 $root->allExtends = $this->doExtendChaining( $root->allExtends, $root->allExtends); 8777 8778 $this->allExtendsStack = array(); 8779 $this->allExtendsStack[] = &$root->allExtends; 8780 8781 return $this->visitObj( $root ); 8782 } 8783 8784 private function doExtendChaining( $extendsList, $extendsListTarget, $iterationCount = 0){ 8785 // 8786 // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting 8787 // the selector we would do normally, but we are also adding an extend with the same target selector 8788 // this means this new extend can then go and alter other extends 8789 // 8790 // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors 8791 // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if 8792 // we look at each selector at a time, as is done in visitRuleset 8793 8794 $extendsToAdd = array(); 8795 8796 8797 //loop through comparing every extend with every target extend. 8798 // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place 8799 // e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one 8800 // and the second is the target. 8801 // the seperation into two lists allows us to process a subset of chains with a bigger set, as is the 8802 // case when processing media queries 8803 for( $extendIndex = 0, $extendsList_len = count($extendsList); $extendIndex < $extendsList_len; $extendIndex++ ){ 8804 for( $targetExtendIndex = 0; $targetExtendIndex < count($extendsListTarget); $targetExtendIndex++ ){ 8805 8806 $extend = $extendsList[$extendIndex]; 8807 $targetExtend = $extendsListTarget[$targetExtendIndex]; 8808 8809 // look for circular references 8810 if( in_array($targetExtend->object_id, $extend->parent_ids,true) ){ 8811 continue; 8812 } 8813 8814 // find a match in the target extends self selector (the bit before :extend) 8815 $selectorPath = array( $targetExtend->selfSelectors[0] ); 8816 $matches = $this->findMatch( $extend, $selectorPath); 8817 8818 8819 if( $matches ){ 8820 8821 // we found a match, so for each self selector.. 8822 foreach($extend->selfSelectors as $selfSelector ){ 8823 8824 8825 // process the extend as usual 8826 $newSelector = $this->extendSelector( $matches, $selectorPath, $selfSelector); 8827 8828 // but now we create a new extend from it 8829 $newExtend = new Less_Tree_Extend( $targetExtend->selector, $targetExtend->option, 0); 8830 $newExtend->selfSelectors = $newSelector; 8831 8832 // add the extend onto the list of extends for that selector 8833 end($newSelector)->extendList = array($newExtend); 8834 //$newSelector[ count($newSelector)-1]->extendList = array($newExtend); 8835 8836 // record that we need to add it. 8837 $extendsToAdd[] = $newExtend; 8838 $newExtend->ruleset = $targetExtend->ruleset; 8839 8840 //remember its parents for circular references 8841 $newExtend->parent_ids = array_merge($newExtend->parent_ids,$targetExtend->parent_ids,$extend->parent_ids); 8842 8843 // only process the selector once.. if we have :extend(.a,.b) then multiple 8844 // extends will look at the same selector path, so when extending 8845 // we know that any others will be duplicates in terms of what is added to the css 8846 if( $targetExtend->firstExtendOnThisSelectorPath ){ 8847 $newExtend->firstExtendOnThisSelectorPath = true; 8848 $targetExtend->ruleset->paths[] = $newSelector; 8849 } 8850 } 8851 } 8852 } 8853 } 8854 8855 if( $extendsToAdd ){ 8856 // try to detect circular references to stop a stack overflow. 8857 // may no longer be needed. $this->extendChainCount++; 8858 if( $iterationCount > 100) { 8859 8860 try{ 8861 $selectorOne = $extendsToAdd[0]->selfSelectors[0]->toCSS(); 8862 $selectorTwo = $extendsToAdd[0]->selector->toCSS(); 8863 }catch(Exception $e){ 8864 $selectorOne = "{unable to calculate}"; 8865 $selectorTwo = "{unable to calculate}"; 8866 } 8867 8868 throw new Less_Exception_Parser("extend circular reference detected. One of the circular extends is currently:" . $selectorOne . ":extend(" . $selectorTwo . ")"); 8869 } 8870 8871 // now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e... 8872 $extendsToAdd = $this->doExtendChaining( $extendsToAdd, $extendsListTarget, $iterationCount+1); 8873 } 8874 8875 return array_merge($extendsList, $extendsToAdd); 8876 } 8877 8878 8879 protected function visitRule( $ruleNode, &$visitDeeper ){ 8880 $visitDeeper = false; 8881 } 8882 8883 protected function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ){ 8884 $visitDeeper = false; 8885 } 8886 8887 protected function visitSelector( $selectorNode, &$visitDeeper ){ 8888 $visitDeeper = false; 8889 } 8890 8891 protected function visitRuleset($rulesetNode){ 8892 8893 8894 if( $rulesetNode->root ){ 8895 return; 8896 } 8897 8898 $allExtends = end($this->allExtendsStack); 8899 $paths_len = count($rulesetNode->paths); 8900 8901 // look at each selector path in the ruleset, find any extend matches and then copy, find and replace 8902 foreach($allExtends as $allExtend){ 8903 for($pathIndex = 0; $pathIndex < $paths_len; $pathIndex++ ){ 8904 8905 // extending extends happens initially, before the main pass 8906 if( isset($rulesetNode->extendOnEveryPath) && $rulesetNode->extendOnEveryPath ){ 8907 continue; 8908 } 8909 8910 $selectorPath = $rulesetNode->paths[$pathIndex]; 8911 8912 if( end($selectorPath)->extendList ){ 8913 continue; 8914 } 8915 8916 $this->ExtendMatch( $rulesetNode, $allExtend, $selectorPath); 8917 8918 } 8919 } 8920 } 8921 8922 8923 private function ExtendMatch( $rulesetNode, $extend, $selectorPath ){ 8924 $matches = $this->findMatch($extend, $selectorPath); 8925 8926 if( $matches ){ 8927 foreach($extend->selfSelectors as $selfSelector ){ 8928 $rulesetNode->paths[] = $this->extendSelector($matches, $selectorPath, $selfSelector); 8929 } 8930 } 8931 } 8932 8933 8934 8935 private function findMatch($extend, $haystackSelectorPath ){ 8936 8937 8938 if( !$this->HasMatches($extend, $haystackSelectorPath) ){ 8939 return false; 8940 } 8941 8942 8943 // 8944 // look through the haystack selector path to try and find the needle - extend.selector 8945 // returns an array of selector matches that can then be replaced 8946 // 8947 $needleElements = $extend->selector->elements; 8948 $potentialMatches = array(); 8949 $potentialMatches_len = 0; 8950 $potentialMatch = null; 8951 $matches = array(); 8952 8953 8954 8955 // loop through the haystack elements 8956 $haystack_path_len = count($haystackSelectorPath); 8957 for($haystackSelectorIndex = 0; $haystackSelectorIndex < $haystack_path_len; $haystackSelectorIndex++ ){ 8958 $hackstackSelector = $haystackSelectorPath[$haystackSelectorIndex]; 8959 8960 $haystack_elements_len = count($hackstackSelector->elements); 8961 for($hackstackElementIndex = 0; $hackstackElementIndex < $haystack_elements_len; $hackstackElementIndex++ ){ 8962 8963 $haystackElement = $hackstackSelector->elements[$hackstackElementIndex]; 8964 8965 // if we allow elements before our match we can add a potential match every time. otherwise only at the first element. 8966 if( $extend->allowBefore || ($haystackSelectorIndex === 0 && $hackstackElementIndex === 0) ){ 8967 $potentialMatches[] = array('pathIndex'=> $haystackSelectorIndex, 'index'=> $hackstackElementIndex, 'matched'=> 0, 'initialCombinator'=> $haystackElement->combinator); 8968 $potentialMatches_len++; 8969 } 8970 8971 for($i = 0; $i < $potentialMatches_len; $i++ ){ 8972 8973 $potentialMatch = &$potentialMatches[$i]; 8974 $potentialMatch = $this->PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex ); 8975 8976 8977 // if we are still valid and have finished, test whether we have elements after and whether these are allowed 8978 if( $potentialMatch && $potentialMatch['matched'] === $extend->selector->elements_len ){ 8979 $potentialMatch['finished'] = true; 8980 8981 if( !$extend->allowAfter && ($hackstackElementIndex+1 < $haystack_elements_len || $haystackSelectorIndex+1 < $haystack_path_len) ){ 8982 $potentialMatch = null; 8983 } 8984 } 8985 8986 // if null we remove, if not, we are still valid, so either push as a valid match or continue 8987 if( $potentialMatch ){ 8988 if( $potentialMatch['finished'] ){ 8989 $potentialMatch['length'] = $extend->selector->elements_len; 8990 $potentialMatch['endPathIndex'] = $haystackSelectorIndex; 8991 $potentialMatch['endPathElementIndex'] = $hackstackElementIndex + 1; // index after end of match 8992 $potentialMatches = array(); // we don't allow matches to overlap, so start matching again 8993 $potentialMatches_len = 0; 8994 $matches[] = $potentialMatch; 8995 } 8996 continue; 8997 } 8998 8999 array_splice($potentialMatches, $i, 1); 9000 $potentialMatches_len--; 9001 $i--; 9002 } 9003 } 9004 } 9005 9006 return $matches; 9007 } 9008 9009 9010 // Before going through all the nested loops, lets check to see if a match is possible 9011 // Reduces Bootstrap 3.1 compile time from ~6.5s to ~5.6s 9012 private function HasMatches($extend, $haystackSelectorPath){ 9013 9014 if( !$extend->selector->cacheable ){ 9015 return true; 9016 } 9017 9018 $first_el = $extend->selector->_oelements[0]; 9019 9020 foreach($haystackSelectorPath as $hackstackSelector){ 9021 if( !$hackstackSelector->cacheable ){ 9022 return true; 9023 } 9024 9025 if( in_array($first_el, $hackstackSelector->_oelements) ){ 9026 return true; 9027 } 9028 } 9029 9030 return false; 9031 } 9032 9033 9034 /** 9035 * @param integer $hackstackElementIndex 9036 */ 9037 private function PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex ){ 9038 9039 9040 if( $potentialMatch['matched'] > 0 ){ 9041 9042 // selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't 9043 // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out 9044 // what the resulting combinator will be 9045 $targetCombinator = $haystackElement->combinator; 9046 if( $targetCombinator === '' && $hackstackElementIndex === 0 ){ 9047 $targetCombinator = ' '; 9048 } 9049 9050 if( $needleElements[ $potentialMatch['matched'] ]->combinator !== $targetCombinator ){ 9051 return null; 9052 } 9053 } 9054 9055 // if we don't match, null our match to indicate failure 9056 if( !$this->isElementValuesEqual( $needleElements[$potentialMatch['matched'] ]->value, $haystackElement->value) ){ 9057 return null; 9058 } 9059 9060 $potentialMatch['finished'] = false; 9061 $potentialMatch['matched']++; 9062 9063 return $potentialMatch; 9064 } 9065 9066 9067 private function isElementValuesEqual( $elementValue1, $elementValue2 ){ 9068 9069 if( $elementValue1 === $elementValue2 ){ 9070 return true; 9071 } 9072 9073 if( is_string($elementValue1) || is_string($elementValue2) ) { 9074 return false; 9075 } 9076 9077 if( $elementValue1 instanceof Less_Tree_Attribute ){ 9078 return $this->isAttributeValuesEqual( $elementValue1, $elementValue2 ); 9079 } 9080 9081 $elementValue1 = $elementValue1->value; 9082 if( $elementValue1 instanceof Less_Tree_Selector ){ 9083 return $this->isSelectorValuesEqual( $elementValue1, $elementValue2 ); 9084 } 9085 9086 return false; 9087 } 9088 9089 9090 /** 9091 * @param Less_Tree_Selector $elementValue1 9092 */ 9093 private function isSelectorValuesEqual( $elementValue1, $elementValue2 ){ 9094 9095 $elementValue2 = $elementValue2->value; 9096 if( !($elementValue2 instanceof Less_Tree_Selector) || $elementValue1->elements_len !== $elementValue2->elements_len ){ 9097 return false; 9098 } 9099 9100 for( $i = 0; $i < $elementValue1->elements_len; $i++ ){ 9101 9102 if( $elementValue1->elements[$i]->combinator !== $elementValue2->elements[$i]->combinator ){ 9103 if( $i !== 0 || ($elementValue1->elements[$i]->combinator || ' ') !== ($elementValue2->elements[$i]->combinator || ' ') ){ 9104 return false; 9105 } 9106 } 9107 9108 if( !$this->isElementValuesEqual($elementValue1->elements[$i]->value, $elementValue2->elements[$i]->value) ){ 9109 return false; 9110 } 9111 } 9112 9113 return true; 9114 } 9115 9116 9117 /** 9118 * @param Less_Tree_Attribute $elementValue1 9119 */ 9120 private function isAttributeValuesEqual( $elementValue1, $elementValue2 ){ 9121 9122 if( $elementValue1->op !== $elementValue2->op || $elementValue1->key !== $elementValue2->key ){ 9123 return false; 9124 } 9125 9126 if( !$elementValue1->value || !$elementValue2->value ){ 9127 if( $elementValue1->value || $elementValue2->value ) { 9128 return false; 9129 } 9130 return true; 9131 } 9132 9133 $elementValue1 = ($elementValue1->value->value ? $elementValue1->value->value : $elementValue1->value ); 9134 $elementValue2 = ($elementValue2->value->value ? $elementValue2->value->value : $elementValue2->value ); 9135 9136 return $elementValue1 === $elementValue2; 9137 } 9138 9139 9140 private function extendSelector($matches, $selectorPath, $replacementSelector){ 9141 9142 //for a set of matches, replace each match with the replacement selector 9143 9144 $currentSelectorPathIndex = 0; 9145 $currentSelectorPathElementIndex = 0; 9146 $path = array(); 9147 $selectorPath_len = count($selectorPath); 9148 9149 for($matchIndex = 0, $matches_len = count($matches); $matchIndex < $matches_len; $matchIndex++ ){ 9150 9151 9152 $match = $matches[$matchIndex]; 9153 $selector = $selectorPath[ $match['pathIndex'] ]; 9154 9155 $firstElement = new Less_Tree_Element( 9156 $match['initialCombinator'], 9157 $replacementSelector->elements[0]->value, 9158 $replacementSelector->elements[0]->index, 9159 $replacementSelector->elements[0]->currentFileInfo 9160 ); 9161 9162 if( $match['pathIndex'] > $currentSelectorPathIndex && $currentSelectorPathElementIndex > 0 ){ 9163 $last_path = end($path); 9164 $last_path->elements = array_merge( $last_path->elements, array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex)); 9165 $currentSelectorPathElementIndex = 0; 9166 $currentSelectorPathIndex++; 9167 } 9168 9169 $newElements = array_merge( 9170 array_slice($selector->elements, $currentSelectorPathElementIndex, ($match['index'] - $currentSelectorPathElementIndex) ) // last parameter of array_slice is different than the last parameter of javascript's slice 9171 , array($firstElement) 9172 , array_slice($replacementSelector->elements,1) 9173 ); 9174 9175 if( $currentSelectorPathIndex === $match['pathIndex'] && $matchIndex > 0 ){ 9176 $last_key = count($path)-1; 9177 $path[$last_key]->elements = array_merge($path[$last_key]->elements,$newElements); 9178 }else{ 9179 $path = array_merge( $path, array_slice( $selectorPath, $currentSelectorPathIndex, $match['pathIndex'] )); 9180 $path[] = new Less_Tree_Selector( $newElements ); 9181 } 9182 9183 $currentSelectorPathIndex = $match['endPathIndex']; 9184 $currentSelectorPathElementIndex = $match['endPathElementIndex']; 9185 if( $currentSelectorPathElementIndex >= count($selectorPath[$currentSelectorPathIndex]->elements) ){ 9186 $currentSelectorPathElementIndex = 0; 9187 $currentSelectorPathIndex++; 9188 } 9189 } 9190 9191 if( $currentSelectorPathIndex < $selectorPath_len && $currentSelectorPathElementIndex > 0 ){ 9192 $last_path = end($path); 9193 $last_path->elements = array_merge( $last_path->elements, array_slice($selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex)); 9194 $currentSelectorPathIndex++; 9195 } 9196 9197 $slice_len = $selectorPath_len - $currentSelectorPathIndex; 9198 $path = array_merge($path, array_slice($selectorPath, $currentSelectorPathIndex, $slice_len)); 9199 9200 return $path; 9201 } 9202 9203 9204 protected function visitMedia( $mediaNode ){ 9205 $newAllExtends = array_merge( $mediaNode->allExtends, end($this->allExtendsStack) ); 9206 $this->allExtendsStack[] = $this->doExtendChaining($newAllExtends, $mediaNode->allExtends); 9207 } 9208 9209 protected function visitMediaOut(){ 9210 array_pop( $this->allExtendsStack ); 9211 } 9212 9213 protected function visitDirective( $directiveNode ){ 9214 $newAllExtends = array_merge( $directiveNode->allExtends, end($this->allExtendsStack) ); 9215 $this->allExtendsStack[] = $this->doExtendChaining($newAllExtends, $directiveNode->allExtends); 9216 } 9217 9218 protected function visitDirectiveOut(){ 9219 array_pop($this->allExtendsStack); 9220 } 9221 9222 } 9223 9224 /** 9225 * toCSS Visitor 9226 * 9227 * @package Less 9228 * @subpackage visitor 9229 */ 9230 class Less_Visitor_toCSS extends Less_VisitorReplacing{ 9231 9232 private $charset; 9233 9234 public function __construct(){ 9235 parent::__construct(); 9236 } 9237 9238 /** 9239 * @param Less_Tree_Ruleset $root 9240 */ 9241 public function run( $root ){ 9242 return $this->visitObj($root); 9243 } 9244 9245 public function visitRule( $ruleNode ){ 9246 if( $ruleNode->variable ){ 9247 return array(); 9248 } 9249 return $ruleNode; 9250 } 9251 9252 public function visitMixinDefinition($mixinNode){ 9253 // mixin definitions do not get eval'd - this means they keep state 9254 // so we have to clear that state here so it isn't used if toCSS is called twice 9255 $mixinNode->frames = array(); 9256 return array(); 9257 } 9258 9259 public function visitExtend(){ 9260 return array(); 9261 } 9262 9263 public function visitComment( $commentNode ){ 9264 if( $commentNode->isSilent() ){ 9265 return array(); 9266 } 9267 return $commentNode; 9268 } 9269 9270 public function visitMedia( $mediaNode, &$visitDeeper ){ 9271 $mediaNode->accept($this); 9272 $visitDeeper = false; 9273 9274 if( !$mediaNode->rules ){ 9275 return array(); 9276 } 9277 return $mediaNode; 9278 } 9279 9280 public function visitDirective( $directiveNode ){ 9281 if( isset($directiveNode->currentFileInfo['reference']) && (!property_exists($directiveNode,'isReferenced') || !$directiveNode->isReferenced) ){ 9282 return array(); 9283 } 9284 if( $directiveNode->name === '@charset' ){ 9285 // Only output the debug info together with subsequent @charset definitions 9286 // a comment (or @media statement) before the actual @charset directive would 9287 // be considered illegal css as it has to be on the first line 9288 if( isset($this->charset) && $this->charset ){ 9289 9290 //if( $directiveNode->debugInfo ){ 9291 // $comment = new Less_Tree_Comment('/* ' . str_replace("\n",'',$directiveNode->toCSS())." */\n"); 9292 // $comment->debugInfo = $directiveNode->debugInfo; 9293 // return $this->visit($comment); 9294 //} 9295 9296 9297 return array(); 9298 } 9299 $this->charset = true; 9300 } 9301 return $directiveNode; 9302 } 9303 9304 public function checkPropertiesInRoot( $rulesetNode ){ 9305 9306 if( !$rulesetNode->firstRoot ){ 9307 return; 9308 } 9309 9310 foreach($rulesetNode->rules as $ruleNode){ 9311 if( $ruleNode instanceof Less_Tree_Rule && !$ruleNode->variable ){ 9312 $msg = "properties must be inside selector blocks, they cannot be in the root. Index ".$ruleNode->index.($ruleNode->currentFileInfo ? (' Filename: '.$ruleNode->currentFileInfo['filename']) : null); 9313 throw new Less_Exception_Compiler($msg); 9314 } 9315 } 9316 } 9317 9318 9319 public function visitRuleset( $rulesetNode, &$visitDeeper ){ 9320 9321 $visitDeeper = false; 9322 9323 $this->checkPropertiesInRoot( $rulesetNode ); 9324 9325 if( $rulesetNode->root ){ 9326 return $this->visitRulesetRoot( $rulesetNode ); 9327 } 9328 9329 $rulesets = array(); 9330 $rulesetNode->paths = $this->visitRulesetPaths($rulesetNode); 9331 9332 9333 // Compile rules and rulesets 9334 $nodeRuleCnt = count($rulesetNode->rules); 9335 for( $i = 0; $i < $nodeRuleCnt; ){ 9336 $rule = $rulesetNode->rules[$i]; 9337 9338 if( property_exists($rule,'rules') ){ 9339 // visit because we are moving them out from being a child 9340 $rulesets[] = $this->visitObj($rule); 9341 array_splice($rulesetNode->rules,$i,1); 9342 $nodeRuleCnt--; 9343 continue; 9344 } 9345 $i++; 9346 } 9347 9348 9349 // accept the visitor to remove rules and refactor itself 9350 // then we can decide now whether we want it or not 9351 if( $nodeRuleCnt > 0 ){ 9352 $rulesetNode->accept($this); 9353 9354 if( $rulesetNode->rules ){ 9355 9356 if( count($rulesetNode->rules) > 1 ){ 9357 $this->_mergeRules( $rulesetNode->rules ); 9358 $this->_removeDuplicateRules( $rulesetNode->rules ); 9359 } 9360 9361 // now decide whether we keep the ruleset 9362 if( $rulesetNode->paths ){ 9363 //array_unshift($rulesets, $rulesetNode); 9364 array_splice($rulesets,0,0,array($rulesetNode)); 9365 } 9366 } 9367 9368 } 9369 9370 9371 if( count($rulesets) === 1 ){ 9372 return $rulesets[0]; 9373 } 9374 return $rulesets; 9375 } 9376 9377 9378 /** 9379 * Helper function for visitiRuleset 9380 * 9381 * return array|Less_Tree_Ruleset 9382 */ 9383 private function visitRulesetRoot( $rulesetNode ){ 9384 $rulesetNode->accept( $this ); 9385 if( $rulesetNode->firstRoot || $rulesetNode->rules ){ 9386 return $rulesetNode; 9387 } 9388 return array(); 9389 } 9390 9391 9392 /** 9393 * Helper function for visitRuleset() 9394 * 9395 * @return array 9396 */ 9397 private function visitRulesetPaths($rulesetNode){ 9398 9399 $paths = array(); 9400 foreach($rulesetNode->paths as $p){ 9401 if( $p[0]->elements[0]->combinator === ' ' ){ 9402 $p[0]->elements[0]->combinator = ''; 9403 } 9404 9405 foreach($p as $pi){ 9406 if( $pi->getIsReferenced() && $pi->getIsOutput() ){ 9407 $paths[] = $p; 9408 break; 9409 } 9410 } 9411 } 9412 9413 return $paths; 9414 } 9415 9416 protected function _removeDuplicateRules( &$rules ){ 9417 // remove duplicates 9418 $ruleCache = array(); 9419 for( $i = count($rules)-1; $i >= 0 ; $i-- ){ 9420 $rule = $rules[$i]; 9421 if( $rule instanceof Less_Tree_Rule || $rule instanceof Less_Tree_NameValue ){ 9422 9423 if( !isset($ruleCache[$rule->name]) ){ 9424 $ruleCache[$rule->name] = $rule; 9425 }else{ 9426 $ruleList =& $ruleCache[$rule->name]; 9427 9428 if( $ruleList instanceof Less_Tree_Rule || $ruleList instanceof Less_Tree_NameValue ){ 9429 $ruleList = $ruleCache[$rule->name] = array( $ruleCache[$rule->name]->toCSS() ); 9430 } 9431 9432 $ruleCSS = $rule->toCSS(); 9433 if( array_search($ruleCSS,$ruleList) !== false ){ 9434 array_splice($rules,$i,1); 9435 }else{ 9436 $ruleList[] = $ruleCSS; 9437 } 9438 } 9439 } 9440 } 9441 } 9442 9443 protected function _mergeRules( &$rules ){ 9444 $groups = array(); 9445 9446 //obj($rules); 9447 9448 $rules_len = count($rules); 9449 for( $i = 0; $i < $rules_len; $i++ ){ 9450 $rule = $rules[$i]; 9451 9452 if( ($rule instanceof Less_Tree_Rule) && $rule->merge ){ 9453 9454 $key = $rule->name; 9455 if( $rule->important ){ 9456 $key .= ',!'; 9457 } 9458 9459 if( !isset($groups[$key]) ){ 9460 $groups[$key] = array(); 9461 }else{ 9462 array_splice($rules, $i--, 1); 9463 $rules_len--; 9464 } 9465 9466 $groups[$key][] = $rule; 9467 } 9468 } 9469 9470 9471 foreach($groups as $parts){ 9472 9473 if( count($parts) > 1 ){ 9474 $rule = $parts[0]; 9475 $spacedGroups = array(); 9476 $lastSpacedGroup = array(); 9477 $parts_mapped = array(); 9478 foreach($parts as $p){ 9479 if( $p->merge === '+' ){ 9480 if( $lastSpacedGroup ){ 9481 $spacedGroups[] = self::toExpression($lastSpacedGroup); 9482 } 9483 $lastSpacedGroup = array(); 9484 } 9485 $lastSpacedGroup[] = $p; 9486 } 9487 9488 $spacedGroups[] = self::toExpression($lastSpacedGroup); 9489 $rule->value = self::toValue($spacedGroups); 9490 } 9491 } 9492 9493 } 9494 9495 public static function toExpression($values){ 9496 $mapped = array(); 9497 foreach($values as $p){ 9498 $mapped[] = $p->value; 9499 } 9500 return new Less_Tree_Expression( $mapped ); 9501 } 9502 9503 public static function toValue($values){ 9504 //return new Less_Tree_Value($values); ?? 9505 9506 $mapped = array(); 9507 foreach($values as $p){ 9508 $mapped[] = $p; 9509 } 9510 return new Less_Tree_Value($mapped); 9511 } 9512 } 9513 9514 9515 9516 /** 9517 * Parser Exception 9518 * 9519 * @package Less 9520 * @subpackage exception 9521 */ 9522 class Less_Exception_Parser extends Exception{ 9523 9524 /** 9525 * The current file 9526 * 9527 * @var Less_ImportedFile 9528 */ 9529 public $currentFile; 9530 9531 /** 9532 * The current parser index 9533 * 9534 * @var integer 9535 */ 9536 public $index; 9537 9538 protected $input; 9539 9540 protected $details = array(); 9541 9542 9543 /** 9544 * Constructor 9545 * 9546 * @param string $message 9547 * @param Exception $previous Previous exception 9548 * @param integer $index The current parser index 9549 * @param Less_FileInfo|string $currentFile The file 9550 * @param integer $code The exception code 9551 */ 9552 public function __construct($message = null, Exception $previous = null, $index = null, $currentFile = null, $code = 0){ 9553 9554 if (PHP_VERSION_ID < 50300) { 9555 $this->previous = $previous; 9556 parent::__construct($message, $code); 9557 } else { 9558 parent::__construct($message, $code, $previous); 9559 } 9560 9561 $this->currentFile = $currentFile; 9562 $this->index = $index; 9563 9564 $this->genMessage(); 9565 } 9566 9567 9568 protected function getInput(){ 9569 9570 if( !$this->input && $this->currentFile && $this->currentFile['filename'] && file_exists($this->currentFile['filename']) ){ 9571 $this->input = file_get_contents( $this->currentFile['filename'] ); 9572 } 9573 } 9574 9575 9576 9577 /** 9578 * Converts the exception to string 9579 * 9580 * @return string 9581 */ 9582 public function genMessage(){ 9583 9584 if( $this->currentFile && $this->currentFile['filename'] ){ 9585 $this->message .= ' in '.basename($this->currentFile['filename']); 9586 } 9587 9588 if( $this->index !== null ){ 9589 $this->getInput(); 9590 if( $this->input ){ 9591 $line = self::getLineNumber(); 9592 $this->message .= ' on line '.$line.', column '.self::getColumn(); 9593 9594 $lines = explode("\n",$this->input); 9595 9596 $count = count($lines); 9597 $start_line = max(0, $line-3); 9598 $last_line = min($count, $start_line+6); 9599 $num_len = strlen($last_line); 9600 for( $i = $start_line; $i < $last_line; $i++ ){ 9601 $this->message .= "\n".str_pad($i+1,$num_len,'0',STR_PAD_LEFT).'| '.$lines[$i]; 9602 } 9603 } 9604 } 9605 9606 } 9607 9608 /** 9609 * Returns the line number the error was encountered 9610 * 9611 * @return integer 9612 */ 9613 public function getLineNumber(){ 9614 if( $this->index ){ 9615 // https://bugs.php.net/bug.php?id=49790 9616 if (ini_get("mbstring.func_overload")) { 9617 return substr_count(substr($this->input, 0, $this->index), "\n") + 1; 9618 } else { 9619 return substr_count($this->input, "\n", 0, $this->index) + 1; 9620 } 9621 } 9622 return 1; 9623 } 9624 9625 9626 /** 9627 * Returns the column the error was encountered 9628 * 9629 * @return integer 9630 */ 9631 public function getColumn(){ 9632 9633 $part = substr($this->input, 0, $this->index); 9634 $pos = strrpos($part,"\n"); 9635 return $this->index - $pos; 9636 } 9637 9638 } 9639 9640 9641 /** 9642 * Chunk Exception 9643 * 9644 * @package Less 9645 * @subpackage exception 9646 */ 9647 class Less_Exception_Chunk extends Less_Exception_Parser{ 9648 9649 9650 protected $parserCurrentIndex = 0; 9651 9652 protected $emitFrom = 0; 9653 9654 protected $input_len; 9655 9656 9657 /** 9658 * Constructor 9659 * 9660 * @param string $input 9661 * @param Exception $previous Previous exception 9662 * @param integer $index The current parser index 9663 * @param Less_FileInfo|string $currentFile The file 9664 * @param integer $code The exception code 9665 */ 9666 public function __construct($input, Exception $previous = null, $index = null, $currentFile = null, $code = 0){ 9667 9668 $this->message = 'ParseError: Unexpected input'; //default message 9669 9670 $this->index = $index; 9671 9672 $this->currentFile = $currentFile; 9673 9674 $this->input = $input; 9675 $this->input_len = strlen($input); 9676 9677 $this->Chunks(); 9678 $this->genMessage(); 9679 } 9680 9681 9682 /** 9683 * See less.js chunks() 9684 * We don't actually need the chunks 9685 * 9686 */ 9687 protected function Chunks(){ 9688 $level = 0; 9689 $parenLevel = 0; 9690 $lastMultiCommentEndBrace = null; 9691 $lastOpening = null; 9692 $lastMultiComment = null; 9693 $lastParen = null; 9694 9695 for( $this->parserCurrentIndex = 0; $this->parserCurrentIndex < $this->input_len; $this->parserCurrentIndex++ ){ 9696 $cc = $this->CharCode($this->parserCurrentIndex); 9697 if ((($cc >= 97) && ($cc <= 122)) || ($cc < 34)) { 9698 // a-z or whitespace 9699 continue; 9700 } 9701 9702 switch ($cc) { 9703 9704 // ( 9705 case 40: 9706 $parenLevel++; 9707 $lastParen = $this->parserCurrentIndex; 9708 break; 9709 9710 // ) 9711 case 41: 9712 $parenLevel--; 9713 if( $parenLevel < 0 ){ 9714 return $this->fail("missing opening `(`"); 9715 } 9716 break; 9717 9718 // ; 9719 case 59: 9720 //if (!$parenLevel) { $this->emitChunk(); } 9721 break; 9722 9723 // { 9724 case 123: 9725 $level++; 9726 $lastOpening = $this->parserCurrentIndex; 9727 break; 9728 9729 // } 9730 case 125: 9731 $level--; 9732 if( $level < 0 ){ 9733 return $this->fail("missing opening `{`"); 9734 9735 } 9736 //if (!$level && !$parenLevel) { $this->emitChunk(); } 9737 break; 9738 // \ 9739 case 92: 9740 if ($this->parserCurrentIndex < $this->input_len - 1) { $this->parserCurrentIndex++; break; } 9741 return $this->fail("unescaped `\\`"); 9742 9743 // ", ' and ` 9744 case 34: 9745 case 39: 9746 case 96: 9747 $matched = 0; 9748 $currentChunkStartIndex = $this->parserCurrentIndex; 9749 for ($this->parserCurrentIndex = $this->parserCurrentIndex + 1; $this->parserCurrentIndex < $this->input_len; $this->parserCurrentIndex++) { 9750 $cc2 = $this->CharCode($this->parserCurrentIndex); 9751 if ($cc2 > 96) { continue; } 9752 if ($cc2 == $cc) { $matched = 1; break; } 9753 if ($cc2 == 92) { // \ 9754 if ($this->parserCurrentIndex == $this->input_len - 1) { 9755 return $this->fail("unescaped `\\`"); 9756 } 9757 $this->parserCurrentIndex++; 9758 } 9759 } 9760 if ($matched) { break; } 9761 return $this->fail("unmatched `" . chr($cc) . "`", $currentChunkStartIndex); 9762 9763 // /, check for comment 9764 case 47: 9765 if ($parenLevel || ($this->parserCurrentIndex == $this->input_len - 1)) { break; } 9766 $cc2 = $this->CharCode($this->parserCurrentIndex+1); 9767 if ($cc2 == 47) { 9768 // //, find lnfeed 9769 for ($this->parserCurrentIndex = $this->parserCurrentIndex + 2; $this->parserCurrentIndex < $this->input_len; $this->parserCurrentIndex++) { 9770 $cc2 = $this->CharCode($this->parserCurrentIndex); 9771 if (($cc2 <= 13) && (($cc2 == 10) || ($cc2 == 13))) { break; } 9772 } 9773 } else if ($cc2 == 42) { 9774 // /*, find */ 9775 $lastMultiComment = $currentChunkStartIndex = $this->parserCurrentIndex; 9776 for ($this->parserCurrentIndex = $this->parserCurrentIndex + 2; $this->parserCurrentIndex < $this->input_len - 1; $this->parserCurrentIndex++) { 9777 $cc2 = $this->CharCode($this->parserCurrentIndex); 9778 if ($cc2 == 125) { $lastMultiCommentEndBrace = $this->parserCurrentIndex; } 9779 if ($cc2 != 42) { continue; } 9780 if ($this->CharCode($this->parserCurrentIndex+1) == 47) { break; } 9781 } 9782 if ($this->parserCurrentIndex == $this->input_len - 1) { 9783 return $this->fail("missing closing `*/`", $currentChunkStartIndex); 9784 } 9785 } 9786 break; 9787 9788 // *, check for unmatched */ 9789 case 42: 9790 if (($this->parserCurrentIndex < $this->input_len - 1) && ($this->CharCode($this->parserCurrentIndex+1) == 47)) { 9791 return $this->fail("unmatched `/*`"); 9792 } 9793 break; 9794 } 9795 } 9796 9797 if( $level !== 0 ){ 9798 if( ($lastMultiComment > $lastOpening) && ($lastMultiCommentEndBrace > $lastMultiComment) ){ 9799 return $this->fail("missing closing `}` or `*/`", $lastOpening); 9800 } else { 9801 return $this->fail("missing closing `}`", $lastOpening); 9802 } 9803 } else if ( $parenLevel !== 0 ){ 9804 return $this->fail("missing closing `)`", $lastParen); 9805 } 9806 9807 9808 //chunk didn't fail 9809 9810 9811 //$this->emitChunk(true); 9812 } 9813 9814 public function CharCode($pos){ 9815 return ord($this->input[$pos]); 9816 } 9817 9818 9819 public function fail( $msg, $index = null ){ 9820 9821 if( !$index ){ 9822 $this->index = $this->parserCurrentIndex; 9823 }else{ 9824 $this->index = $index; 9825 } 9826 $this->message = 'ParseError: '.$msg; 9827 } 9828 9829 9830 /* 9831 function emitChunk( $force = false ){ 9832 $len = $this->parserCurrentIndex - $this->emitFrom; 9833 if ((($len < 512) && !$force) || !$len) { 9834 return; 9835 } 9836 $chunks[] = substr($this->input, $this->emitFrom, $this->parserCurrentIndex + 1 - $this->emitFrom ); 9837 $this->emitFrom = $this->parserCurrentIndex + 1; 9838 } 9839 */ 9840 9841 } 9842 9843 9844 /** 9845 * Compiler Exception 9846 * 9847 * @package Less 9848 * @subpackage exception 9849 */ 9850 class Less_Exception_Compiler extends Less_Exception_Parser{ 9851 9852 } 9853 9854 /** 9855 * Parser output with source map 9856 * 9857 * @package Less 9858 * @subpackage Output 9859 */ 9860 class Less_Output_Mapped extends Less_Output { 9861 9862 /** 9863 * The source map generator 9864 * 9865 * @var Less_SourceMap_Generator 9866 */ 9867 protected $generator; 9868 9869 /** 9870 * Current line 9871 * 9872 * @var integer 9873 */ 9874 protected $lineNumber = 0; 9875 9876 /** 9877 * Current column 9878 * 9879 * @var integer 9880 */ 9881 protected $column = 0; 9882 9883 /** 9884 * Array of contents map (file and its content) 9885 * 9886 * @var array 9887 */ 9888 protected $contentsMap = array(); 9889 9890 /** 9891 * Constructor 9892 * 9893 * @param array $contentsMap Array of filename to contents map 9894 * @param Less_SourceMap_Generator $generator 9895 */ 9896 public function __construct(array $contentsMap, $generator){ 9897 $this->contentsMap = $contentsMap; 9898 $this->generator = $generator; 9899 } 9900 9901 /** 9902 * Adds a chunk to the stack 9903 * The $index for less.php may be different from less.js since less.php does not chunkify inputs 9904 * 9905 * @param string $chunk 9906 * @param string $fileInfo 9907 * @param integer $index 9908 * @param mixed $mapLines 9909 */ 9910 public function add($chunk, $fileInfo = null, $index = 0, $mapLines = null){ 9911 9912 //ignore adding empty strings 9913 if( $chunk === '' ){ 9914 return; 9915 } 9916 9917 9918 $sourceLines = array(); 9919 $sourceColumns = ' '; 9920 9921 9922 if( $fileInfo ){ 9923 9924 $url = $fileInfo['currentUri']; 9925 9926 if( isset($this->contentsMap[$url]) ){ 9927 $inputSource = substr($this->contentsMap[$url], 0, $index); 9928 $sourceLines = explode("\n", $inputSource); 9929 $sourceColumns = end($sourceLines); 9930 }else{ 9931 throw new Exception('Filename '.$url.' not in contentsMap'); 9932 } 9933 9934 } 9935 9936 $lines = explode("\n", $chunk); 9937 $columns = end($lines); 9938 9939 if($fileInfo){ 9940 9941 if(!$mapLines){ 9942 $this->generator->addMapping( 9943 $this->lineNumber + 1, // generated_line 9944 $this->column, // generated_column 9945 count($sourceLines), // original_line 9946 strlen($sourceColumns), // original_column 9947 $fileInfo 9948 ); 9949 }else{ 9950 for($i = 0, $count = count($lines); $i < $count; $i++){ 9951 $this->generator->addMapping( 9952 $this->lineNumber + $i + 1, // generated_line 9953 $i === 0 ? $this->column : 0, // generated_column 9954 count($sourceLines) + $i, // original_line 9955 $i === 0 ? strlen($sourceColumns) : 0, // original_column 9956 $fileInfo 9957 ); 9958 } 9959 } 9960 } 9961 9962 if(count($lines) === 1){ 9963 $this->column += strlen($columns); 9964 }else{ 9965 $this->lineNumber += count($lines) - 1; 9966 $this->column = strlen($columns); 9967 } 9968 9969 // add only chunk 9970 parent::add($chunk); 9971 } 9972 9973 } 9974 9975 /** 9976 * Encode / Decode Base64 VLQ. 9977 * 9978 * @package Less 9979 * @subpackage SourceMap 9980 */ 9981 class Less_SourceMap_Base64VLQ { 9982 9983 /** 9984 * Shift 9985 * 9986 * @var integer 9987 */ 9988 private $shift = 5; 9989 9990 /** 9991 * Mask 9992 * 9993 * @var integer 9994 */ 9995 private $mask = 0x1F; // == (1 << shift) == 0b00011111 9996 9997 /** 9998 * Continuation bit 9999 * 10000 * @var integer 10001 */ 10002 private $continuationBit = 0x20; // == (mask - 1 ) == 0b00100000 10003 10004 /** 10005 * Char to integer map 10006 * 10007 * @var array 10008 */ 10009 private $charToIntMap = array( 10010 'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, 10011 'H' => 7,'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, 10012 'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, 10013 'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25, 'a' => 26, 'b' => 27, 10014 'c' => 28, 'd' => 29, 'e' => 30, 'f' => 31, 'g' => 32, 'h' => 33, 'i' => 34, 10015 'j' => 35, 'k' => 36, 'l' => 37, 'm' => 38, 'n' => 39, 'o' => 40, 'p' => 41, 10016 'q' => 42, 'r' => 43, 's' => 44, 't' => 45, 'u' => 46, 'v' => 47, 'w' => 48, 10017 'x' => 49, 'y' => 50, 'z' => 51, 0 => 52, 1 => 53, 2 => 54, 3 => 55, 4 => 56, 10018 5 => 57, 6 => 58, 7 => 59, 8 => 60, 9 => 61, '+' => 62, '/' => 63, 10019 ); 10020 10021 /** 10022 * Integer to char map 10023 * 10024 * @var array 10025 */ 10026 private $intToCharMap = array( 10027 0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D', 4 => 'E', 5 => 'F', 6 => 'G', 10028 7 => 'H', 8 => 'I', 9 => 'J', 10 => 'K', 11 => 'L', 12 => 'M', 13 => 'N', 10029 14 => 'O', 15 => 'P', 16 => 'Q', 17 => 'R', 18 => 'S', 19 => 'T', 20 => 'U', 10030 21 => 'V', 22 => 'W', 23 => 'X', 24 => 'Y', 25 => 'Z', 26 => 'a', 27 => 'b', 10031 28 => 'c', 29 => 'd', 30 => 'e', 31 => 'f', 32 => 'g', 33 => 'h', 34 => 'i', 10032 35 => 'j', 36 => 'k', 37 => 'l', 38 => 'm', 39 => 'n', 40 => 'o', 41 => 'p', 10033 42 => 'q', 43 => 'r', 44 => 's', 45 => 't', 46 => 'u', 47 => 'v', 48 => 'w', 10034 49 => 'x', 50 => 'y', 51 => 'z', 52 => '0', 53 => '1', 54 => '2', 55 => '3', 10035 56 => '4', 57 => '5', 58 => '6', 59 => '7', 60 => '8', 61 => '9', 62 => '+', 10036 63 => '/', 10037 ); 10038 10039 /** 10040 * Constructor 10041 */ 10042 public function __construct(){ 10043 // I leave it here for future reference 10044 // foreach(str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') as $i => $char) 10045 // { 10046 // $this->charToIntMap[$char] = $i; 10047 // $this->intToCharMap[$i] = $char; 10048 // } 10049 } 10050 10051 /** 10052 * Convert from a two-complement value to a value where the sign bit is 10053 * is placed in the least significant bit. For example, as decimals: 10054 * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary) 10055 * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary) 10056 * We generate the value for 32 bit machines, hence -2147483648 becomes 1, not 4294967297, 10057 * even on a 64 bit machine. 10058 * @param string $aValue 10059 */ 10060 public function toVLQSigned($aValue){ 10061 return 0xffffffff & ($aValue < 0 ? ((-$aValue) << 1) + 1 : ($aValue << 1) + 0); 10062 } 10063 10064 /** 10065 * Convert to a two-complement value from a value where the sign bit is 10066 * is placed in the least significant bit. For example, as decimals: 10067 * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1 10068 * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2 10069 * We assume that the value was generated with a 32 bit machine in mind. 10070 * Hence 10071 * 1 becomes -2147483648 10072 * even on a 64 bit machine. 10073 * @param integer $aValue 10074 */ 10075 public function fromVLQSigned($aValue){ 10076 return $aValue & 1 ? $this->zeroFill(~$aValue + 2, 1) | (-1 - 0x7fffffff) : $this->zeroFill($aValue, 1); 10077 } 10078 10079 /** 10080 * Return the base 64 VLQ encoded value. 10081 * 10082 * @param string $aValue The value to encode 10083 * @return string The encoded value 10084 */ 10085 public function encode($aValue){ 10086 $encoded = ''; 10087 $vlq = $this->toVLQSigned($aValue); 10088 do 10089 { 10090 $digit = $vlq & $this->mask; 10091 $vlq = $this->zeroFill($vlq, $this->shift); 10092 if($vlq > 0){ 10093 $digit |= $this->continuationBit; 10094 } 10095 $encoded .= $this->base64Encode($digit); 10096 } while($vlq > 0); 10097 10098 return $encoded; 10099 } 10100 10101 /** 10102 * Return the value decoded from base 64 VLQ. 10103 * 10104 * @param string $encoded The encoded value to decode 10105 * @return integer The decoded value 10106 */ 10107 public function decode($encoded){ 10108 $vlq = 0; 10109 $i = 0; 10110 do 10111 { 10112 $digit = $this->base64Decode($encoded[$i]); 10113 $vlq |= ($digit & $this->mask) << ($i * $this->shift); 10114 $i++; 10115 } while($digit & $this->continuationBit); 10116 10117 return $this->fromVLQSigned($vlq); 10118 } 10119 10120 /** 10121 * Right shift with zero fill. 10122 * 10123 * @param integer $a number to shift 10124 * @param integer $b number of bits to shift 10125 * @return integer 10126 */ 10127 public function zeroFill($a, $b){ 10128 return ($a >= 0) ? ($a >> $b) : ($a >> $b) & (PHP_INT_MAX >> ($b - 1)); 10129 } 10130 10131 /** 10132 * Encode single 6-bit digit as base64. 10133 * 10134 * @param integer $number 10135 * @return string 10136 * @throws Exception If the number is invalid 10137 */ 10138 public function base64Encode($number){ 10139 if($number < 0 || $number > 63){ 10140 throw new Exception(sprintf('Invalid number "%s" given. Must be between 0 and 63.', $number)); 10141 } 10142 return $this->intToCharMap[$number]; 10143 } 10144 10145 /** 10146 * Decode single 6-bit digit from base64 10147 * 10148 * @param string $char 10149 * @return number 10150 * @throws Exception If the number is invalid 10151 */ 10152 public function base64Decode($char){ 10153 if(!array_key_exists($char, $this->charToIntMap)){ 10154 throw new Exception(sprintf('Invalid base 64 digit "%s" given.', $char)); 10155 } 10156 return $this->charToIntMap[$char]; 10157 } 10158 10159 } 10160 10161 10162 /** 10163 * Source map generator 10164 * 10165 * @package Less 10166 * @subpackage Output 10167 */ 10168 class Less_SourceMap_Generator extends Less_Configurable { 10169 10170 /** 10171 * What version of source map does the generator generate? 10172 */ 10173 const VERSION = 3; 10174 10175 /** 10176 * Array of default options 10177 * 10178 * @var array 10179 */ 10180 protected $defaultOptions = array( 10181 // an optional source root, useful for relocating source files 10182 // on a server or removing repeated values in the 'sources' entry. 10183 // This value is prepended to the individual entries in the 'source' field. 10184 'sourceRoot' => '', 10185 10186 // an optional name of the generated code that this source map is associated with. 10187 'sourceMapFilename' => null, 10188 10189 // url of the map 10190 'sourceMapURL' => null, 10191 10192 // absolute path to a file to write the map to 10193 'sourceMapWriteTo' => null, 10194 10195 // output source contents? 10196 'outputSourceFiles' => false, 10197 10198 // base path for filename normalization 10199 'sourceMapRootpath' => '', 10200 10201 // base path for filename normalization 10202 'sourceMapBasepath' => '' 10203 ); 10204 10205 /** 10206 * The base64 VLQ encoder 10207 * 10208 * @var Less_SourceMap_Base64VLQ 10209 */ 10210 protected $encoder; 10211 10212 /** 10213 * Array of mappings 10214 * 10215 * @var array 10216 */ 10217 protected $mappings = array(); 10218 10219 /** 10220 * The root node 10221 * 10222 * @var Less_Tree_Ruleset 10223 */ 10224 protected $root; 10225 10226 /** 10227 * Array of contents map 10228 * 10229 * @var array 10230 */ 10231 protected $contentsMap = array(); 10232 10233 /** 10234 * File to content map 10235 * 10236 * @var array 10237 */ 10238 protected $sources = array(); 10239 protected $source_keys = array(); 10240 10241 /** 10242 * Constructor 10243 * 10244 * @param Less_Tree_Ruleset $root The root node 10245 * @param array $options Array of options 10246 */ 10247 public function __construct(Less_Tree_Ruleset $root, $contentsMap, $options = array()){ 10248 $this->root = $root; 10249 $this->contentsMap = $contentsMap; 10250 $this->encoder = new Less_SourceMap_Base64VLQ(); 10251 10252 $this->SetOptions($options); 10253 10254 $this->options['sourceMapRootpath'] = $this->fixWindowsPath($this->options['sourceMapRootpath'], true); 10255 $this->options['sourceMapBasepath'] = $this->fixWindowsPath($this->options['sourceMapBasepath'], true); 10256 } 10257 10258 /** 10259 * Generates the CSS 10260 * 10261 * @return string 10262 */ 10263 public function generateCSS(){ 10264 $output = new Less_Output_Mapped($this->contentsMap, $this); 10265 10266 // catch the output 10267 $this->root->genCSS($output); 10268 10269 10270 $sourceMapUrl = $this->getOption('sourceMapURL'); 10271 $sourceMapFilename = $this->getOption('sourceMapFilename'); 10272 $sourceMapContent = $this->generateJson(); 10273 $sourceMapWriteTo = $this->getOption('sourceMapWriteTo'); 10274 10275 if( !$sourceMapUrl && $sourceMapFilename ){ 10276 $sourceMapUrl = $this->normalizeFilename($sourceMapFilename); 10277 } 10278 10279 // write map to a file 10280 if( $sourceMapWriteTo ){ 10281 $this->saveMap($sourceMapWriteTo, $sourceMapContent); 10282 } 10283 10284 // inline the map 10285 if( !$sourceMapUrl ){ 10286 $sourceMapUrl = sprintf('data:application/json,%s', Less_Functions::encodeURIComponent($sourceMapContent)); 10287 } 10288 10289 if( $sourceMapUrl ){ 10290 $output->add( sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl) ); 10291 } 10292 10293 return $output->toString(); 10294 } 10295 10296 /** 10297 * Saves the source map to a file 10298 * 10299 * @param string $file The absolute path to a file 10300 * @param string $content The content to write 10301 * @throws Exception If the file could not be saved 10302 */ 10303 protected function saveMap($file, $content){ 10304 $dir = dirname($file); 10305 // directory does not exist 10306 if( !is_dir($dir) ){ 10307 // FIXME: create the dir automatically? 10308 throw new Exception(sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir)); 10309 } 10310 // FIXME: proper saving, with dir write check! 10311 if(file_put_contents($file, $content) === false){ 10312 throw new Exception(sprintf('Cannot save the source map to "%s"', $file)); 10313 } 10314 return true; 10315 } 10316 10317 /** 10318 * Normalizes the filename 10319 * 10320 * @param string $filename 10321 * @return string 10322 */ 10323 protected function normalizeFilename($filename){ 10324 10325 $filename = $this->fixWindowsPath($filename); 10326 10327 $rootpath = $this->getOption('sourceMapRootpath'); 10328 $basePath = $this->getOption('sourceMapBasepath'); 10329 10330 // "Trim" the 'sourceMapBasepath' from the output filename. 10331 //if (strpos($filename, strval($basePath)) === 0) { 10332 // $filename = substr($filename, strlen($basePath)); 10333 //} 10334 10335 // Remove extra leading path separators. 10336 if(strpos($filename, '\\') === 0 || strpos($filename, '/') === 0){ 10337 $filename = substr($filename, 1); 10338 } 10339 10340 return $rootpath . $filename; 10341 } 10342 10343 /** 10344 * Adds a mapping 10345 * 10346 * @param integer $generatedLine The line number in generated file 10347 * @param integer $generatedColumn The column number in generated file 10348 * @param integer $originalLine The line number in original file 10349 * @param integer $originalColumn The column number in original file 10350 * @param string $sourceFile The original source file 10351 */ 10352 public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $fileInfo ){ 10353 10354 $this->mappings[] = array( 10355 'generated_line' => $generatedLine, 10356 'generated_column' => $generatedColumn, 10357 'original_line' => $originalLine, 10358 'original_column' => $originalColumn, 10359 'source_file' => $fileInfo['currentUri'] 10360 ); 10361 10362 $this->sources[$fileInfo['currentUri']] = $fileInfo['filename']; 10363 } 10364 10365 10366 /** 10367 * Generates the JSON source map 10368 * 10369 * @return string 10370 * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit# 10371 */ 10372 protected function generateJson(){ 10373 10374 $sourceMap = array(); 10375 $mappings = $this->generateMappings(); 10376 10377 // File version (always the first entry in the object) and must be a positive integer. 10378 $sourceMap['version'] = self::VERSION; 10379 10380 10381 // An optional name of the generated code that this source map is associated with. 10382 $file = $this->getOption('sourceMapFilename'); 10383 if( $file ){ 10384 $sourceMap['file'] = $file; 10385 } 10386 10387 10388 // An optional source root, useful for relocating source files on a server or removing repeated values in the 'sources' entry. This value is prepended to the individual entries in the 'source' field. 10389 $root = $this->getOption('sourceRoot'); 10390 if( $root ){ 10391 $sourceMap['sourceRoot'] = $root; 10392 } 10393 10394 10395 // A list of original sources used by the 'mappings' entry. 10396 $sourceMap['sources'] = array(); 10397 foreach($this->sources as $source_uri => $source_filename){ 10398 $sourceMap['sources'][] = $this->normalizeFilename($source_filename); 10399 } 10400 10401 10402 // A list of symbol names used by the 'mappings' entry. 10403 $sourceMap['names'] = array(); 10404 10405 // A string with the encoded mapping data. 10406 $sourceMap['mappings'] = $mappings; 10407 10408 if( $this->getOption('outputSourceFiles') ){ 10409 // An optional list of source content, useful when the 'source' can't be hosted. 10410 // The contents are listed in the same order as the sources above. 10411 // 'null' may be used if some original sources should be retrieved by name. 10412 $sourceMap['sourcesContent'] = $this->getSourcesContent(); 10413 } 10414 10415 // less.js compat fixes 10416 if( count($sourceMap['sources']) && empty($sourceMap['sourceRoot']) ){ 10417 unset($sourceMap['sourceRoot']); 10418 } 10419 10420 return json_encode($sourceMap); 10421 } 10422 10423 /** 10424 * Returns the sources contents 10425 * 10426 * @return array|null 10427 */ 10428 protected function getSourcesContent(){ 10429 if(empty($this->sources)){ 10430 return; 10431 } 10432 $content = array(); 10433 foreach($this->sources as $sourceFile){ 10434 $content[] = file_get_contents($sourceFile); 10435 } 10436 return $content; 10437 } 10438 10439 /** 10440 * Generates the mappings string 10441 * 10442 * @return string 10443 */ 10444 public function generateMappings(){ 10445 10446 if( !count($this->mappings) ){ 10447 return ''; 10448 } 10449 10450 $this->source_keys = array_flip(array_keys($this->sources)); 10451 10452 10453 // group mappings by generated line number. 10454 $groupedMap = $groupedMapEncoded = array(); 10455 foreach($this->mappings as $m){ 10456 $groupedMap[$m['generated_line']][] = $m; 10457 } 10458 ksort($groupedMap); 10459 10460 $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0; 10461 10462 foreach($groupedMap as $lineNumber => $line_map){ 10463 while(++$lastGeneratedLine < $lineNumber){ 10464 $groupedMapEncoded[] = ';'; 10465 } 10466 10467 $lineMapEncoded = array(); 10468 $lastGeneratedColumn = 0; 10469 10470 foreach($line_map as $m){ 10471 $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn); 10472 $lastGeneratedColumn = $m['generated_column']; 10473 10474 // find the index 10475 if( $m['source_file'] ){ 10476 $index = $this->findFileIndex($m['source_file']); 10477 if( $index !== false ){ 10478 $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex); 10479 $lastOriginalIndex = $index; 10480 10481 // lines are stored 0-based in SourceMap spec version 3 10482 $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine); 10483 $lastOriginalLine = $m['original_line'] - 1; 10484 10485 $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn); 10486 $lastOriginalColumn = $m['original_column']; 10487 } 10488 } 10489 10490 $lineMapEncoded[] = $mapEncoded; 10491 } 10492 10493 $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';'; 10494 } 10495 10496 return rtrim(implode($groupedMapEncoded), ';'); 10497 } 10498 10499 /** 10500 * Finds the index for the filename 10501 * 10502 * @param string $filename 10503 * @return integer|false 10504 */ 10505 protected function findFileIndex($filename){ 10506 return $this->source_keys[$filename]; 10507 } 10508 10509 /** 10510 * fix windows paths 10511 * @param string $path 10512 * @return string 10513 */ 10514 public function fixWindowsPath($path, $addEndSlash = false){ 10515 $slash = ($addEndSlash) ? '/' : ''; 10516 if( !empty($path) ){ 10517 $path = str_replace('\\', '/', $path); 10518 $path = rtrim($path,'/') . $slash; 10519 } 10520 10521 return $path; 10522 } 10523 10524 }