[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/lessphp/ -> Parser.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1