PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.2.20
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.2.20
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / vendor / marcusschwarz / lesserphp / lessc.inc.php

lessc.inc.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 3.2.20, at vendor/marcusschwarz/lesserphp/lessc.inc.php

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