source: spip-zone/_plugins_/lesspip/lessphp/lessc.inc.php @ 42069

Last change on this file since 42069 was 42069, checked in by monsieur.camille@…, 10 years ago

Premier commit

  • Property svn:executable set to *
File size: 31.9 KB
Line 
1<?php
2
3/**
4 * lessphp v0.2.0
5 * http://leafo.net/lessphp
6 *
7 * LESS Css compiler, adapted from http://lesscss.org/docs.html
8 *
9 * Copyright 2010, Leaf Corcoran <leafot@gmail.com>
10 * Licensed under MIT or GPLv3, see LICENSE
11 */
12
13//
14// investigate trouble with ^M
15// fix the alpha value with color when using a percent
16//
17
18class lessc {
19        private $buffer;
20        private $count;
21        private $line;
22        private $expandStack;
23
24        private $env = array();
25
26        public $vPrefix = '@';
27        public $mPrefix = '$';
28        public $imPrefix = '!';
29        public $selfSelector = '&';
30
31        static private $precedence = array(
32                '+' => 0,
33                '-' => 0,
34                '*' => 1,
35                '/' => 1,
36                '%' => 1,
37        );
38        static private $operatorString; // regex string to match any of the operators
39
40        static private $dtypes = array('expression', 'variable', 'function', 'negative'); // types with delayed computation
41        static private $units = array(
42                'px', '%', 'in', 'cm', 'mm', 'em', 'ex', 'pt', 'pc', 'ms', 's', 'deg');
43
44        public $importDisabled = false;
45        public $importDir = '';
46
47        // compile chunk off the head of buffer
48        function chunk() {
49                if (empty($this->buffer)) return false;
50                $s = $this->seek();
51
52                // a property
53                if ($this->keyword($key) && $this->assign() && $this->propertyValue($value) && $this->end()) {
54                        // look for important prefix
55                        if ($key{0} == $this->imPrefix && strlen($key) > 1) {
56                                $key = substr($key, 1);
57                                if ($value[0] == 'list' && $value[1] == ' ') {
58                                        $value[2][] = array('keyword', '!important');
59                                } else {
60                                        $value = array('list', ' ', array($value, array('keyword', '!important')));
61                                }
62                        }
63                        $this->append($key, $value);
64
65                        if (count($this->env) == 1)
66                                return $this->compileProperty($key, array($value))."\n";
67                        else
68                                return true;
69                } else {
70                        $this->seek($s);
71                }
72
73                // look for special css @ directives
74                if (count($this->env) == 1 && $this->count < strlen($this->buffer) && $this->buffer[$this->count] == '@') {
75                        // a font-face block
76                        if ($this->literal('@font-face') && $this->literal('{')) {
77                                $this->push();
78                                $this->set('__tags', array('@font-face'));
79                                $this->set('__dontsave', true);
80                                return true;
81                        } else {
82                                $this->seek($s);
83                        }
84
85                        // charset
86                        if ($this->literal('@charset') && $this->propertyValue($value) && $this->end()) {
87                                return "@charset ".$this->compileValue($value).";\n";
88                        } else {
89                                $this->seek($s);
90                        }
91                }
92
93                // opening abstract block
94                if ($this->tag($tag, true) && $this->argumentDef($args) && $this->literal('{')) {
95                        $this->push();
96
97                        // move out of variable scope
98                        if ($tag{0} == $this->vPrefix) $tag[0] = $this->mPrefix;
99
100                        $this->set('__tags', array($tag));
101                        if (isset($args)) $this->set('__args', $args);
102
103                        return true;
104                } else {
105                        $this->seek($s);
106                }
107
108                // opening css block
109                if ($this->tags($tags) && $this->literal('{')) {
110                        //  move @ tags out of variable namespace!
111                        foreach($tags as &$tag) {
112                                if ($tag{0} == $this->vPrefix) $tag[0] = $this->mPrefix;
113                        }
114
115                        $this->push();
116                        $this->set('__tags', $tags);   
117
118                        return true;
119                } else {
120                        $this->seek($s);
121                }
122
123                // closing block
124                if ($this->literal('}')) {
125                        $tags = $this->multiplyTags();
126                        $env = end($this->env);
127                        $ctags = $env['__tags'];
128                        unset($env['__tags']);
129
130                        // insert the default arguments
131                        if (isset($env['__args'])) {
132                                foreach ($env['__args'] as $arg) {
133                                        if (isset($arg[1])) {
134                                                $this->prepend($this->vPrefix.$arg[0], $arg[1]);
135                                        }
136                                }
137                        }
138
139                        if (!empty($tags))
140                                $out = $this->compileBlock($tags, $env);
141
142                        $this->pop();
143
144                        // make the block(s) available in the new current scope
145                        if (!isset($env['__dontsave'])) {
146                                foreach ($ctags as $t) {
147                                        // if the block already exists then merge
148                                        if ($this->get($t, array(end($this->env)))) {
149                                                $this->merge($t, $env);
150                                        } else {
151                                                $this->set($t, $env);
152                                        }
153                                }
154                        }
155
156                        return isset($out) ? $out : true;
157                } 
158               
159                // import statement
160                if ($this->import($url, $media)) {
161                        if ($this->importDisabled) return "/* import is disabled */\n";
162
163                        $full = $this->importDir.$url;
164                        if (file_exists($file = $full) || file_exists($file = $full.'.less')) {
165                                $loaded = $this->removeComments(ltrim(file_get_contents($file).";"));
166                                $this->buffer = substr($this->buffer, 0, $this->count).$loaded.substr($this->buffer, $this->count);
167                                return true;
168                        }
169
170                        return '@import url("'.$url.'")'.($media ? ' '.$media : '').";\n";
171                }
172
173                // setting variable
174                if ($this->variable($name) && $this->assign() && $this->propertyValue($value) && $this->end()) {
175                        $this->append($this->vPrefix.$name, $value);
176                        return true;
177                } else {
178                        $this->seek($s);
179                }
180
181                // mixin/function expand
182                if ($this->tags($tags, true, '>') && ($this->argumentValues($argv) || true) && $this->end()) {
183                        $env = $this->getEnv($tags);
184                        if ($env == null) return true;
185
186                        // if we have arguments then insert them
187                        if (!empty($env['__args'])) {
188                                foreach($env['__args'] as $arg) {
189                                        $vname = $this->vPrefix.$arg[0];
190                                        $value = is_array($argv) ? array_shift($argv) : null;
191                                        // copy default value if there isn't one supplied
192                                        if ($value == null && isset($arg[1]))
193                                                $value = $arg[1];
194
195                                        // if ($value == null) continue; // don't define so it can search up
196
197                                        // create new entry if var doesn't exist in scope
198                                        if (isset($env[$vname])) {
199                                                array_unshift($env[$vname], $value);
200                                        } else {
201                                                // new element
202                                                $env[$vname] = array($value);
203                                        }
204                                }
205                        }
206
207                        // set all properties
208                        ob_start();
209                        $blocks = array();
210                        foreach ($env as $name => $value) {
211                                // skip the metatdata
212                                if (preg_match('/^__/', $name)) continue;
213
214                                // if it is a block, remember it to compile after everything
215                                // is mixed in
216                                if (!isset($value[0]))
217                                        $blocks[] = array($name, $value);
218
219                                // copy the data
220                                // don't overwrite previous value, look in current env for name
221                                if ($this->get($name, array(end($this->env)))) {
222                                        while ($tval = array_shift($value))
223                                                $this->append($name, $tval);
224                                } else 
225                                        $this->set($name, $value); 
226                        }
227
228                        // render sub blocks
229                        foreach ($blocks as $b) {
230                                $rtags = $this->multiplyTags(array($b[0]));
231                                echo $this->compileBlock($rtags, $b[1]);
232                        }
233
234                        return ob_get_clean();
235                } else {
236                        $this->seek($s);
237                }
238
239                // spare ;
240                if ($this->literal(';')) return true;
241
242                return false; // couldn't match anything, throw error
243        }
244
245        // recursively find the cartesian product of all tags in stack
246        function multiplyTags($tags = array(' '), $d = null) {
247                if ($d === null) $d = count($this->env) - 1;
248
249                $parents = $d == 0 ? $this->env[$d]['__tags']
250                        : $this->multiplyTags($this->env[$d]['__tags'], $d - 1);
251
252                $rtags = array();
253                foreach ($parents as $p) {
254                        foreach ($tags as $t) {
255                                if ($t{0} == $this->mPrefix) continue; // skip functions
256                                $d = ' ';
257                                if ($t{0} == ':' || $t{0} == $this->selfSelector) {
258                                        $t = ltrim($t, $this->selfSelector);
259                                        $d = '';
260                                }
261                                $rtags[] = trim($p.$d.$t);
262                        }
263                }
264
265                return $rtags;
266        }
267
268        // a list of expressions
269        function expressionList(&$exps) {
270                $values = array();     
271
272                while ($this->expression($exp)) {
273                        $values[] = $exp;
274                }
275               
276                if (count($values) == 0) return false;
277
278                $exps = $this->compressList($values, ' ');
279                return true;
280        }
281
282        // a single expression
283        function expression(&$out) {
284                $s = $this->seek();
285                $needWhite = true;
286                if ($this->literal('(') && $this->expression($exp) && $this->literal(')')) {
287                        $lhs = $exp;
288                        $needWhite = false;
289                } elseif ($this->seek($s) && $this->value($val)) {
290                        $lhs = $val;
291                } else {
292                        return false;
293                }
294
295                $out = $this->expHelper($lhs, 0, $needWhite);
296                return true;
297        }
298
299        // resursively parse infix equation with $lhs at precedence $minP
300        function expHelper($lhs, $minP, $needWhite = true) {
301                $ss = $this->seek();
302                // try to find a valid operator
303                while ($this->match(self::$operatorString.($needWhite ? '\s+' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
304                        $needWhite = true;
305                        // get rhs
306                        $s = $this->seek();
307                        if ($this->literal('(') && $this->expression($exp) && $this->literal(')')) {
308                                $needWhite = false;
309                                $rhs = $exp;
310                        } elseif ($this->seek($s) && $this->value($val)) {
311                                $rhs = $val;
312                        } else break;
313
314                        // peek for next operator to see what to do with rhs
315                        if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > $minP) {
316                                $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
317                        }
318
319                        // don't evaluate yet if it is dynamic
320                        if (in_array($rhs[0], self::$dtypes) || in_array($lhs[0], self::$dtypes))
321                                $lhs = array('expression', $m[1], $lhs, $rhs);
322                        else
323                                $lhs = $this->evaluate($m[1], $lhs, $rhs);
324
325                        $ss = $this->seek();
326                }
327                $this->seek($ss);
328
329                return $lhs;
330        }
331
332        // consume a list of values for a property
333        function propertyValue(&$value) {
334                $values = array();     
335               
336                $s = null;
337                while ($this->expressionList($v)) {
338                        $values[] = $v;
339                        $s = $this->seek();
340                        if (!$this->literal(',')) break;
341                }
342
343                if ($s) $this->seek($s);
344
345                if (count($values) == 0) return false;
346
347                $value = $this->compressList($values, ', ');
348                return true;
349        }
350
351        // a single value
352        function value(&$value) {
353                // try a unit
354                if ($this->unit($value)) return true;   
355
356                // see if there is a negation
357                $s = $this->seek();
358                if ($this->literal('-', false) && $this->variable($vname)) {
359                        $value = array('negative', array('variable', $this->vPrefix.$vname));
360                        return true;
361                } else {
362                        $this->seek($s);
363                }
364
365                // accessor
366                // must be done before color
367                // this needs negation too
368                if ($this->accessor($a)) {
369                        $tmp = $this->getEnv($a[0]);
370                        if ($tmp && isset($tmp[$a[1]]))
371                                $value = end($tmp[$a[1]]);
372                        return true;
373                }
374               
375                // color
376                if ($this->color($value)) return true;
377
378                // css function
379                // must be done after color
380                if ($this->func($value)) return true;
381
382                // string
383                if ($this->string($tmp, $d)) {
384                        $value = array('string', $d.$tmp.$d);
385                        return true;
386                }
387
388                // try a keyword
389                if ($this->keyword($word)) {
390                        $value = array('keyword', $word);
391                        return true;
392                }
393
394                // try a variable
395                if ($this->variable($vname)) {
396                        $value = array('variable', $this->vPrefix.$vname);
397                        return true;
398                }
399
400                return false;
401        }
402
403        // an import statement
404        function import(&$url, &$media) {
405                $s = $this->seek();
406                if (!$this->literal('@import')) return false;
407
408                // @import "something.css" media;
409                // @import url("something.css") media;
410                // @import url(something.css) media;
411
412                if ($this->literal('url(')) $parens = true; else $parens = false;
413
414                if (!$this->string($url)) {
415                        if ($parens && $this->to(')', $url)) {
416                                $parens = false; // got em
417                        } else {
418                                $this->seek($s);
419                                return false;
420                        }
421                }
422
423                if ($parens && !$this->literal(')')) {
424                        $this->seek($s);
425                        return false;
426                }
427
428                // now the rest is media
429                return $this->to(';', $media, false, true);
430        }
431
432        // a scoped value accessor
433        // .hello > @scope1 > @scope2['value'];
434        function accessor(&$var) {
435                $s = $this->seek();
436
437                if (!$this->tags($scope, true, '>') || !$this->literal('[')) {
438                        $this->seek($s);
439                        return false;
440                }
441
442                // either it is a variable or a property
443                // why is a property wrapped in quotes, who knows!
444                if ($this->variable($name)) {
445                        $name = $this->vPrefix.$name;
446                } elseif($this->literal("'") && $this->keyword($name) && $this->literal("'")) {
447                        // .. $this->count is messed up if we wanted to test another access type
448                } else {
449                        $this->seek($s);
450                        return false;
451                }
452
453                if (!$this->literal(']')) {
454                        $this->seek($s);
455                        return false;
456                }
457
458                $var = array($scope, $name);
459                return true;
460        }
461
462        // a string
463        function string(&$string, &$d = null) {
464                $s = $this->seek();
465                if ($this->literal('"', false)) {
466                        $delim = '"';
467                } else if($this->literal("'", false)) {
468                        $delim = "'";
469                } else {
470                        return false;
471                }
472
473                if (!$this->to($delim, $string)) {
474                        $this->seek($s);
475                        return false;
476                }
477               
478                $d = $delim;
479                return true;
480        }
481
482        // a numerical unit
483        function unit(&$unit, $allowed = null) {
484                $simpleCase = $allowed == null;
485                if (!$allowed) $allowed = self::$units;
486
487                if ($this->match('(-?[0-9]*(\.)?[0-9]+)('.implode('|', $allowed).')?', $m, !$simpleCase)) {
488                        if (!isset($m[3])) $m[3] = 'number';
489                        $unit = array($m[3], $m[1]);
490
491                        // check for size/height font unit.. should this even be here?
492                        if ($simpleCase) {
493                                $s = $this->seek();
494                                if ($this->literal('/', false) && $this->unit($right, self::$units)) {
495                                        $unit = array('keyword', $this->compileValue($unit).'/'.$this->compileValue($right));
496                                } else {
497                                        // get rid of whitespace
498                                        $this->seek($s);
499                                        $this->match('', $_);
500                                }
501                        }
502
503                        return true;
504                }
505
506                return false;
507        }
508
509        // a # color
510        function color(&$out) {
511                $color = array('color');
512
513                if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
514                        if (isset($m[3])) {
515                                $num = $m[3];
516                                $width = 16;
517                        } else {
518                                $num = $m[2];
519                                $width = 256;
520                        }
521
522                        $num = hexdec($num);
523                        foreach(array(3,2,1) as $i) {
524                                $t = $num % $width;
525                                $num /= $width;
526
527                                $color[$i] = $t * (256/$width) + $t * floor(16/$width);
528                        }
529                       
530                        $out = $color;
531                        return true;
532                } 
533
534                return false;
535        }
536
537        // consume a list of property values delimited by ; and wrapped in ()
538        function argumentValues(&$args, $delim = ';') {
539                $s = $this->seek();
540                if (!$this->literal('(')) return false;
541
542                $values = array();
543                while ($this->propertyValue($value)) {
544                        $values[] = $value;
545                        if (!$this->literal($delim)) break;
546                }
547
548                if (!$this->literal(')')) {
549                        $this->seek($s);
550                        return false;
551                }
552               
553                $args = $values;
554                return true;
555        }
556
557        // consume an argument definition list surrounded by (), each argument is a variable name with optional value
558        function argumentDef(&$args, $delim = ';') {
559                $s = $this->seek();
560                if (!$this->literal('(')) return false;
561
562                $values = array();
563                while ($this->variable($vname)) {
564                        $arg = array($vname);
565                        if ($this->assign() && $this->propertyValue($value)) {
566                                $arg[] = $value;
567                                // let the : slide if there is no value
568                        }
569
570                        $values[] = $arg;
571                        if (!$this->literal($delim)) break;
572                }
573
574                if (!$this->literal(')')) {
575                        $this->seek($s);
576                        return false;
577                }
578
579                $args = $values;
580                return true;
581        }
582
583        // consume a list of tags
584        // this accepts a hanging delimiter
585        function tags(&$tags, $simple = false, $delim = ',') {
586                $tags = array();
587                while ($this->tag($tt, $simple)) {
588                        $tags[] = $tt;
589                        if (!$this->literal($delim)) break;
590                }
591                if (count($tags) == 0) return false;
592
593                return true;
594        }
595
596        // a single tag
597        function tag(&$tag, $simple = false) {
598                if ($simple)
599                        $chars = '^,:;{}\][>\(\) ';
600                else
601                        $chars = '^,;{}[';
602
603                $tag = '';
604                while ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
605                        $tag.= $m[1];
606                        if ($simple) break;
607
608                        $s = $this->seek();
609                        if ($this->literal('[') && $this->to(']', $c, true) && $this->literal(']')) {
610                                $tag .= '['.$c.'] ';
611                        } else {
612                                $this->seek($s);
613                                break;
614                        }
615                }
616                $tag = trim($tag);
617                if ($tag == '') return false;
618
619                return true;
620        }
621
622        // a css function
623        function func(&$func) {
624                $s = $this->seek();
625
626                if ($this->match('([\w\-_][\w\-_:\.]*)', $m) && $this->literal('(')) {
627                        $fname = $m[1];
628                        if ($fname == 'url') {
629                                $this->to(')', $content, true);
630                                $args = array('string', $content);
631                        } else {
632                                $args = array();
633                                while (true) {
634                                        $ss = $this->seek();
635                                        if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
636                                                $args[] = array('list', '=', array(array('keyword', $name), $value));
637                                        } else {
638                                                $this->seek($ss);
639                                                if ($this->expressionList($value)) {
640                                                        $args[] = $value;
641                                                }
642                                        }
643
644                                        if (!$this->literal(',')) break;
645                                }
646                                $args = array('list', ',', $args);
647                        }
648
649                        if ($this->literal(')')) {
650                                $func = array('function', $fname, $args);
651                                return true;
652                        }
653                }
654
655                $this->seek($s);
656                return false;
657        }
658
659        // consume a less variable
660        function variable(&$name) {
661                $s = $this->seek();
662                if ($this->literal($this->vPrefix, false) && $this->keyword($name)) {
663                        return true;   
664                }
665                return false;
666        }
667
668        // consume an assignment operator
669        function assign() {
670                return $this->literal(':') || $this->literal('=');
671        }
672
673        // consume a keyword
674        function keyword(&$word) {
675                if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
676                        $word = $m[1];
677                        return true;
678                }
679                return false;
680        }
681
682        // consume an end of statement delimiter
683        function end() {
684                if ($this->literal(';'))
685                        return true;
686                elseif ($this->count == strlen($this->buffer) || $this->buffer{$this->count} == '}') {
687                        // if there is end of file or a closing block next then we don't need a ;
688                        return true;
689                }
690                return false;
691        }
692
693        function compressList($items, $delim) {
694                if (count($items) == 1) return $items[0];       
695                else return array('list', $delim, $items);
696        }
697
698        function compileBlock($rtags, $env) {
699                // don't render functions
700                // todo: this shouldn't need to happen because multiplyTags prunes them, verify
701                /*
702                foreach ($rtags as $i => $tag) {
703                        if (preg_match('/( |^)%/', $tag))
704                                unset($rtags[$i]);
705                }
706                 */
707                if (empty($rtags)) return '';
708
709                $props = 0;
710                // print all the visible properties
711                ob_start();
712                foreach ($env as $name => $value) {
713                        // todo: change this, poor hack
714                        // make a better name storage system!!! (value types are fine)
715                        // but.. don't render special properties (blocks, vars, metadata)
716                        if (isset($value[0]) && $name{0} != $this->vPrefix && $name != '__args') {
717                                echo $this->compileProperty($name, $value, 1)."\n";
718                                $props++;
719                        }
720                }
721                $list = ob_get_clean();
722
723                if ($props == 0) return '';
724
725                // do some formatting
726                if ($props == 1) $list = ' '.trim($list).' ';
727                return implode(", ", $rtags).' {'.($props  > 1 ? "\n" : '').
728                        $list."}\n";
729
730        }
731
732        function compileProperty($name, $value, $level = 0) {
733                // output all repeated properties
734                foreach ($value as $v)
735                        $props[] = str_repeat('  ', $level).
736                                $name.':'.$this->compileValue($v).';';
737
738                return implode("\n", $props);
739        }
740
741        function compileValue($value) {
742                switch($value[0]) {
743                case 'list':
744                        // [1] - delimiter
745                        // [2] - array of values
746                        return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
747                case 'keyword':
748                        // [1] - the keyword
749                case 'number':
750                        // [1] - the number
751                        return $value[1];
752                case 'expression':
753                        // [1] - operator
754                        // [2] - value of left hand side
755                        // [3] - value of right
756                        return $this->compileValue($this->evaluate($value[1], $value[2], $value[3]));
757                case 'string':
758                        // [1] - contents of string (includes quotes)
759                       
760                        // search for inline variables to replace
761                        $replace = array();
762                        if (preg_match_all('/{(@[\w-_][0-9\w-_]*)}/', $value[1], $m)) {
763                                foreach($m[1] as $name) {
764                                        if (!isset($replace[$name]))
765                                                $replace[$name] = $this->compileValue(array('variable', $name));
766                                }
767                        }
768                        foreach ($replace as $var=>$val) {
769                                // strip quotes
770                                if (preg_match('/^(["\']).*?(\1)$/', $val)) {
771                                        $val = substr($val, 1, -1);
772                                }
773                                $value[1] = str_replace('{'.$var.'}', $val, $value[1]);
774                        }
775
776                        return $value[1];
777                case 'color':
778                        // [1] - red component (either number for a %)
779                        // [2] - green component
780                        // [3] - blue component
781                        // [4] - optional alpha component
782                        if (count($value) == 5) { // rgba
783                                return 'rgba('.$value[1].','.$value[2].','.$value[3].','.$value[4].')';
784                        }
785
786                        $out = '#';
787                        foreach (range(1,3) as $i)
788                                $out .= ($value[$i] < 16 ? '0' : '').dechex($value[$i]);
789                        return $out;
790                case 'variable':
791                        // [1] - the name of the variable including @
792                        $tmp = $this->compileValue(
793                                $this->getVal($value[1], $this->pushName($value[1]))
794                        );
795                        $this->popName();
796
797                        return $tmp;
798                case 'negative':
799                        // [1] - some value that needs to become negative
800                        return $this->compileValue($this->reduce($value));
801                case 'function':
802                        // [1] - function name
803                        // [2] - some value representing arguments
804
805                        // see if there is a library function for this func
806                        $f = array($this, 'lib_'.$value[1]);
807                        if (is_callable($f)) {
808                                return call_user_func($f, $value[2]);
809                        }
810
811                        return $value[1].'('.$this->compileValue($value[2]).')';
812
813                default: // assumed to be unit 
814                        return $value[1].$value[0];
815                }
816        }
817
818        function lib_quote($arg) {
819                return '"'.$this->compileValue($arg).'"';
820        }
821
822        function lib_unquote($arg) {
823                $out = $this->compileValue($arg);
824                if ($this->quoted($out)) $out = substr($out, 1, -1);
825                return $out;
826        }
827
828        // is a string surrounded in quotes? returns the quoting char if true
829        function quoted($s) {
830                if (preg_match('/^("|\').*?\1$/', $s, $m))
831                        return $m[1];
832                else return false;
833        }
834
835        // convert rgb, rgba into color type suitable for math
836        // todo: add hsl
837        function funcToColor($func) {
838                $fname = $func[1];
839                if (!preg_match('/^(rgb|rgba)$/', $fname)) return false;
840                if ($func[2][0] != 'list') return false; // need a list of arguments
841
842                $components = array();
843                $i = 1;
844                foreach ($func[2][2] as $c) {
845                        $c = $this->reduce($c);
846                        if ($i < 4) {
847                                if ($c[0] == '%') $components[] = 255 * ($c[1] / 100);
848                                else $components[] = floatval($c[1]); 
849                        } elseif ($i == 4) {
850                                if ($c[0] == '%') $components[] = 1.0 * ($c[1] / 100);
851                                else $components[] = floatval($c[1]);
852                        } else break;
853
854                        $i++;
855                }
856                while (count($components) < 3) $components[] = 0;
857
858                array_unshift($components, 'color');
859                return $this->fixColor($components);
860        }
861
862        // reduce a delayed type to its final value
863        // dereference variables and solve equations
864        function reduce($var, $defaultValue = array('number', 0)) {
865                $pushed = 0; // number of variable names pushed
866
867                while (in_array($var[0], self::$dtypes)) {
868                        if ($var[0] == 'expression') {
869                                $var = $this->evaluate($var[1], $var[2], $var[3]);
870                        } else if ($var[0] == 'variable') {
871                                $var = $this->getVal($var[1], $this->pushName($var[1]), $defaultValue);
872                                $pushed++;
873                        } else if ($var[0] == 'function') {
874                                $color = $this->funcToColor($var);
875                                if ($color) $var = $color;
876                                break; // no where to go after a function
877                        } else if ($var[0] == 'negative') {
878                                $value = $this->reduce($var[1]);
879                                if (is_numeric($value[1])) {
880                                        $value[1] = -1*$value[1];
881                                } 
882                                $var = $value;
883                        }
884                }
885
886                while ($pushed != 0) { $this->popName(); $pushed--; }
887                return $var;
888        }
889
890        // evaluate an expression
891        function evaluate($op, $left, $right) {
892                $left = $this->reduce($left);
893                $right = $this->reduce($right);
894
895                if ($left[0] == 'color' && $right[0] == 'color') {
896                        $out = $this->op_color_color($op, $left, $right);
897                        return $out;
898                }
899
900                if ($left[0] == 'color') {
901                        return $this->op_color_number($op, $left, $right);
902                }
903
904                if ($right[0] == 'color') {
905                        return $this->op_number_color($op, $left, $right);
906                }
907
908                // concatenate strings
909                if ($op == '+' && $left[0] == 'string') {
910                        $append = $this->compileValue($right);
911                        if ($this->quoted($append)) $append = substr($append, 1, -1);
912
913                        $lhs = $this->compileValue($left);
914                        if ($q = $this->quoted($lhs)) $lhs = substr($lhs, 1, -1);
915                        if (!$q) $q = '';
916
917                        return array('string', $q.$lhs.$append.$q);
918                }
919
920                if ($left[0] == 'keyword' || $right[0] == 'keyword' ||
921                        $left[0] == 'string' || $right[0] == 'string')
922                {
923                        // look for negative op
924                        if ($op == '-') $right[1] = '-'.$right[1];
925                        return array('keyword', $this->compileValue($left) .' '. $this->compileValue($right));
926                }
927       
928                // default to number operation
929                return $this->op_number_number($op, $left, $right);
930        }
931
932        // make sure a color's components don't go out of bounds
933        function fixColor($c) {
934                foreach (range(1, 3) as $i) {
935                        if ($c[$i] < 0) $c[$i] = 0;
936                        if ($c[$i] > 255) $c[$i] = 255;
937                        $c[$i] = floor($c[$i]);
938                }
939
940                return $c;
941        }
942
943        function op_number_color($op, $lft, $rgt) {
944                if ($op == '+' || $op = '*') {
945                        return $this->op_color_number($op, $rgt, $lft);
946                }
947        }
948
949        function op_color_number($op, $lft, $rgt) {
950                if ($rgt[0] == '%') $rgt[1] /= 100;
951
952                return $this->op_color_color($op, $lft,
953                        array_fill(1, count($lft) - 1, $rgt[1]));
954        }
955
956        function op_color_color($op, $left, $right) {
957                $out = array('color');
958                $max = count($left) > count($right) ? count($left) : count($right);
959                foreach (range(1, $max - 1) as $i) {
960                        $lval = isset($left[$i]) ? $left[$i] : 0;
961                        $rval = isset($right[$i]) ? $right[$i] : 0;
962                        switch ($op) {
963                        case '+':
964                                $out[] = $lval + $rval;
965                                break;
966                        case '-':
967                                $out[] = $lval - $rval;
968                                break;
969                        case '*':
970                                $out[] = $lval * $rval;
971                                break;
972                        case '%':
973                                $out[] = $lval % $rval;
974                                break;
975                        case '/':
976                                if ($rval == 0) throw new exception("evaluate error: can't divide by zero");
977                                $out[] = $lval / $rval;
978                                break;
979                        default:
980                                throw new exception('evaluate error: color op number failed on op '.$op);
981                        }
982                }
983                return $this->fixColor($out);
984        }
985
986        // operator on two numbers
987        function op_number_number($op, $left, $right) {
988                if ($right[0] == '%') $right[1] /= 100;
989
990                // figure out type
991                if ($right[0] == 'number' || $right[0] == '%') $type = $left[0];
992                else $type = $right[0];
993
994                $value = 0;
995                switch($op) {
996                case '+':
997                        $value = $left[1] + $right[1];
998                        break; 
999                case '*':
1000                        $value = $left[1] * $right[1];
1001                        break; 
1002                case '-':
1003                        $value = $left[1] - $right[1];
1004                        break; 
1005                case '%':
1006                        $value = $left[1] % $right[1];
1007                        break; 
1008                case '/':
1009                        if ($right[1] == 0) throw new exception('parse error: divide by zero');
1010                        $value = $left[1] / $right[1];
1011                        break;
1012                default:
1013                        throw new exception('parse error: unknown number operator: '.$op);     
1014                }
1015
1016                return array($type, $value);
1017        }
1018
1019
1020        /* environment functions */
1021
1022        // push name on expand stack, and return its
1023        // count before being pushed
1024        function pushName($name) {
1025                $count = array_count_values($this->expandStack);
1026                $count = isset($count[$name]) ? $count[$name] : 0;
1027
1028                $this->expandStack[] = $name;
1029
1030                return $count;
1031        }
1032
1033        // pop name off expand stack and return it
1034        function popName() {
1035                return array_pop($this->expandStack);
1036        }
1037
1038        // push a new environment
1039        function push() {
1040                $this->level++;
1041                $this->env[] = array();
1042        }
1043
1044        // pop environment off the stack
1045        function pop() {
1046                if ($this->level == 1)
1047                        throw new exception('parse error: unexpected end of block');
1048
1049                $this->level--;
1050                return array_pop($this->env);
1051        }
1052
1053        // set something in the current env
1054        function set($name, $value) {
1055                $this->env[count($this->env) - 1][$name] = $value;
1056        }
1057
1058        // append to array in the current env
1059        function append($name, $value) {
1060                $this->env[count($this->env) - 1][$name][] = $value;
1061        }
1062
1063        // put on the front of the value
1064        function prepend($name, $value) {
1065                if (isset($this->env[count($this->env) - 1][$name]))
1066                        array_unshift($this->env[count($this->env) - 1][$name], $value);
1067                else $this->append($name, $value);
1068        }
1069
1070        // get the highest occurrence of value
1071        function get($name, $env = null) {
1072                if (empty($env)) $env = $this->env;
1073
1074                for ($i = count($env) - 1; $i >= 0; $i--)
1075                        if (isset($env[$i][$name])) return $env[$i][$name];
1076
1077                return null;
1078        }
1079
1080        // get the most recent value of a variable
1081        // return default if it isn't found
1082        // $skip is number of vars to skip
1083        function getVal($name, $skip = 0, $default = array('keyword', '')) {
1084                $val = $this->get($name);
1085                if ($val == null) return $default;
1086
1087                $tmp = $this->env;
1088                while (!isset($tmp[count($tmp) - 1][$name])) array_pop($tmp);
1089                while ($skip > 0) {
1090                        $skip--;
1091
1092                        if (!empty($val)) {
1093                                array_pop($val);
1094                        }
1095
1096                        if (empty($val)) {
1097                                array_pop($tmp);
1098                                $val = $this->get($name, $tmp);
1099                        }
1100
1101                        if (empty($val)) return $default;
1102                }
1103
1104                return end($val);
1105        }
1106
1107        // get the environment described by path, an array of env names
1108        function getEnv($path) {
1109                if (!is_array($path)) $path = array($path);
1110
1111                //  move @ tags out of variable namespace
1112                foreach($path as &$tag)
1113                        if ($tag{0} == $this->vPrefix) $tag[0] = $this->mPrefix;
1114
1115                $env = $this->get(array_shift($path));
1116                while ($sub = array_shift($path)) {
1117                        if (isset($env[$sub]))  // todo add a type check for environment
1118                                $env = $env[$sub];
1119                        else {
1120                                $env = null;
1121                                break;
1122                        }
1123                }
1124                return $env;
1125        }
1126
1127        // merge a block into the current env
1128        function merge($name, $value) {
1129                // if the current block isn't there then just set
1130                $top =& $this->env[count($this->env) - 1];
1131                if (!isset($top[$name])) return $this->set($name, $value);
1132
1133                // copy the block into the old one, including meta data
1134                foreach ($value as $k=>$v) {
1135                        // todo: merge property values instead of replacing
1136                        // have to check type for this
1137                        $top[$name][$k] = $v;
1138                }
1139        }
1140
1141        function literal($what, $eatWhitespace = true) {
1142                // this is here mainly prevent notice from { } string accessor
1143                if ($this->count >= strlen($this->buffer)) return false;
1144
1145                // shortcut on single letter
1146                if (!$eatWhitespace and strlen($what) == 1) {
1147                        if ($this->buffer{$this->count} == $what) {
1148                                $this->count++;
1149                                return true;
1150                        }
1151                        else return false;
1152                }
1153
1154                return $this->match($this->preg_quote($what), $m, $eatWhitespace);
1155        }
1156
1157        function preg_quote($what) {
1158                return preg_quote($what, '/');
1159        }
1160
1161        // advance counter to next occurrence of $what
1162        // $until - don't include $what in advance
1163        function to($what, &$out, $until = false, $allowNewline = false) {
1164                $validChars = $allowNewline ? "[^\n]" : '.';
1165                if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false;
1166                if ($until) $this->count -= strlen($what); // give back $what
1167                $out = $m[1];
1168                return true;
1169        }
1170       
1171        // try to match something on head of buffer
1172        function match($regex, &$out, $eatWhitespace = true) {
1173                $r = '/'.$regex.($eatWhitespace ? '\s*' : '').'/Ais';
1174                if (preg_match($r, $this->buffer, $out, null, $this->count)) {
1175                        $this->count += strlen($out[0]);
1176                        return true;
1177                }
1178                return false;
1179        }
1180
1181
1182        // match something without consuming it
1183        function peek($regex, &$out = null) {
1184                $r = '/'.$regex.'/Ais';
1185                $result =  preg_match($r, $this->buffer, $out, null, $this->count);
1186               
1187                return $result;
1188        }
1189
1190        // seek to a spot in the buffer or return where we are on no argument
1191        function seek($where = null) {
1192                if ($where === null) return $this->count;
1193                else $this->count = $where;
1194                return true;
1195        }
1196
1197        // parse and compile buffer
1198        function parse($str = null) {
1199                if ($str) $this->buffer = $str;         
1200
1201                $this->env = array();
1202                $this->expandStack = array();
1203                $this->count = 0;
1204                $this->line = 1;
1205
1206                $this->buffer = $this->removeComments($this->buffer);
1207                $this->push(); // set up global scope
1208                $this->set('__tags', array('')); // equivalent to 1 in tag multiplication
1209
1210                // trim whitespace on head
1211                if (preg_match('/^\s+/', $this->buffer, $m)) {
1212                        $this->line  += substr_count($m[0], "\n");
1213                        $this->buffer = ltrim($this->buffer);
1214                }
1215
1216                $out = '';
1217                while (false !== ($compiled = $this->chunk())) {
1218                        if (is_string($compiled)) $out .= $compiled;
1219                }
1220
1221                if ($this->count != strlen($this->buffer)) $this->throwParseError();
1222
1223                if (count($this->env) > 1)
1224                        throw new exception('parse error: unclosed block');
1225
1226                // print_r($this->env);
1227                return $out;
1228        }
1229
1230        function throwParseError($msg = 'parse error') {
1231                $line = $this->line + substr_count(substr($this->buffer, 0, $this->count), "\n");
1232                if ($this->peek("(.*?)\n", $m))
1233                        throw new exception($msg.': failed at `'.$m[1].'` line: '.$line);
1234        }
1235
1236        function __construct($fname = null) {
1237                if (!self::$operatorString) {
1238                        self::$operatorString = 
1239                                '('.implode('|', array_map(array($this, 'preg_quote'), array_keys(self::$precedence))).')';
1240                }
1241
1242                if ($fname) {
1243                        if (!is_file($fname)) {
1244                                throw new Exception('load error: failed to find '.$fname);
1245                        }
1246                        $pi = pathinfo($fname);
1247
1248                        $this->fileName = $fname;
1249                        $this->importDir = $pi['dirname'].'/';
1250                        $this->buffer = file_get_contents($fname);
1251                }
1252        }
1253
1254        // remove comments from $text
1255        // todo: make it work for all functions, not just url
1256        // todo: make it not mess up line counter with block comments
1257        function removeComments($text) {
1258                $out = '';
1259
1260                while (!empty($text) &&
1261                        preg_match('/^(.*?)("|\'|\/\/|\/\*|url\(|$)/is', $text, $m))
1262                {
1263                        if (!trim($text)) break;
1264
1265                        $out .= $m[1];
1266                        $text = substr($text, strlen($m[0]));
1267
1268                        switch ($m[2]) {
1269                        case 'url(':
1270                                preg_match('/^(.*?)(\)|$)/is', $text, $inner);
1271                                $text = substr($text, strlen($inner[0]));
1272                                $out .= $m[2].$inner[1].$inner[2];
1273                                break;
1274                        case '//':
1275                                preg_match("/^(.*?)(\n|$)/is", $text, $inner);
1276                                // give back the newline
1277                                $text = substr($text, strlen($inner[0]) - 1);
1278                                break;
1279                        case '/*';
1280                                preg_match("/^(.*?)(\*\/|$)/is", $text, $inner);
1281                                $text = substr($text, strlen($inner[0]));
1282                                break;
1283                        case '"':
1284                        case "'":
1285                                preg_match("/^(.*?)(".$m[2]."|$)/is", $text, $inner);
1286                                $text = substr($text, strlen($inner[0]));
1287                                $out .= $m[2].$inner[1].$inner[2];
1288                                break;
1289                        }
1290                }
1291
1292                return $out;
1293        }
1294
1295
1296        // compile to $in to $out if $in is newer than $out
1297        // returns true when it compiles, false otherwise
1298        public static function ccompile($in, $out) {
1299                if (!is_file($out) || filemtime($in) > filemtime($out)) {
1300                        $less = new lessc($in);
1301                        file_put_contents($out, $less->parse());
1302                        return true;
1303                }
1304
1305                return false;
1306        }
1307
1308}
1309
1310
1311
1312?>
Note: See TracBrowser for help on using the repository browser.