MediaWiki
REL1_22
|
00001 <?php 00002 00068 class lessc { 00069 static public $VERSION = "v0.4.0"; 00070 00071 static public $TRUE = array("keyword", "true"); 00072 static public $FALSE = array("keyword", "false"); 00073 00074 protected $libFunctions = array(); 00075 protected $registeredVars = array(); 00076 protected $preserveComments = false; 00077 00078 public $vPrefix = '@'; // prefix of abstract properties 00079 public $mPrefix = '$'; // prefix of abstract blocks 00080 public $parentSelector = '&'; 00081 00082 public $importDisabled = false; 00083 public $importDir = ''; 00084 00085 protected $numberPrecision = null; 00086 00087 protected $allParsedFiles = array(); 00088 00089 // set to the parser that generated the current line when compiling 00090 // so we know how to create error messages 00091 protected $sourceParser = null; 00092 protected $sourceLoc = null; 00093 00094 static protected $nextImportId = 0; // uniquely identify imports 00095 00096 // attempts to find the path of an import url, returns null for css files 00097 protected function findImport($url) { 00098 foreach ((array)$this->importDir as $dir) { 00099 $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url; 00100 if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) { 00101 return $file; 00102 } 00103 } 00104 00105 return null; 00106 } 00107 00108 protected function fileExists($name) { 00109 return is_file($name); 00110 } 00111 00112 static public function compressList($items, $delim) { 00113 if (!isset($items[1]) && isset($items[0])) return $items[0]; 00114 else return array('list', $delim, $items); 00115 } 00116 00117 static public function preg_quote($what) { 00118 return preg_quote($what, '/'); 00119 } 00120 00121 protected function tryImport($importPath, $parentBlock, $out) { 00122 if ($importPath[0] == "function" && $importPath[1] == "url") { 00123 $importPath = $this->flattenList($importPath[2]); 00124 } 00125 00126 $str = $this->coerceString($importPath); 00127 if ($str === null) return false; 00128 00129 $url = $this->compileValue($this->lib_e($str)); 00130 00131 // don't import if it ends in css 00132 if (substr_compare($url, '.css', -4, 4) === 0) return false; 00133 00134 $realPath = $this->findImport($url); 00135 00136 if ($realPath === null) return false; 00137 00138 if ($this->importDisabled) { 00139 return array(false, "/* import disabled */"); 00140 } 00141 00142 if (isset($this->allParsedFiles[realpath($realPath)])) { 00143 return array(false, null); 00144 } 00145 00146 $this->addParsedFile($realPath); 00147 $parser = $this->makeParser($realPath); 00148 $root = $parser->parse(file_get_contents($realPath)); 00149 00150 // set the parents of all the block props 00151 foreach ($root->props as $prop) { 00152 if ($prop[0] == "block") { 00153 $prop[1]->parent = $parentBlock; 00154 } 00155 } 00156 00157 // copy mixins into scope, set their parents 00158 // bring blocks from import into current block 00159 // TODO: need to mark the source parser these came from this file 00160 foreach ($root->children as $childName => $child) { 00161 if (isset($parentBlock->children[$childName])) { 00162 $parentBlock->children[$childName] = array_merge( 00163 $parentBlock->children[$childName], 00164 $child); 00165 } else { 00166 $parentBlock->children[$childName] = $child; 00167 } 00168 } 00169 00170 $pi = pathinfo($realPath); 00171 $dir = $pi["dirname"]; 00172 00173 list($top, $bottom) = $this->sortProps($root->props, true); 00174 $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir); 00175 00176 return array(true, $bottom, $parser, $dir); 00177 } 00178 00179 protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) { 00180 $oldSourceParser = $this->sourceParser; 00181 00182 $oldImport = $this->importDir; 00183 00184 // TODO: this is because the importDir api is stupid 00185 $this->importDir = (array)$this->importDir; 00186 array_unshift($this->importDir, $importDir); 00187 00188 foreach ($props as $prop) { 00189 $this->compileProp($prop, $block, $out); 00190 } 00191 00192 $this->importDir = $oldImport; 00193 $this->sourceParser = $oldSourceParser; 00194 } 00195 00217 protected function compileBlock($block) { 00218 switch ($block->type) { 00219 case "root": 00220 $this->compileRoot($block); 00221 break; 00222 case null: 00223 $this->compileCSSBlock($block); 00224 break; 00225 case "media": 00226 $this->compileMedia($block); 00227 break; 00228 case "directive": 00229 $name = "@" . $block->name; 00230 if (!empty($block->value)) { 00231 $name .= " " . $this->compileValue($this->reduce($block->value)); 00232 } 00233 00234 $this->compileNestedBlock($block, array($name)); 00235 break; 00236 default: 00237 $this->throwError("unknown block type: $block->type\n"); 00238 } 00239 } 00240 00241 protected function compileCSSBlock($block) { 00242 $env = $this->pushEnv(); 00243 00244 $selectors = $this->compileSelectors($block->tags); 00245 $env->selectors = $this->multiplySelectors($selectors); 00246 $out = $this->makeOutputBlock(null, $env->selectors); 00247 00248 $this->scope->children[] = $out; 00249 $this->compileProps($block, $out); 00250 00251 $block->scope = $env; // mixins carry scope with them! 00252 $this->popEnv(); 00253 } 00254 00255 protected function compileMedia($media) { 00256 $env = $this->pushEnv($media); 00257 $parentScope = $this->mediaParent($this->scope); 00258 00259 $query = $this->compileMediaQuery($this->multiplyMedia($env)); 00260 00261 $this->scope = $this->makeOutputBlock($media->type, array($query)); 00262 $parentScope->children[] = $this->scope; 00263 00264 $this->compileProps($media, $this->scope); 00265 00266 if (count($this->scope->lines) > 0) { 00267 $orphanSelelectors = $this->findClosestSelectors(); 00268 if (!is_null($orphanSelelectors)) { 00269 $orphan = $this->makeOutputBlock(null, $orphanSelelectors); 00270 $orphan->lines = $this->scope->lines; 00271 array_unshift($this->scope->children, $orphan); 00272 $this->scope->lines = array(); 00273 } 00274 } 00275 00276 $this->scope = $this->scope->parent; 00277 $this->popEnv(); 00278 } 00279 00280 protected function mediaParent($scope) { 00281 while (!empty($scope->parent)) { 00282 if (!empty($scope->type) && $scope->type != "media") { 00283 break; 00284 } 00285 $scope = $scope->parent; 00286 } 00287 00288 return $scope; 00289 } 00290 00291 protected function compileNestedBlock($block, $selectors) { 00292 $this->pushEnv($block); 00293 $this->scope = $this->makeOutputBlock($block->type, $selectors); 00294 $this->scope->parent->children[] = $this->scope; 00295 00296 $this->compileProps($block, $this->scope); 00297 00298 $this->scope = $this->scope->parent; 00299 $this->popEnv(); 00300 } 00301 00302 protected function compileRoot($root) { 00303 $this->pushEnv(); 00304 $this->scope = $this->makeOutputBlock($root->type); 00305 $this->compileProps($root, $this->scope); 00306 $this->popEnv(); 00307 } 00308 00309 protected function compileProps($block, $out) { 00310 foreach ($this->sortProps($block->props) as $prop) { 00311 $this->compileProp($prop, $block, $out); 00312 } 00313 $out->lines = $this->deduplicate($out->lines); 00314 } 00315 00321 protected function deduplicate($lines) { 00322 $unique = array(); 00323 $comments = array(); 00324 00325 foreach($lines as $line) { 00326 if (strpos($line, '/*') === 0) { 00327 $comments[] = $line; 00328 continue; 00329 } 00330 if (!in_array($line, $unique)) { 00331 $unique[] = $line; 00332 } 00333 array_splice($unique, array_search($line, $unique), 0, $comments); 00334 $comments = array(); 00335 } 00336 return array_merge($unique, $comments); 00337 } 00338 00339 protected function sortProps($props, $split = false) { 00340 $vars = array(); 00341 $imports = array(); 00342 $other = array(); 00343 $stack = array(); 00344 00345 foreach ($props as $prop) { 00346 switch ($prop[0]) { 00347 case "comment": 00348 $stack[] = $prop; 00349 break; 00350 case "assign": 00351 $stack[] = $prop; 00352 if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) { 00353 $vars = array_merge($vars, $stack); 00354 } else { 00355 $other = array_merge($other, $stack); 00356 } 00357 $stack = array(); 00358 break; 00359 case "import": 00360 $id = self::$nextImportId++; 00361 $prop[] = $id; 00362 $stack[] = $prop; 00363 $imports = array_merge($imports, $stack); 00364 $other[] = array("import_mixin", $id); 00365 $stack = array(); 00366 break; 00367 default: 00368 $stack[] = $prop; 00369 $other = array_merge($other, $stack); 00370 $stack = array(); 00371 break; 00372 } 00373 } 00374 $other = array_merge($other, $stack); 00375 00376 if ($split) { 00377 return array(array_merge($vars, $imports), $other); 00378 } else { 00379 return array_merge($vars, $imports, $other); 00380 } 00381 } 00382 00383 protected function compileMediaQuery($queries) { 00384 $compiledQueries = array(); 00385 foreach ($queries as $query) { 00386 $parts = array(); 00387 foreach ($query as $q) { 00388 switch ($q[0]) { 00389 case "mediaType": 00390 $parts[] = implode(" ", array_slice($q, 1)); 00391 break; 00392 case "mediaExp": 00393 if (isset($q[2])) { 00394 $parts[] = "($q[1]: " . 00395 $this->compileValue($this->reduce($q[2])) . ")"; 00396 } else { 00397 $parts[] = "($q[1])"; 00398 } 00399 break; 00400 case "variable": 00401 $parts[] = $this->compileValue($this->reduce($q)); 00402 break; 00403 } 00404 } 00405 00406 if (count($parts) > 0) { 00407 $compiledQueries[] = implode(" and ", $parts); 00408 } 00409 } 00410 00411 $out = "@media"; 00412 if (!empty($parts)) { 00413 $out .= " " . 00414 implode($this->formatter->selectorSeparator, $compiledQueries); 00415 } 00416 return $out; 00417 } 00418 00419 protected function multiplyMedia($env, $childQueries = null) { 00420 if (is_null($env) || 00421 !empty($env->block->type) && $env->block->type != "media") 00422 { 00423 return $childQueries; 00424 } 00425 00426 // plain old block, skip 00427 if (empty($env->block->type)) { 00428 return $this->multiplyMedia($env->parent, $childQueries); 00429 } 00430 00431 $out = array(); 00432 $queries = $env->block->queries; 00433 if (is_null($childQueries)) { 00434 $out = $queries; 00435 } else { 00436 foreach ($queries as $parent) { 00437 foreach ($childQueries as $child) { 00438 $out[] = array_merge($parent, $child); 00439 } 00440 } 00441 } 00442 00443 return $this->multiplyMedia($env->parent, $out); 00444 } 00445 00446 protected function expandParentSelectors(&$tag, $replace) { 00447 $parts = explode("$&$", $tag); 00448 $count = 0; 00449 foreach ($parts as &$part) { 00450 $part = str_replace($this->parentSelector, $replace, $part, $c); 00451 $count += $c; 00452 } 00453 $tag = implode($this->parentSelector, $parts); 00454 return $count; 00455 } 00456 00457 protected function findClosestSelectors() { 00458 $env = $this->env; 00459 $selectors = null; 00460 while ($env !== null) { 00461 if (isset($env->selectors)) { 00462 $selectors = $env->selectors; 00463 break; 00464 } 00465 $env = $env->parent; 00466 } 00467 00468 return $selectors; 00469 } 00470 00471 00472 // multiply $selectors against the nearest selectors in env 00473 protected function multiplySelectors($selectors) { 00474 // find parent selectors 00475 00476 $parentSelectors = $this->findClosestSelectors(); 00477 if (is_null($parentSelectors)) { 00478 // kill parent reference in top level selector 00479 foreach ($selectors as &$s) { 00480 $this->expandParentSelectors($s, ""); 00481 } 00482 00483 return $selectors; 00484 } 00485 00486 $out = array(); 00487 foreach ($parentSelectors as $parent) { 00488 foreach ($selectors as $child) { 00489 $count = $this->expandParentSelectors($child, $parent); 00490 00491 // don't prepend the parent tag if & was used 00492 if ($count > 0) { 00493 $out[] = trim($child); 00494 } else { 00495 $out[] = trim($parent . ' ' . $child); 00496 } 00497 } 00498 } 00499 00500 return $out; 00501 } 00502 00503 // reduces selector expressions 00504 protected function compileSelectors($selectors) { 00505 $out = array(); 00506 00507 foreach ($selectors as $s) { 00508 if (is_array($s)) { 00509 list(, $value) = $s; 00510 $out[] = trim($this->compileValue($this->reduce($value))); 00511 } else { 00512 $out[] = $s; 00513 } 00514 } 00515 00516 return $out; 00517 } 00518 00519 protected function eq($left, $right) { 00520 return $left == $right; 00521 } 00522 00523 protected function patternMatch($block, $orderedArgs, $keywordArgs) { 00524 // match the guards if it has them 00525 // any one of the groups must have all its guards pass for a match 00526 if (!empty($block->guards)) { 00527 $groupPassed = false; 00528 foreach ($block->guards as $guardGroup) { 00529 foreach ($guardGroup as $guard) { 00530 $this->pushEnv(); 00531 $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); 00532 00533 $negate = false; 00534 if ($guard[0] == "negate") { 00535 $guard = $guard[1]; 00536 $negate = true; 00537 } 00538 00539 $passed = $this->reduce($guard) == self::$TRUE; 00540 if ($negate) $passed = !$passed; 00541 00542 $this->popEnv(); 00543 00544 if ($passed) { 00545 $groupPassed = true; 00546 } else { 00547 $groupPassed = false; 00548 break; 00549 } 00550 } 00551 00552 if ($groupPassed) break; 00553 } 00554 00555 if (!$groupPassed) { 00556 return false; 00557 } 00558 } 00559 00560 if (empty($block->args)) { 00561 return $block->isVararg || empty($orderedArgs) && empty($keywordArgs); 00562 } 00563 00564 $remainingArgs = $block->args; 00565 if ($keywordArgs) { 00566 $remainingArgs = array(); 00567 foreach ($block->args as $arg) { 00568 if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) { 00569 continue; 00570 } 00571 00572 $remainingArgs[] = $arg; 00573 } 00574 } 00575 00576 $i = -1; // no args 00577 // try to match by arity or by argument literal 00578 foreach ($remainingArgs as $i => $arg) { 00579 switch ($arg[0]) { 00580 case "lit": 00581 if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) { 00582 return false; 00583 } 00584 break; 00585 case "arg": 00586 // no arg and no default value 00587 if (!isset($orderedArgs[$i]) && !isset($arg[2])) { 00588 return false; 00589 } 00590 break; 00591 case "rest": 00592 $i--; // rest can be empty 00593 break 2; 00594 } 00595 } 00596 00597 if ($block->isVararg) { 00598 return true; // not having enough is handled above 00599 } else { 00600 $numMatched = $i + 1; 00601 // greater than becuase default values always match 00602 return $numMatched >= count($orderedArgs); 00603 } 00604 } 00605 00606 protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) { 00607 $matches = null; 00608 foreach ($blocks as $block) { 00609 // skip seen blocks that don't have arguments 00610 if (isset($skip[$block->id]) && !isset($block->args)) { 00611 continue; 00612 } 00613 00614 if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) { 00615 $matches[] = $block; 00616 } 00617 } 00618 00619 return $matches; 00620 } 00621 00622 // attempt to find blocks matched by path and args 00623 protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) { 00624 if ($searchIn == null) return null; 00625 if (isset($seen[$searchIn->id])) return null; 00626 $seen[$searchIn->id] = true; 00627 00628 $name = $path[0]; 00629 00630 if (isset($searchIn->children[$name])) { 00631 $blocks = $searchIn->children[$name]; 00632 if (count($path) == 1) { 00633 $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen); 00634 if (!empty($matches)) { 00635 // This will return all blocks that match in the closest 00636 // scope that has any matching block, like lessjs 00637 return $matches; 00638 } 00639 } else { 00640 $matches = array(); 00641 foreach ($blocks as $subBlock) { 00642 $subMatches = $this->findBlocks($subBlock, 00643 array_slice($path, 1), $orderedArgs, $keywordArgs, $seen); 00644 00645 if (!is_null($subMatches)) { 00646 foreach ($subMatches as $sm) { 00647 $matches[] = $sm; 00648 } 00649 } 00650 } 00651 00652 return count($matches) > 0 ? $matches : null; 00653 } 00654 } 00655 if ($searchIn->parent === $searchIn) return null; 00656 return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen); 00657 } 00658 00659 // sets all argument names in $args to either the default value 00660 // or the one passed in through $values 00661 protected function zipSetArgs($args, $orderedValues, $keywordValues) { 00662 $assignedValues = array(); 00663 00664 $i = 0; 00665 foreach ($args as $a) { 00666 if ($a[0] == "arg") { 00667 if (isset($keywordValues[$a[1]])) { 00668 // has keyword arg 00669 $value = $keywordValues[$a[1]]; 00670 } elseif (isset($orderedValues[$i])) { 00671 // has ordered arg 00672 $value = $orderedValues[$i]; 00673 $i++; 00674 } elseif (isset($a[2])) { 00675 // has default value 00676 $value = $a[2]; 00677 } else { 00678 $this->throwError("Failed to assign arg " . $a[1]); 00679 $value = null; // :( 00680 } 00681 00682 $value = $this->reduce($value); 00683 $this->set($a[1], $value); 00684 $assignedValues[] = $value; 00685 } else { 00686 // a lit 00687 $i++; 00688 } 00689 } 00690 00691 // check for a rest 00692 $last = end($args); 00693 if ($last[0] == "rest") { 00694 $rest = array_slice($orderedValues, count($args) - 1); 00695 $this->set($last[1], $this->reduce(array("list", " ", $rest))); 00696 } 00697 00698 // wow is this the only true use of PHP's + operator for arrays? 00699 $this->env->arguments = $assignedValues + $orderedValues; 00700 } 00701 00702 // compile a prop and update $lines or $blocks appropriately 00703 protected function compileProp($prop, $block, $out) { 00704 // set error position context 00705 $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1; 00706 00707 switch ($prop[0]) { 00708 case 'assign': 00709 list(, $name, $value) = $prop; 00710 if ($name[0] == $this->vPrefix) { 00711 $this->set($name, $value); 00712 } else { 00713 $out->lines[] = $this->formatter->property($name, 00714 $this->compileValue($this->reduce($value))); 00715 } 00716 break; 00717 case 'block': 00718 list(, $child) = $prop; 00719 $this->compileBlock($child); 00720 break; 00721 case 'mixin': 00722 list(, $path, $args, $suffix) = $prop; 00723 00724 $orderedArgs = array(); 00725 $keywordArgs = array(); 00726 foreach ((array)$args as $arg) { 00727 $argval = null; 00728 switch ($arg[0]) { 00729 case "arg": 00730 if (!isset($arg[2])) { 00731 $orderedArgs[] = $this->reduce(array("variable", $arg[1])); 00732 } else { 00733 $keywordArgs[$arg[1]] = $this->reduce($arg[2]); 00734 } 00735 break; 00736 00737 case "lit": 00738 $orderedArgs[] = $this->reduce($arg[1]); 00739 break; 00740 default: 00741 $this->throwError("Unknown arg type: " . $arg[0]); 00742 } 00743 } 00744 00745 $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); 00746 00747 if ($mixins === null) { 00748 $this->throwError("{$prop[1][0]} is undefined"); 00749 } 00750 00751 foreach ($mixins as $mixin) { 00752 if ($mixin === $block && !$orderedArgs) { 00753 continue; 00754 } 00755 00756 $haveScope = false; 00757 if (isset($mixin->parent->scope)) { 00758 $haveScope = true; 00759 $mixinParentEnv = $this->pushEnv(); 00760 $mixinParentEnv->storeParent = $mixin->parent->scope; 00761 } 00762 00763 $haveArgs = false; 00764 if (isset($mixin->args)) { 00765 $haveArgs = true; 00766 $this->pushEnv(); 00767 $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs); 00768 } 00769 00770 $oldParent = $mixin->parent; 00771 if ($mixin != $block) $mixin->parent = $block; 00772 00773 foreach ($this->sortProps($mixin->props) as $subProp) { 00774 if ($suffix !== null && 00775 $subProp[0] == "assign" && 00776 is_string($subProp[1]) && 00777 $subProp[1]{0} != $this->vPrefix) 00778 { 00779 $subProp[2] = array( 00780 'list', ' ', 00781 array($subProp[2], array('keyword', $suffix)) 00782 ); 00783 } 00784 00785 $this->compileProp($subProp, $mixin, $out); 00786 } 00787 00788 $mixin->parent = $oldParent; 00789 00790 if ($haveArgs) $this->popEnv(); 00791 if ($haveScope) $this->popEnv(); 00792 } 00793 00794 break; 00795 case 'raw': 00796 $out->lines[] = $prop[1]; 00797 break; 00798 case "directive": 00799 list(, $name, $value) = $prop; 00800 $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';'; 00801 break; 00802 case "comment": 00803 $out->lines[] = $prop[1]; 00804 break; 00805 case "import"; 00806 list(, $importPath, $importId) = $prop; 00807 $importPath = $this->reduce($importPath); 00808 00809 if (!isset($this->env->imports)) { 00810 $this->env->imports = array(); 00811 } 00812 00813 $result = $this->tryImport($importPath, $block, $out); 00814 00815 $this->env->imports[$importId] = $result === false ? 00816 array(false, "@import " . $this->compileValue($importPath).";") : 00817 $result; 00818 00819 break; 00820 case "import_mixin": 00821 list(,$importId) = $prop; 00822 $import = $this->env->imports[$importId]; 00823 if ($import[0] === false) { 00824 if (isset($import[1])) { 00825 $out->lines[] = $import[1]; 00826 } 00827 } else { 00828 list(, $bottom, $parser, $importDir) = $import; 00829 $this->compileImportedProps($bottom, $block, $out, $parser, $importDir); 00830 } 00831 00832 break; 00833 default: 00834 $this->throwError("unknown op: {$prop[0]}\n"); 00835 } 00836 } 00837 00838 00850 protected function compileValue($value) { 00851 switch ($value[0]) { 00852 case 'list': 00853 // [1] - delimiter 00854 // [2] - array of values 00855 return implode($value[1], array_map(array($this, 'compileValue'), $value[2])); 00856 case 'raw_color': 00857 if (!empty($this->formatter->compressColors)) { 00858 return $this->compileValue($this->coerceColor($value)); 00859 } 00860 return $value[1]; 00861 case 'keyword': 00862 // [1] - the keyword 00863 return $value[1]; 00864 case 'number': 00865 list(, $num, $unit) = $value; 00866 // [1] - the number 00867 // [2] - the unit 00868 if ($this->numberPrecision !== null) { 00869 $num = round($num, $this->numberPrecision); 00870 } 00871 return $num . $unit; 00872 case 'string': 00873 // [1] - contents of string (includes quotes) 00874 list(, $delim, $content) = $value; 00875 foreach ($content as &$part) { 00876 if (is_array($part)) { 00877 $part = $this->compileValue($part); 00878 } 00879 } 00880 return $delim . implode($content) . $delim; 00881 case 'color': 00882 // [1] - red component (either number or a %) 00883 // [2] - green component 00884 // [3] - blue component 00885 // [4] - optional alpha component 00886 list(, $r, $g, $b) = $value; 00887 $r = round($r); 00888 $g = round($g); 00889 $b = round($b); 00890 00891 if (count($value) == 5 && $value[4] != 1) { // rgba 00892 return 'rgba('.$r.','.$g.','.$b.','.$value[4].')'; 00893 } 00894 00895 $h = sprintf("#%02x%02x%02x", $r, $g, $b); 00896 00897 if (!empty($this->formatter->compressColors)) { 00898 // Converting hex color to short notation (e.g. #003399 to #039) 00899 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { 00900 $h = '#' . $h[1] . $h[3] . $h[5]; 00901 } 00902 } 00903 00904 return $h; 00905 00906 case 'function': 00907 list(, $name, $args) = $value; 00908 return $name.'('.$this->compileValue($args).')'; 00909 default: // assumed to be unit 00910 $this->throwError("unknown value type: $value[0]"); 00911 } 00912 } 00913 00914 protected function lib_pow($args) { 00915 list($base, $exp) = $this->assertArgs($args, 2, "pow"); 00916 return pow($this->assertNumber($base), $this->assertNumber($exp)); 00917 } 00918 00919 protected function lib_pi() { 00920 return pi(); 00921 } 00922 00923 protected function lib_mod($args) { 00924 list($a, $b) = $this->assertArgs($args, 2, "mod"); 00925 return $this->assertNumber($a) % $this->assertNumber($b); 00926 } 00927 00928 protected function lib_tan($num) { 00929 return tan($this->assertNumber($num)); 00930 } 00931 00932 protected function lib_sin($num) { 00933 return sin($this->assertNumber($num)); 00934 } 00935 00936 protected function lib_cos($num) { 00937 return cos($this->assertNumber($num)); 00938 } 00939 00940 protected function lib_atan($num) { 00941 $num = atan($this->assertNumber($num)); 00942 return array("number", $num, "rad"); 00943 } 00944 00945 protected function lib_asin($num) { 00946 $num = asin($this->assertNumber($num)); 00947 return array("number", $num, "rad"); 00948 } 00949 00950 protected function lib_acos($num) { 00951 $num = acos($this->assertNumber($num)); 00952 return array("number", $num, "rad"); 00953 } 00954 00955 protected function lib_sqrt($num) { 00956 return sqrt($this->assertNumber($num)); 00957 } 00958 00959 protected function lib_extract($value) { 00960 list($list, $idx) = $this->assertArgs($value, 2, "extract"); 00961 $idx = $this->assertNumber($idx); 00962 // 1 indexed 00963 if ($list[0] == "list" && isset($list[2][$idx - 1])) { 00964 return $list[2][$idx - 1]; 00965 } 00966 } 00967 00968 protected function lib_isnumber($value) { 00969 return $this->toBool($value[0] == "number"); 00970 } 00971 00972 protected function lib_isstring($value) { 00973 return $this->toBool($value[0] == "string"); 00974 } 00975 00976 protected function lib_iscolor($value) { 00977 return $this->toBool($this->coerceColor($value)); 00978 } 00979 00980 protected function lib_iskeyword($value) { 00981 return $this->toBool($value[0] == "keyword"); 00982 } 00983 00984 protected function lib_ispixel($value) { 00985 return $this->toBool($value[0] == "number" && $value[2] == "px"); 00986 } 00987 00988 protected function lib_ispercentage($value) { 00989 return $this->toBool($value[0] == "number" && $value[2] == "%"); 00990 } 00991 00992 protected function lib_isem($value) { 00993 return $this->toBool($value[0] == "number" && $value[2] == "em"); 00994 } 00995 00996 protected function lib_isrem($value) { 00997 return $this->toBool($value[0] == "number" && $value[2] == "rem"); 00998 } 00999 01000 protected function lib_rgbahex($color) { 01001 $color = $this->coerceColor($color); 01002 if (is_null($color)) 01003 $this->throwError("color expected for rgbahex"); 01004 01005 return sprintf("#%02x%02x%02x%02x", 01006 isset($color[4]) ? $color[4]*255 : 255, 01007 $color[1],$color[2], $color[3]); 01008 } 01009 01010 protected function lib_argb($color){ 01011 return $this->lib_rgbahex($color); 01012 } 01013 01014 // utility func to unquote a string 01015 protected function lib_e($arg) { 01016 switch ($arg[0]) { 01017 case "list": 01018 $items = $arg[2]; 01019 if (isset($items[0])) { 01020 return $this->lib_e($items[0]); 01021 } 01022 $this->throwError("unrecognised input"); 01023 case "string": 01024 $arg[1] = ""; 01025 return $arg; 01026 case "keyword": 01027 return $arg; 01028 default: 01029 return array("keyword", $this->compileValue($arg)); 01030 } 01031 } 01032 01033 protected function lib__sprintf($args) { 01034 if ($args[0] != "list") return $args; 01035 $values = $args[2]; 01036 $string = array_shift($values); 01037 $template = $this->compileValue($this->lib_e($string)); 01038 01039 $i = 0; 01040 if (preg_match_all('/%[dsa]/', $template, $m)) { 01041 foreach ($m[0] as $match) { 01042 $val = isset($values[$i]) ? 01043 $this->reduce($values[$i]) : array('keyword', ''); 01044 01045 // lessjs compat, renders fully expanded color, not raw color 01046 if ($color = $this->coerceColor($val)) { 01047 $val = $color; 01048 } 01049 01050 $i++; 01051 $rep = $this->compileValue($this->lib_e($val)); 01052 $template = preg_replace('/'.self::preg_quote($match).'/', 01053 $rep, $template, 1); 01054 } 01055 } 01056 01057 $d = $string[0] == "string" ? $string[1] : '"'; 01058 return array("string", $d, array($template)); 01059 } 01060 01061 protected function lib_floor($arg) { 01062 $value = $this->assertNumber($arg); 01063 return array("number", floor($value), $arg[2]); 01064 } 01065 01066 protected function lib_ceil($arg) { 01067 $value = $this->assertNumber($arg); 01068 return array("number", ceil($value), $arg[2]); 01069 } 01070 01071 protected function lib_round($arg) { 01072 if($arg[0] != "list") { 01073 $value = $this->assertNumber($arg); 01074 return array("number", round($value), $arg[2]); 01075 } else { 01076 $value = $this->assertNumber($arg[2][0]); 01077 $precision = $this->assertNumber($arg[2][1]); 01078 return array("number", round($value, $precision), $arg[2][0][2]); 01079 } 01080 } 01081 01082 protected function lib_unit($arg) { 01083 if ($arg[0] == "list") { 01084 list($number, $newUnit) = $arg[2]; 01085 return array("number", $this->assertNumber($number), 01086 $this->compileValue($this->lib_e($newUnit))); 01087 } else { 01088 return array("number", $this->assertNumber($arg), ""); 01089 } 01090 } 01091 01096 public function colorArgs($args) { 01097 if ($args[0] != 'list' || count($args[2]) < 2) { 01098 return array(array('color', 0, 0, 0), 0); 01099 } 01100 list($color, $delta) = $args[2]; 01101 $color = $this->assertColor($color); 01102 $delta = floatval($delta[1]); 01103 01104 return array($color, $delta); 01105 } 01106 01107 protected function lib_darken($args) { 01108 list($color, $delta) = $this->colorArgs($args); 01109 01110 $hsl = $this->toHSL($color); 01111 $hsl[3] = $this->clamp($hsl[3] - $delta, 100); 01112 return $this->toRGB($hsl); 01113 } 01114 01115 protected function lib_lighten($args) { 01116 list($color, $delta) = $this->colorArgs($args); 01117 01118 $hsl = $this->toHSL($color); 01119 $hsl[3] = $this->clamp($hsl[3] + $delta, 100); 01120 return $this->toRGB($hsl); 01121 } 01122 01123 protected function lib_saturate($args) { 01124 list($color, $delta) = $this->colorArgs($args); 01125 01126 $hsl = $this->toHSL($color); 01127 $hsl[2] = $this->clamp($hsl[2] + $delta, 100); 01128 return $this->toRGB($hsl); 01129 } 01130 01131 protected function lib_desaturate($args) { 01132 list($color, $delta) = $this->colorArgs($args); 01133 01134 $hsl = $this->toHSL($color); 01135 $hsl[2] = $this->clamp($hsl[2] - $delta, 100); 01136 return $this->toRGB($hsl); 01137 } 01138 01139 protected function lib_spin($args) { 01140 list($color, $delta) = $this->colorArgs($args); 01141 01142 $hsl = $this->toHSL($color); 01143 01144 $hsl[1] = $hsl[1] + $delta % 360; 01145 if ($hsl[1] < 0) $hsl[1] += 360; 01146 01147 return $this->toRGB($hsl); 01148 } 01149 01150 protected function lib_fadeout($args) { 01151 list($color, $delta) = $this->colorArgs($args); 01152 $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100); 01153 return $color; 01154 } 01155 01156 protected function lib_fadein($args) { 01157 list($color, $delta) = $this->colorArgs($args); 01158 $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100); 01159 return $color; 01160 } 01161 01162 protected function lib_hue($color) { 01163 $hsl = $this->toHSL($this->assertColor($color)); 01164 return round($hsl[1]); 01165 } 01166 01167 protected function lib_saturation($color) { 01168 $hsl = $this->toHSL($this->assertColor($color)); 01169 return round($hsl[2]); 01170 } 01171 01172 protected function lib_lightness($color) { 01173 $hsl = $this->toHSL($this->assertColor($color)); 01174 return round($hsl[3]); 01175 } 01176 01177 // get the alpha of a color 01178 // defaults to 1 for non-colors or colors without an alpha 01179 protected function lib_alpha($value) { 01180 if (!is_null($color = $this->coerceColor($value))) { 01181 return isset($color[4]) ? $color[4] : 1; 01182 } 01183 } 01184 01185 // set the alpha of the color 01186 protected function lib_fade($args) { 01187 list($color, $alpha) = $this->colorArgs($args); 01188 $color[4] = $this->clamp($alpha / 100.0); 01189 return $color; 01190 } 01191 01192 protected function lib_percentage($arg) { 01193 $num = $this->assertNumber($arg); 01194 return array("number", $num*100, "%"); 01195 } 01196 01197 // mixes two colors by weight 01198 // mix(@color1, @color2, [@weight: 50%]); 01199 // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method 01200 protected function lib_mix($args) { 01201 if ($args[0] != "list" || count($args[2]) < 2) 01202 $this->throwError("mix expects (color1, color2, weight)"); 01203 01204 list($first, $second) = $args[2]; 01205 $first = $this->assertColor($first); 01206 $second = $this->assertColor($second); 01207 01208 $first_a = $this->lib_alpha($first); 01209 $second_a = $this->lib_alpha($second); 01210 01211 if (isset($args[2][2])) { 01212 $weight = $args[2][2][1] / 100.0; 01213 } else { 01214 $weight = 0.5; 01215 } 01216 01217 $w = $weight * 2 - 1; 01218 $a = $first_a - $second_a; 01219 01220 $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; 01221 $w2 = 1.0 - $w1; 01222 01223 $new = array('color', 01224 $w1 * $first[1] + $w2 * $second[1], 01225 $w1 * $first[2] + $w2 * $second[2], 01226 $w1 * $first[3] + $w2 * $second[3], 01227 ); 01228 01229 if ($first_a != 1.0 || $second_a != 1.0) { 01230 $new[] = $first_a * $weight + $second_a * ($weight - 1); 01231 } 01232 01233 return $this->fixColor($new); 01234 } 01235 01236 protected function lib_contrast($args) { 01237 if ($args[0] != 'list' || count($args[2]) < 3) { 01238 return array(array('color', 0, 0, 0), 0); 01239 } 01240 01241 list($inputColor, $darkColor, $lightColor) = $args[2]; 01242 01243 $inputColor = $this->assertColor($inputColor); 01244 $darkColor = $this->assertColor($darkColor); 01245 $lightColor = $this->assertColor($lightColor); 01246 $hsl = $this->toHSL($inputColor); 01247 01248 if ($hsl[3] > 50) { 01249 return $darkColor; 01250 } 01251 01252 return $lightColor; 01253 } 01254 01255 public function assertColor($value, $error = "expected color value") { 01256 $color = $this->coerceColor($value); 01257 if (is_null($color)) $this->throwError($error); 01258 return $color; 01259 } 01260 01261 public function assertNumber($value, $error = "expecting number") { 01262 if ($value[0] == "number") return $value[1]; 01263 $this->throwError($error); 01264 } 01265 01266 public function assertArgs($value, $expectedArgs, $name="") { 01267 if ($expectedArgs == 1) { 01268 return $value; 01269 } else { 01270 if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list"); 01271 $values = $value[2]; 01272 $numValues = count($values); 01273 if ($expectedArgs != $numValues) { 01274 if ($name) { 01275 $name = $name . ": "; 01276 } 01277 01278 $this->throwError("${name}expecting $expectedArgs arguments, got $numValues"); 01279 } 01280 01281 return $values; 01282 } 01283 } 01284 01285 protected function toHSL($color) { 01286 if ($color[0] == 'hsl') return $color; 01287 01288 $r = $color[1] / 255; 01289 $g = $color[2] / 255; 01290 $b = $color[3] / 255; 01291 01292 $min = min($r, $g, $b); 01293 $max = max($r, $g, $b); 01294 01295 $L = ($min + $max) / 2; 01296 if ($min == $max) { 01297 $S = $H = 0; 01298 } else { 01299 if ($L < 0.5) 01300 $S = ($max - $min)/($max + $min); 01301 else 01302 $S = ($max - $min)/(2.0 - $max - $min); 01303 01304 if ($r == $max) $H = ($g - $b)/($max - $min); 01305 elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min); 01306 elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min); 01307 01308 } 01309 01310 $out = array('hsl', 01311 ($H < 0 ? $H + 6 : $H)*60, 01312 $S*100, 01313 $L*100, 01314 ); 01315 01316 if (count($color) > 4) $out[] = $color[4]; // copy alpha 01317 return $out; 01318 } 01319 01320 protected function toRGB_helper($comp, $temp1, $temp2) { 01321 if ($comp < 0) $comp += 1.0; 01322 elseif ($comp > 1) $comp -= 1.0; 01323 01324 if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp; 01325 if (2 * $comp < 1) return $temp2; 01326 if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; 01327 01328 return $temp1; 01329 } 01330 01335 protected function toRGB($color) { 01336 if ($color[0] == 'color') return $color; 01337 01338 $H = $color[1] / 360; 01339 $S = $color[2] / 100; 01340 $L = $color[3] / 100; 01341 01342 if ($S == 0) { 01343 $r = $g = $b = $L; 01344 } else { 01345 $temp2 = $L < 0.5 ? 01346 $L*(1.0 + $S) : 01347 $L + $S - $L * $S; 01348 01349 $temp1 = 2.0 * $L - $temp2; 01350 01351 $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2); 01352 $g = $this->toRGB_helper($H, $temp1, $temp2); 01353 $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2); 01354 } 01355 01356 // $out = array('color', round($r*255), round($g*255), round($b*255)); 01357 $out = array('color', $r*255, $g*255, $b*255); 01358 if (count($color) > 4) $out[] = $color[4]; // copy alpha 01359 return $out; 01360 } 01361 01362 protected function clamp($v, $max = 1, $min = 0) { 01363 return min($max, max($min, $v)); 01364 } 01365 01370 protected function funcToColor($func) { 01371 $fname = $func[1]; 01372 if ($func[2][0] != 'list') return false; // need a list of arguments 01373 $rawComponents = $func[2][2]; 01374 01375 if ($fname == 'hsl' || $fname == 'hsla') { 01376 $hsl = array('hsl'); 01377 $i = 0; 01378 foreach ($rawComponents as $c) { 01379 $val = $this->reduce($c); 01380 $val = isset($val[1]) ? floatval($val[1]) : 0; 01381 01382 if ($i == 0) $clamp = 360; 01383 elseif ($i < 3) $clamp = 100; 01384 else $clamp = 1; 01385 01386 $hsl[] = $this->clamp($val, $clamp); 01387 $i++; 01388 } 01389 01390 while (count($hsl) < 4) $hsl[] = 0; 01391 return $this->toRGB($hsl); 01392 01393 } elseif ($fname == 'rgb' || $fname == 'rgba') { 01394 $components = array(); 01395 $i = 1; 01396 foreach ($rawComponents as $c) { 01397 $c = $this->reduce($c); 01398 if ($i < 4) { 01399 if ($c[0] == "number" && $c[2] == "%") { 01400 $components[] = 255 * ($c[1] / 100); 01401 } else { 01402 $components[] = floatval($c[1]); 01403 } 01404 } elseif ($i == 4) { 01405 if ($c[0] == "number" && $c[2] == "%") { 01406 $components[] = 1.0 * ($c[1] / 100); 01407 } else { 01408 $components[] = floatval($c[1]); 01409 } 01410 } else break; 01411 01412 $i++; 01413 } 01414 while (count($components) < 3) $components[] = 0; 01415 array_unshift($components, 'color'); 01416 return $this->fixColor($components); 01417 } 01418 01419 return false; 01420 } 01421 01422 protected function reduce($value, $forExpression = false) { 01423 switch ($value[0]) { 01424 case "interpolate": 01425 $reduced = $this->reduce($value[1]); 01426 $var = $this->compileValue($reduced); 01427 $res = $this->reduce(array("variable", $this->vPrefix . $var)); 01428 01429 if ($res[0] == "raw_color") { 01430 $res = $this->coerceColor($res); 01431 } 01432 01433 if (empty($value[2])) $res = $this->lib_e($res); 01434 01435 return $res; 01436 case "variable": 01437 $key = $value[1]; 01438 if (is_array($key)) { 01439 $key = $this->reduce($key); 01440 $key = $this->vPrefix . $this->compileValue($this->lib_e($key)); 01441 } 01442 01443 $seen =& $this->env->seenNames; 01444 01445 if (!empty($seen[$key])) { 01446 $this->throwError("infinite loop detected: $key"); 01447 } 01448 01449 $seen[$key] = true; 01450 $out = $this->reduce($this->get($key)); 01451 $seen[$key] = false; 01452 return $out; 01453 case "list": 01454 foreach ($value[2] as &$item) { 01455 $item = $this->reduce($item, $forExpression); 01456 } 01457 return $value; 01458 case "expression": 01459 return $this->evaluate($value); 01460 case "string": 01461 foreach ($value[2] as &$part) { 01462 if (is_array($part)) { 01463 $strip = $part[0] == "variable"; 01464 $part = $this->reduce($part); 01465 if ($strip) $part = $this->lib_e($part); 01466 } 01467 } 01468 return $value; 01469 case "escape": 01470 list(,$inner) = $value; 01471 return $this->lib_e($this->reduce($inner)); 01472 case "function": 01473 $color = $this->funcToColor($value); 01474 if ($color) return $color; 01475 01476 list(, $name, $args) = $value; 01477 if ($name == "%") $name = "_sprintf"; 01478 $f = isset($this->libFunctions[$name]) ? 01479 $this->libFunctions[$name] : array($this, 'lib_'.$name); 01480 01481 if (is_callable($f)) { 01482 if ($args[0] == 'list') 01483 $args = self::compressList($args[2], $args[1]); 01484 01485 $ret = call_user_func($f, $this->reduce($args, true), $this); 01486 01487 if (is_null($ret)) { 01488 return array("string", "", array( 01489 $name, "(", $args, ")" 01490 )); 01491 } 01492 01493 // convert to a typed value if the result is a php primitive 01494 if (is_numeric($ret)) $ret = array('number', $ret, ""); 01495 elseif (!is_array($ret)) $ret = array('keyword', $ret); 01496 01497 return $ret; 01498 } 01499 01500 // plain function, reduce args 01501 $value[2] = $this->reduce($value[2]); 01502 return $value; 01503 case "unary": 01504 list(, $op, $exp) = $value; 01505 $exp = $this->reduce($exp); 01506 01507 if ($exp[0] == "number") { 01508 switch ($op) { 01509 case "+": 01510 return $exp; 01511 case "-": 01512 $exp[1] *= -1; 01513 return $exp; 01514 } 01515 } 01516 return array("string", "", array($op, $exp)); 01517 } 01518 01519 if ($forExpression) { 01520 switch ($value[0]) { 01521 case "keyword": 01522 if ($color = $this->coerceColor($value)) { 01523 return $color; 01524 } 01525 break; 01526 case "raw_color": 01527 return $this->coerceColor($value); 01528 } 01529 } 01530 01531 return $value; 01532 } 01533 01534 01535 // coerce a value for use in color operation 01536 protected function coerceColor($value) { 01537 switch($value[0]) { 01538 case 'color': return $value; 01539 case 'raw_color': 01540 $c = array("color", 0, 0, 0); 01541 $colorStr = substr($value[1], 1); 01542 $num = hexdec($colorStr); 01543 $width = strlen($colorStr) == 3 ? 16 : 256; 01544 01545 for ($i = 3; $i > 0; $i--) { // 3 2 1 01546 $t = $num % $width; 01547 $num /= $width; 01548 01549 $c[$i] = $t * (256/$width) + $t * floor(16/$width); 01550 } 01551 01552 return $c; 01553 case 'keyword': 01554 $name = $value[1]; 01555 if (isset(self::$cssColors[$name])) { 01556 $rgba = explode(',', self::$cssColors[$name]); 01557 01558 if(isset($rgba[3])) 01559 return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]); 01560 01561 return array('color', $rgba[0], $rgba[1], $rgba[2]); 01562 } 01563 return null; 01564 } 01565 } 01566 01567 // make something string like into a string 01568 protected function coerceString($value) { 01569 switch ($value[0]) { 01570 case "string": 01571 return $value; 01572 case "keyword": 01573 return array("string", "", array($value[1])); 01574 } 01575 return null; 01576 } 01577 01578 // turn list of length 1 into value type 01579 protected function flattenList($value) { 01580 if ($value[0] == "list" && count($value[2]) == 1) { 01581 return $this->flattenList($value[2][0]); 01582 } 01583 return $value; 01584 } 01585 01586 public function toBool($a) { 01587 if ($a) return self::$TRUE; 01588 else return self::$FALSE; 01589 } 01590 01591 // evaluate an expression 01592 protected function evaluate($exp) { 01593 list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; 01594 01595 $left = $this->reduce($left, true); 01596 $right = $this->reduce($right, true); 01597 01598 if ($leftColor = $this->coerceColor($left)) { 01599 $left = $leftColor; 01600 } 01601 01602 if ($rightColor = $this->coerceColor($right)) { 01603 $right = $rightColor; 01604 } 01605 01606 $ltype = $left[0]; 01607 $rtype = $right[0]; 01608 01609 // operators that work on all types 01610 if ($op == "and") { 01611 return $this->toBool($left == self::$TRUE && $right == self::$TRUE); 01612 } 01613 01614 if ($op == "=") { 01615 return $this->toBool($this->eq($left, $right) ); 01616 } 01617 01618 if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) { 01619 return $str; 01620 } 01621 01622 // type based operators 01623 $fname = "op_${ltype}_${rtype}"; 01624 if (is_callable(array($this, $fname))) { 01625 $out = $this->$fname($op, $left, $right); 01626 if (!is_null($out)) return $out; 01627 } 01628 01629 // make the expression look it did before being parsed 01630 $paddedOp = $op; 01631 if ($whiteBefore) $paddedOp = " " . $paddedOp; 01632 if ($whiteAfter) $paddedOp .= " "; 01633 01634 return array("string", "", array($left, $paddedOp, $right)); 01635 } 01636 01637 protected function stringConcatenate($left, $right) { 01638 if ($strLeft = $this->coerceString($left)) { 01639 if ($right[0] == "string") { 01640 $right[1] = ""; 01641 } 01642 $strLeft[2][] = $right; 01643 return $strLeft; 01644 } 01645 01646 if ($strRight = $this->coerceString($right)) { 01647 array_unshift($strRight[2], $left); 01648 return $strRight; 01649 } 01650 } 01651 01652 01653 // make sure a color's components don't go out of bounds 01654 protected function fixColor($c) { 01655 foreach (range(1, 3) as $i) { 01656 if ($c[$i] < 0) $c[$i] = 0; 01657 if ($c[$i] > 255) $c[$i] = 255; 01658 } 01659 01660 return $c; 01661 } 01662 01663 protected function op_number_color($op, $lft, $rgt) { 01664 if ($op == '+' || $op == '*') { 01665 return $this->op_color_number($op, $rgt, $lft); 01666 } 01667 } 01668 01669 protected function op_color_number($op, $lft, $rgt) { 01670 if ($rgt[0] == '%') $rgt[1] /= 100; 01671 01672 return $this->op_color_color($op, $lft, 01673 array_fill(1, count($lft) - 1, $rgt[1])); 01674 } 01675 01676 protected function op_color_color($op, $left, $right) { 01677 $out = array('color'); 01678 $max = count($left) > count($right) ? count($left) : count($right); 01679 foreach (range(1, $max - 1) as $i) { 01680 $lval = isset($left[$i]) ? $left[$i] : 0; 01681 $rval = isset($right[$i]) ? $right[$i] : 0; 01682 switch ($op) { 01683 case '+': 01684 $out[] = $lval + $rval; 01685 break; 01686 case '-': 01687 $out[] = $lval - $rval; 01688 break; 01689 case '*': 01690 $out[] = $lval * $rval; 01691 break; 01692 case '%': 01693 $out[] = $lval % $rval; 01694 break; 01695 case '/': 01696 if ($rval == 0) $this->throwError("evaluate error: can't divide by zero"); 01697 $out[] = $lval / $rval; 01698 break; 01699 default: 01700 $this->throwError('evaluate error: color op number failed on op '.$op); 01701 } 01702 } 01703 return $this->fixColor($out); 01704 } 01705 01706 function lib_red($color){ 01707 $color = $this->coerceColor($color); 01708 if (is_null($color)) { 01709 $this->throwError('color expected for red()'); 01710 } 01711 01712 return $color[1]; 01713 } 01714 01715 function lib_green($color){ 01716 $color = $this->coerceColor($color); 01717 if (is_null($color)) { 01718 $this->throwError('color expected for green()'); 01719 } 01720 01721 return $color[2]; 01722 } 01723 01724 function lib_blue($color){ 01725 $color = $this->coerceColor($color); 01726 if (is_null($color)) { 01727 $this->throwError('color expected for blue()'); 01728 } 01729 01730 return $color[3]; 01731 } 01732 01733 01734 // operator on two numbers 01735 protected function op_number_number($op, $left, $right) { 01736 $unit = empty($left[2]) ? $right[2] : $left[2]; 01737 01738 $value = 0; 01739 switch ($op) { 01740 case '+': 01741 $value = $left[1] + $right[1]; 01742 break; 01743 case '*': 01744 $value = $left[1] * $right[1]; 01745 break; 01746 case '-': 01747 $value = $left[1] - $right[1]; 01748 break; 01749 case '%': 01750 $value = $left[1] % $right[1]; 01751 break; 01752 case '/': 01753 if ($right[1] == 0) $this->throwError('parse error: divide by zero'); 01754 $value = $left[1] / $right[1]; 01755 break; 01756 case '<': 01757 return $this->toBool($left[1] < $right[1]); 01758 case '>': 01759 return $this->toBool($left[1] > $right[1]); 01760 case '>=': 01761 return $this->toBool($left[1] >= $right[1]); 01762 case '=<': 01763 return $this->toBool($left[1] <= $right[1]); 01764 default: 01765 $this->throwError('parse error: unknown number operator: '.$op); 01766 } 01767 01768 return array("number", $value, $unit); 01769 } 01770 01771 01772 /* environment functions */ 01773 01774 protected function makeOutputBlock($type, $selectors = null) { 01775 $b = new stdclass; 01776 $b->lines = array(); 01777 $b->children = array(); 01778 $b->selectors = $selectors; 01779 $b->type = $type; 01780 $b->parent = $this->scope; 01781 return $b; 01782 } 01783 01784 // the state of execution 01785 protected function pushEnv($block = null) { 01786 $e = new stdclass; 01787 $e->parent = $this->env; 01788 $e->store = array(); 01789 $e->block = $block; 01790 01791 $this->env = $e; 01792 return $e; 01793 } 01794 01795 // pop something off the stack 01796 protected function popEnv() { 01797 $old = $this->env; 01798 $this->env = $this->env->parent; 01799 return $old; 01800 } 01801 01802 // set something in the current env 01803 protected function set($name, $value) { 01804 $this->env->store[$name] = $value; 01805 } 01806 01807 01808 // get the highest occurrence entry for a name 01809 protected function get($name) { 01810 $current = $this->env; 01811 01812 $isArguments = $name == $this->vPrefix . 'arguments'; 01813 while ($current) { 01814 if ($isArguments && isset($current->arguments)) { 01815 return array('list', ' ', $current->arguments); 01816 } 01817 01818 if (isset($current->store[$name])) 01819 return $current->store[$name]; 01820 else { 01821 $current = isset($current->storeParent) ? 01822 $current->storeParent : $current->parent; 01823 } 01824 } 01825 01826 $this->throwError("variable $name is undefined"); 01827 } 01828 01829 // inject array of unparsed strings into environment as variables 01830 protected function injectVariables($args) { 01831 $this->pushEnv(); 01832 $parser = new lessc_parser($this, __METHOD__); 01833 foreach ($args as $name => $strValue) { 01834 if ($name{0} != '@') $name = '@'.$name; 01835 $parser->count = 0; 01836 $parser->buffer = (string)$strValue; 01837 if (!$parser->propertyValue($value)) { 01838 throw new Exception("failed to parse passed in variable $name: $strValue"); 01839 } 01840 01841 $this->set($name, $value); 01842 } 01843 } 01844 01849 public function __construct($fname = null) { 01850 if ($fname !== null) { 01851 // used for deprecated parse method 01852 $this->_parseFile = $fname; 01853 } 01854 } 01855 01856 public function compile($string, $name = null) { 01857 $locale = setlocale(LC_NUMERIC, 0); 01858 setlocale(LC_NUMERIC, "C"); 01859 01860 $this->parser = $this->makeParser($name); 01861 $root = $this->parser->parse($string); 01862 01863 $this->env = null; 01864 $this->scope = null; 01865 01866 $this->formatter = $this->newFormatter(); 01867 01868 if (!empty($this->registeredVars)) { 01869 $this->injectVariables($this->registeredVars); 01870 } 01871 01872 $this->sourceParser = $this->parser; // used for error messages 01873 $this->compileBlock($root); 01874 01875 ob_start(); 01876 $this->formatter->block($this->scope); 01877 $out = ob_get_clean(); 01878 setlocale(LC_NUMERIC, $locale); 01879 return $out; 01880 } 01881 01882 public function compileFile($fname, $outFname = null) { 01883 if (!is_readable($fname)) { 01884 throw new Exception('load error: failed to find '.$fname); 01885 } 01886 01887 $pi = pathinfo($fname); 01888 01889 $oldImport = $this->importDir; 01890 01891 $this->importDir = (array)$this->importDir; 01892 $this->importDir[] = $pi['dirname'].'/'; 01893 01894 $this->addParsedFile($fname); 01895 01896 $out = $this->compile(file_get_contents($fname), $fname); 01897 01898 $this->importDir = $oldImport; 01899 01900 if ($outFname !== null) { 01901 return file_put_contents($outFname, $out); 01902 } 01903 01904 return $out; 01905 } 01906 01907 // compile only if changed input has changed or output doesn't exist 01908 public function checkedCompile($in, $out) { 01909 if (!is_file($out) || filemtime($in) > filemtime($out)) { 01910 $this->compileFile($in, $out); 01911 return true; 01912 } 01913 return false; 01914 } 01915 01936 public function cachedCompile($in, $force = false) { 01937 // assume no root 01938 $root = null; 01939 01940 if (is_string($in)) { 01941 $root = $in; 01942 } elseif (is_array($in) and isset($in['root'])) { 01943 if ($force or ! isset($in['files'])) { 01944 // If we are forcing a recompile or if for some reason the 01945 // structure does not contain any file information we should 01946 // specify the root to trigger a rebuild. 01947 $root = $in['root']; 01948 } elseif (isset($in['files']) and is_array($in['files'])) { 01949 foreach ($in['files'] as $fname => $ftime ) { 01950 if (!file_exists($fname) or filemtime($fname) > $ftime) { 01951 // One of the files we knew about previously has changed 01952 // so we should look at our incoming root again. 01953 $root = $in['root']; 01954 break; 01955 } 01956 } 01957 } 01958 } else { 01959 // TODO: Throw an exception? We got neither a string nor something 01960 // that looks like a compatible lessphp cache structure. 01961 return null; 01962 } 01963 01964 if ($root !== null) { 01965 // If we have a root value which means we should rebuild. 01966 $out = array(); 01967 $out['root'] = $root; 01968 $out['compiled'] = $this->compileFile($root); 01969 $out['files'] = $this->allParsedFiles(); 01970 $out['updated'] = time(); 01971 return $out; 01972 } else { 01973 // No changes, pass back the structure 01974 // we were given initially. 01975 return $in; 01976 } 01977 01978 } 01979 01980 // parse and compile buffer 01981 // This is deprecated 01982 public function parse($str = null, $initialVariables = null) { 01983 if (is_array($str)) { 01984 $initialVariables = $str; 01985 $str = null; 01986 } 01987 01988 $oldVars = $this->registeredVars; 01989 if ($initialVariables !== null) { 01990 $this->setVariables($initialVariables); 01991 } 01992 01993 if ($str == null) { 01994 if (empty($this->_parseFile)) { 01995 throw new exception("nothing to parse"); 01996 } 01997 01998 $out = $this->compileFile($this->_parseFile); 01999 } else { 02000 $out = $this->compile($str); 02001 } 02002 02003 $this->registeredVars = $oldVars; 02004 return $out; 02005 } 02006 02007 protected function makeParser($name) { 02008 $parser = new lessc_parser($this, $name); 02009 $parser->writeComments = $this->preserveComments; 02010 02011 return $parser; 02012 } 02013 02014 public function setFormatter($name) { 02015 $this->formatterName = $name; 02016 } 02017 02018 protected function newFormatter() { 02019 $className = "lessc_formatter_lessjs"; 02020 if (!empty($this->formatterName)) { 02021 if (!is_string($this->formatterName)) 02022 return $this->formatterName; 02023 $className = "lessc_formatter_$this->formatterName"; 02024 } 02025 02026 return new $className; 02027 } 02028 02029 public function setPreserveComments($preserve) { 02030 $this->preserveComments = $preserve; 02031 } 02032 02033 public function registerFunction($name, $func) { 02034 $this->libFunctions[$name] = $func; 02035 } 02036 02037 public function unregisterFunction($name) { 02038 unset($this->libFunctions[$name]); 02039 } 02040 02041 public function setVariables($variables) { 02042 $this->registeredVars = array_merge($this->registeredVars, $variables); 02043 } 02044 02045 public function unsetVariable($name) { 02046 unset($this->registeredVars[$name]); 02047 } 02048 02049 public function setImportDir($dirs) { 02050 $this->importDir = (array)$dirs; 02051 } 02052 02053 public function addImportDir($dir) { 02054 $this->importDir = (array)$this->importDir; 02055 $this->importDir[] = $dir; 02056 } 02057 02058 public function allParsedFiles() { 02059 return $this->allParsedFiles; 02060 } 02061 02062 public function addParsedFile($file) { 02063 $this->allParsedFiles[realpath($file)] = filemtime($file); 02064 } 02065 02069 public function throwError($msg = null) { 02070 if ($this->sourceLoc >= 0) { 02071 $this->sourceParser->throwError($msg, $this->sourceLoc); 02072 } 02073 throw new exception($msg); 02074 } 02075 02076 // compile file $in to file $out if $in is newer than $out 02077 // returns true when it compiles, false otherwise 02078 public static function ccompile($in, $out, $less = null) { 02079 if ($less === null) { 02080 $less = new self; 02081 } 02082 return $less->checkedCompile($in, $out); 02083 } 02084 02085 public static function cexecute($in, $force = false, $less = null) { 02086 if ($less === null) { 02087 $less = new self; 02088 } 02089 return $less->cachedCompile($in, $force); 02090 } 02091 02092 static protected $cssColors = array( 02093 'aliceblue' => '240,248,255', 02094 'antiquewhite' => '250,235,215', 02095 'aqua' => '0,255,255', 02096 'aquamarine' => '127,255,212', 02097 'azure' => '240,255,255', 02098 'beige' => '245,245,220', 02099 'bisque' => '255,228,196', 02100 'black' => '0,0,0', 02101 'blanchedalmond' => '255,235,205', 02102 'blue' => '0,0,255', 02103 'blueviolet' => '138,43,226', 02104 'brown' => '165,42,42', 02105 'burlywood' => '222,184,135', 02106 'cadetblue' => '95,158,160', 02107 'chartreuse' => '127,255,0', 02108 'chocolate' => '210,105,30', 02109 'coral' => '255,127,80', 02110 'cornflowerblue' => '100,149,237', 02111 'cornsilk' => '255,248,220', 02112 'crimson' => '220,20,60', 02113 'cyan' => '0,255,255', 02114 'darkblue' => '0,0,139', 02115 'darkcyan' => '0,139,139', 02116 'darkgoldenrod' => '184,134,11', 02117 'darkgray' => '169,169,169', 02118 'darkgreen' => '0,100,0', 02119 'darkgrey' => '169,169,169', 02120 'darkkhaki' => '189,183,107', 02121 'darkmagenta' => '139,0,139', 02122 'darkolivegreen' => '85,107,47', 02123 'darkorange' => '255,140,0', 02124 'darkorchid' => '153,50,204', 02125 'darkred' => '139,0,0', 02126 'darksalmon' => '233,150,122', 02127 'darkseagreen' => '143,188,143', 02128 'darkslateblue' => '72,61,139', 02129 'darkslategray' => '47,79,79', 02130 'darkslategrey' => '47,79,79', 02131 'darkturquoise' => '0,206,209', 02132 'darkviolet' => '148,0,211', 02133 'deeppink' => '255,20,147', 02134 'deepskyblue' => '0,191,255', 02135 'dimgray' => '105,105,105', 02136 'dimgrey' => '105,105,105', 02137 'dodgerblue' => '30,144,255', 02138 'firebrick' => '178,34,34', 02139 'floralwhite' => '255,250,240', 02140 'forestgreen' => '34,139,34', 02141 'fuchsia' => '255,0,255', 02142 'gainsboro' => '220,220,220', 02143 'ghostwhite' => '248,248,255', 02144 'gold' => '255,215,0', 02145 'goldenrod' => '218,165,32', 02146 'gray' => '128,128,128', 02147 'green' => '0,128,0', 02148 'greenyellow' => '173,255,47', 02149 'grey' => '128,128,128', 02150 'honeydew' => '240,255,240', 02151 'hotpink' => '255,105,180', 02152 'indianred' => '205,92,92', 02153 'indigo' => '75,0,130', 02154 'ivory' => '255,255,240', 02155 'khaki' => '240,230,140', 02156 'lavender' => '230,230,250', 02157 'lavenderblush' => '255,240,245', 02158 'lawngreen' => '124,252,0', 02159 'lemonchiffon' => '255,250,205', 02160 'lightblue' => '173,216,230', 02161 'lightcoral' => '240,128,128', 02162 'lightcyan' => '224,255,255', 02163 'lightgoldenrodyellow' => '250,250,210', 02164 'lightgray' => '211,211,211', 02165 'lightgreen' => '144,238,144', 02166 'lightgrey' => '211,211,211', 02167 'lightpink' => '255,182,193', 02168 'lightsalmon' => '255,160,122', 02169 'lightseagreen' => '32,178,170', 02170 'lightskyblue' => '135,206,250', 02171 'lightslategray' => '119,136,153', 02172 'lightslategrey' => '119,136,153', 02173 'lightsteelblue' => '176,196,222', 02174 'lightyellow' => '255,255,224', 02175 'lime' => '0,255,0', 02176 'limegreen' => '50,205,50', 02177 'linen' => '250,240,230', 02178 'magenta' => '255,0,255', 02179 'maroon' => '128,0,0', 02180 'mediumaquamarine' => '102,205,170', 02181 'mediumblue' => '0,0,205', 02182 'mediumorchid' => '186,85,211', 02183 'mediumpurple' => '147,112,219', 02184 'mediumseagreen' => '60,179,113', 02185 'mediumslateblue' => '123,104,238', 02186 'mediumspringgreen' => '0,250,154', 02187 'mediumturquoise' => '72,209,204', 02188 'mediumvioletred' => '199,21,133', 02189 'midnightblue' => '25,25,112', 02190 'mintcream' => '245,255,250', 02191 'mistyrose' => '255,228,225', 02192 'moccasin' => '255,228,181', 02193 'navajowhite' => '255,222,173', 02194 'navy' => '0,0,128', 02195 'oldlace' => '253,245,230', 02196 'olive' => '128,128,0', 02197 'olivedrab' => '107,142,35', 02198 'orange' => '255,165,0', 02199 'orangered' => '255,69,0', 02200 'orchid' => '218,112,214', 02201 'palegoldenrod' => '238,232,170', 02202 'palegreen' => '152,251,152', 02203 'paleturquoise' => '175,238,238', 02204 'palevioletred' => '219,112,147', 02205 'papayawhip' => '255,239,213', 02206 'peachpuff' => '255,218,185', 02207 'peru' => '205,133,63', 02208 'pink' => '255,192,203', 02209 'plum' => '221,160,221', 02210 'powderblue' => '176,224,230', 02211 'purple' => '128,0,128', 02212 'red' => '255,0,0', 02213 'rosybrown' => '188,143,143', 02214 'royalblue' => '65,105,225', 02215 'saddlebrown' => '139,69,19', 02216 'salmon' => '250,128,114', 02217 'sandybrown' => '244,164,96', 02218 'seagreen' => '46,139,87', 02219 'seashell' => '255,245,238', 02220 'sienna' => '160,82,45', 02221 'silver' => '192,192,192', 02222 'skyblue' => '135,206,235', 02223 'slateblue' => '106,90,205', 02224 'slategray' => '112,128,144', 02225 'slategrey' => '112,128,144', 02226 'snow' => '255,250,250', 02227 'springgreen' => '0,255,127', 02228 'steelblue' => '70,130,180', 02229 'tan' => '210,180,140', 02230 'teal' => '0,128,128', 02231 'thistle' => '216,191,216', 02232 'tomato' => '255,99,71', 02233 'transparent' => '0,0,0,0', 02234 'turquoise' => '64,224,208', 02235 'violet' => '238,130,238', 02236 'wheat' => '245,222,179', 02237 'white' => '255,255,255', 02238 'whitesmoke' => '245,245,245', 02239 'yellow' => '255,255,0', 02240 'yellowgreen' => '154,205,50' 02241 ); 02242 } 02243 02244 // responsible for taking a string of LESS code and converting it into a 02245 // syntax tree 02246 class lessc_parser { 02247 static protected $nextBlockId = 0; // used to uniquely identify blocks 02248 02249 static protected $precedence = array( 02250 '=<' => 0, 02251 '>=' => 0, 02252 '=' => 0, 02253 '<' => 0, 02254 '>' => 0, 02255 02256 '+' => 1, 02257 '-' => 1, 02258 '*' => 2, 02259 '/' => 2, 02260 '%' => 2, 02261 ); 02262 02263 static protected $whitePattern; 02264 static protected $commentMulti; 02265 02266 static protected $commentSingle = "//"; 02267 static protected $commentMultiLeft = "/*"; 02268 static protected $commentMultiRight = "*/"; 02269 02270 // regex string to match any of the operators 02271 static protected $operatorString; 02272 02273 // these properties will supress division unless it's inside parenthases 02274 static protected $supressDivisionProps = 02275 array('/border-radius$/i', '/^font$/i'); 02276 02277 protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport"); 02278 protected $lineDirectives = array("charset"); 02279 02289 protected $inParens = false; 02290 02291 // caches preg escaped literals 02292 static protected $literalCache = array(); 02293 02294 public function __construct($lessc, $sourceName = null) { 02295 $this->eatWhiteDefault = true; 02296 // reference to less needed for vPrefix, mPrefix, and parentSelector 02297 $this->lessc = $lessc; 02298 02299 $this->sourceName = $sourceName; // name used for error messages 02300 02301 $this->writeComments = false; 02302 02303 if (!self::$operatorString) { 02304 self::$operatorString = 02305 '('.implode('|', array_map(array('lessc', 'preg_quote'), 02306 array_keys(self::$precedence))).')'; 02307 02308 $commentSingle = lessc::preg_quote(self::$commentSingle); 02309 $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft); 02310 $commentMultiRight = lessc::preg_quote(self::$commentMultiRight); 02311 02312 self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; 02313 self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; 02314 } 02315 } 02316 02317 public function parse($buffer) { 02318 $this->count = 0; 02319 $this->line = 1; 02320 02321 $this->env = null; // block stack 02322 $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer); 02323 $this->pushSpecialBlock("root"); 02324 $this->eatWhiteDefault = true; 02325 $this->seenComments = array(); 02326 02327 // trim whitespace on head 02328 // if (preg_match('/^\s+/', $this->buffer, $m)) { 02329 // $this->line += substr_count($m[0], "\n"); 02330 // $this->buffer = ltrim($this->buffer); 02331 // } 02332 $this->whitespace(); 02333 02334 // parse the entire file 02335 while (false !== $this->parseChunk()); 02336 02337 if ($this->count != strlen($this->buffer)) 02338 $this->throwError(); 02339 02340 // TODO report where the block was opened 02341 if (!is_null($this->env->parent)) 02342 throw new exception('parse error: unclosed block'); 02343 02344 return $this->env; 02345 } 02346 02383 protected function parseChunk() { 02384 if (empty($this->buffer)) return false; 02385 $s = $this->seek(); 02386 02387 if ($this->whitespace()) { 02388 return true; 02389 } 02390 02391 // setting a property 02392 if ($this->keyword($key) && $this->assign() && 02393 $this->propertyValue($value, $key) && $this->end()) 02394 { 02395 $this->append(array('assign', $key, $value), $s); 02396 return true; 02397 } else { 02398 $this->seek($s); 02399 } 02400 02401 02402 // look for special css blocks 02403 if ($this->literal('@', false)) { 02404 $this->count--; 02405 02406 // media 02407 if ($this->literal('@media')) { 02408 if (($this->mediaQueryList($mediaQueries) || true) 02409 && $this->literal('{')) 02410 { 02411 $media = $this->pushSpecialBlock("media"); 02412 $media->queries = is_null($mediaQueries) ? array() : $mediaQueries; 02413 return true; 02414 } else { 02415 $this->seek($s); 02416 return false; 02417 } 02418 } 02419 02420 if ($this->literal("@", false) && $this->keyword($dirName)) { 02421 if ($this->isDirective($dirName, $this->blockDirectives)) { 02422 if (($this->openString("{", $dirValue, null, array(";")) || true) && 02423 $this->literal("{")) 02424 { 02425 $dir = $this->pushSpecialBlock("directive"); 02426 $dir->name = $dirName; 02427 if (isset($dirValue)) $dir->value = $dirValue; 02428 return true; 02429 } 02430 } elseif ($this->isDirective($dirName, $this->lineDirectives)) { 02431 if ($this->propertyValue($dirValue) && $this->end()) { 02432 $this->append(array("directive", $dirName, $dirValue)); 02433 return true; 02434 } 02435 } 02436 } 02437 02438 $this->seek($s); 02439 } 02440 02441 // setting a variable 02442 if ($this->variable($var) && $this->assign() && 02443 $this->propertyValue($value) && $this->end()) 02444 { 02445 $this->append(array('assign', $var, $value), $s); 02446 return true; 02447 } else { 02448 $this->seek($s); 02449 } 02450 02451 if ($this->import($importValue)) { 02452 $this->append($importValue, $s); 02453 return true; 02454 } 02455 02456 // opening parametric mixin 02457 if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && 02458 ($this->guards($guards) || true) && 02459 $this->literal('{')) 02460 { 02461 $block = $this->pushBlock($this->fixTags(array($tag))); 02462 $block->args = $args; 02463 $block->isVararg = $isVararg; 02464 if (!empty($guards)) $block->guards = $guards; 02465 return true; 02466 } else { 02467 $this->seek($s); 02468 } 02469 02470 // opening a simple block 02471 if ($this->tags($tags) && $this->literal('{', false)) { 02472 $tags = $this->fixTags($tags); 02473 $this->pushBlock($tags); 02474 return true; 02475 } else { 02476 $this->seek($s); 02477 } 02478 02479 // closing a block 02480 if ($this->literal('}', false)) { 02481 try { 02482 $block = $this->pop(); 02483 } catch (exception $e) { 02484 $this->seek($s); 02485 $this->throwError($e->getMessage()); 02486 } 02487 02488 $hidden = false; 02489 if (is_null($block->type)) { 02490 $hidden = true; 02491 if (!isset($block->args)) { 02492 foreach ($block->tags as $tag) { 02493 if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) { 02494 $hidden = false; 02495 break; 02496 } 02497 } 02498 } 02499 02500 foreach ($block->tags as $tag) { 02501 if (is_string($tag)) { 02502 $this->env->children[$tag][] = $block; 02503 } 02504 } 02505 } 02506 02507 if (!$hidden) { 02508 $this->append(array('block', $block), $s); 02509 } 02510 02511 // this is done here so comments aren't bundled into he block that 02512 // was just closed 02513 $this->whitespace(); 02514 return true; 02515 } 02516 02517 // mixin 02518 if ($this->mixinTags($tags) && 02519 ($this->argumentDef($argv, $isVararg) || true) && 02520 ($this->keyword($suffix) || true) && $this->end()) 02521 { 02522 $tags = $this->fixTags($tags); 02523 $this->append(array('mixin', $tags, $argv, $suffix), $s); 02524 return true; 02525 } else { 02526 $this->seek($s); 02527 } 02528 02529 // spare ; 02530 if ($this->literal(';')) return true; 02531 02532 return false; // got nothing, throw error 02533 } 02534 02535 protected function isDirective($dirname, $directives) { 02536 // TODO: cache pattern in parser 02537 $pattern = implode("|", 02538 array_map(array("lessc", "preg_quote"), $directives)); 02539 $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; 02540 02541 return preg_match($pattern, $dirname); 02542 } 02543 02544 protected function fixTags($tags) { 02545 // move @ tags out of variable namespace 02546 foreach ($tags as &$tag) { 02547 if ($tag{0} == $this->lessc->vPrefix) 02548 $tag[0] = $this->lessc->mPrefix; 02549 } 02550 return $tags; 02551 } 02552 02553 // a list of expressions 02554 protected function expressionList(&$exps) { 02555 $values = array(); 02556 02557 while ($this->expression($exp)) { 02558 $values[] = $exp; 02559 } 02560 02561 if (count($values) == 0) return false; 02562 02563 $exps = lessc::compressList($values, ' '); 02564 return true; 02565 } 02566 02571 protected function expression(&$out) { 02572 if ($this->value($lhs)) { 02573 $out = $this->expHelper($lhs, 0); 02574 02575 // look for / shorthand 02576 if (!empty($this->env->supressedDivision)) { 02577 unset($this->env->supressedDivision); 02578 $s = $this->seek(); 02579 if ($this->literal("/") && $this->value($rhs)) { 02580 $out = array("list", "", 02581 array($out, array("keyword", "/"), $rhs)); 02582 } else { 02583 $this->seek($s); 02584 } 02585 } 02586 02587 return true; 02588 } 02589 return false; 02590 } 02591 02595 protected function expHelper($lhs, $minP) { 02596 $this->inExp = true; 02597 $ss = $this->seek(); 02598 02599 while (true) { 02600 $whiteBefore = isset($this->buffer[$this->count - 1]) && 02601 ctype_space($this->buffer[$this->count - 1]); 02602 02603 // If there is whitespace before the operator, then we require 02604 // whitespace after the operator for it to be an expression 02605 $needWhite = $whiteBefore && !$this->inParens; 02606 02607 if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) { 02608 if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) { 02609 foreach (self::$supressDivisionProps as $pattern) { 02610 if (preg_match($pattern, $this->env->currentProperty)) { 02611 $this->env->supressedDivision = true; 02612 break 2; 02613 } 02614 } 02615 } 02616 02617 02618 $whiteAfter = isset($this->buffer[$this->count - 1]) && 02619 ctype_space($this->buffer[$this->count - 1]); 02620 02621 if (!$this->value($rhs)) break; 02622 02623 // peek for next operator to see what to do with rhs 02624 if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) { 02625 $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); 02626 } 02627 02628 $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter); 02629 $ss = $this->seek(); 02630 02631 continue; 02632 } 02633 02634 break; 02635 } 02636 02637 $this->seek($ss); 02638 02639 return $lhs; 02640 } 02641 02642 // consume a list of values for a property 02643 public function propertyValue(&$value, $keyName = null) { 02644 $values = array(); 02645 02646 if ($keyName !== null) $this->env->currentProperty = $keyName; 02647 02648 $s = null; 02649 while ($this->expressionList($v)) { 02650 $values[] = $v; 02651 $s = $this->seek(); 02652 if (!$this->literal(',')) break; 02653 } 02654 02655 if ($s) $this->seek($s); 02656 02657 if ($keyName !== null) unset($this->env->currentProperty); 02658 02659 if (count($values) == 0) return false; 02660 02661 $value = lessc::compressList($values, ', '); 02662 return true; 02663 } 02664 02665 protected function parenValue(&$out) { 02666 $s = $this->seek(); 02667 02668 // speed shortcut 02669 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") { 02670 return false; 02671 } 02672 02673 $inParens = $this->inParens; 02674 if ($this->literal("(") && 02675 ($this->inParens = true) && $this->expression($exp) && 02676 $this->literal(")")) 02677 { 02678 $out = $exp; 02679 $this->inParens = $inParens; 02680 return true; 02681 } else { 02682 $this->inParens = $inParens; 02683 $this->seek($s); 02684 } 02685 02686 return false; 02687 } 02688 02689 // a single value 02690 protected function value(&$value) { 02691 $s = $this->seek(); 02692 02693 // speed shortcut 02694 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") { 02695 // negation 02696 if ($this->literal("-", false) && 02697 (($this->variable($inner) && $inner = array("variable", $inner)) || 02698 $this->unit($inner) || 02699 $this->parenValue($inner))) 02700 { 02701 $value = array("unary", "-", $inner); 02702 return true; 02703 } else { 02704 $this->seek($s); 02705 } 02706 } 02707 02708 if ($this->parenValue($value)) return true; 02709 if ($this->unit($value)) return true; 02710 if ($this->color($value)) return true; 02711 if ($this->func($value)) return true; 02712 if ($this->string($value)) return true; 02713 02714 if ($this->keyword($word)) { 02715 $value = array('keyword', $word); 02716 return true; 02717 } 02718 02719 // try a variable 02720 if ($this->variable($var)) { 02721 $value = array('variable', $var); 02722 return true; 02723 } 02724 02725 // unquote string (should this work on any type? 02726 if ($this->literal("~") && $this->string($str)) { 02727 $value = array("escape", $str); 02728 return true; 02729 } else { 02730 $this->seek($s); 02731 } 02732 02733 // css hack: \0 02734 if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { 02735 $value = array('keyword', '\\'.$m[1]); 02736 return true; 02737 } else { 02738 $this->seek($s); 02739 } 02740 02741 return false; 02742 } 02743 02744 // an import statement 02745 protected function import(&$out) { 02746 if (!$this->literal('@import')) return false; 02747 02748 // @import "something.css" media; 02749 // @import url("something.css") media; 02750 // @import url(something.css) media; 02751 02752 if ($this->propertyValue($value)) { 02753 $out = array("import", $value); 02754 return true; 02755 } 02756 } 02757 02758 protected function mediaQueryList(&$out) { 02759 if ($this->genericList($list, "mediaQuery", ",", false)) { 02760 $out = $list[2]; 02761 return true; 02762 } 02763 return false; 02764 } 02765 02766 protected function mediaQuery(&$out) { 02767 $s = $this->seek(); 02768 02769 $expressions = null; 02770 $parts = array(); 02771 02772 if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) { 02773 $prop = array("mediaType"); 02774 if (isset($only)) $prop[] = "only"; 02775 if (isset($not)) $prop[] = "not"; 02776 $prop[] = $mediaType; 02777 $parts[] = $prop; 02778 } else { 02779 $this->seek($s); 02780 } 02781 02782 02783 if (!empty($mediaType) && !$this->literal("and")) { 02784 // ~ 02785 } else { 02786 $this->genericList($expressions, "mediaExpression", "and", false); 02787 if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); 02788 } 02789 02790 if (count($parts) == 0) { 02791 $this->seek($s); 02792 return false; 02793 } 02794 02795 $out = $parts; 02796 return true; 02797 } 02798 02799 protected function mediaExpression(&$out) { 02800 $s = $this->seek(); 02801 $value = null; 02802 if ($this->literal("(") && 02803 $this->keyword($feature) && 02804 ($this->literal(":") && $this->expression($value) || true) && 02805 $this->literal(")")) 02806 { 02807 $out = array("mediaExp", $feature); 02808 if ($value) $out[] = $value; 02809 return true; 02810 } elseif ($this->variable($variable)) { 02811 $out = array('variable', $variable); 02812 return true; 02813 } 02814 02815 $this->seek($s); 02816 return false; 02817 } 02818 02819 // an unbounded string stopped by $end 02820 protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) { 02821 $oldWhite = $this->eatWhiteDefault; 02822 $this->eatWhiteDefault = false; 02823 02824 $stop = array("'", '"', "@{", $end); 02825 $stop = array_map(array("lessc", "preg_quote"), $stop); 02826 // $stop[] = self::$commentMulti; 02827 02828 if (!is_null($rejectStrs)) { 02829 $stop = array_merge($stop, $rejectStrs); 02830 } 02831 02832 $patt = '(.*?)('.implode("|", $stop).')'; 02833 02834 $nestingLevel = 0; 02835 02836 $content = array(); 02837 while ($this->match($patt, $m, false)) { 02838 if (!empty($m[1])) { 02839 $content[] = $m[1]; 02840 if ($nestingOpen) { 02841 $nestingLevel += substr_count($m[1], $nestingOpen); 02842 } 02843 } 02844 02845 $tok = $m[2]; 02846 02847 $this->count-= strlen($tok); 02848 if ($tok == $end) { 02849 if ($nestingLevel == 0) { 02850 break; 02851 } else { 02852 $nestingLevel--; 02853 } 02854 } 02855 02856 if (($tok == "'" || $tok == '"') && $this->string($str)) { 02857 $content[] = $str; 02858 continue; 02859 } 02860 02861 if ($tok == "@{" && $this->interpolation($inter)) { 02862 $content[] = $inter; 02863 continue; 02864 } 02865 02866 if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { 02867 break; 02868 } 02869 02870 $content[] = $tok; 02871 $this->count+= strlen($tok); 02872 } 02873 02874 $this->eatWhiteDefault = $oldWhite; 02875 02876 if (count($content) == 0) return false; 02877 02878 // trim the end 02879 if (is_string(end($content))) { 02880 $content[count($content) - 1] = rtrim(end($content)); 02881 } 02882 02883 $out = array("string", "", $content); 02884 return true; 02885 } 02886 02887 protected function string(&$out) { 02888 $s = $this->seek(); 02889 if ($this->literal('"', false)) { 02890 $delim = '"'; 02891 } elseif ($this->literal("'", false)) { 02892 $delim = "'"; 02893 } else { 02894 return false; 02895 } 02896 02897 $content = array(); 02898 02899 // look for either ending delim , escape, or string interpolation 02900 $patt = '([^\n]*?)(@\{|\\\\|' . 02901 lessc::preg_quote($delim).')'; 02902 02903 $oldWhite = $this->eatWhiteDefault; 02904 $this->eatWhiteDefault = false; 02905 02906 while ($this->match($patt, $m, false)) { 02907 $content[] = $m[1]; 02908 if ($m[2] == "@{") { 02909 $this->count -= strlen($m[2]); 02910 if ($this->interpolation($inter, false)) { 02911 $content[] = $inter; 02912 } else { 02913 $this->count += strlen($m[2]); 02914 $content[] = "@{"; // ignore it 02915 } 02916 } elseif ($m[2] == '\\') { 02917 $content[] = $m[2]; 02918 if ($this->literal($delim, false)) { 02919 $content[] = $delim; 02920 } 02921 } else { 02922 $this->count -= strlen($delim); 02923 break; // delim 02924 } 02925 } 02926 02927 $this->eatWhiteDefault = $oldWhite; 02928 02929 if ($this->literal($delim)) { 02930 $out = array("string", $delim, $content); 02931 return true; 02932 } 02933 02934 $this->seek($s); 02935 return false; 02936 } 02937 02938 protected function interpolation(&$out) { 02939 $oldWhite = $this->eatWhiteDefault; 02940 $this->eatWhiteDefault = true; 02941 02942 $s = $this->seek(); 02943 if ($this->literal("@{") && 02944 $this->openString("}", $interp, null, array("'", '"', ";")) && 02945 $this->literal("}", false)) 02946 { 02947 $out = array("interpolate", $interp); 02948 $this->eatWhiteDefault = $oldWhite; 02949 if ($this->eatWhiteDefault) $this->whitespace(); 02950 return true; 02951 } 02952 02953 $this->eatWhiteDefault = $oldWhite; 02954 $this->seek($s); 02955 return false; 02956 } 02957 02958 protected function unit(&$unit) { 02959 // speed shortcut 02960 if (isset($this->buffer[$this->count])) { 02961 $char = $this->buffer[$this->count]; 02962 if (!ctype_digit($char) && $char != ".") return false; 02963 } 02964 02965 if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { 02966 $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]); 02967 return true; 02968 } 02969 return false; 02970 } 02971 02972 // a # color 02973 protected function color(&$out) { 02974 if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { 02975 if (strlen($m[1]) > 7) { 02976 $out = array("string", "", array($m[1])); 02977 } else { 02978 $out = array("raw_color", $m[1]); 02979 } 02980 return true; 02981 } 02982 02983 return false; 02984 } 02985 02986 // consume an argument definition list surrounded by () 02987 // each argument is a variable name with optional value 02988 // or at the end a ... or a variable named followed by ... 02989 // arguments are separated by , unless a ; is in the list, then ; is the 02990 // delimiter. 02991 protected function argumentDef(&$args, &$isVararg) { 02992 $s = $this->seek(); 02993 if (!$this->literal('(')) return false; 02994 02995 $values = array(); 02996 $delim = ","; 02997 $method = "expressionList"; 02998 02999 $isVararg = false; 03000 while (true) { 03001 if ($this->literal("...")) { 03002 $isVararg = true; 03003 break; 03004 } 03005 03006 if ($this->$method($value)) { 03007 if ($value[0] == "variable") { 03008 $arg = array("arg", $value[1]); 03009 $ss = $this->seek(); 03010 03011 if ($this->assign() && $this->$method($rhs)) { 03012 $arg[] = $rhs; 03013 } else { 03014 $this->seek($ss); 03015 if ($this->literal("...")) { 03016 $arg[0] = "rest"; 03017 $isVararg = true; 03018 } 03019 } 03020 03021 $values[] = $arg; 03022 if ($isVararg) break; 03023 continue; 03024 } else { 03025 $values[] = array("lit", $value); 03026 } 03027 } 03028 03029 03030 if (!$this->literal($delim)) { 03031 if ($delim == "," && $this->literal(";")) { 03032 // found new delim, convert existing args 03033 $delim = ";"; 03034 $method = "propertyValue"; 03035 03036 // transform arg list 03037 if (isset($values[1])) { // 2 items 03038 $newList = array(); 03039 foreach ($values as $i => $arg) { 03040 switch($arg[0]) { 03041 case "arg": 03042 if ($i) { 03043 $this->throwError("Cannot mix ; and , as delimiter types"); 03044 } 03045 $newList[] = $arg[2]; 03046 break; 03047 case "lit": 03048 $newList[] = $arg[1]; 03049 break; 03050 case "rest": 03051 $this->throwError("Unexpected rest before semicolon"); 03052 } 03053 } 03054 03055 $newList = array("list", ", ", $newList); 03056 03057 switch ($values[0][0]) { 03058 case "arg": 03059 $newArg = array("arg", $values[0][1], $newList); 03060 break; 03061 case "lit": 03062 $newArg = array("lit", $newList); 03063 break; 03064 } 03065 03066 } elseif ($values) { // 1 item 03067 $newArg = $values[0]; 03068 } 03069 03070 if ($newArg) { 03071 $values = array($newArg); 03072 } 03073 } else { 03074 break; 03075 } 03076 } 03077 } 03078 03079 if (!$this->literal(')')) { 03080 $this->seek($s); 03081 return false; 03082 } 03083 03084 $args = $values; 03085 03086 return true; 03087 } 03088 03089 // consume a list of tags 03090 // this accepts a hanging delimiter 03091 protected function tags(&$tags, $simple = false, $delim = ',') { 03092 $tags = array(); 03093 while ($this->tag($tt, $simple)) { 03094 $tags[] = $tt; 03095 if (!$this->literal($delim)) break; 03096 } 03097 if (count($tags) == 0) return false; 03098 03099 return true; 03100 } 03101 03102 // list of tags of specifying mixin path 03103 // optionally separated by > (lazy, accepts extra >) 03104 protected function mixinTags(&$tags) { 03105 $tags = array(); 03106 while ($this->tag($tt, true)) { 03107 $tags[] = $tt; 03108 $this->literal(">"); 03109 } 03110 03111 if (count($tags) == 0) return false; 03112 03113 return true; 03114 } 03115 03116 // a bracketed value (contained within in a tag definition) 03117 protected function tagBracket(&$parts, &$hasExpression) { 03118 // speed shortcut 03119 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") { 03120 return false; 03121 } 03122 03123 $s = $this->seek(); 03124 03125 $hasInterpolation = false; 03126 03127 if ($this->literal("[", false)) { 03128 $attrParts = array("["); 03129 // keyword, string, operator 03130 while (true) { 03131 if ($this->literal("]", false)) { 03132 $this->count--; 03133 break; // get out early 03134 } 03135 03136 if ($this->match('\s+', $m)) { 03137 $attrParts[] = " "; 03138 continue; 03139 } 03140 if ($this->string($str)) { 03141 // escape parent selector, (yuck) 03142 foreach ($str[2] as &$chunk) { 03143 $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk); 03144 } 03145 03146 $attrParts[] = $str; 03147 $hasInterpolation = true; 03148 continue; 03149 } 03150 03151 if ($this->keyword($word)) { 03152 $attrParts[] = $word; 03153 continue; 03154 } 03155 03156 if ($this->interpolation($inter, false)) { 03157 $attrParts[] = $inter; 03158 $hasInterpolation = true; 03159 continue; 03160 } 03161 03162 // operator, handles attr namespace too 03163 if ($this->match('[|-~\$\*\^=]+', $m)) { 03164 $attrParts[] = $m[0]; 03165 continue; 03166 } 03167 03168 break; 03169 } 03170 03171 if ($this->literal("]", false)) { 03172 $attrParts[] = "]"; 03173 foreach ($attrParts as $part) { 03174 $parts[] = $part; 03175 } 03176 $hasExpression = $hasExpression || $hasInterpolation; 03177 return true; 03178 } 03179 $this->seek($s); 03180 } 03181 03182 $this->seek($s); 03183 return false; 03184 } 03185 03186 // a space separated list of selectors 03187 protected function tag(&$tag, $simple = false) { 03188 if ($simple) 03189 $chars = '^@,:;{}\][>\(\) "\''; 03190 else 03191 $chars = '^@,;{}["\''; 03192 03193 $s = $this->seek(); 03194 03195 $hasExpression = false; 03196 $parts = array(); 03197 while ($this->tagBracket($parts, $hasExpression)); 03198 03199 $oldWhite = $this->eatWhiteDefault; 03200 $this->eatWhiteDefault = false; 03201 03202 while (true) { 03203 if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) { 03204 $parts[] = $m[1]; 03205 if ($simple) break; 03206 03207 while ($this->tagBracket($parts, $hasExpression)); 03208 continue; 03209 } 03210 03211 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { 03212 if ($this->interpolation($interp)) { 03213 $hasExpression = true; 03214 $interp[2] = true; // don't unescape 03215 $parts[] = $interp; 03216 continue; 03217 } 03218 03219 if ($this->literal("@")) { 03220 $parts[] = "@"; 03221 continue; 03222 } 03223 } 03224 03225 if ($this->unit($unit)) { // for keyframes 03226 $parts[] = $unit[1]; 03227 $parts[] = $unit[2]; 03228 continue; 03229 } 03230 03231 break; 03232 } 03233 03234 $this->eatWhiteDefault = $oldWhite; 03235 if (!$parts) { 03236 $this->seek($s); 03237 return false; 03238 } 03239 03240 if ($hasExpression) { 03241 $tag = array("exp", array("string", "", $parts)); 03242 } else { 03243 $tag = trim(implode($parts)); 03244 } 03245 03246 $this->whitespace(); 03247 return true; 03248 } 03249 03250 // a css function 03251 protected function func(&$func) { 03252 $s = $this->seek(); 03253 03254 if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) { 03255 $fname = $m[1]; 03256 03257 $sPreArgs = $this->seek(); 03258 03259 $args = array(); 03260 while (true) { 03261 $ss = $this->seek(); 03262 // this ugly nonsense is for ie filter properties 03263 if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { 03264 $args[] = array("string", "", array($name, "=", $value)); 03265 } else { 03266 $this->seek($ss); 03267 if ($this->expressionList($value)) { 03268 $args[] = $value; 03269 } 03270 } 03271 03272 if (!$this->literal(',')) break; 03273 } 03274 $args = array('list', ',', $args); 03275 03276 if ($this->literal(')')) { 03277 $func = array('function', $fname, $args); 03278 return true; 03279 } elseif ($fname == 'url') { 03280 // couldn't parse and in url? treat as string 03281 $this->seek($sPreArgs); 03282 if ($this->openString(")", $string) && $this->literal(")")) { 03283 $func = array('function', $fname, $string); 03284 return true; 03285 } 03286 } 03287 } 03288 03289 $this->seek($s); 03290 return false; 03291 } 03292 03293 // consume a less variable 03294 protected function variable(&$name) { 03295 $s = $this->seek(); 03296 if ($this->literal($this->lessc->vPrefix, false) && 03297 ($this->variable($sub) || $this->keyword($name))) 03298 { 03299 if (!empty($sub)) { 03300 $name = array('variable', $sub); 03301 } else { 03302 $name = $this->lessc->vPrefix.$name; 03303 } 03304 return true; 03305 } 03306 03307 $name = null; 03308 $this->seek($s); 03309 return false; 03310 } 03311 03316 protected function assign($name = null) { 03317 if ($name) $this->currentProperty = $name; 03318 return $this->literal(':') || $this->literal('='); 03319 } 03320 03321 // consume a keyword 03322 protected function keyword(&$word) { 03323 if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { 03324 $word = $m[1]; 03325 return true; 03326 } 03327 return false; 03328 } 03329 03330 // consume an end of statement delimiter 03331 protected function end() { 03332 if ($this->literal(';', false)) { 03333 return true; 03334 } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { 03335 // if there is end of file or a closing block next then we don't need a ; 03336 return true; 03337 } 03338 return false; 03339 } 03340 03341 protected function guards(&$guards) { 03342 $s = $this->seek(); 03343 03344 if (!$this->literal("when")) { 03345 $this->seek($s); 03346 return false; 03347 } 03348 03349 $guards = array(); 03350 03351 while ($this->guardGroup($g)) { 03352 $guards[] = $g; 03353 if (!$this->literal(",")) break; 03354 } 03355 03356 if (count($guards) == 0) { 03357 $guards = null; 03358 $this->seek($s); 03359 return false; 03360 } 03361 03362 return true; 03363 } 03364 03365 // a bunch of guards that are and'd together 03366 // TODO rename to guardGroup 03367 protected function guardGroup(&$guardGroup) { 03368 $s = $this->seek(); 03369 $guardGroup = array(); 03370 while ($this->guard($guard)) { 03371 $guardGroup[] = $guard; 03372 if (!$this->literal("and")) break; 03373 } 03374 03375 if (count($guardGroup) == 0) { 03376 $guardGroup = null; 03377 $this->seek($s); 03378 return false; 03379 } 03380 03381 return true; 03382 } 03383 03384 protected function guard(&$guard) { 03385 $s = $this->seek(); 03386 $negate = $this->literal("not"); 03387 03388 if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) { 03389 $guard = $exp; 03390 if ($negate) $guard = array("negate", $guard); 03391 return true; 03392 } 03393 03394 $this->seek($s); 03395 return false; 03396 } 03397 03398 /* raw parsing functions */ 03399 03400 protected function literal($what, $eatWhitespace = null) { 03401 if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 03402 03403 // shortcut on single letter 03404 if (!isset($what[1]) && isset($this->buffer[$this->count])) { 03405 if ($this->buffer[$this->count] == $what) { 03406 if (!$eatWhitespace) { 03407 $this->count++; 03408 return true; 03409 } 03410 // goes below... 03411 } else { 03412 return false; 03413 } 03414 } 03415 03416 if (!isset(self::$literalCache[$what])) { 03417 self::$literalCache[$what] = lessc::preg_quote($what); 03418 } 03419 03420 return $this->match(self::$literalCache[$what], $m, $eatWhitespace); 03421 } 03422 03423 protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { 03424 $s = $this->seek(); 03425 $items = array(); 03426 while ($this->$parseItem($value)) { 03427 $items[] = $value; 03428 if ($delim) { 03429 if (!$this->literal($delim)) break; 03430 } 03431 } 03432 03433 if (count($items) == 0) { 03434 $this->seek($s); 03435 return false; 03436 } 03437 03438 if ($flatten && count($items) == 1) { 03439 $out = $items[0]; 03440 } else { 03441 $out = array("list", $delim, $items); 03442 } 03443 03444 return true; 03445 } 03446 03447 03448 // advance counter to next occurrence of $what 03449 // $until - don't include $what in advance 03450 // $allowNewline, if string, will be used as valid char set 03451 protected function to($what, &$out, $until = false, $allowNewline = false) { 03452 if (is_string($allowNewline)) { 03453 $validChars = $allowNewline; 03454 } else { 03455 $validChars = $allowNewline ? "." : "[^\n]"; 03456 } 03457 if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false; 03458 if ($until) $this->count -= strlen($what); // give back $what 03459 $out = $m[1]; 03460 return true; 03461 } 03462 03463 // try to match something on head of buffer 03464 protected function match($regex, &$out, $eatWhitespace = null) { 03465 if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 03466 03467 $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais'; 03468 if (preg_match($r, $this->buffer, $out, null, $this->count)) { 03469 $this->count += strlen($out[0]); 03470 if ($eatWhitespace && $this->writeComments) $this->whitespace(); 03471 return true; 03472 } 03473 return false; 03474 } 03475 03476 // match some whitespace 03477 protected function whitespace() { 03478 if ($this->writeComments) { 03479 $gotWhite = false; 03480 while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { 03481 if (isset($m[1]) && empty($this->seenComments[$this->count])) { 03482 $this->append(array("comment", $m[1])); 03483 $this->seenComments[$this->count] = true; 03484 } 03485 $this->count += strlen($m[0]); 03486 $gotWhite = true; 03487 } 03488 return $gotWhite; 03489 } else { 03490 $this->match("", $m); 03491 return strlen($m[0]) > 0; 03492 } 03493 } 03494 03495 // match something without consuming it 03496 protected function peek($regex, &$out = null, $from=null) { 03497 if (is_null($from)) $from = $this->count; 03498 $r = '/'.$regex.'/Ais'; 03499 $result = preg_match($r, $this->buffer, $out, null, $from); 03500 03501 return $result; 03502 } 03503 03504 // seek to a spot in the buffer or return where we are on no argument 03505 protected function seek($where = null) { 03506 if ($where === null) return $this->count; 03507 else $this->count = $where; 03508 return true; 03509 } 03510 03511 /* misc functions */ 03512 03513 public function throwError($msg = "parse error", $count = null) { 03514 $count = is_null($count) ? $this->count : $count; 03515 03516 $line = $this->line + 03517 substr_count(substr($this->buffer, 0, $count), "\n"); 03518 03519 if (!empty($this->sourceName)) { 03520 $loc = "$this->sourceName on line $line"; 03521 } else { 03522 $loc = "line: $line"; 03523 } 03524 03525 // TODO this depends on $this->count 03526 if ($this->peek("(.*?)(\n|$)", $m, $count)) { 03527 throw new exception("$msg: failed at `$m[1]` $loc"); 03528 } else { 03529 throw new exception("$msg: $loc"); 03530 } 03531 } 03532 03533 protected function pushBlock($selectors=null, $type=null) { 03534 $b = new stdclass; 03535 $b->parent = $this->env; 03536 03537 $b->type = $type; 03538 $b->id = self::$nextBlockId++; 03539 03540 $b->isVararg = false; // TODO: kill me from here 03541 $b->tags = $selectors; 03542 03543 $b->props = array(); 03544 $b->children = array(); 03545 03546 $this->env = $b; 03547 return $b; 03548 } 03549 03550 // push a block that doesn't multiply tags 03551 protected function pushSpecialBlock($type) { 03552 return $this->pushBlock(null, $type); 03553 } 03554 03555 // append a property to the current block 03556 protected function append($prop, $pos = null) { 03557 if ($pos !== null) $prop[-1] = $pos; 03558 $this->env->props[] = $prop; 03559 } 03560 03561 // pop something off the stack 03562 protected function pop() { 03563 $old = $this->env; 03564 $this->env = $this->env->parent; 03565 return $old; 03566 } 03567 03568 // remove comments from $text 03569 // todo: make it work for all functions, not just url 03570 protected function removeComments($text) { 03571 $look = array( 03572 'url(', '//', '/*', '"', "'" 03573 ); 03574 03575 $out = ''; 03576 $min = null; 03577 while (true) { 03578 // find the next item 03579 foreach ($look as $token) { 03580 $pos = strpos($text, $token); 03581 if ($pos !== false) { 03582 if (!isset($min) || $pos < $min[1]) $min = array($token, $pos); 03583 } 03584 } 03585 03586 if (is_null($min)) break; 03587 03588 $count = $min[1]; 03589 $skip = 0; 03590 $newlines = 0; 03591 switch ($min[0]) { 03592 case 'url(': 03593 if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) 03594 $count += strlen($m[0]) - strlen($min[0]); 03595 break; 03596 case '"': 03597 case "'": 03598 if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count)) 03599 $count += strlen($m[0]) - 1; 03600 break; 03601 case '//': 03602 $skip = strpos($text, "\n", $count); 03603 if ($skip === false) $skip = strlen($text) - $count; 03604 else $skip -= $count; 03605 break; 03606 case '/*': 03607 if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) { 03608 $skip = strlen($m[0]); 03609 $newlines = substr_count($m[0], "\n"); 03610 } 03611 break; 03612 } 03613 03614 if ($skip == 0) $count += strlen($min[0]); 03615 03616 $out .= substr($text, 0, $count).str_repeat("\n", $newlines); 03617 $text = substr($text, $count + $skip); 03618 03619 $min = null; 03620 } 03621 03622 return $out.$text; 03623 } 03624 03625 } 03626 03627 class lessc_formatter_classic { 03628 public $indentChar = " "; 03629 03630 public $break = "\n"; 03631 public $open = " {"; 03632 public $close = "}"; 03633 public $selectorSeparator = ", "; 03634 public $assignSeparator = ":"; 03635 03636 public $openSingle = " { "; 03637 public $closeSingle = " }"; 03638 03639 public $disableSingle = false; 03640 public $breakSelectors = false; 03641 03642 public $compressColors = false; 03643 03644 public function __construct() { 03645 $this->indentLevel = 0; 03646 } 03647 03648 public function indentStr($n = 0) { 03649 return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); 03650 } 03651 03652 public function property($name, $value) { 03653 return $name . $this->assignSeparator . $value . ";"; 03654 } 03655 03656 protected function isEmpty($block) { 03657 if (empty($block->lines)) { 03658 foreach ($block->children as $child) { 03659 if (!$this->isEmpty($child)) return false; 03660 } 03661 03662 return true; 03663 } 03664 return false; 03665 } 03666 03667 public function block($block) { 03668 if ($this->isEmpty($block)) return; 03669 03670 $inner = $pre = $this->indentStr(); 03671 03672 $isSingle = !$this->disableSingle && 03673 is_null($block->type) && count($block->lines) == 1; 03674 03675 if (!empty($block->selectors)) { 03676 $this->indentLevel++; 03677 03678 if ($this->breakSelectors) { 03679 $selectorSeparator = $this->selectorSeparator . $this->break . $pre; 03680 } else { 03681 $selectorSeparator = $this->selectorSeparator; 03682 } 03683 03684 echo $pre . 03685 implode($selectorSeparator, $block->selectors); 03686 if ($isSingle) { 03687 echo $this->openSingle; 03688 $inner = ""; 03689 } else { 03690 echo $this->open . $this->break; 03691 $inner = $this->indentStr(); 03692 } 03693 03694 } 03695 03696 if (!empty($block->lines)) { 03697 $glue = $this->break.$inner; 03698 echo $inner . implode($glue, $block->lines); 03699 if (!$isSingle && !empty($block->children)) { 03700 echo $this->break; 03701 } 03702 } 03703 03704 foreach ($block->children as $child) { 03705 $this->block($child); 03706 } 03707 03708 if (!empty($block->selectors)) { 03709 if (!$isSingle && empty($block->children)) echo $this->break; 03710 03711 if ($isSingle) { 03712 echo $this->closeSingle . $this->break; 03713 } else { 03714 echo $pre . $this->close . $this->break; 03715 } 03716 03717 $this->indentLevel--; 03718 } 03719 } 03720 } 03721 03722 class lessc_formatter_compressed extends lessc_formatter_classic { 03723 public $disableSingle = true; 03724 public $open = "{"; 03725 public $selectorSeparator = ","; 03726 public $assignSeparator = ":"; 03727 public $break = ""; 03728 public $compressColors = true; 03729 03730 public function indentStr($n = 0) { 03731 return ""; 03732 } 03733 } 03734 03735 class lessc_formatter_lessjs extends lessc_formatter_classic { 03736 public $disableSingle = true; 03737 public $breakSelectors = true; 03738 public $assignSeparator = ": "; 03739 public $selectorSeparator = ","; 03740 } 03741 03742