File indexing completed on 2024-12-22 05:36:22
0001 <?php 0002 0003 /** 0004 * lessphp v0.4.0 0005 * http://leafo.net/lessphp 0006 * 0007 * LESS css compiler, adapted from http://lesscss.org 0008 * 0009 * Copyright 2012, Leaf Corcoran <leafot@gmail.com> 0010 * Licensed under MIT or GPLv3, see LICENSE 0011 */ 0012 0013 0014 /** 0015 * The less compiler and parser. 0016 * 0017 * Converting LESS to CSS is a three stage process. The incoming file is parsed 0018 * by `lessc_parser` into a syntax tree, then it is compiled into another tree 0019 * representing the CSS structure by `lessc`. The CSS tree is fed into a 0020 * formatter, like `lessc_formatter` which then outputs CSS as a string. 0021 * 0022 * During the first compile, all values are *reduced*, which means that their 0023 * types are brought to the lowest form before being dump as strings. This 0024 * handles math equations, variable dereferences, and the like. 0025 * 0026 * The `parse` function of `lessc` is the entry point. 0027 * 0028 * In summary: 0029 * 0030 * The `lessc` class creates an intstance of the parser, feeds it LESS code, 0031 * then transforms the resulting tree to a CSS tree. This class also holds the 0032 * evaluation context, such as all available mixins and variables at any given 0033 * time. 0034 * 0035 * The `lessc_parser` class is only concerned with parsing its input. 0036 * 0037 * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string, 0038 * handling things like indentation. 0039 */ 0040 class lessc { 0041 static public $VERSION = "v0.4.0"; 0042 static protected $TRUE = array("keyword", "true"); 0043 static protected $FALSE = array("keyword", "false"); 0044 0045 protected $libFunctions = array(); 0046 protected $registeredVars = array(); 0047 protected $preserveComments = false; 0048 0049 public $vPrefix = '@'; // prefix of abstract properties 0050 public $mPrefix = '$'; // prefix of abstract blocks 0051 public $parentSelector = '&'; 0052 0053 public $importDisabled = false; 0054 public $importDir = ''; 0055 0056 protected $numberPrecision = null; 0057 0058 protected $allParsedFiles = array(); 0059 0060 // set to the parser that generated the current line when compiling 0061 // so we know how to create error messages 0062 protected $sourceParser = null; 0063 protected $sourceLoc = null; 0064 0065 static public $defaultValue = array("keyword", ""); 0066 0067 static protected $nextImportId = 0; // uniquely identify imports 0068 0069 // attempts to find the path of an import url, returns null for css files 0070 protected function findImport($url) { 0071 foreach ((array)$this->importDir as $dir) { 0072 $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url; 0073 if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) { 0074 return $file; 0075 } 0076 } 0077 0078 return null; 0079 } 0080 0081 protected function fileExists($name) { 0082 return is_file($name); 0083 } 0084 0085 static public function compressList($items, $delim) { 0086 if (!isset($items[1]) && isset($items[0])) return $items[0]; 0087 else return array('list', $delim, $items); 0088 } 0089 0090 static public function preg_quote($what) { 0091 return preg_quote($what, '/'); 0092 } 0093 0094 protected function tryImport($importPath, $parentBlock, $out) { 0095 if ($importPath[0] == "function" && $importPath[1] == "url") { 0096 $importPath = $this->flattenList($importPath[2]); 0097 } 0098 0099 $str = $this->coerceString($importPath); 0100 if ($str === null) return false; 0101 0102 $url = $this->compileValue($this->lib_e($str)); 0103 0104 // don't import if it ends in css 0105 if (substr_compare($url, '.css', -4, 4) === 0) return false; 0106 0107 $realPath = $this->findImport($url); 0108 0109 if ($realPath === null) return false; 0110 0111 if ($this->importDisabled) { 0112 return array(false, "/* import disabled */"); 0113 } 0114 0115 if (isset($this->allParsedFiles[realpath($realPath)])) { 0116 return array(false, null); 0117 } 0118 0119 $this->addParsedFile($realPath); 0120 $parser = $this->makeParser($realPath); 0121 $root = $parser->parse(file_get_contents($realPath)); 0122 0123 // set the parents of all the block props 0124 foreach ($root->props as $prop) { 0125 if ($prop[0] == "block") { 0126 $prop[1]->parent = $parentBlock; 0127 } 0128 } 0129 0130 // copy mixins into scope, set their parents 0131 // bring blocks from import into current block 0132 // TODO: need to mark the source parser these came from this file 0133 foreach ($root->children as $childName => $child) { 0134 if (isset($parentBlock->children[$childName])) { 0135 $parentBlock->children[$childName] = array_merge( 0136 $parentBlock->children[$childName], 0137 $child); 0138 } else { 0139 $parentBlock->children[$childName] = $child; 0140 } 0141 } 0142 0143 $pi = pathinfo($realPath); 0144 $dir = $pi["dirname"]; 0145 0146 list($top, $bottom) = $this->sortProps($root->props, true); 0147 $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir); 0148 0149 return array(true, $bottom, $parser, $dir); 0150 } 0151 0152 protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) { 0153 $oldSourceParser = $this->sourceParser; 0154 0155 $oldImport = $this->importDir; 0156 0157 // TODO: this is because the importDir api is stupid 0158 $this->importDir = (array)$this->importDir; 0159 array_unshift($this->importDir, $importDir); 0160 0161 foreach ($props as $prop) { 0162 $this->compileProp($prop, $block, $out); 0163 } 0164 0165 $this->importDir = $oldImport; 0166 $this->sourceParser = $oldSourceParser; 0167 } 0168 0169 /** 0170 * Recursively compiles a block. 0171 * 0172 * A block is analogous to a CSS block in most cases. A single LESS document 0173 * is encapsulated in a block when parsed, but it does not have parent tags 0174 * so all of it's children appear on the root level when compiled. 0175 * 0176 * Blocks are made up of props and children. 0177 * 0178 * Props are property instructions, array tuples which describe an action 0179 * to be taken, eg. write a property, set a variable, mixin a block. 0180 * 0181 * The children of a block are just all the blocks that are defined within. 0182 * This is used to look up mixins when performing a mixin. 0183 * 0184 * Compiling the block involves pushing a fresh environment on the stack, 0185 * and iterating through the props, compiling each one. 0186 * 0187 * See lessc::compileProp() 0188 * 0189 */ 0190 protected function compileBlock($block) { 0191 switch ($block->type) { 0192 case "root": 0193 $this->compileRoot($block); 0194 break; 0195 case null: 0196 $this->compileCSSBlock($block); 0197 break; 0198 case "media": 0199 $this->compileMedia($block); 0200 break; 0201 case "directive": 0202 $name = "@" . $block->name; 0203 if (!empty($block->value)) { 0204 $name .= " " . $this->compileValue($this->reduce($block->value)); 0205 } 0206 0207 $this->compileNestedBlock($block, array($name)); 0208 break; 0209 default: 0210 $this->throwError("unknown block type: $block->type\n"); 0211 } 0212 } 0213 0214 protected function compileCSSBlock($block) { 0215 $env = $this->pushEnv(); 0216 0217 $selectors = $this->compileSelectors($block->tags); 0218 $env->selectors = $this->multiplySelectors($selectors); 0219 $out = $this->makeOutputBlock(null, $env->selectors); 0220 0221 $this->scope->children[] = $out; 0222 $this->compileProps($block, $out); 0223 0224 $block->scope = $env; // mixins carry scope with them! 0225 $this->popEnv(); 0226 } 0227 0228 protected function compileMedia($media) { 0229 $env = $this->pushEnv($media); 0230 $parentScope = $this->mediaParent($this->scope); 0231 0232 $query = $this->compileMediaQuery($this->multiplyMedia($env)); 0233 0234 $this->scope = $this->makeOutputBlock($media->type, array($query)); 0235 $parentScope->children[] = $this->scope; 0236 0237 $this->compileProps($media, $this->scope); 0238 0239 if (count($this->scope->lines) > 0) { 0240 $orphanSelelectors = $this->findClosestSelectors(); 0241 if (!is_null($orphanSelelectors)) { 0242 $orphan = $this->makeOutputBlock(null, $orphanSelelectors); 0243 $orphan->lines = $this->scope->lines; 0244 array_unshift($this->scope->children, $orphan); 0245 $this->scope->lines = array(); 0246 } 0247 } 0248 0249 $this->scope = $this->scope->parent; 0250 $this->popEnv(); 0251 } 0252 0253 protected function mediaParent($scope) { 0254 while (!empty($scope->parent)) { 0255 if (!empty($scope->type) && $scope->type != "media") { 0256 break; 0257 } 0258 $scope = $scope->parent; 0259 } 0260 0261 return $scope; 0262 } 0263 0264 protected function compileNestedBlock($block, $selectors) { 0265 $this->pushEnv($block); 0266 $this->scope = $this->makeOutputBlock($block->type, $selectors); 0267 $this->scope->parent->children[] = $this->scope; 0268 0269 $this->compileProps($block, $this->scope); 0270 0271 $this->scope = $this->scope->parent; 0272 $this->popEnv(); 0273 } 0274 0275 protected function compileRoot($root) { 0276 $this->pushEnv(); 0277 $this->scope = $this->makeOutputBlock($root->type); 0278 $this->compileProps($root, $this->scope); 0279 $this->popEnv(); 0280 } 0281 0282 protected function compileProps($block, $out) { 0283 foreach ($this->sortProps($block->props) as $prop) { 0284 $this->compileProp($prop, $block, $out); 0285 } 0286 0287 $out->lines = array_values(array_unique($out->lines)); 0288 } 0289 0290 protected function sortProps($props, $split = false) { 0291 $vars = array(); 0292 $imports = array(); 0293 $other = array(); 0294 0295 foreach ($props as $prop) { 0296 switch ($prop[0]) { 0297 case "assign": 0298 if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) { 0299 $vars[] = $prop; 0300 } else { 0301 $other[] = $prop; 0302 } 0303 break; 0304 case "import": 0305 $id = self::$nextImportId++; 0306 $prop[] = $id; 0307 $imports[] = $prop; 0308 $other[] = array("import_mixin", $id); 0309 break; 0310 default: 0311 $other[] = $prop; 0312 } 0313 } 0314 0315 if ($split) { 0316 return array(array_merge($vars, $imports), $other); 0317 } else { 0318 return array_merge($vars, $imports, $other); 0319 } 0320 } 0321 0322 protected function compileMediaQuery($queries) { 0323 $compiledQueries = array(); 0324 foreach ($queries as $query) { 0325 $parts = array(); 0326 foreach ($query as $q) { 0327 switch ($q[0]) { 0328 case "mediaType": 0329 $parts[] = implode(" ", array_slice($q, 1)); 0330 break; 0331 case "mediaExp": 0332 if (isset($q[2])) { 0333 $parts[] = "($q[1]: " . 0334 $this->compileValue($this->reduce($q[2])) . ")"; 0335 } else { 0336 $parts[] = "($q[1])"; 0337 } 0338 break; 0339 case "variable": 0340 $parts[] = $this->compileValue($this->reduce($q)); 0341 break; 0342 } 0343 } 0344 0345 if (count($parts) > 0) { 0346 $compiledQueries[] = implode(" and ", $parts); 0347 } 0348 } 0349 0350 $out = "@media"; 0351 if (!empty($parts)) { 0352 $out .= " " . 0353 implode($this->formatter->selectorSeparator, $compiledQueries); 0354 } 0355 return $out; 0356 } 0357 0358 protected function multiplyMedia($env, $childQueries = null) { 0359 if (is_null($env) || 0360 !empty($env->block->type) && $env->block->type != "media") 0361 { 0362 return $childQueries; 0363 } 0364 0365 // plain old block, skip 0366 if (empty($env->block->type)) { 0367 return $this->multiplyMedia($env->parent, $childQueries); 0368 } 0369 0370 $out = array(); 0371 $queries = $env->block->queries; 0372 if (is_null($childQueries)) { 0373 $out = $queries; 0374 } else { 0375 foreach ($queries as $parent) { 0376 foreach ($childQueries as $child) { 0377 $out[] = array_merge($parent, $child); 0378 } 0379 } 0380 } 0381 0382 return $this->multiplyMedia($env->parent, $out); 0383 } 0384 0385 protected function expandParentSelectors(&$tag, $replace) { 0386 $parts = explode("$&$", $tag); 0387 $count = 0; 0388 foreach ($parts as &$part) { 0389 $part = str_replace($this->parentSelector, $replace, $part, $c); 0390 $count += $c; 0391 } 0392 $tag = implode($this->parentSelector, $parts); 0393 return $count; 0394 } 0395 0396 protected function findClosestSelectors() { 0397 $env = $this->env; 0398 $selectors = null; 0399 while ($env !== null) { 0400 if (isset($env->selectors)) { 0401 $selectors = $env->selectors; 0402 break; 0403 } 0404 $env = $env->parent; 0405 } 0406 0407 return $selectors; 0408 } 0409 0410 0411 // multiply $selectors against the nearest selectors in env 0412 protected function multiplySelectors($selectors) { 0413 // find parent selectors 0414 0415 $parentSelectors = $this->findClosestSelectors(); 0416 if (is_null($parentSelectors)) { 0417 // kill parent reference in top level selector 0418 foreach ($selectors as &$s) { 0419 $this->expandParentSelectors($s, ""); 0420 } 0421 0422 return $selectors; 0423 } 0424 0425 $out = array(); 0426 foreach ($parentSelectors as $parent) { 0427 foreach ($selectors as $child) { 0428 $count = $this->expandParentSelectors($child, $parent); 0429 0430 // don't prepend the parent tag if & was used 0431 if ($count > 0) { 0432 $out[] = trim($child); 0433 } else { 0434 $out[] = trim($parent . ' ' . $child); 0435 } 0436 } 0437 } 0438 0439 return $out; 0440 } 0441 0442 // reduces selector expressions 0443 protected function compileSelectors($selectors) { 0444 $out = array(); 0445 0446 foreach ($selectors as $s) { 0447 if (is_array($s)) { 0448 list(, $value) = $s; 0449 $out[] = trim($this->compileValue($this->reduce($value))); 0450 } else { 0451 $out[] = $s; 0452 } 0453 } 0454 0455 return $out; 0456 } 0457 0458 protected function eq($left, $right) { 0459 return $left == $right; 0460 } 0461 0462 protected function patternMatch($block, $orderedArgs, $keywordArgs) { 0463 // match the guards if it has them 0464 // any one of the groups must have all its guards pass for a match 0465 if (!empty($block->guards)) { 0466 $groupPassed = false; 0467 foreach ($block->guards as $guardGroup) { 0468 foreach ($guardGroup as $guard) { 0469 $this->pushEnv(); 0470 $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); 0471 0472 $negate = false; 0473 if ($guard[0] == "negate") { 0474 $guard = $guard[1]; 0475 $negate = true; 0476 } 0477 0478 $passed = $this->reduce($guard) == self::$TRUE; 0479 if ($negate) $passed = !$passed; 0480 0481 $this->popEnv(); 0482 0483 if ($passed) { 0484 $groupPassed = true; 0485 } else { 0486 $groupPassed = false; 0487 break; 0488 } 0489 } 0490 0491 if ($groupPassed) break; 0492 } 0493 0494 if (!$groupPassed) { 0495 return false; 0496 } 0497 } 0498 0499 if (empty($block->args)) { 0500 return $block->isVararg || empty($orderedArgs) && empty($keywordArgs); 0501 } 0502 0503 $remainingArgs = $block->args; 0504 if ($keywordArgs) { 0505 $remainingArgs = array(); 0506 foreach ($block->args as $arg) { 0507 if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) { 0508 continue; 0509 } 0510 0511 $remainingArgs[] = $arg; 0512 } 0513 } 0514 0515 $i = -1; // no args 0516 // try to match by arity or by argument literal 0517 foreach ($remainingArgs as $i => $arg) { 0518 switch ($arg[0]) { 0519 case "lit": 0520 if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) { 0521 return false; 0522 } 0523 break; 0524 case "arg": 0525 // no arg and no default value 0526 if (!isset($orderedArgs[$i]) && !isset($arg[2])) { 0527 return false; 0528 } 0529 break; 0530 case "rest": 0531 $i--; // rest can be empty 0532 break 2; 0533 } 0534 } 0535 0536 if ($block->isVararg) { 0537 return true; // not having enough is handled above 0538 } else { 0539 $numMatched = $i + 1; 0540 // greater than becuase default values always match 0541 return $numMatched >= count($orderedArgs); 0542 } 0543 } 0544 0545 protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) { 0546 $matches = null; 0547 foreach ($blocks as $block) { 0548 // skip seen blocks that don't have arguments 0549 if (isset($skip[$block->id]) && !isset($block->args)) { 0550 continue; 0551 } 0552 0553 if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) { 0554 $matches[] = $block; 0555 } 0556 } 0557 0558 return $matches; 0559 } 0560 0561 // attempt to find blocks matched by path and args 0562 protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) { 0563 if ($searchIn == null) return null; 0564 if (isset($seen[$searchIn->id])) return null; 0565 $seen[$searchIn->id] = true; 0566 0567 $name = $path[0]; 0568 0569 if (isset($searchIn->children[$name])) { 0570 $blocks = $searchIn->children[$name]; 0571 if (count($path) == 1) { 0572 $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen); 0573 if (!empty($matches)) { 0574 // This will return all blocks that match in the closest 0575 // scope that has any matching block, like lessjs 0576 return $matches; 0577 } 0578 } else { 0579 $matches = array(); 0580 foreach ($blocks as $subBlock) { 0581 $subMatches = $this->findBlocks($subBlock, 0582 array_slice($path, 1), $orderedArgs, $keywordArgs, $seen); 0583 0584 if (!is_null($subMatches)) { 0585 foreach ($subMatches as $sm) { 0586 $matches[] = $sm; 0587 } 0588 } 0589 } 0590 0591 return count($matches) > 0 ? $matches : null; 0592 } 0593 } 0594 if ($searchIn->parent === $searchIn) return null; 0595 return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen); 0596 } 0597 0598 // sets all argument names in $args to either the default value 0599 // or the one passed in through $values 0600 protected function zipSetArgs($args, $orderedValues, $keywordValues) { 0601 $assignedValues = array(); 0602 0603 $i = 0; 0604 foreach ($args as $a) { 0605 if ($a[0] == "arg") { 0606 if (isset($keywordValues[$a[1]])) { 0607 // has keyword arg 0608 $value = $keywordValues[$a[1]]; 0609 } elseif (isset($orderedValues[$i])) { 0610 // has ordered arg 0611 $value = $orderedValues[$i]; 0612 $i++; 0613 } elseif (isset($a[2])) { 0614 // has default value 0615 $value = $a[2]; 0616 } else { 0617 $this->throwError("Failed to assign arg " . $a[1]); 0618 $value = null; // :( 0619 } 0620 0621 $value = $this->reduce($value); 0622 $this->set($a[1], $value); 0623 $assignedValues[] = $value; 0624 } else { 0625 // a lit 0626 $i++; 0627 } 0628 } 0629 0630 // check for a rest 0631 $last = end($args); 0632 if ($last[0] == "rest") { 0633 $rest = array_slice($orderedValues, count($args) - 1); 0634 $this->set($last[1], $this->reduce(array("list", " ", $rest))); 0635 } 0636 0637 // wow is this the only true use of PHP's + operator for arrays? 0638 $this->env->arguments = $assignedValues + $orderedValues; 0639 } 0640 0641 // compile a prop and update $lines or $blocks appropriately 0642 protected function compileProp($prop, $block, $out) { 0643 // set error position context 0644 $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1; 0645 0646 switch ($prop[0]) { 0647 case 'assign': 0648 list(, $name, $value) = $prop; 0649 if ($name[0] == $this->vPrefix) { 0650 $this->set($name, $value); 0651 } else { 0652 $out->lines[] = $this->formatter->property($name, 0653 $this->compileValue($this->reduce($value))); 0654 } 0655 break; 0656 case 'block': 0657 list(, $child) = $prop; 0658 $this->compileBlock($child); 0659 break; 0660 case 'mixin': 0661 list(, $path, $args, $suffix) = $prop; 0662 0663 $orderedArgs = array(); 0664 $keywordArgs = array(); 0665 foreach ((array)$args as $arg) { 0666 $argval = null; 0667 switch ($arg[0]) { 0668 case "arg": 0669 if (!isset($arg[2])) { 0670 $orderedArgs[] = $this->reduce(array("variable", $arg[1])); 0671 } else { 0672 $keywordArgs[$arg[1]] = $this->reduce($arg[2]); 0673 } 0674 break; 0675 0676 case "lit": 0677 $orderedArgs[] = $this->reduce($arg[1]); 0678 break; 0679 default: 0680 $this->throwError("Unknown arg type: " . $arg[0]); 0681 } 0682 } 0683 0684 $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); 0685 0686 if ($mixins === null) { 0687 // fwrite(STDERR,"failed to find block: ".implode(" > ", $path)."\n"); 0688 break; // throw error here?? 0689 } 0690 0691 foreach ($mixins as $mixin) { 0692 if ($mixin === $block && !$orderedArgs) { 0693 continue; 0694 } 0695 0696 $haveScope = false; 0697 if (isset($mixin->parent->scope)) { 0698 $haveScope = true; 0699 $mixinParentEnv = $this->pushEnv(); 0700 $mixinParentEnv->storeParent = $mixin->parent->scope; 0701 } 0702 0703 $haveArgs = false; 0704 if (isset($mixin->args)) { 0705 $haveArgs = true; 0706 $this->pushEnv(); 0707 $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs); 0708 } 0709 0710 $oldParent = $mixin->parent; 0711 if ($mixin != $block) $mixin->parent = $block; 0712 0713 foreach ($this->sortProps($mixin->props) as $subProp) { 0714 if ($suffix !== null && 0715 $subProp[0] == "assign" && 0716 is_string($subProp[1]) && 0717 $subProp[1]{0} != $this->vPrefix) 0718 { 0719 $subProp[2] = array( 0720 'list', ' ', 0721 array($subProp[2], array('keyword', $suffix)) 0722 ); 0723 } 0724 0725 $this->compileProp($subProp, $mixin, $out); 0726 } 0727 0728 $mixin->parent = $oldParent; 0729 0730 if ($haveArgs) $this->popEnv(); 0731 if ($haveScope) $this->popEnv(); 0732 } 0733 0734 break; 0735 case 'raw': 0736 $out->lines[] = $prop[1]; 0737 break; 0738 case "directive": 0739 list(, $name, $value) = $prop; 0740 $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';'; 0741 break; 0742 case "comment": 0743 $out->lines[] = $prop[1]; 0744 break; 0745 case "import"; 0746 list(, $importPath, $importId) = $prop; 0747 $importPath = $this->reduce($importPath); 0748 0749 if (!isset($this->env->imports)) { 0750 $this->env->imports = array(); 0751 } 0752 0753 $result = $this->tryImport($importPath, $block, $out); 0754 0755 $this->env->imports[$importId] = $result === false ? 0756 array(false, "@import " . $this->compileValue($importPath).";") : 0757 $result; 0758 0759 break; 0760 case "import_mixin": 0761 list(,$importId) = $prop; 0762 $import = $this->env->imports[$importId]; 0763 if ($import[0] === false) { 0764 if (isset($import[1])) { 0765 $out->lines[] = $import[1]; 0766 } 0767 } else { 0768 list(, $bottom, $parser, $importDir) = $import; 0769 $this->compileImportedProps($bottom, $block, $out, $parser, $importDir); 0770 } 0771 0772 break; 0773 default: 0774 $this->throwError("unknown op: {$prop[0]}\n"); 0775 } 0776 } 0777 0778 0779 /** 0780 * Compiles a primitive value into a CSS property value. 0781 * 0782 * Values in lessphp are typed by being wrapped in arrays, their format is 0783 * typically: 0784 * 0785 * array(type, contents [, additional_contents]*) 0786 * 0787 * The input is expected to be reduced. This function will not work on 0788 * things like expressions and variables. 0789 */ 0790 protected function compileValue($value) { 0791 switch ($value[0]) { 0792 case 'list': 0793 // [1] - delimiter 0794 // [2] - array of values 0795 return implode($value[1], array_map(array($this, 'compileValue'), $value[2])); 0796 case 'raw_color': 0797 if (!empty($this->formatter->compressColors)) { 0798 return $this->compileValue($this->coerceColor($value)); 0799 } 0800 return $value[1]; 0801 case 'keyword': 0802 // [1] - the keyword 0803 return $value[1]; 0804 case 'number': 0805 list(, $num, $unit) = $value; 0806 // [1] - the number 0807 // [2] - the unit 0808 if ($this->numberPrecision !== null) { 0809 $num = round($num, $this->numberPrecision); 0810 } 0811 return $num . $unit; 0812 case 'string': 0813 // [1] - contents of string (includes quotes) 0814 list(, $delim, $content) = $value; 0815 foreach ($content as &$part) { 0816 if (is_array($part)) { 0817 $part = $this->compileValue($part); 0818 } 0819 } 0820 return $delim . implode($content) . $delim; 0821 case 'color': 0822 // [1] - red component (either number or a %) 0823 // [2] - green component 0824 // [3] - blue component 0825 // [4] - optional alpha component 0826 list(, $r, $g, $b) = $value; 0827 $r = round($r); 0828 $g = round($g); 0829 $b = round($b); 0830 0831 if (count($value) == 5 && $value[4] != 1) { // rgba 0832 return 'rgba('.$r.','.$g.','.$b.','.$value[4].')'; 0833 } 0834 0835 $h = sprintf("#%02x%02x%02x", $r, $g, $b); 0836 0837 if (!empty($this->formatter->compressColors)) { 0838 // Converting hex color to short notation (e.g. #003399 to #039) 0839 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { 0840 $h = '#' . $h[1] . $h[3] . $h[5]; 0841 } 0842 } 0843 0844 return $h; 0845 0846 case 'function': 0847 list(, $name, $args) = $value; 0848 return $name.'('.$this->compileValue($args).')'; 0849 default: // assumed to be unit 0850 $this->throwError("unknown value type: $value[0]"); 0851 } 0852 } 0853 0854 protected function lib_pow($args) { 0855 list($base, $exp) = $this->assertArgs($args, 2, "pow"); 0856 return pow($this->assertNumber($base), $this->assertNumber($exp)); 0857 } 0858 0859 protected function lib_pi() { 0860 return pi(); 0861 } 0862 0863 protected function lib_mod($args) { 0864 list($a, $b) = $this->assertArgs($args, 2, "mod"); 0865 return $this->assertNumber($a) % $this->assertNumber($b); 0866 } 0867 0868 protected function lib_tan($num) { 0869 return tan($this->assertNumber($num)); 0870 } 0871 0872 protected function lib_sin($num) { 0873 return sin($this->assertNumber($num)); 0874 } 0875 0876 protected function lib_cos($num) { 0877 return cos($this->assertNumber($num)); 0878 } 0879 0880 protected function lib_atan($num) { 0881 $num = atan($this->assertNumber($num)); 0882 return array("number", $num, "rad"); 0883 } 0884 0885 protected function lib_asin($num) { 0886 $num = asin($this->assertNumber($num)); 0887 return array("number", $num, "rad"); 0888 } 0889 0890 protected function lib_acos($num) { 0891 $num = acos($this->assertNumber($num)); 0892 return array("number", $num, "rad"); 0893 } 0894 0895 protected function lib_sqrt($num) { 0896 return sqrt($this->assertNumber($num)); 0897 } 0898 0899 protected function lib_extract($value) { 0900 list($list, $idx) = $this->assertArgs($value, 2, "extract"); 0901 $idx = $this->assertNumber($idx); 0902 // 1 indexed 0903 if ($list[0] == "list" && isset($list[2][$idx - 1])) { 0904 return $list[2][$idx - 1]; 0905 } 0906 } 0907 0908 protected function lib_isnumber($value) { 0909 return $this->toBool($value[0] == "number"); 0910 } 0911 0912 protected function lib_isstring($value) { 0913 return $this->toBool($value[0] == "string"); 0914 } 0915 0916 protected function lib_iscolor($value) { 0917 return $this->toBool($this->coerceColor($value)); 0918 } 0919 0920 protected function lib_iskeyword($value) { 0921 return $this->toBool($value[0] == "keyword"); 0922 } 0923 0924 protected function lib_ispixel($value) { 0925 return $this->toBool($value[0] == "number" && $value[2] == "px"); 0926 } 0927 0928 protected function lib_ispercentage($value) { 0929 return $this->toBool($value[0] == "number" && $value[2] == "%"); 0930 } 0931 0932 protected function lib_isem($value) { 0933 return $this->toBool($value[0] == "number" && $value[2] == "em"); 0934 } 0935 0936 protected function lib_isrem($value) { 0937 return $this->toBool($value[0] == "number" && $value[2] == "rem"); 0938 } 0939 0940 protected function lib_rgbahex($color) { 0941 $color = $this->coerceColor($color); 0942 if (is_null($color)) 0943 $this->throwError("color expected for rgbahex"); 0944 0945 return sprintf("#%02x%02x%02x%02x", 0946 isset($color[4]) ? $color[4]*255 : 255, 0947 $color[1],$color[2], $color[3]); 0948 } 0949 0950 protected function lib_argb($color){ 0951 return $this->lib_rgbahex($color); 0952 } 0953 0954 // utility func to unquote a string 0955 protected function lib_e($arg) { 0956 switch ($arg[0]) { 0957 case "list": 0958 $items = $arg[2]; 0959 if (isset($items[0])) { 0960 return $this->lib_e($items[0]); 0961 } 0962 return self::$defaultValue; 0963 case "string": 0964 $arg[1] = ""; 0965 return $arg; 0966 case "keyword": 0967 return $arg; 0968 default: 0969 return array("keyword", $this->compileValue($arg)); 0970 } 0971 } 0972 0973 protected function lib__sprintf($args) { 0974 if ($args[0] != "list") return $args; 0975 $values = $args[2]; 0976 $string = array_shift($values); 0977 $template = $this->compileValue($this->lib_e($string)); 0978 0979 $i = 0; 0980 if (preg_match_all('/%[dsa]/', $template, $m)) { 0981 foreach ($m[0] as $match) { 0982 $val = isset($values[$i]) ? 0983 $this->reduce($values[$i]) : array('keyword', ''); 0984 0985 // lessjs compat, renders fully expanded color, not raw color 0986 if ($color = $this->coerceColor($val)) { 0987 $val = $color; 0988 } 0989 0990 $i++; 0991 $rep = $this->compileValue($this->lib_e($val)); 0992 $template = preg_replace('/'.self::preg_quote($match).'/', 0993 $rep, $template, 1); 0994 } 0995 } 0996 0997 $d = $string[0] == "string" ? $string[1] : '"'; 0998 return array("string", $d, array($template)); 0999 } 1000 1001 protected function lib_floor($arg) { 1002 $value = $this->assertNumber($arg); 1003 return array("number", floor($value), $arg[2]); 1004 } 1005 1006 protected function lib_ceil($arg) { 1007 $value = $this->assertNumber($arg); 1008 return array("number", ceil($value), $arg[2]); 1009 } 1010 1011 protected function lib_round($arg) { 1012 $value = $this->assertNumber($arg); 1013 return array("number", round($value), $arg[2]); 1014 } 1015 1016 protected function lib_unit($arg) { 1017 if ($arg[0] == "list") { 1018 list($number, $newUnit) = $arg[2]; 1019 return array("number", $this->assertNumber($number), 1020 $this->compileValue($this->lib_e($newUnit))); 1021 } else { 1022 return array("number", $this->assertNumber($arg), ""); 1023 } 1024 } 1025 1026 /** 1027 * Helper function to get arguments for color manipulation functions. 1028 * takes a list that contains a color like thing and a percentage 1029 */ 1030 protected function colorArgs($args) { 1031 if ($args[0] != 'list' || count($args[2]) < 2) { 1032 return array(array('color', 0, 0, 0), 0); 1033 } 1034 list($color, $delta) = $args[2]; 1035 $color = $this->assertColor($color); 1036 $delta = floatval($delta[1]); 1037 1038 return array($color, $delta); 1039 } 1040 1041 protected function lib_darken($args) { 1042 list($color, $delta) = $this->colorArgs($args); 1043 1044 $hsl = $this->toHSL($color); 1045 $hsl[3] = $this->clamp($hsl[3] - $delta, 100); 1046 return $this->toRGB($hsl); 1047 } 1048 1049 protected function lib_lighten($args) { 1050 list($color, $delta) = $this->colorArgs($args); 1051 1052 $hsl = $this->toHSL($color); 1053 $hsl[3] = $this->clamp($hsl[3] + $delta, 100); 1054 return $this->toRGB($hsl); 1055 } 1056 1057 protected function lib_saturate($args) { 1058 list($color, $delta) = $this->colorArgs($args); 1059 1060 $hsl = $this->toHSL($color); 1061 $hsl[2] = $this->clamp($hsl[2] + $delta, 100); 1062 return $this->toRGB($hsl); 1063 } 1064 1065 protected function lib_desaturate($args) { 1066 list($color, $delta) = $this->colorArgs($args); 1067 1068 $hsl = $this->toHSL($color); 1069 $hsl[2] = $this->clamp($hsl[2] - $delta, 100); 1070 return $this->toRGB($hsl); 1071 } 1072 1073 protected function lib_spin($args) { 1074 list($color, $delta) = $this->colorArgs($args); 1075 1076 $hsl = $this->toHSL($color); 1077 1078 $hsl[1] = $hsl[1] + $delta % 360; 1079 if ($hsl[1] < 0) $hsl[1] += 360; 1080 1081 return $this->toRGB($hsl); 1082 } 1083 1084 protected function lib_fadeout($args) { 1085 list($color, $delta) = $this->colorArgs($args); 1086 $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100); 1087 return $color; 1088 } 1089 1090 protected function lib_fadein($args) { 1091 list($color, $delta) = $this->colorArgs($args); 1092 $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100); 1093 return $color; 1094 } 1095 1096 protected function lib_hue($color) { 1097 $hsl = $this->toHSL($this->assertColor($color)); 1098 return round($hsl[1]); 1099 } 1100 1101 protected function lib_saturation($color) { 1102 $hsl = $this->toHSL($this->assertColor($color)); 1103 return round($hsl[2]); 1104 } 1105 1106 protected function lib_lightness($color) { 1107 $hsl = $this->toHSL($this->assertColor($color)); 1108 return round($hsl[3]); 1109 } 1110 1111 // get the alpha of a color 1112 // defaults to 1 for non-colors or colors without an alpha 1113 protected function lib_alpha($value) { 1114 if (!is_null($color = $this->coerceColor($value))) { 1115 return isset($color[4]) ? $color[4] : 1; 1116 } 1117 } 1118 1119 // set the alpha of the color 1120 protected function lib_fade($args) { 1121 list($color, $alpha) = $this->colorArgs($args); 1122 $color[4] = $this->clamp($alpha / 100.0); 1123 return $color; 1124 } 1125 1126 protected function lib_percentage($arg) { 1127 $num = $this->assertNumber($arg); 1128 return array("number", $num*100, "%"); 1129 } 1130 1131 // mixes two colors by weight 1132 // mix(@color1, @color2, [@weight: 50%]); 1133 // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method 1134 protected function lib_mix($args) { 1135 if ($args[0] != "list" || count($args[2]) < 2) 1136 $this->throwError("mix expects (color1, color2, weight)"); 1137 1138 list($first, $second) = $args[2]; 1139 $first = $this->assertColor($first); 1140 $second = $this->assertColor($second); 1141 1142 $first_a = $this->lib_alpha($first); 1143 $second_a = $this->lib_alpha($second); 1144 1145 if (isset($args[2][2])) { 1146 $weight = $args[2][2][1] / 100.0; 1147 } else { 1148 $weight = 0.5; 1149 } 1150 1151 $w = $weight * 2 - 1; 1152 $a = $first_a - $second_a; 1153 1154 $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; 1155 $w2 = 1.0 - $w1; 1156 1157 $new = array('color', 1158 $w1 * $first[1] + $w2 * $second[1], 1159 $w1 * $first[2] + $w2 * $second[2], 1160 $w1 * $first[3] + $w2 * $second[3], 1161 ); 1162 1163 if ($first_a != 1.0 || $second_a != 1.0) { 1164 $new[] = $first_a * $weight + $second_a * ($weight - 1); 1165 } 1166 1167 return $this->fixColor($new); 1168 } 1169 1170 protected function lib_contrast($args) { 1171 if ($args[0] != 'list' || count($args[2]) < 3) { 1172 return array(array('color', 0, 0, 0), 0); 1173 } 1174 1175 list($inputColor, $darkColor, $lightColor) = $args[2]; 1176 1177 $inputColor = $this->assertColor($inputColor); 1178 $darkColor = $this->assertColor($darkColor); 1179 $lightColor = $this->assertColor($lightColor); 1180 $hsl = $this->toHSL($inputColor); 1181 1182 if ($hsl[3] > 50) { 1183 return $darkColor; 1184 } 1185 1186 return $lightColor; 1187 } 1188 1189 protected function assertColor($value, $error = "expected color value") { 1190 $color = $this->coerceColor($value); 1191 if (is_null($color)) $this->throwError($error); 1192 return $color; 1193 } 1194 1195 protected function assertNumber($value, $error = "expecting number") { 1196 if ($value[0] == "number") return $value[1]; 1197 $this->throwError($error); 1198 } 1199 1200 protected function assertArgs($value, $expectedArgs, $name="") { 1201 if ($expectedArgs == 1) { 1202 return $value; 1203 } else { 1204 if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list"); 1205 $values = $value[2]; 1206 $numValues = count($values); 1207 if ($expectedArgs != $numValues) { 1208 if ($name) { 1209 $name = $name . ": "; 1210 } 1211 1212 $this->throwError("${name}expecting $expectedArgs arguments, got $numValues"); 1213 } 1214 1215 return $values; 1216 } 1217 } 1218 1219 protected function toHSL($color) { 1220 if ($color[0] == 'hsl') return $color; 1221 1222 $r = $color[1] / 255; 1223 $g = $color[2] / 255; 1224 $b = $color[3] / 255; 1225 1226 $min = min($r, $g, $b); 1227 $max = max($r, $g, $b); 1228 1229 $L = ($min + $max) / 2; 1230 if ($min == $max) { 1231 $S = $H = 0; 1232 } else { 1233 if ($L < 0.5) 1234 $S = ($max - $min)/($max + $min); 1235 else 1236 $S = ($max - $min)/(2.0 - $max - $min); 1237 1238 if ($r == $max) $H = ($g - $b)/($max - $min); 1239 elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min); 1240 elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min); 1241 1242 } 1243 1244 $out = array('hsl', 1245 ($H < 0 ? $H + 6 : $H)*60, 1246 $S*100, 1247 $L*100, 1248 ); 1249 1250 if (count($color) > 4) $out[] = $color[4]; // copy alpha 1251 return $out; 1252 } 1253 1254 protected function toRGB_helper($comp, $temp1, $temp2) { 1255 if ($comp < 0) $comp += 1.0; 1256 elseif ($comp > 1) $comp -= 1.0; 1257 1258 if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp; 1259 if (2 * $comp < 1) return $temp2; 1260 if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; 1261 1262 return $temp1; 1263 } 1264 1265 /** 1266 * Converts a hsl array into a color value in rgb. 1267 * Expects H to be in range of 0 to 360, S and L in 0 to 100 1268 */ 1269 protected function toRGB($color) { 1270 if ($color[0] == 'color') return $color; 1271 1272 $H = $color[1] / 360; 1273 $S = $color[2] / 100; 1274 $L = $color[3] / 100; 1275 1276 if ($S == 0) { 1277 $r = $g = $b = $L; 1278 } else { 1279 $temp2 = $L < 0.5 ? 1280 $L*(1.0 + $S) : 1281 $L + $S - $L * $S; 1282 1283 $temp1 = 2.0 * $L - $temp2; 1284 1285 $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2); 1286 $g = $this->toRGB_helper($H, $temp1, $temp2); 1287 $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2); 1288 } 1289 1290 // $out = array('color', round($r*255), round($g*255), round($b*255)); 1291 $out = array('color', $r*255, $g*255, $b*255); 1292 if (count($color) > 4) $out[] = $color[4]; // copy alpha 1293 return $out; 1294 } 1295 1296 protected function clamp($v, $max = 1, $min = 0) { 1297 return min($max, max($min, $v)); 1298 } 1299 1300 /** 1301 * Convert the rgb, rgba, hsl color literals of function type 1302 * as returned by the parser into values of color type. 1303 */ 1304 protected function funcToColor($func) { 1305 $fname = $func[1]; 1306 if ($func[2][0] != 'list') return false; // need a list of arguments 1307 $rawComponents = $func[2][2]; 1308 1309 if ($fname == 'hsl' || $fname == 'hsla') { 1310 $hsl = array('hsl'); 1311 $i = 0; 1312 foreach ($rawComponents as $c) { 1313 $val = $this->reduce($c); 1314 $val = isset($val[1]) ? floatval($val[1]) : 0; 1315 1316 if ($i == 0) $clamp = 360; 1317 elseif ($i < 3) $clamp = 100; 1318 else $clamp = 1; 1319 1320 $hsl[] = $this->clamp($val, $clamp); 1321 $i++; 1322 } 1323 1324 while (count($hsl) < 4) $hsl[] = 0; 1325 return $this->toRGB($hsl); 1326 1327 } elseif ($fname == 'rgb' || $fname == 'rgba') { 1328 $components = array(); 1329 $i = 1; 1330 foreach ($rawComponents as $c) { 1331 $c = $this->reduce($c); 1332 if ($i < 4) { 1333 if ($c[0] == "number" && $c[2] == "%") { 1334 $components[] = 255 * ($c[1] / 100); 1335 } else { 1336 $components[] = floatval($c[1]); 1337 } 1338 } elseif ($i == 4) { 1339 if ($c[0] == "number" && $c[2] == "%") { 1340 $components[] = 1.0 * ($c[1] / 100); 1341 } else { 1342 $components[] = floatval($c[1]); 1343 } 1344 } else break; 1345 1346 $i++; 1347 } 1348 while (count($components) < 3) $components[] = 0; 1349 array_unshift($components, 'color'); 1350 return $this->fixColor($components); 1351 } 1352 1353 return false; 1354 } 1355 1356 protected function reduce($value, $forExpression = false) { 1357 switch ($value[0]) { 1358 case "interpolate": 1359 $reduced = $this->reduce($value[1]); 1360 $var = $this->compileValue($reduced); 1361 $res = $this->reduce(array("variable", $this->vPrefix . $var)); 1362 1363 if ($res[0] == "raw_color") { 1364 $res = $this->coerceColor($res); 1365 } 1366 1367 if (empty($value[2])) $res = $this->lib_e($res); 1368 1369 return $res; 1370 case "variable": 1371 $key = $value[1]; 1372 if (is_array($key)) { 1373 $key = $this->reduce($key); 1374 $key = $this->vPrefix . $this->compileValue($this->lib_e($key)); 1375 } 1376 1377 $seen =& $this->env->seenNames; 1378 1379 if (!empty($seen[$key])) { 1380 $this->throwError("infinite loop detected: $key"); 1381 } 1382 1383 $seen[$key] = true; 1384 $out = $this->reduce($this->get($key, self::$defaultValue)); 1385 $seen[$key] = false; 1386 return $out; 1387 case "list": 1388 foreach ($value[2] as &$item) { 1389 $item = $this->reduce($item, $forExpression); 1390 } 1391 return $value; 1392 case "expression": 1393 return $this->evaluate($value); 1394 case "string": 1395 foreach ($value[2] as &$part) { 1396 if (is_array($part)) { 1397 $strip = $part[0] == "variable"; 1398 $part = $this->reduce($part); 1399 if ($strip) $part = $this->lib_e($part); 1400 } 1401 } 1402 return $value; 1403 case "escape": 1404 list(,$inner) = $value; 1405 return $this->lib_e($this->reduce($inner)); 1406 case "function": 1407 $color = $this->funcToColor($value); 1408 if ($color) return $color; 1409 1410 list(, $name, $args) = $value; 1411 if ($name == "%") $name = "_sprintf"; 1412 $f = isset($this->libFunctions[$name]) ? 1413 $this->libFunctions[$name] : array($this, 'lib_'.$name); 1414 1415 if (is_callable($f)) { 1416 if ($args[0] == 'list') 1417 $args = self::compressList($args[2], $args[1]); 1418 1419 $ret = call_user_func($f, $this->reduce($args, true), $this); 1420 1421 if (is_null($ret)) { 1422 return array("string", "", array( 1423 $name, "(", $args, ")" 1424 )); 1425 } 1426 1427 // convert to a typed value if the result is a php primitive 1428 if (is_numeric($ret)) $ret = array('number', $ret, ""); 1429 elseif (!is_array($ret)) $ret = array('keyword', $ret); 1430 1431 return $ret; 1432 } 1433 1434 // plain function, reduce args 1435 $value[2] = $this->reduce($value[2]); 1436 return $value; 1437 case "unary": 1438 list(, $op, $exp) = $value; 1439 $exp = $this->reduce($exp); 1440 1441 if ($exp[0] == "number") { 1442 switch ($op) { 1443 case "+": 1444 return $exp; 1445 case "-": 1446 $exp[1] *= -1; 1447 return $exp; 1448 } 1449 } 1450 return array("string", "", array($op, $exp)); 1451 } 1452 1453 if ($forExpression) { 1454 switch ($value[0]) { 1455 case "keyword": 1456 if ($color = $this->coerceColor($value)) { 1457 return $color; 1458 } 1459 break; 1460 case "raw_color": 1461 return $this->coerceColor($value); 1462 } 1463 } 1464 1465 return $value; 1466 } 1467 1468 1469 // coerce a value for use in color operation 1470 protected function coerceColor($value) { 1471 switch($value[0]) { 1472 case 'color': return $value; 1473 case 'raw_color': 1474 $c = array("color", 0, 0, 0); 1475 $colorStr = substr($value[1], 1); 1476 $num = hexdec($colorStr); 1477 $width = strlen($colorStr) == 3 ? 16 : 256; 1478 1479 for ($i = 3; $i > 0; $i--) { // 3 2 1 1480 $t = $num % $width; 1481 $num /= $width; 1482 1483 $c[$i] = $t * (256/$width) + $t * floor(16/$width); 1484 } 1485 1486 return $c; 1487 case 'keyword': 1488 $name = $value[1]; 1489 if (isset(self::$cssColors[$name])) { 1490 $rgba = explode(',', self::$cssColors[$name]); 1491 1492 if(isset($rgba[3])) 1493 return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]); 1494 1495 return array('color', $rgba[0], $rgba[1], $rgba[2]); 1496 } 1497 return null; 1498 } 1499 } 1500 1501 // make something string like into a string 1502 protected function coerceString($value) { 1503 switch ($value[0]) { 1504 case "string": 1505 return $value; 1506 case "keyword": 1507 return array("string", "", array($value[1])); 1508 } 1509 return null; 1510 } 1511 1512 // turn list of length 1 into value type 1513 protected function flattenList($value) { 1514 if ($value[0] == "list" && count($value[2]) == 1) { 1515 return $this->flattenList($value[2][0]); 1516 } 1517 return $value; 1518 } 1519 1520 protected function toBool($a) { 1521 if ($a) return self::$TRUE; 1522 else return self::$FALSE; 1523 } 1524 1525 // evaluate an expression 1526 protected function evaluate($exp) { 1527 list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; 1528 1529 $left = $this->reduce($left, true); 1530 $right = $this->reduce($right, true); 1531 1532 if ($leftColor = $this->coerceColor($left)) { 1533 $left = $leftColor; 1534 } 1535 1536 if ($rightColor = $this->coerceColor($right)) { 1537 $right = $rightColor; 1538 } 1539 1540 $ltype = $left[0]; 1541 $rtype = $right[0]; 1542 1543 // operators that work on all types 1544 if ($op == "and") { 1545 return $this->toBool($left == self::$TRUE && $right == self::$TRUE); 1546 } 1547 1548 if ($op == "=") { 1549 return $this->toBool($this->eq($left, $right) ); 1550 } 1551 1552 if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) { 1553 return $str; 1554 } 1555 1556 // type based operators 1557 $fname = "op_${ltype}_${rtype}"; 1558 if (is_callable(array($this, $fname))) { 1559 $out = $this->$fname($op, $left, $right); 1560 if (!is_null($out)) return $out; 1561 } 1562 1563 // make the expression look it did before being parsed 1564 $paddedOp = $op; 1565 if ($whiteBefore) $paddedOp = " " . $paddedOp; 1566 if ($whiteAfter) $paddedOp .= " "; 1567 1568 return array("string", "", array($left, $paddedOp, $right)); 1569 } 1570 1571 protected function stringConcatenate($left, $right) { 1572 if ($strLeft = $this->coerceString($left)) { 1573 if ($right[0] == "string") { 1574 $right[1] = ""; 1575 } 1576 $strLeft[2][] = $right; 1577 return $strLeft; 1578 } 1579 1580 if ($strRight = $this->coerceString($right)) { 1581 array_unshift($strRight[2], $left); 1582 return $strRight; 1583 } 1584 } 1585 1586 1587 // make sure a color's components don't go out of bounds 1588 protected function fixColor($c) { 1589 foreach (range(1, 3) as $i) { 1590 if ($c[$i] < 0) $c[$i] = 0; 1591 if ($c[$i] > 255) $c[$i] = 255; 1592 } 1593 1594 return $c; 1595 } 1596 1597 protected function op_number_color($op, $lft, $rgt) { 1598 if ($op == '+' || $op == '*') { 1599 return $this->op_color_number($op, $rgt, $lft); 1600 } 1601 } 1602 1603 protected function op_color_number($op, $lft, $rgt) { 1604 if ($rgt[0] == '%') $rgt[1] /= 100; 1605 1606 return $this->op_color_color($op, $lft, 1607 array_fill(1, count($lft) - 1, $rgt[1])); 1608 } 1609 1610 protected function op_color_color($op, $left, $right) { 1611 $out = array('color'); 1612 $max = count($left) > count($right) ? count($left) : count($right); 1613 foreach (range(1, $max - 1) as $i) { 1614 $lval = isset($left[$i]) ? $left[$i] : 0; 1615 $rval = isset($right[$i]) ? $right[$i] : 0; 1616 switch ($op) { 1617 case '+': 1618 $out[] = $lval + $rval; 1619 break; 1620 case '-': 1621 $out[] = $lval - $rval; 1622 break; 1623 case '*': 1624 $out[] = $lval * $rval; 1625 break; 1626 case '%': 1627 $out[] = $lval % $rval; 1628 break; 1629 case '/': 1630 if ($rval == 0) $this->throwError("evaluate error: can't divide by zero"); 1631 $out[] = $lval / $rval; 1632 break; 1633 default: 1634 $this->throwError('evaluate error: color op number failed on op '.$op); 1635 } 1636 } 1637 return $this->fixColor($out); 1638 } 1639 1640 function lib_red($color){ 1641 $color = $this->coerceColor($color); 1642 if (is_null($color)) { 1643 $this->throwError('color expected for red()'); 1644 } 1645 1646 return $color[1]; 1647 } 1648 1649 function lib_green($color){ 1650 $color = $this->coerceColor($color); 1651 if (is_null($color)) { 1652 $this->throwError('color expected for green()'); 1653 } 1654 1655 return $color[2]; 1656 } 1657 1658 function lib_blue($color){ 1659 $color = $this->coerceColor($color); 1660 if (is_null($color)) { 1661 $this->throwError('color expected for blue()'); 1662 } 1663 1664 return $color[3]; 1665 } 1666 1667 1668 // operator on two numbers 1669 protected function op_number_number($op, $left, $right) { 1670 $unit = empty($left[2]) ? $right[2] : $left[2]; 1671 1672 $value = 0; 1673 switch ($op) { 1674 case '+': 1675 $value = $left[1] + $right[1]; 1676 break; 1677 case '*': 1678 $value = $left[1] * $right[1]; 1679 break; 1680 case '-': 1681 $value = $left[1] - $right[1]; 1682 break; 1683 case '%': 1684 $value = $left[1] % $right[1]; 1685 break; 1686 case '/': 1687 if ($right[1] == 0) $this->throwError('parse error: divide by zero'); 1688 $value = $left[1] / $right[1]; 1689 break; 1690 case '<': 1691 return $this->toBool($left[1] < $right[1]); 1692 case '>': 1693 return $this->toBool($left[1] > $right[1]); 1694 case '>=': 1695 return $this->toBool($left[1] >= $right[1]); 1696 case '=<': 1697 return $this->toBool($left[1] <= $right[1]); 1698 default: 1699 $this->throwError('parse error: unknown number operator: '.$op); 1700 } 1701 1702 return array("number", $value, $unit); 1703 } 1704 1705 1706 /* environment functions */ 1707 1708 protected function makeOutputBlock($type, $selectors = null) { 1709 $b = new stdclass; 1710 $b->lines = array(); 1711 $b->children = array(); 1712 $b->selectors = $selectors; 1713 $b->type = $type; 1714 $b->parent = $this->scope; 1715 return $b; 1716 } 1717 1718 // the state of execution 1719 protected function pushEnv($block = null) { 1720 $e = new stdclass; 1721 $e->parent = $this->env; 1722 $e->store = array(); 1723 $e->block = $block; 1724 1725 $this->env = $e; 1726 return $e; 1727 } 1728 1729 // pop something off the stack 1730 protected function popEnv() { 1731 $old = $this->env; 1732 $this->env = $this->env->parent; 1733 return $old; 1734 } 1735 1736 // set something in the current env 1737 protected function set($name, $value) { 1738 $this->env->store[$name] = $value; 1739 } 1740 1741 1742 // get the highest occurrence entry for a name 1743 protected function get($name, $default=null) { 1744 $current = $this->env; 1745 1746 $isArguments = $name == $this->vPrefix . 'arguments'; 1747 while ($current) { 1748 if ($isArguments && isset($current->arguments)) { 1749 return array('list', ' ', $current->arguments); 1750 } 1751 1752 if (isset($current->store[$name])) 1753 return $current->store[$name]; 1754 else { 1755 $current = isset($current->storeParent) ? 1756 $current->storeParent : $current->parent; 1757 } 1758 } 1759 1760 return $default; 1761 } 1762 1763 // inject array of unparsed strings into environment as variables 1764 protected function injectVariables($args) { 1765 $this->pushEnv(); 1766 $parser = new lessc_parser($this, __METHOD__); 1767 foreach ($args as $name => $strValue) { 1768 if ($name{0} != '@') $name = '@'.$name; 1769 $parser->count = 0; 1770 $parser->buffer = (string)$strValue; 1771 if (!$parser->propertyValue($value)) { 1772 throw new Exception("failed to parse passed in variable $name: $strValue"); 1773 } 1774 1775 $this->set($name, $value); 1776 } 1777 } 1778 1779 /** 1780 * Initialize any static state, can initialize parser for a file 1781 * $opts isn't used yet 1782 */ 1783 public function __construct($fname = null) { 1784 if ($fname !== null) { 1785 // used for deprecated parse method 1786 $this->_parseFile = $fname; 1787 } 1788 } 1789 1790 public function compile($string, $name = null) { 1791 $locale = setlocale(LC_NUMERIC, 0); 1792 setlocale(LC_NUMERIC, "C"); 1793 1794 $this->parser = $this->makeParser($name); 1795 $root = $this->parser->parse($string); 1796 1797 $this->env = null; 1798 $this->scope = null; 1799 1800 $this->formatter = $this->newFormatter(); 1801 1802 if (!empty($this->registeredVars)) { 1803 $this->injectVariables($this->registeredVars); 1804 } 1805 1806 $this->sourceParser = $this->parser; // used for error messages 1807 $this->compileBlock($root); 1808 1809 ob_start(); 1810 $this->formatter->block($this->scope); 1811 $out = ob_get_clean(); 1812 setlocale(LC_NUMERIC, $locale); 1813 return $out; 1814 } 1815 1816 public function compileFile($fname, $outFname = null) { 1817 if (!is_readable($fname)) { 1818 throw new Exception('load error: failed to find '.$fname); 1819 } 1820 1821 $pi = pathinfo($fname); 1822 1823 $oldImport = $this->importDir; 1824 1825 $this->importDir = (array)$this->importDir; 1826 $this->importDir[] = $pi['dirname'].'/'; 1827 1828 $this->addParsedFile($fname); 1829 1830 $out = $this->compile(file_get_contents($fname), $fname); 1831 1832 $this->importDir = $oldImport; 1833 1834 if ($outFname !== null) { 1835 return file_put_contents($outFname, $out); 1836 } 1837 1838 return $out; 1839 } 1840 1841 // compile only if changed input has changed or output doesn't exist 1842 public function checkedCompile($in, $out) { 1843 if (!is_file($out) || filemtime($in) > filemtime($out)) { 1844 $this->compileFile($in, $out); 1845 return true; 1846 } 1847 return false; 1848 } 1849 1850 /** 1851 * Execute lessphp on a .less file or a lessphp cache structure 1852 * 1853 * The lessphp cache structure contains information about a specific 1854 * less file having been parsed. It can be used as a hint for future 1855 * calls to determine whether or not a rebuild is required. 1856 * 1857 * The cache structure contains two important keys that may be used 1858 * externally: 1859 * 1860 * compiled: The final compiled CSS 1861 * updated: The time (in seconds) the CSS was last compiled 1862 * 1863 * The cache structure is a plain-ol' PHP associative array and can 1864 * be serialized and unserialized without a hitch. 1865 * 1866 * @param mixed $in Input 1867 * @param bool $force Force rebuild? 1868 * @return array lessphp cache structure 1869 */ 1870 public function cachedCompile($in, $force = false) { 1871 // assume no root 1872 $root = null; 1873 1874 if (is_string($in)) { 1875 $root = $in; 1876 } elseif (is_array($in) and isset($in['root'])) { 1877 if ($force or ! isset($in['files'])) { 1878 // If we are forcing a recompile or if for some reason the 1879 // structure does not contain any file information we should 1880 // specify the root to trigger a rebuild. 1881 $root = $in['root']; 1882 } elseif (isset($in['files']) and is_array($in['files'])) { 1883 foreach ($in['files'] as $fname => $ftime ) { 1884 if (!file_exists($fname) or filemtime($fname) > $ftime) { 1885 // One of the files we knew about previously has changed 1886 // so we should look at our incoming root again. 1887 $root = $in['root']; 1888 break; 1889 } 1890 } 1891 } 1892 } else { 1893 // TODO: Throw an exception? We got neither a string nor something 1894 // that looks like a compatible lessphp cache structure. 1895 return null; 1896 } 1897 1898 if ($root !== null) { 1899 // If we have a root value which means we should rebuild. 1900 $out = array(); 1901 $out['root'] = $root; 1902 $out['compiled'] = $this->compileFile($root); 1903 $out['files'] = $this->allParsedFiles(); 1904 $out['updated'] = time(); 1905 return $out; 1906 } else { 1907 // No changes, pass back the structure 1908 // we were given initially. 1909 return $in; 1910 } 1911 1912 } 1913 1914 // parse and compile buffer 1915 // This is deprecated 1916 public function parse($str = null, $initialVariables = null) { 1917 if (is_array($str)) { 1918 $initialVariables = $str; 1919 $str = null; 1920 } 1921 1922 $oldVars = $this->registeredVars; 1923 if ($initialVariables !== null) { 1924 $this->setVariables($initialVariables); 1925 } 1926 1927 if ($str == null) { 1928 if (empty($this->_parseFile)) { 1929 throw new exception("nothing to parse"); 1930 } 1931 1932 $out = $this->compileFile($this->_parseFile); 1933 } else { 1934 $out = $this->compile($str); 1935 } 1936 1937 $this->registeredVars = $oldVars; 1938 return $out; 1939 } 1940 1941 protected function makeParser($name) { 1942 $parser = new lessc_parser($this, $name); 1943 $parser->writeComments = $this->preserveComments; 1944 1945 return $parser; 1946 } 1947 1948 public function setFormatter($name) { 1949 $this->formatterName = $name; 1950 } 1951 1952 protected function newFormatter() { 1953 $className = "lessc_formatter_lessjs"; 1954 if (!empty($this->formatterName)) { 1955 if (!is_string($this->formatterName)) 1956 return $this->formatterName; 1957 $className = "lessc_formatter_$this->formatterName"; 1958 } 1959 1960 return new $className; 1961 } 1962 1963 public function setPreserveComments($preserve) { 1964 $this->preserveComments = $preserve; 1965 } 1966 1967 public function registerFunction($name, $func) { 1968 $this->libFunctions[$name] = $func; 1969 } 1970 1971 public function unregisterFunction($name) { 1972 unset($this->libFunctions[$name]); 1973 } 1974 1975 public function setVariables($variables) { 1976 $this->registeredVars = array_merge($this->registeredVars, $variables); 1977 } 1978 1979 public function unsetVariable($name) { 1980 unset($this->registeredVars[$name]); 1981 } 1982 1983 public function setImportDir($dirs) { 1984 $this->importDir = (array)$dirs; 1985 } 1986 1987 public function addImportDir($dir) { 1988 $this->importDir = (array)$this->importDir; 1989 $this->importDir[] = $dir; 1990 } 1991 1992 public function allParsedFiles() { 1993 return $this->allParsedFiles; 1994 } 1995 1996 protected function addParsedFile($file) { 1997 $this->allParsedFiles[realpath($file)] = filemtime($file); 1998 } 1999 2000 /** 2001 * Uses the current value of $this->count to show line and line number 2002 */ 2003 protected function throwError($msg = null) { 2004 if ($this->sourceLoc >= 0) { 2005 $this->sourceParser->throwError($msg, $this->sourceLoc); 2006 } 2007 throw new exception($msg); 2008 } 2009 2010 // compile file $in to file $out if $in is newer than $out 2011 // returns true when it compiles, false otherwise 2012 public static function ccompile($in, $out, $less = null) { 2013 if ($less === null) { 2014 $less = new self; 2015 } 2016 return $less->checkedCompile($in, $out); 2017 } 2018 2019 public static function cexecute($in, $force = false, $less = null) { 2020 if ($less === null) { 2021 $less = new self; 2022 } 2023 return $less->cachedCompile($in, $force); 2024 } 2025 2026 static protected $cssColors = array( 2027 'aliceblue' => '240,248,255', 2028 'antiquewhite' => '250,235,215', 2029 'aqua' => '0,255,255', 2030 'aquamarine' => '127,255,212', 2031 'azure' => '240,255,255', 2032 'beige' => '245,245,220', 2033 'bisque' => '255,228,196', 2034 'black' => '0,0,0', 2035 'blanchedalmond' => '255,235,205', 2036 'blue' => '0,0,255', 2037 'blueviolet' => '138,43,226', 2038 'brown' => '165,42,42', 2039 'burlywood' => '222,184,135', 2040 'cadetblue' => '95,158,160', 2041 'chartreuse' => '127,255,0', 2042 'chocolate' => '210,105,30', 2043 'coral' => '255,127,80', 2044 'cornflowerblue' => '100,149,237', 2045 'cornsilk' => '255,248,220', 2046 'crimson' => '220,20,60', 2047 'cyan' => '0,255,255', 2048 'darkblue' => '0,0,139', 2049 'darkcyan' => '0,139,139', 2050 'darkgoldenrod' => '184,134,11', 2051 'darkgray' => '169,169,169', 2052 'darkgreen' => '0,100,0', 2053 'darkgrey' => '169,169,169', 2054 'darkkhaki' => '189,183,107', 2055 'darkmagenta' => '139,0,139', 2056 'darkolivegreen' => '85,107,47', 2057 'darkorange' => '255,140,0', 2058 'darkorchid' => '153,50,204', 2059 'darkred' => '139,0,0', 2060 'darksalmon' => '233,150,122', 2061 'darkseagreen' => '143,188,143', 2062 'darkslateblue' => '72,61,139', 2063 'darkslategray' => '47,79,79', 2064 'darkslategrey' => '47,79,79', 2065 'darkturquoise' => '0,206,209', 2066 'darkviolet' => '148,0,211', 2067 'deeppink' => '255,20,147', 2068 'deepskyblue' => '0,191,255', 2069 'dimgray' => '105,105,105', 2070 'dimgrey' => '105,105,105', 2071 'dodgerblue' => '30,144,255', 2072 'firebrick' => '178,34,34', 2073 'floralwhite' => '255,250,240', 2074 'forestgreen' => '34,139,34', 2075 'fuchsia' => '255,0,255', 2076 'gainsboro' => '220,220,220', 2077 'ghostwhite' => '248,248,255', 2078 'gold' => '255,215,0', 2079 'goldenrod' => '218,165,32', 2080 'gray' => '128,128,128', 2081 'green' => '0,128,0', 2082 'greenyellow' => '173,255,47', 2083 'grey' => '128,128,128', 2084 'honeydew' => '240,255,240', 2085 'hotpink' => '255,105,180', 2086 'indianred' => '205,92,92', 2087 'indigo' => '75,0,130', 2088 'ivory' => '255,255,240', 2089 'khaki' => '240,230,140', 2090 'lavender' => '230,230,250', 2091 'lavenderblush' => '255,240,245', 2092 'lawngreen' => '124,252,0', 2093 'lemonchiffon' => '255,250,205', 2094 'lightblue' => '173,216,230', 2095 'lightcoral' => '240,128,128', 2096 'lightcyan' => '224,255,255', 2097 'lightgoldenrodyellow' => '250,250,210', 2098 'lightgray' => '211,211,211', 2099 'lightgreen' => '144,238,144', 2100 'lightgrey' => '211,211,211', 2101 'lightpink' => '255,182,193', 2102 'lightsalmon' => '255,160,122', 2103 'lightseagreen' => '32,178,170', 2104 'lightskyblue' => '135,206,250', 2105 'lightslategray' => '119,136,153', 2106 'lightslategrey' => '119,136,153', 2107 'lightsteelblue' => '176,196,222', 2108 'lightyellow' => '255,255,224', 2109 'lime' => '0,255,0', 2110 'limegreen' => '50,205,50', 2111 'linen' => '250,240,230', 2112 'magenta' => '255,0,255', 2113 'maroon' => '128,0,0', 2114 'mediumaquamarine' => '102,205,170', 2115 'mediumblue' => '0,0,205', 2116 'mediumorchid' => '186,85,211', 2117 'mediumpurple' => '147,112,219', 2118 'mediumseagreen' => '60,179,113', 2119 'mediumslateblue' => '123,104,238', 2120 'mediumspringgreen' => '0,250,154', 2121 'mediumturquoise' => '72,209,204', 2122 'mediumvioletred' => '199,21,133', 2123 'midnightblue' => '25,25,112', 2124 'mintcream' => '245,255,250', 2125 'mistyrose' => '255,228,225', 2126 'moccasin' => '255,228,181', 2127 'navajowhite' => '255,222,173', 2128 'navy' => '0,0,128', 2129 'oldlace' => '253,245,230', 2130 'olive' => '128,128,0', 2131 'olivedrab' => '107,142,35', 2132 'orange' => '255,165,0', 2133 'orangered' => '255,69,0', 2134 'orchid' => '218,112,214', 2135 'palegoldenrod' => '238,232,170', 2136 'palegreen' => '152,251,152', 2137 'paleturquoise' => '175,238,238', 2138 'palevioletred' => '219,112,147', 2139 'papayawhip' => '255,239,213', 2140 'peachpuff' => '255,218,185', 2141 'peru' => '205,133,63', 2142 'pink' => '255,192,203', 2143 'plum' => '221,160,221', 2144 'powderblue' => '176,224,230', 2145 'purple' => '128,0,128', 2146 'red' => '255,0,0', 2147 'rosybrown' => '188,143,143', 2148 'royalblue' => '65,105,225', 2149 'saddlebrown' => '139,69,19', 2150 'salmon' => '250,128,114', 2151 'sandybrown' => '244,164,96', 2152 'seagreen' => '46,139,87', 2153 'seashell' => '255,245,238', 2154 'sienna' => '160,82,45', 2155 'silver' => '192,192,192', 2156 'skyblue' => '135,206,235', 2157 'slateblue' => '106,90,205', 2158 'slategray' => '112,128,144', 2159 'slategrey' => '112,128,144', 2160 'snow' => '255,250,250', 2161 'springgreen' => '0,255,127', 2162 'steelblue' => '70,130,180', 2163 'tan' => '210,180,140', 2164 'teal' => '0,128,128', 2165 'thistle' => '216,191,216', 2166 'tomato' => '255,99,71', 2167 'transparent' => '0,0,0,0', 2168 'turquoise' => '64,224,208', 2169 'violet' => '238,130,238', 2170 'wheat' => '245,222,179', 2171 'white' => '255,255,255', 2172 'whitesmoke' => '245,245,245', 2173 'yellow' => '255,255,0', 2174 'yellowgreen' => '154,205,50' 2175 ); 2176 } 2177 2178 // responsible for taking a string of LESS code and converting it into a 2179 // syntax tree 2180 class lessc_parser { 2181 static protected $nextBlockId = 0; // used to uniquely identify blocks 2182 2183 static protected $precedence = array( 2184 '=<' => 0, 2185 '>=' => 0, 2186 '=' => 0, 2187 '<' => 0, 2188 '>' => 0, 2189 2190 '+' => 1, 2191 '-' => 1, 2192 '*' => 2, 2193 '/' => 2, 2194 '%' => 2, 2195 ); 2196 2197 static protected $whitePattern; 2198 static protected $commentMulti; 2199 2200 static protected $commentSingle = "//"; 2201 static protected $commentMultiLeft = "/*"; 2202 static protected $commentMultiRight = "*/"; 2203 2204 // regex string to match any of the operators 2205 static protected $operatorString; 2206 2207 // these properties will supress division unless it's inside parenthases 2208 static protected $supressDivisionProps = 2209 array('/border-radius$/i', '/^font$/i'); 2210 2211 protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport"); 2212 protected $lineDirectives = array("charset"); 2213 2214 /** 2215 * if we are in parens we can be more liberal with whitespace around 2216 * operators because it must evaluate to a single value and thus is less 2217 * ambiguous. 2218 * 2219 * Consider: 2220 * property1: 10 -5; // is two numbers, 10 and -5 2221 * property2: (10 -5); // should evaluate to 5 2222 */ 2223 protected $inParens = false; 2224 2225 // caches preg escaped literals 2226 static protected $literalCache = array(); 2227 2228 public function __construct($lessc, $sourceName = null) { 2229 $this->eatWhiteDefault = true; 2230 // reference to less needed for vPrefix, mPrefix, and parentSelector 2231 $this->lessc = $lessc; 2232 2233 $this->sourceName = $sourceName; // name used for error messages 2234 2235 $this->writeComments = false; 2236 2237 if (!self::$operatorString) { 2238 self::$operatorString = 2239 '('.implode('|', array_map(array('lessc', 'preg_quote'), 2240 array_keys(self::$precedence))).')'; 2241 2242 $commentSingle = lessc::preg_quote(self::$commentSingle); 2243 $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft); 2244 $commentMultiRight = lessc::preg_quote(self::$commentMultiRight); 2245 2246 self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; 2247 self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; 2248 } 2249 } 2250 2251 public function parse($buffer) { 2252 $this->count = 0; 2253 $this->line = 1; 2254 2255 $this->env = null; // block stack 2256 $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer); 2257 $this->pushSpecialBlock("root"); 2258 $this->eatWhiteDefault = true; 2259 $this->seenComments = array(); 2260 2261 // trim whitespace on head 2262 // if (preg_match('/^\s+/', $this->buffer, $m)) { 2263 // $this->line += substr_count($m[0], "\n"); 2264 // $this->buffer = ltrim($this->buffer); 2265 // } 2266 $this->whitespace(); 2267 2268 // parse the entire file 2269 $lastCount = $this->count; 2270 while (false !== $this->parseChunk()); 2271 2272 if ($this->count != strlen($this->buffer)) 2273 $this->throwError(); 2274 2275 // TODO report where the block was opened 2276 if (!is_null($this->env->parent)) 2277 throw new exception('parse error: unclosed block'); 2278 2279 return $this->env; 2280 } 2281 2282 /** 2283 * Parse a single chunk off the head of the buffer and append it to the 2284 * current parse environment. 2285 * Returns false when the buffer is empty, or when there is an error. 2286 * 2287 * This function is called repeatedly until the entire document is 2288 * parsed. 2289 * 2290 * This parser is most similar to a recursive descent parser. Single 2291 * functions represent discrete grammatical rules for the language, and 2292 * they are able to capture the text that represents those rules. 2293 * 2294 * Consider the function lessc::keyword(). (all parse functions are 2295 * structured the same) 2296 * 2297 * The function takes a single reference argument. When calling the 2298 * function it will attempt to match a keyword on the head of the buffer. 2299 * If it is successful, it will place the keyword in the referenced 2300 * argument, advance the position in the buffer, and return true. If it 2301 * fails then it won't advance the buffer and it will return false. 2302 * 2303 * All of these parse functions are powered by lessc::match(), which behaves 2304 * the same way, but takes a literal regular expression. Sometimes it is 2305 * more convenient to use match instead of creating a new function. 2306 * 2307 * Because of the format of the functions, to parse an entire string of 2308 * grammatical rules, you can chain them together using &&. 2309 * 2310 * But, if some of the rules in the chain succeed before one fails, then 2311 * the buffer position will be left at an invalid state. In order to 2312 * avoid this, lessc::seek() is used to remember and set buffer positions. 2313 * 2314 * Before parsing a chain, use $s = $this->seek() to remember the current 2315 * position into $s. Then if a chain fails, use $this->seek($s) to 2316 * go back where we started. 2317 */ 2318 protected function parseChunk() { 2319 if (empty($this->buffer)) return false; 2320 $s = $this->seek(); 2321 2322 // setting a property 2323 if ($this->keyword($key) && $this->assign() && 2324 $this->propertyValue($value, $key) && $this->end()) 2325 { 2326 $this->append(array('assign', $key, $value), $s); 2327 return true; 2328 } else { 2329 $this->seek($s); 2330 } 2331 2332 2333 // look for special css blocks 2334 if ($this->literal('@', false)) { 2335 $this->count--; 2336 2337 // media 2338 if ($this->literal('@media')) { 2339 if (($this->mediaQueryList($mediaQueries) || true) 2340 && $this->literal('{')) 2341 { 2342 $media = $this->pushSpecialBlock("media"); 2343 $media->queries = is_null($mediaQueries) ? array() : $mediaQueries; 2344 return true; 2345 } else { 2346 $this->seek($s); 2347 return false; 2348 } 2349 } 2350 2351 if ($this->literal("@", false) && $this->keyword($dirName)) { 2352 if ($this->isDirective($dirName, $this->blockDirectives)) { 2353 if (($this->openString("{", $dirValue, null, array(";")) || true) && 2354 $this->literal("{")) 2355 { 2356 $dir = $this->pushSpecialBlock("directive"); 2357 $dir->name = $dirName; 2358 if (isset($dirValue)) $dir->value = $dirValue; 2359 return true; 2360 } 2361 } elseif ($this->isDirective($dirName, $this->lineDirectives)) { 2362 if ($this->propertyValue($dirValue) && $this->end()) { 2363 $this->append(array("directive", $dirName, $dirValue)); 2364 return true; 2365 } 2366 } 2367 } 2368 2369 $this->seek($s); 2370 } 2371 2372 // setting a variable 2373 if ($this->variable($var) && $this->assign() && 2374 $this->propertyValue($value) && $this->end()) 2375 { 2376 $this->append(array('assign', $var, $value), $s); 2377 return true; 2378 } else { 2379 $this->seek($s); 2380 } 2381 2382 if ($this->import($importValue)) { 2383 $this->append($importValue, $s); 2384 return true; 2385 } 2386 2387 // opening parametric mixin 2388 if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && 2389 ($this->guards($guards) || true) && 2390 $this->literal('{')) 2391 { 2392 $block = $this->pushBlock($this->fixTags(array($tag))); 2393 $block->args = $args; 2394 $block->isVararg = $isVararg; 2395 if (!empty($guards)) $block->guards = $guards; 2396 return true; 2397 } else { 2398 $this->seek($s); 2399 } 2400 2401 // opening a simple block 2402 if ($this->tags($tags) && $this->literal('{')) { 2403 $tags = $this->fixTags($tags); 2404 $this->pushBlock($tags); 2405 return true; 2406 } else { 2407 $this->seek($s); 2408 } 2409 2410 // closing a block 2411 if ($this->literal('}', false)) { 2412 try { 2413 $block = $this->pop(); 2414 } catch (exception $e) { 2415 $this->seek($s); 2416 $this->throwError($e->getMessage()); 2417 } 2418 2419 $hidden = false; 2420 if (is_null($block->type)) { 2421 $hidden = true; 2422 if (!isset($block->args)) { 2423 foreach ($block->tags as $tag) { 2424 if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) { 2425 $hidden = false; 2426 break; 2427 } 2428 } 2429 } 2430 2431 foreach ($block->tags as $tag) { 2432 if (is_string($tag)) { 2433 $this->env->children[$tag][] = $block; 2434 } 2435 } 2436 } 2437 2438 if (!$hidden) { 2439 $this->append(array('block', $block), $s); 2440 } 2441 2442 // this is done here so comments aren't bundled into he block that 2443 // was just closed 2444 $this->whitespace(); 2445 return true; 2446 } 2447 2448 // mixin 2449 if ($this->mixinTags($tags) && 2450 ($this->argumentDef($argv, $isVararg) || true) && 2451 ($this->keyword($suffix) || true) && $this->end()) 2452 { 2453 $tags = $this->fixTags($tags); 2454 $this->append(array('mixin', $tags, $argv, $suffix), $s); 2455 return true; 2456 } else { 2457 $this->seek($s); 2458 } 2459 2460 // spare ; 2461 if ($this->literal(';')) return true; 2462 2463 return false; // got nothing, throw error 2464 } 2465 2466 protected function isDirective($dirname, $directives) { 2467 // TODO: cache pattern in parser 2468 $pattern = implode("|", 2469 array_map(array("lessc", "preg_quote"), $directives)); 2470 $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; 2471 2472 return preg_match($pattern, $dirname); 2473 } 2474 2475 protected function fixTags($tags) { 2476 // move @ tags out of variable namespace 2477 foreach ($tags as &$tag) { 2478 if ($tag{0} == $this->lessc->vPrefix) 2479 $tag[0] = $this->lessc->mPrefix; 2480 } 2481 return $tags; 2482 } 2483 2484 // a list of expressions 2485 protected function expressionList(&$exps) { 2486 $values = array(); 2487 2488 while ($this->expression($exp)) { 2489 $values[] = $exp; 2490 } 2491 2492 if (count($values) == 0) return false; 2493 2494 $exps = lessc::compressList($values, ' '); 2495 return true; 2496 } 2497 2498 /** 2499 * Attempt to consume an expression. 2500 * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code 2501 */ 2502 protected function expression(&$out) { 2503 if ($this->value($lhs)) { 2504 $out = $this->expHelper($lhs, 0); 2505 2506 // look for / shorthand 2507 if (!empty($this->env->supressedDivision)) { 2508 unset($this->env->supressedDivision); 2509 $s = $this->seek(); 2510 if ($this->literal("/") && $this->value($rhs)) { 2511 $out = array("list", "", 2512 array($out, array("keyword", "/"), $rhs)); 2513 } else { 2514 $this->seek($s); 2515 } 2516 } 2517 2518 return true; 2519 } 2520 return false; 2521 } 2522 2523 /** 2524 * recursively parse infix equation with $lhs at precedence $minP 2525 */ 2526 protected function expHelper($lhs, $minP) { 2527 $this->inExp = true; 2528 $ss = $this->seek(); 2529 2530 while (true) { 2531 $whiteBefore = isset($this->buffer[$this->count - 1]) && 2532 ctype_space($this->buffer[$this->count - 1]); 2533 2534 // If there is whitespace before the operator, then we require 2535 // whitespace after the operator for it to be an expression 2536 $needWhite = $whiteBefore && !$this->inParens; 2537 2538 if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) { 2539 if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) { 2540 foreach (self::$supressDivisionProps as $pattern) { 2541 if (preg_match($pattern, $this->env->currentProperty)) { 2542 $this->env->supressedDivision = true; 2543 break 2; 2544 } 2545 } 2546 } 2547 2548 2549 $whiteAfter = isset($this->buffer[$this->count - 1]) && 2550 ctype_space($this->buffer[$this->count - 1]); 2551 2552 if (!$this->value($rhs)) break; 2553 2554 // peek for next operator to see what to do with rhs 2555 if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) { 2556 $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); 2557 } 2558 2559 $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter); 2560 $ss = $this->seek(); 2561 2562 continue; 2563 } 2564 2565 break; 2566 } 2567 2568 $this->seek($ss); 2569 2570 return $lhs; 2571 } 2572 2573 // consume a list of values for a property 2574 public function propertyValue(&$value, $keyName = null) { 2575 $values = array(); 2576 2577 if ($keyName !== null) $this->env->currentProperty = $keyName; 2578 2579 $s = null; 2580 while ($this->expressionList($v)) { 2581 $values[] = $v; 2582 $s = $this->seek(); 2583 if (!$this->literal(',')) break; 2584 } 2585 2586 if ($s) $this->seek($s); 2587 2588 if ($keyName !== null) unset($this->env->currentProperty); 2589 2590 if (count($values) == 0) return false; 2591 2592 $value = lessc::compressList($values, ', '); 2593 return true; 2594 } 2595 2596 protected function parenValue(&$out) { 2597 $s = $this->seek(); 2598 2599 // speed shortcut 2600 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") { 2601 return false; 2602 } 2603 2604 $inParens = $this->inParens; 2605 if ($this->literal("(") && 2606 ($this->inParens = true) && $this->expression($exp) && 2607 $this->literal(")")) 2608 { 2609 $out = $exp; 2610 $this->inParens = $inParens; 2611 return true; 2612 } else { 2613 $this->inParens = $inParens; 2614 $this->seek($s); 2615 } 2616 2617 return false; 2618 } 2619 2620 // a single value 2621 protected function value(&$value) { 2622 $s = $this->seek(); 2623 2624 // speed shortcut 2625 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") { 2626 // negation 2627 if ($this->literal("-", false) && 2628 (($this->variable($inner) && $inner = array("variable", $inner)) || 2629 $this->unit($inner) || 2630 $this->parenValue($inner))) 2631 { 2632 $value = array("unary", "-", $inner); 2633 return true; 2634 } else { 2635 $this->seek($s); 2636 } 2637 } 2638 2639 if ($this->parenValue($value)) return true; 2640 if ($this->unit($value)) return true; 2641 if ($this->color($value)) return true; 2642 if ($this->func($value)) return true; 2643 if ($this->string($value)) return true; 2644 2645 if ($this->keyword($word)) { 2646 $value = array('keyword', $word); 2647 return true; 2648 } 2649 2650 // try a variable 2651 if ($this->variable($var)) { 2652 $value = array('variable', $var); 2653 return true; 2654 } 2655 2656 // unquote string (should this work on any type? 2657 if ($this->literal("~") && $this->string($str)) { 2658 $value = array("escape", $str); 2659 return true; 2660 } else { 2661 $this->seek($s); 2662 } 2663 2664 // css hack: \0 2665 if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { 2666 $value = array('keyword', '\\'.$m[1]); 2667 return true; 2668 } else { 2669 $this->seek($s); 2670 } 2671 2672 return false; 2673 } 2674 2675 // an import statement 2676 protected function import(&$out) { 2677 $s = $this->seek(); 2678 if (!$this->literal('@import')) return false; 2679 2680 // @import "something.css" media; 2681 // @import url("something.css") media; 2682 // @import url(something.css) media; 2683 2684 if ($this->propertyValue($value)) { 2685 $out = array("import", $value); 2686 return true; 2687 } 2688 } 2689 2690 protected function mediaQueryList(&$out) { 2691 if ($this->genericList($list, "mediaQuery", ",", false)) { 2692 $out = $list[2]; 2693 return true; 2694 } 2695 return false; 2696 } 2697 2698 protected function mediaQuery(&$out) { 2699 $s = $this->seek(); 2700 2701 $expressions = null; 2702 $parts = array(); 2703 2704 if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) { 2705 $prop = array("mediaType"); 2706 if (isset($only)) $prop[] = "only"; 2707 if (isset($not)) $prop[] = "not"; 2708 $prop[] = $mediaType; 2709 $parts[] = $prop; 2710 } else { 2711 $this->seek($s); 2712 } 2713 2714 2715 if (!empty($mediaType) && !$this->literal("and")) { 2716 // ~ 2717 } else { 2718 $this->genericList($expressions, "mediaExpression", "and", false); 2719 if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); 2720 } 2721 2722 if (count($parts) == 0) { 2723 $this->seek($s); 2724 return false; 2725 } 2726 2727 $out = $parts; 2728 return true; 2729 } 2730 2731 protected function mediaExpression(&$out) { 2732 $s = $this->seek(); 2733 $value = null; 2734 if ($this->literal("(") && 2735 $this->keyword($feature) && 2736 ($this->literal(":") && $this->expression($value) || true) && 2737 $this->literal(")")) 2738 { 2739 $out = array("mediaExp", $feature); 2740 if ($value) $out[] = $value; 2741 return true; 2742 } elseif ($this->variable($variable)) { 2743 $out = array('variable', $variable); 2744 return true; 2745 } 2746 2747 $this->seek($s); 2748 return false; 2749 } 2750 2751 // an unbounded string stopped by $end 2752 protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) { 2753 $oldWhite = $this->eatWhiteDefault; 2754 $this->eatWhiteDefault = false; 2755 2756 $stop = array("'", '"', "@{", $end); 2757 $stop = array_map(array("lessc", "preg_quote"), $stop); 2758 // $stop[] = self::$commentMulti; 2759 2760 if (!is_null($rejectStrs)) { 2761 $stop = array_merge($stop, $rejectStrs); 2762 } 2763 2764 $patt = '(.*?)('.implode("|", $stop).')'; 2765 2766 $nestingLevel = 0; 2767 2768 $content = array(); 2769 while ($this->match($patt, $m, false)) { 2770 if (!empty($m[1])) { 2771 $content[] = $m[1]; 2772 if ($nestingOpen) { 2773 $nestingLevel += substr_count($m[1], $nestingOpen); 2774 } 2775 } 2776 2777 $tok = $m[2]; 2778 2779 $this->count-= strlen($tok); 2780 if ($tok == $end) { 2781 if ($nestingLevel == 0) { 2782 break; 2783 } else { 2784 $nestingLevel--; 2785 } 2786 } 2787 2788 if (($tok == "'" || $tok == '"') && $this->string($str)) { 2789 $content[] = $str; 2790 continue; 2791 } 2792 2793 if ($tok == "@{" && $this->interpolation($inter)) { 2794 $content[] = $inter; 2795 continue; 2796 } 2797 2798 if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { 2799 break; 2800 } 2801 2802 $content[] = $tok; 2803 $this->count+= strlen($tok); 2804 } 2805 2806 $this->eatWhiteDefault = $oldWhite; 2807 2808 if (count($content) == 0) return false; 2809 2810 // trim the end 2811 if (is_string(end($content))) { 2812 $content[count($content) - 1] = rtrim(end($content)); 2813 } 2814 2815 $out = array("string", "", $content); 2816 return true; 2817 } 2818 2819 protected function string(&$out) { 2820 $s = $this->seek(); 2821 if ($this->literal('"', false)) { 2822 $delim = '"'; 2823 } elseif ($this->literal("'", false)) { 2824 $delim = "'"; 2825 } else { 2826 return false; 2827 } 2828 2829 $content = array(); 2830 2831 // look for either ending delim , escape, or string interpolation 2832 $patt = '([^\n]*?)(@\{|\\\\|' . 2833 lessc::preg_quote($delim).')'; 2834 2835 $oldWhite = $this->eatWhiteDefault; 2836 $this->eatWhiteDefault = false; 2837 2838 while ($this->match($patt, $m, false)) { 2839 $content[] = $m[1]; 2840 if ($m[2] == "@{") { 2841 $this->count -= strlen($m[2]); 2842 if ($this->interpolation($inter, false)) { 2843 $content[] = $inter; 2844 } else { 2845 $this->count += strlen($m[2]); 2846 $content[] = "@{"; // ignore it 2847 } 2848 } elseif ($m[2] == '\\') { 2849 $content[] = $m[2]; 2850 if ($this->literal($delim, false)) { 2851 $content[] = $delim; 2852 } 2853 } else { 2854 $this->count -= strlen($delim); 2855 break; // delim 2856 } 2857 } 2858 2859 $this->eatWhiteDefault = $oldWhite; 2860 2861 if ($this->literal($delim)) { 2862 $out = array("string", $delim, $content); 2863 return true; 2864 } 2865 2866 $this->seek($s); 2867 return false; 2868 } 2869 2870 protected function interpolation(&$out) { 2871 $oldWhite = $this->eatWhiteDefault; 2872 $this->eatWhiteDefault = true; 2873 2874 $s = $this->seek(); 2875 if ($this->literal("@{") && 2876 $this->openString("}", $interp, null, array("'", '"', ";")) && 2877 $this->literal("}", false)) 2878 { 2879 $out = array("interpolate", $interp); 2880 $this->eatWhiteDefault = $oldWhite; 2881 if ($this->eatWhiteDefault) $this->whitespace(); 2882 return true; 2883 } 2884 2885 $this->eatWhiteDefault = $oldWhite; 2886 $this->seek($s); 2887 return false; 2888 } 2889 2890 protected function unit(&$unit) { 2891 // speed shortcut 2892 if (isset($this->buffer[$this->count])) { 2893 $char = $this->buffer[$this->count]; 2894 if (!ctype_digit($char) && $char != ".") return false; 2895 } 2896 2897 if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { 2898 $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]); 2899 return true; 2900 } 2901 return false; 2902 } 2903 2904 // a # color 2905 protected function color(&$out) { 2906 if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { 2907 if (strlen($m[1]) > 7) { 2908 $out = array("string", "", array($m[1])); 2909 } else { 2910 $out = array("raw_color", $m[1]); 2911 } 2912 return true; 2913 } 2914 2915 return false; 2916 } 2917 2918 // consume an argument definition list surrounded by () 2919 // each argument is a variable name with optional value 2920 // or at the end a ... or a variable named followed by ... 2921 // arguments are separated by , unless a ; is in the list, then ; is the 2922 // delimiter. 2923 protected function argumentDef(&$args, &$isVararg) { 2924 $s = $this->seek(); 2925 if (!$this->literal('(')) return false; 2926 2927 $values = array(); 2928 $delim = ","; 2929 $method = "expressionList"; 2930 2931 $isVararg = false; 2932 while (true) { 2933 if ($this->literal("...")) { 2934 $isVararg = true; 2935 break; 2936 } 2937 2938 if ($this->$method($value)) { 2939 if ($value[0] == "variable") { 2940 $arg = array("arg", $value[1]); 2941 $ss = $this->seek(); 2942 2943 if ($this->assign() && $this->$method($rhs)) { 2944 $arg[] = $rhs; 2945 } else { 2946 $this->seek($ss); 2947 if ($this->literal("...")) { 2948 $arg[0] = "rest"; 2949 $isVararg = true; 2950 } 2951 } 2952 2953 $values[] = $arg; 2954 if ($isVararg) break; 2955 continue; 2956 } else { 2957 $values[] = array("lit", $value); 2958 } 2959 } 2960 2961 2962 if (!$this->literal($delim)) { 2963 if ($delim == "," && $this->literal(";")) { 2964 // found new delim, convert existing args 2965 $delim = ";"; 2966 $method = "propertyValue"; 2967 2968 // transform arg list 2969 if (isset($values[1])) { // 2 items 2970 $newList = array(); 2971 foreach ($values as $i => $arg) { 2972 switch($arg[0]) { 2973 case "arg": 2974 if ($i) { 2975 $this->throwError("Cannot mix ; and , as delimiter types"); 2976 } 2977 $newList[] = $arg[2]; 2978 break; 2979 case "lit": 2980 $newList[] = $arg[1]; 2981 break; 2982 case "rest": 2983 $this->throwError("Unexpected rest before semicolon"); 2984 } 2985 } 2986 2987 $newList = array("list", ", ", $newList); 2988 2989 switch ($values[0][0]) { 2990 case "arg": 2991 $newArg = array("arg", $values[0][1], $newList); 2992 break; 2993 case "lit": 2994 $newArg = array("lit", $newList); 2995 break; 2996 } 2997 2998 } elseif ($values) { // 1 item 2999 $newArg = $values[0]; 3000 } 3001 3002 if ($newArg) { 3003 $values = array($newArg); 3004 } 3005 } else { 3006 break; 3007 } 3008 } 3009 } 3010 3011 if (!$this->literal(')')) { 3012 $this->seek($s); 3013 return false; 3014 } 3015 3016 $args = $values; 3017 3018 return true; 3019 } 3020 3021 // consume a list of tags 3022 // this accepts a hanging delimiter 3023 protected function tags(&$tags, $simple = false, $delim = ',') { 3024 $tags = array(); 3025 while ($this->tag($tt, $simple)) { 3026 $tags[] = $tt; 3027 if (!$this->literal($delim)) break; 3028 } 3029 if (count($tags) == 0) return false; 3030 3031 return true; 3032 } 3033 3034 // list of tags of specifying mixin path 3035 // optionally separated by > (lazy, accepts extra >) 3036 protected function mixinTags(&$tags) { 3037 $s = $this->seek(); 3038 $tags = array(); 3039 while ($this->tag($tt, true)) { 3040 $tags[] = $tt; 3041 $this->literal(">"); 3042 } 3043 3044 if (count($tags) == 0) return false; 3045 3046 return true; 3047 } 3048 3049 // a bracketed value (contained within in a tag definition) 3050 protected function tagBracket(&$parts, &$hasExpression) { 3051 // speed shortcut 3052 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") { 3053 return false; 3054 } 3055 3056 $s = $this->seek(); 3057 3058 $hasInterpolation = false; 3059 3060 if ($this->literal("[", false)) { 3061 $attrParts = array("["); 3062 // keyword, string, operator 3063 while (true) { 3064 if ($this->literal("]", false)) { 3065 $this->count--; 3066 break; // get out early 3067 } 3068 3069 if ($this->match('\s+', $m)) { 3070 $attrParts[] = " "; 3071 continue; 3072 } 3073 if ($this->string($str)) { 3074 // escape parent selector, (yuck) 3075 foreach ($str[2] as &$chunk) { 3076 $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk); 3077 } 3078 3079 $attrParts[] = $str; 3080 $hasInterpolation = true; 3081 continue; 3082 } 3083 3084 if ($this->keyword($word)) { 3085 $attrParts[] = $word; 3086 continue; 3087 } 3088 3089 if ($this->interpolation($inter, false)) { 3090 $attrParts[] = $inter; 3091 $hasInterpolation = true; 3092 continue; 3093 } 3094 3095 // operator, handles attr namespace too 3096 if ($this->match('[|-~\$\*\^=]+', $m)) { 3097 $attrParts[] = $m[0]; 3098 continue; 3099 } 3100 3101 break; 3102 } 3103 3104 if ($this->literal("]", false)) { 3105 $attrParts[] = "]"; 3106 foreach ($attrParts as $part) { 3107 $parts[] = $part; 3108 } 3109 $hasExpression = $hasExpression || $hasInterpolation; 3110 return true; 3111 } 3112 $this->seek($s); 3113 } 3114 3115 $this->seek($s); 3116 return false; 3117 } 3118 3119 // a space separated list of selectors 3120 protected function tag(&$tag, $simple = false) { 3121 if ($simple) 3122 $chars = '^@,:;{}\][>\(\) "\''; 3123 else 3124 $chars = '^@,;{}["\''; 3125 3126 $s = $this->seek(); 3127 3128 $hasExpression = false; 3129 $parts = array(); 3130 while ($this->tagBracket($parts, $hasExpression)); 3131 3132 $oldWhite = $this->eatWhiteDefault; 3133 $this->eatWhiteDefault = false; 3134 3135 while (true) { 3136 if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) { 3137 $parts[] = $m[1]; 3138 if ($simple) break; 3139 3140 while ($this->tagBracket($parts, $hasExpression)); 3141 continue; 3142 } 3143 3144 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { 3145 if ($this->interpolation($interp)) { 3146 $hasExpression = true; 3147 $interp[2] = true; // don't unescape 3148 $parts[] = $interp; 3149 continue; 3150 } 3151 3152 if ($this->literal("@")) { 3153 $parts[] = "@"; 3154 continue; 3155 } 3156 } 3157 3158 if ($this->unit($unit)) { // for keyframes 3159 $parts[] = $unit[1]; 3160 $parts[] = $unit[2]; 3161 continue; 3162 } 3163 3164 break; 3165 } 3166 3167 $this->eatWhiteDefault = $oldWhite; 3168 if (!$parts) { 3169 $this->seek($s); 3170 return false; 3171 } 3172 3173 if ($hasExpression) { 3174 $tag = array("exp", array("string", "", $parts)); 3175 } else { 3176 $tag = trim(implode($parts)); 3177 } 3178 3179 $this->whitespace(); 3180 return true; 3181 } 3182 3183 // a css function 3184 protected function func(&$func) { 3185 $s = $this->seek(); 3186 3187 if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) { 3188 $fname = $m[1]; 3189 3190 $sPreArgs = $this->seek(); 3191 3192 $args = array(); 3193 while (true) { 3194 $ss = $this->seek(); 3195 // this ugly nonsense is for ie filter properties 3196 if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { 3197 $args[] = array("string", "", array($name, "=", $value)); 3198 } else { 3199 $this->seek($ss); 3200 if ($this->expressionList($value)) { 3201 $args[] = $value; 3202 } 3203 } 3204 3205 if (!$this->literal(',')) break; 3206 } 3207 $args = array('list', ',', $args); 3208 3209 if ($this->literal(')')) { 3210 $func = array('function', $fname, $args); 3211 return true; 3212 } elseif ($fname == 'url') { 3213 // couldn't parse and in url? treat as string 3214 $this->seek($sPreArgs); 3215 if ($this->openString(")", $string) && $this->literal(")")) { 3216 $func = array('function', $fname, $string); 3217 return true; 3218 } 3219 } 3220 } 3221 3222 $this->seek($s); 3223 return false; 3224 } 3225 3226 // consume a less variable 3227 protected function variable(&$name) { 3228 $s = $this->seek(); 3229 if ($this->literal($this->lessc->vPrefix, false) && 3230 ($this->variable($sub) || $this->keyword($name))) 3231 { 3232 if (!empty($sub)) { 3233 $name = array('variable', $sub); 3234 } else { 3235 $name = $this->lessc->vPrefix.$name; 3236 } 3237 return true; 3238 } 3239 3240 $name = null; 3241 $this->seek($s); 3242 return false; 3243 } 3244 3245 /** 3246 * Consume an assignment operator 3247 * Can optionally take a name that will be set to the current property name 3248 */ 3249 protected function assign($name = null) { 3250 if ($name) $this->currentProperty = $name; 3251 return $this->literal(':') || $this->literal('='); 3252 } 3253 3254 // consume a keyword 3255 protected function keyword(&$word) { 3256 if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { 3257 $word = $m[1]; 3258 return true; 3259 } 3260 return false; 3261 } 3262 3263 // consume an end of statement delimiter 3264 protected function end() { 3265 if ($this->literal(';')) { 3266 return true; 3267 } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { 3268 // if there is end of file or a closing block next then we don't need a ; 3269 return true; 3270 } 3271 return false; 3272 } 3273 3274 protected function guards(&$guards) { 3275 $s = $this->seek(); 3276 3277 if (!$this->literal("when")) { 3278 $this->seek($s); 3279 return false; 3280 } 3281 3282 $guards = array(); 3283 3284 while ($this->guardGroup($g)) { 3285 $guards[] = $g; 3286 if (!$this->literal(",")) break; 3287 } 3288 3289 if (count($guards) == 0) { 3290 $guards = null; 3291 $this->seek($s); 3292 return false; 3293 } 3294 3295 return true; 3296 } 3297 3298 // a bunch of guards that are and'd together 3299 // TODO rename to guardGroup 3300 protected function guardGroup(&$guardGroup) { 3301 $s = $this->seek(); 3302 $guardGroup = array(); 3303 while ($this->guard($guard)) { 3304 $guardGroup[] = $guard; 3305 if (!$this->literal("and")) break; 3306 } 3307 3308 if (count($guardGroup) == 0) { 3309 $guardGroup = null; 3310 $this->seek($s); 3311 return false; 3312 } 3313 3314 return true; 3315 } 3316 3317 protected function guard(&$guard) { 3318 $s = $this->seek(); 3319 $negate = $this->literal("not"); 3320 3321 if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) { 3322 $guard = $exp; 3323 if ($negate) $guard = array("negate", $guard); 3324 return true; 3325 } 3326 3327 $this->seek($s); 3328 return false; 3329 } 3330 3331 /* raw parsing functions */ 3332 3333 protected function literal($what, $eatWhitespace = null) { 3334 if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 3335 3336 // shortcut on single letter 3337 if (!isset($what[1]) && isset($this->buffer[$this->count])) { 3338 if ($this->buffer[$this->count] == $what) { 3339 if (!$eatWhitespace) { 3340 $this->count++; 3341 return true; 3342 } 3343 // goes below... 3344 } else { 3345 return false; 3346 } 3347 } 3348 3349 if (!isset(self::$literalCache[$what])) { 3350 self::$literalCache[$what] = lessc::preg_quote($what); 3351 } 3352 3353 return $this->match(self::$literalCache[$what], $m, $eatWhitespace); 3354 } 3355 3356 protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { 3357 $s = $this->seek(); 3358 $items = array(); 3359 while ($this->$parseItem($value)) { 3360 $items[] = $value; 3361 if ($delim) { 3362 if (!$this->literal($delim)) break; 3363 } 3364 } 3365 3366 if (count($items) == 0) { 3367 $this->seek($s); 3368 return false; 3369 } 3370 3371 if ($flatten && count($items) == 1) { 3372 $out = $items[0]; 3373 } else { 3374 $out = array("list", $delim, $items); 3375 } 3376 3377 return true; 3378 } 3379 3380 3381 // advance counter to next occurrence of $what 3382 // $until - don't include $what in advance 3383 // $allowNewline, if string, will be used as valid char set 3384 protected function to($what, &$out, $until = false, $allowNewline = false) { 3385 if (is_string($allowNewline)) { 3386 $validChars = $allowNewline; 3387 } else { 3388 $validChars = $allowNewline ? "." : "[^\n]"; 3389 } 3390 if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false; 3391 if ($until) $this->count -= strlen($what); // give back $what 3392 $out = $m[1]; 3393 return true; 3394 } 3395 3396 // try to match something on head of buffer 3397 protected function match($regex, &$out, $eatWhitespace = null) { 3398 if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 3399 3400 $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais'; 3401 if (preg_match($r, $this->buffer, $out, null, $this->count)) { 3402 $this->count += strlen($out[0]); 3403 if ($eatWhitespace && $this->writeComments) $this->whitespace(); 3404 return true; 3405 } 3406 return false; 3407 } 3408 3409 // match some whitespace 3410 protected function whitespace() { 3411 if ($this->writeComments) { 3412 $gotWhite = false; 3413 while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { 3414 if (isset($m[1]) && empty($this->commentsSeen[$this->count])) { 3415 $this->append(array("comment", $m[1])); 3416 $this->commentsSeen[$this->count] = true; 3417 } 3418 $this->count += strlen($m[0]); 3419 $gotWhite = true; 3420 } 3421 return $gotWhite; 3422 } else { 3423 $this->match("", $m); 3424 return strlen($m[0]) > 0; 3425 } 3426 } 3427 3428 // match something without consuming it 3429 protected function peek($regex, &$out = null, $from=null) { 3430 if (is_null($from)) $from = $this->count; 3431 $r = '/'.$regex.'/Ais'; 3432 $result = preg_match($r, $this->buffer, $out, null, $from); 3433 3434 return $result; 3435 } 3436 3437 // seek to a spot in the buffer or return where we are on no argument 3438 protected function seek($where = null) { 3439 if ($where === null) return $this->count; 3440 else $this->count = $where; 3441 return true; 3442 } 3443 3444 /* misc functions */ 3445 3446 public function throwError($msg = "parse error", $count = null) { 3447 $count = is_null($count) ? $this->count : $count; 3448 3449 $line = $this->line + 3450 substr_count(substr($this->buffer, 0, $count), "\n"); 3451 3452 if (!empty($this->sourceName)) { 3453 $loc = "$this->sourceName on line $line"; 3454 } else { 3455 $loc = "line: $line"; 3456 } 3457 3458 // TODO this depends on $this->count 3459 if ($this->peek("(.*?)(\n|$)", $m, $count)) { 3460 throw new exception("$msg: failed at `$m[1]` $loc"); 3461 } else { 3462 throw new exception("$msg: $loc"); 3463 } 3464 } 3465 3466 protected function pushBlock($selectors=null, $type=null) { 3467 $b = new stdclass; 3468 $b->parent = $this->env; 3469 3470 $b->type = $type; 3471 $b->id = self::$nextBlockId++; 3472 3473 $b->isVararg = false; // TODO: kill me from here 3474 $b->tags = $selectors; 3475 3476 $b->props = array(); 3477 $b->children = array(); 3478 3479 $this->env = $b; 3480 return $b; 3481 } 3482 3483 // push a block that doesn't multiply tags 3484 protected function pushSpecialBlock($type) { 3485 return $this->pushBlock(null, $type); 3486 } 3487 3488 // append a property to the current block 3489 protected function append($prop, $pos = null) { 3490 if ($pos !== null) $prop[-1] = $pos; 3491 $this->env->props[] = $prop; 3492 } 3493 3494 // pop something off the stack 3495 protected function pop() { 3496 $old = $this->env; 3497 $this->env = $this->env->parent; 3498 return $old; 3499 } 3500 3501 // remove comments from $text 3502 // todo: make it work for all functions, not just url 3503 protected function removeComments($text) { 3504 $look = array( 3505 'url(', '//', '/*', '"', "'" 3506 ); 3507 3508 $out = ''; 3509 $min = null; 3510 while (true) { 3511 // find the next item 3512 foreach ($look as $token) { 3513 $pos = strpos($text, $token); 3514 if ($pos !== false) { 3515 if (!isset($min) || $pos < $min[1]) $min = array($token, $pos); 3516 } 3517 } 3518 3519 if (is_null($min)) break; 3520 3521 $count = $min[1]; 3522 $skip = 0; 3523 $newlines = 0; 3524 switch ($min[0]) { 3525 case 'url(': 3526 if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) 3527 $count += strlen($m[0]) - strlen($min[0]); 3528 break; 3529 case '"': 3530 case "'": 3531 if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count)) 3532 $count += strlen($m[0]) - 1; 3533 break; 3534 case '//': 3535 $skip = strpos($text, "\n", $count); 3536 if ($skip === false) $skip = strlen($text) - $count; 3537 else $skip -= $count; 3538 break; 3539 case '/*': 3540 if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) { 3541 $skip = strlen($m[0]); 3542 $newlines = substr_count($m[0], "\n"); 3543 } 3544 break; 3545 } 3546 3547 if ($skip == 0) $count += strlen($min[0]); 3548 3549 $out .= substr($text, 0, $count).str_repeat("\n", $newlines); 3550 $text = substr($text, $count + $skip); 3551 3552 $min = null; 3553 } 3554 3555 return $out.$text; 3556 } 3557 3558 } 3559 3560 class lessc_formatter_classic { 3561 public $indentChar = " "; 3562 3563 public $break = "\n"; 3564 public $open = " {"; 3565 public $close = "}"; 3566 public $selectorSeparator = ", "; 3567 public $assignSeparator = ":"; 3568 3569 public $openSingle = " { "; 3570 public $closeSingle = " }"; 3571 3572 public $disableSingle = false; 3573 public $breakSelectors = false; 3574 3575 public $compressColors = false; 3576 3577 public function __construct() { 3578 $this->indentLevel = 0; 3579 } 3580 3581 public function indentStr($n = 0) { 3582 return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); 3583 } 3584 3585 public function property($name, $value) { 3586 return $name . $this->assignSeparator . $value . ";"; 3587 } 3588 3589 protected function isEmpty($block) { 3590 if (empty($block->lines)) { 3591 foreach ($block->children as $child) { 3592 if (!$this->isEmpty($child)) return false; 3593 } 3594 3595 return true; 3596 } 3597 return false; 3598 } 3599 3600 public function block($block) { 3601 if ($this->isEmpty($block)) return; 3602 3603 $inner = $pre = $this->indentStr(); 3604 3605 $isSingle = !$this->disableSingle && 3606 is_null($block->type) && count($block->lines) == 1; 3607 3608 if (!empty($block->selectors)) { 3609 $this->indentLevel++; 3610 3611 if ($this->breakSelectors) { 3612 $selectorSeparator = $this->selectorSeparator . $this->break . $pre; 3613 } else { 3614 $selectorSeparator = $this->selectorSeparator; 3615 } 3616 3617 echo $pre . 3618 implode($selectorSeparator, $block->selectors); 3619 if ($isSingle) { 3620 echo $this->openSingle; 3621 $inner = ""; 3622 } else { 3623 echo $this->open . $this->break; 3624 $inner = $this->indentStr(); 3625 } 3626 3627 } 3628 3629 if (!empty($block->lines)) { 3630 $glue = $this->break.$inner; 3631 echo $inner . implode($glue, $block->lines); 3632 if (!$isSingle && !empty($block->children)) { 3633 echo $this->break; 3634 } 3635 } 3636 3637 foreach ($block->children as $child) { 3638 $this->block($child); 3639 } 3640 3641 if (!empty($block->selectors)) { 3642 if (!$isSingle && empty($block->children)) echo $this->break; 3643 3644 if ($isSingle) { 3645 echo $this->closeSingle . $this->break; 3646 } else { 3647 echo $pre . $this->close . $this->break; 3648 } 3649 3650 $this->indentLevel--; 3651 } 3652 } 3653 } 3654 3655 class lessc_formatter_compressed extends lessc_formatter_classic { 3656 public $disableSingle = true; 3657 public $open = "{"; 3658 public $selectorSeparator = ","; 3659 public $assignSeparator = ":"; 3660 public $break = ""; 3661 public $compressColors = true; 3662 3663 public function indentStr($n = 0) { 3664 return ""; 3665 } 3666 } 3667 3668 class lessc_formatter_lessjs extends lessc_formatter_classic { 3669 public $disableSingle = true; 3670 public $breakSelectors = true; 3671 public $assignSeparator = ": "; 3672 public $selectorSeparator = ","; 3673 } 3674 3675