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