source: spip-zone/_plugins_/less-css/trunk/lessphp/lessc.inc.php @ 48830

Last change on this file since 48830 was 48830, checked in by cedric@…, 10 years ago

Ne pas compiler une meme less deux fois dans le hit (cas si echec au premier coup)
Generer une erreur de compilation SPIP en cas d'erreur de compilation LESS
Definir le importDir de la LESS compilee pour résoudre les @import less
Corriger un bug sur l'import qui provoque une erreur fatale si le fichier importé commence par des commentaires (on fix ici sans reporter au dev de lessPHP car le modele interne de compilation LESS a completement changé depuis cette release)

  • 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 = ltrim($this->removeComments(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.