1 <?php
2
3 4 5 6 7 8 9 10 11
12
13
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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 = false;
49
50 public $vPrefix = '@';
51 public $mPrefix = '$';
52 public $parentSelector = '&';
53
54 public $importDisabled = false;
55 public $importDir = '';
56
57 protected $numberPrecision = null;
58
59 protected $allParsedFiles = array();
60
61
62
63 protected $sourceParser = null;
64 protected $sourceLoc = null;
65
66 static protected $nextImportId = 0;
67
68
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
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
123 foreach ($root->props as $prop) {
124 if ($prop[0] == "block") {
125 $prop[1]->parent = $parentBlock;
126 }
127 }
128
129
130
131
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
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 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 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;
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 290 291 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
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
445 protected function multiplySelectors($selectors) {
446
447
448 $parentSelectors = $this->findClosestSelectors();
449 if (is_null($parentSelectors)) {
450
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
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
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
497
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;
549
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
559 if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
560 return false;
561 }
562 break;
563 case "rest":
564 $i--;
565 break 2;
566 }
567 }
568
569 if ($block->isVararg) {
570 return true;
571 } else {
572 $numMatched = $i + 1;
573
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
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
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
608
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
632
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
641 $value = $keywordValues[$a[1]];
642 } elseif (isset($orderedValues[$i])) {
643
644 $value = $orderedValues[$i];
645 $i++;
646 } elseif (isset($a[2])) {
647
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
659 $i++;
660 }
661 }
662
663
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
671 $this->env->arguments = $assignedValues + $orderedValues;
672 }
673
674
675 protected function compileProp($prop, $block, $out) {
676
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 813 814 815 816 817 818 819 820 821
822 public function compileValue($value) {
823 switch ($value[0]) {
824 case 'list':
825
826
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
835 return $value[1];
836 case 'number':
837 list(, $num, $unit) = $value;
838
839
840 if ($this->numberPrecision !== null) {
841 $num = round($num, $this->numberPrecision);
842 }
843 return $num . $unit;
844 case 'string':
845
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
855
856
857
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) {
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
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:
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 ($value) {
932 list($list, $idx) = $this->assertArgs($value, 2, "extract");
933 $idx = $this->assertNumber($idx);
934
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 988 989 990 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
1000 if($fsize/1024 < 32) {
1001 if(is_null($mime)) {
1002 if(class_exists('finfo')) {
1003 $finfo = new finfo(FILEINFO_MIME);
1004 $mime = explode('; ', $finfo->file($fullpath));
1005 $mime = $mime[0];
1006 } elseif(function_exists('mime_content_type')) {
1007 $mime = mime_content_type($fullpath);
1008 }
1009 }
1010
1011 if(!is_null($mime))
1012 $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath)));
1013 }
1014 }
1015
1016 return 'url("'.$url.'")';
1017 }
1018
1019
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
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 1099 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
1183
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
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
1203
1204
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
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];
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 1358 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
1382 $out = array('color', $r*255, $g*255, $b*255);
1383 if (count($color) > 4) $out[] = $color[4];
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 1393 1394
1395 protected function funcToColor($func) {
1396 $fname = $func[1];
1397 if ($func[2][0] != 'list') return false;
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
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
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
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--) {
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
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
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
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
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
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
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
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
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
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
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
1822 protected function popEnv() {
1823 $old = $this->env;
1824 $this->env = $this->env->parent;
1825 return $old;
1826 }
1827
1828
1829 protected function set($name, $value) {
1830 $this->env->store[$name] = $value;
1831 }
1832
1833
1834
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
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 1873 1874
1875 public function __construct($fname = null) {
1876 if ($fname !== null) {
1877
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;
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
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 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961
1962 public function cachedCompile($in, $force = false) {
1963
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
1971
1972
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
1978
1979 $root = $in['root'];
1980 break;
1981 }
1982 }
1983 }
1984 } else {
1985
1986
1987 return null;
1988 }
1989
1990 if ($root !== null) {
1991
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
2000
2001 return $in;
2002 }
2003
2004 }
2005
2006
2007
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 ($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 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
2103
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
2271
2272 class lessc_parser {
2273 static protected $nextBlockId = 0;
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 ;
2291
2292 static protected = "//";
2293 static protected = "/*";
2294 static protected = "*/";
2295
2296
2297 static protected $operatorString;
2298
2299
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 2308 2309 2310 2311 2312 2313 2314
2315 protected $inParens = false;
2316
2317
2318 static protected $literalCache = array();
2319
2320 public function __construct($lessc, $sourceName = null) {
2321 $this->eatWhiteDefault = true;
2322
2323 $this->lessc = $lessc;
2324
2325 $this->sourceName = $sourceName;
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;
2348 $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
2349 $this->pushSpecialBlock("root");
2350 $this->eatWhiteDefault = true;
2351 $this->seenComments = array();
2352
2353
2354
2355
2356
2357
2358 $this->whitespace();
2359
2360
2361 while (false !== $this->parseChunk());
2362
2363 if ($this->count != strlen($this->buffer))
2364 $this->throwError();
2365
2366
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 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 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
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
2429 if ($this->literal('@', false)) {
2430 $this->count--;
2431
2432
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
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
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
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
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
2538
2539 $this->whitespace();
2540 return true;
2541 }
2542
2543
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
2556 if ($this->literal(';')) return true;
2557
2558 return false;
2559 }
2560
2561 protected function isDirective($dirname, $directives) {
2562
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
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
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 2595 2596
2597 protected function expression(&$out) {
2598 if ($this->value($lhs)) {
2599 $out = $this->expHelper($lhs, 0);
2600
2601
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 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
2630
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
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
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
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
2716 protected function value(&$value) {
2717 $s = $this->seek();
2718
2719
2720 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") {
2721
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
2746 if ($this->variable($var)) {
2747 $value = array('variable', $var);
2748 return true;
2749 }
2750
2751
2752 if ($this->literal("~") && $this->string($str)) {
2753 $value = array("escape", $str);
2754 return true;
2755 } else {
2756 $this->seek($s);
2757 }
2758
2759
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
2771 protected function import(&$out) {
2772 if (!$this->literal('@import')) return false;
2773
2774
2775
2776
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
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
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
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
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[] = "@{";
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;
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
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
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
3013
3014
3015
3016
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
3059 $delim = ";";
3060 $method = "propertyValue";
3061
3062
3063 if (isset($values[1])) {
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) {
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
3116
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
3129
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
3143 protected function tagBracket(&$parts, &$hasExpression) {
3144
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
3156 while (true) {
3157 if ($this->literal("]", false)) {
3158 $this->count--;
3159 break;
3160 }
3161
3162 if ($this->match('\s+', $m)) {
3163 $attrParts[] = " ";
3164 continue;
3165 }
3166 if ($this->string($str)) {
3167
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
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
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;
3241 $parts[] = $interp;
3242 continue;
3243 }
3244
3245 if ($this->literal("@")) {
3246 $parts[] = "@";
3247 continue;
3248 }
3249 }
3250
3251 if ($this->unit($unit)) {
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
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
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
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
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 3340 3341
3342 protected function assign($name = null) {
3343 if ($name) $this->currentProperty = $name;
3344 return $this->literal(':') || $this->literal('=');
3345 }
3346
3347
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
3357 protected function end() {
3358 if ($this->literal(';', false)) {
3359 return true;
3360 } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
3361
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
3392
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
3425
3426 protected function literal($what, $eatWhitespace = null) {
3427 if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
3428
3429
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
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
3475
3476
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);
3485 $out = $m[1];
3486 return true;
3487 }
3488
3489
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
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
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
3531 protected function seek($where = null) {
3532 if ($where === null) return $this->count;
3533 else $this->count = $where;
3534 return true;
3535 }
3536
3537
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
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;
3567 $b->tags = $selectors;
3568
3569 $b->props = array();
3570 $b->children = array();
3571
3572 $this->env = $b;
3573 return $b;
3574 }
3575
3576
3577 protected function pushSpecialBlock($type) {
3578 return $this->pushBlock(null, $type);
3579 }
3580
3581
3582 protected function append($prop, $pos = null) {
3583 if ($pos !== null) $prop[-1] = $pos;
3584 $this->env->props[] = $prop;
3585 }
3586
3587
3588 protected function pop() {
3589 $old = $this->env;
3590 $this->env = $this->env->parent;
3591 return $old;
3592 }
3593
3594
3595
3596 protected function ($text) {
3597 $look = array(
3598 'url(', '//', '/*', '"', "'"
3599 );
3600
3601 $out = '';
3602 $min = null;
3603 while (true) {
3604
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