1 <?php
2 3 4 5 6 7
8
9 defined('FOF_INCLUDED') or die;
10
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
28 class FOFLess
29 {
30 public static $VERSION = "v0.3.9";
31
32 protected static $TRUE = array("keyword", "true");
33
34 protected static $FALSE = array("keyword", "false");
35
36 protected $libFunctions = array();
37
38 protected $registeredVars = array();
39
40 protected = false;
41
42 43 44 45 46
47 public $vPrefix = '@';
48
49 50 51 52 53
54 public $mPrefix = '$';
55
56 public $parentSelector = '&';
57
58 public $importDisabled = false;
59
60 public $importDir = '';
61
62 protected $numberPrecision = null;
63
64 65 66 67 68 69
70 protected $sourceParser = null;
71
72 protected $sourceLoc = null;
73
74 public static $defaultValue = array("keyword", "");
75
76 77 78 79 80
81 protected static $nextImportId = 0;
82
83 84 85 86 87 88 89
90 protected function findImport($url)
91 {
92 foreach ((array) $this->importDir as $dir)
93 {
94 $full = $dir . (substr($dir, -1) != '/' ? '/' : '') . $url;
95
96 if ($this->fileExists($file = $full . '.less') || $this->fileExists($file = $full))
97 {
98 return $file;
99 }
100 }
101
102 return null;
103 }
104
105 106 107 108 109 110 111
112 protected function fileExists($name)
113 {
114
115 return FOFPlatform::getInstance()->getIntegrationObject('filesystem')->fileExists($name);
116
117 }
118
119 120 121 122 123 124 125 126
127 public static function compressList($items, $delim)
128 {
129 if (!isset($items[1]) && isset($items[0]))
130 {
131 return $items[0];
132 }
133 else
134 {
135 return array('list', $delim, $items);
136 }
137 }
138
139 140 141 142 143 144 145
146 public static function preg_quote($what)
147 {
148 return preg_quote($what, '/');
149 }
150
151 152 153 154 155 156 157 158 159
160 protected function tryImport($importPath, $parentBlock, $out)
161 {
162 if ($importPath[0] == "function" && $importPath[1] == "url")
163 {
164 $importPath = $this->flattenList($importPath[2]);
165 }
166
167 $str = $this->coerceString($importPath);
168
169 if ($str === null)
170 {
171 return false;
172 }
173
174 $url = $this->compileValue($this->lib_e($str));
175
176
177 if (substr_compare($url, '.css', -4, 4) === 0)
178 {
179 return false;
180 }
181
182 $realPath = $this->findImport($url);
183
184 if ($realPath === null)
185 {
186 return false;
187 }
188
189 if ($this->importDisabled)
190 {
191 return array(false, "/* import disabled */");
192 }
193
194 $this->addParsedFile($realPath);
195 $parser = $this->makeParser($realPath);
196 $root = $parser->parse(file_get_contents($realPath));
197
198
199 foreach ($root->props as $prop)
200 {
201 if ($prop[0] == "block")
202 {
203 $prop[1]->parent = $parentBlock;
204 }
205 }
206
207 208 209 210 211
212 foreach ($root->children as $childName => $child)
213 {
214 if (isset($parentBlock->children[$childName]))
215 {
216 $parentBlock->children[$childName] = array_merge(
217 $parentBlock->children[$childName], $child
218 );
219 }
220 else
221 {
222 $parentBlock->children[$childName] = $child;
223 }
224 }
225
226 $pi = pathinfo($realPath);
227 $dir = $pi["dirname"];
228
229 list($top, $bottom) = $this->sortProps($root->props, true);
230 $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir);
231
232 return array(true, $bottom, $parser, $dir);
233 }
234
235 236 237 238 239 240 241 242 243 244 245
246 protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir)
247 {
248 $oldSourceParser = $this->sourceParser;
249
250 $oldImport = $this->importDir;
251
252
253 $this->importDir = (array) $this->importDir;
254 array_unshift($this->importDir, $importDir);
255
256 foreach ($props as $prop)
257 {
258 $this->compileProp($prop, $block, $out);
259 }
260
261 $this->importDir = $oldImport;
262 $this->sourceParser = $oldSourceParser;
263 }
264
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
289 protected function compileBlock($block)
290 {
291 switch ($block->type)
292 {
293 case "root":
294 $this->compileRoot($block);
295 break;
296 case null:
297 $this->compileCSSBlock($block);
298 break;
299 case "media":
300 $this->compileMedia($block);
301 break;
302 case "directive":
303 $name = "@" . $block->name;
304
305 if (!empty($block->value))
306 {
307 $name .= " " . $this->compileValue($this->reduce($block->value));
308 }
309
310 $this->compileNestedBlock($block, array($name));
311 break;
312 default:
313 $this->throwError("unknown block type: $block->type\n");
314 }
315 }
316
317 318 319 320 321 322 323
324 protected function compileCSSBlock($block)
325 {
326 $env = $this->pushEnv();
327
328 $selectors = $this->compileSelectors($block->tags);
329 $env->selectors = $this->multiplySelectors($selectors);
330 $out = $this->makeOutputBlock(null, $env->selectors);
331
332 $this->scope->children[] = $out;
333 $this->compileProps($block, $out);
334
335
336 $block->scope = $env;
337 $this->popEnv();
338 }
339
340 341 342 343 344 345 346
347 protected function compileMedia($media)
348 {
349 $env = $this->pushEnv($media);
350 $parentScope = $this->mediaParent($this->scope);
351
352 $query = $this->compileMediaQuery($this->multiplyMedia($env));
353
354 $this->scope = $this->makeOutputBlock($media->type, array($query));
355 $parentScope->children[] = $this->scope;
356
357 $this->compileProps($media, $this->scope);
358
359 if (count($this->scope->lines) > 0)
360 {
361 $orphanSelelectors = $this->findClosestSelectors();
362
363 if (!is_null($orphanSelelectors))
364 {
365 $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
366 $orphan->lines = $this->scope->lines;
367 array_unshift($this->scope->children, $orphan);
368 $this->scope->lines = array();
369 }
370 }
371
372 $this->scope = $this->scope->parent;
373 $this->popEnv();
374 }
375
376 377 378 379 380 381 382
383 protected function mediaParent($scope)
384 {
385 while (!empty($scope->parent))
386 {
387 if (!empty($scope->type) && $scope->type != "media")
388 {
389 break;
390 }
391
392 $scope = $scope->parent;
393 }
394
395 return $scope;
396 }
397
398 399 400 401 402 403 404 405
406 protected function compileNestedBlock($block, $selectors)
407 {
408 $this->pushEnv($block);
409 $this->scope = $this->makeOutputBlock($block->type, $selectors);
410 $this->scope->parent->children[] = $this->scope;
411
412 $this->compileProps($block, $this->scope);
413
414 $this->scope = $this->scope->parent;
415 $this->popEnv();
416 }
417
418 419 420 421 422 423 424
425 protected function compileRoot($root)
426 {
427 $this->pushEnv();
428 $this->scope = $this->makeOutputBlock($root->type);
429 $this->compileProps($root, $this->scope);
430 $this->popEnv();
431 }
432
433 434 435 436 437 438 439 440
441 protected function compileProps($block, $out)
442 {
443 foreach ($this->sortProps($block->props) as $prop)
444 {
445 $this->compileProp($prop, $block, $out);
446 }
447 }
448
449 450 451 452 453 454 455 456
457 protected function sortProps($props, $split = false)
458 {
459 $vars = array();
460 $imports = array();
461 $other = array();
462
463 foreach ($props as $prop)
464 {
465 switch ($prop[0])
466 {
467 case "assign":
468 if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix)
469 {
470 $vars[] = $prop;
471 }
472 else
473 {
474 $other[] = $prop;
475 }
476 break;
477 case "import":
478 $id = self::$nextImportId++;
479 $prop[] = $id;
480 $imports[] = $prop;
481 $other[] = array("import_mixin", $id);
482 break;
483 default:
484 $other[] = $prop;
485 }
486 }
487
488 if ($split)
489 {
490 return array(array_merge($vars, $imports), $other);
491 }
492 else
493 {
494 return array_merge($vars, $imports, $other);
495 }
496 }
497
498 499 500 501 502 503 504
505 protected function compileMediaQuery($queries)
506 {
507 $compiledQueries = array();
508
509 foreach ($queries as $query)
510 {
511 $parts = array();
512
513 foreach ($query as $q)
514 {
515 switch ($q[0])
516 {
517 case "mediaType":
518 $parts[] = implode(" ", array_slice($q, 1));
519 break;
520 case "mediaExp":
521 if (isset($q[2]))
522 {
523 $parts[] = "($q[1]: " .
524 $this->compileValue($this->reduce($q[2])) . ")";
525 }
526 else
527 {
528 $parts[] = "($q[1])";
529 }
530 break;
531 case "variable":
532 $parts[] = $this->compileValue($this->reduce($q));
533 break;
534 }
535 }
536
537 if (count($parts) > 0)
538 {
539 $compiledQueries[] = implode(" and ", $parts);
540 }
541 }
542
543 $out = "@media";
544
545 if (!empty($parts))
546 {
547 $out .= " " .
548 implode($this->formatter->selectorSeparator, $compiledQueries);
549 }
550
551 return $out;
552 }
553
554 555 556 557 558 559 560 561
562 protected function multiplyMedia($env, $childQueries = null)
563 {
564 if (is_null($env)
565 || !empty($env->block->type)
566 && $env->block->type != "media")
567 {
568 return $childQueries;
569 }
570
571
572 if (empty($env->block->type))
573 {
574 return $this->multiplyMedia($env->parent, $childQueries);
575 }
576
577 $out = array();
578 $queries = $env->block->queries;
579
580 if (is_null($childQueries))
581 {
582 $out = $queries;
583 }
584 else
585 {
586 foreach ($queries as $parent)
587 {
588 foreach ($childQueries as $child)
589 {
590 $out[] = array_merge($parent, $child);
591 }
592 }
593 }
594
595 return $this->multiplyMedia($env->parent, $out);
596 }
597
598 599 600 601 602 603 604 605
606 protected function expandParentSelectors(&$tag, $replace)
607 {
608 $parts = explode("$&$", $tag);
609 $count = 0;
610
611 foreach ($parts as &$part)
612 {
613 $part = str_replace($this->parentSelector, $replace, $part, $c);
614 $count += $c;
615 }
616
617 $tag = implode($this->parentSelector, $parts);
618
619 return $count;
620 }
621
622 623 624 625 626
627 protected function findClosestSelectors()
628 {
629 $env = $this->env;
630 $selectors = null;
631
632 while ($env !== null)
633 {
634 if (isset($env->selectors))
635 {
636 $selectors = $env->selectors;
637 break;
638 }
639
640 $env = $env->parent;
641 }
642
643 return $selectors;
644 }
645
646 647 648 649 650 651 652
653 protected function multiplySelectors($selectors)
654 {
655
656
657 $parentSelectors = $this->findClosestSelectors();
658
659 if (is_null($parentSelectors))
660 {
661
662 foreach ($selectors as &$s)
663 {
664 $this->expandParentSelectors($s, "");
665 }
666
667 return $selectors;
668 }
669
670 $out = array();
671
672 foreach ($parentSelectors as $parent)
673 {
674 foreach ($selectors as $child)
675 {
676 $count = $this->expandParentSelectors($child, $parent);
677
678
679 if ($count > 0)
680 {
681 $out[] = trim($child);
682 }
683 else
684 {
685 $out[] = trim($parent . ' ' . $child);
686 }
687 }
688 }
689
690 return $out;
691 }
692
693 694 695 696 697 698 699
700 protected function compileSelectors($selectors)
701 {
702 $out = array();
703
704 foreach ($selectors as $s)
705 {
706 if (is_array($s))
707 {
708 list(, $value) = $s;
709 $out[] = trim($this->compileValue($this->reduce($value)));
710 }
711 else
712 {
713 $out[] = $s;
714 }
715 }
716
717 return $out;
718 }
719
720 721 722 723 724 725 726 727
728 protected function eq($left, $right)
729 {
730 return $left == $right;
731 }
732
733 734 735 736 737 738 739 740
741 protected function patternMatch($block, $callingArgs)
742 {
743 744 745 746
747 if (!empty($block->guards))
748 {
749 $groupPassed = false;
750
751 foreach ($block->guards as $guardGroup)
752 {
753 foreach ($guardGroup as $guard)
754 {
755 $this->pushEnv();
756 $this->zipSetArgs($block->args, $callingArgs);
757
758 $negate = false;
759
760 if ($guard[0] == "negate")
761 {
762 $guard = $guard[1];
763 $negate = true;
764 }
765
766 $passed = $this->reduce($guard) == self::$TRUE;
767
768 if ($negate)
769 {
770 $passed = !$passed;
771 }
772
773 $this->popEnv();
774
775 if ($passed)
776 {
777 $groupPassed = true;
778 }
779 else
780 {
781 $groupPassed = false;
782 break;
783 }
784 }
785
786 if ($groupPassed)
787 {
788 break;
789 }
790 }
791
792 if (!$groupPassed)
793 {
794 return false;
795 }
796 }
797
798 $numCalling = count($callingArgs);
799
800 if (empty($block->args))
801 {
802 return $block->isVararg || $numCalling == 0;
803 }
804
805
806 $i = -1;
807
808
809 foreach ($block->args as $i => $arg)
810 {
811 switch ($arg[0])
812 {
813 case "lit":
814 if (empty($callingArgs[$i]) || !$this->eq($arg[1], $callingArgs[$i]))
815 {
816 return false;
817 }
818 break;
819 case "arg":
820
821 if (!isset($callingArgs[$i]) && !isset($arg[2]))
822 {
823 return false;
824 }
825 break;
826 case "rest":
827
828 $i--;
829 break 2;
830 }
831 }
832
833 if ($block->isVararg)
834 {
835
836 return true;
837 }
838 else
839 {
840 $numMatched = $i + 1;
841
842
843 return $numMatched >= $numCalling;
844 }
845 }
846
847 848 849 850 851 852 853 854
855 protected function patternMatchAll($blocks, $callingArgs)
856 {
857 $matches = null;
858
859 foreach ($blocks as $block)
860 {
861 if ($this->patternMatch($block, $callingArgs))
862 {
863 $matches[] = $block;
864 }
865 }
866
867 return $matches;
868 }
869
870 871 872 873 874 875 876 877 878 879
880 protected function findBlocks($searchIn, $path, $args, $seen = array())
881 {
882 if ($searchIn == null)
883 {
884 return null;
885 }
886
887 if (isset($seen[$searchIn->id]))
888 {
889 return null;
890 }
891
892 $seen[$searchIn->id] = true;
893
894 $name = $path[0];
895
896 if (isset($searchIn->children[$name]))
897 {
898 $blocks = $searchIn->children[$name];
899
900 if (count($path) == 1)
901 {
902 $matches = $this->patternMatchAll($blocks, $args);
903
904 if (!empty($matches))
905 {
906
907
908 return $matches;
909 }
910 }
911 else
912 {
913 $matches = array();
914
915 foreach ($blocks as $subBlock)
916 {
917 $subMatches = $this->findBlocks($subBlock, array_slice($path, 1), $args, $seen);
918
919 if (!is_null($subMatches))
920 {
921 foreach ($subMatches as $sm)
922 {
923 $matches[] = $sm;
924 }
925 }
926 }
927
928 return count($matches) > 0 ? $matches : null;
929 }
930 }
931
932 if ($searchIn->parent === $searchIn)
933 {
934 return null;
935 }
936
937 return $this->findBlocks($searchIn->parent, $path, $args, $seen);
938 }
939
940 941 942 943 944 945 946 947 948
949 protected function zipSetArgs($args, $values)
950 {
951 $i = 0;
952 $assignedValues = array();
953
954 foreach ($args as $a)
955 {
956 if ($a[0] == "arg")
957 {
958 if ($i < count($values) && !is_null($values[$i]))
959 {
960 $value = $values[$i];
961 }
962 elseif (isset($a[2]))
963 {
964 $value = $a[2];
965 }
966 else
967 {
968 $value = null;
969 }
970
971 $value = $this->reduce($value);
972 $this->set($a[1], $value);
973 $assignedValues[] = $value;
974 }
975
976 $i++;
977 }
978
979
980 $last = end($args);
981
982 if ($last[0] == "rest")
983 {
984 $rest = array_slice($values, count($args) - 1);
985 $this->set($last[1], $this->reduce(array("list", " ", $rest)));
986 }
987
988 $this->env->arguments = $assignedValues;
989 }
990
991 992 993 994 995 996 997 998 999
1000 protected function compileProp($prop, $block, $out)
1001 {
1002
1003 $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
1004
1005 switch ($prop[0])
1006 {
1007 case 'assign':
1008 list(, $name, $value) = $prop;
1009
1010 if ($name[0] == $this->vPrefix)
1011 {
1012 $this->set($name, $value);
1013 }
1014 else
1015 {
1016 $out->lines[] = $this->formatter->property($name, $this->compileValue($this->reduce($value)));
1017 }
1018 break;
1019 case 'block':
1020 list(, $child) = $prop;
1021 $this->compileBlock($child);
1022 break;
1023 case 'mixin':
1024 list(, $path, $args, $suffix) = $prop;
1025
1026 $args = array_map(array($this, "reduce"), (array) $args);
1027 $mixins = $this->findBlocks($block, $path, $args);
1028
1029 if ($mixins === null)
1030 {
1031
1032 break;
1033 }
1034
1035 foreach ($mixins as $mixin)
1036 {
1037 $haveScope = false;
1038
1039 if (isset($mixin->parent->scope))
1040 {
1041 $haveScope = true;
1042 $mixinParentEnv = $this->pushEnv();
1043 $mixinParentEnv->storeParent = $mixin->parent->scope;
1044 }
1045
1046 $haveArgs = false;
1047
1048 if (isset($mixin->args))
1049 {
1050 $haveArgs = true;
1051 $this->pushEnv();
1052 $this->zipSetArgs($mixin->args, $args);
1053 }
1054
1055 $oldParent = $mixin->parent;
1056
1057 if ($mixin != $block)
1058 {
1059 $mixin->parent = $block;
1060 }
1061
1062 foreach ($this->sortProps($mixin->props) as $subProp)
1063 {
1064 if ($suffix !== null
1065 && $subProp[0] == "assign"
1066 && is_string($subProp[1])
1067 && $subProp[1]{0} != $this->vPrefix)
1068 {
1069 $subProp[2] = array(
1070 'list', ' ',
1071 array($subProp[2], array('keyword', $suffix))
1072 );
1073 }
1074
1075 $this->compileProp($subProp, $mixin, $out);
1076 }
1077
1078 $mixin->parent = $oldParent;
1079
1080 if ($haveArgs)
1081 {
1082 $this->popEnv();
1083 }
1084
1085 if ($haveScope)
1086 {
1087 $this->popEnv();
1088 }
1089 }
1090
1091 break;
1092 case 'raw':
1093 $out->lines[] = $prop[1];
1094 break;
1095 case "directive":
1096 list(, $name, $value) = $prop;
1097 $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)) . ';';
1098 break;
1099 case "comment":
1100 $out->lines[] = $prop[1];
1101 break;
1102 case "import";
1103 list(, $importPath, $importId) = $prop;
1104 $importPath = $this->reduce($importPath);
1105
1106 if (!isset($this->env->imports))
1107 {
1108 $this->env->imports = array();
1109 }
1110
1111 $result = $this->tryImport($importPath, $block, $out);
1112
1113 $this->env->imports[$importId] = $result === false ?
1114 array(false, "@import " . $this->compileValue($importPath) . ";") :
1115 $result;
1116
1117 break;
1118 case "import_mixin":
1119 list(, $importId) = $prop;
1120 $import = $this->env->imports[$importId];
1121
1122 if ($import[0] === false)
1123 {
1124 $out->lines[] = $import[1];
1125 }
1126 else
1127 {
1128 list(, $bottom, $parser, $importDir) = $import;
1129 $this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
1130 }
1131
1132 break;
1133 default:
1134 $this->throwError("unknown op: {$prop[0]}\n");
1135 }
1136 }
1137
1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152
1153 protected function compileValue($value)
1154 {
1155 switch ($value[0])
1156 {
1157 case 'list':
1158
1159
1160 return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
1161 case 'raw_color':
1162 if (!empty($this->formatter->compressColors))
1163 {
1164 return $this->compileValue($this->coerceColor($value));
1165 }
1166
1167 return $value[1];
1168 case 'keyword':
1169
1170 return $value[1];
1171 case 'number':
1172
1173 list(, $num, $unit) = $value;
1174
1175 if ($this->numberPrecision !== null)
1176 {
1177 $num = round($num, $this->numberPrecision);
1178 }
1179
1180 return $num . $unit;
1181 case 'string':
1182
1183 list(, $delim, $content) = $value;
1184
1185 foreach ($content as &$part)
1186 {
1187 if (is_array($part))
1188 {
1189 $part = $this->compileValue($part);
1190 }
1191 }
1192
1193 return $delim . implode($content) . $delim;
1194 case 'color':
1195 1196 1197 1198 1199 1200 1201 1202
1203 list(, $r, $g, $b) = $value;
1204 $r = round($r);
1205 $g = round($g);
1206 $b = round($b);
1207
1208 if (count($value) == 5 && $value[4] != 1)
1209 {
1210
1211 return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $value[4] . ')';
1212 }
1213
1214 $h = sprintf("#%02x%02x%02x", $r, $g, $b);
1215
1216 if (!empty($this->formatter->compressColors))
1217 {
1218
1219 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6])
1220 {
1221 $h = '#' . $h[1] . $h[3] . $h[5];
1222 }
1223 }
1224
1225 return $h;
1226
1227 case 'function':
1228 list(, $name, $args) = $value;
1229
1230 return $name . '(' . $this->compileValue($args) . ')';
1231
1232 default:
1233
1234 $this->throwError("unknown value type: $value[0]");
1235 }
1236 }
1237
1238 1239 1240 1241 1242 1243 1244
1245 protected function lib_isnumber($value)
1246 {
1247 return $this->toBool($value[0] == "number");
1248 }
1249
1250 1251 1252 1253 1254 1255 1256
1257 protected function lib_isstring($value)
1258 {
1259 return $this->toBool($value[0] == "string");
1260 }
1261
1262 1263 1264 1265 1266 1267 1268
1269 protected function lib_iscolor($value)
1270 {
1271 return $this->toBool($this->coerceColor($value));
1272 }
1273
1274 1275 1276 1277 1278 1279 1280
1281 protected function lib_iskeyword($value)
1282 {
1283 return $this->toBool($value[0] == "keyword");
1284 }
1285
1286 1287 1288 1289 1290 1291 1292
1293 protected function lib_ispixel($value)
1294 {
1295 return $this->toBool($value[0] == "number" && $value[2] == "px");
1296 }
1297
1298 1299 1300 1301 1302 1303 1304
1305 protected function lib_ispercentage($value)
1306 {
1307 return $this->toBool($value[0] == "number" && $value[2] == "%");
1308 }
1309
1310 1311 1312 1313 1314 1315 1316
1317 protected function lib_isem($value)
1318 {
1319 return $this->toBool($value[0] == "number" && $value[2] == "em");
1320 }
1321
1322 1323 1324 1325 1326 1327 1328
1329 protected function lib_isrem($value)
1330 {
1331 return $this->toBool($value[0] == "number" && $value[2] == "rem");
1332 }
1333
1334 1335 1336 1337 1338 1339 1340
1341 protected function lib_rgbahex($color)
1342 {
1343 $color = $this->coerceColor($color);
1344
1345 if (is_null($color))
1346 {
1347 $this->throwError("color expected for rgbahex");
1348 }
1349
1350 return sprintf("#%02x%02x%02x%02x", isset($color[4]) ? $color[4] * 255 : 255, $color[1], $color[2], $color[3]);
1351 }
1352
1353 1354 1355 1356 1357 1358 1359
1360 protected function lib_argb($color)
1361 {
1362 return $this->lib_rgbahex($color);
1363 }
1364
1365 1366 1367 1368 1369 1370 1371
1372 protected function lib_e($arg)
1373 {
1374 switch ($arg[0])
1375 {
1376 case "list":
1377 $items = $arg[2];
1378
1379 if (isset($items[0]))
1380 {
1381 return $this->lib_e($items[0]);
1382 }
1383
1384 return self::$defaultValue;
1385
1386 case "string":
1387 $arg[1] = "";
1388
1389 return $arg;
1390
1391 case "keyword":
1392 return $arg;
1393
1394 default:
1395 return array("keyword", $this->compileValue($arg));
1396 }
1397 }
1398
1399 1400 1401 1402 1403 1404 1405
1406 protected function lib__sprintf($args)
1407 {
1408 if ($args[0] != "list")
1409 {
1410 return $args;
1411 }
1412
1413 $values = $args[2];
1414 $string = array_shift($values);
1415 $template = $this->compileValue($this->lib_e($string));
1416
1417 $i = 0;
1418
1419 if (preg_match_all('/%[dsa]/', $template, $m))
1420 {
1421 foreach ($m[0] as $match)
1422 {
1423 $val = isset($values[$i]) ?
1424 $this->reduce($values[$i]) : array('keyword', '');
1425
1426
1427 if ($color = $this->coerceColor($val))
1428 {
1429 $val = $color;
1430 }
1431
1432 $i++;
1433 $rep = $this->compileValue($this->lib_e($val));
1434 $template = preg_replace('/' . self::preg_quote($match) . '/', $rep, $template, 1);
1435 }
1436 }
1437
1438 $d = $string[0] == "string" ? $string[1] : '"';
1439
1440 return array("string", $d, array($template));
1441 }
1442
1443 1444 1445 1446 1447 1448 1449
1450 protected function lib_floor($arg)
1451 {
1452 $value = $this->assertNumber($arg);
1453
1454 return array("number", floor($value), $arg[2]);
1455 }
1456
1457 1458 1459 1460 1461 1462 1463
1464 protected function lib_ceil($arg)
1465 {
1466 $value = $this->assertNumber($arg);
1467
1468 return array("number", ceil($value), $arg[2]);
1469 }
1470
1471 1472 1473 1474 1475 1476 1477
1478 protected function lib_round($arg)
1479 {
1480 $value = $this->assertNumber($arg);
1481
1482 return array("number", round($value), $arg[2]);
1483 }
1484
1485 1486 1487 1488 1489 1490 1491
1492 protected function lib_unit($arg)
1493 {
1494 if ($arg[0] == "list")
1495 {
1496 list($number, $newUnit) = $arg[2];
1497 return array("number", $this->assertNumber($number), $this->compileValue($this->lib_e($newUnit)));
1498 }
1499 else
1500 {
1501 return array("number", $this->assertNumber($arg), "");
1502 }
1503 }
1504
1505 1506 1507 1508 1509 1510 1511 1512
1513 protected function colorArgs($args)
1514 {
1515 if ($args[0] != 'list' || count($args[2]) < 2)
1516 {
1517 return array(array('color', 0, 0, 0), 0);
1518 }
1519
1520 list($color, $delta) = $args[2];
1521 $color = $this->assertColor($color);
1522 $delta = floatval($delta[1]);
1523
1524 return array($color, $delta);
1525 }
1526
1527 1528 1529 1530 1531 1532 1533
1534 protected function lib_darken($args)
1535 {
1536 list($color, $delta) = $this->colorArgs($args);
1537
1538 $hsl = $this->toHSL($color);
1539 $hsl[3] = $this->clamp($hsl[3] - $delta, 100);
1540
1541 return $this->toRGB($hsl);
1542 }
1543
1544 1545 1546 1547 1548 1549 1550
1551 protected function lib_lighten($args)
1552 {
1553 list($color, $delta) = $this->colorArgs($args);
1554
1555 $hsl = $this->toHSL($color);
1556 $hsl[3] = $this->clamp($hsl[3] + $delta, 100);
1557
1558 return $this->toRGB($hsl);
1559 }
1560
1561 1562 1563 1564 1565 1566 1567
1568 protected function lib_saturate($args)
1569 {
1570 list($color, $delta) = $this->colorArgs($args);
1571
1572 $hsl = $this->toHSL($color);
1573 $hsl[2] = $this->clamp($hsl[2] + $delta, 100);
1574
1575 return $this->toRGB($hsl);
1576 }
1577
1578 1579 1580 1581 1582 1583 1584
1585 protected function lib_desaturate($args)
1586 {
1587 list($color, $delta) = $this->colorArgs($args);
1588
1589 $hsl = $this->toHSL($color);
1590 $hsl[2] = $this->clamp($hsl[2] - $delta, 100);
1591
1592 return $this->toRGB($hsl);
1593 }
1594
1595 1596 1597 1598 1599 1600 1601
1602 protected function lib_spin($args)
1603 {
1604 list($color, $delta) = $this->colorArgs($args);
1605
1606 $hsl = $this->toHSL($color);
1607
1608 $hsl[1] = $hsl[1] + $delta % 360;
1609
1610 if ($hsl[1] < 0)
1611 {
1612 $hsl[1] += 360;
1613 }
1614
1615 return $this->toRGB($hsl);
1616 }
1617
1618 1619 1620 1621 1622 1623 1624
1625 protected function lib_fadeout($args)
1626 {
1627 list($color, $delta) = $this->colorArgs($args);
1628 $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta / 100);
1629
1630 return $color;
1631 }
1632
1633 1634 1635 1636 1637 1638 1639
1640 protected function lib_fadein($args)
1641 {
1642 list($color, $delta) = $this->colorArgs($args);
1643 $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta / 100);
1644
1645 return $color;
1646 }
1647
1648 1649 1650 1651 1652 1653 1654
1655 protected function lib_hue($color)
1656 {
1657 $hsl = $this->toHSL($this->assertColor($color));
1658
1659 return round($hsl[1]);
1660 }
1661
1662 1663 1664 1665 1666 1667 1668
1669 protected function lib_saturation($color)
1670 {
1671 $hsl = $this->toHSL($this->assertColor($color));
1672
1673 return round($hsl[2]);
1674 }
1675
1676 1677 1678 1679 1680 1681 1682
1683 protected function lib_lightness($color)
1684 {
1685 $hsl = $this->toHSL($this->assertColor($color));
1686
1687 return round($hsl[3]);
1688 }
1689
1690 1691 1692 1693 1694 1695 1696 1697
1698 protected function lib_alpha($value)
1699 {
1700 if (!is_null($color = $this->coerceColor($value)))
1701 {
1702 return isset($color[4]) ? $color[4] : 1;
1703 }
1704 }
1705
1706 1707 1708 1709 1710 1711 1712
1713 protected function lib_fade($args)
1714 {
1715 list($color, $alpha) = $this->colorArgs($args);
1716 $color[4] = $this->clamp($alpha / 100.0);
1717
1718 return $color;
1719 }
1720
1721 1722 1723 1724 1725 1726 1727
1728 protected function lib_percentage($arg)
1729 {
1730 $num = $this->assertNumber($arg);
1731
1732 return array("number", $num * 100, "%");
1733 }
1734
1735 1736 1737 1738 1739 1740 1741 1742 1743
1744 protected function lib_mix($args)
1745 {
1746 if ($args[0] != "list" || count($args[2]) < 3)
1747 {
1748 $this->throwError("mix expects (color1, color2, weight)");
1749 }
1750
1751 list($first, $second, $weight) = $args[2];
1752 $first = $this->assertColor($first);
1753 $second = $this->assertColor($second);
1754
1755 $first_a = $this->lib_alpha($first);
1756 $second_a = $this->lib_alpha($second);
1757 $weight = $weight[1] / 100.0;
1758
1759 $w = $weight * 2 - 1;
1760 $a = $first_a - $second_a;
1761
1762 $w1 = (($w * $a == -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
1763 $w2 = 1.0 - $w1;
1764
1765 $new = array('color',
1766 $w1 * $first[1] + $w2 * $second[1],
1767 $w1 * $first[2] + $w2 * $second[2],
1768 $w1 * $first[3] + $w2 * $second[3],
1769 );
1770
1771 if ($first_a != 1.0 || $second_a != 1.0)
1772 {
1773 $new[] = $first_a * $weight + $second_a * ($weight - 1);
1774 }
1775
1776 return $this->fixColor($new);
1777 }
1778
1779 1780 1781 1782 1783 1784 1785
1786 protected function lib_contrast($args)
1787 {
1788 if ($args[0] != 'list' || count($args[2]) < 3)
1789 {
1790 return array(array('color', 0, 0, 0), 0);
1791 }
1792
1793 list($inputColor, $darkColor, $lightColor) = $args[2];
1794
1795 $inputColor = $this->assertColor($inputColor);
1796 $darkColor = $this->assertColor($darkColor);
1797 $lightColor = $this->assertColor($lightColor);
1798 $hsl = $this->toHSL($inputColor);
1799
1800 if ($hsl[3] > 50)
1801 {
1802 return $darkColor;
1803 }
1804
1805 return $lightColor;
1806 }
1807
1808 1809 1810 1811 1812 1813 1814 1815
1816 protected function assertColor($value, $error = "expected color value")
1817 {
1818 $color = $this->coerceColor($value);
1819
1820 if (is_null($color))
1821 {
1822 $this->throwError($error);
1823 }
1824
1825 return $color;
1826 }
1827
1828 1829 1830 1831 1832 1833 1834 1835
1836 protected function assertNumber($value, $error = "expecting number")
1837 {
1838 if ($value[0] == "number")
1839 {
1840 return $value[1];
1841 }
1842
1843 $this->throwError($error);
1844 }
1845
1846 1847 1848 1849 1850 1851 1852
1853 protected function toHSL($color)
1854 {
1855 if ($color[0] == 'hsl')
1856 {
1857 return $color;
1858 }
1859
1860 $r = $color[1] / 255;
1861 $g = $color[2] / 255;
1862 $b = $color[3] / 255;
1863
1864 $min = min($r, $g, $b);
1865 $max = max($r, $g, $b);
1866
1867 $L = ($min + $max) / 2;
1868
1869 if ($min == $max)
1870 {
1871 $S = $H = 0;
1872 }
1873 else
1874 {
1875 if ($L < 0.5)
1876 {
1877 $S = ($max - $min) / ($max + $min);
1878 }
1879 else
1880 {
1881 $S = ($max - $min) / (2.0 - $max - $min);
1882 }
1883
1884 if ($r == $max)
1885 {
1886 $H = ($g - $b) / ($max - $min);
1887 }
1888 elseif ($g == $max)
1889 {
1890 $H = 2.0 + ($b - $r) / ($max - $min);
1891 }
1892 elseif ($b == $max)
1893 {
1894 $H = 4.0 + ($r - $g) / ($max - $min);
1895 }
1896 }
1897
1898 $out = array('hsl',
1899 ($H < 0 ? $H + 6 : $H) * 60,
1900 $S * 100,
1901 $L * 100,
1902 );
1903
1904 if (count($color) > 4)
1905 {
1906
1907 $out[] = $color[4];
1908 }
1909
1910 return $out;
1911 }
1912
1913 1914 1915 1916 1917 1918 1919 1920 1921
1922 protected function toRGB_helper($comp, $temp1, $temp2)
1923 {
1924 if ($comp < 0)
1925 {
1926 $comp += 1.0;
1927 }
1928 elseif ($comp > 1)
1929 {
1930 $comp -= 1.0;
1931 }
1932
1933 if (6 * $comp < 1)
1934 {
1935 return $temp1 + ($temp2 - $temp1) * 6 * $comp;
1936 }
1937
1938 if (2 * $comp < 1)
1939 {
1940 return $temp2;
1941 }
1942
1943 if (3 * $comp < 2)
1944 {
1945 return $temp1 + ($temp2 - $temp1) * ((2 / 3) - $comp) * 6;
1946 }
1947
1948 return $temp1;
1949 }
1950
1951 1952 1953 1954 1955 1956 1957 1958
1959 protected function toRGB($color)
1960 {
1961 if ($color == 'color')
1962 {
1963 return $color;
1964 }
1965
1966 $H = $color[1] / 360;
1967 $S = $color[2] / 100;
1968 $L = $color[3] / 100;
1969
1970 if ($S == 0)
1971 {
1972 $r = $g = $b = $L;
1973 }
1974 else
1975 {
1976 $temp2 = $L < 0.5 ?
1977 $L * (1.0 + $S) :
1978 $L + $S - $L * $S;
1979
1980 $temp1 = 2.0 * $L - $temp2;
1981
1982 $r = $this->toRGB_helper($H + 1 / 3, $temp1, $temp2);
1983 $g = $this->toRGB_helper($H, $temp1, $temp2);
1984 $b = $this->toRGB_helper($H - 1 / 3, $temp1, $temp2);
1985 }
1986
1987
1988 $out = array('color', $r * 255, $g * 255, $b * 255);
1989
1990 if (count($color) > 4)
1991 {
1992
1993 $out[] = $color[4];
1994 }
1995
1996 return $out;
1997 }
1998
1999 2000 2001 2002 2003 2004 2005 2006 2007
2008 protected function clamp($v, $max = 1, $min = 0)
2009 {
2010 return min($max, max($min, $v));
2011 }
2012
2013 2014 2015 2016 2017 2018 2019 2020
2021 protected function funcToColor($func)
2022 {
2023 $fname = $func[1];
2024
2025 if ($func[2][0] != 'list')
2026 {
2027
2028 return false;
2029 }
2030
2031 $rawComponents = $func[2][2];
2032
2033 if ($fname == 'hsl' || $fname == 'hsla')
2034 {
2035 $hsl = array('hsl');
2036 $i = 0;
2037
2038 foreach ($rawComponents as $c)
2039 {
2040 $val = $this->reduce($c);
2041 $val = isset($val[1]) ? floatval($val[1]) : 0;
2042
2043 if ($i == 0)
2044 {
2045 $clamp = 360;
2046 }
2047 elseif ($i < 3)
2048 {
2049 $clamp = 100;
2050 }
2051 else
2052 {
2053 $clamp = 1;
2054 }
2055
2056 $hsl[] = $this->clamp($val, $clamp);
2057 $i++;
2058 }
2059
2060 while (count($hsl) < 4)
2061 {
2062 $hsl[] = 0;
2063 }
2064
2065 return $this->toRGB($hsl);
2066 }
2067 elseif ($fname == 'rgb' || $fname == 'rgba')
2068 {
2069 $components = array();
2070 $i = 1;
2071
2072 foreach ($rawComponents as $c)
2073 {
2074 $c = $this->reduce($c);
2075
2076 if ($i < 4)
2077 {
2078 if ($c[0] == "number" && $c[2] == "%")
2079 {
2080 $components[] = 255 * ($c[1] / 100);
2081 }
2082 else
2083 {
2084 $components[] = floatval($c[1]);
2085 }
2086 }
2087 elseif ($i == 4)
2088 {
2089 if ($c[0] == "number" && $c[2] == "%")
2090 {
2091 $components[] = 1.0 * ($c[1] / 100);
2092 }
2093 else
2094 {
2095 $components[] = floatval($c[1]);
2096 }
2097 }
2098 else
2099 {
2100 break;
2101 }
2102
2103 $i++;
2104 }
2105
2106 while (count($components) < 3)
2107 {
2108 $components[] = 0;
2109 }
2110
2111 array_unshift($components, 'color');
2112
2113 return $this->fixColor($components);
2114 }
2115
2116 return false;
2117 }
2118
2119 2120 2121 2122 2123 2124 2125 2126
2127 protected function reduce($value, $forExpression = false)
2128 {
2129 switch ($value[0])
2130 {
2131 case "interpolate":
2132 $reduced = $this->reduce($value[1]);
2133 $var = $this->compileValue($reduced);
2134 $res = $this->reduce(array("variable", $this->vPrefix . $var));
2135
2136 if (empty($value[2]))
2137 {
2138 $res = $this->lib_e($res);
2139 }
2140
2141 return $res;
2142 case "variable":
2143 $key = $value[1];
2144 if (is_array($key))
2145 {
2146 $key = $this->reduce($key);
2147 $key = $this->vPrefix . $this->compileValue($this->lib_e($key));
2148 }
2149
2150 $seen = & $this->env->seenNames;
2151
2152 if (!empty($seen[$key]))
2153 {
2154 $this->throwError("infinite loop detected: $key");
2155 }
2156
2157 $seen[$key] = true;
2158 $out = $this->reduce($this->get($key, self::$defaultValue));
2159 $seen[$key] = false;
2160
2161 return $out;
2162 case "list":
2163 foreach ($value[2] as &$item)
2164 {
2165 $item = $this->reduce($item, $forExpression);
2166 }
2167
2168 return $value;
2169 case "expression":
2170 return $this->evaluate($value);
2171 case "string":
2172 foreach ($value[2] as &$part)
2173 {
2174 if (is_array($part))
2175 {
2176 $strip = $part[0] == "variable";
2177 $part = $this->reduce($part);
2178
2179 if ($strip)
2180 {
2181 $part = $this->lib_e($part);
2182 }
2183 }
2184 }
2185
2186 return $value;
2187 case "escape":
2188 list(, $inner) = $value;
2189
2190 return $this->lib_e($this->reduce($inner));
2191 case "function":
2192 $color = $this->funcToColor($value);
2193
2194 if ($color)
2195 {
2196 return $color;
2197 }
2198
2199 list(, $name, $args) = $value;
2200
2201 if ($name == "%")
2202 {
2203 $name = "_sprintf";
2204 }
2205
2206 $f = isset($this->libFunctions[$name]) ?
2207 $this->libFunctions[$name] : array($this, 'lib_' . $name);
2208
2209 if (is_callable($f))
2210 {
2211 if ($args[0] == 'list')
2212 {
2213 $args = self::compressList($args[2], $args[1]);
2214 }
2215
2216 $ret = call_user_func($f, $this->reduce($args, true), $this);
2217
2218 if (is_null($ret))
2219 {
2220 return array("string", "", array(
2221 $name, "(", $args, ")"
2222 ));
2223 }
2224
2225
2226 if (is_numeric($ret))
2227 {
2228 $ret = array('number', $ret, "");
2229 }
2230 elseif (!is_array($ret))
2231 {
2232 $ret = array('keyword', $ret);
2233 }
2234
2235 return $ret;
2236 }
2237
2238
2239 $value[2] = $this->reduce($value[2]);
2240
2241 return $value;
2242 case "unary":
2243 list(, $op, $exp) = $value;
2244 $exp = $this->reduce($exp);
2245
2246 if ($exp[0] == "number")
2247 {
2248 switch ($op)
2249 {
2250 case "+":
2251 return $exp;
2252 case "-":
2253 $exp[1] *= -1;
2254
2255 return $exp;
2256 }
2257 }
2258
2259 return array("string", "", array($op, $exp));
2260 }
2261
2262 if ($forExpression)
2263 {
2264 switch ($value[0])
2265 {
2266 case "keyword":
2267 if ($color = $this->coerceColor($value))
2268 {
2269 return $color;
2270 }
2271 break;
2272 case "raw_color":
2273 return $this->coerceColor($value);
2274 }
2275 }
2276
2277 return $value;
2278 }
2279
2280 2281 2282 2283 2284 2285 2286
2287 protected function coerceColor($value)
2288 {
2289 switch ($value[0])
2290 {
2291 case 'color':
2292 return $value;
2293 case 'raw_color':
2294 $c = array("color", 0, 0, 0);
2295 $colorStr = substr($value[1], 1);
2296 $num = hexdec($colorStr);
2297 $width = strlen($colorStr) == 3 ? 16 : 256;
2298
2299 for ($i = 3; $i > 0; $i--)
2300 {
2301
2302 $t = $num % $width;
2303 $num /= $width;
2304
2305 $c[$i] = $t * (256 / $width) + $t * floor(16 / $width);
2306 }
2307
2308 return $c;
2309 case 'keyword':
2310 $name = $value[1];
2311
2312 if (isset(self::$cssColors[$name]))
2313 {
2314 $rgba = explode(',', self::$cssColors[$name]);
2315
2316 if (isset($rgba[3]))
2317 {
2318 return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
2319 }
2320
2321 return array('color', $rgba[0], $rgba[1], $rgba[2]);
2322 }
2323
2324 return null;
2325 }
2326 }
2327
2328 2329 2330 2331 2332 2333 2334
2335 protected function coerceString($value)
2336 {
2337 switch ($value[0])
2338 {
2339 case "string":
2340 return $value;
2341 case "keyword":
2342 return array("string", "", array($value[1]));
2343 }
2344
2345 return null;
2346 }
2347
2348 2349 2350 2351 2352 2353 2354
2355 protected function flattenList($value)
2356 {
2357 if ($value[0] == "list" && count($value[2]) == 1)
2358 {
2359 return $this->flattenList($value[2][0]);
2360 }
2361
2362 return $value;
2363 }
2364
2365 2366 2367 2368 2369 2370 2371
2372 protected function toBool($a)
2373 {
2374 if ($a)
2375 {
2376 return self::$TRUE;
2377 }
2378 else
2379 {
2380 return self::$FALSE;
2381 }
2382 }
2383
2384 2385 2386 2387 2388 2389 2390
2391 protected function evaluate($exp)
2392 {
2393 list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
2394
2395 $left = $this->reduce($left, true);
2396 $right = $this->reduce($right, true);
2397
2398 if ($leftColor = $this->coerceColor($left))
2399 {
2400 $left = $leftColor;
2401 }
2402
2403 if ($rightColor = $this->coerceColor($right))
2404 {
2405 $right = $rightColor;
2406 }
2407
2408 $ltype = $left[0];
2409 $rtype = $right[0];
2410
2411
2412 if ($op == "and")
2413 {
2414 return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
2415 }
2416
2417 if ($op == "=")
2418 {
2419 return $this->toBool($this->eq($left, $right));
2420 }
2421
2422 if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right)))
2423 {
2424 return $str;
2425 }
2426
2427
2428 $fname = "op_${ltype}_${rtype}";
2429
2430 if (is_callable(array($this, $fname)))
2431 {
2432 $out = $this->$fname($op, $left, $right);
2433
2434 if (!is_null($out))
2435 {
2436 return $out;
2437 }
2438 }
2439
2440
2441 $paddedOp = $op;
2442
2443 if ($whiteBefore)
2444 {
2445 $paddedOp = " " . $paddedOp;
2446 }
2447
2448 if ($whiteAfter)
2449 {
2450 $paddedOp .= " ";
2451 }
2452
2453 return array("string", "", array($left, $paddedOp, $right));
2454 }
2455
2456 2457 2458 2459 2460 2461 2462 2463
2464 protected function stringConcatenate($left, $right)
2465 {
2466 if ($strLeft = $this->coerceString($left))
2467 {
2468 if ($right[0] == "string")
2469 {
2470 $right[1] = "";
2471 }
2472
2473 $strLeft[2][] = $right;
2474
2475 return $strLeft;
2476 }
2477
2478 if ($strRight = $this->coerceString($right))
2479 {
2480 array_unshift($strRight[2], $left);
2481
2482 return $strRight;
2483 }
2484 }
2485
2486 2487 2488 2489 2490 2491 2492
2493 protected function fixColor($c)
2494 {
2495 foreach (range(1, 3) as $i)
2496 {
2497 if ($c[$i] < 0)
2498 {
2499 $c[$i] = 0;
2500 }
2501
2502 if ($c[$i] > 255)
2503 {
2504 $c[$i] = 255;
2505 }
2506 }
2507
2508 return $c;
2509 }
2510
2511 2512 2513 2514 2515 2516 2517 2518 2519
2520 protected function op_number_color($op, $lft, $rgt)
2521 {
2522 if ($op == '+' || $op == '*')
2523 {
2524 return $this->op_color_number($op, $rgt, $lft);
2525 }
2526 }
2527
2528 2529 2530 2531 2532 2533 2534 2535 2536
2537 protected function op_color_number($op, $lft, $rgt)
2538 {
2539 if ($rgt[0] == '%')
2540 {
2541 $rgt[1] /= 100;
2542 }
2543
2544 return $this->op_color_color($op, $lft, array_fill(1, count($lft) - 1, $rgt[1]));
2545 }
2546
2547 2548 2549 2550 2551 2552 2553 2554 2555
2556 protected function op_color_color($op, $left, $right)
2557 {
2558 $out = array('color');
2559 $max = count($left) > count($right) ? count($left) : count($right);
2560
2561 foreach (range(1, $max - 1) as $i)
2562 {
2563 $lval = isset($left[$i]) ? $left[$i] : 0;
2564 $rval = isset($right[$i]) ? $right[$i] : 0;
2565
2566 switch ($op)
2567 {
2568 case '+':
2569 $out[] = $lval + $rval;
2570 break;
2571 case '-':
2572 $out[] = $lval - $rval;
2573 break;
2574 case '*':
2575 $out[] = $lval * $rval;
2576 break;
2577 case '%':
2578 $out[] = $lval % $rval;
2579 break;
2580 case '/':
2581 if ($rval == 0)
2582 {
2583 $this->throwError("evaluate error: can't divide by zero");
2584 }
2585
2586 $out[] = $lval / $rval;
2587 break;
2588 default:
2589 $this->throwError('evaluate error: color op number failed on op ' . $op);
2590 }
2591 }
2592
2593 return $this->fixColor($out);
2594 }
2595
2596 2597 2598 2599 2600 2601 2602
2603 public function lib_red($color)
2604 {
2605 $color = $this->coerceColor($color);
2606
2607 if (is_null($color))
2608 {
2609 $this->throwError('color expected for red()');
2610 }
2611
2612 return $color[1];
2613 }
2614
2615 2616 2617 2618 2619 2620 2621
2622 public function lib_green($color)
2623 {
2624 $color = $this->coerceColor($color);
2625
2626 if (is_null($color))
2627 {
2628 $this->throwError('color expected for green()');
2629 }
2630
2631 return $color[2];
2632 }
2633
2634 2635 2636 2637 2638 2639 2640
2641 public function lib_blue($color)
2642 {
2643 $color = $this->coerceColor($color);
2644
2645 if (is_null($color))
2646 {
2647 $this->throwError('color expected for blue()');
2648 }
2649
2650 return $color[3];
2651 }
2652
2653 2654 2655 2656 2657 2658 2659 2660 2661
2662 protected function op_number_number($op, $left, $right)
2663 {
2664 $unit = empty($left[2]) ? $right[2] : $left[2];
2665
2666 $value = 0;
2667
2668 switch ($op)
2669 {
2670 case '+':
2671 $value = $left[1] + $right[1];
2672 break;
2673 case '*':
2674 $value = $left[1] * $right[1];
2675 break;
2676 case '-':
2677 $value = $left[1] - $right[1];
2678 break;
2679 case '%':
2680 $value = $left[1] % $right[1];
2681 break;
2682 case '/':
2683 if ($right[1] == 0)
2684 {
2685 $this->throwError('parse error: divide by zero');
2686 }
2687
2688 $value = $left[1] / $right[1];
2689 break;
2690 case '<':
2691 return $this->toBool($left[1] < $right[1]);
2692 case '>':
2693 return $this->toBool($left[1] > $right[1]);
2694 case '>=':
2695 return $this->toBool($left[1] >= $right[1]);
2696 case '=<':
2697 return $this->toBool($left[1] <= $right[1]);
2698 default:
2699 $this->throwError('parse error: unknown number operator: ' . $op);
2700 }
2701
2702 return array("number", $value, $unit);
2703 }
2704
2705 2706 2707 2708 2709 2710 2711 2712
2713 protected function makeOutputBlock($type, $selectors = null)
2714 {
2715 $b = new stdclass;
2716 $b->lines = array();
2717 $b->children = array();
2718 $b->selectors = $selectors;
2719 $b->type = $type;
2720 $b->parent = $this->scope;
2721
2722 return $b;
2723 }
2724
2725 2726 2727 2728 2729 2730 2731
2732 protected function pushEnv($block = null)
2733 {
2734 $e = new stdclass;
2735 $e->parent = $this->env;
2736 $e->store = array();
2737 $e->block = $block;
2738
2739 $this->env = $e;
2740
2741 return $e;
2742 }
2743
2744 2745 2746 2747 2748
2749 protected function popEnv()
2750 {
2751 $old = $this->env;
2752 $this->env = $this->env->parent;
2753
2754 return $old;
2755 }
2756
2757 2758 2759 2760 2761 2762 2763 2764
2765 protected function set($name, $value)
2766 {
2767 $this->env->store[$name] = $value;
2768 }
2769
2770 2771 2772 2773 2774 2775 2776 2777
2778 protected function get($name, $default = null)
2779 {
2780 $current = $this->env;
2781
2782 $isArguments = $name == $this->vPrefix . 'arguments';
2783
2784 while ($current)
2785 {
2786 if ($isArguments && isset($current->arguments))
2787 {
2788 return array('list', ' ', $current->arguments);
2789 }
2790
2791 if (isset($current->store[$name]))
2792 {
2793 return $current->store[$name];
2794 }
2795 else
2796 {
2797 $current = isset($current->storeParent) ?
2798 $current->storeParent : $current->parent;
2799 }
2800 }
2801
2802 return $default;
2803 }
2804
2805 2806 2807 2808 2809 2810 2811 2812 2813
2814 protected function injectVariables($args)
2815 {
2816 $this->pushEnv();
2817
2818 $parser = new FOFLessParser($this, __METHOD__);
2819
2820 foreach ($args as $name => $strValue)
2821 {
2822 if ($name{0} != '@')
2823 {
2824 $name = '@' . $name;
2825 }
2826
2827 $parser->count = 0;
2828 $parser->buffer = (string) $strValue;
2829
2830 if (!$parser->propertyValue($value))
2831 {
2832 throw new Exception("failed to parse passed in variable $name: $strValue");
2833 }
2834
2835 $this->set($name, $value);
2836 }
2837 }
2838
2839 2840 2841 2842 2843
2844 public function __construct($fname = null)
2845 {
2846 if ($fname !== null)
2847 {
2848
2849 $this->_parseFile = $fname;
2850 }
2851 }
2852
2853 2854 2855 2856 2857 2858 2859 2860
2861 public function compile($string, $name = null)
2862 {
2863 $locale = setlocale(LC_NUMERIC, 0);
2864 setlocale(LC_NUMERIC, "C");
2865
2866 $this->parser = $this->makeParser($name);
2867 $root = $this->parser->parse($string);
2868
2869 $this->env = null;
2870 $this->scope = null;
2871
2872 $this->formatter = $this->newFormatter();
2873
2874 if (!empty($this->registeredVars))
2875 {
2876 $this->injectVariables($this->registeredVars);
2877 }
2878
2879
2880 $this->sourceParser = $this->parser;
2881 $this->compileBlock($root);
2882
2883 ob_start();
2884 $this->formatter->block($this->scope);
2885 $out = ob_get_clean();
2886 setlocale(LC_NUMERIC, $locale);
2887
2888 return $out;
2889 }
2890
2891 2892 2893 2894 2895 2896 2897 2898 2899 2900
2901 public function compileFile($fname, $outFname = null)
2902 {
2903 if (!is_readable($fname))
2904 {
2905 throw new Exception('load error: failed to find ' . $fname);
2906 }
2907
2908 $pi = pathinfo($fname);
2909
2910 $oldImport = $this->importDir;
2911
2912 $this->importDir = (array) $this->importDir;
2913 $this->importDir[] = $pi['dirname'] . '/';
2914
2915 $this->allParsedFiles = array();
2916 $this->addParsedFile($fname);
2917
2918 $out = $this->compile(file_get_contents($fname), $fname);
2919
2920 $this->importDir = $oldImport;
2921
2922 if ($outFname !== null)
2923 {
2924
2925 return FOFPlatform::getInstance()->getIntegrationObject('filesystem')->fileWrite($outFname, $out);
2926
2927 }
2928
2929 return $out;
2930 }
2931
2932 2933 2934 2935 2936 2937 2938 2939
2940 public function checkedCompile($in, $out)
2941 {
2942 if (!is_file($out) || filemtime($in) > filemtime($out))
2943 {
2944 $this->compileFile($in, $out);
2945
2946 return true;
2947 }
2948
2949 return false;
2950 }
2951
2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972
2973 public function cachedCompile($in, $force = false)
2974 {
2975
2976 $root = null;
2977
2978 if (is_string($in))
2979 {
2980 $root = $in;
2981 }
2982 elseif (is_array($in) and isset($in['root']))
2983 {
2984 if ($force or !isset($in['files']))
2985 {
2986 2987 2988 2989 2990
2991 $root = $in['root'];
2992 }
2993 elseif (isset($in['files']) and is_array($in['files']))
2994 {
2995 foreach ($in['files'] as $fname => $ftime)
2996 {
2997 if (!file_exists($fname) or filemtime($fname) > $ftime)
2998 {
2999 3000 3001 3002
3003 $root = $in['root'];
3004 break;
3005 }
3006 }
3007 }
3008 }
3009 else
3010 {
3011 3012 3013 3014
3015 return null;
3016 }
3017
3018 if ($root !== null)
3019 {
3020
3021 $out = array();
3022 $out['root'] = $root;
3023 $out['compiled'] = $this->compileFile($root);
3024 $out['files'] = $this->allParsedFiles();
3025 $out['updated'] = time();
3026
3027 return $out;
3028 }
3029 else
3030 {
3031
3032
3033 return $in;
3034 }
3035 }
3036
3037
3038
3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050
3051 public function parse($str = null, $initialVariables = null)
3052 {
3053 if (is_array($str))
3054 {
3055 $initialVariables = $str;
3056 $str = null;
3057 }
3058
3059 $oldVars = $this->registeredVars;
3060
3061 if ($initialVariables !== null)
3062 {
3063 $this->setVariables($initialVariables);
3064 }
3065
3066 if ($str == null)
3067 {
3068 if (empty($this->_parseFile))
3069 {
3070 throw new exception("nothing to parse");
3071 }
3072
3073 $out = $this->compileFile($this->_parseFile);
3074 }
3075 else
3076 {
3077 $out = $this->compile($str);
3078 }
3079
3080 $this->registeredVars = $oldVars;
3081
3082 return $out;
3083 }
3084
3085 3086 3087 3088 3089 3090 3091
3092 protected function makeParser($name)
3093 {
3094
3095 $parser = new FOFLessParser($this, $name);
3096
3097 $parser->writeComments = $this->preserveComments;
3098
3099 return $parser;
3100 }
3101
3102 3103 3104 3105 3106 3107 3108
3109 public function setFormatter($name)
3110 {
3111 $this->formatterName = $name;
3112 }
3113
3114 3115 3116 3117 3118
3119 protected function newFormatter()
3120 {
3121
3122 $className = "FOFLessFormatterLessjs";
3123
3124 if (!empty($this->formatterName))
3125 {
3126 if (!is_string($this->formatterName))
3127 return $this->formatterName;
3128
3129 $className = "FOFLessFormatter" . ucfirst($this->formatterName);
3130
3131 }
3132
3133 return new $className;
3134 }
3135
3136 3137 3138 3139 3140 3141 3142
3143 public function ($preserve)
3144 {
3145 $this->preserveComments = $preserve;
3146 }
3147
3148 3149 3150 3151 3152 3153 3154 3155
3156 public function registerFunction($name, $func)
3157 {
3158 $this->libFunctions[$name] = $func;
3159 }
3160
3161 3162 3163 3164 3165 3166 3167
3168 public function unregisterFunction($name)
3169 {
3170 unset($this->libFunctions[$name]);
3171 }
3172
3173 3174 3175 3176 3177 3178 3179
3180 public function setVariables($variables)
3181 {
3182 $this->registeredVars = array_merge($this->registeredVars, $variables);
3183 }
3184
3185 3186 3187 3188 3189 3190 3191
3192 public function unsetVariable($name)
3193 {
3194 unset($this->registeredVars[$name]);
3195 }
3196
3197 3198 3199 3200 3201 3202 3203
3204 public function setImportDir($dirs)
3205 {
3206 $this->importDir = (array) $dirs;
3207 }
3208
3209 3210 3211 3212 3213 3214 3215
3216 public function addImportDir($dir)
3217 {
3218 $this->importDir = (array) $this->importDir;
3219 $this->importDir[] = $dir;
3220 }
3221
3222 3223 3224 3225 3226
3227 public function allParsedFiles()
3228 {
3229 return $this->allParsedFiles;
3230 }
3231
3232 3233 3234 3235 3236 3237 3238
3239 protected function addParsedFile($file)
3240 {
3241 $this->allParsedFiles[realpath($file)] = filemtime($file);
3242 }
3243
3244 3245 3246 3247 3248 3249 3250
3251 protected function throwError($msg = null)
3252 {
3253 if ($this->sourceLoc >= 0)
3254 {
3255 $this->sourceParser->throwError($msg, $this->sourceLoc);
3256 }
3257
3258 throw new exception($msg);
3259 }
3260
3261 3262 3263 3264 3265 3266 3267 3268 3269 3270
3271 public static function ccompile($in, $out, $less = null)
3272 {
3273 if ($less === null)
3274 {
3275 $less = new self;
3276 }
3277
3278 return $less->checkedCompile($in, $out);
3279 }
3280
3281 3282 3283 3284 3285 3286 3287 3288 3289
3290 public static function cexecute($in, $force = false, $less = null)
3291 {
3292 if ($less === null)
3293 {
3294 $less = new self;
3295 }
3296
3297 return $less->cachedCompile($in, $force);
3298 }
3299
3300 protected static $cssColors = array(
3301 'aliceblue' => '240,248,255',
3302 'antiquewhite' => '250,235,215',
3303 'aqua' => '0,255,255',
3304 'aquamarine' => '127,255,212',
3305 'azure' => '240,255,255',
3306 'beige' => '245,245,220',
3307 'bisque' => '255,228,196',
3308 'black' => '0,0,0',
3309 'blanchedalmond' => '255,235,205',
3310 'blue' => '0,0,255',
3311 'blueviolet' => '138,43,226',
3312 'brown' => '165,42,42',
3313 'burlywood' => '222,184,135',
3314 'cadetblue' => '95,158,160',
3315 'chartreuse' => '127,255,0',
3316 'chocolate' => '210,105,30',
3317 'coral' => '255,127,80',
3318 'cornflowerblue' => '100,149,237',
3319 'cornsilk' => '255,248,220',
3320 'crimson' => '220,20,60',
3321 'cyan' => '0,255,255',
3322 'darkblue' => '0,0,139',
3323 'darkcyan' => '0,139,139',
3324 'darkgoldenrod' => '184,134,11',
3325 'darkgray' => '169,169,169',
3326 'darkgreen' => '0,100,0',
3327 'darkgrey' => '169,169,169',
3328 'darkkhaki' => '189,183,107',
3329 'darkmagenta' => '139,0,139',
3330 'darkolivegreen' => '85,107,47',
3331 'darkorange' => '255,140,0',
3332 'darkorchid' => '153,50,204',
3333 'darkred' => '139,0,0',
3334 'darksalmon' => '233,150,122',
3335 'darkseagreen' => '143,188,143',
3336 'darkslateblue' => '72,61,139',
3337 'darkslategray' => '47,79,79',
3338 'darkslategrey' => '47,79,79',
3339 'darkturquoise' => '0,206,209',
3340 'darkviolet' => '148,0,211',
3341 'deeppink' => '255,20,147',
3342 'deepskyblue' => '0,191,255',
3343 'dimgray' => '105,105,105',
3344 'dimgrey' => '105,105,105',
3345 'dodgerblue' => '30,144,255',
3346 'firebrick' => '178,34,34',
3347 'floralwhite' => '255,250,240',
3348 'forestgreen' => '34,139,34',
3349 'fuchsia' => '255,0,255',
3350 'gainsboro' => '220,220,220',
3351 'ghostwhite' => '248,248,255',
3352 'gold' => '255,215,0',
3353 'goldenrod' => '218,165,32',
3354 'gray' => '128,128,128',
3355 'green' => '0,128,0',
3356 'greenyellow' => '173,255,47',
3357 'grey' => '128,128,128',
3358 'honeydew' => '240,255,240',
3359 'hotpink' => '255,105,180',
3360 'indianred' => '205,92,92',
3361 'indigo' => '75,0,130',
3362 'ivory' => '255,255,240',
3363 'khaki' => '240,230,140',
3364 'lavender' => '230,230,250',
3365 'lavenderblush' => '255,240,245',
3366 'lawngreen' => '124,252,0',
3367 'lemonchiffon' => '255,250,205',
3368 'lightblue' => '173,216,230',
3369 'lightcoral' => '240,128,128',
3370 'lightcyan' => '224,255,255',
3371 'lightgoldenrodyellow' => '250,250,210',
3372 'lightgray' => '211,211,211',
3373 'lightgreen' => '144,238,144',
3374 'lightgrey' => '211,211,211',
3375 'lightpink' => '255,182,193',
3376 'lightsalmon' => '255,160,122',
3377 'lightseagreen' => '32,178,170',
3378 'lightskyblue' => '135,206,250',
3379 'lightslategray' => '119,136,153',
3380 'lightslategrey' => '119,136,153',
3381 'lightsteelblue' => '176,196,222',
3382 'lightyellow' => '255,255,224',
3383 'lime' => '0,255,0',
3384 'limegreen' => '50,205,50',
3385 'linen' => '250,240,230',
3386 'magenta' => '255,0,255',
3387 'maroon' => '128,0,0',
3388 'mediumaquamarine' => '102,205,170',
3389 'mediumblue' => '0,0,205',
3390 'mediumorchid' => '186,85,211',
3391 'mediumpurple' => '147,112,219',
3392 'mediumseagreen' => '60,179,113',
3393 'mediumslateblue' => '123,104,238',
3394 'mediumspringgreen' => '0,250,154',
3395 'mediumturquoise' => '72,209,204',
3396 'mediumvioletred' => '199,21,133',
3397 'midnightblue' => '25,25,112',
3398 'mintcream' => '245,255,250',
3399 'mistyrose' => '255,228,225',
3400 'moccasin' => '255,228,181',
3401 'navajowhite' => '255,222,173',
3402 'navy' => '0,0,128',
3403 'oldlace' => '253,245,230',
3404 'olive' => '128,128,0',
3405 'olivedrab' => '107,142,35',
3406 'orange' => '255,165,0',
3407 'orangered' => '255,69,0',
3408 'orchid' => '218,112,214',
3409 'palegoldenrod' => '238,232,170',
3410 'palegreen' => '152,251,152',
3411 'paleturquoise' => '175,238,238',
3412 'palevioletred' => '219,112,147',
3413 'papayawhip' => '255,239,213',
3414 'peachpuff' => '255,218,185',
3415 'peru' => '205,133,63',
3416 'pink' => '255,192,203',
3417 'plum' => '221,160,221',
3418 'powderblue' => '176,224,230',
3419 'purple' => '128,0,128',
3420 'red' => '255,0,0',
3421 'rosybrown' => '188,143,143',
3422 'royalblue' => '65,105,225',
3423 'saddlebrown' => '139,69,19',
3424 'salmon' => '250,128,114',
3425 'sandybrown' => '244,164,96',
3426 'seagreen' => '46,139,87',
3427 'seashell' => '255,245,238',
3428 'sienna' => '160,82,45',
3429 'silver' => '192,192,192',
3430 'skyblue' => '135,206,235',
3431 'slateblue' => '106,90,205',
3432 'slategray' => '112,128,144',
3433 'slategrey' => '112,128,144',
3434 'snow' => '255,250,250',
3435 'springgreen' => '0,255,127',
3436 'steelblue' => '70,130,180',
3437 'tan' => '210,180,140',
3438 'teal' => '0,128,128',
3439 'thistle' => '216,191,216',
3440 'tomato' => '255,99,71',
3441 'transparent' => '0,0,0,0',
3442 'turquoise' => '64,224,208',
3443 'violet' => '238,130,238',
3444 'wheat' => '245,222,179',
3445 'white' => '255,255,255',
3446 'whitesmoke' => '245,245,245',
3447 'yellow' => '255,255,0',
3448 'yellowgreen' => '154,205,50'
3449 );
3450 }
3451