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