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