MediaWiki
REL1_24
|
00001 <?php 00002 // @codingStandardsIgnoreFile File external to MediaWiki. Ignore coding conventions checks. 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 public 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 01020 protected function lib_data_uri($value) { 01021 $mime = ($value[0] === 'list') ? $value[2][0][2] : null; 01022 $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0]; 01023 01024 $fullpath = $this->findImport($url); 01025 01026 if($fullpath && ($fsize = filesize($fullpath)) !== false) { 01027 // IE8 can't handle data uris larger than 32KB 01028 if($fsize/1024 < 32) { 01029 if(is_null($mime)) { 01030 if(class_exists('finfo')) { // php 5.3+ 01031 $finfo = new finfo(FILEINFO_MIME); 01032 $mime = explode('; ', $finfo->file($fullpath)); 01033 $mime = $mime[0]; 01034 } elseif(function_exists('mime_content_type')) { // PHP 5.2 01035 $mime = mime_content_type($fullpath); 01036 } 01037 } 01038 01039 if(!is_null($mime)) // fallback if the MIME type is still unknown 01040 $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath))); 01041 } 01042 } 01043 01044 return 'url("'.$url.'")'; 01045 } 01046 01047 // utility func to unquote a string 01048 protected function lib_e($arg) { 01049 switch ($arg[0]) { 01050 case "list": 01051 $items = $arg[2]; 01052 if (isset($items[0])) { 01053 return $this->lib_e($items[0]); 01054 } 01055 $this->throwError("unrecognised input"); 01056 case "string": 01057 $arg[1] = ""; 01058 return $arg; 01059 case "keyword": 01060 return $arg; 01061 default: 01062 return array("keyword", $this->compileValue($arg)); 01063 } 01064 } 01065 01066 protected function lib__sprintf($args) { 01067 if ($args[0] != "list") return $args; 01068 $values = $args[2]; 01069 $string = array_shift($values); 01070 $template = $this->compileValue($this->lib_e($string)); 01071 01072 $i = 0; 01073 if (preg_match_all('/%[dsa]/', $template, $m)) { 01074 foreach ($m[0] as $match) { 01075 $val = isset($values[$i]) ? 01076 $this->reduce($values[$i]) : array('keyword', ''); 01077 01078 // lessjs compat, renders fully expanded color, not raw color 01079 if ($color = $this->coerceColor($val)) { 01080 $val = $color; 01081 } 01082 01083 $i++; 01084 $rep = $this->compileValue($this->lib_e($val)); 01085 $template = preg_replace('/'.self::preg_quote($match).'/', 01086 $rep, $template, 1); 01087 } 01088 } 01089 01090 $d = $string[0] == "string" ? $string[1] : '"'; 01091 return array("string", $d, array($template)); 01092 } 01093 01094 protected function lib_floor($arg) { 01095 $value = $this->assertNumber($arg); 01096 return array("number", floor($value), $arg[2]); 01097 } 01098 01099 protected function lib_ceil($arg) { 01100 $value = $this->assertNumber($arg); 01101 return array("number", ceil($value), $arg[2]); 01102 } 01103 01104 protected function lib_round($arg) { 01105 if($arg[0] != "list") { 01106 $value = $this->assertNumber($arg); 01107 return array("number", round($value), $arg[2]); 01108 } else { 01109 $value = $this->assertNumber($arg[2][0]); 01110 $precision = $this->assertNumber($arg[2][1]); 01111 return array("number", round($value, $precision), $arg[2][0][2]); 01112 } 01113 } 01114 01115 protected function lib_unit($arg) { 01116 if ($arg[0] == "list") { 01117 list($number, $newUnit) = $arg[2]; 01118 return array("number", $this->assertNumber($number), 01119 $this->compileValue($this->lib_e($newUnit))); 01120 } else { 01121 return array("number", $this->assertNumber($arg), ""); 01122 } 01123 } 01124 01129 public function colorArgs($args) { 01130 if ($args[0] != 'list' || count($args[2]) < 2) { 01131 return array(array('color', 0, 0, 0), 0); 01132 } 01133 list($color, $delta) = $args[2]; 01134 $color = $this->assertColor($color); 01135 $delta = floatval($delta[1]); 01136 01137 return array($color, $delta); 01138 } 01139 01140 protected function lib_darken($args) { 01141 list($color, $delta) = $this->colorArgs($args); 01142 01143 $hsl = $this->toHSL($color); 01144 $hsl[3] = $this->clamp($hsl[3] - $delta, 100); 01145 return $this->toRGB($hsl); 01146 } 01147 01148 protected function lib_lighten($args) { 01149 list($color, $delta) = $this->colorArgs($args); 01150 01151 $hsl = $this->toHSL($color); 01152 $hsl[3] = $this->clamp($hsl[3] + $delta, 100); 01153 return $this->toRGB($hsl); 01154 } 01155 01156 protected function lib_saturate($args) { 01157 list($color, $delta) = $this->colorArgs($args); 01158 01159 $hsl = $this->toHSL($color); 01160 $hsl[2] = $this->clamp($hsl[2] + $delta, 100); 01161 return $this->toRGB($hsl); 01162 } 01163 01164 protected function lib_desaturate($args) { 01165 list($color, $delta) = $this->colorArgs($args); 01166 01167 $hsl = $this->toHSL($color); 01168 $hsl[2] = $this->clamp($hsl[2] - $delta, 100); 01169 return $this->toRGB($hsl); 01170 } 01171 01172 protected function lib_spin($args) { 01173 list($color, $delta) = $this->colorArgs($args); 01174 01175 $hsl = $this->toHSL($color); 01176 01177 $hsl[1] = $hsl[1] + $delta % 360; 01178 if ($hsl[1] < 0) $hsl[1] += 360; 01179 01180 return $this->toRGB($hsl); 01181 } 01182 01183 protected function lib_fadeout($args) { 01184 list($color, $delta) = $this->colorArgs($args); 01185 $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100); 01186 return $color; 01187 } 01188 01189 protected function lib_fadein($args) { 01190 list($color, $delta) = $this->colorArgs($args); 01191 $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100); 01192 return $color; 01193 } 01194 01195 protected function lib_hue($color) { 01196 $hsl = $this->toHSL($this->assertColor($color)); 01197 return round($hsl[1]); 01198 } 01199 01200 protected function lib_saturation($color) { 01201 $hsl = $this->toHSL($this->assertColor($color)); 01202 return round($hsl[2]); 01203 } 01204 01205 protected function lib_lightness($color) { 01206 $hsl = $this->toHSL($this->assertColor($color)); 01207 return round($hsl[3]); 01208 } 01209 01210 // get the alpha of a color 01211 // defaults to 1 for non-colors or colors without an alpha 01212 protected function lib_alpha($value) { 01213 if (!is_null($color = $this->coerceColor($value))) { 01214 return isset($color[4]) ? $color[4] : 1; 01215 } 01216 } 01217 01218 // set the alpha of the color 01219 protected function lib_fade($args) { 01220 list($color, $alpha) = $this->colorArgs($args); 01221 $color[4] = $this->clamp($alpha / 100.0); 01222 return $color; 01223 } 01224 01225 protected function lib_percentage($arg) { 01226 $num = $this->assertNumber($arg); 01227 return array("number", $num*100, "%"); 01228 } 01229 01230 // mixes two colors by weight 01231 // mix(@color1, @color2, [@weight: 50%]); 01232 // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method 01233 protected function lib_mix($args) { 01234 if ($args[0] != "list" || count($args[2]) < 2) 01235 $this->throwError("mix expects (color1, color2, weight)"); 01236 01237 list($first, $second) = $args[2]; 01238 $first = $this->assertColor($first); 01239 $second = $this->assertColor($second); 01240 01241 $first_a = $this->lib_alpha($first); 01242 $second_a = $this->lib_alpha($second); 01243 01244 if (isset($args[2][2])) { 01245 $weight = $args[2][2][1] / 100.0; 01246 } else { 01247 $weight = 0.5; 01248 } 01249 01250 $w = $weight * 2 - 1; 01251 $a = $first_a - $second_a; 01252 01253 $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; 01254 $w2 = 1.0 - $w1; 01255 01256 $new = array('color', 01257 $w1 * $first[1] + $w2 * $second[1], 01258 $w1 * $first[2] + $w2 * $second[2], 01259 $w1 * $first[3] + $w2 * $second[3], 01260 ); 01261 01262 if ($first_a != 1.0 || $second_a != 1.0) { 01263 $new[] = $first_a * $weight + $second_a * ($weight - 1); 01264 } 01265 01266 return $this->fixColor($new); 01267 } 01268 01269 protected function lib_contrast($args) { 01270 $darkColor = array('color', 0, 0, 0); 01271 $lightColor = array('color', 255, 255, 255); 01272 $threshold = 0.43; 01273 01274 if ( $args[0] == 'list' ) { 01275 $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0]) : $lightColor; 01276 $darkColor = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1]) : $darkColor; 01277 $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2]) : $lightColor; 01278 $threshold = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold; 01279 } 01280 else { 01281 $inputColor = $this->assertColor($args); 01282 } 01283 01284 $inputColor = $this->coerceColor($inputColor); 01285 $darkColor = $this->coerceColor($darkColor); 01286 $lightColor = $this->coerceColor($lightColor); 01287 01288 //Figure out which is actually light and dark! 01289 if ( $this->lib_luma($darkColor) > $this->lib_luma($lightColor) ) { 01290 $t = $lightColor; 01291 $lightColor = $darkColor; 01292 $darkColor = $t; 01293 } 01294 01295 $inputColor_alpha = $this->lib_alpha($inputColor); 01296 if ( ( $this->lib_luma($inputColor) * $inputColor_alpha) < $threshold) { 01297 return $lightColor; 01298 } 01299 return $darkColor; 01300 } 01301 01302 protected function lib_luma($color) { 01303 $color = $this->coerceColor($color); 01304 return (0.2126 * $color[0] / 255) + (0.7152 * $color[1] / 255) + (0.0722 * $color[2] / 255); 01305 } 01306 01307 01308 public function assertColor($value, $error = "expected color value") { 01309 $color = $this->coerceColor($value); 01310 if (is_null($color)) $this->throwError($error); 01311 return $color; 01312 } 01313 01314 public function assertNumber($value, $error = "expecting number") { 01315 if ($value[0] == "number") return $value[1]; 01316 $this->throwError($error); 01317 } 01318 01319 public function assertArgs($value, $expectedArgs, $name="") { 01320 if ($expectedArgs == 1) { 01321 return $value; 01322 } else { 01323 if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list"); 01324 $values = $value[2]; 01325 $numValues = count($values); 01326 if ($expectedArgs != $numValues) { 01327 if ($name) { 01328 $name = $name . ": "; 01329 } 01330 01331 $this->throwError("${name}expecting $expectedArgs arguments, got $numValues"); 01332 } 01333 01334 return $values; 01335 } 01336 } 01337 01338 protected function toHSL($color) { 01339 if ($color[0] == 'hsl') return $color; 01340 01341 $r = $color[1] / 255; 01342 $g = $color[2] / 255; 01343 $b = $color[3] / 255; 01344 01345 $min = min($r, $g, $b); 01346 $max = max($r, $g, $b); 01347 01348 $L = ($min + $max) / 2; 01349 if ($min == $max) { 01350 $S = $H = 0; 01351 } else { 01352 if ($L < 0.5) 01353 $S = ($max - $min)/($max + $min); 01354 else 01355 $S = ($max - $min)/(2.0 - $max - $min); 01356 01357 if ($r == $max) $H = ($g - $b)/($max - $min); 01358 elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min); 01359 elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min); 01360 01361 } 01362 01363 $out = array('hsl', 01364 ($H < 0 ? $H + 6 : $H)*60, 01365 $S*100, 01366 $L*100, 01367 ); 01368 01369 if (count($color) > 4) $out[] = $color[4]; // copy alpha 01370 return $out; 01371 } 01372 01373 protected function toRGB_helper($comp, $temp1, $temp2) { 01374 if ($comp < 0) $comp += 1.0; 01375 elseif ($comp > 1) $comp -= 1.0; 01376 01377 if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp; 01378 if (2 * $comp < 1) return $temp2; 01379 if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; 01380 01381 return $temp1; 01382 } 01383 01388 protected function toRGB($color) { 01389 if ($color[0] == 'color') return $color; 01390 01391 $H = $color[1] / 360; 01392 $S = $color[2] / 100; 01393 $L = $color[3] / 100; 01394 01395 if ($S == 0) { 01396 $r = $g = $b = $L; 01397 } else { 01398 $temp2 = $L < 0.5 ? 01399 $L*(1.0 + $S) : 01400 $L + $S - $L * $S; 01401 01402 $temp1 = 2.0 * $L - $temp2; 01403 01404 $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2); 01405 $g = $this->toRGB_helper($H, $temp1, $temp2); 01406 $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2); 01407 } 01408 01409 // $out = array('color', round($r*255), round($g*255), round($b*255)); 01410 $out = array('color', $r*255, $g*255, $b*255); 01411 if (count($color) > 4) $out[] = $color[4]; // copy alpha 01412 return $out; 01413 } 01414 01415 protected function clamp($v, $max = 1, $min = 0) { 01416 return min($max, max($min, $v)); 01417 } 01418 01423 protected function funcToColor($func) { 01424 $fname = $func[1]; 01425 if ($func[2][0] != 'list') return false; // need a list of arguments 01426 $rawComponents = $func[2][2]; 01427 01428 if ($fname == 'hsl' || $fname == 'hsla') { 01429 $hsl = array('hsl'); 01430 $i = 0; 01431 foreach ($rawComponents as $c) { 01432 $val = $this->reduce($c); 01433 $val = isset($val[1]) ? floatval($val[1]) : 0; 01434 01435 if ($i == 0) $clamp = 360; 01436 elseif ($i < 3) $clamp = 100; 01437 else $clamp = 1; 01438 01439 $hsl[] = $this->clamp($val, $clamp); 01440 $i++; 01441 } 01442 01443 while (count($hsl) < 4) $hsl[] = 0; 01444 return $this->toRGB($hsl); 01445 01446 } elseif ($fname == 'rgb' || $fname == 'rgba') { 01447 $components = array(); 01448 $i = 1; 01449 foreach ($rawComponents as $c) { 01450 $c = $this->reduce($c); 01451 if ($i < 4) { 01452 if ($c[0] == "number" && $c[2] == "%") { 01453 $components[] = 255 * ($c[1] / 100); 01454 } else { 01455 $components[] = floatval($c[1]); 01456 } 01457 } elseif ($i == 4) { 01458 if ($c[0] == "number" && $c[2] == "%") { 01459 $components[] = 1.0 * ($c[1] / 100); 01460 } else { 01461 $components[] = floatval($c[1]); 01462 } 01463 } else break; 01464 01465 $i++; 01466 } 01467 while (count($components) < 3) $components[] = 0; 01468 array_unshift($components, 'color'); 01469 return $this->fixColor($components); 01470 } 01471 01472 return false; 01473 } 01474 01475 protected function reduce($value, $forExpression = false) { 01476 switch ($value[0]) { 01477 case "interpolate": 01478 $reduced = $this->reduce($value[1]); 01479 $var = $this->compileValue($reduced); 01480 $res = $this->reduce(array("variable", $this->vPrefix . $var)); 01481 01482 if ($res[0] == "raw_color") { 01483 $res = $this->coerceColor($res); 01484 } 01485 01486 if (empty($value[2])) $res = $this->lib_e($res); 01487 01488 return $res; 01489 case "variable": 01490 $key = $value[1]; 01491 if (is_array($key)) { 01492 $key = $this->reduce($key); 01493 $key = $this->vPrefix . $this->compileValue($this->lib_e($key)); 01494 } 01495 01496 $seen =& $this->env->seenNames; 01497 01498 if (!empty($seen[$key])) { 01499 $this->throwError("infinite loop detected: $key"); 01500 } 01501 01502 $seen[$key] = true; 01503 $out = $this->reduce($this->get($key)); 01504 $seen[$key] = false; 01505 return $out; 01506 case "list": 01507 foreach ($value[2] as &$item) { 01508 $item = $this->reduce($item, $forExpression); 01509 } 01510 return $value; 01511 case "expression": 01512 return $this->evaluate($value); 01513 case "string": 01514 foreach ($value[2] as &$part) { 01515 if (is_array($part)) { 01516 $strip = $part[0] == "variable"; 01517 $part = $this->reduce($part); 01518 if ($strip) $part = $this->lib_e($part); 01519 } 01520 } 01521 return $value; 01522 case "escape": 01523 list(,$inner) = $value; 01524 return $this->lib_e($this->reduce($inner)); 01525 case "function": 01526 $color = $this->funcToColor($value); 01527 if ($color) return $color; 01528 01529 list(, $name, $args) = $value; 01530 if ($name == "%") $name = "_sprintf"; 01531 01532 $f = isset($this->libFunctions[$name]) ? 01533 $this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name)); 01534 01535 if (is_callable($f)) { 01536 if ($args[0] == 'list') 01537 $args = self::compressList($args[2], $args[1]); 01538 01539 $ret = call_user_func($f, $this->reduce($args, true), $this); 01540 01541 if (is_null($ret)) { 01542 return array("string", "", array( 01543 $name, "(", $args, ")" 01544 )); 01545 } 01546 01547 // convert to a typed value if the result is a php primitive 01548 if (is_numeric($ret)) $ret = array('number', $ret, ""); 01549 elseif (!is_array($ret)) $ret = array('keyword', $ret); 01550 01551 return $ret; 01552 } 01553 01554 // plain function, reduce args 01555 $value[2] = $this->reduce($value[2]); 01556 return $value; 01557 case "unary": 01558 list(, $op, $exp) = $value; 01559 $exp = $this->reduce($exp); 01560 01561 if ($exp[0] == "number") { 01562 switch ($op) { 01563 case "+": 01564 return $exp; 01565 case "-": 01566 $exp[1] *= -1; 01567 return $exp; 01568 } 01569 } 01570 return array("string", "", array($op, $exp)); 01571 } 01572 01573 if ($forExpression) { 01574 switch ($value[0]) { 01575 case "keyword": 01576 if ($color = $this->coerceColor($value)) { 01577 return $color; 01578 } 01579 break; 01580 case "raw_color": 01581 return $this->coerceColor($value); 01582 } 01583 } 01584 01585 return $value; 01586 } 01587 01588 01589 // coerce a value for use in color operation 01590 protected function coerceColor($value) { 01591 switch($value[0]) { 01592 case 'color': return $value; 01593 case 'raw_color': 01594 $c = array("color", 0, 0, 0); 01595 $colorStr = substr($value[1], 1); 01596 $num = hexdec($colorStr); 01597 $width = strlen($colorStr) == 3 ? 16 : 256; 01598 01599 for ($i = 3; $i > 0; $i--) { // 3 2 1 01600 $t = $num % $width; 01601 $num /= $width; 01602 01603 $c[$i] = $t * (256/$width) + $t * floor(16/$width); 01604 } 01605 01606 return $c; 01607 case 'keyword': 01608 $name = $value[1]; 01609 if (isset(self::$cssColors[$name])) { 01610 $rgba = explode(',', self::$cssColors[$name]); 01611 01612 if(isset($rgba[3])) 01613 return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]); 01614 01615 return array('color', $rgba[0], $rgba[1], $rgba[2]); 01616 } 01617 return null; 01618 } 01619 } 01620 01621 // make something string like into a string 01622 protected function coerceString($value) { 01623 switch ($value[0]) { 01624 case "string": 01625 return $value; 01626 case "keyword": 01627 return array("string", "", array($value[1])); 01628 } 01629 return null; 01630 } 01631 01632 // turn list of length 1 into value type 01633 protected function flattenList($value) { 01634 if ($value[0] == "list" && count($value[2]) == 1) { 01635 return $this->flattenList($value[2][0]); 01636 } 01637 return $value; 01638 } 01639 01640 public function toBool($a) { 01641 if ($a) return self::$TRUE; 01642 else return self::$FALSE; 01643 } 01644 01645 // evaluate an expression 01646 protected function evaluate($exp) { 01647 list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; 01648 01649 $left = $this->reduce($left, true); 01650 $right = $this->reduce($right, true); 01651 01652 if ($leftColor = $this->coerceColor($left)) { 01653 $left = $leftColor; 01654 } 01655 01656 if ($rightColor = $this->coerceColor($right)) { 01657 $right = $rightColor; 01658 } 01659 01660 $ltype = $left[0]; 01661 $rtype = $right[0]; 01662 01663 // operators that work on all types 01664 if ($op == "and") { 01665 return $this->toBool($left == self::$TRUE && $right == self::$TRUE); 01666 } 01667 01668 if ($op == "=") { 01669 return $this->toBool($this->eq($left, $right) ); 01670 } 01671 01672 if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) { 01673 return $str; 01674 } 01675 01676 // type based operators 01677 $fname = "op_${ltype}_${rtype}"; 01678 if (is_callable(array($this, $fname))) { 01679 $out = $this->$fname($op, $left, $right); 01680 if (!is_null($out)) return $out; 01681 } 01682 01683 // make the expression look it did before being parsed 01684 $paddedOp = $op; 01685 if ($whiteBefore) $paddedOp = " " . $paddedOp; 01686 if ($whiteAfter) $paddedOp .= " "; 01687 01688 return array("string", "", array($left, $paddedOp, $right)); 01689 } 01690 01691 protected function stringConcatenate($left, $right) { 01692 if ($strLeft = $this->coerceString($left)) { 01693 if ($right[0] == "string") { 01694 $right[1] = ""; 01695 } 01696 $strLeft[2][] = $right; 01697 return $strLeft; 01698 } 01699 01700 if ($strRight = $this->coerceString($right)) { 01701 array_unshift($strRight[2], $left); 01702 return $strRight; 01703 } 01704 } 01705 01706 01707 // make sure a color's components don't go out of bounds 01708 protected function fixColor($c) { 01709 foreach (range(1, 3) as $i) { 01710 if ($c[$i] < 0) $c[$i] = 0; 01711 if ($c[$i] > 255) $c[$i] = 255; 01712 } 01713 01714 return $c; 01715 } 01716 01717 protected function op_number_color($op, $lft, $rgt) { 01718 if ($op == '+' || $op == '*') { 01719 return $this->op_color_number($op, $rgt, $lft); 01720 } 01721 } 01722 01723 protected function op_color_number($op, $lft, $rgt) { 01724 if ($rgt[0] == '%') $rgt[1] /= 100; 01725 01726 return $this->op_color_color($op, $lft, 01727 array_fill(1, count($lft) - 1, $rgt[1])); 01728 } 01729 01730 protected function op_color_color($op, $left, $right) { 01731 $out = array('color'); 01732 $max = count($left) > count($right) ? count($left) : count($right); 01733 foreach (range(1, $max - 1) as $i) { 01734 $lval = isset($left[$i]) ? $left[$i] : 0; 01735 $rval = isset($right[$i]) ? $right[$i] : 0; 01736 switch ($op) { 01737 case '+': 01738 $out[] = $lval + $rval; 01739 break; 01740 case '-': 01741 $out[] = $lval - $rval; 01742 break; 01743 case '*': 01744 $out[] = $lval * $rval; 01745 break; 01746 case '%': 01747 $out[] = $lval % $rval; 01748 break; 01749 case '/': 01750 if ($rval == 0) $this->throwError("evaluate error: can't divide by zero"); 01751 $out[] = $lval / $rval; 01752 break; 01753 default: 01754 $this->throwError('evaluate error: color op number failed on op '.$op); 01755 } 01756 } 01757 return $this->fixColor($out); 01758 } 01759 01760 function lib_red($color){ 01761 $color = $this->coerceColor($color); 01762 if (is_null($color)) { 01763 $this->throwError('color expected for red()'); 01764 } 01765 01766 return $color[1]; 01767 } 01768 01769 function lib_green($color){ 01770 $color = $this->coerceColor($color); 01771 if (is_null($color)) { 01772 $this->throwError('color expected for green()'); 01773 } 01774 01775 return $color[2]; 01776 } 01777 01778 function lib_blue($color){ 01779 $color = $this->coerceColor($color); 01780 if (is_null($color)) { 01781 $this->throwError('color expected for blue()'); 01782 } 01783 01784 return $color[3]; 01785 } 01786 01787 01788 // operator on two numbers 01789 protected function op_number_number($op, $left, $right) { 01790 $unit = empty($left[2]) ? $right[2] : $left[2]; 01791 01792 $value = 0; 01793 switch ($op) { 01794 case '+': 01795 $value = $left[1] + $right[1]; 01796 break; 01797 case '*': 01798 $value = $left[1] * $right[1]; 01799 break; 01800 case '-': 01801 $value = $left[1] - $right[1]; 01802 break; 01803 case '%': 01804 $value = $left[1] % $right[1]; 01805 break; 01806 case '/': 01807 if ($right[1] == 0) $this->throwError('parse error: divide by zero'); 01808 $value = $left[1] / $right[1]; 01809 break; 01810 case '<': 01811 return $this->toBool($left[1] < $right[1]); 01812 case '>': 01813 return $this->toBool($left[1] > $right[1]); 01814 case '>=': 01815 return $this->toBool($left[1] >= $right[1]); 01816 case '=<': 01817 return $this->toBool($left[1] <= $right[1]); 01818 default: 01819 $this->throwError('parse error: unknown number operator: '.$op); 01820 } 01821 01822 return array("number", $value, $unit); 01823 } 01824 01825 01826 /* environment functions */ 01827 01828 protected function makeOutputBlock($type, $selectors = null) { 01829 $b = new stdclass; 01830 $b->lines = array(); 01831 $b->children = array(); 01832 $b->selectors = $selectors; 01833 $b->type = $type; 01834 $b->parent = $this->scope; 01835 return $b; 01836 } 01837 01838 // the state of execution 01839 protected function pushEnv($block = null) { 01840 $e = new stdclass; 01841 $e->parent = $this->env; 01842 $e->store = array(); 01843 $e->block = $block; 01844 01845 $this->env = $e; 01846 return $e; 01847 } 01848 01849 // pop something off the stack 01850 protected function popEnv() { 01851 $old = $this->env; 01852 $this->env = $this->env->parent; 01853 return $old; 01854 } 01855 01856 // set something in the current env 01857 protected function set($name, $value) { 01858 $this->env->store[$name] = $value; 01859 } 01860 01861 01862 // get the highest occurrence entry for a name 01863 protected function get($name) { 01864 $current = $this->env; 01865 01866 $isArguments = $name == $this->vPrefix . 'arguments'; 01867 while ($current) { 01868 if ($isArguments && isset($current->arguments)) { 01869 return array('list', ' ', $current->arguments); 01870 } 01871 01872 if (isset($current->store[$name])) 01873 return $current->store[$name]; 01874 else { 01875 $current = isset($current->storeParent) ? 01876 $current->storeParent : $current->parent; 01877 } 01878 } 01879 01880 $this->throwError("variable $name is undefined"); 01881 } 01882 01883 // inject array of unparsed strings into environment as variables 01884 protected function injectVariables($args) { 01885 $this->pushEnv(); 01886 $parser = new lessc_parser($this, __METHOD__); 01887 foreach ($args as $name => $strValue) { 01888 if ($name{0} != '@') $name = '@'.$name; 01889 $parser->count = 0; 01890 $parser->buffer = (string)$strValue; 01891 if (!$parser->propertyValue($value)) { 01892 throw new Exception("failed to parse passed in variable $name: $strValue"); 01893 } 01894 01895 $this->set($name, $value); 01896 } 01897 } 01898 01903 public function __construct($fname = null) { 01904 if ($fname !== null) { 01905 // used for deprecated parse method 01906 $this->_parseFile = $fname; 01907 } 01908 } 01909 01910 public function compile($string, $name = null) { 01911 $locale = setlocale(LC_NUMERIC, 0); 01912 setlocale(LC_NUMERIC, "C"); 01913 01914 $this->parser = $this->makeParser($name); 01915 $root = $this->parser->parse($string); 01916 01917 $this->env = null; 01918 $this->scope = null; 01919 01920 $this->formatter = $this->newFormatter(); 01921 01922 if (!empty($this->registeredVars)) { 01923 $this->injectVariables($this->registeredVars); 01924 } 01925 01926 $this->sourceParser = $this->parser; // used for error messages 01927 $this->compileBlock($root); 01928 01929 ob_start(); 01930 $this->formatter->block($this->scope); 01931 $out = ob_get_clean(); 01932 setlocale(LC_NUMERIC, $locale); 01933 return $out; 01934 } 01935 01936 public function compileFile($fname, $outFname = null) { 01937 if (!is_readable($fname)) { 01938 throw new Exception('load error: failed to find '.$fname); 01939 } 01940 01941 $pi = pathinfo($fname); 01942 01943 $oldImport = $this->importDir; 01944 01945 $this->importDir = (array)$this->importDir; 01946 $this->importDir[] = $pi['dirname'].'/'; 01947 01948 $this->addParsedFile($fname); 01949 01950 $out = $this->compile(file_get_contents($fname), $fname); 01951 01952 $this->importDir = $oldImport; 01953 01954 if ($outFname !== null) { 01955 return file_put_contents($outFname, $out); 01956 } 01957 01958 return $out; 01959 } 01960 01961 // compile only if changed input has changed or output doesn't exist 01962 public function checkedCompile($in, $out) { 01963 if (!is_file($out) || filemtime($in) > filemtime($out)) { 01964 $this->compileFile($in, $out); 01965 return true; 01966 } 01967 return false; 01968 } 01969 01990 public function cachedCompile($in, $force = false) { 01991 // assume no root 01992 $root = null; 01993 01994 if (is_string($in)) { 01995 $root = $in; 01996 } elseif (is_array($in) and isset($in['root'])) { 01997 if ($force or ! isset($in['files'])) { 01998 // If we are forcing a recompile or if for some reason the 01999 // structure does not contain any file information we should 02000 // specify the root to trigger a rebuild. 02001 $root = $in['root']; 02002 } elseif (isset($in['files']) and is_array($in['files'])) { 02003 foreach ($in['files'] as $fname => $ftime ) { 02004 if (!file_exists($fname) or filemtime($fname) > $ftime) { 02005 // One of the files we knew about previously has changed 02006 // so we should look at our incoming root again. 02007 $root = $in['root']; 02008 break; 02009 } 02010 } 02011 } 02012 } else { 02013 // TODO: Throw an exception? We got neither a string nor something 02014 // that looks like a compatible lessphp cache structure. 02015 return null; 02016 } 02017 02018 if ($root !== null) { 02019 // If we have a root value which means we should rebuild. 02020 $out = array(); 02021 $out['root'] = $root; 02022 $out['compiled'] = $this->compileFile($root); 02023 $out['files'] = $this->allParsedFiles(); 02024 $out['updated'] = time(); 02025 return $out; 02026 } else { 02027 // No changes, pass back the structure 02028 // we were given initially. 02029 return $in; 02030 } 02031 02032 } 02033 02034 // parse and compile buffer 02035 // This is deprecated 02036 public function parse($str = null, $initialVariables = null) { 02037 if (is_array($str)) { 02038 $initialVariables = $str; 02039 $str = null; 02040 } 02041 02042 $oldVars = $this->registeredVars; 02043 if ($initialVariables !== null) { 02044 $this->setVariables($initialVariables); 02045 } 02046 02047 if ($str == null) { 02048 if (empty($this->_parseFile)) { 02049 throw new exception("nothing to parse"); 02050 } 02051 02052 $out = $this->compileFile($this->_parseFile); 02053 } else { 02054 $out = $this->compile($str); 02055 } 02056 02057 $this->registeredVars = $oldVars; 02058 return $out; 02059 } 02060 02061 protected function makeParser($name) { 02062 $parser = new lessc_parser($this, $name); 02063 $parser->writeComments = $this->preserveComments; 02064 02065 return $parser; 02066 } 02067 02068 public function setFormatter($name) { 02069 $this->formatterName = $name; 02070 } 02071 02072 protected function newFormatter() { 02073 $className = "lessc_formatter_lessjs"; 02074 if (!empty($this->formatterName)) { 02075 if (!is_string($this->formatterName)) 02076 return $this->formatterName; 02077 $className = "lessc_formatter_$this->formatterName"; 02078 } 02079 02080 return new $className; 02081 } 02082 02083 public function setPreserveComments($preserve) { 02084 $this->preserveComments = $preserve; 02085 } 02086 02087 public function registerFunction($name, $func) { 02088 $this->libFunctions[$name] = $func; 02089 } 02090 02091 public function unregisterFunction($name) { 02092 unset($this->libFunctions[$name]); 02093 } 02094 02095 public function setVariables($variables) { 02096 $this->registeredVars = array_merge($this->registeredVars, $variables); 02097 } 02098 02099 public function unsetVariable($name) { 02100 unset($this->registeredVars[$name]); 02101 } 02102 02103 public function setImportDir($dirs) { 02104 $this->importDir = (array)$dirs; 02105 } 02106 02107 public function addImportDir($dir) { 02108 $this->importDir = (array)$this->importDir; 02109 $this->importDir[] = $dir; 02110 } 02111 02112 public function allParsedFiles() { 02113 return $this->allParsedFiles; 02114 } 02115 02116 public function addParsedFile($file) { 02117 $this->allParsedFiles[realpath($file)] = filemtime($file); 02118 } 02119 02123 public function throwError($msg = null) { 02124 if ($this->sourceLoc >= 0) { 02125 $this->sourceParser->throwError($msg, $this->sourceLoc); 02126 } 02127 throw new exception($msg); 02128 } 02129 02130 // compile file $in to file $out if $in is newer than $out 02131 // returns true when it compiles, false otherwise 02132 public static function ccompile($in, $out, $less = null) { 02133 if ($less === null) { 02134 $less = new self; 02135 } 02136 return $less->checkedCompile($in, $out); 02137 } 02138 02139 public static function cexecute($in, $force = false, $less = null) { 02140 if ($less === null) { 02141 $less = new self; 02142 } 02143 return $less->cachedCompile($in, $force); 02144 } 02145 02146 static protected $cssColors = array( 02147 'aliceblue' => '240,248,255', 02148 'antiquewhite' => '250,235,215', 02149 'aqua' => '0,255,255', 02150 'aquamarine' => '127,255,212', 02151 'azure' => '240,255,255', 02152 'beige' => '245,245,220', 02153 'bisque' => '255,228,196', 02154 'black' => '0,0,0', 02155 'blanchedalmond' => '255,235,205', 02156 'blue' => '0,0,255', 02157 'blueviolet' => '138,43,226', 02158 'brown' => '165,42,42', 02159 'burlywood' => '222,184,135', 02160 'cadetblue' => '95,158,160', 02161 'chartreuse' => '127,255,0', 02162 'chocolate' => '210,105,30', 02163 'coral' => '255,127,80', 02164 'cornflowerblue' => '100,149,237', 02165 'cornsilk' => '255,248,220', 02166 'crimson' => '220,20,60', 02167 'cyan' => '0,255,255', 02168 'darkblue' => '0,0,139', 02169 'darkcyan' => '0,139,139', 02170 'darkgoldenrod' => '184,134,11', 02171 'darkgray' => '169,169,169', 02172 'darkgreen' => '0,100,0', 02173 'darkgrey' => '169,169,169', 02174 'darkkhaki' => '189,183,107', 02175 'darkmagenta' => '139,0,139', 02176 'darkolivegreen' => '85,107,47', 02177 'darkorange' => '255,140,0', 02178 'darkorchid' => '153,50,204', 02179 'darkred' => '139,0,0', 02180 'darksalmon' => '233,150,122', 02181 'darkseagreen' => '143,188,143', 02182 'darkslateblue' => '72,61,139', 02183 'darkslategray' => '47,79,79', 02184 'darkslategrey' => '47,79,79', 02185 'darkturquoise' => '0,206,209', 02186 'darkviolet' => '148,0,211', 02187 'deeppink' => '255,20,147', 02188 'deepskyblue' => '0,191,255', 02189 'dimgray' => '105,105,105', 02190 'dimgrey' => '105,105,105', 02191 'dodgerblue' => '30,144,255', 02192 'firebrick' => '178,34,34', 02193 'floralwhite' => '255,250,240', 02194 'forestgreen' => '34,139,34', 02195 'fuchsia' => '255,0,255', 02196 'gainsboro' => '220,220,220', 02197 'ghostwhite' => '248,248,255', 02198 'gold' => '255,215,0', 02199 'goldenrod' => '218,165,32', 02200 'gray' => '128,128,128', 02201 'green' => '0,128,0', 02202 'greenyellow' => '173,255,47', 02203 'grey' => '128,128,128', 02204 'honeydew' => '240,255,240', 02205 'hotpink' => '255,105,180', 02206 'indianred' => '205,92,92', 02207 'indigo' => '75,0,130', 02208 'ivory' => '255,255,240', 02209 'khaki' => '240,230,140', 02210 'lavender' => '230,230,250', 02211 'lavenderblush' => '255,240,245', 02212 'lawngreen' => '124,252,0', 02213 'lemonchiffon' => '255,250,205', 02214 'lightblue' => '173,216,230', 02215 'lightcoral' => '240,128,128', 02216 'lightcyan' => '224,255,255', 02217 'lightgoldenrodyellow' => '250,250,210', 02218 'lightgray' => '211,211,211', 02219 'lightgreen' => '144,238,144', 02220 'lightgrey' => '211,211,211', 02221 'lightpink' => '255,182,193', 02222 'lightsalmon' => '255,160,122', 02223 'lightseagreen' => '32,178,170', 02224 'lightskyblue' => '135,206,250', 02225 'lightslategray' => '119,136,153', 02226 'lightslategrey' => '119,136,153', 02227 'lightsteelblue' => '176,196,222', 02228 'lightyellow' => '255,255,224', 02229 'lime' => '0,255,0', 02230 'limegreen' => '50,205,50', 02231 'linen' => '250,240,230', 02232 'magenta' => '255,0,255', 02233 'maroon' => '128,0,0', 02234 'mediumaquamarine' => '102,205,170', 02235 'mediumblue' => '0,0,205', 02236 'mediumorchid' => '186,85,211', 02237 'mediumpurple' => '147,112,219', 02238 'mediumseagreen' => '60,179,113', 02239 'mediumslateblue' => '123,104,238', 02240 'mediumspringgreen' => '0,250,154', 02241 'mediumturquoise' => '72,209,204', 02242 'mediumvioletred' => '199,21,133', 02243 'midnightblue' => '25,25,112', 02244 'mintcream' => '245,255,250', 02245 'mistyrose' => '255,228,225', 02246 'moccasin' => '255,228,181', 02247 'navajowhite' => '255,222,173', 02248 'navy' => '0,0,128', 02249 'oldlace' => '253,245,230', 02250 'olive' => '128,128,0', 02251 'olivedrab' => '107,142,35', 02252 'orange' => '255,165,0', 02253 'orangered' => '255,69,0', 02254 'orchid' => '218,112,214', 02255 'palegoldenrod' => '238,232,170', 02256 'palegreen' => '152,251,152', 02257 'paleturquoise' => '175,238,238', 02258 'palevioletred' => '219,112,147', 02259 'papayawhip' => '255,239,213', 02260 'peachpuff' => '255,218,185', 02261 'peru' => '205,133,63', 02262 'pink' => '255,192,203', 02263 'plum' => '221,160,221', 02264 'powderblue' => '176,224,230', 02265 'purple' => '128,0,128', 02266 'red' => '255,0,0', 02267 'rosybrown' => '188,143,143', 02268 'royalblue' => '65,105,225', 02269 'saddlebrown' => '139,69,19', 02270 'salmon' => '250,128,114', 02271 'sandybrown' => '244,164,96', 02272 'seagreen' => '46,139,87', 02273 'seashell' => '255,245,238', 02274 'sienna' => '160,82,45', 02275 'silver' => '192,192,192', 02276 'skyblue' => '135,206,235', 02277 'slateblue' => '106,90,205', 02278 'slategray' => '112,128,144', 02279 'slategrey' => '112,128,144', 02280 'snow' => '255,250,250', 02281 'springgreen' => '0,255,127', 02282 'steelblue' => '70,130,180', 02283 'tan' => '210,180,140', 02284 'teal' => '0,128,128', 02285 'thistle' => '216,191,216', 02286 'tomato' => '255,99,71', 02287 'transparent' => '0,0,0,0', 02288 'turquoise' => '64,224,208', 02289 'violet' => '238,130,238', 02290 'wheat' => '245,222,179', 02291 'white' => '255,255,255', 02292 'whitesmoke' => '245,245,245', 02293 'yellow' => '255,255,0', 02294 'yellowgreen' => '154,205,50' 02295 ); 02296 } 02297 02298 // responsible for taking a string of LESS code and converting it into a 02299 // syntax tree 02300 class lessc_parser { 02301 static protected $nextBlockId = 0; // used to uniquely identify blocks 02302 02303 static protected $precedence = array( 02304 '=<' => 0, 02305 '>=' => 0, 02306 '=' => 0, 02307 '<' => 0, 02308 '>' => 0, 02309 02310 '+' => 1, 02311 '-' => 1, 02312 '*' => 2, 02313 '/' => 2, 02314 '%' => 2, 02315 ); 02316 02317 static protected $whitePattern; 02318 static protected $commentMulti; 02319 02320 static protected $commentSingle = "//"; 02321 static protected $commentMultiLeft = "/*"; 02322 static protected $commentMultiRight = "*/"; 02323 02324 // regex string to match any of the operators 02325 static protected $operatorString; 02326 02327 // these properties will supress division unless it's inside parenthases 02328 static protected $supressDivisionProps = 02329 array('/border-radius$/i', '/^font$/i'); 02330 02331 protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport"); 02332 protected $lineDirectives = array("charset"); 02333 02343 protected $inParens = false; 02344 02345 // caches preg escaped literals 02346 static protected $literalCache = array(); 02347 02348 public function __construct($lessc, $sourceName = null) { 02349 $this->eatWhiteDefault = true; 02350 // reference to less needed for vPrefix, mPrefix, and parentSelector 02351 $this->lessc = $lessc; 02352 02353 $this->sourceName = $sourceName; // name used for error messages 02354 02355 $this->writeComments = false; 02356 02357 if (!self::$operatorString) { 02358 self::$operatorString = 02359 '('.implode('|', array_map(array('lessc', 'preg_quote'), 02360 array_keys(self::$precedence))).')'; 02361 02362 $commentSingle = lessc::preg_quote(self::$commentSingle); 02363 $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft); 02364 $commentMultiRight = lessc::preg_quote(self::$commentMultiRight); 02365 02366 self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; 02367 self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; 02368 } 02369 } 02370 02371 public function parse($buffer) { 02372 $this->count = 0; 02373 $this->line = 1; 02374 02375 $this->env = null; // block stack 02376 $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer); 02377 $this->pushSpecialBlock("root"); 02378 $this->eatWhiteDefault = true; 02379 $this->seenComments = array(); 02380 02381 // trim whitespace on head 02382 // if (preg_match('/^\s+/', $this->buffer, $m)) { 02383 // $this->line += substr_count($m[0], "\n"); 02384 // $this->buffer = ltrim($this->buffer); 02385 // } 02386 $this->whitespace(); 02387 02388 // parse the entire file 02389 while (false !== $this->parseChunk()); 02390 02391 if ($this->count != strlen($this->buffer)) 02392 $this->throwError(); 02393 02394 // TODO report where the block was opened 02395 if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) ) 02396 throw new exception('parse error: unclosed block'); 02397 02398 return $this->env; 02399 } 02400 02437 protected function parseChunk() { 02438 if (empty($this->buffer)) return false; 02439 $s = $this->seek(); 02440 02441 if ($this->whitespace()) { 02442 return true; 02443 } 02444 02445 // setting a property 02446 if ($this->keyword($key) && $this->assign() && 02447 $this->propertyValue($value, $key) && $this->end()) 02448 { 02449 $this->append(array('assign', $key, $value), $s); 02450 return true; 02451 } else { 02452 $this->seek($s); 02453 } 02454 02455 02456 // look for special css blocks 02457 if ($this->literal('@', false)) { 02458 $this->count--; 02459 02460 // media 02461 if ($this->literal('@media')) { 02462 if (($this->mediaQueryList($mediaQueries) || true) 02463 && $this->literal('{')) 02464 { 02465 $media = $this->pushSpecialBlock("media"); 02466 $media->queries = is_null($mediaQueries) ? array() : $mediaQueries; 02467 return true; 02468 } else { 02469 $this->seek($s); 02470 return false; 02471 } 02472 } 02473 02474 if ($this->literal("@", false) && $this->keyword($dirName)) { 02475 if ($this->isDirective($dirName, $this->blockDirectives)) { 02476 if (($this->openString("{", $dirValue, null, array(";")) || true) && 02477 $this->literal("{")) 02478 { 02479 $dir = $this->pushSpecialBlock("directive"); 02480 $dir->name = $dirName; 02481 if (isset($dirValue)) $dir->value = $dirValue; 02482 return true; 02483 } 02484 } elseif ($this->isDirective($dirName, $this->lineDirectives)) { 02485 if ($this->propertyValue($dirValue) && $this->end()) { 02486 $this->append(array("directive", $dirName, $dirValue)); 02487 return true; 02488 } 02489 } 02490 } 02491 02492 $this->seek($s); 02493 } 02494 02495 // setting a variable 02496 if ($this->variable($var) && $this->assign() && 02497 $this->propertyValue($value) && $this->end()) 02498 { 02499 $this->append(array('assign', $var, $value), $s); 02500 return true; 02501 } else { 02502 $this->seek($s); 02503 } 02504 02505 if ($this->import($importValue)) { 02506 $this->append($importValue, $s); 02507 return true; 02508 } 02509 02510 // opening parametric mixin 02511 if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && 02512 ($this->guards($guards) || true) && 02513 $this->literal('{')) 02514 { 02515 $block = $this->pushBlock($this->fixTags(array($tag))); 02516 $block->args = $args; 02517 $block->isVararg = $isVararg; 02518 if (!empty($guards)) $block->guards = $guards; 02519 return true; 02520 } else { 02521 $this->seek($s); 02522 } 02523 02524 // opening a simple block 02525 if ($this->tags($tags) && $this->literal('{', false)) { 02526 $tags = $this->fixTags($tags); 02527 $this->pushBlock($tags); 02528 return true; 02529 } else { 02530 $this->seek($s); 02531 } 02532 02533 // closing a block 02534 if ($this->literal('}', false)) { 02535 try { 02536 $block = $this->pop(); 02537 } catch (exception $e) { 02538 $this->seek($s); 02539 $this->throwError($e->getMessage()); 02540 } 02541 02542 $hidden = false; 02543 if (is_null($block->type)) { 02544 $hidden = true; 02545 if (!isset($block->args)) { 02546 foreach ($block->tags as $tag) { 02547 if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) { 02548 $hidden = false; 02549 break; 02550 } 02551 } 02552 } 02553 02554 foreach ($block->tags as $tag) { 02555 if (is_string($tag)) { 02556 $this->env->children[$tag][] = $block; 02557 } 02558 } 02559 } 02560 02561 if (!$hidden) { 02562 $this->append(array('block', $block), $s); 02563 } 02564 02565 // this is done here so comments aren't bundled into he block that 02566 // was just closed 02567 $this->whitespace(); 02568 return true; 02569 } 02570 02571 // mixin 02572 if ($this->mixinTags($tags) && 02573 ($this->argumentDef($argv, $isVararg) || true) && 02574 ($this->keyword($suffix) || true) && $this->end()) 02575 { 02576 $tags = $this->fixTags($tags); 02577 $this->append(array('mixin', $tags, $argv, $suffix), $s); 02578 return true; 02579 } else { 02580 $this->seek($s); 02581 } 02582 02583 // spare ; 02584 if ($this->literal(';')) return true; 02585 02586 return false; // got nothing, throw error 02587 } 02588 02589 protected function isDirective($dirname, $directives) { 02590 // TODO: cache pattern in parser 02591 $pattern = implode("|", 02592 array_map(array("lessc", "preg_quote"), $directives)); 02593 $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; 02594 02595 return preg_match($pattern, $dirname); 02596 } 02597 02598 protected function fixTags($tags) { 02599 // move @ tags out of variable namespace 02600 foreach ($tags as &$tag) { 02601 if ($tag{0} == $this->lessc->vPrefix) 02602 $tag[0] = $this->lessc->mPrefix; 02603 } 02604 return $tags; 02605 } 02606 02607 // a list of expressions 02608 protected function expressionList(&$exps) { 02609 $values = array(); 02610 02611 while ($this->expression($exp)) { 02612 $values[] = $exp; 02613 } 02614 02615 if (count($values) == 0) return false; 02616 02617 $exps = lessc::compressList($values, ' '); 02618 return true; 02619 } 02620 02625 protected function expression(&$out) { 02626 if ($this->value($lhs)) { 02627 $out = $this->expHelper($lhs, 0); 02628 02629 // look for / shorthand 02630 if (!empty($this->env->supressedDivision)) { 02631 unset($this->env->supressedDivision); 02632 $s = $this->seek(); 02633 if ($this->literal("/") && $this->value($rhs)) { 02634 $out = array("list", "", 02635 array($out, array("keyword", "/"), $rhs)); 02636 } else { 02637 $this->seek($s); 02638 } 02639 } 02640 02641 return true; 02642 } 02643 return false; 02644 } 02645 02649 protected function expHelper($lhs, $minP) { 02650 $this->inExp = true; 02651 $ss = $this->seek(); 02652 02653 while (true) { 02654 $whiteBefore = isset($this->buffer[$this->count - 1]) && 02655 ctype_space($this->buffer[$this->count - 1]); 02656 02657 // If there is whitespace before the operator, then we require 02658 // whitespace after the operator for it to be an expression 02659 $needWhite = $whiteBefore && !$this->inParens; 02660 02661 if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) { 02662 if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) { 02663 foreach (self::$supressDivisionProps as $pattern) { 02664 if (preg_match($pattern, $this->env->currentProperty)) { 02665 $this->env->supressedDivision = true; 02666 break 2; 02667 } 02668 } 02669 } 02670 02671 02672 $whiteAfter = isset($this->buffer[$this->count - 1]) && 02673 ctype_space($this->buffer[$this->count - 1]); 02674 02675 if (!$this->value($rhs)) break; 02676 02677 // peek for next operator to see what to do with rhs 02678 if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) { 02679 $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); 02680 } 02681 02682 $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter); 02683 $ss = $this->seek(); 02684 02685 continue; 02686 } 02687 02688 break; 02689 } 02690 02691 $this->seek($ss); 02692 02693 return $lhs; 02694 } 02695 02696 // consume a list of values for a property 02697 public function propertyValue(&$value, $keyName = null) { 02698 $values = array(); 02699 02700 if ($keyName !== null) $this->env->currentProperty = $keyName; 02701 02702 $s = null; 02703 while ($this->expressionList($v)) { 02704 $values[] = $v; 02705 $s = $this->seek(); 02706 if (!$this->literal(',')) break; 02707 } 02708 02709 if ($s) $this->seek($s); 02710 02711 if ($keyName !== null) unset($this->env->currentProperty); 02712 02713 if (count($values) == 0) return false; 02714 02715 $value = lessc::compressList($values, ', '); 02716 return true; 02717 } 02718 02719 protected function parenValue(&$out) { 02720 $s = $this->seek(); 02721 02722 // speed shortcut 02723 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") { 02724 return false; 02725 } 02726 02727 $inParens = $this->inParens; 02728 if ($this->literal("(") && 02729 ($this->inParens = true) && $this->expression($exp) && 02730 $this->literal(")")) 02731 { 02732 $out = $exp; 02733 $this->inParens = $inParens; 02734 return true; 02735 } else { 02736 $this->inParens = $inParens; 02737 $this->seek($s); 02738 } 02739 02740 return false; 02741 } 02742 02743 // a single value 02744 protected function value(&$value) { 02745 $s = $this->seek(); 02746 02747 // speed shortcut 02748 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") { 02749 // negation 02750 if ($this->literal("-", false) && 02751 (($this->variable($inner) && $inner = array("variable", $inner)) || 02752 $this->unit($inner) || 02753 $this->parenValue($inner))) 02754 { 02755 $value = array("unary", "-", $inner); 02756 return true; 02757 } else { 02758 $this->seek($s); 02759 } 02760 } 02761 02762 if ($this->parenValue($value)) return true; 02763 if ($this->unit($value)) return true; 02764 if ($this->color($value)) return true; 02765 if ($this->func($value)) return true; 02766 if ($this->string($value)) return true; 02767 02768 if ($this->keyword($word)) { 02769 $value = array('keyword', $word); 02770 return true; 02771 } 02772 02773 // try a variable 02774 if ($this->variable($var)) { 02775 $value = array('variable', $var); 02776 return true; 02777 } 02778 02779 // unquote string (should this work on any type? 02780 if ($this->literal("~") && $this->string($str)) { 02781 $value = array("escape", $str); 02782 return true; 02783 } else { 02784 $this->seek($s); 02785 } 02786 02787 // css hack: \0 02788 if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { 02789 $value = array('keyword', '\\'.$m[1]); 02790 return true; 02791 } else { 02792 $this->seek($s); 02793 } 02794 02795 return false; 02796 } 02797 02798 // an import statement 02799 protected function import(&$out) { 02800 if (!$this->literal('@import')) return false; 02801 02802 // @import "something.css" media; 02803 // @import url("something.css") media; 02804 // @import url(something.css) media; 02805 02806 if ($this->propertyValue($value)) { 02807 $out = array("import", $value); 02808 return true; 02809 } 02810 } 02811 02812 protected function mediaQueryList(&$out) { 02813 if ($this->genericList($list, "mediaQuery", ",", false)) { 02814 $out = $list[2]; 02815 return true; 02816 } 02817 return false; 02818 } 02819 02820 protected function mediaQuery(&$out) { 02821 $s = $this->seek(); 02822 02823 $expressions = null; 02824 $parts = array(); 02825 02826 if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) { 02827 $prop = array("mediaType"); 02828 if (isset($only)) $prop[] = "only"; 02829 if (isset($not)) $prop[] = "not"; 02830 $prop[] = $mediaType; 02831 $parts[] = $prop; 02832 } else { 02833 $this->seek($s); 02834 } 02835 02836 02837 if (!empty($mediaType) && !$this->literal("and")) { 02838 // ~ 02839 } else { 02840 $this->genericList($expressions, "mediaExpression", "and", false); 02841 if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); 02842 } 02843 02844 if (count($parts) == 0) { 02845 $this->seek($s); 02846 return false; 02847 } 02848 02849 $out = $parts; 02850 return true; 02851 } 02852 02853 protected function mediaExpression(&$out) { 02854 $s = $this->seek(); 02855 $value = null; 02856 if ($this->literal("(") && 02857 $this->keyword($feature) && 02858 ($this->literal(":") && $this->expression($value) || true) && 02859 $this->literal(")")) 02860 { 02861 $out = array("mediaExp", $feature); 02862 if ($value) $out[] = $value; 02863 return true; 02864 } elseif ($this->variable($variable)) { 02865 $out = array('variable', $variable); 02866 return true; 02867 } 02868 02869 $this->seek($s); 02870 return false; 02871 } 02872 02873 // an unbounded string stopped by $end 02874 protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) { 02875 $oldWhite = $this->eatWhiteDefault; 02876 $this->eatWhiteDefault = false; 02877 02878 $stop = array("'", '"', "@{", $end); 02879 $stop = array_map(array("lessc", "preg_quote"), $stop); 02880 // $stop[] = self::$commentMulti; 02881 02882 if (!is_null($rejectStrs)) { 02883 $stop = array_merge($stop, $rejectStrs); 02884 } 02885 02886 $patt = '(.*?)('.implode("|", $stop).')'; 02887 02888 $nestingLevel = 0; 02889 02890 $content = array(); 02891 while ($this->match($patt, $m, false)) { 02892 if (!empty($m[1])) { 02893 $content[] = $m[1]; 02894 if ($nestingOpen) { 02895 $nestingLevel += substr_count($m[1], $nestingOpen); 02896 } 02897 } 02898 02899 $tok = $m[2]; 02900 02901 $this->count-= strlen($tok); 02902 if ($tok == $end) { 02903 if ($nestingLevel == 0) { 02904 break; 02905 } else { 02906 $nestingLevel--; 02907 } 02908 } 02909 02910 if (($tok == "'" || $tok == '"') && $this->string($str)) { 02911 $content[] = $str; 02912 continue; 02913 } 02914 02915 if ($tok == "@{" && $this->interpolation($inter)) { 02916 $content[] = $inter; 02917 continue; 02918 } 02919 02920 if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { 02921 break; 02922 } 02923 02924 $content[] = $tok; 02925 $this->count+= strlen($tok); 02926 } 02927 02928 $this->eatWhiteDefault = $oldWhite; 02929 02930 if (count($content) == 0) return false; 02931 02932 // trim the end 02933 if (is_string(end($content))) { 02934 $content[count($content) - 1] = rtrim(end($content)); 02935 } 02936 02937 $out = array("string", "", $content); 02938 return true; 02939 } 02940 02941 protected function string(&$out) { 02942 $s = $this->seek(); 02943 if ($this->literal('"', false)) { 02944 $delim = '"'; 02945 } elseif ($this->literal("'", false)) { 02946 $delim = "'"; 02947 } else { 02948 return false; 02949 } 02950 02951 $content = array(); 02952 02953 // look for either ending delim , escape, or string interpolation 02954 $patt = '([^\n]*?)(@\{|\\\\|' . 02955 lessc::preg_quote($delim).')'; 02956 02957 $oldWhite = $this->eatWhiteDefault; 02958 $this->eatWhiteDefault = false; 02959 02960 while ($this->match($patt, $m, false)) { 02961 $content[] = $m[1]; 02962 if ($m[2] == "@{") { 02963 $this->count -= strlen($m[2]); 02964 if ($this->interpolation($inter, false)) { 02965 $content[] = $inter; 02966 } else { 02967 $this->count += strlen($m[2]); 02968 $content[] = "@{"; // ignore it 02969 } 02970 } elseif ($m[2] == '\\') { 02971 $content[] = $m[2]; 02972 if ($this->literal($delim, false)) { 02973 $content[] = $delim; 02974 } 02975 } else { 02976 $this->count -= strlen($delim); 02977 break; // delim 02978 } 02979 } 02980 02981 $this->eatWhiteDefault = $oldWhite; 02982 02983 if ($this->literal($delim)) { 02984 $out = array("string", $delim, $content); 02985 return true; 02986 } 02987 02988 $this->seek($s); 02989 return false; 02990 } 02991 02992 protected function interpolation(&$out) { 02993 $oldWhite = $this->eatWhiteDefault; 02994 $this->eatWhiteDefault = true; 02995 02996 $s = $this->seek(); 02997 if ($this->literal("@{") && 02998 $this->openString("}", $interp, null, array("'", '"', ";")) && 02999 $this->literal("}", false)) 03000 { 03001 $out = array("interpolate", $interp); 03002 $this->eatWhiteDefault = $oldWhite; 03003 if ($this->eatWhiteDefault) $this->whitespace(); 03004 return true; 03005 } 03006 03007 $this->eatWhiteDefault = $oldWhite; 03008 $this->seek($s); 03009 return false; 03010 } 03011 03012 protected function unit(&$unit) { 03013 // speed shortcut 03014 if (isset($this->buffer[$this->count])) { 03015 $char = $this->buffer[$this->count]; 03016 if (!ctype_digit($char) && $char != ".") return false; 03017 } 03018 03019 if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { 03020 $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]); 03021 return true; 03022 } 03023 return false; 03024 } 03025 03026 // a # color 03027 protected function color(&$out) { 03028 if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { 03029 if (strlen($m[1]) > 7) { 03030 $out = array("string", "", array($m[1])); 03031 } else { 03032 $out = array("raw_color", $m[1]); 03033 } 03034 return true; 03035 } 03036 03037 return false; 03038 } 03039 03040 // consume an argument definition list surrounded by () 03041 // each argument is a variable name with optional value 03042 // or at the end a ... or a variable named followed by ... 03043 // arguments are separated by , unless a ; is in the list, then ; is the 03044 // delimiter. 03045 protected function argumentDef(&$args, &$isVararg) { 03046 $s = $this->seek(); 03047 if (!$this->literal('(')) return false; 03048 03049 $values = array(); 03050 $delim = ","; 03051 $method = "expressionList"; 03052 03053 $isVararg = false; 03054 while (true) { 03055 if ($this->literal("...")) { 03056 $isVararg = true; 03057 break; 03058 } 03059 03060 if ($this->$method($value)) { 03061 if ($value[0] == "variable") { 03062 $arg = array("arg", $value[1]); 03063 $ss = $this->seek(); 03064 03065 if ($this->assign() && $this->$method($rhs)) { 03066 $arg[] = $rhs; 03067 } else { 03068 $this->seek($ss); 03069 if ($this->literal("...")) { 03070 $arg[0] = "rest"; 03071 $isVararg = true; 03072 } 03073 } 03074 03075 $values[] = $arg; 03076 if ($isVararg) break; 03077 continue; 03078 } else { 03079 $values[] = array("lit", $value); 03080 } 03081 } 03082 03083 03084 if (!$this->literal($delim)) { 03085 if ($delim == "," && $this->literal(";")) { 03086 // found new delim, convert existing args 03087 $delim = ";"; 03088 $method = "propertyValue"; 03089 03090 // transform arg list 03091 if (isset($values[1])) { // 2 items 03092 $newList = array(); 03093 foreach ($values as $i => $arg) { 03094 switch($arg[0]) { 03095 case "arg": 03096 if ($i) { 03097 $this->throwError("Cannot mix ; and , as delimiter types"); 03098 } 03099 $newList[] = $arg[2]; 03100 break; 03101 case "lit": 03102 $newList[] = $arg[1]; 03103 break; 03104 case "rest": 03105 $this->throwError("Unexpected rest before semicolon"); 03106 } 03107 } 03108 03109 $newList = array("list", ", ", $newList); 03110 03111 switch ($values[0][0]) { 03112 case "arg": 03113 $newArg = array("arg", $values[0][1], $newList); 03114 break; 03115 case "lit": 03116 $newArg = array("lit", $newList); 03117 break; 03118 } 03119 03120 } elseif ($values) { // 1 item 03121 $newArg = $values[0]; 03122 } 03123 03124 if ($newArg) { 03125 $values = array($newArg); 03126 } 03127 } else { 03128 break; 03129 } 03130 } 03131 } 03132 03133 if (!$this->literal(')')) { 03134 $this->seek($s); 03135 return false; 03136 } 03137 03138 $args = $values; 03139 03140 return true; 03141 } 03142 03143 // consume a list of tags 03144 // this accepts a hanging delimiter 03145 protected function tags(&$tags, $simple = false, $delim = ',') { 03146 $tags = array(); 03147 while ($this->tag($tt, $simple)) { 03148 $tags[] = $tt; 03149 if (!$this->literal($delim)) break; 03150 } 03151 if (count($tags) == 0) return false; 03152 03153 return true; 03154 } 03155 03156 // list of tags of specifying mixin path 03157 // optionally separated by > (lazy, accepts extra >) 03158 protected function mixinTags(&$tags) { 03159 $tags = array(); 03160 while ($this->tag($tt, true)) { 03161 $tags[] = $tt; 03162 $this->literal(">"); 03163 } 03164 03165 if (count($tags) == 0) return false; 03166 03167 return true; 03168 } 03169 03170 // a bracketed value (contained within in a tag definition) 03171 protected function tagBracket(&$parts, &$hasExpression) { 03172 // speed shortcut 03173 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") { 03174 return false; 03175 } 03176 03177 $s = $this->seek(); 03178 03179 $hasInterpolation = false; 03180 03181 if ($this->literal("[", false)) { 03182 $attrParts = array("["); 03183 // keyword, string, operator 03184 while (true) { 03185 if ($this->literal("]", false)) { 03186 $this->count--; 03187 break; // get out early 03188 } 03189 03190 if ($this->match('\s+', $m)) { 03191 $attrParts[] = " "; 03192 continue; 03193 } 03194 if ($this->string($str)) { 03195 // escape parent selector, (yuck) 03196 foreach ($str[2] as &$chunk) { 03197 $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk); 03198 } 03199 03200 $attrParts[] = $str; 03201 $hasInterpolation = true; 03202 continue; 03203 } 03204 03205 if ($this->keyword($word)) { 03206 $attrParts[] = $word; 03207 continue; 03208 } 03209 03210 if ($this->interpolation($inter, false)) { 03211 $attrParts[] = $inter; 03212 $hasInterpolation = true; 03213 continue; 03214 } 03215 03216 // operator, handles attr namespace too 03217 if ($this->match('[|-~\$\*\^=]+', $m)) { 03218 $attrParts[] = $m[0]; 03219 continue; 03220 } 03221 03222 break; 03223 } 03224 03225 if ($this->literal("]", false)) { 03226 $attrParts[] = "]"; 03227 foreach ($attrParts as $part) { 03228 $parts[] = $part; 03229 } 03230 $hasExpression = $hasExpression || $hasInterpolation; 03231 return true; 03232 } 03233 $this->seek($s); 03234 } 03235 03236 $this->seek($s); 03237 return false; 03238 } 03239 03240 // a space separated list of selectors 03241 protected function tag(&$tag, $simple = false) { 03242 if ($simple) 03243 $chars = '^@,:;{}\][>\(\) "\''; 03244 else 03245 $chars = '^@,;{}["\''; 03246 03247 $s = $this->seek(); 03248 03249 $hasExpression = false; 03250 $parts = array(); 03251 while ($this->tagBracket($parts, $hasExpression)); 03252 03253 $oldWhite = $this->eatWhiteDefault; 03254 $this->eatWhiteDefault = false; 03255 03256 while (true) { 03257 if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) { 03258 $parts[] = $m[1]; 03259 if ($simple) break; 03260 03261 while ($this->tagBracket($parts, $hasExpression)); 03262 continue; 03263 } 03264 03265 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { 03266 if ($this->interpolation($interp)) { 03267 $hasExpression = true; 03268 $interp[2] = true; // don't unescape 03269 $parts[] = $interp; 03270 continue; 03271 } 03272 03273 if ($this->literal("@")) { 03274 $parts[] = "@"; 03275 continue; 03276 } 03277 } 03278 03279 if ($this->unit($unit)) { // for keyframes 03280 $parts[] = $unit[1]; 03281 $parts[] = $unit[2]; 03282 continue; 03283 } 03284 03285 break; 03286 } 03287 03288 $this->eatWhiteDefault = $oldWhite; 03289 if (!$parts) { 03290 $this->seek($s); 03291 return false; 03292 } 03293 03294 if ($hasExpression) { 03295 $tag = array("exp", array("string", "", $parts)); 03296 } else { 03297 $tag = trim(implode($parts)); 03298 } 03299 03300 $this->whitespace(); 03301 return true; 03302 } 03303 03304 // a css function 03305 protected function func(&$func) { 03306 $s = $this->seek(); 03307 03308 if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) { 03309 $fname = $m[1]; 03310 03311 $sPreArgs = $this->seek(); 03312 03313 $args = array(); 03314 while (true) { 03315 $ss = $this->seek(); 03316 // this ugly nonsense is for ie filter properties 03317 if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { 03318 $args[] = array("string", "", array($name, "=", $value)); 03319 } else { 03320 $this->seek($ss); 03321 if ($this->expressionList($value)) { 03322 $args[] = $value; 03323 } 03324 } 03325 03326 if (!$this->literal(',')) break; 03327 } 03328 $args = array('list', ',', $args); 03329 03330 if ($this->literal(')')) { 03331 $func = array('function', $fname, $args); 03332 return true; 03333 } elseif ($fname == 'url') { 03334 // couldn't parse and in url? treat as string 03335 $this->seek($sPreArgs); 03336 if ($this->openString(")", $string) && $this->literal(")")) { 03337 $func = array('function', $fname, $string); 03338 return true; 03339 } 03340 } 03341 } 03342 03343 $this->seek($s); 03344 return false; 03345 } 03346 03347 // consume a less variable 03348 protected function variable(&$name) { 03349 $s = $this->seek(); 03350 if ($this->literal($this->lessc->vPrefix, false) && 03351 ($this->variable($sub) || $this->keyword($name))) 03352 { 03353 if (!empty($sub)) { 03354 $name = array('variable', $sub); 03355 } else { 03356 $name = $this->lessc->vPrefix.$name; 03357 } 03358 return true; 03359 } 03360 03361 $name = null; 03362 $this->seek($s); 03363 return false; 03364 } 03365 03370 protected function assign($name = null) { 03371 if ($name) $this->currentProperty = $name; 03372 return $this->literal(':') || $this->literal('='); 03373 } 03374 03375 // consume a keyword 03376 protected function keyword(&$word) { 03377 if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { 03378 $word = $m[1]; 03379 return true; 03380 } 03381 return false; 03382 } 03383 03384 // consume an end of statement delimiter 03385 protected function end() { 03386 if ($this->literal(';', false)) { 03387 return true; 03388 } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { 03389 // if there is end of file or a closing block next then we don't need a ; 03390 return true; 03391 } 03392 return false; 03393 } 03394 03395 protected function guards(&$guards) { 03396 $s = $this->seek(); 03397 03398 if (!$this->literal("when")) { 03399 $this->seek($s); 03400 return false; 03401 } 03402 03403 $guards = array(); 03404 03405 while ($this->guardGroup($g)) { 03406 $guards[] = $g; 03407 if (!$this->literal(",")) break; 03408 } 03409 03410 if (count($guards) == 0) { 03411 $guards = null; 03412 $this->seek($s); 03413 return false; 03414 } 03415 03416 return true; 03417 } 03418 03419 // a bunch of guards that are and'd together 03420 // TODO rename to guardGroup 03421 protected function guardGroup(&$guardGroup) { 03422 $s = $this->seek(); 03423 $guardGroup = array(); 03424 while ($this->guard($guard)) { 03425 $guardGroup[] = $guard; 03426 if (!$this->literal("and")) break; 03427 } 03428 03429 if (count($guardGroup) == 0) { 03430 $guardGroup = null; 03431 $this->seek($s); 03432 return false; 03433 } 03434 03435 return true; 03436 } 03437 03438 protected function guard(&$guard) { 03439 $s = $this->seek(); 03440 $negate = $this->literal("not"); 03441 03442 if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) { 03443 $guard = $exp; 03444 if ($negate) $guard = array("negate", $guard); 03445 return true; 03446 } 03447 03448 $this->seek($s); 03449 return false; 03450 } 03451 03452 /* raw parsing functions */ 03453 03454 protected function literal($what, $eatWhitespace = null) { 03455 if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 03456 03457 // shortcut on single letter 03458 if (!isset($what[1]) && isset($this->buffer[$this->count])) { 03459 if ($this->buffer[$this->count] == $what) { 03460 if (!$eatWhitespace) { 03461 $this->count++; 03462 return true; 03463 } 03464 // goes below... 03465 } else { 03466 return false; 03467 } 03468 } 03469 03470 if (!isset(self::$literalCache[$what])) { 03471 self::$literalCache[$what] = lessc::preg_quote($what); 03472 } 03473 03474 return $this->match(self::$literalCache[$what], $m, $eatWhitespace); 03475 } 03476 03477 protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { 03478 $s = $this->seek(); 03479 $items = array(); 03480 while ($this->$parseItem($value)) { 03481 $items[] = $value; 03482 if ($delim) { 03483 if (!$this->literal($delim)) break; 03484 } 03485 } 03486 03487 if (count($items) == 0) { 03488 $this->seek($s); 03489 return false; 03490 } 03491 03492 if ($flatten && count($items) == 1) { 03493 $out = $items[0]; 03494 } else { 03495 $out = array("list", $delim, $items); 03496 } 03497 03498 return true; 03499 } 03500 03501 03502 // advance counter to next occurrence of $what 03503 // $until - don't include $what in advance 03504 // $allowNewline, if string, will be used as valid char set 03505 protected function to($what, &$out, $until = false, $allowNewline = false) { 03506 if (is_string($allowNewline)) { 03507 $validChars = $allowNewline; 03508 } else { 03509 $validChars = $allowNewline ? "." : "[^\n]"; 03510 } 03511 if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false; 03512 if ($until) $this->count -= strlen($what); // give back $what 03513 $out = $m[1]; 03514 return true; 03515 } 03516 03517 // try to match something on head of buffer 03518 protected function match($regex, &$out, $eatWhitespace = null) { 03519 if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 03520 03521 $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais'; 03522 if (preg_match($r, $this->buffer, $out, null, $this->count)) { 03523 $this->count += strlen($out[0]); 03524 if ($eatWhitespace && $this->writeComments) $this->whitespace(); 03525 return true; 03526 } 03527 return false; 03528 } 03529 03530 // match some whitespace 03531 protected function whitespace() { 03532 if ($this->writeComments) { 03533 $gotWhite = false; 03534 while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { 03535 if (isset($m[1]) && empty($this->seenComments[$this->count])) { 03536 $this->append(array("comment", $m[1])); 03537 $this->seenComments[$this->count] = true; 03538 } 03539 $this->count += strlen($m[0]); 03540 $gotWhite = true; 03541 } 03542 return $gotWhite; 03543 } else { 03544 $this->match("", $m); 03545 return strlen($m[0]) > 0; 03546 } 03547 } 03548 03549 // match something without consuming it 03550 protected function peek($regex, &$out = null, $from=null) { 03551 if (is_null($from)) $from = $this->count; 03552 $r = '/'.$regex.'/Ais'; 03553 $result = preg_match($r, $this->buffer, $out, null, $from); 03554 03555 return $result; 03556 } 03557 03558 // seek to a spot in the buffer or return where we are on no argument 03559 protected function seek($where = null) { 03560 if ($where === null) return $this->count; 03561 else $this->count = $where; 03562 return true; 03563 } 03564 03565 /* misc functions */ 03566 03567 public function throwError($msg = "parse error", $count = null) { 03568 $count = is_null($count) ? $this->count : $count; 03569 03570 $line = $this->line + 03571 substr_count(substr($this->buffer, 0, $count), "\n"); 03572 03573 if (!empty($this->sourceName)) { 03574 $loc = "$this->sourceName on line $line"; 03575 } else { 03576 $loc = "line: $line"; 03577 } 03578 03579 // TODO this depends on $this->count 03580 if ($this->peek("(.*?)(\n|$)", $m, $count)) { 03581 throw new exception("$msg: failed at `$m[1]` $loc"); 03582 } else { 03583 throw new exception("$msg: $loc"); 03584 } 03585 } 03586 03587 protected function pushBlock($selectors=null, $type=null) { 03588 $b = new stdclass; 03589 $b->parent = $this->env; 03590 03591 $b->type = $type; 03592 $b->id = self::$nextBlockId++; 03593 03594 $b->isVararg = false; // TODO: kill me from here 03595 $b->tags = $selectors; 03596 03597 $b->props = array(); 03598 $b->children = array(); 03599 03600 $this->env = $b; 03601 return $b; 03602 } 03603 03604 // push a block that doesn't multiply tags 03605 protected function pushSpecialBlock($type) { 03606 return $this->pushBlock(null, $type); 03607 } 03608 03609 // append a property to the current block 03610 protected function append($prop, $pos = null) { 03611 if ($pos !== null) $prop[-1] = $pos; 03612 $this->env->props[] = $prop; 03613 } 03614 03615 // pop something off the stack 03616 protected function pop() { 03617 $old = $this->env; 03618 $this->env = $this->env->parent; 03619 return $old; 03620 } 03621 03622 // remove comments from $text 03623 // todo: make it work for all functions, not just url 03624 protected function removeComments($text) { 03625 $look = array( 03626 'url(', '//', '/*', '"', "'" 03627 ); 03628 03629 $out = ''; 03630 $min = null; 03631 while (true) { 03632 // find the next item 03633 foreach ($look as $token) { 03634 $pos = strpos($text, $token); 03635 if ($pos !== false) { 03636 if (!isset($min) || $pos < $min[1]) $min = array($token, $pos); 03637 } 03638 } 03639 03640 if (is_null($min)) break; 03641 03642 $count = $min[1]; 03643 $skip = 0; 03644 $newlines = 0; 03645 switch ($min[0]) { 03646 case 'url(': 03647 if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) 03648 $count += strlen($m[0]) - strlen($min[0]); 03649 break; 03650 case '"': 03651 case "'": 03652 if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count)) 03653 $count += strlen($m[0]) - 1; 03654 break; 03655 case '//': 03656 $skip = strpos($text, "\n", $count); 03657 if ($skip === false) $skip = strlen($text) - $count; 03658 else $skip -= $count; 03659 break; 03660 case '/*': 03661 if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) { 03662 $skip = strlen($m[0]); 03663 $newlines = substr_count($m[0], "\n"); 03664 } 03665 break; 03666 } 03667 03668 if ($skip == 0) $count += strlen($min[0]); 03669 03670 $out .= substr($text, 0, $count).str_repeat("\n", $newlines); 03671 $text = substr($text, $count + $skip); 03672 03673 $min = null; 03674 } 03675 03676 return $out.$text; 03677 } 03678 03679 } 03680 03681 class lessc_formatter_classic { 03682 public $indentChar = " "; 03683 03684 public $break = "\n"; 03685 public $open = " {"; 03686 public $close = "}"; 03687 public $selectorSeparator = ", "; 03688 public $assignSeparator = ":"; 03689 03690 public $openSingle = " { "; 03691 public $closeSingle = " }"; 03692 03693 public $disableSingle = false; 03694 public $breakSelectors = false; 03695 03696 public $compressColors = false; 03697 03698 public function __construct() { 03699 $this->indentLevel = 0; 03700 } 03701 03702 public function indentStr($n = 0) { 03703 return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); 03704 } 03705 03706 public function property($name, $value) { 03707 return $name . $this->assignSeparator . $value . ";"; 03708 } 03709 03710 protected function isEmpty($block) { 03711 if (empty($block->lines)) { 03712 foreach ($block->children as $child) { 03713 if (!$this->isEmpty($child)) return false; 03714 } 03715 03716 return true; 03717 } 03718 return false; 03719 } 03720 03721 public function block($block) { 03722 if ($this->isEmpty($block)) return; 03723 03724 $inner = $pre = $this->indentStr(); 03725 03726 $isSingle = !$this->disableSingle && 03727 is_null($block->type) && count($block->lines) == 1; 03728 03729 if (!empty($block->selectors)) { 03730 $this->indentLevel++; 03731 03732 if ($this->breakSelectors) { 03733 $selectorSeparator = $this->selectorSeparator . $this->break . $pre; 03734 } else { 03735 $selectorSeparator = $this->selectorSeparator; 03736 } 03737 03738 echo $pre . 03739 implode($selectorSeparator, $block->selectors); 03740 if ($isSingle) { 03741 echo $this->openSingle; 03742 $inner = ""; 03743 } else { 03744 echo $this->open . $this->break; 03745 $inner = $this->indentStr(); 03746 } 03747 03748 } 03749 03750 if (!empty($block->lines)) { 03751 $glue = $this->break.$inner; 03752 echo $inner . implode($glue, $block->lines); 03753 if (!$isSingle && !empty($block->children)) { 03754 echo $this->break; 03755 } 03756 } 03757 03758 foreach ($block->children as $child) { 03759 $this->block($child); 03760 } 03761 03762 if (!empty($block->selectors)) { 03763 if (!$isSingle && empty($block->children)) echo $this->break; 03764 03765 if ($isSingle) { 03766 echo $this->closeSingle . $this->break; 03767 } else { 03768 echo $pre . $this->close . $this->break; 03769 } 03770 03771 $this->indentLevel--; 03772 } 03773 } 03774 } 03775 03776 class lessc_formatter_compressed extends lessc_formatter_classic { 03777 public $disableSingle = true; 03778 public $open = "{"; 03779 public $selectorSeparator = ","; 03780 public $assignSeparator = ":"; 03781 public $break = ""; 03782 public $compressColors = true; 03783 03784 public function indentStr($n = 0) { 03785 return ""; 03786 } 03787 } 03788 03789 class lessc_formatter_lessjs extends lessc_formatter_classic { 03790 public $disableSingle = true; 03791 public $breakSelectors = true; 03792 public $assignSeparator = ": "; 03793 public $selectorSeparator = ","; 03794 } 03795 03796