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 | |
---|
18 | class 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 | ?> |
---|