PluginProbe
MaxButtons – Create buttons / 6.28
MaxButtons – Create buttons v6.28
6.23 6.24 6.25 6.26 6.26.1 6.27 6.28 6.3 6.4 6.5 6.6 6.7 6.8 6.9 7.0 7.1 7.1.1 7.1.2 7.1.3 7.10 7.11 7.13 7.13.1 7.13.2 7.13.3 All 100 releases
maxbuttons / assets / libraries / scssphp / src / Parser.php

Parser.php in MaxButtons – Create buttons 6.28, at assets/libraries/scssphp/src/Parser.php

2,378 lines 56.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SCSSPHP
4 *
5 * @copyright 2012-2015 Leaf Corcoran
6 *
7 * @license http://opensource.org/licenses/MIT MIT
8 *
9 * @link http://leafo.github.io/scssphp
10 */
11
12 namespace Leafo\ScssPhp;
13
14 use Leafo\ScssPhp\Block;
15 use Leafo\ScssPhp\Compiler;
16 use Leafo\ScssPhp\Node;
17 use Leafo\ScssPhp\Type;
18
19 /**
20 * SCSS parser
21 *
22 * @author Leaf Corcoran <leafot@gmail.com>
23 */
24 class Parser
25 {
26 const SOURCE_INDEX = -1;
27 const SOURCE_POSITION = -2;
28
29 /**
30 * @var array
31 */
32 protected static $precedence = array(
33 '=' => 0,
34 'or' => 1,
35 'and' => 2,
36 '==' => 3,
37 '!=' => 3,
38 '<=>' => 3,
39 '<=' => 4,
40 '>=' => 4,
41 '<' => 4,
42 '>' => 4,
43 '+' => 5,
44 '-' => 5,
45 '*' => 6,
46 '/' => 6,
47 '%' => 6,
48 );
49
50 protected static $commentPattern;
51 protected static $operatorPattern;
52 protected static $whitePattern;
53
54 private $sourceName;
55 private $sourceIndex;
56 private $charset;
57 private $count;
58 private $env;
59 private $inParens;
60 private $eatWhiteDefault;
61 private $buffer;
62
63 /**
64 * Constructor
65 *
66 * @api
67 *
68 * @param string $sourceName
69 * @param integer $sourceIndex
70 */
71 public function __construct($sourceName, $sourceIndex = 0)
72 {
73 $this->sourceName = $sourceName ?: '(stdin)';
74 $this->sourceIndex = $sourceIndex;
75 $this->charset = null;
76
77 if (empty(self::$operatorPattern)) {
78 self::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
79
80 $commentSingle = '\/\/';
81 $commentMultiLeft = '\/\*';
82 $commentMultiRight = '\*\/';
83
84 self::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
85 self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentPattern . ')\s*|\s+/Ais';
86 }
87 }
88
89 /**
90 * Get source file name
91 *
92 * @api
93 *
94 * @return string
95 */
96 public function getSourceName()
97 {
98 return $this->sourceName;
99 }
100
101 /**
102 * Get source line number (given character position in the buffer)
103 *
104 * @api
105 *
106 * @param integer $pos
107 *
108 * @return integer
109 */
110 public function getLineNo($pos)
111 {
112 return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
113 }
114
115 /**
116 * Throw parser error
117 *
118 * @api
119 *
120 * @param string $msg
121 * @param integer $count
122 *
123 * @throws \Exception
124 */
125 public function throwParseError($msg = 'parse error', $count = null)
126 {
127 $count = ! isset($count) ? $this->count : $count;
128
129 $line = $this->getLineNo($count);
130
131 if (! empty($this->sourceName)) {
132 $loc = "$this->sourceName on line $line";
133 } else {
134 $loc = "line: $line";
135 }
136
137 if ($this->peek("(.*?)(\n|$)", $m, $count)) {
138 throw new \Exception("$msg: failed at `$m[1]` $loc");
139 }
140
141 throw new \Exception("$msg: $loc");
142 }
143
144 /**
145 * Parser buffer
146 *
147 * @api
148 *
149 * @param string $buffer
150 *
151 * @return \Leafo\ScssPhp\Block
152 */
153 public function parse($buffer)
154 {
155 $this->count = 0;
156 $this->env = null;
157 $this->inParens = false;
158 $this->eatWhiteDefault = true;
159 $this->buffer = rtrim($buffer, "\x00..\x1f");
160
161 $this->pushBlock(null); // root block
162
163 $this->whitespace();
164 $this->pushBlock(null);
165 $this->popBlock();
166
167 while ($this->parseChunk()) {
168 ;
169 }
170
171 if ($this->count !== strlen($this->buffer)) {
172 $this->throwParseError();
173 }
174
175 if (! empty($this->env->parent)) {
176 $this->throwParseError('unclosed block');
177 }
178
179 if ($this->charset) {
180 array_unshift($this->env->children, $this->charset);
181 }
182
183 $this->env->isRoot = true;
184
185 return $this->env;
186 }
187
188 /**
189 * Parse a value or value list
190 *
191 * @api
192 *
193 * @param string $buffer
194 * @param string $out
195 *
196 * @return boolean
197 */
198 public function parseValue($buffer, &$out)
199 {
200 $this->count = 0;
201 $this->env = null;
202 $this->inParens = false;
203 $this->eatWhiteDefault = true;
204 $this->buffer = (string) $buffer;
205
206 return $this->valueList($out);
207 }
208
209 /**
210 * Parse a selector or selector list
211 *
212 * @api
213 *
214 * @param string $buffer
215 * @param string $out
216 *
217 * @return boolean
218 */
219 public function parseSelector($buffer, &$out)
220 {
221 $this->count = 0;
222 $this->env = null;
223 $this->inParens = false;
224 $this->eatWhiteDefault = true;
225 $this->buffer = (string) $buffer;
226
227 return $this->selectors($out);
228 }
229
230 /**
231 * Parse a single chunk off the head of the buffer and append it to the
232 * current parse environment.
233 *
234 * Returns false when the buffer is empty, or when there is an error.
235 *
236 * This function is called repeatedly until the entire document is
237 * parsed.
238 *
239 * This parser is most similar to a recursive descent parser. Single
240 * functions represent discrete grammatical rules for the language, and
241 * they are able to capture the text that represents those rules.
242 *
243 * Consider the function Compiler::keyword(). (All parse functions are
244 * structured the same.)
245 *
246 * The function takes a single reference argument. When calling the
247 * function it will attempt to match a keyword on the head of the buffer.
248 * If it is successful, it will place the keyword in the referenced
249 * argument, advance the position in the buffer, and return true. If it
250 * fails then it won't advance the buffer and it will return false.
251 *
252 * All of these parse functions are powered by Compiler::match(), which behaves
253 * the same way, but takes a literal regular expression. Sometimes it is
254 * more convenient to use match instead of creating a new function.
255 *
256 * Because of the format of the functions, to parse an entire string of
257 * grammatical rules, you can chain them together using &&.
258 *
259 * But, if some of the rules in the chain succeed before one fails, then
260 * the buffer position will be left at an invalid state. In order to
261 * avoid this, Compiler::seek() is used to remember and set buffer positions.
262 *
263 * Before parsing a chain, use $s = $this->seek() to remember the current
264 * position into $s. Then if a chain fails, use $this->seek($s) to
265 * go back where we started.
266 *
267 * @return boolean
268 */
269 protected function parseChunk()
270 {
271 $s = $this->seek();
272
273 // the directives
274 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
275 if ($this->literal('@at-root') &&
276 ($this->selectors($selector) || true) &&
277 ($this->map($with) || true) &&
278 $this->literal('{')
279 ) {
280 $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
281 $atRoot->selector = $selector;
282 $atRoot->with = $with;
283
284 return true;
285 }
286
287 $this->seek($s);
288
289 if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
290 $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
291 $media->queryList = $mediaQueryList[2];
292
293 return true;
294 }
295
296 $this->seek($s);
297
298 if ($this->literal('@mixin') &&
299 $this->keyword($mixinName) &&
300 ($this->argumentDef($args) || true) &&
301 $this->literal('{')
302 ) {
303 $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
304 $mixin->name = $mixinName;
305 $mixin->args = $args;
306
307 return true;
308 }
309
310 $this->seek($s);
311
312 if ($this->literal('@include') &&
313 $this->keyword($mixinName) &&
314 ($this->literal('(') &&
315 ($this->argValues($argValues) || true) &&
316 $this->literal(')') || true) &&
317 ($this->end() ||
318 $this->literal('{') && $hasBlock = true)
319 ) {
320 $child = array(Type::T_INCLUDE,
321 $mixinName, isset($argValues) ? $argValues : null, null);
322
323 if (! empty($hasBlock)) {
324 $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
325 $include->child = $child;
326 } else {
327 $this->append($child, $s);
328 }
329
330 return true;
331 }
332
333 $this->seek($s);
334
335 if ($this->literal('@import') &&
336 $this->valueList($importPath) &&
337 $this->end()
338 ) {
339 $this->append(array(Type::T_IMPORT, $importPath), $s);
340
341 return true;
342 }
343
344 $this->seek($s);
345
346 if ($this->literal('@import') &&
347 $this->url($importPath) &&
348 $this->end()
349 ) {
350 $this->append(array(Type::T_IMPORT, $importPath), $s);
351
352 return true;
353 }
354
355 $this->seek($s);
356
357 if ($this->literal('@extend') &&
358 $this->selectors($selector) &&
359 $this->end()
360 ) {
361 $this->append(array(Type::T_EXTEND, $selector), $s);
362
363 return true;
364 }
365
366 $this->seek($s);
367
368 if ($this->literal('@function') &&
369 $this->keyword($fnName) &&
370 $this->argumentDef($args) &&
371 $this->literal('{')
372 ) {
373 $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
374 $func->name = $fnName;
375 $func->args = $args;
376
377 return true;
378 }
379
380 $this->seek($s);
381
382 if ($this->literal('@break') && $this->end()) {
383 $this->append(array(Type::T_BREAK), $s);
384
385 return true;
386 }
387
388 $this->seek($s);
389
390 if ($this->literal('@continue') && $this->end()) {
391 $this->append(array(Type::T_CONTINUE), $s);
392
393 return true;
394 }
395
396 $this->seek($s);
397
398
399 if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) {
400 $this->append(array(Type::T_RETURN, isset($retVal) ? $retVal : array(Type::T_NULL)), $s);
401
402 return true;
403 }
404
405 $this->seek($s);
406
407 if ($this->literal('@each') &&
408 $this->genericList($varNames, 'variable', ',', false) &&
409 $this->literal('in') &&
410 $this->valueList($list) &&
411 $this->literal('{')
412 ) {
413 $each = $this->pushSpecialBlock(Type::T_EACH, $s);
414
415 foreach ($varNames[2] as $varName) {
416 $each->vars[] = $varName[1];
417 }
418
419 $each->list = $list;
420
421 return true;
422 }
423
424 $this->seek($s);
425
426 if ($this->literal('@while') &&
427 $this->expression($cond) &&
428 $this->literal('{')
429 ) {
430 $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
431 $while->cond = $cond;
432
433 return true;
434 }
435
436 $this->seek($s);
437
438 if ($this->literal('@for') &&
439 $this->variable($varName) &&
440 $this->literal('from') &&
441 $this->expression($start) &&
442 ($this->literal('through') ||
443 ($forUntil = true && $this->literal('to'))) &&
444 $this->expression($end) &&
445 $this->literal('{')
446 ) {
447 $for = $this->pushSpecialBlock(Type::T_FOR, $s);
448 $for->var = $varName[1];
449 $for->start = $start;
450 $for->end = $end;
451 $for->until = isset($forUntil);
452
453 return true;
454 }
455
456 $this->seek($s);
457
458 if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
459 $if = $this->pushSpecialBlock(Type::T_IF, $s);
460 $if->cond = $cond;
461 $if->cases = array();
462
463 return true;
464 }
465
466 $this->seek($s);
467
468 if ($this->literal('@debug') &&
469 $this->valueList($value) &&
470 $this->end()
471 ) {
472 $this->append(array(Type::T_DEBUG, $value), $s);
473
474 return true;
475 }
476
477 $this->seek($s);
478
479 if ($this->literal('@warn') &&
480 $this->valueList($value) &&
481 $this->end()
482 ) {
483 $this->append(array(Type::T_WARN, $value), $s);
484
485 return true;
486 }
487
488 $this->seek($s);
489
490 if ($this->literal('@error') &&
491 $this->valueList($value) &&
492 $this->end()
493 ) {
494 $this->append(array(Type::T_ERROR, $value), $s);
495
496 return true;
497 }
498
499 $this->seek($s);
500
501 if ($this->literal('@content') && $this->end()) {
502 $this->append(array(Type::T_MIXIN_CONTENT), $s);
503
504 return true;
505 }
506
507 $this->seek($s);
508
509 $last = $this->last();
510
511 if (isset($last) && $last[0] === Type::T_IF) {
512 list(, $if) = $last;
513
514 if ($this->literal('@else')) {
515 if ($this->literal('{')) {
516 $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
517 } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
518 $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
519 $else->cond = $cond;
520 }
521
522 if (isset($else)) {
523 $else->dontAppend = true;
524 $if->cases[] = $else;
525
526 return true;
527 }
528 }
529
530 $this->seek($s);
531 }
532
533 // only retain the first @charset directive encountered
534 if ($this->literal('@charset') &&
535 $this->valueList($charset) &&
536 $this->end()
537 ) {
538 if (! isset($this->charset)) {
539 $statement = array(Type::T_CHARSET, $charset);
540
541 $statement[self::SOURCE_POSITION] = $s;
542 $statement[self::SOURCE_INDEX] = $this->sourceIndex;
543
544 $this->charset = $statement;
545 }
546
547 return true;
548 }
549
550 $this->seek($s);
551
552 // doesn't match built in directive, do generic one
553 if ($this->literal('@', false) &&
554 $this->keyword($dirName) &&
555 ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
556 $this->literal('{')
557 ) {
558 if ($dirName === 'media') {
559 $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
560 } else {
561 $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
562 $directive->name = $dirName;
563 }
564
565 if (isset($dirValue)) {
566 $directive->value = $dirValue;
567 }
568
569 return true;
570 }
571
572 $this->seek($s);
573
574 return false;
575 }
576
577 // property shortcut
578 // captures most properties before having to parse a selector
579 if ($this->keyword($name, false) &&
580 $this->literal(': ') &&
581 $this->valueList($value) &&
582 $this->end()
583 ) {
584 $name = array(Type::T_STRING, '', array($name));
585 $this->append(array(Type::T_ASSIGN, $name, $value), $s);
586
587 return true;
588 }
589
590 $this->seek($s);
591
592 // variable assigns
593 if ($this->variable($name) &&
594 $this->literal(':') &&
595 $this->valueList($value) &&
596 $this->end()
597 ) {
598 // check for '!flag'
599 $assignmentFlag = $this->stripAssignmentFlag($value);
600 $this->append(array(Type::T_ASSIGN, $name, $value, $assignmentFlag), $s);
601
602 return true;
603 }
604
605 $this->seek($s);
606
607 // misc
608 if ($this->literal('-->')) {
609 return true;
610 }
611
612 // opening css block
613 if ($this->selectors($selectors) && $this->literal('{')) {
614 $b = $this->pushBlock($selectors, $s);
615
616 return true;
617 }
618
619 $this->seek($s);
620
621 // property assign, or nested assign
622 if ($this->propertyName($name) && $this->literal(':')) {
623 $foundSomething = false;
624
625 if ($this->valueList($value)) {
626 $this->append(array(Type::T_ASSIGN, $name, $value), $s);
627 $foundSomething = true;
628 }
629
630 if ($this->literal('{')) {
631 $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
632 $propBlock->prefix = $name;
633 $foundSomething = true;
634 } elseif ($foundSomething) {
635 $foundSomething = $this->end();
636 }
637
638 if ($foundSomething) {
639 return true;
640 }
641 }
642
643 $this->seek($s);
644
645 // closing a block
646 if ($this->literal('}')) {
647 $block = $this->popBlock();
648
649 if (isset($block->type) && $block->type === Type::T_INCLUDE) {
650 $include = $block->child;
651 unset($block->child);
652 $include[3] = $block;
653 $this->append($include, $s);
654 } elseif (empty($block->dontAppend)) {
655 $type = isset($block->type) ? $block->type : Type::T_BLOCK;
656 $this->append(array($type, $block), $s);
657 }
658
659 return true;
660 }
661
662 // extra stuff
663 if ($this->literal(';') ||
664 $this->literal('<!--')
665 ) {
666 return true;
667 }
668
669 return false;
670 }
671
672 /**
673 * Push block onto parse tree
674 *
675 * @param array $selectors
676 * @param integer $pos
677 *
678 * @return \Leafo\ScssPhp\Block
679 */
680 protected function pushBlock($selectors, $pos = 0)
681 {
682 $b = new Block;
683 $b->parent = $this->env;
684 $b->sourcePosition = $pos;
685 $b->sourceIndex = $this->sourceIndex;
686 $b->selectors = $selectors;
687 $b->comments = array();
688
689 if (! $this->env) {
690 $b->children = array();
691 } elseif (empty($this->env->children)) {
692 $this->env->children = $this->env->comments;
693 $b->children = array();
694 $this->env->comments = array();
695 } else {
696 $b->children = $this->env->comments;
697 $this->env->comments = array();
698 }
699
700 $this->env = $b;
701
702 return $b;
703 }
704
705 /**
706 * Push special (named) block onto parse tree
707 *
708 * @param string $type
709 * @param integer $pos
710 *
711 * @return \Leafo\ScssPhp\Block
712 */
713 protected function pushSpecialBlock($type, $pos)
714 {
715 $block = $this->pushBlock(null, $pos);
716 $block->type = $type;
717
718 return $block;
719 }
720
721 /**
722 * Pop scope and return last block
723 *
724 * @return \Leafo\ScssPhp\Block
725 *
726 * @throws \Exception
727 */
728 protected function popBlock()
729 {
730 $block = $this->env;
731
732 if (empty($block->parent)) {
733 $this->throwParseError('unexpected }');
734 }
735
736 $this->env = $block->parent;
737 unset($block->parent);
738
739 $comments = $block->comments;
740 if (count($comments)) {
741 $this->env->comments = $comments;
742 unset($block->comments);
743 }
744
745 return $block;
746 }
747
748 /**
749 * Peek input stream
750 *
751 * @param string $regex
752 * @param array $out
753 * @param integer $from
754 *
755 * @return integer
756 */
757 protected function peek($regex, &$out, $from = null)
758 {
759 if (! isset($from)) {
760 $from = $this->count;
761 }
762
763 $r = '/' . $regex . '/Ais';
764 $result = preg_match($r, $this->buffer, $out, null, $from);
765
766 return $result;
767 }
768
769 /**
770 * Seek to position in input stream (or return current position in input stream)
771 *
772 * @param integer $where
773 *
774 * @return integer
775 */
776 protected function seek($where = null)
777 {
778 if ($where === null) {
779 return $this->count;
780 }
781
782 $this->count = $where;
783
784 return true;
785 }
786
787 /**
788 * Match string looking for either ending delim, escape, or string interpolation
789 *
790 * {@internal This is a workaround for preg_match's 250K string match limit. }}
791 *
792 * @param array $m Matches (passed by reference)
793 * @param string $delim Delimeter
794 *
795 * @return boolean True if match; false otherwise
796 */
797 protected function matchString(&$m, $delim)
798 {
799 $token = null;
800
801 $end = strlen($this->buffer);
802
803 // look for either ending delim, escape, or string interpolation
804 foreach (array('#{', '\\', $delim) as $lookahead) {
805 $pos = strpos($this->buffer, $lookahead, $this->count);
806
807 if ($pos !== false && $pos < $end) {
808 $end = $pos;
809 $token = $lookahead;
810 }
811 }
812
813 if (! isset($token)) {
814 return false;
815 }
816
817 $match = substr($this->buffer, $this->count, $end - $this->count);
818 $m = array(
819 $match . $token,
820 $match,
821 $token
822 );
823 $this->count = $end + strlen($token);
824
825 return true;
826 }
827
828 /**
829 * Try to match something on head of buffer
830 *
831 * @param string $regex
832 * @param array $out
833 * @param boolean $eatWhitespace
834 *
835 * @return boolean
836 */
837 protected function match($regex, &$out, $eatWhitespace = null)
838 {
839 if (! isset($eatWhitespace)) {
840 $eatWhitespace = $this->eatWhiteDefault;
841 }
842
843 $r = '/' . $regex . '/Ais';
844
845 if (preg_match($r, $this->buffer, $out, null, $this->count)) {
846 $this->count += strlen($out[0]);
847
848 if ($eatWhitespace) {
849 $this->whitespace();
850 }
851
852 return true;
853 }
854
855 return false;
856 }
857
858 /**
859 * Match literal string
860 *
861 * @param string $what
862 * @param boolean $eatWhitespace
863 *
864 * @return boolean
865 */
866 protected function literal($what, $eatWhitespace = null)
867 {
868 if (! isset($eatWhitespace)) {
869 $eatWhitespace = $this->eatWhiteDefault;
870 }
871
872 // shortcut on single letter
873 if (! isset($what[1]) && isset($this->buffer[$this->count])) {
874 if ($this->buffer[$this->count] === $what) {
875 if (! $eatWhitespace) {
876 $this->count++;
877
878 return true;
879 }
880
881 // goes below...
882 } else {
883 return false;
884 }
885 }
886
887 return $this->match($this->pregQuote($what), $m, $eatWhitespace);
888 }
889
890 /**
891 * Match some whitespace
892 *
893 * @return boolean
894 */
895 protected function whitespace()
896 {
897 $gotWhite = false;
898
899 while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
900 if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
901 $this->appendComment(array(Type::T_COMMENT, $m[1]));
902
903 $this->commentsSeen[$this->count] = true;
904 }
905
906 $this->count += strlen($m[0]);
907 $gotWhite = true;
908 }
909
910 return $gotWhite;
911 }
912
913 /**
914 * Append comment to current block
915 *
916 * @param array $comment
917 */
918 protected function appendComment($comment)
919 {
920 $comment[1] = substr(preg_replace(array('/^\s+/m', '/^(.)/m'), array('', ' \1'), $comment[1]), 1);
921
922 $this->env->comments[] = $comment;
923 }
924
925 /**
926 * Append statement to current block
927 *
928 * @param array $statement
929 * @param integer $pos
930 */
931 protected function append($statement, $pos = null)
932 {
933 if ($pos !== null) {
934 $statement[self::SOURCE_POSITION] = $pos;
935 $statement[self::SOURCE_INDEX] = $this->sourceIndex;
936 }
937
938 $this->env->children[] = $statement;
939
940 $comments = $this->env->comments;
941
942 if (count($comments)) {
943 $this->env->children = array_merge($this->env->children, $comments);
944 $this->env->comments = array();
945 }
946 }
947
948 /**
949 * Returns last child was appended
950 *
951 * @return array|null
952 */
953 protected function last()
954 {
955 $i = count($this->env->children) - 1;
956
957 if (isset($this->env->children[$i])) {
958 return $this->env->children[$i];
959 }
960 }
961
962 /**
963 * Parse media query list
964 *
965 * @param array $out
966 *
967 * @return boolean
968 */
969 protected function mediaQueryList(&$out)
970 {
971 return $this->genericList($out, 'mediaQuery', ',', false);
972 }
973
974 /**
975 * Parse media query
976 *
977 * @param array $out
978 *
979 * @return boolean
980 */
981 protected function mediaQuery(&$out)
982 {
983 $s = $this->seek();
984
985 $expressions = null;
986 $parts = array();
987
988 if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
989 $this->mixedKeyword($mediaType)
990 ) {
991 $prop = array(Type::T_MEDIA_TYPE);
992
993 if (isset($only)) {
994 $prop[] = array(Type::T_KEYWORD, 'only');
995 }
996
997 if (isset($not)) {
998 $prop[] = array(Type::T_KEYWORD, 'not');
999 }
1000
1001 $media = array(Type::T_LIST, '', array());
1002
1003 foreach ((array)$mediaType as $type) {
1004 if (is_array($type)) {
1005 $media[2][] = $type;
1006 } else {
1007 $media[2][] = array(Type::T_KEYWORD, $type);
1008 }
1009 }
1010
1011 $prop[] = $media;
1012 $parts[] = $prop;
1013 }
1014
1015 if (empty($parts) || $this->literal('and')) {
1016 $this->genericList($expressions, 'mediaExpression', 'and', false);
1017
1018 if (is_array($expressions)) {
1019 $parts = array_merge($parts, $expressions[2]);
1020 }
1021 }
1022
1023 $out = $parts;
1024
1025 return true;
1026 }
1027
1028 /**
1029 * Parse media expression
1030 *
1031 * @param array $out
1032 *
1033 * @return boolean
1034 */
1035 protected function mediaExpression(&$out)
1036 {
1037 $s = $this->seek();
1038 $value = null;
1039
1040 if ($this->literal('(') &&
1041 $this->expression($feature) &&
1042 ($this->literal(':') && $this->expression($value) || true) &&
1043 $this->literal(')')
1044 ) {
1045 $out = array(Type::T_MEDIA_EXPRESSION, $feature);
1046
1047 if ($value) {
1048 $out[] = $value;
1049 }
1050
1051 return true;
1052 }
1053
1054 $this->seek($s);
1055
1056 return false;
1057 }
1058
1059 /**
1060 * Parse argument values
1061 *
1062 * @param array $out
1063 *
1064 * @return boolean
1065 */
1066 protected function argValues(&$out)
1067 {
1068 if ($this->genericList($list, 'argValue', ',', false)) {
1069 $out = $list[2];
1070
1071 return true;
1072 }
1073
1074 return false;
1075 }
1076
1077 /**
1078 * Parse argument value
1079 *
1080 * @param array $out
1081 *
1082 * @return boolean
1083 */
1084 protected function argValue(&$out)
1085 {
1086 $s = $this->seek();
1087
1088 $keyword = null;
1089
1090 if (! $this->variable($keyword) || ! $this->literal(':')) {
1091 $this->seek($s);
1092 $keyword = null;
1093 }
1094
1095 if ($this->genericList($value, 'expression')) {
1096 $out = array($keyword, $value, false);
1097 $s = $this->seek();
1098
1099 if ($this->literal('...')) {
1100 $out[2] = true;
1101 } else {
1102 $this->seek($s);
1103 }
1104
1105 return true;
1106 }
1107
1108 return false;
1109 }
1110
1111 /**
1112 * Parse comma separated value list
1113 *
1114 * @param string $out
1115 *
1116 * @return boolean
1117 */
1118 protected function valueList(&$out)
1119 {
1120 return $this->genericList($out, 'spaceList', ',');
1121 }
1122
1123 /**
1124 * Parse space separated value list
1125 *
1126 * @param array $out
1127 *
1128 * @return boolean
1129 */
1130 protected function spaceList(&$out)
1131 {
1132 return $this->genericList($out, 'expression');
1133 }
1134
1135 /**
1136 * Parse generic list
1137 *
1138 * @param array $out
1139 * @param callable $parseItem
1140 * @param string $delim
1141 * @param boolean $flatten
1142 *
1143 * @return boolean
1144 */
1145 protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1146 {
1147 $s = $this->seek();
1148 $items = array();
1149
1150 while ($this->$parseItem($value)) {
1151 $items[] = $value;
1152
1153 if ($delim) {
1154 if (! $this->literal($delim)) {
1155 break;
1156 }
1157 }
1158 }
1159
1160 if (count($items) === 0) {
1161 $this->seek($s);
1162
1163 return false;
1164 }
1165
1166 if ($flatten && count($items) === 1) {
1167 $out = $items[0];
1168 } else {
1169 $out = array(Type::T_LIST, $delim, $items);
1170 }
1171
1172 return true;
1173 }
1174
1175 /**
1176 * Parse expression
1177 *
1178 * @param array $out
1179 *
1180 * @return boolean
1181 */
1182 protected function expression(&$out)
1183 {
1184 $s = $this->seek();
1185
1186 if ($this->literal('(')) {
1187 if ($this->literal(')')) {
1188 $out = array(Type::T_LIST, '', array());
1189
1190 return true;
1191 }
1192
1193 if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) {
1194 return true;
1195 }
1196
1197 $this->seek($s);
1198
1199 if ($this->map($out)) {
1200 return true;
1201 }
1202
1203 $this->seek($s);
1204 }
1205
1206 if ($this->value($lhs)) {
1207 $out = $this->expHelper($lhs, 0);
1208
1209 return true;
1210 }
1211
1212 return false;
1213 }
1214
1215 /**
1216 * Parse left-hand side of subexpression
1217 *
1218 * @param array $lhs
1219 * @param integer $minP
1220 *
1221 * @return array
1222 */
1223 protected function expHelper($lhs, $minP)
1224 {
1225 $operators = self::$operatorPattern;
1226
1227 $ss = $this->seek();
1228 $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1229 ctype_space($this->buffer[$this->count - 1]);
1230
1231 while ($this->match($operators, $m, false) && self::$precedence[$m[1]] >= $minP) {
1232 $whiteAfter = isset($this->buffer[$this->count]) &&
1233 ctype_space($this->buffer[$this->count]);
1234 $varAfter = isset($this->buffer[$this->count]) &&
1235 $this->buffer[$this->count] === '$';
1236
1237 $this->whitespace();
1238
1239 $op = $m[1];
1240
1241 // don't turn negative numbers into expressions
1242 if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
1243 break;
1244 }
1245
1246 if (! $this->value($rhs)) {
1247 break;
1248 }
1249
1250 // peek and see if rhs belongs to next operator
1251 if ($this->peek($operators, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
1252 $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
1253 }
1254
1255 $lhs = array(Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
1256 $ss = $this->seek();
1257 $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1258 ctype_space($this->buffer[$this->count - 1]);
1259 }
1260
1261 $this->seek($ss);
1262
1263 return $lhs;
1264 }
1265
1266 /**
1267 * Parse value
1268 *
1269 * @param array $out
1270 *
1271 * @return boolean
1272 */
1273 protected function value(&$out)
1274 {
1275 $s = $this->seek();
1276
1277 if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
1278 $out = array(Type::T_UNARY, 'not', $inner, $this->inParens);
1279
1280 return true;
1281 }
1282
1283 $this->seek($s);
1284
1285 if ($this->literal('not', false) && $this->parenValue($inner)) {
1286 $out = array(Type::T_UNARY, 'not', $inner, $this->inParens);
1287
1288 return true;
1289 }
1290
1291 $this->seek($s);
1292
1293 if ($this->literal('+') && $this->value($inner)) {
1294 $out = array(Type::T_UNARY, '+', $inner, $this->inParens);
1295
1296 return true;
1297 }
1298
1299 $this->seek($s);
1300
1301 // negation
1302 if ($this->literal('-', false) &&
1303 ($this->variable($inner) ||
1304 $this->unit($inner) ||
1305 $this->parenValue($inner))
1306 ) {
1307 $out = array(Type::T_UNARY, '-', $inner, $this->inParens);
1308
1309 return true;
1310 }
1311
1312 $this->seek($s);
1313
1314 if ($this->parenValue($out) ||
1315 $this->interpolation($out) ||
1316 $this->variable($out) ||
1317 $this->color($out) ||
1318 $this->unit($out) ||
1319 $this->string($out) ||
1320 $this->func($out) ||
1321 $this->progid($out)
1322 ) {
1323 return true;
1324 }
1325
1326 if ($this->keyword($keyword)) {
1327 if ($keyword === 'null') {
1328 $out = array(Type::T_NULL);
1329 } else {
1330 $out = array(Type::T_KEYWORD, $keyword);
1331 }
1332
1333 return true;
1334 }
1335
1336 return false;
1337 }
1338
1339 /**
1340 * Parse parenthesized value
1341 *
1342 * @param array $out
1343 *
1344 * @return boolean
1345 */
1346 protected function parenValue(&$out)
1347 {
1348 $s = $this->seek();
1349
1350 $inParens = $this->inParens;
1351
1352 if ($this->literal('(')) {
1353 if ($this->literal(')')) {
1354 $out = array(Type::T_LIST, '', array());
1355
1356 return true;
1357 }
1358
1359 $this->inParens = true;
1360
1361 if ($this->expression($exp) && $this->literal(')')) {
1362 $out = $exp;
1363 $this->inParens = $inParens;
1364
1365 return true;
1366 }
1367 }
1368
1369 $this->inParens = $inParens;
1370 $this->seek($s);
1371
1372 return false;
1373 }
1374
1375 /**
1376 * Parse "progid:"
1377 *
1378 * @param array $out
1379 *
1380 * @return boolean
1381 */
1382 protected function progid(&$out)
1383 {
1384 $s = $this->seek();
1385
1386 if ($this->literal('progid:', false) &&
1387 $this->openString('(', $fn) &&
1388 $this->literal('(')
1389 ) {
1390 $this->openString(')', $args, '(');
1391
1392 if ($this->literal(')')) {
1393 $out = array(Type::T_STRING, '', array(
1394 'progid:', $fn, '(', $args, ')'
1395 ));
1396
1397 return true;
1398 }
1399 }
1400
1401 $this->seek($s);
1402
1403 return false;
1404 }
1405
1406 /**
1407 * Parse function call
1408 *
1409 * @param array $out
1410 *
1411 * @return boolean
1412 */
1413 protected function func(&$func)
1414 {
1415 $s = $this->seek();
1416
1417 if ($this->keyword($name, false) &&
1418 $this->literal('(')
1419 ) {
1420 if ($name === 'alpha' && $this->argumentList($args)) {
1421 $func = array(Type::T_FUNCTION, $name, array(Type::T_STRING, '', $args));
1422
1423 return true;
1424 }
1425
1426 if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1427 $ss = $this->seek();
1428
1429 if ($this->argValues($args) && $this->literal(')')) {
1430 $func = array(Type::T_FUNCTION_CALL, $name, $args);
1431
1432 return true;
1433 }
1434
1435 $this->seek($ss);
1436 }
1437
1438 if (($this->openString(')', $str, '(') || true ) &&
1439 $this->literal(')')
1440 ) {
1441 $args = array();
1442
1443 if (! empty($str)) {
1444 $args[] = array(null, array(Type::T_STRING, '', array($str)));
1445 }
1446
1447 $func = array(Type::T_FUNCTION_CALL, $name, $args);
1448
1449 return true;
1450 }
1451 }
1452
1453 $this->seek($s);
1454
1455 return false;
1456 }
1457
1458 /**
1459 * Parse function call argument list
1460 *
1461 * @param array $out
1462 *
1463 * @return boolean
1464 */
1465 protected function argumentList(&$out)
1466 {
1467 $s = $this->seek();
1468 $this->literal('(');
1469
1470 $args = array();
1471
1472 while ($this->keyword($var)) {
1473 $ss = $this->seek();
1474
1475 if ($this->literal('=') && $this->expression($exp)) {
1476 $args[] = array(Type::T_STRING, '', array($var . '='));
1477 $arg = $exp;
1478 } else {
1479 break;
1480 }
1481
1482 $args[] = $arg;
1483
1484 if (! $this->literal(',')) {
1485 break;
1486 }
1487
1488 $args[] = array(Type::T_STRING, '', array(', '));
1489 }
1490
1491 if (! $this->literal(')') || ! count($args)) {
1492 $this->seek($s);
1493
1494 return false;
1495 }
1496
1497 $out = $args;
1498
1499 return true;
1500 }
1501
1502 /**
1503 * Parse mixin/function definition argument list
1504 *
1505 * @param array $out
1506 *
1507 * @return boolean
1508 */
1509 protected function argumentDef(&$out)
1510 {
1511 $s = $this->seek();
1512 $this->literal('(');
1513
1514 $args = array();
1515
1516 while ($this->variable($var)) {
1517 $arg = array($var[1], null, false);
1518
1519 $ss = $this->seek();
1520
1521 if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
1522 $arg[1] = $defaultVal;
1523 } else {
1524 $this->seek($ss);
1525 }
1526
1527 $ss = $this->seek();
1528
1529 if ($this->literal('...')) {
1530 $sss = $this->seek();
1531
1532 if (! $this->literal(')')) {
1533 $this->throwParseError('... has to be after the final argument');
1534 }
1535
1536 $arg[2] = true;
1537 $this->seek($sss);
1538 } else {
1539 $this->seek($ss);
1540 }
1541
1542 $args[] = $arg;
1543
1544 if (! $this->literal(',')) {
1545 break;
1546 }
1547 }
1548
1549 if (! $this->literal(')')) {
1550 $this->seek($s);
1551
1552 return false;
1553 }
1554
1555 $out = $args;
1556
1557 return true;
1558 }
1559
1560 /**
1561 * Parse map
1562 *
1563 * @param array $out
1564 *
1565 * @return boolean
1566 */
1567 protected function map(&$out)
1568 {
1569 $s = $this->seek();
1570
1571 if (! $this->literal('(')) {
1572 return false;
1573 }
1574
1575 $keys = array();
1576 $values = array();
1577
1578 while ($this->genericList($key, 'expression') && $this->literal(':') &&
1579 $this->genericList($value, 'expression')
1580 ) {
1581 $keys[] = $key;
1582 $values[] = $value;
1583
1584 if (! $this->literal(',')) {
1585 break;
1586 }
1587 }
1588
1589 if (! count($keys) || ! $this->literal(')')) {
1590 $this->seek($s);
1591
1592 return false;
1593 }
1594
1595 $out = array(Type::T_MAP, $keys, $values);
1596
1597 return true;
1598 }
1599
1600 /**
1601 * Parse color
1602 *
1603 * @param array $out
1604 *
1605 * @return boolean
1606 */
1607 protected function color(&$out)
1608 {
1609 $color = array(Type::T_COLOR);
1610
1611 if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
1612 if (isset($m[3])) {
1613 $num = hexdec($m[3]);
1614
1615 foreach (array(3, 2, 1) as $i) {
1616 $t = $num & 0xf;
1617 $color[$i] = $t << 4 | $t;
1618 $num >>= 4;
1619 }
1620 } else {
1621 $num = hexdec($m[2]);
1622
1623 foreach (array(3, 2, 1) as $i) {
1624 $color[$i] = $num & 0xff;
1625 $num >>= 8;
1626 }
1627 }
1628
1629 $out = $color;
1630
1631 return true;
1632 }
1633
1634 return false;
1635 }
1636
1637 /**
1638 * Parse number with unit
1639 *
1640 * @param array $out
1641 *
1642 * @return boolean
1643 */
1644 protected function unit(&$unit)
1645 {
1646 if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
1647 $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
1648
1649 return true;
1650 }
1651
1652 return false;
1653 }
1654
1655 /**
1656 * Parse string
1657 *
1658 * @param array $out
1659 *
1660 * @return boolean
1661 */
1662 protected function string(&$out)
1663 {
1664 $s = $this->seek();
1665
1666 if ($this->literal('"', false)) {
1667 $delim = '"';
1668 } elseif ($this->literal('\'', false)) {
1669 $delim = '\'';
1670 } else {
1671 return false;
1672 }
1673
1674 $content = array();
1675 $oldWhite = $this->eatWhiteDefault;
1676 $this->eatWhiteDefault = false;
1677
1678 while ($this->matchString($m, $delim)) {
1679 $content[] = $m[1];
1680
1681 if ($m[2] === '#{') {
1682 $this->count -= strlen($m[2]);
1683
1684 if ($this->interpolation($inter, false)) {
1685 $content[] = $inter;
1686 } else {
1687 $this->count += strlen($m[2]);
1688 $content[] = '#{'; // ignore it
1689 }
1690 } elseif ($m[2] === '\\') {
1691 $content[] = $m[2];
1692
1693 if ($this->literal($delim, false)) {
1694 $content[] = $delim;
1695 }
1696 } else {
1697 $this->count -= strlen($delim);
1698 break; // delim
1699 }
1700 }
1701
1702 $this->eatWhiteDefault = $oldWhite;
1703
1704 if ($this->literal($delim)) {
1705 $out = array(Type::T_STRING, $delim, $content);
1706
1707 return true;
1708 }
1709
1710 $this->seek($s);
1711
1712 return false;
1713 }
1714
1715 /**
1716 * Parse keyword or interpolation
1717 *
1718 * @param array $out
1719 *
1720 * @return boolean
1721 */
1722 protected function mixedKeyword(&$out)
1723 {
1724 $s = $this->seek();
1725
1726 $parts = array();
1727
1728 $oldWhite = $this->eatWhiteDefault;
1729 $this->eatWhiteDefault = false;
1730
1731 for (;;) {
1732 if ($this->keyword($key)) {
1733 $parts[] = $key;
1734 continue;
1735 }
1736
1737 if ($this->interpolation($inter)) {
1738 $parts[] = $inter;
1739 continue;
1740 }
1741
1742 break;
1743 }
1744
1745 $this->eatWhiteDefault = $oldWhite;
1746
1747 if (count($parts) === 0) {
1748 return false;
1749 }
1750
1751 if ($this->eatWhiteDefault) {
1752 $this->whitespace();
1753 }
1754
1755 $out = $parts;
1756
1757 return true;
1758 }
1759
1760 /**
1761 * Parse an unbounded string stopped by $end
1762 *
1763 * @param string $end
1764 * @param array $out
1765 * @param string $nestingOpen
1766 *
1767 * @return boolean
1768 */
1769 protected function openString($end, &$out, $nestingOpen = null)
1770 {
1771 $oldWhite = $this->eatWhiteDefault;
1772 $this->eatWhiteDefault = false;
1773
1774 $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . self::$commentPattern . ')';
1775
1776 $nestingLevel = 0;
1777
1778 $content = array();
1779
1780 while ($this->match($patt, $m, false)) {
1781 if (isset($m[1]) && $m[1] !== '') {
1782 $content[] = $m[1];
1783
1784 if ($nestingOpen) {
1785 $nestingLevel += substr_count($m[1], $nestingOpen);
1786 }
1787 }
1788
1789 $tok = $m[2];
1790
1791 $this->count-= strlen($tok);
1792
1793 if ($tok === $end && ! $nestingLevel--) {
1794 break;
1795 }
1796
1797 if (($tok === '\'' || $tok === '"') && $this->string($str)) {
1798 $content[] = $str;
1799 continue;
1800 }
1801
1802 if ($tok === '#{' && $this->interpolation($inter)) {
1803 $content[] = $inter;
1804 continue;
1805 }
1806
1807 $content[] = $tok;
1808 $this->count+= strlen($tok);
1809 }
1810
1811 $this->eatWhiteDefault = $oldWhite;
1812
1813 if (count($content) === 0) {
1814 return false;
1815 }
1816
1817 // trim the end
1818 if (is_string(end($content))) {
1819 $content[count($content) - 1] = rtrim(end($content));
1820 }
1821
1822 $out = array(Type::T_STRING, '', $content);
1823
1824 return true;
1825 }
1826
1827 /**
1828 * Parser interpolation
1829 *
1830 * @param array $out
1831 * @param boolean $lookWhite save information about whitespace before and after
1832 *
1833 * @return boolean
1834 */
1835 protected function interpolation(&$out, $lookWhite = true)
1836 {
1837 $oldWhite = $this->eatWhiteDefault;
1838 $this->eatWhiteDefault = true;
1839
1840 $s = $this->seek();
1841
1842 if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
1843 // TODO: don't error if out of bounds
1844
1845 if ($lookWhite) {
1846 $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
1847 $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
1848 } else {
1849 $left = $right = false;
1850 }
1851
1852 $out = array(Type::T_INTERPOLATE, $value, $left, $right);
1853 $this->eatWhiteDefault = $oldWhite;
1854
1855 if ($this->eatWhiteDefault) {
1856 $this->whitespace();
1857 }
1858
1859 return true;
1860 }
1861
1862 $this->seek($s);
1863 $this->eatWhiteDefault = $oldWhite;
1864
1865 return false;
1866 }
1867
1868 /**
1869 * Parse property name (as an array of parts or a string)
1870 *
1871 * @param array $out
1872 *
1873 * @return boolean
1874 */
1875 protected function propertyName(&$out)
1876 {
1877 $s = $this->seek();
1878 $parts = array();
1879
1880 $oldWhite = $this->eatWhiteDefault;
1881 $this->eatWhiteDefault = false;
1882
1883 for (;;) {
1884 if ($this->interpolation($inter)) {
1885 $parts[] = $inter;
1886 continue;
1887 }
1888
1889 if ($this->keyword($text)) {
1890 $parts[] = $text;
1891 continue;
1892 }
1893
1894 if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
1895 // css hacks
1896 $parts[] = $m[0];
1897 continue;
1898 }
1899
1900 break;
1901 }
1902
1903 $this->eatWhiteDefault = $oldWhite;
1904
1905 if (count($parts) === 0) {
1906 return false;
1907 }
1908
1909 // match comment hack
1910 if (preg_match(
1911 self::$whitePattern,
1912 $this->buffer,
1913 $m,
1914 null,
1915 $this->count
1916 )) {
1917 if (! empty($m[0])) {
1918 $parts[] = $m[0];
1919 $this->count += strlen($m[0]);
1920 }
1921 }
1922
1923 $this->whitespace(); // get any extra whitespace
1924
1925 $out = array(Type::T_STRING, '', $parts);
1926
1927 return true;
1928 }
1929
1930 /**
1931 * Parse comma separated selector list
1932 *
1933 * @param array $out
1934 *
1935 * @return boolean
1936 */
1937 protected function selectors(&$out)
1938 {
1939 $s = $this->seek();
1940 $selectors = array();
1941
1942 while ($this->selector($sel)) {
1943 $selectors[] = $sel;
1944
1945 if (! $this->literal(',')) {
1946 break;
1947 }
1948
1949 while ($this->literal(',')) {
1950 ; // ignore extra
1951 }
1952 }
1953
1954 if (count($selectors) === 0) {
1955 $this->seek($s);
1956
1957 return false;
1958 }
1959
1960 $out = $selectors;
1961
1962 return true;
1963 }
1964
1965 /**
1966 * Parse whitespace separated selector list
1967 *
1968 * @param array $out
1969 *
1970 * @return boolean
1971 */
1972 protected function selector(&$out)
1973 {
1974 $selector = array();
1975
1976 for (;;) {
1977 if ($this->match('[>+~]+', $m)) {
1978 $selector[] = array($m[0]);
1979 continue;
1980 }
1981
1982 if ($this->selectorSingle($part)) {
1983 $selector[] = $part;
1984 $this->match('\s+', $m);
1985 continue;
1986 }
1987
1988 if ($this->match('\/[^\/]+\/', $m)) {
1989 $selector[] = array($m[0]);
1990 continue;
1991 }
1992
1993 break;
1994 }
1995
1996 if (count($selector) === 0) {
1997 return false;
1998 }
1999
2000 $out = $selector;
2001 return true;
2002 }
2003
2004 /**
2005 * Parse the parts that make up a selector
2006 *
2007 * {@internal
2008 * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
2009 * }}
2010 *
2011 * @param array $out
2012 *
2013 * @return boolean
2014 */
2015 protected function selectorSingle(&$out)
2016 {
2017 $oldWhite = $this->eatWhiteDefault;
2018 $this->eatWhiteDefault = false;
2019
2020 $parts = array();
2021
2022 if ($this->literal('*', false)) {
2023 $parts[] = '*';
2024 }
2025
2026 for (;;) {
2027 // see if we can stop early
2028 if ($this->match('\s*[{,]', $m)) {
2029 $this->count--;
2030 break;
2031 }
2032
2033 $s = $this->seek();
2034
2035 // self
2036 if ($this->literal('&', false)) {
2037 $parts[] = Compiler::$selfSelector;
2038 continue;
2039 }
2040
2041 if ($this->literal('.', false)) {
2042 $parts[] = '.';
2043 continue;
2044 }
2045
2046 if ($this->literal('|', false)) {
2047 $parts[] = '|';
2048 continue;
2049 }
2050
2051 if ($this->match('\\\\\S', $m)) {
2052 $parts[] = $m[0];
2053 continue;
2054 }
2055
2056 // for keyframes
2057 if ($this->unit($unit)) {
2058 $parts[] = $unit;
2059 continue;
2060 }
2061
2062 if ($this->keyword($name)) {
2063 $parts[] = $name;
2064 continue;
2065 }
2066
2067 if ($this->interpolation($inter)) {
2068 $parts[] = $inter;
2069 continue;
2070 }
2071
2072 if ($this->literal('%', false) && $this->placeholder($placeholder)) {
2073 $parts[] = '%';
2074 $parts[] = $placeholder;
2075 continue;
2076 }
2077
2078 if ($this->literal('#', false)) {
2079 $parts[] = '#';
2080 continue;
2081 }
2082
2083 // a pseudo selector
2084 if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
2085 $parts[] = $m[0];
2086
2087 foreach ($nameParts as $sub) {
2088 $parts[] = $sub;
2089 }
2090
2091 $ss = $this->seek();
2092
2093 if ($this->literal('(') &&
2094 ($this->openString(')', $str, '(') || true ) &&
2095 $this->literal(')')
2096 ) {
2097 $parts[] = '(';
2098
2099 if (! empty($str)) {
2100 $parts[] = $str;
2101 }
2102
2103 $parts[] = ')';
2104 } else {
2105 $this->seek($ss);
2106 }
2107
2108 continue;
2109 }
2110
2111 $this->seek($s);
2112
2113 // attribute selector
2114 // TODO: replace with open string?
2115 if ($this->literal('[', false)) {
2116 $attrParts = array('[');
2117
2118 // keyword, string, operator
2119 for (;;) {
2120 if ($this->literal(']', false)) {
2121 $this->count--;
2122 break; // get out early
2123 }
2124
2125 if ($this->match('\s+', $m)) {
2126 $attrParts[] = ' ';
2127 continue;
2128 }
2129
2130 if ($this->string($str)) {
2131 $attrParts[] = $str;
2132 continue;
2133 }
2134
2135 if ($this->keyword($word)) {
2136 $attrParts[] = $word;
2137 continue;
2138 }
2139
2140 if ($this->interpolation($inter, false)) {
2141 $attrParts[] = $inter;
2142 continue;
2143 }
2144
2145 // operator, handles attr namespace too
2146 if ($this->match('[|-~\$\*\^=]+', $m)) {
2147 $attrParts[] = $m[0];
2148 continue;
2149 }
2150
2151 break;
2152 }
2153
2154 if ($this->literal(']', false)) {
2155 $attrParts[] = ']';
2156
2157 foreach ($attrParts as $part) {
2158 $parts[] = $part;
2159 }
2160
2161 continue;
2162 }
2163
2164 $this->seek($s);
2165 // TODO: should just break here?
2166 }
2167
2168 break;
2169 }
2170
2171 $this->eatWhiteDefault = $oldWhite;
2172
2173 if (count($parts) === 0) {
2174 return false;
2175 }
2176
2177 $out = $parts;
2178
2179 return true;
2180 }
2181
2182 /**
2183 * Parse a variable
2184 *
2185 * @param array $out
2186 *
2187 * @return boolean
2188 */
2189 protected function variable(&$out)
2190 {
2191 $s = $this->seek();
2192
2193 if ($this->literal('$', false) && $this->keyword($name)) {
2194 $out = array(Type::T_VARIABLE, $name);
2195
2196 return true;
2197 }
2198
2199 $this->seek($s);
2200
2201 return false;
2202 }
2203
2204 /**
2205 * Parse a keyword
2206 *
2207 * @param string $word
2208 * @param boolean $eatWhitespace
2209 *
2210 * @return boolean
2211 */
2212 protected function keyword(&$word, $eatWhitespace = null)
2213 {
2214 if ($this->match(
2215 '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2216 $m,
2217 $eatWhitespace
2218 )) {
2219 $word = $m[1];
2220
2221 return true;
2222 }
2223
2224 return false;
2225 }
2226
2227 /**
2228 * Parse a placeholder
2229 *
2230 * @param string $placeholder
2231 *
2232 * @return boolean
2233 */
2234 protected function placeholder(&$placeholder)
2235 {
2236 if ($this->match('([\w\-_]+|#[{][$][\w\-_]+[}])', $m)) {
2237 $placeholder = $m[1];
2238
2239 return true;
2240 }
2241
2242 return false;
2243 }
2244
2245 /**
2246 * Parse a url
2247 *
2248 * @param array $out
2249 *
2250 * @return boolean
2251 */
2252 protected function url(&$out)
2253 {
2254 if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
2255 $out = array(Type::T_STRING, '', array('url(' . $m[2] . $m[3] . $m[2] . ')'));
2256
2257 return true;
2258 }
2259
2260 return false;
2261 }
2262
2263 /**
2264 * Consume an end of statement delimiter
2265 *
2266 * @return boolean
2267 */
2268 protected function end()
2269 {
2270 if ($this->literal(';')) {
2271 return true;
2272 }
2273
2274 if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
2275 // if there is end of file or a closing block next then we don't need a ;
2276 return true;
2277 }
2278
2279 return false;
2280 }
2281
2282 /**
2283 * Strip assignment flag from the list
2284 *
2285 * @param array $value
2286 *
2287 * @return string
2288 */
2289 protected function stripAssignmentFlag(&$value)
2290 {
2291 $token = &$value;
2292
2293 for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
2294 $lastNode = &$token[2][$s - 1];
2295
2296 if ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], array('!default', '!global'))) {
2297 array_pop($token[2]);
2298
2299 $token = $this->flattenList($token);
2300
2301 return $lastNode[1];
2302 }
2303 }
2304
2305 return false;
2306 }
2307
2308 /**
2309 * Turn list of length 1 into value type
2310 *
2311 * @param array $value
2312 *
2313 * @return array
2314 */
2315 protected function flattenList($value)
2316 {
2317 if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
2318 return $this->flattenList($value[2][0]);
2319 }
2320
2321 return $value;
2322 }
2323
2324 /**
2325 * @deprecated
2326 *
2327 * {@internal
2328 * advance counter to next occurrence of $what
2329 * $until - don't include $what in advance
2330 * $allowNewline, if string, will be used as valid char set
2331 * }}
2332 */
2333 protected function to($what, &$out, $until = false, $allowNewline = false)
2334 {
2335 if (is_string($allowNewline)) {
2336 $validChars = $allowNewline;
2337 } else {
2338 $validChars = $allowNewline ? '.' : "[^\n]";
2339 }
2340
2341 if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
2342 return false;
2343 }
2344
2345 if ($until) {
2346 $this->count -= strlen($what); // give back $what
2347 }
2348
2349 $out = $m[1];
2350
2351 return true;
2352 }
2353
2354 /**
2355 * @deprecated
2356 */
2357 protected function show()
2358 {
2359 if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
2360 return $m[1];
2361 }
2362
2363 return '';
2364 }
2365
2366 /**
2367 * Quote regular expression
2368 *
2369 * @param string $what
2370 *
2371 * @return string
2372 */
2373 private function pregQuote($what)
2374 {
2375 return preg_quote($what, '/');
2376 }
2377 }
2378