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