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