PluginProbe
Responsive Menu – Create Mobile-Friendly Menu / 3.0.6
Responsive Menu – Create Mobile-Friendly Menu v3.0.6
4.7.3 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.10 4.1.11 4.1.12 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 All 90 releases
responsive-menu / src / app / Mappers / scssphp / scss.inc.php

scss.inc.php in Responsive Menu – Create Mobile-Friendly Menu 3.0.6, at src/app/Mappers/scssphp/scss.inc.php

4,578 lines 108.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SCSS compiler written in PHP
4 *
5 * @copyright 2012-2013 Leaf Corcoran
6 *
7 * @license http://opensource.org/licenses/gpl-license GPL-3.0
8 * @license http://opensource.org/licenses/MIT MIT
9 *
10 * @link http://leafo.net/scssphp
11 */
12
13 /**
14 * The scss compiler and parser.
15 *
16 * Converting SCSS to CSS is a three stage process. The incoming file is parsed
17 * by `scss_parser` into a syntax tree, then it is compiled into another tree
18 * representing the CSS structure by `scssc`. The CSS tree is fed into a
19 * formatter, like `scss_formatter` which then outputs CSS as a string.
20 *
21 * During the first compile, all values are *reduced*, which means that their
22 * types are brought to the lowest form before being dump as strings. This
23 * handles math equations, variable dereferences, and the like.
24 *
25 * The `parse` function of `scssc` is the entry point.
26 *
27 * In summary:
28 *
29 * The `scssc` class creates an instance of the parser, feeds it SCSS code,
30 * then transforms the resulting tree to a CSS tree. This class also holds the
31 * evaluation context, such as all available mixins and variables at any given
32 * time.
33 *
34 * The `scss_parser` class is only concerned with parsing its input.
35 *
36 * The `scss_formatter` takes a CSS tree, and dumps it to a formatted string,
37 * handling things like indentation.
38 */
39
40 /**
41 * SCSS compiler
42 *
43 * @author Leaf Corcoran <leafot@gmail.com>
44 */
45 class scssc {
46 static public $VERSION = 'v0.0.12';
47
48 static protected $operatorNames = array(
49 '+' => "add",
50 '-' => "sub",
51 '*' => "mul",
52 '/' => "div",
53 '%' => "mod",
54
55 '==' => "eq",
56 '!=' => "neq",
57 '<' => "lt",
58 '>' => "gt",
59
60 '<=' => "lte",
61 '>=' => "gte",
62 );
63
64 static protected $namespaces = array(
65 "special" => "%",
66 "mixin" => "@",
67 "function" => "^",
68 );
69
70 static protected $unitTable = array(
71 "in" => array(
72 "in" => 1,
73 "pt" => 72,
74 "pc" => 6,
75 "cm" => 2.54,
76 "mm" => 25.4,
77 "px" => 96,
78 )
79 );
80
81 static public $true = array("keyword", "true");
82 static public $false = array("keyword", "false");
83 static public $null = array("null");
84
85 static public $defaultValue = array("keyword", "");
86 static public $selfSelector = array("self");
87
88 protected $importPaths = array("");
89 protected $importCache = array();
90
91 protected $userFunctions = array();
92 protected $registeredVars = array();
93
94 protected $numberPrecision = 5;
95
96 protected $formatter = "scss_formatter_nested";
97
98 /**
99 * Compile scss
100 *
101 * @param string $code
102 * @param string $name
103 *
104 * @return string
105 */
106 public function compile($code, $name = null)
107 {
108 try {
109 $this->indentLevel = -1;
110 $this->commentsSeen = array();
111 $this->extends = array();
112 $this->extendsMap = array();
113 $this->parsedFiles = array();
114 $this->env = null;
115 $this->scope = null;
116
117 $locale = setlocale(LC_NUMERIC, 0);
118 setlocale(LC_NUMERIC, "C");
119
120 $this->parser = new scss_parser($name);
121
122 $tree = $this->parser->parse($code);
123
124 $this->formatter = new $this->formatter();
125
126 $this->pushEnv($tree);
127 $this->injectVariables($this->registeredVars);
128 $this->compileRoot($tree);
129 $this->popEnv();
130
131 $out = $this->formatter->format($this->scope);
132
133 setlocale(LC_NUMERIC, $locale);
134
135 return $out;
136 } catch(Exception $e) {}
137
138 }
139
140 protected function isSelfExtend($target, $origin) {
141 foreach ($origin as $sel) {
142 if (in_array($target, $sel)) {
143 return true;
144 }
145 }
146
147 return false;
148 }
149
150 protected function pushExtends($target, $origin) {
151 if ($this->isSelfExtend($target, $origin)) {
152 return;
153 }
154
155 $i = count($this->extends);
156 $this->extends[] = array($target, $origin);
157
158 foreach ($target as $part) {
159 if (isset($this->extendsMap[$part])) {
160 $this->extendsMap[$part][] = $i;
161 } else {
162 $this->extendsMap[$part] = array($i);
163 }
164 }
165 }
166
167 protected function makeOutputBlock($type, $selectors = null) {
168 $out = new stdClass;
169 $out->type = $type;
170 $out->lines = array();
171 $out->children = array();
172 $out->parent = $this->scope;
173 $out->selectors = $selectors;
174 $out->depth = $this->env->depth;
175
176 return $out;
177 }
178
179 protected function matchExtendsSingle($single, &$outOrigin) {
180 $counts = array();
181 foreach ($single as $part) {
182 if (!is_string($part)) return false; // hmm
183
184 if (isset($this->extendsMap[$part])) {
185 foreach ($this->extendsMap[$part] as $idx) {
186 $counts[$idx] =
187 isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
188 }
189 }
190 }
191
192 $outOrigin = array();
193 $found = false;
194
195 foreach ($counts as $idx => $count) {
196 list($target, $origin) = $this->extends[$idx];
197
198 // check count
199 if ($count != count($target)) continue;
200
201 // check if target is subset of single
202 if (array_diff(array_intersect($single, $target), $target)) continue;
203
204 $rem = array_diff($single, $target);
205
206 foreach ($origin as $j => $new) {
207 // prevent infinite loop when target extends itself
208 foreach ($new as $new_selector) {
209 if (!array_diff($single, $new_selector)) {
210 continue 2;
211 }
212 }
213
214 $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
215 }
216
217 $outOrigin = array_merge($outOrigin, $origin);
218
219 $found = true;
220 }
221
222 return $found;
223 }
224
225 protected function combineSelectorSingle($base, $other) {
226 $tag = null;
227 $out = array();
228
229 foreach (array($base, $other) as $single) {
230 foreach ($single as $part) {
231 if (preg_match('/^[^\[.#:]/', $part)) {
232 $tag = $part;
233 } else {
234 $out[] = $part;
235 }
236 }
237 }
238
239 if ($tag) {
240 array_unshift($out, $tag);
241 }
242
243 return $out;
244 }
245
246 protected function matchExtends($selector, &$out, $from = 0, $initial=true) {
247 foreach ($selector as $i => $part) {
248 if ($i < $from) continue;
249
250 if ($this->matchExtendsSingle($part, $origin)) {
251 $before = array_slice($selector, 0, $i);
252 $after = array_slice($selector, $i + 1);
253
254 foreach ($origin as $new) {
255 $k = 0;
256
257 // remove shared parts
258 if ($initial) {
259 foreach ($before as $k => $val) {
260 if (!isset($new[$k]) || $val != $new[$k]) {
261 break;
262 }
263 }
264 }
265
266 $result = array_merge(
267 $before,
268 $k > 0 ? array_slice($new, $k) : $new,
269 $after);
270
271
272 if ($result == $selector) continue;
273 $out[] = $result;
274
275 // recursively check for more matches
276 $this->matchExtends($result, $out, $i, false);
277
278 // selector sequence merging
279 if (!empty($before) && count($new) > 1) {
280 $result2 = array_merge(
281 array_slice($new, 0, -1),
282 $k > 0 ? array_slice($before, $k) : $before,
283 array_slice($new, -1),
284 $after);
285
286 $out[] = $result2;
287 }
288 }
289 }
290 }
291 }
292
293 protected function flattenSelectors($block, $parentKey = null) {
294 if ($block->selectors) {
295 $selectors = array();
296 foreach ($block->selectors as $s) {
297 $selectors[] = $s;
298 if (!is_array($s)) continue;
299 // check extends
300 if (!empty($this->extendsMap)) {
301 $this->matchExtends($s, $selectors);
302 }
303 }
304
305 $block->selectors = array();
306 $placeholderSelector = false;
307 foreach ($selectors as $selector) {
308 if ($this->hasSelectorPlaceholder($selector)) {
309 $placeholderSelector = true;
310 continue;
311 }
312 $block->selectors[] = $this->compileSelector($selector);
313 }
314
315 if ($placeholderSelector && 0 == count($block->selectors) && null !== $parentKey) {
316 unset($block->parent->children[$parentKey]);
317 return;
318 }
319 }
320
321 foreach ($block->children as $key => $child) {
322 $this->flattenSelectors($child, $key);
323 }
324 }
325
326 protected function compileRoot($rootBlock)
327 {
328 $this->scope = $this->makeOutputBlock('root');
329
330 $this->compileChildren($rootBlock->children, $this->scope);
331 $this->flattenSelectors($this->scope);
332 }
333
334 protected function compileMedia($media) {
335 $this->pushEnv($media);
336
337 $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
338
339 if (!empty($mediaQuery)) {
340
341 $this->scope = $this->makeOutputBlock("media", array($mediaQuery));
342
343 $parentScope = $this->mediaParent($this->scope);
344
345 $parentScope->children[] = $this->scope;
346
347 // top level properties in a media cause it to be wrapped
348 $needsWrap = false;
349 foreach ($media->children as $child) {
350 $type = $child[0];
351 if ($type !== 'block' && $type !== 'media' && $type !== 'directive') {
352 $needsWrap = true;
353 break;
354 }
355 }
356
357 if ($needsWrap) {
358 $wrapped = (object)array(
359 "selectors" => array(),
360 "children" => $media->children
361 );
362 $media->children = array(array("block", $wrapped));
363 }
364
365 $this->compileChildren($media->children, $this->scope);
366
367 $this->scope = $this->scope->parent;
368 }
369
370 $this->popEnv();
371 }
372
373 protected function mediaParent($scope) {
374 while (!empty($scope->parent)) {
375 if (!empty($scope->type) && $scope->type != "media") {
376 break;
377 }
378 $scope = $scope->parent;
379 }
380
381 return $scope;
382 }
383
384 // TODO refactor compileNestedBlock and compileMedia into same thing
385 protected function compileNestedBlock($block, $selectors) {
386 $this->pushEnv($block);
387
388 $this->scope = $this->makeOutputBlock($block->type, $selectors);
389 $this->scope->parent->children[] = $this->scope;
390 $this->compileChildren($block->children, $this->scope);
391
392 $this->scope = $this->scope->parent;
393 $this->popEnv();
394 }
395
396 /**
397 * Recursively compiles a block.
398 *
399 * A block is analogous to a CSS block in most cases. A single SCSS document
400 * is encapsulated in a block when parsed, but it does not have parent tags
401 * so all of its children appear on the root level when compiled.
402 *
403 * Blocks are made up of selectors and children.
404 *
405 * The children of a block are just all the blocks that are defined within.
406 *
407 * Compiling the block involves pushing a fresh environment on the stack,
408 * and iterating through the props, compiling each one.
409 *
410 * @see scss::compileChild()
411 *
412 * @param \StdClass $block
413 */
414 protected function compileBlock($block) {
415 $env = $this->pushEnv($block);
416
417 $env->selectors =
418 array_map(array($this, "evalSelector"), $block->selectors);
419
420 $out = $this->makeOutputBlock(null, $this->multiplySelectors($env));
421 $this->scope->children[] = $out;
422 $this->compileChildren($block->children, $out);
423
424 $this->popEnv();
425 }
426
427 // joins together .classes and #ids
428 protected function flattenSelectorSingle($single) {
429 $joined = array();
430 foreach ($single as $part) {
431 if (empty($joined) ||
432 !is_string($part) ||
433 preg_match('/[\[.:#%]/', $part))
434 {
435 $joined[] = $part;
436 continue;
437 }
438
439 if (is_array(end($joined))) {
440 $joined[] = $part;
441 } else {
442 $joined[count($joined) - 1] .= $part;
443 }
444 }
445
446 return $joined;
447 }
448
449 // replaces all the interpolates
450 protected function evalSelector($selector) {
451 return array_map(array($this, "evalSelectorPart"), $selector);
452 }
453
454 protected function evalSelectorPart($piece) {
455 foreach ($piece as &$p) {
456 if (!is_array($p)) continue;
457
458 switch ($p[0]) {
459 case "interpolate":
460 $p = $this->compileValue($p);
461 break;
462 case "string":
463 $p = $this->compileValue($p);
464 break;
465 }
466 }
467
468 return $this->flattenSelectorSingle($piece);
469 }
470
471 // compiles to string
472 // self(&) should have been replaced by now
473 protected function compileSelector($selector) {
474 if (!is_array($selector)) return $selector; // media and the like
475
476 return implode(" ", array_map(
477 array($this, "compileSelectorPart"), $selector));
478 }
479
480 protected function compileSelectorPart($piece) {
481 foreach ($piece as &$p) {
482 if (!is_array($p)) continue;
483
484 switch ($p[0]) {
485 case "self":
486 $p = "&";
487 break;
488 default:
489 $p = $this->compileValue($p);
490 break;
491 }
492 }
493
494 return implode($piece);
495 }
496
497 protected function hasSelectorPlaceholder($selector)
498 {
499 if (!is_array($selector)) return false;
500
501 foreach ($selector as $parts) {
502 foreach ($parts as $part) {
503 if ('%' == $part[0]) {
504 return true;
505 }
506 }
507 }
508
509 return false;
510 }
511
512 protected function compileChildren($stms, $out) {
513 foreach ($stms as $stm) {
514 $ret = $this->compileChild($stm, $out);
515 if (isset($ret)) return $ret;
516 }
517 }
518
519 protected function compileMediaQuery($queryList) {
520 $out = "@media";
521 $first = true;
522 foreach ($queryList as $query){
523 $type = null;
524 $parts = array();
525 foreach ($query as $q) {
526 switch ($q[0]) {
527 case "mediaType":
528 if ($type) {
529 $type = $this->mergeMediaTypes($type, array_map(array($this, "compileValue"), array_slice($q, 1)));
530 if (empty($type)) { // merge failed
531 return null;
532 }
533 } else {
534 $type = array_map(array($this, "compileValue"), array_slice($q, 1));
535 }
536 break;
537 case "mediaExp":
538 if (isset($q[2])) {
539 $parts[] = "(". $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")";
540 } else {
541 $parts[] = "(" . $this->compileValue($q[1]) . ")";
542 }
543 break;
544 }
545 }
546 if ($type) {
547 array_unshift($parts, implode(' ', array_filter($type)));
548 }
549 if (!empty($parts)) {
550 if ($first) {
551 $first = false;
552 $out .= " ";
553 } else {
554 $out .= $this->formatter->tagSeparator;
555 }
556 $out .= implode(" and ", $parts);
557 }
558 }
559 return $out;
560 }
561
562 protected function mergeMediaTypes($type1, $type2) {
563 if (empty($type1)) {
564 return $type2;
565 }
566 if (empty($type2)) {
567 return $type1;
568 }
569 $m1 = '';
570 $t1 = '';
571 if (count($type1) > 1) {
572 $m1= strtolower($type1[0]);
573 $t1= strtolower($type1[1]);
574 } else {
575 $t1 = strtolower($type1[0]);
576 }
577 $m2 = '';
578 $t2 = '';
579 if (count($type2) > 1) {
580 $m2 = strtolower($type2[0]);
581 $t2 = strtolower($type2[1]);
582 } else {
583 $t2 = strtolower($type2[0]);
584 }
585 if (($m1 == 'not') ^ ($m2 == 'not')) {
586 if ($t1 == $t2) {
587 return null;
588 }
589 return array(
590 $m1 == 'not' ? $m2 : $m1,
591 $m1 == 'not' ? $t2 : $t1
592 );
593 } elseif ($m1 == 'not' && $m2 == 'not') {
594 # CSS has no way of representing "neither screen nor print"
595 if ($t1 != $t2) {
596 return null;
597 }
598 return array('not', $t1);
599 } elseif ($t1 != $t2) {
600 return null;
601 } else { // t1 == t2, neither m1 nor m2 are "not"
602 return array(empty($m1)? $m2 : $m1, $t1);
603 }
604 }
605
606 // returns true if the value was something that could be imported
607 protected function compileImport($rawPath, $out) {
608 if ($rawPath[0] == "string") {
609 $path = $this->compileStringContent($rawPath);
610 if ($path = $this->findImport($path)) {
611 $this->importFile($path, $out);
612 return true;
613 }
614 return false;
615 }
616 if ($rawPath[0] == "list") {
617 // handle a list of strings
618 if (count($rawPath[2]) == 0) return false;
619 foreach ($rawPath[2] as $path) {
620 if ($path[0] != "string") return false;
621 }
622
623 foreach ($rawPath[2] as $path) {
624 $this->compileImport($path, $out);
625 }
626
627 return true;
628 }
629
630 return false;
631 }
632
633 // return a value to halt execution
634 protected function compileChild($child, $out) {
635 $this->sourcePos = isset($child[-1]) ? $child[-1] : -1;
636 $this->sourceParser = isset($child[-2]) ? $child[-2] : $this->parser;
637
638 switch ($child[0]) {
639 case "import":
640 list(,$rawPath) = $child;
641 $rawPath = $this->reduce($rawPath);
642 if (!$this->compileImport($rawPath, $out)) {
643 $out->lines[] = "@import " . $this->compileValue($rawPath) . ";";
644 }
645 break;
646 case "directive":
647 list(, $directive) = $child;
648 $s = "@" . $directive->name;
649 if (!empty($directive->value)) {
650 $s .= " " . $this->compileValue($directive->value);
651 }
652 $this->compileNestedBlock($directive, array($s));
653 break;
654 case "media":
655 $this->compileMedia($child[1]);
656 break;
657 case "block":
658 $this->compileBlock($child[1]);
659 break;
660 case "charset":
661 $out->lines[] = "@charset ".$this->compileValue($child[1]).";";
662 break;
663 case "assign":
664 list(,$name, $value) = $child;
665 if ($name[0] == "var") {
666 $isDefault = !empty($child[3]);
667
668 if ($isDefault) {
669 $existingValue = $this->get($name[1], true);
670 $shouldSet = $existingValue === true || $existingValue == self::$null;
671 }
672
673 if (!$isDefault || $shouldSet) {
674 $this->set($name[1], $this->reduce($value));
675 }
676 break;
677 }
678
679 // if the value reduces to null from something else then
680 // the property should be discarded
681 if ($value[0] != "null") {
682 $value = $this->reduce($value);
683 if ($value[0] == "null") {
684 break;
685 }
686 }
687
688 $compiledValue = $this->compileValue($value);
689 $out->lines[] = $this->formatter->property(
690 $this->compileValue($name),
691 $compiledValue);
692 break;
693 case "comment":
694 $out->lines[] = $child[1];
695 break;
696 case "mixin":
697 case "function":
698 list(,$block) = $child;
699 $this->set(self::$namespaces[$block->type] . $block->name, $block);
700 break;
701 case "extend":
702 list(, $selectors) = $child;
703 foreach ($selectors as $sel) {
704 // only use the first one
705 $sel = current($this->evalSelector($sel));
706 $this->pushExtends($sel, $out->selectors);
707 }
708 break;
709 case "if":
710 list(, $if) = $child;
711 if ($this->isTruthy($this->reduce($if->cond, true))) {
712 return $this->compileChildren($if->children, $out);
713 } else {
714 foreach ($if->cases as $case) {
715 if ($case->type == "else" ||
716 $case->type == "elseif" && $this->isTruthy($this->reduce($case->cond)))
717 {
718 return $this->compileChildren($case->children, $out);
719 }
720 }
721 }
722 break;
723 case "return":
724 return $this->reduce($child[1], true);
725 case "each":
726 list(,$each) = $child;
727 $list = $this->coerceList($this->reduce($each->list));
728 foreach ($list[2] as $item) {
729 $this->pushEnv();
730 $this->set($each->var, $item);
731 // TODO: allow return from here
732 $this->compileChildren($each->children, $out);
733 $this->popEnv();
734 }
735 break;
736 case "while":
737 list(,$while) = $child;
738 while ($this->isTruthy($this->reduce($while->cond, true))) {
739 $ret = $this->compileChildren($while->children, $out);
740 if ($ret) return $ret;
741 }
742 break;
743 case "for":
744 list(,$for) = $child;
745 $start = $this->reduce($for->start, true);
746 $start = $start[1];
747 $end = $this->reduce($for->end, true);
748 $end = $end[1];
749 $d = $start < $end ? 1 : -1;
750
751 while (true) {
752 if ((!$for->until && $start - $d == $end) ||
753 ($for->until && $start == $end))
754 {
755 break;
756 }
757
758 $this->set($for->var, array("number", $start, ""));
759 $start += $d;
760
761 $ret = $this->compileChildren($for->children, $out);
762 if ($ret) return $ret;
763 }
764
765 break;
766 case "nestedprop":
767 list(,$prop) = $child;
768 $prefixed = array();
769 $prefix = $this->compileValue($prop->prefix) . "-";
770 foreach ($prop->children as $child) {
771 if ($child[0] == "assign") {
772 array_unshift($child[1][2], $prefix);
773 }
774 if ($child[0] == "nestedprop") {
775 array_unshift($child[1]->prefix[2], $prefix);
776 }
777 $prefixed[] = $child;
778 }
779 $this->compileChildren($prefixed, $out);
780 break;
781 case "include": // including a mixin
782 list(,$name, $argValues, $content) = $child;
783 $mixin = $this->get(self::$namespaces["mixin"] . $name, false);
784 if (!$mixin) {
785 $this->throwError("Undefined mixin $name");
786 }
787
788 $callingScope = $this->env;
789
790 // push scope, apply args
791 $this->pushEnv();
792 if ($this->env->depth > 0) {
793 $this->env->depth--;
794 }
795
796 if (isset($content)) {
797 $content->scope = $callingScope;
798 $this->setRaw(self::$namespaces["special"] . "content", $content);
799 }
800
801 if (isset($mixin->args)) {
802 $this->applyArguments($mixin->args, $argValues);
803 }
804
805 foreach ($mixin->children as $child) {
806 $this->compileChild($child, $out);
807 }
808
809 $this->popEnv();
810
811 break;
812 case "mixin_content":
813 $content = $this->get(self::$namespaces["special"] . "content");
814 if (!isset($content)) {
815 $this->throwError("Expected @content inside of mixin");
816 }
817
818 $strongTypes = array('include', 'block', 'for', 'while');
819 foreach ($content->children as $child) {
820 $this->storeEnv = (in_array($child[0], $strongTypes))
821 ? null
822 : $content->scope;
823
824 $this->compileChild($child, $out);
825 }
826
827 unset($this->storeEnv);
828 break;
829 case "debug":
830 list(,$value, $pos) = $child;
831 $line = $this->parser->getLineNo($pos);
832 $value = $this->compileValue($this->reduce($value, true));
833 fwrite(STDERR, "Line $line DEBUG: $value\n");
834 break;
835 default:
836 $this->throwError("unknown child type: $child[0]");
837 }
838 }
839
840 protected function expToString($exp) {
841 list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
842 $content = array($this->reduce($left));
843 if ($whiteLeft) $content[] = " ";
844 $content[] = $op;
845 if ($whiteRight) $content[] = " ";
846 $content[] = $this->reduce($right);
847 return array("string", "", $content);
848 }
849
850 protected function isTruthy($value) {
851 return $value != self::$false && $value != self::$null;
852 }
853
854 // should $value cause its operand to eval
855 protected function shouldEval($value) {
856 switch ($value[0]) {
857 case "exp":
858 if ($value[1] == "/") {
859 return $this->shouldEval($value[2], $value[3]);
860 }
861 case "var":
862 case "fncall":
863 return true;
864 }
865 return false;
866 }
867
868 protected function reduce($value, $inExp = false) {
869 list($type) = $value;
870 switch ($type) {
871 case "exp":
872 list(, $op, $left, $right, $inParens) = $value;
873 $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
874
875 $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
876
877 $left = $this->reduce($left, true);
878 $right = $this->reduce($right, true);
879
880 // only do division in special cases
881 if ($opName == "div" && !$inParens && !$inExp) {
882 if ($left[0] != "color" && $right[0] != "color") {
883 return $this->expToString($value);
884 }
885 }
886
887 $left = $this->coerceForExpression($left);
888 $right = $this->coerceForExpression($right);
889
890 $ltype = $left[0];
891 $rtype = $right[0];
892
893 // this tries:
894 // 1. op_[op name]_[left type]_[right type]
895 // 2. op_[left type]_[right type] (passing the op as first arg
896 // 3. op_[op name]
897 $fn = "op_${opName}_${ltype}_${rtype}";
898 if (is_callable(array($this, $fn)) ||
899 (($fn = "op_${ltype}_${rtype}") &&
900 is_callable(array($this, $fn)) &&
901 $passOp = true) ||
902 (($fn = "op_${opName}") &&
903 is_callable(array($this, $fn)) &&
904 $genOp = true))
905 {
906 $unitChange = false;
907 if (!isset($genOp) &&
908 $left[0] == "number" && $right[0] == "number")
909 {
910 if ($opName == "mod" && $right[2] != "") {
911 $this->throwError("Cannot modulo by a number with units: $right[1]$right[2].");
912 }
913
914 $unitChange = true;
915 $emptyUnit = $left[2] == "" || $right[2] == "";
916 $targetUnit = "" != $left[2] ? $left[2] : $right[2];
917
918 if ($opName != "mul") {
919 $left[2] = "" != $left[2] ? $left[2] : $targetUnit;
920 $right[2] = "" != $right[2] ? $right[2] : $targetUnit;
921 }
922
923 if ($opName != "mod") {
924 $left = $this->normalizeNumber($left);
925 $right = $this->normalizeNumber($right);
926 }
927
928 if ($opName == "div" && !$emptyUnit && $left[2] == $right[2]) {
929 $targetUnit = "";
930 }
931
932 if ($opName == "mul") {
933 $left[2] = "" != $left[2] ? $left[2] : $right[2];
934 $right[2] = "" != $right[2] ? $right[2] : $left[2];
935 } elseif ($opName == "div" && $left[2] == $right[2]) {
936 $left[2] = "";
937 $right[2] = "";
938 }
939 }
940
941 $shouldEval = $inParens || $inExp;
942 if (isset($passOp)) {
943 $out = $this->$fn($op, $left, $right, $shouldEval);
944 } else {
945 $out = $this->$fn($left, $right, $shouldEval);
946 }
947
948 if (isset($out)) {
949 if ($unitChange && $out[0] == "number") {
950 $out = $this->coerceUnit($out, $targetUnit);
951 }
952 return $out;
953 }
954 }
955
956 return $this->expToString($value);
957 case "unary":
958 list(, $op, $exp, $inParens) = $value;
959 $inExp = $inExp || $this->shouldEval($exp);
960
961 $exp = $this->reduce($exp);
962 if ($exp[0] == "number") {
963 switch ($op) {
964 case "+":
965 return $exp;
966 case "-":
967 $exp[1] *= -1;
968 return $exp;
969 }
970 }
971
972 if ($op == "not") {
973 if ($inExp || $inParens) {
974 if ($exp == self::$false) {
975 return self::$true;
976 } else {
977 return self::$false;
978 }
979 } else {
980 $op = $op . " ";
981 }
982 }
983
984 return array("string", "", array($op, $exp));
985 case "var":
986 list(, $name) = $value;
987 return $this->reduce($this->get($name));
988 case "list":
989 foreach ($value[2] as &$item) {
990 $item = $this->reduce($item);
991 }
992 return $value;
993 case "string":
994 foreach ($value[2] as &$item) {
995 if (is_array($item)) {
996 $item = $this->reduce($item);
997 }
998 }
999 return $value;
1000 case "interpolate":
1001 $value[1] = $this->reduce($value[1]);
1002 return $value;
1003 case "fncall":
1004 list(,$name, $argValues) = $value;
1005
1006 // user defined function?
1007 $func = $this->get(self::$namespaces["function"] . $name, false);
1008 if ($func) {
1009 $this->pushEnv();
1010
1011 // set the args
1012 if (isset($func->args)) {
1013 $this->applyArguments($func->args, $argValues);
1014 }
1015
1016 // throw away lines and children
1017 $tmp = (object)array(
1018 "lines" => array(),
1019 "children" => array()
1020 );
1021 $ret = $this->compileChildren($func->children, $tmp);
1022 $this->popEnv();
1023
1024 return !isset($ret) ? self::$defaultValue : $ret;
1025 }
1026
1027 // built in function
1028 if ($this->callBuiltin($name, $argValues, $returnValue)) {
1029 return $returnValue;
1030 }
1031
1032 // need to flatten the arguments into a list
1033 $listArgs = array();
1034 foreach ((array)$argValues as $arg) {
1035 if (empty($arg[0])) {
1036 $listArgs[] = $this->reduce($arg[1]);
1037 }
1038 }
1039 return array("function", $name, array("list", ",", $listArgs));
1040 default:
1041 return $value;
1042 }
1043 }
1044
1045 public function normalizeValue($value) {
1046 $value = $this->coerceForExpression($this->reduce($value));
1047 list($type) = $value;
1048
1049 switch ($type) {
1050 case "list":
1051 $value = $this->extractInterpolation($value);
1052 if ($value[0] != "list") {
1053 return array("keyword", $this->compileValue($value));
1054 }
1055 foreach ($value[2] as $key => $item) {
1056 $value[2][$key] = $this->normalizeValue($item);
1057 }
1058 return $value;
1059 case "number":
1060 return $this->normalizeNumber($value);
1061 default:
1062 return $value;
1063 }
1064 }
1065
1066 // just does physical lengths for now
1067 protected function normalizeNumber($number) {
1068 list(, $value, $unit) = $number;
1069 if (isset(self::$unitTable["in"][$unit])) {
1070 $conv = self::$unitTable["in"][$unit];
1071 return array("number", $value / $conv, "in");
1072 }
1073 return $number;
1074 }
1075
1076 // $number should be normalized
1077 protected function coerceUnit($number, $unit) {
1078 list(, $value, $baseUnit) = $number;
1079 if (isset(self::$unitTable[$baseUnit][$unit])) {
1080 $value = $value * self::$unitTable[$baseUnit][$unit];
1081 }
1082
1083 return array("number", $value, $unit);
1084 }
1085
1086 protected function op_add_number_number($left, $right) {
1087 return array("number", $left[1] + $right[1], $left[2]);
1088 }
1089
1090 protected function op_mul_number_number($left, $right) {
1091 return array("number", $left[1] * $right[1], $left[2]);
1092 }
1093
1094 protected function op_sub_number_number($left, $right) {
1095 return array("number", $left[1] - $right[1], $left[2]);
1096 }
1097
1098 protected function op_div_number_number($left, $right) {
1099 return array("number", $left[1] / $right[1], $left[2]);
1100 }
1101
1102 protected function op_mod_number_number($left, $right) {
1103 return array("number", $left[1] % $right[1], $left[2]);
1104 }
1105
1106 // adding strings
1107 protected function op_add($left, $right) {
1108 if ($strLeft = $this->coerceString($left)) {
1109 if ($right[0] == "string") {
1110 $right[1] = "";
1111 }
1112 $strLeft[2][] = $right;
1113 return $strLeft;
1114 }
1115
1116 if ($strRight = $this->coerceString($right)) {
1117 if ($left[0] == "string") {
1118 $left[1] = "";
1119 }
1120 array_unshift($strRight[2], $left);
1121 return $strRight;
1122 }
1123 }
1124
1125 protected function op_and($left, $right, $shouldEval) {
1126 if (!$shouldEval) return;
1127 if ($left != self::$false) return $right;
1128 return $left;
1129 }
1130
1131 protected function op_or($left, $right, $shouldEval) {
1132 if (!$shouldEval) return;
1133 if ($left != self::$false) return $left;
1134 return $right;
1135 }
1136
1137 protected function op_color_color($op, $left, $right) {
1138 $out = array('color');
1139 foreach (range(1, 3) as $i) {
1140 $lval = isset($left[$i]) ? $left[$i] : 0;
1141 $rval = isset($right[$i]) ? $right[$i] : 0;
1142 switch ($op) {
1143 case '+':
1144 $out[] = $lval + $rval;
1145 break;
1146 case '-':
1147 $out[] = $lval - $rval;
1148 break;
1149 case '*':
1150 $out[] = $lval * $rval;
1151 break;
1152 case '%':
1153 $out[] = $lval % $rval;
1154 break;
1155 case '/':
1156 if ($rval == 0) {
1157 $this->throwError("color: Can't divide by zero");
1158 }
1159 $out[] = $lval / $rval;
1160 break;
1161 case "==":
1162 return $this->op_eq($left, $right);
1163 case "!=":
1164 return $this->op_neq($left, $right);
1165 default:
1166 $this->throwError("color: unknown op $op");
1167 }
1168 }
1169
1170 if (isset($left[4])) $out[4] = $left[4];
1171 elseif (isset($right[4])) $out[4] = $right[4];
1172
1173 return $this->fixColor($out);
1174 }
1175
1176 protected function op_color_number($op, $left, $right) {
1177 $value = $right[1];
1178 return $this->op_color_color($op, $left,
1179 array("color", $value, $value, $value));
1180 }
1181
1182 protected function op_number_color($op, $left, $right) {
1183 $value = $left[1];
1184 return $this->op_color_color($op,
1185 array("color", $value, $value, $value), $right);
1186 }
1187
1188 protected function op_eq($left, $right) {
1189 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
1190 $lStr[1] = "";
1191 $rStr[1] = "";
1192 return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr));
1193 }
1194
1195 return $this->toBool($left == $right);
1196 }
1197
1198 protected function op_neq($left, $right) {
1199 return $this->toBool($left != $right);
1200 }
1201
1202 protected function op_gte_number_number($left, $right) {
1203 return $this->toBool($left[1] >= $right[1]);
1204 }
1205
1206 protected function op_gt_number_number($left, $right) {
1207 return $this->toBool($left[1] > $right[1]);
1208 }
1209
1210 protected function op_lte_number_number($left, $right) {
1211 return $this->toBool($left[1] <= $right[1]);
1212 }
1213
1214 protected function op_lt_number_number($left, $right) {
1215 return $this->toBool($left[1] < $right[1]);
1216 }
1217
1218 public function toBool($thing) {
1219 return $thing ? self::$true : self::$false;
1220 }
1221
1222 /**
1223 * Compiles a primitive value into a CSS property value.
1224 *
1225 * Values in scssphp are typed by being wrapped in arrays, their format is
1226 * typically:
1227 *
1228 * array(type, contents [, additional_contents]*)
1229 *
1230 * The input is expected to be reduced. This function will not work on
1231 * things like expressions and variables.
1232 *
1233 * @param array $value
1234 */
1235 protected function compileValue($value) {
1236 $value = $this->reduce($value);
1237
1238 list($type) = $value;
1239 switch ($type) {
1240 case "keyword":
1241 return $value[1];
1242 case "color":
1243 // [1] - red component (either number for a %)
1244 // [2] - green component
1245 // [3] - blue component
1246 // [4] - optional alpha component
1247 list(, $r, $g, $b) = $value;
1248
1249 $r = round($r);
1250 $g = round($g);
1251 $b = round($b);
1252
1253 if (count($value) == 5 && $value[4] != 1) { // rgba
1254 return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')';
1255 }
1256
1257 $h = sprintf("#%02x%02x%02x", $r, $g, $b);
1258
1259 // Converting hex color to short notation (e.g. #003399 to #039)
1260 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
1261 $h = '#' . $h[1] . $h[3] . $h[5];
1262 }
1263
1264 return $h;
1265 case "number":
1266 return round($value[1], $this->numberPrecision) . $value[2];
1267 case "string":
1268 return $value[1] . $this->compileStringContent($value) . $value[1];
1269 case "function":
1270 $args = !empty($value[2]) ? $this->compileValue($value[2]) : "";
1271 return "$value[1]($args)";
1272 case "list":
1273 $value = $this->extractInterpolation($value);
1274 if ($value[0] != "list") return $this->compileValue($value);
1275
1276 list(, $delim, $items) = $value;
1277
1278 $filtered = array();
1279 foreach ($items as $item) {
1280 if ($item[0] == "null") continue;
1281 $filtered[] = $this->compileValue($item);
1282 }
1283
1284 return implode("$delim ", $filtered);
1285 case "interpolated": # node created by extractInterpolation
1286 list(, $interpolate, $left, $right) = $value;
1287 list(,, $whiteLeft, $whiteRight) = $interpolate;
1288
1289 $left = count($left[2]) > 0 ?
1290 $this->compileValue($left).$whiteLeft : "";
1291
1292 $right = count($right[2]) > 0 ?
1293 $whiteRight.$this->compileValue($right) : "";
1294
1295 return $left.$this->compileValue($interpolate).$right;
1296
1297 case "interpolate": # raw parse node
1298 list(, $exp) = $value;
1299
1300 // strip quotes if it's a string
1301 $reduced = $this->reduce($exp);
1302 switch ($reduced[0]) {
1303 case "string":
1304 $reduced = array("keyword",
1305 $this->compileStringContent($reduced));
1306 break;
1307 case "null":
1308 $reduced = array("keyword", "");
1309 }
1310
1311 return $this->compileValue($reduced);
1312 case "null":
1313 return "null";
1314 default:
1315 $this->throwError("unknown value type: $type");
1316 }
1317 }
1318
1319 protected function compileStringContent($string) {
1320 $parts = array();
1321 foreach ($string[2] as $part) {
1322 if (is_array($part)) {
1323 $parts[] = $this->compileValue($part);
1324 } else {
1325 $parts[] = $part;
1326 }
1327 }
1328
1329 return implode($parts);
1330 }
1331
1332 // doesn't need to be recursive, compileValue will handle that
1333 protected function extractInterpolation($list) {
1334 $items = $list[2];
1335 foreach ($items as $i => $item) {
1336 if ($item[0] == "interpolate") {
1337 $before = array("list", $list[1], array_slice($items, 0, $i));
1338 $after = array("list", $list[1], array_slice($items, $i + 1));
1339 return array("interpolated", $item, $before, $after);
1340 }
1341 }
1342 return $list;
1343 }
1344
1345 // find the final set of selectors
1346 protected function multiplySelectors($env) {
1347 $envs = array();
1348 while (null !== $env) {
1349 if (!empty($env->selectors)) {
1350 $envs[] = $env;
1351 }
1352 $env = $env->parent;
1353 };
1354
1355 $selectors = array();
1356 $parentSelectors = array(array());
1357 while ($env = array_pop($envs)) {
1358 $selectors = array();
1359 foreach ($env->selectors as $selector) {
1360 foreach ($parentSelectors as $parent) {
1361 $selectors[] = $this->joinSelectors($parent, $selector);
1362 }
1363 }
1364 $parentSelectors = $selectors;
1365 }
1366
1367 return $selectors;
1368 }
1369
1370 // looks for & to replace, or append parent before child
1371 protected function joinSelectors($parent, $child) {
1372 $setSelf = false;
1373 $out = array();
1374 foreach ($child as $part) {
1375 $newPart = array();
1376 foreach ($part as $p) {
1377 if ($p == self::$selfSelector) {
1378 $setSelf = true;
1379 foreach ($parent as $i => $parentPart) {
1380 if ($i > 0) {
1381 $out[] = $newPart;
1382 $newPart = array();
1383 }
1384
1385 foreach ($parentPart as $pp) {
1386 $newPart[] = $pp;
1387 }
1388 }
1389 } else {
1390 $newPart[] = $p;
1391 }
1392 }
1393
1394 $out[] = $newPart;
1395 }
1396
1397 return $setSelf ? $out : array_merge($parent, $child);
1398 }
1399
1400 protected function multiplyMedia($env, $childQueries = null) {
1401 if (!isset($env) ||
1402 !empty($env->block->type) && $env->block->type != "media")
1403 {
1404 return $childQueries;
1405 }
1406
1407 // plain old block, skip
1408 if (empty($env->block->type)) {
1409 return $this->multiplyMedia($env->parent, $childQueries);
1410 }
1411
1412 $parentQueries = $env->block->queryList;
1413 if ($childQueries == null) {
1414 $childQueries = $parentQueries;
1415 } else {
1416 $originalQueries = $childQueries;
1417 $childQueries = array();
1418
1419 foreach ($parentQueries as $parentQuery){
1420 foreach ($originalQueries as $childQuery) {
1421 $childQueries []= array_merge($parentQuery, $childQuery);
1422 }
1423 }
1424 }
1425
1426 return $this->multiplyMedia($env->parent, $childQueries);
1427 }
1428
1429 // convert something to list
1430 protected function coerceList($item, $delim = ",") {
1431 if (isset($item) && $item[0] == "list") {
1432 return $item;
1433 }
1434
1435 return array("list", $delim, !isset($item) ? array(): array($item));
1436 }
1437
1438 protected function applyArguments($argDef, $argValues) {
1439 $hasVariable = false;
1440 $args = array();
1441 foreach ($argDef as $i => $arg) {
1442 list($name, $default, $isVariable) = $argDef[$i];
1443 $args[$name] = array($i, $name, $default, $isVariable);
1444 $hasVariable |= $isVariable;
1445 }
1446
1447 $keywordArgs = array();
1448 $deferredKeywordArgs = array();
1449 $remaining = array();
1450 // assign the keyword args
1451 foreach ((array) $argValues as $arg) {
1452 if (!empty($arg[0])) {
1453 if (!isset($args[$arg[0][1]])) {
1454 if ($hasVariable) {
1455 $deferredKeywordArgs[$arg[0][1]] = $arg[1];
1456 } else {
1457 $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
1458 }
1459 } elseif ($args[$arg[0][1]][0] < count($remaining)) {
1460 $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
1461 } else {
1462 $keywordArgs[$arg[0][1]] = $arg[1];
1463 }
1464 } elseif (count($keywordArgs)) {
1465 $this->throwError('Positional arguments must come before keyword arguments.');
1466 } elseif ($arg[2] == true) {
1467 $val = $this->reduce($arg[1], true);
1468 if ($val[0] == "list") {
1469 foreach ($val[2] as $name => $item) {
1470 if (!is_numeric($name)) {
1471 $keywordArgs[$name] = $item;
1472 } else {
1473 $remaining[] = $item;
1474 }
1475 }
1476 } else {
1477 $remaining[] = $val;
1478 }
1479 } else {
1480 $remaining[] = $arg[1];
1481 }
1482 }
1483
1484 foreach ($args as $arg) {
1485 list($i, $name, $default, $isVariable) = $arg;
1486 if ($isVariable) {
1487 $val = array("list", ",", array());
1488 for ($count = count($remaining); $i < $count; $i++) {
1489 $val[2][] = $remaining[$i];
1490 }
1491 foreach ($deferredKeywordArgs as $itemName => $item) {
1492 $val[2][$itemName] = $item;
1493 }
1494 } elseif (isset($remaining[$i])) {
1495 $val = $remaining[$i];
1496 } elseif (isset($keywordArgs[$name])) {
1497 $val = $keywordArgs[$name];
1498 } elseif (!empty($default)) {
1499 $val = $default;
1500 } else {
1501 $this->throwError("Missing argument $name");
1502 }
1503
1504 $this->set($name, $this->reduce($val, true), true);
1505 }
1506 }
1507
1508 protected function pushEnv($block=null) {
1509 $env = new stdClass;
1510 $env->parent = $this->env;
1511 $env->store = array();
1512 $env->block = $block;
1513 $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
1514
1515 $this->env = $env;
1516 return $env;
1517 }
1518
1519 protected function normalizeName($name) {
1520 return str_replace("-", "_", $name);
1521 }
1522
1523 protected function getStoreEnv() {
1524 return isset($this->storeEnv) ? $this->storeEnv : $this->env;
1525 }
1526
1527 protected function set($name, $value, $shadow=false) {
1528 $name = $this->normalizeName($name);
1529
1530 if ($shadow) {
1531 $this->setRaw($name, $value);
1532 } else {
1533 $this->setExisting($name, $value);
1534 }
1535 }
1536
1537 protected function setExisting($name, $value, $env = null) {
1538 if (!isset($env)) $env = $this->getStoreEnv();
1539
1540 if (isset($env->store[$name]) || !isset($env->parent)) {
1541 $env->store[$name] = $value;
1542 } else {
1543 $this->setExisting($name, $value, $env->parent);
1544 }
1545 }
1546
1547 protected function setRaw($name, $value) {
1548 $env = $this->getStoreEnv();
1549 $env->store[$name] = $value;
1550 }
1551
1552 public function get($name, $defaultValue = null, $env = null) {
1553 $name = $this->normalizeName($name);
1554
1555 if (!isset($env)) $env = $this->getStoreEnv();
1556 if (!isset($defaultValue)) $defaultValue = self::$defaultValue;
1557
1558 if (isset($env->store[$name])) {
1559 return $env->store[$name];
1560 } elseif (isset($env->parent)) {
1561 return $this->get($name, $defaultValue, $env->parent);
1562 }
1563
1564 return $defaultValue; // found nothing
1565 }
1566
1567 protected function injectVariables(array $args)
1568 {
1569 if (empty($args)) {
1570 return;
1571 }
1572
1573 $parser = new scss_parser(__METHOD__, false);
1574
1575 foreach ($args as $name => $strValue) {
1576 if ($name[0] === '$') {
1577 $name = substr($name, 1);
1578 }
1579
1580 $parser->env = null;
1581 $parser->count = 0;
1582 $parser->buffer = (string) $strValue;
1583 $parser->inParens = false;
1584 $parser->eatWhiteDefault = true;
1585 $parser->insertComments = true;
1586
1587 if ( ! $parser->valueList($value)) {
1588 throw new Exception("failed to parse passed in variable $name: $strValue");
1589 }
1590
1591 $this->set($name, $value);
1592 }
1593 }
1594
1595 /**
1596 * Set variables
1597 *
1598 * @param array $variables
1599 */
1600 public function setVariables(array $variables)
1601 {
1602 $this->registeredVars = array_merge($this->registeredVars, $variables);
1603 }
1604
1605 /**
1606 * Unset variable
1607 *
1608 * @param string $name
1609 */
1610 public function unsetVariable($name)
1611 {
1612 unset($this->registeredVars[$name]);
1613 }
1614
1615 protected function popEnv() {
1616 $env = $this->env;
1617 $this->env = $this->env->parent;
1618 return $env;
1619 }
1620
1621 public function getParsedFiles() {
1622 return $this->parsedFiles;
1623 }
1624
1625 public function addImportPath($path) {
1626 $this->importPaths[] = $path;
1627 }
1628
1629 public function setImportPaths($path) {
1630 $this->importPaths = (array)$path;
1631 }
1632
1633 public function setNumberPrecision($numberPrecision) {
1634 $this->numberPrecision = $numberPrecision;
1635 }
1636
1637 public function setFormatter($formatterName) {
1638 $this->formatter = $formatterName;
1639 }
1640
1641 public function registerFunction($name, $func) {
1642 $this->userFunctions[$this->normalizeName($name)] = $func;
1643 }
1644
1645 public function unregisterFunction($name) {
1646 unset($this->userFunctions[$this->normalizeName($name)]);
1647 }
1648
1649 protected function importFile($path, $out) {
1650 // see if tree is cached
1651 $realPath = realpath($path);
1652 if (isset($this->importCache[$realPath])) {
1653 $tree = $this->importCache[$realPath];
1654 } else {
1655 $code = file_get_contents($path);
1656 $parser = new scss_parser($path, false);
1657 $tree = $parser->parse($code);
1658 $this->parsedFiles[] = $path;
1659
1660 $this->importCache[$realPath] = $tree;
1661 }
1662
1663 $pi = pathinfo($path);
1664 array_unshift($this->importPaths, $pi['dirname']);
1665 $this->compileChildren($tree->children, $out);
1666 array_shift($this->importPaths);
1667 }
1668
1669 // results the file path for an import url if it exists
1670 public function findImport($url) {
1671 $urls = array();
1672
1673 // for "normal" scss imports (ignore vanilla css and external requests)
1674 if (!preg_match('/\.css|^http:\/\/$/', $url)) {
1675 // try both normal and the _partial filename
1676 $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
1677 }
1678
1679 foreach ($this->importPaths as $dir) {
1680 if (is_string($dir)) {
1681 // check urls for normal import paths
1682 foreach ($urls as $full) {
1683 $full = $dir .
1684 (!empty($dir) && substr($dir, -1) != '/' ? '/' : '') .
1685 $full;
1686
1687 if ($this->fileExists($file = $full.'.scss') ||
1688 $this->fileExists($file = $full))
1689 {
1690 return $file;
1691 }
1692 }
1693 } else {
1694 // check custom callback for import path
1695 $file = call_user_func($dir,$url,$this);
1696 if ($file !== null) {
1697 return $file;
1698 }
1699 }
1700 }
1701
1702 return null;
1703 }
1704
1705 protected function fileExists($name) {
1706 return is_file($name);
1707 }
1708
1709 protected function callBuiltin($name, $args, &$returnValue) {
1710 // try a lib function
1711 $name = $this->normalizeName($name);
1712 $libName = "lib_".$name;
1713 $f = array($this, $libName);
1714 if (is_callable($f)) {
1715 $prototype = isset(self::$$libName) ? self::$$libName : null;
1716 $sorted = $this->sortArgs($prototype, $args);
1717 foreach ($sorted as &$val) {
1718 $val = $this->reduce($val, true);
1719 }
1720 $returnValue = call_user_func($f, $sorted, $this);
1721 } elseif (isset($this->userFunctions[$name])) {
1722 // see if we can find a user function
1723 $fn = $this->userFunctions[$name];
1724
1725 foreach ($args as &$val) {
1726 $val = $this->reduce($val[1], true);
1727 }
1728
1729 $returnValue = call_user_func($fn, $args, $this);
1730 }
1731
1732 if (isset($returnValue)) {
1733 // coerce a php value into a scss one
1734 if (is_numeric($returnValue)) {
1735 $returnValue = array('number', $returnValue, "");
1736 } elseif (is_bool($returnValue)) {
1737 $returnValue = $returnValue ? self::$true : self::$false;
1738 } elseif (!is_array($returnValue)) {
1739 $returnValue = array('keyword', $returnValue);
1740 }
1741
1742 return true;
1743 }
1744
1745 return false;
1746 }
1747
1748 // sorts any keyword arguments
1749 // TODO: merge with apply arguments
1750 protected function sortArgs($prototype, $args) {
1751 $keyArgs = array();
1752 $posArgs = array();
1753
1754 foreach ($args as $arg) {
1755 list($key, $value) = $arg;
1756 $key = $key[1];
1757 if (empty($key)) {
1758 $posArgs[] = $value;
1759 } else {
1760 $keyArgs[$key] = $value;
1761 }
1762 }
1763
1764 if (!isset($prototype)) return $posArgs;
1765
1766 $finalArgs = array();
1767 foreach ($prototype as $i => $names) {
1768 if (isset($posArgs[$i])) {
1769 $finalArgs[] = $posArgs[$i];
1770 continue;
1771 }
1772
1773 $set = false;
1774 foreach ((array)$names as $name) {
1775 if (isset($keyArgs[$name])) {
1776 $finalArgs[] = $keyArgs[$name];
1777 $set = true;
1778 break;
1779 }
1780 }
1781
1782 if (!$set) {
1783 $finalArgs[] = null;
1784 }
1785 }
1786
1787 return $finalArgs;
1788 }
1789
1790 protected function coerceForExpression($value) {
1791 if ($color = $this->coerceColor($value)) {
1792 return $color;
1793 }
1794
1795 return $value;
1796 }
1797
1798 protected function coerceColor($value) {
1799 switch ($value[0]) {
1800 case "color": return $value;
1801 case "keyword":
1802 $name = $value[1];
1803 if (isset(self::$cssColors[$name])) {
1804 $rgba = explode(',', self::$cssColors[$name]);
1805 return isset($rgba[3])
1806 ? array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3])
1807 : array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
1808 }
1809 return null;
1810 }
1811
1812 return null;
1813 }
1814
1815 protected function coerceString($value) {
1816 switch ($value[0]) {
1817 case "string":
1818 return $value;
1819 case "keyword":
1820 return array("string", "", array($value[1]));
1821 }
1822 return null;
1823 }
1824
1825 public function assertList($value) {
1826 if ($value[0] != "list")
1827 $this->throwError("expecting list");
1828 return $value;
1829 }
1830
1831 public function assertColor($value) {
1832 if ($color = $this->coerceColor($value)) return $color;
1833 $this->throwError("expecting color");
1834 }
1835
1836 public function assertNumber($value) {
1837 if ($value[0] != "number")
1838 $this->throwError("expecting number");
1839 return $value[1];
1840 }
1841
1842 protected function coercePercent($value) {
1843 if ($value[0] == "number") {
1844 if ($value[2] == "%") {
1845 return $value[1] / 100;
1846 }
1847 return $value[1];
1848 }
1849 return 0;
1850 }
1851
1852 // make sure a color's components don't go out of bounds
1853 protected function fixColor($c) {
1854 foreach (range(1, 3) as $i) {
1855 if ($c[$i] < 0) $c[$i] = 0;
1856 if ($c[$i] > 255) $c[$i] = 255;
1857 }
1858
1859 return $c;
1860 }
1861
1862 public function toHSL($red, $green, $blue) {
1863 $min = min($red, $green, $blue);
1864 $max = max($red, $green, $blue);
1865
1866 $l = $min + $max;
1867
1868 if ($min == $max) {
1869 $s = $h = 0;
1870 } else {
1871 $d = $max - $min;
1872
1873 if ($l < 255)
1874 $s = $d / $l;
1875 else
1876 $s = $d / (510 - $l);
1877
1878 if ($red == $max)
1879 $h = 60 * ($green - $blue) / $d;
1880 elseif ($green == $max)
1881 $h = 60 * ($blue - $red) / $d + 120;
1882 elseif ($blue == $max)
1883 $h = 60 * ($red - $green) / $d + 240;
1884 }
1885
1886 return array('hsl', fmod($h, 360), $s * 100, $l / 5.1);
1887 }
1888
1889 public function hueToRGB($m1, $m2, $h) {
1890 if ($h < 0)
1891 $h += 1;
1892 elseif ($h > 1)
1893 $h -= 1;
1894
1895 if ($h * 6 < 1)
1896 return $m1 + ($m2 - $m1) * $h * 6;
1897
1898 if ($h * 2 < 1)
1899 return $m2;
1900
1901 if ($h * 3 < 2)
1902 return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
1903
1904 return $m1;
1905 }
1906
1907 // H from 0 to 360, S and L from 0 to 100
1908 public function toRGB($hue, $saturation, $lightness) {
1909 if ($hue < 0) {
1910 $hue += 360;
1911 }
1912
1913 $h = $hue / 360;
1914 $s = min(100, max(0, $saturation)) / 100;
1915 $l = min(100, max(0, $lightness)) / 100;
1916
1917 $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
1918 $m1 = $l * 2 - $m2;
1919
1920 $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
1921 $g = $this->hueToRGB($m1, $m2, $h) * 255;
1922 $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
1923
1924 $out = array('color', $r, $g, $b);
1925 return $out;
1926 }
1927
1928 // Built in functions
1929
1930 protected static $lib_if = array("condition", "if-true", "if-false");
1931 protected function lib_if($args) {
1932 list($cond,$t, $f) = $args;
1933 if (!$this->isTruthy($cond)) return $f;
1934 return $t;
1935 }
1936
1937 protected static $lib_index = array("list", "value");
1938 protected function lib_index($args) {
1939 list($list, $value) = $args;
1940 $list = $this->assertList($list);
1941
1942 $values = array();
1943 foreach ($list[2] as $item) {
1944 $values[] = $this->normalizeValue($item);
1945 }
1946 $key = array_search($this->normalizeValue($value), $values);
1947
1948 return false === $key ? false : $key + 1;
1949 }
1950
1951 protected static $lib_rgb = array("red", "green", "blue");
1952 protected function lib_rgb($args) {
1953 list($r,$g,$b) = $args;
1954 return array("color", $r[1], $g[1], $b[1]);
1955 }
1956
1957 protected static $lib_rgba = array(
1958 array("red", "color"),
1959 "green", "blue", "alpha");
1960 protected function lib_rgba($args) {
1961 if ($color = $this->coerceColor($args[0])) {
1962 $num = !isset($args[1]) ? $args[3] : $args[1];
1963 $alpha = $this->assertNumber($num);
1964 $color[4] = $alpha;
1965 return $color;
1966 }
1967
1968 list($r,$g,$b, $a) = $args;
1969 return array("color", $r[1], $g[1], $b[1], $a[1]);
1970 }
1971
1972 // helper function for adjust_color, change_color, and scale_color
1973 protected function alter_color($args, $fn) {
1974 $color = $this->assertColor($args[0]);
1975
1976 foreach (array(1,2,3,7) as $i) {
1977 if (isset($args[$i])) {
1978 $val = $this->assertNumber($args[$i]);
1979 $ii = $i == 7 ? 4 : $i; // alpha
1980 $color[$ii] =
1981 $this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
1982 }
1983 }
1984
1985 if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
1986 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
1987 foreach (array(4,5,6) as $i) {
1988 if (isset($args[$i])) {
1989 $val = $this->assertNumber($args[$i]);
1990 $hsl[$i - 3] = $this->$fn($hsl[$i - 3], $val, $i);
1991 }
1992 }
1993
1994 $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
1995 if (isset($color[4])) $rgb[4] = $color[4];
1996 $color = $rgb;
1997 }
1998
1999 return $color;
2000 }
2001
2002 protected static $lib_adjust_color = array(
2003 "color", "red", "green", "blue",
2004 "hue", "saturation", "lightness", "alpha"
2005 );
2006 protected function adjust_color_helper($base, $alter, $i) {
2007 return $base += $alter;
2008 }
2009 protected function lib_adjust_color($args) {
2010 return $this->alter_color($args, "adjust_color_helper");
2011 }
2012
2013 protected static $lib_change_color = array(
2014 "color", "red", "green", "blue",
2015 "hue", "saturation", "lightness", "alpha"
2016 );
2017 protected function change_color_helper($base, $alter, $i) {
2018 return $alter;
2019 }
2020 protected function lib_change_color($args) {
2021 return $this->alter_color($args, "change_color_helper");
2022 }
2023
2024 protected static $lib_scale_color = array(
2025 "color", "red", "green", "blue",
2026 "hue", "saturation", "lightness", "alpha"
2027 );
2028 protected function scale_color_helper($base, $scale, $i) {
2029 // 1,2,3 - rgb
2030 // 4, 5, 6 - hsl
2031 // 7 - a
2032 switch ($i) {
2033 case 1:
2034 case 2:
2035 case 3:
2036 $max = 255; break;
2037 case 4:
2038 $max = 360; break;
2039 case 7:
2040 $max = 1; break;
2041 default:
2042 $max = 100;
2043 }
2044
2045 $scale = $scale / 100;
2046 if ($scale < 0) {
2047 return $base * $scale + $base;
2048 } else {
2049 return ($max - $base) * $scale + $base;
2050 }
2051 }
2052 protected function lib_scale_color($args) {
2053 return $this->alter_color($args, "scale_color_helper");
2054 }
2055
2056 protected static $lib_ie_hex_str = array("color");
2057 protected function lib_ie_hex_str($args) {
2058 $color = $this->coerceColor($args[0]);
2059 $color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
2060
2061 return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
2062 }
2063
2064 protected static $lib_red = array("color");
2065 protected function lib_red($args) {
2066 $color = $this->coerceColor($args[0]);
2067 return $color[1];
2068 }
2069
2070 protected static $lib_green = array("color");
2071 protected function lib_green($args) {
2072 $color = $this->coerceColor($args[0]);
2073 return $color[2];
2074 }
2075
2076 protected static $lib_blue = array("color");
2077 protected function lib_blue($args) {
2078 $color = $this->coerceColor($args[0]);
2079 return $color[3];
2080 }
2081
2082 protected static $lib_alpha = array("color");
2083 protected function lib_alpha($args) {
2084 if ($color = $this->coerceColor($args[0])) {
2085 return isset($color[4]) ? $color[4] : 1;
2086 }
2087
2088 // this might be the IE function, so return value unchanged
2089 return null;
2090 }
2091
2092 protected static $lib_opacity = array("color");
2093 protected function lib_opacity($args) {
2094 $value = $args[0];
2095 if ($value[0] === 'number') return null;
2096 return $this->lib_alpha($args);
2097 }
2098
2099 // mix two colors
2100 protected static $lib_mix = array("color-1", "color-2", "weight");
2101 protected function lib_mix($args) {
2102 list($first, $second, $weight) = $args;
2103 $first = $this->assertColor($first);
2104 $second = $this->assertColor($second);
2105
2106 if (!isset($weight)) {
2107 $weight = 0.5;
2108 } else {
2109 $weight = $this->coercePercent($weight);
2110 }
2111
2112 $firstAlpha = isset($first[4]) ? $first[4] : 1;
2113 $secondAlpha = isset($second[4]) ? $second[4] : 1;
2114
2115 $w = $weight * 2 - 1;
2116 $a = $firstAlpha - $secondAlpha;
2117
2118 $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
2119 $w2 = 1.0 - $w1;
2120
2121 $new = array('color',
2122 $w1 * $first[1] + $w2 * $second[1],
2123 $w1 * $first[2] + $w2 * $second[2],
2124 $w1 * $first[3] + $w2 * $second[3],
2125 );
2126
2127 if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
2128 $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
2129 }
2130
2131 return $this->fixColor($new);
2132 }
2133
2134 protected static $lib_hsl = array("hue", "saturation", "lightness");
2135 protected function lib_hsl($args) {
2136 list($h, $s, $l) = $args;
2137 return $this->toRGB($h[1], $s[1], $l[1]);
2138 }
2139
2140 protected static $lib_hsla = array("hue", "saturation",
2141 "lightness", "alpha");
2142 protected function lib_hsla($args) {
2143 list($h, $s, $l, $a) = $args;
2144 $color = $this->toRGB($h[1], $s[1], $l[1]);
2145 $color[4] = $a[1];
2146 return $color;
2147 }
2148
2149 protected static $lib_hue = array("color");
2150 protected function lib_hue($args) {
2151 $color = $this->assertColor($args[0]);
2152 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2153 return array("number", $hsl[1], "deg");
2154 }
2155
2156 protected static $lib_saturation = array("color");
2157 protected function lib_saturation($args) {
2158 $color = $this->assertColor($args[0]);
2159 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2160 return array("number", $hsl[2], "%");
2161 }
2162
2163 protected static $lib_lightness = array("color");
2164 protected function lib_lightness($args) {
2165 $color = $this->assertColor($args[0]);
2166 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2167 return array("number", $hsl[3], "%");
2168 }
2169
2170 protected function adjustHsl($color, $idx, $amount) {
2171 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2172 $hsl[$idx] += $amount;
2173 $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
2174 if (isset($color[4])) $out[4] = $color[4];
2175 return $out;
2176 }
2177
2178 protected static $lib_adjust_hue = array("color", "degrees");
2179 protected function lib_adjust_hue($args) {
2180 $color = $this->assertColor($args[0]);
2181 $degrees = $this->assertNumber($args[1]);
2182 return $this->adjustHsl($color, 1, $degrees);
2183 }
2184
2185 protected static $lib_lighten = array("color", "amount");
2186 protected function lib_lighten($args) {
2187 $color = $this->assertColor($args[0]);
2188 $amount = 100*$this->coercePercent($args[1]);
2189 return $this->adjustHsl($color, 3, $amount);
2190 }
2191
2192 protected static $lib_darken = array("color", "amount");
2193 protected function lib_darken($args) {
2194 $color = $this->assertColor($args[0]);
2195 $amount = 100*$this->coercePercent($args[1]);
2196 return $this->adjustHsl($color, 3, -$amount);
2197 }
2198
2199 protected static $lib_saturate = array("color", "amount");
2200 protected function lib_saturate($args) {
2201 $value = $args[0];
2202 if ($value[0] === 'number') return null;
2203 $color = $this->assertColor($value);
2204 $amount = 100*$this->coercePercent($args[1]);
2205 return $this->adjustHsl($color, 2, $amount);
2206 }
2207
2208 protected static $lib_desaturate = array("color", "amount");
2209 protected function lib_desaturate($args) {
2210 $color = $this->assertColor($args[0]);
2211 $amount = 100*$this->coercePercent($args[1]);
2212 return $this->adjustHsl($color, 2, -$amount);
2213 }
2214
2215 protected static $lib_grayscale = array("color");
2216 protected function lib_grayscale($args) {
2217 $value = $args[0];
2218 if ($value[0] === 'number') return null;
2219 return $this->adjustHsl($this->assertColor($value), 2, -100);
2220 }
2221
2222 protected static $lib_complement = array("color");
2223 protected function lib_complement($args) {
2224 return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
2225 }
2226
2227 protected static $lib_invert = array("color");
2228 protected function lib_invert($args) {
2229 $value = $args[0];
2230 if ($value[0] === 'number') return null;
2231 $color = $this->assertColor($value);
2232 $color[1] = 255 - $color[1];
2233 $color[2] = 255 - $color[2];
2234 $color[3] = 255 - $color[3];
2235 return $color;
2236 }
2237
2238 // increases opacity by amount
2239 protected static $lib_opacify = array("color", "amount");
2240 protected function lib_opacify($args) {
2241 $color = $this->assertColor($args[0]);
2242 $amount = $this->coercePercent($args[1]);
2243
2244 $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
2245 $color[4] = min(1, max(0, $color[4]));
2246 return $color;
2247 }
2248
2249 protected static $lib_fade_in = array("color", "amount");
2250 protected function lib_fade_in($args) {
2251 return $this->lib_opacify($args);
2252 }
2253
2254 // decreases opacity by amount
2255 protected static $lib_transparentize = array("color", "amount");
2256 protected function lib_transparentize($args) {
2257 $color = $this->assertColor($args[0]);
2258 $amount = $this->coercePercent($args[1]);
2259
2260 $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
2261 $color[4] = min(1, max(0, $color[4]));
2262 return $color;
2263 }
2264
2265 protected static $lib_fade_out = array("color", "amount");
2266 protected function lib_fade_out($args) {
2267 return $this->lib_transparentize($args);
2268 }
2269
2270 protected static $lib_unquote = array("string");
2271 protected function lib_unquote($args) {
2272 $str = $args[0];
2273 if ($str[0] == "string") $str[1] = "";
2274 return $str;
2275 }
2276
2277 protected static $lib_quote = array("string");
2278 protected function lib_quote($args) {
2279 $value = $args[0];
2280 if ($value[0] == "string" && !empty($value[1]))
2281 return $value;
2282 return array("string", '"', array($value));
2283 }
2284
2285 protected static $lib_percentage = array("value");
2286 protected function lib_percentage($args) {
2287 return array("number",
2288 $this->coercePercent($args[0]) * 100,
2289 "%");
2290 }
2291
2292 protected static $lib_round = array("value");
2293 protected function lib_round($args) {
2294 $num = $args[0];
2295 $num[1] = round($num[1]);
2296 return $num;
2297 }
2298
2299 protected static $lib_floor = array("value");
2300 protected function lib_floor($args) {
2301 $num = $args[0];
2302 $num[1] = floor($num[1]);
2303 return $num;
2304 }
2305
2306 protected static $lib_ceil = array("value");
2307 protected function lib_ceil($args) {
2308 $num = $args[0];
2309 $num[1] = ceil($num[1]);
2310 return $num;
2311 }
2312
2313 protected static $lib_abs = array("value");
2314 protected function lib_abs($args) {
2315 $num = $args[0];
2316 $num[1] = abs($num[1]);
2317 return $num;
2318 }
2319
2320 protected function lib_min($args) {
2321 $numbers = $this->getNormalizedNumbers($args);
2322 $min = null;
2323 foreach ($numbers as $key => $number) {
2324 if (null === $min || $number[1] <= $min[1]) {
2325 $min = array($key, $number[1]);
2326 }
2327 }
2328
2329 return $args[$min[0]];
2330 }
2331
2332 protected function lib_max($args) {
2333 $numbers = $this->getNormalizedNumbers($args);
2334 $max = null;
2335 foreach ($numbers as $key => $number) {
2336 if (null === $max || $number[1] >= $max[1]) {
2337 $max = array($key, $number[1]);
2338 }
2339 }
2340
2341 return $args[$max[0]];
2342 }
2343
2344 protected function getNormalizedNumbers($args) {
2345 $unit = null;
2346 $originalUnit = null;
2347 $numbers = array();
2348 foreach ($args as $key => $item) {
2349 if ('number' != $item[0]) {
2350 $this->throwError("%s is not a number", $item[0]);
2351 }
2352 $number = $this->normalizeNumber($item);
2353
2354 if (null === $unit) {
2355 $unit = $number[2];
2356 $originalUnit = $item[2];
2357 } elseif ($unit !== $number[2]) {
2358 $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]);
2359 }
2360
2361 $numbers[$key] = $number;
2362 }
2363
2364 return $numbers;
2365 }
2366
2367 protected static $lib_length = array("list");
2368 protected function lib_length($args) {
2369 $list = $this->coerceList($args[0]);
2370 return count($list[2]);
2371 }
2372
2373 protected static $lib_nth = array("list", "n");
2374 protected function lib_nth($args) {
2375 $list = $this->coerceList($args[0]);
2376 $n = $this->assertNumber($args[1]) - 1;
2377 return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
2378 }
2379
2380 protected function listSeparatorForJoin($list1, $sep) {
2381 if (!isset($sep)) return $list1[1];
2382 switch ($this->compileValue($sep)) {
2383 case "comma":
2384 return ",";
2385 case "space":
2386 return "";
2387 default:
2388 return $list1[1];
2389 }
2390 }
2391
2392 protected static $lib_join = array("list1", "list2", "separator");
2393 protected function lib_join($args) {
2394 list($list1, $list2, $sep) = $args;
2395 $list1 = $this->coerceList($list1, " ");
2396 $list2 = $this->coerceList($list2, " ");
2397 $sep = $this->listSeparatorForJoin($list1, $sep);
2398 return array("list", $sep, array_merge($list1[2], $list2[2]));
2399 }
2400
2401 protected static $lib_append = array("list", "val", "separator");
2402 protected function lib_append($args) {
2403 list($list1, $value, $sep) = $args;
2404 $list1 = $this->coerceList($list1, " ");
2405 $sep = $this->listSeparatorForJoin($list1, $sep);
2406 return array("list", $sep, array_merge($list1[2], array($value)));
2407 }
2408
2409 protected function lib_zip($args) {
2410 foreach ($args as $arg) {
2411 $this->assertList($arg);
2412 }
2413
2414 $lists = array();
2415 $firstList = array_shift($args);
2416 foreach ($firstList[2] as $key => $item) {
2417 $list = array("list", "", array($item));
2418 foreach ($args as $arg) {
2419 if (isset($arg[2][$key])) {
2420 $list[2][] = $arg[2][$key];
2421 } else {
2422 break 2;
2423 }
2424 }
2425 $lists[] = $list;
2426 }
2427
2428 return array("list", ",", $lists);
2429 }
2430
2431 protected static $lib_type_of = array("value");
2432 protected function lib_type_of($args) {
2433 $value = $args[0];
2434 switch ($value[0]) {
2435 case "keyword":
2436 if ($value == self::$true || $value == self::$false) {
2437 return "bool";
2438 }
2439
2440 if ($this->coerceColor($value)) {
2441 return "color";
2442 }
2443
2444 return "string";
2445 default:
2446 return $value[0];
2447 }
2448 }
2449
2450 protected static $lib_unit = array("number");
2451 protected function lib_unit($args) {
2452 $num = $args[0];
2453 if ($num[0] == "number") {
2454 return array("string", '"', array($num[2]));
2455 }
2456 return "";
2457 }
2458
2459 protected static $lib_unitless = array("number");
2460 protected function lib_unitless($args) {
2461 $value = $args[0];
2462 return $value[0] == "number" && empty($value[2]);
2463 }
2464
2465 protected static $lib_comparable = array("number-1", "number-2");
2466 protected function lib_comparable($args) {
2467 list($number1, $number2) = $args;
2468 if (!isset($number1[0]) || $number1[0] != "number" || !isset($number2[0]) || $number2[0] != "number") {
2469 $this->throwError('Invalid argument(s) for "comparable"');
2470 }
2471
2472 $number1 = $this->normalizeNumber($number1);
2473 $number2 = $this->normalizeNumber($number2);
2474
2475 return $number1[2] == $number2[2] || $number1[2] == "" || $number2[2] == "";
2476 }
2477
2478 /**
2479 * Workaround IE7's content counter bug.
2480 *
2481 * @param array $args
2482 */
2483 protected function lib_counter($args) {
2484 $list = array_map(array($this, 'compileValue'), $args);
2485 return array('string', '', array('counter(' . implode(',', $list) . ')'));
2486 }
2487
2488 public function throwError($msg = null) {
2489 if (func_num_args() > 1) {
2490 $msg = call_user_func_array("sprintf", func_get_args());
2491 }
2492
2493 if ($this->sourcePos >= 0 && isset($this->sourceParser)) {
2494 $this->sourceParser->throwParseError($msg, $this->sourcePos);
2495 }
2496
2497 throw new Exception($msg);
2498 }
2499
2500 /**
2501 * CSS Colors
2502 *
2503 * @see http://www.w3.org/TR/css3-color
2504 */
2505 static protected $cssColors = array(
2506 'aliceblue' => '240,248,255',
2507 'antiquewhite' => '250,235,215',
2508 'aqua' => '0,255,255',
2509 'aquamarine' => '127,255,212',
2510 'azure' => '240,255,255',
2511 'beige' => '245,245,220',
2512 'bisque' => '255,228,196',
2513 'black' => '0,0,0',
2514 'blanchedalmond' => '255,235,205',
2515 'blue' => '0,0,255',
2516 'blueviolet' => '138,43,226',
2517 'brown' => '165,42,42',
2518 'burlywood' => '222,184,135',
2519 'cadetblue' => '95,158,160',
2520 'chartreuse' => '127,255,0',
2521 'chocolate' => '210,105,30',
2522 'coral' => '255,127,80',
2523 'cornflowerblue' => '100,149,237',
2524 'cornsilk' => '255,248,220',
2525 'crimson' => '220,20,60',
2526 'cyan' => '0,255,255',
2527 'darkblue' => '0,0,139',
2528 'darkcyan' => '0,139,139',
2529 'darkgoldenrod' => '184,134,11',
2530 'darkgray' => '169,169,169',
2531 'darkgreen' => '0,100,0',
2532 'darkgrey' => '169,169,169',
2533 'darkkhaki' => '189,183,107',
2534 'darkmagenta' => '139,0,139',
2535 'darkolivegreen' => '85,107,47',
2536 'darkorange' => '255,140,0',
2537 'darkorchid' => '153,50,204',
2538 'darkred' => '139,0,0',
2539 'darksalmon' => '233,150,122',
2540 'darkseagreen' => '143,188,143',
2541 'darkslateblue' => '72,61,139',
2542 'darkslategray' => '47,79,79',
2543 'darkslategrey' => '47,79,79',
2544 'darkturquoise' => '0,206,209',
2545 'darkviolet' => '148,0,211',
2546 'deeppink' => '255,20,147',
2547 'deepskyblue' => '0,191,255',
2548 'dimgray' => '105,105,105',
2549 'dimgrey' => '105,105,105',
2550 'dodgerblue' => '30,144,255',
2551 'firebrick' => '178,34,34',
2552 'floralwhite' => '255,250,240',
2553 'forestgreen' => '34,139,34',
2554 'fuchsia' => '255,0,255',
2555 'gainsboro' => '220,220,220',
2556 'ghostwhite' => '248,248,255',
2557 'gold' => '255,215,0',
2558 'goldenrod' => '218,165,32',
2559 'gray' => '128,128,128',
2560 'green' => '0,128,0',
2561 'greenyellow' => '173,255,47',
2562 'grey' => '128,128,128',
2563 'honeydew' => '240,255,240',
2564 'hotpink' => '255,105,180',
2565 'indianred' => '205,92,92',
2566 'indigo' => '75,0,130',
2567 'ivory' => '255,255,240',
2568 'khaki' => '240,230,140',
2569 'lavender' => '230,230,250',
2570 'lavenderblush' => '255,240,245',
2571 'lawngreen' => '124,252,0',
2572 'lemonchiffon' => '255,250,205',
2573 'lightblue' => '173,216,230',
2574 'lightcoral' => '240,128,128',
2575 'lightcyan' => '224,255,255',
2576 'lightgoldenrodyellow' => '250,250,210',
2577 'lightgray' => '211,211,211',
2578 'lightgreen' => '144,238,144',
2579 'lightgrey' => '211,211,211',
2580 'lightpink' => '255,182,193',
2581 'lightsalmon' => '255,160,122',
2582 'lightseagreen' => '32,178,170',
2583 'lightskyblue' => '135,206,250',
2584 'lightslategray' => '119,136,153',
2585 'lightslategrey' => '119,136,153',
2586 'lightsteelblue' => '176,196,222',
2587 'lightyellow' => '255,255,224',
2588 'lime' => '0,255,0',
2589 'limegreen' => '50,205,50',
2590 'linen' => '250,240,230',
2591 'magenta' => '255,0,255',
2592 'maroon' => '128,0,0',
2593 'mediumaquamarine' => '102,205,170',
2594 'mediumblue' => '0,0,205',
2595 'mediumorchid' => '186,85,211',
2596 'mediumpurple' => '147,112,219',
2597 'mediumseagreen' => '60,179,113',
2598 'mediumslateblue' => '123,104,238',
2599 'mediumspringgreen' => '0,250,154',
2600 'mediumturquoise' => '72,209,204',
2601 'mediumvioletred' => '199,21,133',
2602 'midnightblue' => '25,25,112',
2603 'mintcream' => '245,255,250',
2604 'mistyrose' => '255,228,225',
2605 'moccasin' => '255,228,181',
2606 'navajowhite' => '255,222,173',
2607 'navy' => '0,0,128',
2608 'oldlace' => '253,245,230',
2609 'olive' => '128,128,0',
2610 'olivedrab' => '107,142,35',
2611 'orange' => '255,165,0',
2612 'orangered' => '255,69,0',
2613 'orchid' => '218,112,214',
2614 'palegoldenrod' => '238,232,170',
2615 'palegreen' => '152,251,152',
2616 'paleturquoise' => '175,238,238',
2617 'palevioletred' => '219,112,147',
2618 'papayawhip' => '255,239,213',
2619 'peachpuff' => '255,218,185',
2620 'peru' => '205,133,63',
2621 'pink' => '255,192,203',
2622 'plum' => '221,160,221',
2623 'powderblue' => '176,224,230',
2624 'purple' => '128,0,128',
2625 'red' => '255,0,0',
2626 'rosybrown' => '188,143,143',
2627 'royalblue' => '65,105,225',
2628 'saddlebrown' => '139,69,19',
2629 'salmon' => '250,128,114',
2630 'sandybrown' => '244,164,96',
2631 'seagreen' => '46,139,87',
2632 'seashell' => '255,245,238',
2633 'sienna' => '160,82,45',
2634 'silver' => '192,192,192',
2635 'skyblue' => '135,206,235',
2636 'slateblue' => '106,90,205',
2637 'slategray' => '112,128,144',
2638 'slategrey' => '112,128,144',
2639 'snow' => '255,250,250',
2640 'springgreen' => '0,255,127',
2641 'steelblue' => '70,130,180',
2642 'tan' => '210,180,140',
2643 'teal' => '0,128,128',
2644 'thistle' => '216,191,216',
2645 'tomato' => '255,99,71',
2646 'transparent' => '0,0,0,0',
2647 'turquoise' => '64,224,208',
2648 'violet' => '238,130,238',
2649 'wheat' => '245,222,179',
2650 'white' => '255,255,255',
2651 'whitesmoke' => '245,245,245',
2652 'yellow' => '255,255,0',
2653 'yellowgreen' => '154,205,50'
2654 );
2655 }
2656
2657 /**
2658 * SCSS parser
2659 *
2660 * @author Leaf Corcoran <leafot@gmail.com>
2661 */
2662 class scss_parser {
2663 static protected $precedence = array(
2664 "or" => 0,
2665 "and" => 1,
2666
2667 '==' => 2,
2668 '!=' => 2,
2669 '<=' => 2,
2670 '>=' => 2,
2671 '=' => 2,
2672 '<' => 3,
2673 '>' => 2,
2674
2675 '+' => 3,
2676 '-' => 3,
2677 '*' => 4,
2678 '/' => 4,
2679 '%' => 4,
2680 );
2681
2682 static protected $operators = array("+", "-", "*", "/", "%",
2683 "==", "!=", "<=", ">=", "<", ">", "and", "or");
2684
2685 static protected $operatorStr;
2686 static protected $whitePattern;
2687 static protected $commentMulti;
2688
2689 static protected $commentSingle = "//";
2690 static protected $commentMultiLeft = "/*";
2691 static protected $commentMultiRight = "*/";
2692
2693 /**
2694 * Constructor
2695 *
2696 * @param string $sourceName
2697 * @param boolean $rootParser
2698 */
2699 public function __construct($sourceName = null, $rootParser = true) {
2700 $this->sourceName = $sourceName;
2701 $this->rootParser = $rootParser;
2702
2703 if (empty(self::$operatorStr)) {
2704 self::$operatorStr = $this->makeOperatorStr(self::$operators);
2705
2706 $commentSingle = $this->preg_quote(self::$commentSingle);
2707 $commentMultiLeft = $this->preg_quote(self::$commentMultiLeft);
2708 $commentMultiRight = $this->preg_quote(self::$commentMultiRight);
2709 self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
2710 self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
2711 }
2712 }
2713
2714 static protected function makeOperatorStr($operators) {
2715 return '('.implode('|', array_map(array('scss_parser','preg_quote'),
2716 $operators)).')';
2717 }
2718
2719 /**
2720 * Parser buffer
2721 *
2722 * @param string $buffer;
2723 *
2724 * @return \StdClass
2725 */
2726 public function parse($buffer)
2727 {
2728 $this->count = 0;
2729 $this->env = null;
2730 $this->inParens = false;
2731 $this->eatWhiteDefault = true;
2732 $this->insertComments = true;
2733 $this->buffer = $buffer;
2734
2735 $this->pushBlock(null); // root block
2736 $this->whitespace();
2737
2738 while (false !== $this->parseChunk())
2739 ;
2740
2741 if ($this->count != strlen($this->buffer)) {
2742 $this->throwParseError();
2743 }
2744
2745 if (!empty($this->env->parent)) {
2746 $this->throwParseError("unclosed block");
2747 }
2748
2749 $this->env->isRoot = true;
2750
2751 return $this->env;
2752 }
2753
2754 /**
2755 * Parse a single chunk off the head of the buffer and append it to the
2756 * current parse environment.
2757 *
2758 * Returns false when the buffer is empty, or when there is an error.
2759 *
2760 * This function is called repeatedly until the entire document is
2761 * parsed.
2762 *
2763 * This parser is most similar to a recursive descent parser. Single
2764 * functions represent discrete grammatical rules for the language, and
2765 * they are able to capture the text that represents those rules.
2766 *
2767 * Consider the function scssc::keyword(). (All parse functions are
2768 * structured the same.)
2769 *
2770 * The function takes a single reference argument. When calling the
2771 * function it will attempt to match a keyword on the head of the buffer.
2772 * If it is successful, it will place the keyword in the referenced
2773 * argument, advance the position in the buffer, and return true. If it
2774 * fails then it won't advance the buffer and it will return false.
2775 *
2776 * All of these parse functions are powered by scssc::match(), which behaves
2777 * the same way, but takes a literal regular expression. Sometimes it is
2778 * more convenient to use match instead of creating a new function.
2779 *
2780 * Because of the format of the functions, to parse an entire string of
2781 * grammatical rules, you can chain them together using &&.
2782 *
2783 * But, if some of the rules in the chain succeed before one fails, then
2784 * the buffer position will be left at an invalid state. In order to
2785 * avoid this, scssc::seek() is used to remember and set buffer positions.
2786 *
2787 * Before parsing a chain, use $s = $this->seek() to remember the current
2788 * position into $s. Then if a chain fails, use $this->seek($s) to
2789 * go back where we started.
2790 *
2791 * @return boolean
2792 */
2793 protected function parseChunk() {
2794 $s = $this->seek();
2795
2796 // the directives
2797 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
2798 if ($this->literal("@media") && $this->mediaQueryList($mediaQueryList) && $this->literal("{")) {
2799 $media = $this->pushSpecialBlock("media");
2800 $media->queryList = $mediaQueryList[2];
2801 return true;
2802 } else {
2803 $this->seek($s);
2804 }
2805
2806 if ($this->literal("@mixin") &&
2807 $this->keyword($mixinName) &&
2808 ($this->argumentDef($args) || true) &&
2809 $this->literal("{"))
2810 {
2811 $mixin = $this->pushSpecialBlock("mixin");
2812 $mixin->name = $mixinName;
2813 $mixin->args = $args;
2814 return true;
2815 } else {
2816 $this->seek($s);
2817 }
2818
2819 if ($this->literal("@include") &&
2820 $this->keyword($mixinName) &&
2821 ($this->literal("(") &&
2822 ($this->argValues($argValues) || true) &&
2823 $this->literal(")") || true) &&
2824 ($this->end() ||
2825 $this->literal("{") && $hasBlock = true))
2826 {
2827 $child = array("include",
2828 $mixinName, isset($argValues) ? $argValues : null, null);
2829
2830 if (!empty($hasBlock)) {
2831 $include = $this->pushSpecialBlock("include");
2832 $include->child = $child;
2833 } else {
2834 $this->append($child, $s);
2835 }
2836
2837 return true;
2838 } else {
2839 $this->seek($s);
2840 }
2841
2842 if ($this->literal("@import") &&
2843 $this->valueList($importPath) &&
2844 $this->end())
2845 {
2846 $this->append(array("import", $importPath), $s);
2847 return true;
2848 } else {
2849 $this->seek($s);
2850 }
2851
2852 if ($this->literal("@extend") &&
2853 $this->selectors($selector) &&
2854 $this->end())
2855 {
2856 $this->append(array("extend", $selector), $s);
2857 return true;
2858 } else {
2859 $this->seek($s);
2860 }
2861
2862 if ($this->literal("@function") &&
2863 $this->keyword($fnName) &&
2864 $this->argumentDef($args) &&
2865 $this->literal("{"))
2866 {
2867 $func = $this->pushSpecialBlock("function");
2868 $func->name = $fnName;
2869 $func->args = $args;
2870 return true;
2871 } else {
2872 $this->seek($s);
2873 }
2874
2875 if ($this->literal("@return") && $this->valueList($retVal) && $this->end()) {
2876 $this->append(array("return", $retVal), $s);
2877 return true;
2878 } else {
2879 $this->seek($s);
2880 }
2881
2882 if ($this->literal("@each") &&
2883 $this->variable($varName) &&
2884 $this->literal("in") &&
2885 $this->valueList($list) &&
2886 $this->literal("{"))
2887 {
2888 $each = $this->pushSpecialBlock("each");
2889 $each->var = $varName[1];
2890 $each->list = $list;
2891 return true;
2892 } else {
2893 $this->seek($s);
2894 }
2895
2896 if ($this->literal("@while") &&
2897 $this->expression($cond) &&
2898 $this->literal("{"))
2899 {
2900 $while = $this->pushSpecialBlock("while");
2901 $while->cond = $cond;
2902 return true;
2903 } else {
2904 $this->seek($s);
2905 }
2906
2907 if ($this->literal("@for") &&
2908 $this->variable($varName) &&
2909 $this->literal("from") &&
2910 $this->expression($start) &&
2911 ($this->literal("through") ||
2912 ($forUntil = true && $this->literal("to"))) &&
2913 $this->expression($end) &&
2914 $this->literal("{"))
2915 {
2916 $for = $this->pushSpecialBlock("for");
2917 $for->var = $varName[1];
2918 $for->start = $start;
2919 $for->end = $end;
2920 $for->until = isset($forUntil);
2921 return true;
2922 } else {
2923 $this->seek($s);
2924 }
2925
2926 if ($this->literal("@if") && $this->valueList($cond) && $this->literal("{")) {
2927 $if = $this->pushSpecialBlock("if");
2928 $if->cond = $cond;
2929 $if->cases = array();
2930 return true;
2931 } else {
2932 $this->seek($s);
2933 }
2934
2935 if (($this->literal("@debug") || $this->literal("@warn")) &&
2936 $this->valueList($value) &&
2937 $this->end()) {
2938 $this->append(array("debug", $value, $s), $s);
2939 return true;
2940 } else {
2941 $this->seek($s);
2942 }
2943
2944 if ($this->literal("@content") && $this->end()) {
2945 $this->append(array("mixin_content"), $s);
2946 return true;
2947 } else {
2948 $this->seek($s);
2949 }
2950
2951 $last = $this->last();
2952 if (isset($last) && $last[0] == "if") {
2953 list(, $if) = $last;
2954 if ($this->literal("@else")) {
2955 if ($this->literal("{")) {
2956 $else = $this->pushSpecialBlock("else");
2957 } elseif ($this->literal("if") && $this->valueList($cond) && $this->literal("{")) {
2958 $else = $this->pushSpecialBlock("elseif");
2959 $else->cond = $cond;
2960 }
2961
2962 if (isset($else)) {
2963 $else->dontAppend = true;
2964 $if->cases[] = $else;
2965 return true;
2966 }
2967 }
2968
2969 $this->seek($s);
2970 }
2971
2972 if ($this->literal("@charset") &&
2973 $this->valueList($charset) && $this->end())
2974 {
2975 $this->append(array("charset", $charset), $s);
2976 return true;
2977 } else {
2978 $this->seek($s);
2979 }
2980
2981 // doesn't match built in directive, do generic one
2982 if ($this->literal("@", false) && $this->keyword($dirName) &&
2983 ($this->openString("{", $dirValue) || true) &&
2984 $this->literal("{"))
2985 {
2986 $directive = $this->pushSpecialBlock("directive");
2987 $directive->name = $dirName;
2988 if (isset($dirValue)) $directive->value = $dirValue;
2989 return true;
2990 }
2991
2992 $this->seek($s);
2993 return false;
2994 }
2995
2996 // property shortcut
2997 // captures most properties before having to parse a selector
2998 if ($this->keyword($name, false) &&
2999 $this->literal(": ") &&
3000 $this->valueList($value) &&
3001 $this->end())
3002 {
3003 $name = array("string", "", array($name));
3004 $this->append(array("assign", $name, $value), $s);
3005 return true;
3006 } else {
3007 $this->seek($s);
3008 }
3009
3010 // variable assigns
3011 if ($this->variable($name) &&
3012 $this->literal(":") &&
3013 $this->valueList($value) && $this->end())
3014 {
3015 // check for !default
3016 $defaultVar = $value[0] == "list" && $this->stripDefault($value);
3017 $this->append(array("assign", $name, $value, $defaultVar), $s);
3018 return true;
3019 } else {
3020 $this->seek($s);
3021 }
3022
3023 // misc
3024 if ($this->literal("-->")) {
3025 return true;
3026 }
3027
3028 // opening css block
3029 $oldComments = $this->insertComments;
3030 $this->insertComments = false;
3031 if ($this->selectors($selectors) && $this->literal("{")) {
3032 $this->pushBlock($selectors);
3033 $this->insertComments = $oldComments;
3034 return true;
3035 } else {
3036 $this->seek($s);
3037 }
3038 $this->insertComments = $oldComments;
3039
3040 // property assign, or nested assign
3041 if ($this->propertyName($name) && $this->literal(":")) {
3042 $foundSomething = false;
3043 if ($this->valueList($value)) {
3044 $this->append(array("assign", $name, $value), $s);
3045 $foundSomething = true;
3046 }
3047
3048 if ($this->literal("{")) {
3049 $propBlock = $this->pushSpecialBlock("nestedprop");
3050 $propBlock->prefix = $name;
3051 $foundSomething = true;
3052 } elseif ($foundSomething) {
3053 $foundSomething = $this->end();
3054 }
3055
3056 if ($foundSomething) {
3057 return true;
3058 }
3059
3060 $this->seek($s);
3061 } else {
3062 $this->seek($s);
3063 }
3064
3065 // closing a block
3066 if ($this->literal("}")) {
3067 $block = $this->popBlock();
3068 if (isset($block->type) && $block->type == "include") {
3069 $include = $block->child;
3070 unset($block->child);
3071 $include[3] = $block;
3072 $this->append($include, $s);
3073 } elseif (empty($block->dontAppend)) {
3074 $type = isset($block->type) ? $block->type : "block";
3075 $this->append(array($type, $block), $s);
3076 }
3077 return true;
3078 }
3079
3080 // extra stuff
3081 if ($this->literal(";") ||
3082 $this->literal("<!--"))
3083 {
3084 return true;
3085 }
3086
3087 return false;
3088 }
3089
3090 protected function stripDefault(&$value) {
3091 $def = end($value[2]);
3092 if ($def[0] == "keyword" && $def[1] == "!default") {
3093 array_pop($value[2]);
3094 $value = $this->flattenList($value);
3095 return true;
3096 }
3097
3098 if ($def[0] == "list") {
3099 return $this->stripDefault($value[2][count($value[2]) - 1]);
3100 }
3101
3102 return false;
3103 }
3104
3105 protected function literal($what, $eatWhitespace = null) {
3106 if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
3107
3108 // shortcut on single letter
3109 if (!isset($what[1]) && isset($this->buffer[$this->count])) {
3110 if ($this->buffer[$this->count] == $what) {
3111 if (!$eatWhitespace) {
3112 $this->count++;
3113 return true;
3114 }
3115 // goes below...
3116 } else {
3117 return false;
3118 }
3119 }
3120
3121 return $this->match($this->preg_quote($what), $m, $eatWhitespace);
3122 }
3123
3124 // tree builders
3125
3126 protected function pushBlock($selectors) {
3127 $b = new stdClass;
3128 $b->parent = $this->env; // not sure if we need this yet
3129
3130 $b->selectors = $selectors;
3131 $b->children = array();
3132
3133 $this->env = $b;
3134 return $b;
3135 }
3136
3137 protected function pushSpecialBlock($type) {
3138 $block = $this->pushBlock(null);
3139 $block->type = $type;
3140 return $block;
3141 }
3142
3143 protected function popBlock() {
3144 if (empty($this->env->parent)) {
3145 $this->throwParseError("unexpected }");
3146 }
3147
3148 $old = $this->env;
3149 $this->env = $this->env->parent;
3150 unset($old->parent);
3151 return $old;
3152 }
3153
3154 protected function append($statement, $pos=null) {
3155 if ($pos !== null) {
3156 $statement[-1] = $pos;
3157 if (!$this->rootParser) $statement[-2] = $this;
3158 }
3159 $this->env->children[] = $statement;
3160 }
3161
3162 // last child that was appended
3163 protected function last() {
3164 $i = count($this->env->children) - 1;
3165 if (isset($this->env->children[$i]))
3166 return $this->env->children[$i];
3167 }
3168
3169 // high level parsers (they return parts of ast)
3170
3171 protected function mediaQueryList(&$out) {
3172 return $this->genericList($out, "mediaQuery", ",", false);
3173 }
3174
3175 protected function mediaQuery(&$out) {
3176 $s = $this->seek();
3177
3178 $expressions = null;
3179 $parts = array();
3180
3181 if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->mixedKeyword($mediaType)) {
3182 $prop = array("mediaType");
3183 if (isset($only)) $prop[] = array("keyword", "only");
3184 if (isset($not)) $prop[] = array("keyword", "not");
3185 $media = array("list", "", array());
3186 foreach ((array)$mediaType as $type) {
3187 if (is_array($type)) {
3188 $media[2][] = $type;
3189 } else {
3190 $media[2][] = array("keyword", $type);
3191 }
3192 }
3193 $prop[] = $media;
3194 $parts[] = $prop;
3195 }
3196
3197 if (empty($parts) || $this->literal("and")) {
3198 $this->genericList($expressions, "mediaExpression", "and", false);
3199 if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
3200 }
3201
3202 $out = $parts;
3203 return true;
3204 }
3205
3206 protected function mediaExpression(&$out) {
3207 $s = $this->seek();
3208 $value = null;
3209 if ($this->literal("(") &&
3210 $this->expression($feature) &&
3211 ($this->literal(":") && $this->expression($value) || true) &&
3212 $this->literal(")"))
3213 {
3214 $out = array("mediaExp", $feature);
3215 if ($value) $out[] = $value;
3216 return true;
3217 }
3218
3219 $this->seek($s);
3220 return false;
3221 }
3222
3223 protected function argValues(&$out) {
3224 if ($this->genericList($list, "argValue", ",", false)) {
3225 $out = $list[2];
3226 return true;
3227 }
3228 return false;
3229 }
3230
3231 protected function argValue(&$out) {
3232 $s = $this->seek();
3233
3234 $keyword = null;
3235 if (!$this->variable($keyword) || !$this->literal(":")) {
3236 $this->seek($s);
3237 $keyword = null;
3238 }
3239
3240 if ($this->genericList($value, "expression")) {
3241 $out = array($keyword, $value, false);
3242 $s = $this->seek();
3243 if ($this->literal("...")) {
3244 $out[2] = true;
3245 } else {
3246 $this->seek($s);
3247 }
3248 return true;
3249 }
3250
3251 return false;
3252 }
3253
3254 /**
3255 * Parse list
3256 *
3257 * @param string $out
3258 *
3259 * @return boolean
3260 */
3261 public function valueList(&$out)
3262 {
3263 return $this->genericList($out, 'spaceList', ',');
3264 }
3265
3266 protected function spaceList(&$out)
3267 {
3268 return $this->genericList($out, 'expression');
3269 }
3270
3271 protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
3272 $s = $this->seek();
3273 $items = array();
3274 while ($this->$parseItem($value)) {
3275 $items[] = $value;
3276 if ($delim) {
3277 if (!$this->literal($delim)) break;
3278 }
3279 }
3280
3281 if (count($items) == 0) {
3282 $this->seek($s);
3283 return false;
3284 }
3285
3286 if ($flatten && count($items) == 1) {
3287 $out = $items[0];
3288 } else {
3289 $out = array("list", $delim, $items);
3290 }
3291
3292 return true;
3293 }
3294
3295 protected function expression(&$out) {
3296 $s = $this->seek();
3297
3298 if ($this->literal("(")) {
3299 if ($this->literal(")")) {
3300 $out = array("list", "", array());
3301 return true;
3302 }
3303
3304 if ($this->valueList($out) && $this->literal(')') && $out[0] == "list") {
3305 return true;
3306 }
3307
3308 $this->seek($s);
3309 }
3310
3311 if ($this->value($lhs)) {
3312 $out = $this->expHelper($lhs, 0);
3313 return true;
3314 }
3315
3316 return false;
3317 }
3318
3319 protected function expHelper($lhs, $minP) {
3320 $opstr = self::$operatorStr;
3321
3322 $ss = $this->seek();
3323 $whiteBefore = isset($this->buffer[$this->count - 1]) &&
3324 ctype_space($this->buffer[$this->count - 1]);
3325 while ($this->match($opstr, $m) && self::$precedence[$m[1]] >= $minP) {
3326 $whiteAfter = isset($this->buffer[$this->count - 1]) &&
3327 ctype_space($this->buffer[$this->count - 1]);
3328
3329 $op = $m[1];
3330
3331 // don't turn negative numbers into expressions
3332 if ($op == "-" && $whiteBefore) {
3333 if (!$whiteAfter) break;
3334 }
3335
3336 if (!$this->value($rhs)) break;
3337
3338 // peek and see if rhs belongs to next operator
3339 if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
3340 $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
3341 }
3342
3343 $lhs = array("exp", $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
3344 $ss = $this->seek();
3345 $whiteBefore = isset($this->buffer[$this->count - 1]) &&
3346 ctype_space($this->buffer[$this->count - 1]);
3347 }
3348
3349 $this->seek($ss);
3350 return $lhs;
3351 }
3352
3353 protected function value(&$out) {
3354 $s = $this->seek();
3355
3356 if ($this->literal("not", false) && $this->whitespace() && $this->value($inner)) {
3357 $out = array("unary", "not", $inner, $this->inParens);
3358 return true;
3359 } else {
3360 $this->seek($s);
3361 }
3362
3363 if ($this->literal("+") && $this->value($inner)) {
3364 $out = array("unary", "+", $inner, $this->inParens);
3365 return true;
3366 } else {
3367 $this->seek($s);
3368 }
3369
3370 // negation
3371 if ($this->literal("-", false) &&
3372 ($this->variable($inner) ||
3373 $this->unit($inner) ||
3374 $this->parenValue($inner)))
3375 {
3376 $out = array("unary", "-", $inner, $this->inParens);
3377 return true;
3378 } else {
3379 $this->seek($s);
3380 }
3381
3382 if ($this->parenValue($out)) return true;
3383 if ($this->interpolation($out)) return true;
3384 if ($this->variable($out)) return true;
3385 if ($this->color($out)) return true;
3386 if ($this->unit($out)) return true;
3387 if ($this->string($out)) return true;
3388 if ($this->func($out)) return true;
3389 if ($this->progid($out)) return true;
3390
3391 if ($this->keyword($keyword)) {
3392 if ($keyword == "null") {
3393 $out = array("null");
3394 } else {
3395 $out = array("keyword", $keyword);
3396 }
3397 return true;
3398 }
3399
3400 return false;
3401 }
3402
3403 // value wrappen in parentheses
3404 protected function parenValue(&$out) {
3405 $s = $this->seek();
3406
3407 $inParens = $this->inParens;
3408 if ($this->literal("(") &&
3409 ($this->inParens = true) && $this->expression($exp) &&
3410 $this->literal(")"))
3411 {
3412 $out = $exp;
3413 $this->inParens = $inParens;
3414 return true;
3415 } else {
3416 $this->inParens = $inParens;
3417 $this->seek($s);
3418 }
3419
3420 return false;
3421 }
3422
3423 protected function progid(&$out) {
3424 $s = $this->seek();
3425 if ($this->literal("progid:", false) &&
3426 $this->openString("(", $fn) &&
3427 $this->literal("("))
3428 {
3429 $this->openString(")", $args, "(");
3430 if ($this->literal(")")) {
3431 $out = array("string", "", array(
3432 "progid:", $fn, "(", $args, ")"
3433 ));
3434 return true;
3435 }
3436 }
3437
3438 $this->seek($s);
3439 return false;
3440 }
3441
3442 protected function func(&$func) {
3443 $s = $this->seek();
3444
3445 if ($this->keyword($name, false) &&
3446 $this->literal("("))
3447 {
3448 if ($name == "alpha" && $this->argumentList($args)) {
3449 $func = array("function", $name, array("string", "", $args));
3450 return true;
3451 }
3452
3453 if ($name != "expression" && !preg_match("/^(-[a-z]+-)?calc$/", $name)) {
3454 $ss = $this->seek();
3455 if ($this->argValues($args) && $this->literal(")")) {
3456 $func = array("fncall", $name, $args);
3457 return true;
3458 }
3459 $this->seek($ss);
3460 }
3461
3462 if (($this->openString(")", $str, "(") || true ) &&
3463 $this->literal(")"))
3464 {
3465 $args = array();
3466 if (!empty($str)) {
3467 $args[] = array(null, array("string", "", array($str)));
3468 }
3469
3470 $func = array("fncall", $name, $args);
3471 return true;
3472 }
3473 }
3474
3475 $this->seek($s);
3476 return false;
3477 }
3478
3479 protected function argumentList(&$out) {
3480 $s = $this->seek();
3481 $this->literal("(");
3482
3483 $args = array();
3484 while ($this->keyword($var)) {
3485 $ss = $this->seek();
3486
3487 if ($this->literal("=") && $this->expression($exp)) {
3488 $args[] = array("string", "", array($var."="));
3489 $arg = $exp;
3490 } else {
3491 break;
3492 }
3493
3494 $args[] = $arg;
3495
3496 if (!$this->literal(",")) break;
3497
3498 $args[] = array("string", "", array(", "));
3499 }
3500
3501 if (!$this->literal(")") || !count($args)) {
3502 $this->seek($s);
3503 return false;
3504 }
3505
3506 $out = $args;
3507 return true;
3508 }
3509
3510 protected function argumentDef(&$out) {
3511 $s = $this->seek();
3512 $this->literal("(");
3513
3514 $args = array();
3515 while ($this->variable($var)) {
3516 $arg = array($var[1], null, false);
3517
3518 $ss = $this->seek();
3519 if ($this->literal(":") && $this->genericList($defaultVal, "expression")) {
3520 $arg[1] = $defaultVal;
3521 } else {
3522 $this->seek($ss);
3523 }
3524
3525 $ss = $this->seek();
3526 if ($this->literal("...")) {
3527 $sss = $this->seek();
3528 if (!$this->literal(")")) {
3529 $this->throwParseError("... has to be after the final argument");
3530 }
3531 $arg[2] = true;
3532 $this->seek($sss);
3533 } else {
3534 $this->seek($ss);
3535 }
3536
3537 $args[] = $arg;
3538 if (!$this->literal(",")) break;
3539 }
3540
3541 if (!$this->literal(")")) {
3542 $this->seek($s);
3543 return false;
3544 }
3545
3546 $out = $args;
3547 return true;
3548 }
3549
3550 protected function color(&$out) {
3551 $color = array('color');
3552
3553 if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
3554 if (isset($m[3])) {
3555 $num = $m[3];
3556 $width = 16;
3557 } else {
3558 $num = $m[2];
3559 $width = 256;
3560 }
3561
3562 $num = hexdec($num);
3563 foreach (array(3,2,1) as $i) {
3564 $t = $num % $width;
3565 $num /= $width;
3566
3567 $color[$i] = $t * (256/$width) + $t * floor(16/$width);
3568 }
3569
3570 $out = $color;
3571 return true;
3572 }
3573
3574 return false;
3575 }
3576
3577 protected function unit(&$unit) {
3578 if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
3579 $unit = array("number", $m[1], empty($m[3]) ? "" : $m[3]);
3580 return true;
3581 }
3582 return false;
3583 }
3584
3585 protected function string(&$out) {
3586 $s = $this->seek();
3587 if ($this->literal('"', false)) {
3588 $delim = '"';
3589 } elseif ($this->literal("'", false)) {
3590 $delim = "'";
3591 } else {
3592 return false;
3593 }
3594
3595 $content = array();
3596 $oldWhite = $this->eatWhiteDefault;
3597 $this->eatWhiteDefault = false;
3598
3599 while ($this->matchString($m, $delim)) {
3600 $content[] = $m[1];
3601 if ($m[2] == "#{") {
3602 $this->count -= strlen($m[2]);
3603 if ($this->interpolation($inter, false)) {
3604 $content[] = $inter;
3605 } else {
3606 $this->count += strlen($m[2]);
3607 $content[] = "#{"; // ignore it
3608 }
3609 } elseif ($m[2] == '\\') {
3610 $content[] = $m[2];
3611 if ($this->literal($delim, false)) {
3612 $content[] = $delim;
3613 }
3614 } else {
3615 $this->count -= strlen($delim);
3616 break; // delim
3617 }
3618 }
3619
3620 $this->eatWhiteDefault = $oldWhite;
3621
3622 if ($this->literal($delim)) {
3623 $out = array("string", $delim, $content);
3624 return true;
3625 }
3626
3627 $this->seek($s);
3628 return false;
3629 }
3630
3631 protected function mixedKeyword(&$out) {
3632 $s = $this->seek();
3633
3634 $parts = array();
3635
3636 $oldWhite = $this->eatWhiteDefault;
3637 $this->eatWhiteDefault = false;
3638
3639 while (true) {
3640 if ($this->keyword($key)) {
3641 $parts[] = $key;
3642 continue;
3643 }
3644
3645 if ($this->interpolation($inter)) {
3646 $parts[] = $inter;
3647 continue;
3648 }
3649
3650 break;
3651 }
3652
3653 $this->eatWhiteDefault = $oldWhite;
3654
3655 if (count($parts) == 0) return false;
3656
3657 if ($this->eatWhiteDefault) {
3658 $this->whitespace();
3659 }
3660
3661 $out = $parts;
3662 return true;
3663 }
3664
3665 // an unbounded string stopped by $end
3666 protected function openString($end, &$out, $nestingOpen=null) {
3667 $oldWhite = $this->eatWhiteDefault;
3668 $this->eatWhiteDefault = false;
3669
3670 $stop = array("'", '"', "#{", $end);
3671 $stop = array_map(array($this, "preg_quote"), $stop);
3672 $stop[] = self::$commentMulti;
3673
3674 $patt = '(.*?)('.implode("|", $stop).')';
3675
3676 $nestingLevel = 0;
3677
3678 $content = array();
3679 while ($this->match($patt, $m, false)) {
3680 if (isset($m[1]) && $m[1] !== '') {
3681 $content[] = $m[1];
3682 if ($nestingOpen) {
3683 $nestingLevel += substr_count($m[1], $nestingOpen);
3684 }
3685 }
3686
3687 $tok = $m[2];
3688
3689 $this->count-= strlen($tok);
3690 if ($tok == $end) {
3691 if ($nestingLevel == 0) {
3692 break;
3693 } else {
3694 $nestingLevel--;
3695 }
3696 }
3697
3698 if (($tok == "'" || $tok == '"') && $this->string($str)) {
3699 $content[] = $str;
3700 continue;
3701 }
3702
3703 if ($tok == "#{" && $this->interpolation($inter)) {
3704 $content[] = $inter;
3705 continue;
3706 }
3707
3708 $content[] = $tok;
3709 $this->count+= strlen($tok);
3710 }
3711
3712 $this->eatWhiteDefault = $oldWhite;
3713
3714 if (count($content) == 0) return false;
3715
3716 // trim the end
3717 if (is_string(end($content))) {
3718 $content[count($content) - 1] = rtrim(end($content));
3719 }
3720
3721 $out = array("string", "", $content);
3722 return true;
3723 }
3724
3725 // $lookWhite: save information about whitespace before and after
3726 protected function interpolation(&$out, $lookWhite=true) {
3727 $oldWhite = $this->eatWhiteDefault;
3728 $this->eatWhiteDefault = true;
3729
3730 $s = $this->seek();
3731 if ($this->literal("#{") && $this->valueList($value) && $this->literal("}", false)) {
3732
3733 // TODO: don't error if out of bounds
3734
3735 if ($lookWhite) {
3736 $left = preg_match('/\s/', $this->buffer[$s - 1]) ? " " : "";
3737 $right = preg_match('/\s/', $this->buffer[$this->count]) ? " ": "";
3738 } else {
3739 $left = $right = false;
3740 }
3741
3742 $out = array("interpolate", $value, $left, $right);
3743 $this->eatWhiteDefault = $oldWhite;
3744 if ($this->eatWhiteDefault) $this->whitespace();
3745 return true;
3746 }
3747
3748 $this->seek($s);
3749 $this->eatWhiteDefault = $oldWhite;
3750 return false;
3751 }
3752
3753 // low level parsers
3754
3755 // returns an array of parts or a string
3756 protected function propertyName(&$out) {
3757 $s = $this->seek();
3758 $parts = array();
3759
3760 $oldWhite = $this->eatWhiteDefault;
3761 $this->eatWhiteDefault = false;
3762
3763 while (true) {
3764 if ($this->interpolation($inter)) {
3765 $parts[] = $inter;
3766 } elseif ($this->keyword($text)) {
3767 $parts[] = $text;
3768 } elseif (count($parts) == 0 && $this->match('[:.#]', $m, false)) {
3769 // css hacks
3770 $parts[] = $m[0];
3771 } else {
3772 break;
3773 }
3774 }
3775
3776 $this->eatWhiteDefault = $oldWhite;
3777 if (count($parts) == 0) return false;
3778
3779 // match comment hack
3780 if (preg_match(self::$whitePattern,
3781 $this->buffer, $m, null, $this->count))
3782 {
3783 if (!empty($m[0])) {
3784 $parts[] = $m[0];
3785 $this->count += strlen($m[0]);
3786 }
3787 }
3788
3789 $this->whitespace(); // get any extra whitespace
3790
3791 $out = array("string", "", $parts);
3792 return true;
3793 }
3794
3795 // comma separated list of selectors
3796 protected function selectors(&$out) {
3797 $s = $this->seek();
3798 $selectors = array();
3799 while ($this->selector($sel)) {
3800 $selectors[] = $sel;
3801 if (!$this->literal(",")) break;
3802 while ($this->literal(",")); // ignore extra
3803 }
3804
3805 if (count($selectors) == 0) {
3806 $this->seek($s);
3807 return false;
3808 }
3809
3810 $out = $selectors;
3811 return true;
3812 }
3813
3814 // whitespace separated list of selectorSingle
3815 protected function selector(&$out) {
3816 $selector = array();
3817
3818 while (true) {
3819 if ($this->match('[>+~]+', $m)) {
3820 $selector[] = array($m[0]);
3821 } elseif ($this->selectorSingle($part)) {
3822 $selector[] = $part;
3823 $this->whitespace();
3824 } elseif ($this->match('\/[^\/]+\/', $m)) {
3825 $selector[] = array($m[0]);
3826 } else {
3827 break;
3828 }
3829
3830 }
3831
3832 if (count($selector) == 0) {
3833 return false;
3834 }
3835
3836 $out = $selector;
3837 return true;
3838 }
3839
3840 // the parts that make up
3841 // div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
3842 protected function selectorSingle(&$out) {
3843 $oldWhite = $this->eatWhiteDefault;
3844 $this->eatWhiteDefault = false;
3845
3846 $parts = array();
3847
3848 if ($this->literal("*", false)) {
3849 $parts[] = "*";
3850 }
3851
3852 while (true) {
3853 // see if we can stop early
3854 if ($this->match("\s*[{,]", $m)) {
3855 $this->count--;
3856 break;
3857 }
3858
3859 $s = $this->seek();
3860 // self
3861 if ($this->literal("&", false)) {
3862 $parts[] = scssc::$selfSelector;
3863 continue;
3864 }
3865
3866 if ($this->literal(".", false)) {
3867 $parts[] = ".";
3868 continue;
3869 }
3870
3871 if ($this->literal("|", false)) {
3872 $parts[] = "|";
3873 continue;
3874 }
3875
3876 // for keyframes
3877 if ($this->unit($unit)) {
3878 $parts[] = $unit;
3879 continue;
3880 }
3881
3882 if ($this->keyword($name)) {
3883 $parts[] = $name;
3884 continue;
3885 }
3886
3887 if ($this->interpolation($inter)) {
3888 $parts[] = $inter;
3889 continue;
3890 }
3891
3892 if ($this->literal('%', false) && $this->placeholder($placeholder)) {
3893 $parts[] = '%';
3894 $parts[] = $placeholder;
3895 continue;
3896 }
3897
3898 if ($this->literal("#", false)) {
3899 $parts[] = "#";
3900 continue;
3901 }
3902
3903 // a pseudo selector
3904 if ($this->match("::?", $m) && $this->mixedKeyword($nameParts)) {
3905 $parts[] = $m[0];
3906 foreach ($nameParts as $sub) {
3907 $parts[] = $sub;
3908 }
3909
3910 $ss = $this->seek();
3911 if ($this->literal("(") &&
3912 ($this->openString(")", $str, "(") || true ) &&
3913 $this->literal(")"))
3914 {
3915 $parts[] = "(";
3916 if (!empty($str)) $parts[] = $str;
3917 $parts[] = ")";
3918 } else {
3919 $this->seek($ss);
3920 }
3921
3922 continue;
3923 } else {
3924 $this->seek($s);
3925 }
3926
3927 // attribute selector
3928 // TODO: replace with open string?
3929 if ($this->literal("[", false)) {
3930 $attrParts = array("[");
3931 // keyword, string, operator
3932 while (true) {
3933 if ($this->literal("]", false)) {
3934 $this->count--;
3935 break; // get out early
3936 }
3937
3938 if ($this->match('\s+', $m)) {
3939 $attrParts[] = " ";
3940 continue;
3941 }
3942 if ($this->string($str)) {
3943 $attrParts[] = $str;
3944 continue;
3945 }
3946
3947 if ($this->keyword($word)) {
3948 $attrParts[] = $word;
3949 continue;
3950 }
3951
3952 if ($this->interpolation($inter, false)) {
3953 $attrParts[] = $inter;
3954 continue;
3955 }
3956
3957 // operator, handles attr namespace too
3958 if ($this->match('[|-~\$\*\^=]+', $m)) {
3959 $attrParts[] = $m[0];
3960 continue;
3961 }
3962
3963 break;
3964 }
3965
3966 if ($this->literal("]", false)) {
3967 $attrParts[] = "]";
3968 foreach ($attrParts as $part) {
3969 $parts[] = $part;
3970 }
3971 continue;
3972 }
3973 $this->seek($s);
3974 // should just break here?
3975 }
3976
3977 break;
3978 }
3979
3980 $this->eatWhiteDefault = $oldWhite;
3981
3982 if (count($parts) == 0) return false;
3983
3984 $out = $parts;
3985 return true;
3986 }
3987
3988 protected function variable(&$out) {
3989 $s = $this->seek();
3990 if ($this->literal("$", false) && $this->keyword($name)) {
3991 $out = array("var", $name);
3992 return true;
3993 }
3994 $this->seek($s);
3995 return false;
3996 }
3997
3998 protected function keyword(&$word, $eatWhitespace = null) {
3999 if ($this->match('([\w_\-\*!"\'\\\\][\w\-_"\'\\\\]*)',
4000 $m, $eatWhitespace))
4001 {
4002 $word = $m[1];
4003 return true;
4004 }
4005 return false;
4006 }
4007
4008 protected function placeholder(&$placeholder) {
4009 if ($this->match('([\w\-_]+)', $m)) {
4010 $placeholder = $m[1];
4011 return true;
4012 }
4013 return false;
4014 }
4015
4016 // consume an end of statement delimiter
4017 protected function end() {
4018 if ($this->literal(';')) {
4019 return true;
4020 } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
4021 // if there is end of file or a closing block next then we don't need a ;
4022 return true;
4023 }
4024 return false;
4025 }
4026
4027 // advance counter to next occurrence of $what
4028 // $until - don't include $what in advance
4029 // $allowNewline, if string, will be used as valid char set
4030 protected function to($what, &$out, $until = false, $allowNewline = false) {
4031 if (is_string($allowNewline)) {
4032 $validChars = $allowNewline;
4033 } else {
4034 $validChars = $allowNewline ? "." : "[^\n]";
4035 }
4036 if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false;
4037 if ($until) $this->count -= strlen($what); // give back $what
4038 $out = $m[1];
4039 return true;
4040 }
4041
4042 public function throwParseError($msg = "parse error", $count = null) {
4043 $count = !isset($count) ? $this->count : $count;
4044
4045 $line = $this->getLineNo($count);
4046
4047 if (!empty($this->sourceName)) {
4048 $loc = "$this->sourceName on line $line";
4049 } else {
4050 $loc = "line: $line";
4051 }
4052
4053 if ($this->peek("(.*?)(\n|$)", $m, $count)) {
4054 throw new Exception("$msg: failed at `$m[1]` $loc");
4055 } else {
4056 throw new Exception("$msg: $loc");
4057 }
4058 }
4059
4060 public function getLineNo($pos) {
4061 return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
4062 }
4063
4064 /**
4065 * Match string looking for either ending delim, escape, or string interpolation
4066 *
4067 * {@internal This is a workaround for preg_match's 250K string match limit. }}
4068 *
4069 * @param array $m Matches (passed by reference)
4070 * @param string $delim Delimeter
4071 *
4072 * @return boolean True if match; false otherwise
4073 */
4074 protected function matchString(&$m, $delim) {
4075 $token = null;
4076
4077 $end = strpos($this->buffer, "\n", $this->count);
4078 if ($end === false || $this->buffer[$end - 1] == '\\' || $this->buffer[$end - 2] == '\\' && $this->buffer[$end - 1] == "\r") {
4079 $end = strlen($this->buffer);
4080 }
4081
4082 // look for either ending delim, escape, or string interpolation
4083 foreach (array('#{', '\\', $delim) as $lookahead) {
4084 $pos = strpos($this->buffer, $lookahead, $this->count);
4085 if ($pos !== false && $pos < $end) {
4086 $end = $pos;
4087 $token = $lookahead;
4088 }
4089 }
4090
4091 if (!isset($token)) {
4092 return false;
4093 }
4094
4095 $match = substr($this->buffer, $this->count, $end - $this->count);
4096 $m = array(
4097 $match . $token,
4098 $match,
4099 $token
4100 );
4101 $this->count = $end + strlen($token);
4102
4103 return true;
4104 }
4105
4106 // try to match something on head of buffer
4107 protected function match($regex, &$out, $eatWhitespace = null) {
4108 if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
4109
4110 $r = '/'.$regex.'/Ais';
4111 if (preg_match($r, $this->buffer, $out, null, $this->count)) {
4112 $this->count += strlen($out[0]);
4113 if ($eatWhitespace) $this->whitespace();
4114 return true;
4115 }
4116 return false;
4117 }
4118
4119 // match some whitespace
4120 protected function whitespace() {
4121 $gotWhite = false;
4122 while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
4123 if ($this->insertComments) {
4124 if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
4125 $this->append(array("comment", $m[1]));
4126 $this->commentsSeen[$this->count] = true;
4127 }
4128 }
4129 $this->count += strlen($m[0]);
4130 $gotWhite = true;
4131 }
4132 return $gotWhite;
4133 }
4134
4135 protected function peek($regex, &$out, $from=null) {
4136 if (!isset($from)) $from = $this->count;
4137
4138 $r = '/'.$regex.'/Ais';
4139 $result = preg_match($r, $this->buffer, $out, null, $from);
4140
4141 return $result;
4142 }
4143
4144 protected function seek($where = null) {
4145 if ($where === null) return $this->count;
4146 else $this->count = $where;
4147 return true;
4148 }
4149
4150 static function preg_quote($what) {
4151 return preg_quote($what, '/');
4152 }
4153
4154 protected function show() {
4155 if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
4156 return $m[1];
4157 }
4158 return "";
4159 }
4160
4161 // turn list of length 1 into value type
4162 protected function flattenList($value) {
4163 if ($value[0] == "list" && count($value[2]) == 1) {
4164 return $this->flattenList($value[2][0]);
4165 }
4166 return $value;
4167 }
4168 }
4169
4170 /**
4171 * SCSS base formatter
4172 *
4173 * @author Leaf Corcoran <leafot@gmail.com>
4174 */
4175 class scss_formatter {
4176 public $indentChar = " ";
4177
4178 public $break = "\n";
4179 public $open = " {";
4180 public $close = "}";
4181 public $tagSeparator = ", ";
4182 public $assignSeparator = ": ";
4183
4184 public function __construct() {
4185 $this->indentLevel = 0;
4186 }
4187
4188 public function indentStr($n = 0) {
4189 return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
4190 }
4191
4192 public function property($name, $value) {
4193 return $name . $this->assignSeparator . $value . ";";
4194 }
4195
4196 protected function block($block) {
4197 if (empty($block->lines) && empty($block->children)) return;
4198
4199 $inner = $pre = $this->indentStr();
4200
4201 if (!empty($block->selectors)) {
4202 echo $pre .
4203 implode($this->tagSeparator, $block->selectors) .
4204 $this->open . $this->break;
4205 $this->indentLevel++;
4206 $inner = $this->indentStr();
4207 }
4208
4209 if (!empty($block->lines)) {
4210 $glue = $this->break.$inner;
4211 echo $inner . implode($glue, $block->lines);
4212 if (!empty($block->children)) {
4213 echo $this->break;
4214 }
4215 }
4216
4217 foreach ($block->children as $child) {
4218 $this->block($child);
4219 }
4220
4221 if (!empty($block->selectors)) {
4222 $this->indentLevel--;
4223 if (empty($block->children)) echo $this->break;
4224 echo $pre . $this->close . $this->break;
4225 }
4226 }
4227
4228 public function format($block) {
4229 ob_start();
4230 $this->block($block);
4231 $out = ob_get_clean();
4232
4233 return $out;
4234 }
4235 }
4236
4237 /**
4238 * SCSS nested formatter
4239 *
4240 * @author Leaf Corcoran <leafot@gmail.com>
4241 */
4242 class scss_formatter_nested extends scss_formatter {
4243 public $close = " }";
4244
4245 // adjust the depths of all children, depth first
4246 public function adjustAllChildren($block) {
4247 // flatten empty nested blocks
4248 $children = array();
4249 foreach ($block->children as $i => $child) {
4250 if (empty($child->lines) && empty($child->children)) {
4251 if (isset($block->children[$i + 1])) {
4252 $block->children[$i + 1]->depth = $child->depth;
4253 }
4254 continue;
4255 }
4256 $children[] = $child;
4257 }
4258
4259 $count = count($children);
4260 for ($i = 0; $i < $count; $i++) {
4261 $depth = $children[$i]->depth;
4262 $j = $i + 1;
4263 if (isset($children[$j]) && $depth < $children[$j]->depth) {
4264 $childDepth = $children[$j]->depth;
4265 for (; $j < $count; $j++) {
4266 if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
4267 $children[$j]->depth = $depth + 1;
4268 }
4269 }
4270 }
4271 }
4272
4273 $block->children = $children;
4274
4275 // make relative to parent
4276 foreach ($block->children as $child) {
4277 $this->adjustAllChildren($child);
4278 $child->depth = $child->depth - $block->depth;
4279 }
4280 }
4281
4282 protected function block($block) {
4283 if ($block->type == "root") {
4284 $this->adjustAllChildren($block);
4285 }
4286
4287 $inner = $pre = $this->indentStr($block->depth - 1);
4288 if (!empty($block->selectors)) {
4289 echo $pre .
4290 implode($this->tagSeparator, $block->selectors) .
4291 $this->open . $this->break;
4292 $this->indentLevel++;
4293 $inner = $this->indentStr($block->depth - 1);
4294 }
4295
4296 if (!empty($block->lines)) {
4297 $glue = $this->break.$inner;
4298 echo $inner . implode($glue, $block->lines);
4299 if (!empty($block->children)) echo $this->break;
4300 }
4301
4302 foreach ($block->children as $i => $child) {
4303 // echo "*** block: ".$block->depth." child: ".$child->depth."\n";
4304 $this->block($child);
4305 if ($i < count($block->children) - 1) {
4306 echo $this->break;
4307
4308 if (isset($block->children[$i + 1])) {
4309 $next = $block->children[$i + 1];
4310 if ($next->depth == max($block->depth, 1) && $child->depth >= $next->depth) {
4311 echo $this->break;
4312 }
4313 }
4314 }
4315 }
4316
4317 if (!empty($block->selectors)) {
4318 $this->indentLevel--;
4319 echo $this->close;
4320 }
4321
4322 if ($block->type == "root") {
4323 echo $this->break;
4324 }
4325 }
4326 }
4327
4328 /**
4329 * SCSS compressed formatter
4330 *
4331 * @author Leaf Corcoran <leafot@gmail.com>
4332 */
4333 class scss_formatter_compressed extends scss_formatter {
4334 public $open = "{";
4335 public $tagSeparator = ",";
4336 public $assignSeparator = ":";
4337 public $break = "";
4338
4339 public function indentStr($n = 0) {
4340 return "";
4341 }
4342 }
4343
4344 /**
4345 * SCSS server
4346 *
4347 * @author Leaf Corcoran <leafot@gmail.com>
4348 */
4349 class scss_server {
4350 /**
4351 * Join path components
4352 *
4353 * @param string $left Path component, left of the directory separator
4354 * @param string $right Path component, right of the directory separator
4355 *
4356 * @return string
4357 */
4358 protected function join($left, $right) {
4359 return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
4360 }
4361
4362 /**
4363 * Get name of requested .scss file
4364 *
4365 * @return string|null
4366 */
4367 protected function inputName() {
4368 switch (true) {
4369 case isset($_GET['p']):
4370 return $_GET['p'];
4371 case isset($_SERVER['PATH_INFO']):
4372 return $_SERVER['PATH_INFO'];
4373 case isset($_SERVER['DOCUMENT_URI']):
4374 return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
4375 }
4376 }
4377
4378 /**
4379 * Get path to requested .scss file
4380 *
4381 * @return string
4382 */
4383 protected function findInput() {
4384 if (($input = $this->inputName())
4385 && strpos($input, '..') === false
4386 && substr($input, -5) === '.scss'
4387 ) {
4388 $name = $this->join($this->dir, $input);
4389
4390 if (is_file($name) && is_readable($name)) {
4391 return $name;
4392 }
4393 }
4394
4395 return false;
4396 }
4397
4398 /**
4399 * Get path to cached .css file
4400 *
4401 * @return string
4402 */
4403 protected function cacheName($fname) {
4404 return $this->join($this->cacheDir, md5($fname) . '.css');
4405 }
4406
4407 /**
4408 * Get path to cached imports
4409 *
4410 * @return string
4411 */
4412 protected function importsCacheName($out) {
4413 return $out . '.imports';
4414 }
4415
4416 /**
4417 * Determine whether .scss file needs to be re-compiled.
4418 *
4419 * @param string $in Input path
4420 * @param string $out Output path
4421 *
4422 * @return boolean True if compile required.
4423 */
4424 protected function needsCompile($in, $out) {
4425 if (!is_file($out)) return true;
4426
4427 $mtime = filemtime($out);
4428 if (filemtime($in) > $mtime) return true;
4429
4430 // look for modified imports
4431 $icache = $this->importsCacheName($out);
4432 if (is_readable($icache)) {
4433 $imports = unserialize(file_get_contents($icache));
4434 foreach ($imports as $import) {
4435 if (filemtime($import) > $mtime) return true;
4436 }
4437 }
4438 return false;
4439 }
4440
4441 /**
4442 * Get If-Modified-Since header from client request
4443 *
4444 * @return string
4445 */
4446 protected function getModifiedSinceHeader()
4447 {
4448 $modifiedSince = '';
4449
4450 if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
4451 $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
4452
4453 if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
4454 $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
4455 }
4456 }
4457
4458 return $modifiedSince;
4459 }
4460
4461 /**
4462 * Compile .scss file
4463 *
4464 * @param string $in Input path (.scss)
4465 * @param string $out Output path (.css)
4466 *
4467 * @return string
4468 */
4469 protected function compile($in, $out) {
4470 $start = microtime(true);
4471 $css = $this->scss->compile(file_get_contents($in), $in);
4472 $elapsed = round((microtime(true) - $start), 4);
4473
4474 $v = scssc::$VERSION;
4475 $t = @date('r');
4476 $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
4477
4478 file_put_contents($out, $css);
4479 file_put_contents($this->importsCacheName($out),
4480 serialize($this->scss->getParsedFiles()));
4481 return $css;
4482 }
4483
4484 /**
4485 * Compile requested scss and serve css. Outputs HTTP response.
4486 *
4487 * @param string $salt Prefix a string to the filename for creating the cache name hash
4488 */
4489 public function serve($salt = '') {
4490 $protocol = isset($_SERVER['SERVER_PROTOCOL'])
4491 ? $_SERVER['SERVER_PROTOCOL']
4492 : 'HTTP/1.0';
4493
4494 if ($input = $this->findInput()) {
4495 $output = $this->cacheName($salt . $input);
4496
4497 if ($this->needsCompile($input, $output)) {
4498 try {
4499 $css = $this->compile($input, $output);
4500
4501 $lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT';
4502
4503 header('Last-Modified: ' . $lastModified);
4504 header('Content-type: text/css');
4505
4506 echo $css;
4507
4508 return;
4509 } catch (Exception $e) {
4510 header($protocol . ' 500 Internal Server Error');
4511 header('Content-type: text/plain');
4512
4513 echo 'Parse error: ' . $e->getMessage() . "\n";
4514 }
4515 }
4516
4517 header('X-SCSS-Cache: true');
4518 header('Content-type: text/css');
4519
4520 $modifiedSince = $this->getModifiedSinceHeader();
4521 $mtime = filemtime($output);
4522
4523 if (@strtotime($modifiedSince) === $mtime) {
4524 header($protocol . ' 304 Not Modified');
4525
4526 return;
4527 }
4528
4529 $lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
4530 header('Last-Modified: ' . $lastModified);
4531
4532 echo file_get_contents($output);
4533
4534 return;
4535 }
4536
4537 header($protocol . ' 404 Not Found');
4538 header('Content-type: text/plain');
4539
4540 $v = scssc::$VERSION;
4541 echo "/* INPUT NOT FOUND scss $v */\n";
4542 }
4543
4544 /**
4545 * Constructor
4546 *
4547 * @param string $dir Root directory to .scss files
4548 * @param string $cacheDir Cache directory
4549 * @param \scssc|null $scss SCSS compiler instance
4550 */
4551 public function __construct($dir, $cacheDir=null, $scss=null) {
4552 $this->dir = $dir;
4553
4554 if (!isset($cacheDir)) {
4555 $cacheDir = $this->join($dir, 'scss_cache');
4556 }
4557
4558 $this->cacheDir = $cacheDir;
4559 if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true);
4560
4561 if (!isset($scss)) {
4562 $scss = new scssc();
4563 $scss->setImportPaths($this->dir);
4564 }
4565 $this->scss = $scss;
4566 }
4567
4568 /**
4569 * Helper method to serve compiled scss
4570 *
4571 * @param string $path Root path
4572 */
4573 static public function serveFrom($path) {
4574 $server = new self($path);
4575 $server->serve();
4576 }
4577 }
4578