Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
1 <?php 2 3 /** 4 * SCSSPHP 5 * 6 * @copyright 2012-2020 Leaf Corcoran 7 * 8 * @license http://opensource.org/licenses/MIT MIT 9 * 10 * @link http://scssphp.github.io/scssphp 11 */ 12 13 namespace ScssPhp\ScssPhp; 14 15 use ScssPhp\ScssPhp\Exception\ParserException; 16 17 /** 18 * Parser 19 * 20 * @author Leaf Corcoran <leafot@gmail.com> 21 */ 22 class Parser 23 { 24 const SOURCE_INDEX = -1; 25 const SOURCE_LINE = -2; 26 const SOURCE_COLUMN = -3; 27 28 /** 29 * @var array<string, int> 30 */ 31 protected static $precedence = [ 32 '=' => 0, 33 'or' => 1, 34 'and' => 2, 35 '==' => 3, 36 '!=' => 3, 37 '<=' => 4, 38 '>=' => 4, 39 '<' => 4, 40 '>' => 4, 41 '+' => 5, 42 '-' => 5, 43 '*' => 6, 44 '/' => 6, 45 '%' => 6, 46 ]; 47 48 /** 49 * @var string 50 */ 51 protected static $commentPattern; 52 /** 53 * @var string 54 */ 55 protected static $operatorPattern; 56 /** 57 * @var string 58 */ 59 protected static $whitePattern; 60 61 /** 62 * @var Cache|null 63 */ 64 protected $cache; 65 66 private $sourceName; 67 private $sourceIndex; 68 /** 69 * @var array<int, int> 70 */ 71 private $sourcePositions; 72 /** 73 * @var array|null 74 */ 75 private $charset; 76 /** 77 * The current offset in the buffer 78 * 79 * @var int 80 */ 81 private $count; 82 /** 83 * @var Block 84 */ 85 private $env; 86 /** 87 * @var bool 88 */ 89 private $inParens; 90 /** 91 * @var bool 92 */ 93 private $eatWhiteDefault; 94 /** 95 * @var bool 96 */ 97 private $discardComments; 98 private $allowVars; 99 /** 100 * @var string 101 */ 102 private $buffer; 103 private $utf8; 104 /** 105 * @var string|null 106 */ 107 private $encoding; 108 private $patternModifiers; 109 private $commentsSeen; 110 111 private $cssOnly; 112 113 /** 114 * Constructor 115 * 116 * @api 117 * 118 * @param string $sourceName 119 * @param integer $sourceIndex 120 * @param string|null $encoding 121 * @param Cache|null $cache 122 * @param bool $cssOnly 123 */ 124 public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false) 125 { 126 $this->sourceName = $sourceName ?: '(stdin)'; 127 $this->sourceIndex = $sourceIndex; 128 $this->charset = null; 129 $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; 130 $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; 131 $this->commentsSeen = []; 132 $this->commentsSeen = []; 133 $this->allowVars = true; 134 $this->cssOnly = $cssOnly; 135 136 if (empty(static::$operatorPattern)) { 137 static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)'; 138 139 $commentSingle = '\/\/'; 140 $commentMultiLeft = '\/\*'; 141 $commentMultiRight = '\*\/'; 142 143 static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight; 144 static::$whitePattern = $this->utf8 145 ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS' 146 : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS'; 147 } 148 149 $this->cache = $cache; 150 } 151 152 /** 153 * Get source file name 154 * 155 * @api 156 * 157 * @return string 158 */ 159 public function getSourceName() 160 { 161 return $this->sourceName; 162 } 163 164 /** 165 * Throw parser error 166 * 167 * @api 168 * 169 * @param string $msg 170 * 171 * @throws ParserException 172 * 173 * @deprecated use "parseError" and throw the exception in the caller instead. 174 */ 175 public function throwParseError($msg = 'parse error') 176 { 177 @trigger_error( 178 'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead', 179 E_USER_DEPRECATED 180 ); 181 182 throw $this->parseError($msg); 183 } 184 185 /** 186 * Creates a parser error 187 * 188 * @api 189 * 190 * @param string $msg 191 * 192 * @return ParserException 193 */ 194 public function parseError($msg = 'parse error') 195 { 196 list($line, $column) = $this->getSourcePosition($this->count); 197 198 $loc = empty($this->sourceName) 199 ? "line: $line, column: $column" 200 : "$this->sourceName on line $line, at column $column"; 201 202 if ($this->peek('(.*?)(\n|$)', $m, $this->count)) { 203 $this->restoreEncoding(); 204 205 $e = new ParserException("$msg: failed at `$m[1]` $loc"); 206 $e->setSourcePosition([$this->sourceName, $line, $column]); 207 208 return $e; 209 } 210 211 $this->restoreEncoding(); 212 213 $e = new ParserException("$msg: $loc"); 214 $e->setSourcePosition([$this->sourceName, $line, $column]); 215 216 return $e; 217 } 218 219 /** 220 * Parser buffer 221 * 222 * @api 223 * 224 * @param string $buffer 225 * 226 * @return Block 227 */ 228 public function parse($buffer) 229 { 230 if ($this->cache) { 231 $cacheKey = $this->sourceName . ':' . md5($buffer); 232 $parseOptions = [ 233 'charset' => $this->charset, 234 'utf8' => $this->utf8, 235 ]; 236 $v = $this->cache->getCache('parse', $cacheKey, $parseOptions); 237 238 if (! \is_null($v)) { 239 return $v; 240 } 241 } 242 243 // strip BOM (byte order marker) 244 if (substr($buffer, 0, 3) === "\xef\xbb\xbf") { 245 $buffer = substr($buffer, 3); 246 } 247 248 $this->buffer = rtrim($buffer, "\x00..\x1f"); 249 $this->count = 0; 250 $this->env = null; 251 $this->inParens = false; 252 $this->eatWhiteDefault = true; 253 254 $this->saveEncoding(); 255 $this->extractLineNumbers($buffer); 256 257 $this->pushBlock(null); // root block 258 $this->whitespace(); 259 $this->pushBlock(null); 260 $this->popBlock(); 261 262 while ($this->parseChunk()) { 263 ; 264 } 265 266 if ($this->count !== \strlen($this->buffer)) { 267 throw $this->parseError(); 268 } 269 270 if (! empty($this->env->parent)) { 271 throw $this->parseError('unclosed block'); 272 } 273 274 if ($this->charset) { 275 array_unshift($this->env->children, $this->charset); 276 } 277 278 $this->restoreEncoding(); 279 280 if ($this->cache) { 281 $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions); 282 } 283 284 return $this->env; 285 } 286 287 /** 288 * Parse a value or value list 289 * 290 * @api 291 * 292 * @param string $buffer 293 * @param string|array $out 294 * 295 * @return boolean 296 */ 297 public function parseValue($buffer, &$out) 298 { 299 $this->count = 0; 300 $this->env = null; 301 $this->inParens = false; 302 $this->eatWhiteDefault = true; 303 $this->buffer = (string) $buffer; 304 305 $this->saveEncoding(); 306 $this->extractLineNumbers($this->buffer); 307 308 $list = $this->valueList($out); 309 310 $this->restoreEncoding(); 311 312 return $list; 313 } 314 315 /** 316 * Parse a selector or selector list 317 * 318 * @api 319 * 320 * @param string $buffer 321 * @param string|array $out 322 * @param bool $shouldValidate 323 * 324 * @return boolean 325 */ 326 public function parseSelector($buffer, &$out, $shouldValidate = true) 327 { 328 $this->count = 0; 329 $this->env = null; 330 $this->inParens = false; 331 $this->eatWhiteDefault = true; 332 $this->buffer = (string) $buffer; 333 334 $this->saveEncoding(); 335 $this->extractLineNumbers($this->buffer); 336 337 // discard space/comments at the start 338 $this->discardComments = true; 339 $this->whitespace(); 340 $this->discardComments = false; 341 342 $selector = $this->selectors($out); 343 344 $this->restoreEncoding(); 345 346 if ($shouldValidate && $this->count !== strlen($buffer)) { 347 throw $this->parseError("`" . substr($buffer, $this->count) . "` is not a valid Selector in `$buffer`"); 348 } 349 350 return $selector; 351 } 352 353 /** 354 * Parse a media Query 355 * 356 * @api 357 * 358 * @param string $buffer 359 * @param string|array $out 360 * 361 * @return boolean 362 */ 363 public function parseMediaQueryList($buffer, &$out) 364 { 365 $this->count = 0; 366 $this->env = null; 367 $this->inParens = false; 368 $this->eatWhiteDefault = true; 369 $this->buffer = (string) $buffer; 370 371 $this->saveEncoding(); 372 $this->extractLineNumbers($this->buffer); 373 374 $isMediaQuery = $this->mediaQueryList($out); 375 376 $this->restoreEncoding(); 377 378 return $isMediaQuery; 379 } 380 381 /** 382 * Parse a single chunk off the head of the buffer and append it to the 383 * current parse environment. 384 * 385 * Returns false when the buffer is empty, or when there is an error. 386 * 387 * This function is called repeatedly until the entire document is 388 * parsed. 389 * 390 * This parser is most similar to a recursive descent parser. Single 391 * functions represent discrete grammatical rules for the language, and 392 * they are able to capture the text that represents those rules. 393 * 394 * Consider the function Compiler::keyword(). (All parse functions are 395 * structured the same.) 396 * 397 * The function takes a single reference argument. When calling the 398 * function it will attempt to match a keyword on the head of the buffer. 399 * If it is successful, it will place the keyword in the referenced 400 * argument, advance the position in the buffer, and return true. If it 401 * fails then it won't advance the buffer and it will return false. 402 * 403 * All of these parse functions are powered by Compiler::match(), which behaves 404 * the same way, but takes a literal regular expression. Sometimes it is 405 * more convenient to use match instead of creating a new function. 406 * 407 * Because of the format of the functions, to parse an entire string of 408 * grammatical rules, you can chain them together using &&. 409 * 410 * But, if some of the rules in the chain succeed before one fails, then 411 * the buffer position will be left at an invalid state. In order to 412 * avoid this, Compiler::seek() is used to remember and set buffer positions. 413 * 414 * Before parsing a chain, use $s = $this->count to remember the current 415 * position into $s. Then if a chain fails, use $this->seek($s) to 416 * go back where we started. 417 * 418 * @return boolean 419 */ 420 protected function parseChunk() 421 { 422 $s = $this->count; 423 424 // the directives 425 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') { 426 if ( 427 $this->literal('@at-root', 8) && 428 ($this->selectors($selector) || true) && 429 ($this->map($with) || true) && 430 (($this->matchChar('(') && 431 $this->interpolation($with) && 432 $this->matchChar(')')) || true) && 433 $this->matchChar('{', false) 434 ) { 435 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 436 437 $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s); 438 $atRoot->selector = $selector; 439 $atRoot->with = $with; 440 441 return true; 442 } 443 444 $this->seek($s); 445 446 if ( 447 $this->literal('@media', 6) && 448 $this->mediaQueryList($mediaQueryList) && 449 $this->matchChar('{', false) 450 ) { 451 $media = $this->pushSpecialBlock(Type::T_MEDIA, $s); 452 $media->queryList = $mediaQueryList[2]; 453 454 return true; 455 } 456 457 $this->seek($s); 458 459 if ( 460 $this->literal('@mixin', 6) && 461 $this->keyword($mixinName) && 462 ($this->argumentDef($args) || true) && 463 $this->matchChar('{', false) 464 ) { 465 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 466 467 $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s); 468 $mixin->name = $mixinName; 469 $mixin->args = $args; 470 471 return true; 472 } 473 474 $this->seek($s); 475 476 if ( 477 ($this->literal('@include', 8) && 478 $this->keyword($mixinName) && 479 ($this->matchChar('(') && 480 ($this->argValues($argValues) || true) && 481 $this->matchChar(')') || true) && 482 ($this->end()) || 483 ($this->literal('using', 5) && 484 $this->argumentDef($argUsing) && 485 ($this->end() || $this->matchChar('{') && $hasBlock = true)) || 486 $this->matchChar('{') && $hasBlock = true) 487 ) { 488 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 489 490 $child = [ 491 Type::T_INCLUDE, 492 $mixinName, 493 isset($argValues) ? $argValues : null, 494 null, 495 isset($argUsing) ? $argUsing : null 496 ]; 497 498 if (! empty($hasBlock)) { 499 $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s); 500 $include->child = $child; 501 } else { 502 $this->append($child, $s); 503 } 504 505 return true; 506 } 507 508 $this->seek($s); 509 510 if ( 511 $this->literal('@scssphp-import-once', 20) && 512 $this->valueList($importPath) && 513 $this->end() 514 ) { 515 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 516 517 $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s); 518 519 return true; 520 } 521 522 $this->seek($s); 523 524 if ( 525 $this->literal('@import', 7) && 526 $this->valueList($importPath) && 527 $importPath[0] !== Type::T_FUNCTION_CALL && 528 $this->end() 529 ) { 530 if ($this->cssOnly) { 531 $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); 532 $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); 533 return true; 534 } 535 536 $this->append([Type::T_IMPORT, $importPath], $s); 537 538 return true; 539 } 540 541 $this->seek($s); 542 543 if ( 544 $this->literal('@import', 7) && 545 $this->url($importPath) && 546 $this->end() 547 ) { 548 if ($this->cssOnly) { 549 $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); 550 $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); 551 return true; 552 } 553 554 $this->append([Type::T_IMPORT, $importPath], $s); 555 556 return true; 557 } 558 559 $this->seek($s); 560 561 if ( 562 $this->literal('@extend', 7) && 563 $this->selectors($selectors) && 564 $this->end() 565 ) { 566 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 567 568 // check for '!flag' 569 $optional = $this->stripOptionalFlag($selectors); 570 $this->append([Type::T_EXTEND, $selectors, $optional], $s); 571 572 return true; 573 } 574 575 $this->seek($s); 576 577 if ( 578 $this->literal('@function', 9) && 579 $this->keyword($fnName) && 580 $this->argumentDef($args) && 581 $this->matchChar('{', false) 582 ) { 583 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 584 585 $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s); 586 $func->name = $fnName; 587 $func->args = $args; 588 589 return true; 590 } 591 592 $this->seek($s); 593 594 if ( 595 $this->literal('@return', 7) && 596 ($this->valueList($retVal) || true) && 597 $this->end() 598 ) { 599 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 600 601 $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s); 602 603 return true; 604 } 605 606 $this->seek($s); 607 608 if ( 609 $this->literal('@each', 5) && 610 $this->genericList($varNames, 'variable', ',', false) && 611 $this->literal('in', 2) && 612 $this->valueList($list) && 613 $this->matchChar('{', false) 614 ) { 615 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 616 617 $each = $this->pushSpecialBlock(Type::T_EACH, $s); 618 619 foreach ($varNames[2] as $varName) { 620 $each->vars[] = $varName[1]; 621 } 622 623 $each->list = $list; 624 625 return true; 626 } 627 628 $this->seek($s); 629 630 if ( 631 $this->literal('@while', 6) && 632 $this->expression($cond) && 633 $this->matchChar('{', false) 634 ) { 635 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 636 637 while ( 638 $cond[0] === Type::T_LIST && 639 ! empty($cond['enclosing']) && 640 $cond['enclosing'] === 'parent' && 641 \count($cond[2]) == 1 642 ) { 643 $cond = reset($cond[2]); 644 } 645 646 $while = $this->pushSpecialBlock(Type::T_WHILE, $s); 647 $while->cond = $cond; 648 649 return true; 650 } 651 652 $this->seek($s); 653 654 if ( 655 $this->literal('@for', 4) && 656 $this->variable($varName) && 657 $this->literal('from', 4) && 658 $this->expression($start) && 659 ($this->literal('through', 7) || 660 ($forUntil = true && $this->literal('to', 2))) && 661 $this->expression($end) && 662 $this->matchChar('{', false) 663 ) { 664 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 665 666 $for = $this->pushSpecialBlock(Type::T_FOR, $s); 667 $for->var = $varName[1]; 668 $for->start = $start; 669 $for->end = $end; 670 $for->until = isset($forUntil); 671 672 return true; 673 } 674 675 $this->seek($s); 676 677 if ( 678 $this->literal('@if', 3) && 679 $this->functionCallArgumentsList($cond, false, '{', false) 680 ) { 681 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 682 683 $if = $this->pushSpecialBlock(Type::T_IF, $s); 684 685 while ( 686 $cond[0] === Type::T_LIST && 687 ! empty($cond['enclosing']) && 688 $cond['enclosing'] === 'parent' && 689 \count($cond[2]) == 1 690 ) { 691 $cond = reset($cond[2]); 692 } 693 694 $if->cond = $cond; 695 $if->cases = []; 696 697 return true; 698 } 699 700 $this->seek($s); 701 702 if ( 703 $this->literal('@debug', 6) && 704 $this->functionCallArgumentsList($value, false) 705 ) { 706 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 707 708 $this->append([Type::T_DEBUG, $value], $s); 709 710 return true; 711 } 712 713 $this->seek($s); 714 715 if ( 716 $this->literal('@warn', 5) && 717 $this->functionCallArgumentsList($value, false) 718 ) { 719 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 720 721 $this->append([Type::T_WARN, $value], $s); 722 723 return true; 724 } 725 726 $this->seek($s); 727 728 if ( 729 $this->literal('@error', 6) && 730 $this->functionCallArgumentsList($value, false) 731 ) { 732 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 733 734 $this->append([Type::T_ERROR, $value], $s); 735 736 return true; 737 } 738 739 $this->seek($s); 740 741 if ( 742 $this->literal('@content', 8) && 743 ($this->end() || 744 $this->matchChar('(') && 745 $this->argValues($argContent) && 746 $this->matchChar(')') && 747 $this->end()) 748 ) { 749 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 750 751 $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s); 752 753 return true; 754 } 755 756 $this->seek($s); 757 758 $last = $this->last(); 759 760 if (isset($last) && $last[0] === Type::T_IF) { 761 list(, $if) = $last; 762 763 if ($this->literal('@else', 5)) { 764 if ($this->matchChar('{', false)) { 765 $else = $this->pushSpecialBlock(Type::T_ELSE, $s); 766 } elseif ( 767 $this->literal('if', 2) && 768 $this->functionCallArgumentsList($cond, false, '{', false) 769 ) { 770 $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s); 771 $else->cond = $cond; 772 } 773 774 if (isset($else)) { 775 $else->dontAppend = true; 776 $if->cases[] = $else; 777 778 return true; 779 } 780 } 781 782 $this->seek($s); 783 } 784 785 // only retain the first @charset directive encountered 786 if ( 787 $this->literal('@charset', 8) && 788 $this->valueList($charset) && 789 $this->end() 790 ) { 791 if (! isset($this->charset)) { 792 $statement = [Type::T_CHARSET, $charset]; 793 794 list($line, $column) = $this->getSourcePosition($s); 795 796 $statement[static::SOURCE_LINE] = $line; 797 $statement[static::SOURCE_COLUMN] = $column; 798 $statement[static::SOURCE_INDEX] = $this->sourceIndex; 799 800 $this->charset = $statement; 801 } 802 803 return true; 804 } 805 806 $this->seek($s); 807 808 if ( 809 $this->literal('@supports', 9) && 810 ($t1 = $this->supportsQuery($supportQuery)) && 811 ($t2 = $this->matchChar('{', false)) 812 ) { 813 $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); 814 $directive->name = 'supports'; 815 $directive->value = $supportQuery; 816 817 return true; 818 } 819 820 $this->seek($s); 821 822 // doesn't match built in directive, do generic one 823 if ( 824 $this->matchChar('@', false) && 825 $this->mixedKeyword($dirName) && 826 $this->directiveValue($dirValue, '{') 827 ) { 828 if (count($dirName) === 1 && is_string(reset($dirName))) { 829 $dirName = reset($dirName); 830 } else { 831 $dirName = [Type::T_STRING, '', $dirName]; 832 } 833 if ($dirName === 'media') { 834 $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s); 835 } else { 836 $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); 837 $directive->name = $dirName; 838 } 839 840 if (isset($dirValue)) { 841 ! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue)); 842 $directive->value = $dirValue; 843 } 844 845 return true; 846 } 847 848 $this->seek($s); 849 850 // maybe it's a generic blockless directive 851 if ( 852 $this->matchChar('@', false) && 853 $this->mixedKeyword($dirName) && 854 ! $this->isKnownGenericDirective($dirName) && 855 ($this->end(false) || ($this->directiveValue($dirValue, '') && $this->end(false))) 856 ) { 857 if (\count($dirName) === 1 && \is_string(\reset($dirName))) { 858 $dirName = \reset($dirName); 859 } else { 860 $dirName = [Type::T_STRING, '', $dirName]; 861 } 862 if ( 863 ! empty($this->env->parent) && 864 $this->env->type && 865 ! \in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA]) 866 ) { 867 $plain = \trim(\substr($this->buffer, $s, $this->count - $s)); 868 throw $this->parseError( 869 "Unknown directive `{$plain}` not allowed in `" . $this->env->type . "` block" 870 ); 871 } 872 // blockless directives with a blank line after keeps their blank lines after 873 // sass-spec compliance purpose 874 $s = $this->count; 875 $hasBlankLine = false; 876 if ($this->match('\s*?\n\s*\n', $out, false)) { 877 $hasBlankLine = true; 878 $this->seek($s); 879 } 880 $isNotRoot = ! empty($this->env->parent); 881 $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s); 882 $this->whitespace(); 883 884 return true; 885 } 886 887 $this->seek($s); 888 889 return false; 890 } 891 892 $inCssSelector = null; 893 if ($this->cssOnly) { 894 $inCssSelector = (! empty($this->env->parent) && 895 ! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])); 896 } 897 // custom properties : right part is static 898 if (($this->customProperty($name) ) && $this->matchChar(':', false)) { 899 $start = $this->count; 900 901 // but can be complex and finish with ; or } 902 foreach ([';','}'] as $ending) { 903 if ( 904 $this->openString($ending, $stringValue, '(', ')', false) && 905 $this->end() 906 ) { 907 $end = $this->count; 908 $value = $stringValue; 909 910 // check if we have only a partial value due to nested [] or { } to take in account 911 $nestingPairs = [['[', ']'], ['{', '}']]; 912 913 foreach ($nestingPairs as $nestingPair) { 914 $p = strpos($this->buffer, $nestingPair[0], $start); 915 916 if ($p && $p < $end) { 917 $this->seek($start); 918 919 if ( 920 $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) && 921 $this->end() && 922 $this->count > $end 923 ) { 924 $end = $this->count; 925 $value = $stringValue; 926 } 927 } 928 } 929 930 $this->seek($end); 931 $this->append([Type::T_CUSTOM_PROPERTY, $name, $value], $s); 932 933 return true; 934 } 935 } 936 937 // TODO: output an error here if nothing found according to sass spec 938 } 939 940 $this->seek($s); 941 942 // property shortcut 943 // captures most properties before having to parse a selector 944 if ( 945 $this->keyword($name, false) && 946 $this->literal(': ', 2) && 947 $this->valueList($value) && 948 $this->end() 949 ) { 950 $name = [Type::T_STRING, '', [$name]]; 951 $this->append([Type::T_ASSIGN, $name, $value], $s); 952 953 return true; 954 } 955 956 $this->seek($s); 957 958 // variable assigns 959 if ( 960 $this->variable($name) && 961 $this->matchChar(':') && 962 $this->valueList($value) && 963 $this->end() 964 ) { 965 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 966 967 // check for '!flag' 968 $assignmentFlags = $this->stripAssignmentFlags($value); 969 $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s); 970 971 return true; 972 } 973 974 $this->seek($s); 975 976 // misc 977 if ($this->literal('-->', 3)) { 978 return true; 979 } 980 981 // opening css block 982 if ( 983 $this->selectors($selectors) && 984 $this->matchChar('{', false) 985 ) { 986 ! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false); 987 988 $this->pushBlock($selectors, $s); 989 990 if ($this->eatWhiteDefault) { 991 $this->whitespace(); 992 $this->append(null); // collect comments at the beginning if needed 993 } 994 995 return true; 996 } 997 998 $this->seek($s); 999 1000 // property assign, or nested assign 1001 if ( 1002 $this->propertyName($name) && 1003 $this->matchChar(':') 1004 ) { 1005 $foundSomething = false; 1006 1007 if ($this->valueList($value)) { 1008 if (empty($this->env->parent)) { 1009 throw $this->parseError('expected "{"'); 1010 } 1011 1012 $this->append([Type::T_ASSIGN, $name, $value], $s); 1013 $foundSomething = true; 1014 } 1015 1016 if ($this->matchChar('{', false)) { 1017 ! $this->cssOnly || $this->assertPlainCssValid(false); 1018 1019 $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s); 1020 $propBlock->prefix = $name; 1021 $propBlock->hasValue = $foundSomething; 1022 1023 $foundSomething = true; 1024 } elseif ($foundSomething) { 1025 $foundSomething = $this->end(); 1026 } 1027 1028 if ($foundSomething) { 1029 return true; 1030 } 1031 } 1032 1033 $this->seek($s); 1034 1035 // closing a block 1036 if ($this->matchChar('}', false)) { 1037 $block = $this->popBlock(); 1038 1039 if (! isset($block->type) || $block->type !== Type::T_IF) { 1040 if ($this->env->parent) { 1041 $this->append(null); // collect comments before next statement if needed 1042 } 1043 } 1044 1045 if (isset($block->type) && $block->type === Type::T_INCLUDE) { 1046 $include = $block->child; 1047 unset($block->child); 1048 $include[3] = $block; 1049 $this->append($include, $s); 1050 } elseif (empty($block->dontAppend)) { 1051 $type = isset($block->type) ? $block->type : Type::T_BLOCK; 1052 $this->append([$type, $block], $s); 1053 } 1054 1055 // collect comments just after the block closing if needed 1056 if ($this->eatWhiteDefault) { 1057 $this->whitespace(); 1058 1059 if ($this->env->comments) { 1060 $this->append(null); 1061 } 1062 } 1063 1064 return true; 1065 } 1066 1067 // extra stuff 1068 if ( 1069 $this->matchChar(';') || 1070 $this->literal('<!--', 4) 1071 ) { 1072 return true; 1073 } 1074 1075 return false; 1076 } 1077 1078 /** 1079 * Push block onto parse tree 1080 * 1081 * @param array|null $selectors 1082 * @param integer $pos 1083 * 1084 * @return Block 1085 */ 1086 protected function pushBlock($selectors, $pos = 0) 1087 { 1088 list($line, $column) = $this->getSourcePosition($pos); 1089 1090 $b = new Block(); 1091 $b->sourceName = $this->sourceName; 1092 $b->sourceLine = $line; 1093 $b->sourceColumn = $column; 1094 $b->sourceIndex = $this->sourceIndex; 1095 $b->selectors = $selectors; 1096 $b->comments = []; 1097 $b->parent = $this->env; 1098 1099 if (! $this->env) { 1100 $b->children = []; 1101 } elseif (empty($this->env->children)) { 1102 $this->env->children = $this->env->comments; 1103 $b->children = []; 1104 $this->env->comments = []; 1105 } else { 1106 $b->children = $this->env->comments; 1107 $this->env->comments = []; 1108 } 1109 1110 $this->env = $b; 1111 1112 // collect comments at the beginning of a block if needed 1113 if ($this->eatWhiteDefault) { 1114 $this->whitespace(); 1115 1116 if ($this->env->comments) { 1117 $this->append(null); 1118 } 1119 } 1120 1121 return $b; 1122 } 1123 1124 /** 1125 * Push special (named) block onto parse tree 1126 * 1127 * @param string $type 1128 * @param integer $pos 1129 * 1130 * @return Block 1131 */ 1132 protected function pushSpecialBlock($type, $pos) 1133 { 1134 $block = $this->pushBlock(null, $pos); 1135 $block->type = $type; 1136 1137 return $block; 1138 } 1139 1140 /** 1141 * Pop scope and return last block 1142 * 1143 * @return Block 1144 * 1145 * @throws \Exception 1146 */ 1147 protected function popBlock() 1148 { 1149 1150 // collect comments ending just before of a block closing 1151 if ($this->env->comments) { 1152 $this->append(null); 1153 } 1154 1155 // pop the block 1156 $block = $this->env; 1157 1158 if (empty($block->parent)) { 1159 throw $this->parseError('unexpected }'); 1160 } 1161 1162 if ($block->type == Type::T_AT_ROOT) { 1163 // keeps the parent in case of self selector & 1164 $block->selfParent = $block->parent; 1165 } 1166 1167 $this->env = $block->parent; 1168 1169 unset($block->parent); 1170 1171 return $block; 1172 } 1173 1174 /** 1175 * Peek input stream 1176 * 1177 * @param string $regex 1178 * @param array $out 1179 * @param integer $from 1180 * 1181 * @return integer 1182 */ 1183 protected function peek($regex, &$out, $from = null) 1184 { 1185 if (! isset($from)) { 1186 $from = $this->count; 1187 } 1188 1189 $r = '/' . $regex . '/' . $this->patternModifiers; 1190 $result = preg_match($r, $this->buffer, $out, null, $from); 1191 1192 return $result; 1193 } 1194 1195 /** 1196 * Seek to position in input stream (or return current position in input stream) 1197 * 1198 * @param integer $where 1199 */ 1200 protected function seek($where) 1201 { 1202 $this->count = $where; 1203 } 1204 1205 /** 1206 * Assert a parsed part is plain CSS Valid 1207 * 1208 * @param array|false $parsed 1209 * @param int $startPos 1210 * @throws ParserException 1211 */ 1212 protected function assertPlainCssValid($parsed, $startPos = null) 1213 { 1214 $type = ''; 1215 if ($parsed) { 1216 $type = $parsed[0]; 1217 $parsed = $this->isPlainCssValidElement($parsed); 1218 } 1219 if (! $parsed) { 1220 if (! \is_null($startPos)) { 1221 $plain = rtrim(substr($this->buffer, $startPos, $this->count - $startPos)); 1222 $message = "Error : `{$plain}` isn't allowed in plain CSS"; 1223 } else { 1224 $message = 'Error: SCSS syntax not allowed in CSS file'; 1225 } 1226 if ($type) { 1227 $message .= " ($type)"; 1228 } 1229 throw $this->parseError($message); 1230 } 1231 1232 return $parsed; 1233 } 1234 1235 /** 1236 * Check a parsed element is plain CSS Valid 1237 * @param array $parsed 1238 * @return bool|array 1239 */ 1240 protected function isPlainCssValidElement($parsed, $allowExpression = false) 1241 { 1242 // keep string as is 1243 if (is_string($parsed)) { 1244 return $parsed; 1245 } 1246 1247 if ( 1248 \in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) && 1249 !\in_array($parsed[1], [ 1250 'alpha', 1251 'attr', 1252 'calc', 1253 'cubic-bezier', 1254 'env', 1255 'grayscale', 1256 'hsl', 1257 'hsla', 1258 'invert', 1259 'linear-gradient', 1260 'min', 1261 'max', 1262 'radial-gradient', 1263 'repeating-linear-gradient', 1264 'repeating-radial-gradient', 1265 'rgb', 1266 'rgba', 1267 'rotate', 1268 'saturate', 1269 'var', 1270 ]) && 1271 Compiler::isNativeFunction($parsed[1]) 1272 ) { 1273 return false; 1274 } 1275 1276 switch ($parsed[0]) { 1277 case Type::T_BLOCK: 1278 case Type::T_KEYWORD: 1279 case Type::T_NULL: 1280 case Type::T_NUMBER: 1281 case Type::T_MEDIA: 1282 return $parsed; 1283 1284 case Type::T_COMMENT: 1285 if (isset($parsed[2])) { 1286 return false; 1287 } 1288 return $parsed; 1289 1290 case Type::T_DIRECTIVE: 1291 if (\is_array($parsed[1])) { 1292 $parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]); 1293 if (! $parsed[1][1]) { 1294 return false; 1295 } 1296 } 1297 1298 return $parsed; 1299 1300 case Type::T_IMPORT: 1301 if ($parsed[1][0] === Type::T_LIST) { 1302 return false; 1303 } 1304 $parsed[1] = $this->isPlainCssValidElement($parsed[1]); 1305 if ($parsed[1] === false) { 1306 return false; 1307 } 1308 return $parsed; 1309 1310 case Type::T_STRING: 1311 foreach ($parsed[2] as $k => $substr) { 1312 if (\is_array($substr)) { 1313 $parsed[2][$k] = $this->isPlainCssValidElement($substr); 1314 if (! $parsed[2][$k]) { 1315 return false; 1316 } 1317 } 1318 } 1319 return $parsed; 1320 1321 case Type::T_LIST: 1322 if (!empty($parsed['enclosing'])) { 1323 return false; 1324 } 1325 foreach ($parsed[2] as $k => $listElement) { 1326 $parsed[2][$k] = $this->isPlainCssValidElement($listElement); 1327 if (! $parsed[2][$k]) { 1328 return false; 1329 } 1330 } 1331 return $parsed; 1332 1333 case Type::T_ASSIGN: 1334 foreach ([1, 2, 3] as $k) { 1335 if (! empty($parsed[$k])) { 1336 $parsed[$k] = $this->isPlainCssValidElement($parsed[$k]); 1337 if (! $parsed[$k]) { 1338 return false; 1339 } 1340 } 1341 } 1342 return $parsed; 1343 1344 case Type::T_EXPRESSION: 1345 list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed; 1346 if (! $allowExpression && ! \in_array($op, ['and', 'or', '/'])) { 1347 return false; 1348 } 1349 $lhs = $this->isPlainCssValidElement($lhs, true); 1350 if (! $lhs) { 1351 return false; 1352 } 1353 $rhs = $this->isPlainCssValidElement($rhs, true); 1354 if (! $rhs) { 1355 return false; 1356 } 1357 1358 return [ 1359 Type::T_STRING, 1360 '', [ 1361 $this->inParens ? '(' : '', 1362 $lhs, 1363 ($whiteBefore ? ' ' : '') . $op . ($whiteAfter ? ' ' : ''), 1364 $rhs, 1365 $this->inParens ? ')' : '' 1366 ] 1367 ]; 1368 1369 case Type::T_CUSTOM_PROPERTY: 1370 case Type::T_UNARY: 1371 $parsed[2] = $this->isPlainCssValidElement($parsed[2]); 1372 if (! $parsed[2]) { 1373 return false; 1374 } 1375 return $parsed; 1376 1377 case Type::T_FUNCTION: 1378 $argsList = $parsed[2]; 1379 foreach ($argsList[2] as $argElement) { 1380 if (! $this->isPlainCssValidElement($argElement)) { 1381 return false; 1382 } 1383 } 1384 return $parsed; 1385 1386 case Type::T_FUNCTION_CALL: 1387 $parsed[0] = Type::T_FUNCTION; 1388 $argsList = [Type::T_LIST, ',', []]; 1389 foreach ($parsed[2] as $arg) { 1390 if ($arg[0] || ! empty($arg[2])) { 1391 // no named arguments possible in a css function call 1392 // nor ... argument 1393 return false; 1394 } 1395 $arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc'); 1396 if (! $arg) { 1397 return false; 1398 } 1399 $argsList[2][] = $arg; 1400 } 1401 $parsed[2] = $argsList; 1402 return $parsed; 1403 } 1404 1405 return false; 1406 } 1407 1408 /** 1409 * Match string looking for either ending delim, escape, or string interpolation 1410 * 1411 * {@internal This is a workaround for preg_match's 250K string match limit. }} 1412 * 1413 * @param array $m Matches (passed by reference) 1414 * @param string $delim Delimiter 1415 * 1416 * @return boolean True if match; false otherwise 1417 */ 1418 protected function matchString(&$m, $delim) 1419 { 1420 $token = null; 1421 1422 $end = \strlen($this->buffer); 1423 1424 // look for either ending delim, escape, or string interpolation 1425 foreach (['#{', '\\', "\r", $delim] as $lookahead) { 1426 $pos = strpos($this->buffer, $lookahead, $this->count); 1427 1428 if ($pos !== false && $pos < $end) { 1429 $end = $pos; 1430 $token = $lookahead; 1431 } 1432 } 1433 1434 if (! isset($token)) { 1435 return false; 1436 } 1437 1438 $match = substr($this->buffer, $this->count, $end - $this->count); 1439 $m = [ 1440 $match . $token, 1441 $match, 1442 $token 1443 ]; 1444 $this->count = $end + \strlen($token); 1445 1446 return true; 1447 } 1448 1449 /** 1450 * Try to match something on head of buffer 1451 * 1452 * @param string $regex 1453 * @param array $out 1454 * @param boolean $eatWhitespace 1455 * 1456 * @return boolean 1457 */ 1458 protected function match($regex, &$out, $eatWhitespace = null) 1459 { 1460 $r = '/' . $regex . '/' . $this->patternModifiers; 1461 1462 if (! preg_match($r, $this->buffer, $out, null, $this->count)) { 1463 return false; 1464 } 1465 1466 $this->count += \strlen($out[0]); 1467 1468 if (! isset($eatWhitespace)) { 1469 $eatWhitespace = $this->eatWhiteDefault; 1470 } 1471 1472 if ($eatWhitespace) { 1473 $this->whitespace(); 1474 } 1475 1476 return true; 1477 } 1478 1479 /** 1480 * Match a single string 1481 * 1482 * @param string $char 1483 * @param boolean $eatWhitespace 1484 * 1485 * @return boolean 1486 */ 1487 protected function matchChar($char, $eatWhitespace = null) 1488 { 1489 if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) { 1490 return false; 1491 } 1492 1493 $this->count++; 1494 1495 if (! isset($eatWhitespace)) { 1496 $eatWhitespace = $this->eatWhiteDefault; 1497 } 1498 1499 if ($eatWhitespace) { 1500 $this->whitespace(); 1501 } 1502 1503 return true; 1504 } 1505 1506 /** 1507 * Match literal string 1508 * 1509 * @param string $what 1510 * @param integer $len 1511 * @param boolean $eatWhitespace 1512 * 1513 * @return boolean 1514 */ 1515 protected function literal($what, $len, $eatWhitespace = null) 1516 { 1517 if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) { 1518 return false; 1519 } 1520 1521 $this->count += $len; 1522 1523 if (! isset($eatWhitespace)) { 1524 $eatWhitespace = $this->eatWhiteDefault; 1525 } 1526 1527 if ($eatWhitespace) { 1528 $this->whitespace(); 1529 } 1530 1531 return true; 1532 } 1533 1534 /** 1535 * Match some whitespace 1536 * 1537 * @return boolean 1538 */ 1539 protected function whitespace() 1540 { 1541 $gotWhite = false; 1542 1543 while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) { 1544 if (isset($m[1]) && empty($this->commentsSeen[$this->count])) { 1545 // comment that are kept in the output CSS 1546 $comment = []; 1547 $startCommentCount = $this->count; 1548 $endCommentCount = $this->count + \strlen($m[1]); 1549 1550 // find interpolations in comment 1551 $p = strpos($this->buffer, '#{', $this->count); 1552 1553 while ($p !== false && $p < $endCommentCount) { 1554 $c = substr($this->buffer, $this->count, $p - $this->count); 1555 $comment[] = $c; 1556 $this->count = $p; 1557 $out = null; 1558 1559 if ($this->interpolation($out)) { 1560 // keep right spaces in the following string part 1561 if ($out[3]) { 1562 while ($this->buffer[$this->count - 1] !== '}') { 1563 $this->count--; 1564 } 1565 1566 $out[3] = ''; 1567 } 1568 1569 $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out]; 1570 } else { 1571 $comment[] = substr($this->buffer, $this->count, 2); 1572 1573 $this->count += 2; 1574 } 1575 1576 $p = strpos($this->buffer, '#{', $this->count); 1577 } 1578 1579 // remaining part 1580 $c = substr($this->buffer, $this->count, $endCommentCount - $this->count); 1581 1582 if (! $comment) { 1583 // single part static comment 1584 $this->appendComment([Type::T_COMMENT, $c]); 1585 } else { 1586 $comment[] = $c; 1587 $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount); 1588 $this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]); 1589 } 1590 1591 $this->commentsSeen[$startCommentCount] = true; 1592 $this->count = $endCommentCount; 1593 } else { 1594 // comment that are ignored and not kept in the output css 1595 $this->count += \strlen($m[0]); 1596 // silent comments are not allowed in plain CSS files 1597 ! $this->cssOnly 1598 || ! \strlen(trim($m[0])) 1599 || $this->assertPlainCssValid(false, $this->count - \strlen($m[0])); 1600 } 1601 1602 $gotWhite = true; 1603 } 1604 1605 return $gotWhite; 1606 } 1607 1608 /** 1609 * Append comment to current block 1610 * 1611 * @param array $comment 1612 */ 1613 protected function appendComment($comment) 1614 { 1615 if (! $this->discardComments) { 1616 $this->env->comments[] = $comment; 1617 } 1618 } 1619 1620 /** 1621 * Append statement to current block 1622 * 1623 * @param array|null $statement 1624 * @param integer $pos 1625 */ 1626 protected function append($statement, $pos = null) 1627 { 1628 if (! \is_null($statement)) { 1629 ! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos)); 1630 1631 if (! \is_null($pos)) { 1632 list($line, $column) = $this->getSourcePosition($pos); 1633 1634 $statement[static::SOURCE_LINE] = $line; 1635 $statement[static::SOURCE_COLUMN] = $column; 1636 $statement[static::SOURCE_INDEX] = $this->sourceIndex; 1637 } 1638 1639 $this->env->children[] = $statement; 1640 } 1641 1642 $comments = $this->env->comments; 1643 1644 if ($comments) { 1645 $this->env->children = array_merge($this->env->children, $comments); 1646 $this->env->comments = []; 1647 } 1648 } 1649 1650 /** 1651 * Returns last child was appended 1652 * 1653 * @return array|null 1654 */ 1655 protected function last() 1656 { 1657 $i = \count($this->env->children) - 1; 1658 1659 if (isset($this->env->children[$i])) { 1660 return $this->env->children[$i]; 1661 } 1662 } 1663 1664 /** 1665 * Parse media query list 1666 * 1667 * @param array $out 1668 * 1669 * @return boolean 1670 */ 1671 protected function mediaQueryList(&$out) 1672 { 1673 return $this->genericList($out, 'mediaQuery', ',', false); 1674 } 1675 1676 /** 1677 * Parse media query 1678 * 1679 * @param array $out 1680 * 1681 * @return boolean 1682 */ 1683 protected function mediaQuery(&$out) 1684 { 1685 $expressions = null; 1686 $parts = []; 1687 1688 if ( 1689 ($this->literal('only', 4) && ($only = true) || 1690 $this->literal('not', 3) && ($not = true) || true) && 1691 $this->mixedKeyword($mediaType) 1692 ) { 1693 $prop = [Type::T_MEDIA_TYPE]; 1694 1695 if (isset($only)) { 1696 $prop[] = [Type::T_KEYWORD, 'only']; 1697 } 1698 1699 if (isset($not)) { 1700 $prop[] = [Type::T_KEYWORD, 'not']; 1701 } 1702 1703 $media = [Type::T_LIST, '', []]; 1704 1705 foreach ((array) $mediaType as $type) { 1706 if (\is_array($type)) { 1707 $media[2][] = $type; 1708 } else { 1709 $media[2][] = [Type::T_KEYWORD, $type]; 1710 } 1711 } 1712 1713 $prop[] = $media; 1714 $parts[] = $prop; 1715 } 1716 1717 if (empty($parts) || $this->literal('and', 3)) { 1718 $this->genericList($expressions, 'mediaExpression', 'and', false); 1719 1720 if (\is_array($expressions)) { 1721 $parts = array_merge($parts, $expressions[2]); 1722 } 1723 } 1724 1725 $out = $parts; 1726 1727 return true; 1728 } 1729 1730 /** 1731 * Parse supports query 1732 * 1733 * @param array $out 1734 * 1735 * @return boolean 1736 */ 1737 protected function supportsQuery(&$out) 1738 { 1739 $expressions = null; 1740 $parts = []; 1741 1742 $s = $this->count; 1743 1744 $not = false; 1745 1746 if ( 1747 ($this->literal('not', 3) && ($not = true) || true) && 1748 $this->matchChar('(') && 1749 ($this->expression($property)) && 1750 $this->literal(': ', 2) && 1751 $this->valueList($value) && 1752 $this->matchChar(')') 1753 ) { 1754 $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]]; 1755 $support[2][] = $property; 1756 $support[2][] = [Type::T_KEYWORD, ': ']; 1757 $support[2][] = $value; 1758 $support[2][] = [Type::T_KEYWORD, ')']; 1759 1760 $parts[] = $support; 1761 $s = $this->count; 1762 } else { 1763 $this->seek($s); 1764 } 1765 1766 if ( 1767 $this->matchChar('(') && 1768 $this->supportsQuery($subQuery) && 1769 $this->matchChar(')') 1770 ) { 1771 $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]]; 1772 $s = $this->count; 1773 } else { 1774 $this->seek($s); 1775 } 1776 1777 if ( 1778 $this->literal('not', 3) && 1779 $this->supportsQuery($subQuery) 1780 ) { 1781 $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]]; 1782 $s = $this->count; 1783 } else { 1784 $this->seek($s); 1785 } 1786 1787 if ( 1788 $this->literal('selector(', 9) && 1789 $this->selector($selector) && 1790 $this->matchChar(')') 1791 ) { 1792 $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]]; 1793 1794 $selectorList = [Type::T_LIST, '', []]; 1795 1796 foreach ($selector as $sc) { 1797 $compound = [Type::T_STRING, '', []]; 1798 1799 foreach ($sc as $scp) { 1800 if (\is_array($scp)) { 1801 $compound[2][] = $scp; 1802 } else { 1803 $compound[2][] = [Type::T_KEYWORD, $scp]; 1804 } 1805 } 1806 1807 $selectorList[2][] = $compound; 1808 } 1809 1810 $support[2][] = $selectorList; 1811 $support[2][] = [Type::T_KEYWORD, ')']; 1812 $parts[] = $support; 1813 $s = $this->count; 1814 } else { 1815 $this->seek($s); 1816 } 1817 1818 if ($this->variable($var) or $this->interpolation($var)) { 1819 $parts[] = $var; 1820 $s = $this->count; 1821 } else { 1822 $this->seek($s); 1823 } 1824 1825 if ( 1826 $this->literal('and', 3) && 1827 $this->genericList($expressions, 'supportsQuery', ' and', false) 1828 ) { 1829 array_unshift($expressions[2], [Type::T_STRING, '', $parts]); 1830 1831 $parts = [$expressions]; 1832 $s = $this->count; 1833 } else { 1834 $this->seek($s); 1835 } 1836 1837 if ( 1838 $this->literal('or', 2) && 1839 $this->genericList($expressions, 'supportsQuery', ' or', false) 1840 ) { 1841 array_unshift($expressions[2], [Type::T_STRING, '', $parts]); 1842 1843 $parts = [$expressions]; 1844 $s = $this->count; 1845 } else { 1846 $this->seek($s); 1847 } 1848 1849 if (\count($parts)) { 1850 if ($this->eatWhiteDefault) { 1851 $this->whitespace(); 1852 } 1853 1854 $out = [Type::T_STRING, '', $parts]; 1855 1856 return true; 1857 } 1858 1859 return false; 1860 } 1861 1862 1863 /** 1864 * Parse media expression 1865 * 1866 * @param array $out 1867 * 1868 * @return boolean 1869 */ 1870 protected function mediaExpression(&$out) 1871 { 1872 $s = $this->count; 1873 $value = null; 1874 1875 if ( 1876 $this->matchChar('(') && 1877 $this->expression($feature) && 1878 ($this->matchChar(':') && 1879 $this->expression($value) || true) && 1880 $this->matchChar(')') 1881 ) { 1882 $out = [Type::T_MEDIA_EXPRESSION, $feature]; 1883 1884 if ($value) { 1885 $out[] = $value; 1886 } 1887 1888 return true; 1889 } 1890 1891 $this->seek($s); 1892 1893 return false; 1894 } 1895 1896 /** 1897 * Parse argument values 1898 * 1899 * @param array $out 1900 * 1901 * @return boolean 1902 */ 1903 protected function argValues(&$out) 1904 { 1905 $discardComments = $this->discardComments; 1906 $this->discardComments = true; 1907 1908 if ($this->genericList($list, 'argValue', ',', false)) { 1909 $out = $list[2]; 1910 1911 $this->discardComments = $discardComments; 1912 1913 return true; 1914 } 1915 1916 $this->discardComments = $discardComments; 1917 1918 return false; 1919 } 1920 1921 /** 1922 * Parse argument value 1923 * 1924 * @param array $out 1925 * 1926 * @return boolean 1927 */ 1928 protected function argValue(&$out) 1929 { 1930 $s = $this->count; 1931 1932 $keyword = null; 1933 1934 if (! $this->variable($keyword) || ! $this->matchChar(':')) { 1935 $this->seek($s); 1936 1937 $keyword = null; 1938 } 1939 1940 if ($this->genericList($value, 'expression', '', true)) { 1941 $out = [$keyword, $value, false]; 1942 $s = $this->count; 1943 1944 if ($this->literal('...', 3)) { 1945 $out[2] = true; 1946 } else { 1947 $this->seek($s); 1948 } 1949 1950 return true; 1951 } 1952 1953 return false; 1954 } 1955 1956 /** 1957 * Check if a generic directive is known to be able to allow almost any syntax or not 1958 * @param mixed $directiveName 1959 * @return bool 1960 */ 1961 protected function isKnownGenericDirective($directiveName) 1962 { 1963 if (\is_array($directiveName) && \is_string(reset($directiveName))) { 1964 $directiveName = reset($directiveName); 1965 } 1966 if (! \is_string($directiveName)) { 1967 return false; 1968 } 1969 if ( 1970 \in_array($directiveName, [ 1971 'at-root', 1972 'media', 1973 'mixin', 1974 'include', 1975 'scssphp-import-once', 1976 'import', 1977 'extend', 1978 'function', 1979 'break', 1980 'continue', 1981 'return', 1982 'each', 1983 'while', 1984 'for', 1985 'if', 1986 'debug', 1987 'warn', 1988 'error', 1989 'content', 1990 'else', 1991 'charset', 1992 'supports', 1993 // Todo 1994 'use', 1995 'forward', 1996 ]) 1997 ) { 1998 return true; 1999 } 2000 return false; 2001 } 2002 2003 /** 2004 * Parse directive value list that considers $vars as keyword 2005 * 2006 * @param array $out 2007 * @param boolean|string $endChar 2008 * 2009 * @return boolean 2010 */ 2011 protected function directiveValue(&$out, $endChar = false) 2012 { 2013 $s = $this->count; 2014 2015 if ($this->variable($out)) { 2016 if ($endChar && $this->matchChar($endChar, false)) { 2017 return true; 2018 } 2019 2020 if (! $endChar && $this->end()) { 2021 return true; 2022 } 2023 } 2024 2025 $this->seek($s); 2026 2027 if (\is_string($endChar) && $this->openString($endChar ? $endChar : ';', $out, null, null, true, ";}{")) { 2028 if ($endChar && $this->matchChar($endChar, false)) { 2029 return true; 2030 } 2031 $ss = $this->count; 2032 if (!$endChar && $this->end()) { 2033 $this->seek($ss); 2034 return true; 2035 } 2036 } 2037 2038 $this->seek($s); 2039 2040 $allowVars = $this->allowVars; 2041 $this->allowVars = false; 2042 2043 $res = $this->genericList($out, 'spaceList', ','); 2044 $this->allowVars = $allowVars; 2045 2046 if ($res) { 2047 if ($endChar && $this->matchChar($endChar, false)) { 2048 return true; 2049 } 2050 2051 if (! $endChar && $this->end()) { 2052 return true; 2053 } 2054 } 2055 2056 $this->seek($s); 2057 2058 if ($endChar && $this->matchChar($endChar, false)) { 2059 return true; 2060 } 2061 2062 return false; 2063 } 2064 2065 /** 2066 * Parse comma separated value list 2067 * 2068 * @param array $out 2069 * 2070 * @return boolean 2071 */ 2072 protected function valueList(&$out) 2073 { 2074 $discardComments = $this->discardComments; 2075 $this->discardComments = true; 2076 $res = $this->genericList($out, 'spaceList', ','); 2077 $this->discardComments = $discardComments; 2078 2079 return $res; 2080 } 2081 2082 /** 2083 * Parse a function call, where externals () are part of the call 2084 * and not of the value list 2085 * 2086 * @param $out 2087 * @param bool $mandatoryEnclos 2088 * @param null|string $charAfter 2089 * @param null|bool $eatWhiteSp 2090 * @return bool 2091 */ 2092 protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null) 2093 { 2094 $s = $this->count; 2095 2096 if ( 2097 $this->matchChar('(') && 2098 $this->valueList($out) && 2099 $this->matchChar(')') && 2100 ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end()) 2101 ) { 2102 return true; 2103 } 2104 2105 if (! $mandatoryEnclos) { 2106 $this->seek($s); 2107 2108 if ( 2109 $this->valueList($out) && 2110 ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end()) 2111 ) { 2112 return true; 2113 } 2114 } 2115 2116 $this->seek($s); 2117 2118 return false; 2119 } 2120 2121 /** 2122 * Parse space separated value list 2123 * 2124 * @param array $out 2125 * 2126 * @return boolean 2127 */ 2128 protected function spaceList(&$out) 2129 { 2130 return $this->genericList($out, 'expression'); 2131 } 2132 2133 /** 2134 * Parse generic list 2135 * 2136 * @param array $out 2137 * @param string $parseItem The name of the method used to parse items 2138 * @param string $delim 2139 * @param boolean $flatten 2140 * 2141 * @return boolean 2142 */ 2143 protected function genericList(&$out, $parseItem, $delim = '', $flatten = true) 2144 { 2145 $s = $this->count; 2146 $items = []; 2147 $value = null; 2148 2149 while ($this->$parseItem($value)) { 2150 $trailing_delim = false; 2151 $items[] = $value; 2152 2153 if ($delim) { 2154 if (! $this->literal($delim, \strlen($delim))) { 2155 break; 2156 } 2157 2158 $trailing_delim = true; 2159 } else { 2160 // if no delim watch that a keyword didn't eat the single/double quote 2161 // from the following starting string 2162 if ($value[0] === Type::T_KEYWORD) { 2163 $word = $value[1]; 2164 2165 $last_char = substr($word, -1); 2166 2167 if ( 2168 strlen($word) > 1 && 2169 in_array($last_char, [ "'", '"']) && 2170 substr($word, -2, 1) !== '\\' 2171 ) { 2172 // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake 2173 $word = str_replace('\\' . $last_char, '\\\\', $word); 2174 if (strpos($word, $last_char) < strlen($word) - 1) { 2175 continue; 2176 } 2177 2178 $currentCount = $this->count; 2179 2180 // let's try to rewind to previous char and try a parse 2181 $this->count--; 2182 // in case the keyword also eat spaces 2183 while (substr($this->buffer, $this->count, 1) !== $last_char) { 2184 $this->count--; 2185 } 2186 2187 $nextValue = null; 2188 if ($this->$parseItem($nextValue)) { 2189 if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) { 2190 // bad try, forget it 2191 $this->seek($currentCount); 2192 continue; 2193 } 2194 if ($nextValue[0] !== Type::T_STRING) { 2195 // bad try, forget it 2196 $this->seek($currentCount); 2197 continue; 2198 } 2199 2200 // OK it was a good idea 2201 $value[1] = substr($value[1], 0, -1); 2202 array_pop($items); 2203 $items[] = $value; 2204 $items[] = $nextValue; 2205 } else { 2206 // bad try, forget it 2207 $this->seek($currentCount); 2208 continue; 2209 } 2210 } 2211 } 2212 } 2213 } 2214 2215 if (! $items) { 2216 $this->seek($s); 2217 2218 return false; 2219 } 2220 2221 if ($trailing_delim) { 2222 $items[] = [Type::T_NULL]; 2223 } 2224 2225 if ($flatten && \count($items) === 1) { 2226 $out = $items[0]; 2227 } else { 2228 $out = [Type::T_LIST, $delim, $items]; 2229 } 2230 2231 return true; 2232 } 2233 2234 /** 2235 * Parse expression 2236 * 2237 * @param array $out 2238 * @param boolean $listOnly 2239 * @param boolean $lookForExp 2240 * 2241 * @return boolean 2242 */ 2243 protected function expression(&$out, $listOnly = false, $lookForExp = true) 2244 { 2245 $s = $this->count; 2246 $discard = $this->discardComments; 2247 $this->discardComments = true; 2248 $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]); 2249 2250 if ($this->matchChar('(')) { 2251 if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) { 2252 if ($lookForExp) { 2253 $out = $this->expHelper($lhs, 0); 2254 } else { 2255 $out = $lhs; 2256 } 2257 2258 $this->discardComments = $discard; 2259 2260 return true; 2261 } 2262 2263 $this->seek($s); 2264 } 2265 2266 if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) { 2267 if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) { 2268 if ($lookForExp) { 2269 $out = $this->expHelper($lhs, 0); 2270 } else { 2271 $out = $lhs; 2272 } 2273 2274 $this->discardComments = $discard; 2275 2276 return true; 2277 } 2278 2279 $this->seek($s); 2280 } 2281 2282 if (! $listOnly && $this->value($lhs)) { 2283 if ($lookForExp) { 2284 $out = $this->expHelper($lhs, 0); 2285 } else { 2286 $out = $lhs; 2287 } 2288 2289 $this->discardComments = $discard; 2290 2291 return true; 2292 } 2293 2294 $this->discardComments = $discard; 2295 2296 return false; 2297 } 2298 2299 /** 2300 * Parse expression specifically checking for lists in parenthesis or brackets 2301 * 2302 * @param array $out 2303 * @param integer $s 2304 * @param string $closingParen 2305 * @param array $allowedTypes 2306 * 2307 * @return boolean 2308 */ 2309 protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP]) 2310 { 2311 if ($this->matchChar($closingParen) && \in_array(Type::T_LIST, $allowedTypes)) { 2312 $out = [Type::T_LIST, '', []]; 2313 2314 switch ($closingParen) { 2315 case ')': 2316 $out['enclosing'] = 'parent'; // parenthesis list 2317 break; 2318 2319 case ']': 2320 $out['enclosing'] = 'bracket'; // bracketed list 2321 break; 2322 } 2323 2324 return true; 2325 } 2326 2327 if ( 2328 $this->valueList($out) && 2329 $this->matchChar($closingParen) && ! ($closingParen === ')' && 2330 \in_array($out[0], [Type::T_EXPRESSION, Type::T_UNARY])) && 2331 \in_array(Type::T_LIST, $allowedTypes) 2332 ) { 2333 if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) { 2334 $out = [Type::T_LIST, '', [$out]]; 2335 } 2336 2337 switch ($closingParen) { 2338 case ')': 2339 $out['enclosing'] = 'parent'; // parenthesis list 2340 break; 2341 2342 case ']': 2343 $out['enclosing'] = 'bracket'; // bracketed list 2344 break; 2345 } 2346 2347 return true; 2348 } 2349 2350 $this->seek($s); 2351 2352 if (\in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) { 2353 return true; 2354 } 2355 2356 return false; 2357 } 2358 2359 /** 2360 * Parse left-hand side of subexpression 2361 * 2362 * @param array $lhs 2363 * @param integer $minP 2364 * 2365 * @return array 2366 */ 2367 protected function expHelper($lhs, $minP) 2368 { 2369 $operators = static::$operatorPattern; 2370 2371 $ss = $this->count; 2372 $whiteBefore = isset($this->buffer[$this->count - 1]) && 2373 ctype_space($this->buffer[$this->count - 1]); 2374 2375 while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) { 2376 $whiteAfter = isset($this->buffer[$this->count]) && 2377 ctype_space($this->buffer[$this->count]); 2378 $varAfter = isset($this->buffer[$this->count]) && 2379 $this->buffer[$this->count] === '$'; 2380 2381 $this->whitespace(); 2382 2383 $op = $m[1]; 2384 2385 // don't turn negative numbers into expressions 2386 if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) { 2387 break; 2388 } 2389 2390 if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) { 2391 break; 2392 } 2393 2394 if ($op === '-' && ! $whiteAfter && $rhs[0] === Type::T_KEYWORD) { 2395 break; 2396 } 2397 2398 // peek and see if rhs belongs to next operator 2399 if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) { 2400 $rhs = $this->expHelper($rhs, static::$precedence[$next[1]]); 2401 } 2402 2403 $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter]; 2404 2405 $ss = $this->count; 2406 $whiteBefore = isset($this->buffer[$this->count - 1]) && 2407 ctype_space($this->buffer[$this->count - 1]); 2408 } 2409 2410 $this->seek($ss); 2411 2412 return $lhs; 2413 } 2414 2415 /** 2416 * Parse value 2417 * 2418 * @param array $out 2419 * 2420 * @return boolean 2421 */ 2422 protected function value(&$out) 2423 { 2424 if (! isset($this->buffer[$this->count])) { 2425 return false; 2426 } 2427 2428 $s = $this->count; 2429 $char = $this->buffer[$this->count]; 2430 2431 if ( 2432 $this->literal('url(', 4) && 2433 $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false) 2434 ) { 2435 $len = strspn( 2436 $this->buffer, 2437 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=', 2438 $this->count 2439 ); 2440 2441 $this->count += $len; 2442 2443 if ($this->matchChar(')')) { 2444 $content = substr($this->buffer, $s, $this->count - $s); 2445 $out = [Type::T_KEYWORD, $content]; 2446 2447 return true; 2448 } 2449 } 2450 2451 $this->seek($s); 2452 2453 if ( 2454 $this->literal('url(', 4, false) && 2455 $this->match('\s*(\/\/[^\s\)]+)\s*', $m) 2456 ) { 2457 $content = 'url(' . $m[1]; 2458 2459 if ($this->matchChar(')')) { 2460 $content .= ')'; 2461 $out = [Type::T_KEYWORD, $content]; 2462 2463 return true; 2464 } 2465 } 2466 2467 $this->seek($s); 2468 2469 // not 2470 if ($char === 'n' && $this->literal('not', 3, false)) { 2471 if ( 2472 $this->whitespace() && 2473 $this->value($inner) 2474 ) { 2475 $out = [Type::T_UNARY, 'not', $inner, $this->inParens]; 2476 2477 return true; 2478 } 2479 2480 $this->seek($s); 2481 2482 if ($this->parenValue($inner)) { 2483 $out = [Type::T_UNARY, 'not', $inner, $this->inParens]; 2484 2485 return true; 2486 } 2487 2488 $this->seek($s); 2489 } 2490 2491 // addition 2492 if ($char === '+') { 2493 $this->count++; 2494 2495 $follow_white = $this->whitespace(); 2496 2497 if ($this->value($inner)) { 2498 $out = [Type::T_UNARY, '+', $inner, $this->inParens]; 2499 2500 return true; 2501 } 2502 2503 if ($follow_white) { 2504 $out = [Type::T_KEYWORD, $char]; 2505 return true; 2506 } 2507 2508 $this->seek($s); 2509 2510 return false; 2511 } 2512 2513 // negation 2514 if ($char === '-') { 2515 if ($this->customProperty($out)) { 2516 return true; 2517 } 2518 2519 $this->count++; 2520 2521 $follow_white = $this->whitespace(); 2522 2523 if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) { 2524 $out = [Type::T_UNARY, '-', $inner, $this->inParens]; 2525 2526 return true; 2527 } 2528 2529 if ( 2530 $this->keyword($inner) && 2531 ! $this->func($inner, $out) 2532 ) { 2533 $out = [Type::T_UNARY, '-', $inner, $this->inParens]; 2534 2535 return true; 2536 } 2537 2538 if ($follow_white) { 2539 $out = [Type::T_KEYWORD, $char]; 2540 2541 return true; 2542 } 2543 2544 $this->seek($s); 2545 } 2546 2547 // paren 2548 if ($char === '(' && $this->parenValue($out)) { 2549 return true; 2550 } 2551 2552 if ($char === '#') { 2553 if ($this->interpolation($out) || $this->color($out)) { 2554 return true; 2555 } 2556 2557 $this->count++; 2558 2559 if ($this->keyword($keyword)) { 2560 $out = [Type::T_KEYWORD, '#' . $keyword]; 2561 2562 return true; 2563 } 2564 2565 $this->count--; 2566 } 2567 2568 if ($this->matchChar('&', true)) { 2569 $out = [Type::T_SELF]; 2570 2571 return true; 2572 } 2573 2574 if ($char === '$' && $this->variable($out)) { 2575 return true; 2576 } 2577 2578 if ($char === 'p' && $this->progid($out)) { 2579 return true; 2580 } 2581 2582 if (($char === '"' || $char === "'") && $this->string($out)) { 2583 return true; 2584 } 2585 2586 if ($this->unit($out)) { 2587 return true; 2588 } 2589 2590 // unicode range with wildcards 2591 if ( 2592 $this->literal('U+', 2) && 2593 $this->match('\?+|([0-9A-F]+(\?+|(-[0-9A-F]+))?)', $m, false) 2594 ) { 2595 $unicode = explode('-', $m[0]); 2596 if (strlen(reset($unicode)) <= 6 && strlen(end($unicode)) <= 6) { 2597 $out = [Type::T_KEYWORD, 'U+' . $m[0]]; 2598 2599 return true; 2600 } 2601 $this->count -= strlen($m[0]) + 2; 2602 } 2603 2604 if ($this->keyword($keyword, false)) { 2605 if ($this->func($keyword, $out)) { 2606 return true; 2607 } 2608 2609 $this->whitespace(); 2610 2611 if ($keyword === 'null') { 2612 $out = [Type::T_NULL]; 2613 } else { 2614 $out = [Type::T_KEYWORD, $keyword]; 2615 } 2616 2617 return true; 2618 } 2619 2620 return false; 2621 } 2622 2623 /** 2624 * Parse parenthesized value 2625 * 2626 * @param array $out 2627 * 2628 * @return boolean 2629 */ 2630 protected function parenValue(&$out) 2631 { 2632 $s = $this->count; 2633 2634 $inParens = $this->inParens; 2635 2636 if ($this->matchChar('(')) { 2637 if ($this->matchChar(')')) { 2638 $out = [Type::T_LIST, '', []]; 2639 2640 return true; 2641 } 2642 2643 $this->inParens = true; 2644 2645 if ( 2646 $this->expression($exp) && 2647 $this->matchChar(')') 2648 ) { 2649 $out = $exp; 2650 $this->inParens = $inParens; 2651 2652 return true; 2653 } 2654 } 2655 2656 $this->inParens = $inParens; 2657 $this->seek($s); 2658 2659 return false; 2660 } 2661 2662 /** 2663 * Parse "progid:" 2664 * 2665 * @param array $out 2666 * 2667 * @return boolean 2668 */ 2669 protected function progid(&$out) 2670 { 2671 $s = $this->count; 2672 2673 if ( 2674 $this->literal('progid:', 7, false) && 2675 $this->openString('(', $fn) && 2676 $this->matchChar('(') 2677 ) { 2678 $this->openString(')', $args, '('); 2679 2680 if ($this->matchChar(')')) { 2681 $out = [Type::T_STRING, '', [ 2682 'progid:', $fn, '(', $args, ')' 2683 ]]; 2684 2685 return true; 2686 } 2687 } 2688 2689 $this->seek($s); 2690 2691 return false; 2692 } 2693 2694 /** 2695 * Parse function call 2696 * 2697 * @param string $name 2698 * @param array $func 2699 * 2700 * @return boolean 2701 */ 2702 protected function func($name, &$func) 2703 { 2704 $s = $this->count; 2705 2706 if ($this->matchChar('(')) { 2707 if ($name === 'alpha' && $this->argumentList($args)) { 2708 $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]]; 2709 2710 return true; 2711 } 2712 2713 if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) { 2714 $ss = $this->count; 2715 2716 if ( 2717 $this->argValues($args) && 2718 $this->matchChar(')') 2719 ) { 2720 $func = [Type::T_FUNCTION_CALL, $name, $args]; 2721 2722 return true; 2723 } 2724 2725 $this->seek($ss); 2726 } 2727 2728 if ( 2729 ($this->openString(')', $str, '(') || true) && 2730 $this->matchChar(')') 2731 ) { 2732 $args = []; 2733 2734 if (! empty($str)) { 2735 $args[] = [null, [Type::T_STRING, '', [$str]]]; 2736 } 2737 2738 $func = [Type::T_FUNCTION_CALL, $name, $args]; 2739 2740 return true; 2741 } 2742 } 2743 2744 $this->seek($s); 2745 2746 return false; 2747 } 2748 2749 /** 2750 * Parse function call argument list 2751 * 2752 * @param array $out 2753 * 2754 * @return boolean 2755 */ 2756 protected function argumentList(&$out) 2757 { 2758 $s = $this->count; 2759 $this->matchChar('('); 2760 2761 $args = []; 2762 2763 while ($this->keyword($var)) { 2764 if ( 2765 $this->matchChar('=') && 2766 $this->expression($exp) 2767 ) { 2768 $args[] = [Type::T_STRING, '', [$var . '=']]; 2769 $arg = $exp; 2770 } else { 2771 break; 2772 } 2773 2774 $args[] = $arg; 2775 2776 if (! $this->matchChar(',')) { 2777 break; 2778 } 2779 2780 $args[] = [Type::T_STRING, '', [', ']]; 2781 } 2782 2783 if (! $this->matchChar(')') || ! $args) { 2784 $this->seek($s); 2785 2786 return false; 2787 } 2788 2789 $out = $args; 2790 2791 return true; 2792 } 2793 2794 /** 2795 * Parse mixin/function definition argument list 2796 * 2797 * @param array $out 2798 * 2799 * @return boolean 2800 */ 2801 protected function argumentDef(&$out) 2802 { 2803 $s = $this->count; 2804 $this->matchChar('('); 2805 2806 $args = []; 2807 2808 while ($this->variable($var)) { 2809 $arg = [$var[1], null, false]; 2810 2811 $ss = $this->count; 2812 2813 if ( 2814 $this->matchChar(':') && 2815 $this->genericList($defaultVal, 'expression', '', true) 2816 ) { 2817 $arg[1] = $defaultVal; 2818 } else { 2819 $this->seek($ss); 2820 } 2821 2822 $ss = $this->count; 2823 2824 if ($this->literal('...', 3)) { 2825 $sss = $this->count; 2826 2827 if (! $this->matchChar(')')) { 2828 throw $this->parseError('... has to be after the final argument'); 2829 } 2830 2831 $arg[2] = true; 2832 2833 $this->seek($sss); 2834 } else { 2835 $this->seek($ss); 2836 } 2837 2838 $args[] = $arg; 2839 2840 if (! $this->matchChar(',')) { 2841 break; 2842 } 2843 } 2844 2845 if (! $this->matchChar(')')) { 2846 $this->seek($s); 2847 2848 return false; 2849 } 2850 2851 $out = $args; 2852 2853 return true; 2854 } 2855 2856 /** 2857 * Parse map 2858 * 2859 * @param array $out 2860 * 2861 * @return boolean 2862 */ 2863 protected function map(&$out) 2864 { 2865 $s = $this->count; 2866 2867 if (! $this->matchChar('(')) { 2868 return false; 2869 } 2870 2871 $keys = []; 2872 $values = []; 2873 2874 while ( 2875 $this->genericList($key, 'expression', '', true) && 2876 $this->matchChar(':') && 2877 $this->genericList($value, 'expression', '', true) 2878 ) { 2879 $keys[] = $key; 2880 $values[] = $value; 2881 2882 if (! $this->matchChar(',')) { 2883 break; 2884 } 2885 } 2886 2887 if (! $keys || ! $this->matchChar(')')) { 2888 $this->seek($s); 2889 2890 return false; 2891 } 2892 2893 $out = [Type::T_MAP, $keys, $values]; 2894 2895 return true; 2896 } 2897 2898 /** 2899 * Parse color 2900 * 2901 * @param array $out 2902 * 2903 * @return boolean 2904 */ 2905 protected function color(&$out) 2906 { 2907 $s = $this->count; 2908 2909 if ($this->match('(#([0-9a-f]+)\b)', $m)) { 2910 if (\in_array(\strlen($m[2]), [3,4,6,8])) { 2911 $out = [Type::T_KEYWORD, $m[0]]; 2912 2913 return true; 2914 } 2915 2916 $this->seek($s); 2917 2918 return false; 2919 } 2920 2921 return false; 2922 } 2923 2924 /** 2925 * Parse number with unit 2926 * 2927 * @param array $unit 2928 * 2929 * @return boolean 2930 */ 2931 protected function unit(&$unit) 2932 { 2933 $s = $this->count; 2934 2935 if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) { 2936 if (\strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) { 2937 $this->whitespace(); 2938 2939 $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]); 2940 2941 return true; 2942 } 2943 2944 $this->seek($s); 2945 } 2946 2947 return false; 2948 } 2949 2950 /** 2951 * Parse string 2952 * 2953 * @param array $out 2954 * 2955 * @return boolean 2956 */ 2957 protected function string(&$out, $keepDelimWithInterpolation = false) 2958 { 2959 $s = $this->count; 2960 2961 if ($this->matchChar('"', false)) { 2962 $delim = '"'; 2963 } elseif ($this->matchChar("'", false)) { 2964 $delim = "'"; 2965 } else { 2966 return false; 2967 } 2968 2969 $content = []; 2970 $oldWhite = $this->eatWhiteDefault; 2971 $this->eatWhiteDefault = false; 2972 $hasInterpolation = false; 2973 2974 while ($this->matchString($m, $delim)) { 2975 if ($m[1] !== '') { 2976 $content[] = $m[1]; 2977 } 2978 2979 if ($m[2] === '#{') { 2980 $this->count -= \strlen($m[2]); 2981 2982 if ($this->interpolation($inter, false)) { 2983 $content[] = $inter; 2984 $hasInterpolation = true; 2985 } else { 2986 $this->count += \strlen($m[2]); 2987 $content[] = '#{'; // ignore it 2988 } 2989 } elseif ($m[2] === "\r") { 2990 $content[] = chr(10); 2991 // TODO : warning 2992 # DEPRECATION WARNING on line x, column y of zzz: 2993 # Unescaped multiline strings are deprecated and will be removed in a future version of Sass. 2994 # To include a newline in a string, use "\a" or "\a " as in CSS. 2995 if ($this->matchChar("\n", false)) { 2996 $content[] = ' '; 2997 } 2998 } elseif ($m[2] === '\\') { 2999 if ( 3000 $this->literal("\r\n", 2, false) || 3001 $this->matchChar("\r", false) || 3002 $this->matchChar("\n", false) || 3003 $this->matchChar("\f", false) 3004 ) { 3005 // this is a continuation escaping, to be ignored 3006 } elseif ($this->matchEscapeCharacter($c)) { 3007 $content[] = $c; 3008 } else { 3009 throw $this->parseError('Unterminated escape sequence'); 3010 } 3011 } else { 3012 $this->count -= \strlen($delim); 3013 break; // delim 3014 } 3015 } 3016 3017 $this->eatWhiteDefault = $oldWhite; 3018 3019 if ($this->literal($delim, \strlen($delim))) { 3020 if ($hasInterpolation && ! $keepDelimWithInterpolation) { 3021 $delim = '"'; 3022 } 3023 3024 $out = [Type::T_STRING, $delim, $content]; 3025 3026 return true; 3027 } 3028 3029 $this->seek($s); 3030 3031 return false; 3032 } 3033 3034 /** 3035 * @param string $out 3036 * @param bool $inKeywords 3037 * @return bool 3038 */ 3039 protected function matchEscapeCharacter(&$out, $inKeywords = false) 3040 { 3041 $s = $this->count; 3042 if ($this->match('[a-f0-9]', $m, false)) { 3043 $hex = $m[0]; 3044 3045 for ($i = 5; $i--;) { 3046 if ($this->match('[a-f0-9]', $m, false)) { 3047 $hex .= $m[0]; 3048 } else { 3049 break; 3050 } 3051 } 3052 3053 // CSS allows Unicode escape sequences to be followed by a delimiter space 3054 // (necessary in some cases for shorter sequences to disambiguate their end) 3055 $this->matchChar(' ', false); 3056 3057 $value = hexdec($hex); 3058 3059 if (!$inKeywords && ($value == 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF)) { 3060 $out = "\xEF\xBF\xBD"; // "\u{FFFD}" but with a syntax supported on PHP 5 3061 } elseif ($value < 0x20) { 3062 $out = Util::mbChr($value); 3063 } else { 3064 $out = Util::mbChr($value); 3065 } 3066 3067 return true; 3068 } 3069 3070 if ($this->match('.', $m, false)) { 3071 if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) { 3072 $this->seek($s); 3073 return false; 3074 } 3075 $out = $m[0]; 3076 3077 return true; 3078 } 3079 3080 return false; 3081 } 3082 3083 /** 3084 * Parse keyword or interpolation 3085 * 3086 * @param array $out 3087 * @param boolean $restricted 3088 * 3089 * @return boolean 3090 */ 3091 protected function mixedKeyword(&$out, $restricted = false) 3092 { 3093 $parts = []; 3094 3095 $oldWhite = $this->eatWhiteDefault; 3096 $this->eatWhiteDefault = false; 3097 3098 for (;;) { 3099 if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) { 3100 $parts[] = $key; 3101 continue; 3102 } 3103 3104 if ($this->interpolation($inter)) { 3105 $parts[] = $inter; 3106 continue; 3107 } 3108 3109 break; 3110 } 3111 3112 $this->eatWhiteDefault = $oldWhite; 3113 3114 if (! $parts) { 3115 return false; 3116 } 3117 3118 if ($this->eatWhiteDefault) { 3119 $this->whitespace(); 3120 } 3121 3122 $out = $parts; 3123 3124 return true; 3125 } 3126 3127 /** 3128 * Parse an unbounded string stopped by $end 3129 * 3130 * @param string $end 3131 * @param array $out 3132 * @param string $nestOpen 3133 * @param string $nestClose 3134 * @param boolean $rtrim 3135 * @param string $disallow 3136 * 3137 * @return boolean 3138 */ 3139 protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null) 3140 { 3141 $oldWhite = $this->eatWhiteDefault; 3142 $this->eatWhiteDefault = false; 3143 3144 if ($nestOpen && ! $nestClose) { 3145 $nestClose = $end; 3146 } 3147 3148 $patt = ($disallow ? '[^' . $this->pregQuote($disallow) . ']' : '.'); 3149 $patt = '(' . $patt . '*?)([\'"]|#\{|' 3150 . $this->pregQuote($end) . '|' 3151 . (($nestClose && $nestClose !== $end) ? $this->pregQuote($nestClose) . '|' : '') 3152 . static::$commentPattern . ')'; 3153 3154 $nestingLevel = 0; 3155 3156 $content = []; 3157 3158 while ($this->match($patt, $m, false)) { 3159 if (isset($m[1]) && $m[1] !== '') { 3160 $content[] = $m[1]; 3161 3162 if ($nestOpen) { 3163 $nestingLevel += substr_count($m[1], $nestOpen); 3164 } 3165 } 3166 3167 $tok = $m[2]; 3168 3169 $this->count -= \strlen($tok); 3170 3171 if ($tok === $end && ! $nestingLevel) { 3172 break; 3173 } 3174 3175 if ($tok === $nestClose) { 3176 $nestingLevel--; 3177 } 3178 3179 if (($tok === "'" || $tok === '"') && $this->string($str, true)) { 3180 $content[] = $str; 3181 continue; 3182 } 3183 3184 if ($tok === '#{' && $this->interpolation($inter)) { 3185 $content[] = $inter; 3186 continue; 3187 } 3188 3189 $content[] = $tok; 3190 $this->count += \strlen($tok); 3191 } 3192 3193 $this->eatWhiteDefault = $oldWhite; 3194 3195 if (! $content || $tok !== $end) { 3196 return false; 3197 } 3198 3199 // trim the end 3200 if ($rtrim && \is_string(end($content))) { 3201 $content[\count($content) - 1] = rtrim(end($content)); 3202 } 3203 3204 $out = [Type::T_STRING, '', $content]; 3205 3206 return true; 3207 } 3208 3209 /** 3210 * Parser interpolation 3211 * 3212 * @param string|array $out 3213 * @param boolean $lookWhite save information about whitespace before and after 3214 * 3215 * @return boolean 3216 */ 3217 protected function interpolation(&$out, $lookWhite = true) 3218 { 3219 $oldWhite = $this->eatWhiteDefault; 3220 $allowVars = $this->allowVars; 3221 $this->allowVars = true; 3222 $this->eatWhiteDefault = true; 3223 3224 $s = $this->count; 3225 3226 if ( 3227 $this->literal('#{', 2) && 3228 $this->valueList($value) && 3229 $this->matchChar('}', false) 3230 ) { 3231 if ($value === [Type::T_SELF]) { 3232 $out = $value; 3233 } else { 3234 if ($lookWhite) { 3235 $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : ''; 3236 $right = ( 3237 ! empty($this->buffer[$this->count]) && 3238 preg_match('/\s/', $this->buffer[$this->count]) 3239 ) ? ' ' : ''; 3240 } else { 3241 $left = $right = false; 3242 } 3243 3244 $out = [Type::T_INTERPOLATE, $value, $left, $right]; 3245 } 3246 3247 $this->eatWhiteDefault = $oldWhite; 3248 $this->allowVars = $allowVars; 3249 3250 if ($this->eatWhiteDefault) { 3251 $this->whitespace(); 3252 } 3253 3254 return true; 3255 } 3256 3257 $this->seek($s); 3258 3259 $this->eatWhiteDefault = $oldWhite; 3260 $this->allowVars = $allowVars; 3261 3262 return false; 3263 } 3264 3265 /** 3266 * Parse property name (as an array of parts or a string) 3267 * 3268 * @param array $out 3269 * 3270 * @return boolean 3271 */ 3272 protected function propertyName(&$out) 3273 { 3274 $parts = []; 3275 3276 $oldWhite = $this->eatWhiteDefault; 3277 $this->eatWhiteDefault = false; 3278 3279 for (;;) { 3280 if ($this->interpolation($inter)) { 3281 $parts[] = $inter; 3282 continue; 3283 } 3284 3285 if ($this->keyword($text)) { 3286 $parts[] = $text; 3287 continue; 3288 } 3289 3290 if (! $parts && $this->match('[:.#]', $m, false)) { 3291 // css hacks 3292 $parts[] = $m[0]; 3293 continue; 3294 } 3295 3296 break; 3297 } 3298 3299 $this->eatWhiteDefault = $oldWhite; 3300 3301 if (! $parts) { 3302 return false; 3303 } 3304 3305 // match comment hack 3306 if (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) { 3307 if (! empty($m[0])) { 3308 $parts[] = $m[0]; 3309 $this->count += \strlen($m[0]); 3310 } 3311 } 3312 3313 $this->whitespace(); // get any extra whitespace 3314 3315 $out = [Type::T_STRING, '', $parts]; 3316 3317 return true; 3318 } 3319 3320 /** 3321 * Parse custom property name (as an array of parts or a string) 3322 * 3323 * @param array $out 3324 * 3325 * @return boolean 3326 */ 3327 protected function customProperty(&$out) 3328 { 3329 $s = $this->count; 3330 3331 if (! $this->literal('--', 2, false)) { 3332 return false; 3333 } 3334 3335 $parts = ['--']; 3336 3337 $oldWhite = $this->eatWhiteDefault; 3338 $this->eatWhiteDefault = false; 3339 3340 for (;;) { 3341 if ($this->interpolation($inter)) { 3342 $parts[] = $inter; 3343 continue; 3344 } 3345 3346 if ($this->matchChar('&', false)) { 3347 $parts[] = [Type::T_SELF]; 3348 continue; 3349 } 3350 3351 if ($this->variable($var)) { 3352 $parts[] = $var; 3353 continue; 3354 } 3355 3356 if ($this->keyword($text)) { 3357 $parts[] = $text; 3358 continue; 3359 } 3360 3361 break; 3362 } 3363 3364 $this->eatWhiteDefault = $oldWhite; 3365 3366 if (\count($parts) == 1) { 3367 $this->seek($s); 3368 3369 return false; 3370 } 3371 3372 $this->whitespace(); // get any extra whitespace 3373 3374 $out = [Type::T_STRING, '', $parts]; 3375 3376 return true; 3377 } 3378 3379 /** 3380 * Parse comma separated selector list 3381 * 3382 * @param array $out 3383 * @param string|boolean $subSelector 3384 * 3385 * @return boolean 3386 */ 3387 protected function selectors(&$out, $subSelector = false) 3388 { 3389 $s = $this->count; 3390 $selectors = []; 3391 3392 while ($this->selector($sel, $subSelector)) { 3393 $selectors[] = $sel; 3394 3395 if (! $this->matchChar(',', true)) { 3396 break; 3397 } 3398 3399 while ($this->matchChar(',', true)) { 3400 ; // ignore extra 3401 } 3402 } 3403 3404 if (! $selectors) { 3405 $this->seek($s); 3406 3407 return false; 3408 } 3409 3410 $out = $selectors; 3411 3412 return true; 3413 } 3414 3415 /** 3416 * Parse whitespace separated selector list 3417 * 3418 * @param array $out 3419 * @param string|boolean $subSelector 3420 * 3421 * @return boolean 3422 */ 3423 protected function selector(&$out, $subSelector = false) 3424 { 3425 $selector = []; 3426 3427 $discardComments = $this->discardComments; 3428 $this->discardComments = true; 3429 3430 for (;;) { 3431 $s = $this->count; 3432 3433 if ($this->match('[>+~]+', $m, true)) { 3434 if ( 3435 $subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 && 3436 $m[0] === '+' && $this->match("(\d+|n\b)", $counter) 3437 ) { 3438 $this->seek($s); 3439 } else { 3440 $selector[] = [$m[0]]; 3441 continue; 3442 } 3443 } 3444 3445 if ($this->selectorSingle($part, $subSelector)) { 3446 $selector[] = $part; 3447 $this->whitespace(); 3448 continue; 3449 } 3450 3451 break; 3452 } 3453 3454 $this->discardComments = $discardComments; 3455 3456 if (! $selector) { 3457 return false; 3458 } 3459 3460 $out = $selector; 3461 3462 return true; 3463 } 3464 3465 /** 3466 * parsing escaped chars in selectors: 3467 * - escaped single chars are kept escaped in the selector but in a normalized form 3468 * (if not in 0-9a-f range as this would be ambigous) 3469 * - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form, 3470 * normalized to lowercase 3471 * 3472 * TODO: this is a fallback solution. Ideally escaped chars in selectors should be encoded as the genuine chars, 3473 * and escaping added when printing in the Compiler, where/if it's mandatory 3474 * - but this require a better formal selector representation instead of the array we have now 3475 * 3476 * @param string $out 3477 * @param bool $keepEscapedNumber 3478 * @return bool 3479 */ 3480 protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false) 3481 { 3482 $s_escape = $this->count; 3483 if ($this->match('\\\\', $m)) { 3484 $out = '\\' . $m[0]; 3485 return true; 3486 } 3487 3488 if ($this->matchEscapeCharacter($escapedout, true)) { 3489 if (strlen($escapedout) === 1) { 3490 if (!preg_match(",\w,", $escapedout)) { 3491 $out = '\\' . $escapedout; 3492 return true; 3493 } elseif (! $keepEscapedNumber || ! \is_numeric($escapedout)) { 3494 $out = $escapedout; 3495 return true; 3496 } 3497 } 3498 $escape_sequence = rtrim(substr($this->buffer, $s_escape, $this->count - $s_escape)); 3499 if (strlen($escape_sequence) < 6) { 3500 $escape_sequence .= ' '; 3501 } 3502 $out = '\\' . strtolower($escape_sequence); 3503 return true; 3504 } 3505 if ($this->match('\\S', $m)) { 3506 $out = '\\' . $m[0]; 3507 return true; 3508 } 3509 3510 3511 return false; 3512 } 3513 3514 /** 3515 * Parse the parts that make up a selector 3516 * 3517 * {@internal 3518 * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder 3519 * }} 3520 * 3521 * @param array $out 3522 * @param string|boolean $subSelector 3523 * 3524 * @return boolean 3525 */ 3526 protected function selectorSingle(&$out, $subSelector = false) 3527 { 3528 $oldWhite = $this->eatWhiteDefault; 3529 $this->eatWhiteDefault = false; 3530 3531 $parts = []; 3532 3533 if ($this->matchChar('*', false)) { 3534 $parts[] = '*'; 3535 } 3536 3537 for (;;) { 3538 if (! isset($this->buffer[$this->count])) { 3539 break; 3540 } 3541 3542 $s = $this->count; 3543 $char = $this->buffer[$this->count]; 3544 3545 // see if we can stop early 3546 if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') { 3547 break; 3548 } 3549 3550 // parsing a sub selector in () stop with the closing ) 3551 if ($subSelector && $char === ')') { 3552 break; 3553 } 3554 3555 //self 3556 switch ($char) { 3557 case '&': 3558 $parts[] = Compiler::$selfSelector; 3559 $this->count++; 3560 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 3561 continue 2; 3562 3563 case '.': 3564 $parts[] = '.'; 3565 $this->count++; 3566 continue 2; 3567 3568 case '|': 3569 $parts[] = '|'; 3570 $this->count++; 3571 continue 2; 3572 } 3573 3574 // handling of escaping in selectors : get the escaped char 3575 if ($char === '\\') { 3576 $this->count++; 3577 if ($this->matchEscapeCharacterInSelector($escaped, true)) { 3578 $parts[] = $escaped; 3579 continue; 3580 } 3581 $this->count--; 3582 } 3583 3584 if ($char === '%') { 3585 $this->count++; 3586 3587 if ($this->placeholder($placeholder)) { 3588 $parts[] = '%'; 3589 $parts[] = $placeholder; 3590 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 3591 continue; 3592 } 3593 3594 break; 3595 } 3596 3597 if ($char === '#') { 3598 if ($this->interpolation($inter)) { 3599 $parts[] = $inter; 3600 ! $this->cssOnly || $this->assertPlainCssValid(false, $s); 3601 continue; 3602 } 3603 3604 $parts[] = '#'; 3605 $this->count++; 3606 continue; 3607 } 3608 3609 // a pseudo selector 3610 if ($char === ':') { 3611 if ($this->buffer[$this->count + 1] === ':') { 3612 $this->count += 2; 3613 $part = '::'; 3614 } else { 3615 $this->count++; 3616 $part = ':'; 3617 } 3618 3619 if ($this->mixedKeyword($nameParts, true)) { 3620 $parts[] = $part; 3621 3622 foreach ($nameParts as $sub) { 3623 $parts[] = $sub; 3624 } 3625 3626 $ss = $this->count; 3627 3628 if ( 3629 $nameParts === ['not'] || 3630 $nameParts === ['is'] || 3631 $nameParts === ['has'] || 3632 $nameParts === ['where'] || 3633 $nameParts === ['slotted'] || 3634 $nameParts === ['nth-child'] || 3635 $nameParts === ['nth-last-child'] || 3636 $nameParts === ['nth-of-type'] || 3637 $nameParts === ['nth-last-of-type'] 3638 ) { 3639 if ( 3640 $this->matchChar('(', true) && 3641 ($this->selectors($subs, reset($nameParts)) || true) && 3642 $this->matchChar(')') 3643 ) { 3644 $parts[] = '('; 3645 3646 while ($sub = array_shift($subs)) { 3647 while ($ps = array_shift($sub)) { 3648 foreach ($ps as &$p) { 3649 $parts[] = $p; 3650 } 3651 3652 if (\count($sub) && reset($sub)) { 3653 $parts[] = ' '; 3654 } 3655 } 3656 3657 if (\count($subs) && reset($subs)) { 3658 $parts[] = ', '; 3659 } 3660 } 3661 3662 $parts[] = ')'; 3663 } else { 3664 $this->seek($ss); 3665 } 3666 } elseif ( 3667 $this->matchChar('(', true) && 3668 ($this->openString(')', $str, '(') || true) && 3669 $this->matchChar(')') 3670 ) { 3671 $parts[] = '('; 3672 3673 if (! empty($str)) { 3674 $parts[] = $str; 3675 } 3676 3677 $parts[] = ')'; 3678 } else { 3679 $this->seek($ss); 3680 } 3681 3682 continue; 3683 } 3684 } 3685 3686 $this->seek($s); 3687 3688 // 2n+1 3689 if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0) { 3690 if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) { 3691 $parts[] = $counter[0]; 3692 //$parts[] = str_replace(' ', '', $counter[0]); 3693 continue; 3694 } 3695 } 3696 3697 $this->seek($s); 3698 3699 // attribute selector 3700 if ( 3701 $char === '[' && 3702 $this->matchChar('[') && 3703 ($this->openString(']', $str, '[') || true) && 3704 $this->matchChar(']') 3705 ) { 3706 $parts[] = '['; 3707 3708 if (! empty($str)) { 3709 $parts[] = $str; 3710 } 3711 3712 $parts[] = ']'; 3713 continue; 3714 } 3715 3716 $this->seek($s); 3717 3718 // for keyframes 3719 if ($this->unit($unit)) { 3720 $parts[] = $unit; 3721 continue; 3722 } 3723 3724 if ($this->restrictedKeyword($name, false, true)) { 3725 $parts[] = $name; 3726 continue; 3727 } 3728 3729 break; 3730 } 3731 3732 $this->eatWhiteDefault = $oldWhite; 3733 3734 if (! $parts) { 3735 return false; 3736 } 3737 3738 $out = $parts; 3739 3740 return true; 3741 } 3742 3743 /** 3744 * Parse a variable 3745 * 3746 * @param array $out 3747 * 3748 * @return boolean 3749 */ 3750 protected function variable(&$out) 3751 { 3752 $s = $this->count; 3753 3754 if ( 3755 $this->matchChar('$', false) && 3756 $this->keyword($name) 3757 ) { 3758 if ($this->allowVars) { 3759 $out = [Type::T_VARIABLE, $name]; 3760 } else { 3761 $out = [Type::T_KEYWORD, '$' . $name]; 3762 } 3763 3764 return true; 3765 } 3766 3767 $this->seek($s); 3768 3769 return false; 3770 } 3771 3772 /** 3773 * Parse a keyword 3774 * 3775 * @param string $word 3776 * @param boolean $eatWhitespace 3777 * @param boolean $inSelector 3778 * 3779 * @return boolean 3780 */ 3781 protected function keyword(&$word, $eatWhitespace = null, $inSelector = false) 3782 { 3783 $s = $this->count; 3784 $match = $this->match( 3785 $this->utf8 3786 ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)' 3787 : '(([\w_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\w\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)', 3788 $m, 3789 false 3790 ); 3791 3792 if ($match) { 3793 $word = $m[1]; 3794 3795 // handling of escaping in keyword : get the escaped char 3796 if (strpos($word, '\\') !== false) { 3797 $send = $this->count; 3798 $escapedWord = []; 3799 $this->seek($s); 3800 $previousEscape = false; 3801 while ($this->count < $send) { 3802 $char = $this->buffer[$this->count]; 3803 $this->count++; 3804 if ( 3805 $this->count < $send 3806 && $char === '\\' 3807 && !$previousEscape 3808 && ( 3809 $inSelector ? 3810 $this->matchEscapeCharacterInSelector($out) 3811 : 3812 $this->matchEscapeCharacter($out, true) 3813 ) 3814 ) { 3815 $escapedWord[] = $out; 3816 } else { 3817 if ($previousEscape) { 3818 $previousEscape = false; 3819 } elseif ($char === '\\') { 3820 $previousEscape = true; 3821 } 3822 $escapedWord[] = $char; 3823 } 3824 } 3825 3826 $word = implode('', $escapedWord); 3827 } 3828 3829 if (is_null($eatWhitespace) ? $this->eatWhiteDefault : $eatWhitespace) { 3830 $this->whitespace(); 3831 } 3832 3833 return true; 3834 } 3835 3836 return false; 3837 } 3838 3839 /** 3840 * Parse a keyword that should not start with a number 3841 * 3842 * @param string $word 3843 * @param boolean $eatWhitespace 3844 * @param boolean $inSelector 3845 * 3846 * @return boolean 3847 */ 3848 protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false) 3849 { 3850 $s = $this->count; 3851 3852 if ($this->keyword($word, $eatWhitespace, $inSelector) && (\ord($word[0]) > 57 || \ord($word[0]) < 48)) { 3853 return true; 3854 } 3855 3856 $this->seek($s); 3857 3858 return false; 3859 } 3860 3861 /** 3862 * Parse a placeholder 3863 * 3864 * @param string|array $placeholder 3865 * 3866 * @return boolean 3867 */ 3868 protected function placeholder(&$placeholder) 3869 { 3870 $match = $this->match( 3871 $this->utf8 3872 ? '([\pL\w\-_]+)' 3873 : '([\w\-_]+)', 3874 $m 3875 ); 3876 3877 if ($match) { 3878 $placeholder = $m[1]; 3879 3880 return true; 3881 } 3882 3883 if ($this->interpolation($placeholder)) { 3884 return true; 3885 } 3886 3887 return false; 3888 } 3889 3890 /** 3891 * Parse a url 3892 * 3893 * @param array $out 3894 * 3895 * @return boolean 3896 */ 3897 protected function url(&$out) 3898 { 3899 if ($this->literal('url(', 4)) { 3900 $s = $this->count; 3901 3902 if ( 3903 ($this->string($out) || $this->spaceList($out)) && 3904 $this->matchChar(')') 3905 ) { 3906 $out = [Type::T_STRING, '', ['url(', $out, ')']]; 3907 3908 return true; 3909 } 3910 3911 $this->seek($s); 3912 3913 if ( 3914 $this->openString(')', $out) && 3915 $this->matchChar(')') 3916 ) { 3917 $out = [Type::T_STRING, '', ['url(', $out, ')']]; 3918 3919 return true; 3920 } 3921 } 3922 3923 return false; 3924 } 3925 3926 /** 3927 * Consume an end of statement delimiter 3928 * @param bool $eatWhitespace 3929 * 3930 * @return boolean 3931 */ 3932 protected function end($eatWhitespace = null) 3933 { 3934 if ($this->matchChar(';', $eatWhitespace)) { 3935 return true; 3936 } 3937 3938 if ($this->count === \strlen($this->buffer) || $this->buffer[$this->count] === '}') { 3939 // if there is end of file or a closing block next then we don't need a ; 3940 return true; 3941 } 3942 3943 return false; 3944 } 3945 3946 /** 3947 * Strip assignment flag from the list 3948 * 3949 * @param array $value 3950 * 3951 * @return array 3952 */ 3953 protected function stripAssignmentFlags(&$value) 3954 { 3955 $flags = []; 3956 3957 for ($token = &$value; $token[0] === Type::T_LIST && ($s = \count($token[2])); $token = &$lastNode) { 3958 $lastNode = &$token[2][$s - 1]; 3959 3960 while ($lastNode[0] === Type::T_KEYWORD && \in_array($lastNode[1], ['!default', '!global'])) { 3961 array_pop($token[2]); 3962 3963 $node = end($token[2]); 3964 $token = $this->flattenList($token); 3965 $flags[] = $lastNode[1]; 3966 $lastNode = $node; 3967 } 3968 } 3969 3970 return $flags; 3971 } 3972 3973 /** 3974 * Strip optional flag from selector list 3975 * 3976 * @param array $selectors 3977 * 3978 * @return string 3979 */ 3980 protected function stripOptionalFlag(&$selectors) 3981 { 3982 $optional = false; 3983 $selector = end($selectors); 3984 $part = end($selector); 3985 3986 if ($part === ['!optional']) { 3987 array_pop($selectors[\count($selectors) - 1]); 3988 3989 $optional = true; 3990 } 3991 3992 return $optional; 3993 } 3994 3995 /** 3996 * Turn list of length 1 into value type 3997 * 3998 * @param array $value 3999 * 4000 * @return array 4001 */ 4002 protected function flattenList($value) 4003 { 4004 if ($value[0] === Type::T_LIST && \count($value[2]) === 1) { 4005 return $this->flattenList($value[2][0]); 4006 } 4007 4008 return $value; 4009 } 4010 4011 /** 4012 * Quote regular expression 4013 * 4014 * @param string $what 4015 * 4016 * @return string 4017 */ 4018 private function pregQuote($what) 4019 { 4020 return preg_quote($what, '/'); 4021 } 4022 4023 /** 4024 * Extract line numbers from buffer 4025 * 4026 * @param string $buffer 4027 */ 4028 private function extractLineNumbers($buffer) 4029 { 4030 $this->sourcePositions = [0 => 0]; 4031 $prev = 0; 4032 4033 while (($pos = strpos($buffer, "\n", $prev)) !== false) { 4034 $this->sourcePositions[] = $pos; 4035 $prev = $pos + 1; 4036 } 4037 4038 $this->sourcePositions[] = \strlen($buffer); 4039 4040 if (substr($buffer, -1) !== "\n") { 4041 $this->sourcePositions[] = \strlen($buffer) + 1; 4042 } 4043 } 4044 4045 /** 4046 * Get source line number and column (given character position in the buffer) 4047 * 4048 * @param integer $pos 4049 * 4050 * @return array 4051 */ 4052 private function getSourcePosition($pos) 4053 { 4054 $low = 0; 4055 $high = \count($this->sourcePositions); 4056 4057 while ($low < $high) { 4058 $mid = (int) (($high + $low) / 2); 4059 4060 if ($pos < $this->sourcePositions[$mid]) { 4061 $high = $mid - 1; 4062 continue; 4063 } 4064 4065 if ($pos >= $this->sourcePositions[$mid + 1]) { 4066 $low = $mid + 1; 4067 continue; 4068 } 4069 4070 return [$mid + 1, $pos - $this->sourcePositions[$mid]]; 4071 } 4072 4073 return [$low + 1, $pos - $this->sourcePositions[$low]]; 4074 } 4075 4076 /** 4077 * Save internal encoding 4078 */ 4079 private function saveEncoding() 4080 { 4081 if (\extension_loaded('mbstring')) { 4082 $this->encoding = mb_internal_encoding(); 4083 4084 mb_internal_encoding('iso-8859-1'); 4085 } 4086 } 4087 4088 /** 4089 * Restore internal encoding 4090 */ 4091 private function restoreEncoding() 4092 { 4093 if (\extension_loaded('mbstring') && $this->encoding) { 4094 mb_internal_encoding($this->encoding); 4095 } 4096 } 4097 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body