1e6380ba3SAndreas Gohr<?php 2e6380ba3SAndreas Gohr/** 3e6380ba3SAndreas Gohr * http://leafo.net/lessphp 4e6380ba3SAndreas Gohr * 5e6380ba3SAndreas Gohr * LESS CSS compiler, adapted from http://lesscss.org 6e6380ba3SAndreas Gohr * 7e6380ba3SAndreas Gohr * Copyright 2013, Leaf Corcoran <leafot@gmail.com> 8e6380ba3SAndreas Gohr * Copyright 2016, Marcus Schwarz <github@maswaba.de> 9e6380ba3SAndreas Gohr * Licensed under MIT or GPLv3, see LICENSE 10e6380ba3SAndreas Gohr */ 11e6380ba3SAndreas Gohr 12e6380ba3SAndreas Gohrnamespace LesserPHP; 13e6380ba3SAndreas Gohr 14e6380ba3SAndreas Gohruse Exception; 15e6380ba3SAndreas Gohruse LesserPHP\Utils\Util; 16e6380ba3SAndreas Gohruse stdClass; 17e6380ba3SAndreas Gohr 18e6380ba3SAndreas Gohr/** 19e6380ba3SAndreas Gohr * responsible for taking a string of LESS code and converting it into a syntax tree 20e6380ba3SAndreas Gohr */ 21e6380ba3SAndreas Gohrclass Parser 22e6380ba3SAndreas Gohr{ 23e6380ba3SAndreas Gohr 24e6380ba3SAndreas Gohr public $eatWhiteDefault; 25e6380ba3SAndreas Gohr public $sourceName; 26e6380ba3SAndreas Gohr public $writeComments; 27e6380ba3SAndreas Gohr public $count; 28e6380ba3SAndreas Gohr public $line; 29e6380ba3SAndreas Gohr public $env; 30e6380ba3SAndreas Gohr public $buffer; 31e6380ba3SAndreas Gohr public $seenComments; 32e6380ba3SAndreas Gohr public $inExp; 33e6380ba3SAndreas Gohr 34e6380ba3SAndreas Gohr protected static $nextBlockId = 0; // used to uniquely identify blocks 35e6380ba3SAndreas Gohr 36e6380ba3SAndreas Gohr protected static $precedence = [ 37e6380ba3SAndreas Gohr '=<' => 0, 38e6380ba3SAndreas Gohr '>=' => 0, 39e6380ba3SAndreas Gohr '=' => 0, 40e6380ba3SAndreas Gohr '<' => 0, 41e6380ba3SAndreas Gohr '>' => 0, 42e6380ba3SAndreas Gohr 43e6380ba3SAndreas Gohr '+' => 1, 44e6380ba3SAndreas Gohr '-' => 1, 45e6380ba3SAndreas Gohr '*' => 2, 46e6380ba3SAndreas Gohr '/' => 2, 47e6380ba3SAndreas Gohr '%' => 2, 48e6380ba3SAndreas Gohr ]; 49e6380ba3SAndreas Gohr 50e6380ba3SAndreas Gohr protected static $whitePattern; 51e6380ba3SAndreas Gohr protected static $commentMulti; 52e6380ba3SAndreas Gohr 53e6380ba3SAndreas Gohr protected static $commentSingle = '//'; 54e6380ba3SAndreas Gohr protected static $commentMultiLeft = '/*'; 55e6380ba3SAndreas Gohr protected static $commentMultiRight = '*/'; 56e6380ba3SAndreas Gohr 57e6380ba3SAndreas Gohr // regex string to match any of the operators 58e6380ba3SAndreas Gohr protected static $operatorString; 59e6380ba3SAndreas Gohr 60e6380ba3SAndreas Gohr // these properties will supress division unless it's inside parenthases 61e6380ba3SAndreas Gohr protected static $supressDivisionProps = 62e6380ba3SAndreas Gohr ['/border-radius$/i', '/^font$/i']; 63e6380ba3SAndreas Gohr 64e6380ba3SAndreas Gohr protected $blockDirectives = [ 65e6380ba3SAndreas Gohr 'font-face', 66e6380ba3SAndreas Gohr 'keyframes', 67e6380ba3SAndreas Gohr 'page', 68e6380ba3SAndreas Gohr '-moz-document', 69e6380ba3SAndreas Gohr 'viewport', 70e6380ba3SAndreas Gohr '-moz-viewport', 71e6380ba3SAndreas Gohr '-o-viewport', 72e6380ba3SAndreas Gohr '-ms-viewport' 73e6380ba3SAndreas Gohr ]; 74e6380ba3SAndreas Gohr protected $lineDirectives = ['charset']; 75e6380ba3SAndreas Gohr 76e6380ba3SAndreas Gohr /** 77e6380ba3SAndreas Gohr * if we are in parens we can be more liberal with whitespace around 78e6380ba3SAndreas Gohr * operators because it must evaluate to a single value and thus is less 79e6380ba3SAndreas Gohr * ambiguous. 80e6380ba3SAndreas Gohr * 81e6380ba3SAndreas Gohr * Consider: 82e6380ba3SAndreas Gohr * property1: 10 -5; // is two numbers, 10 and -5 83e6380ba3SAndreas Gohr * property2: (10 -5); // should evaluate to 5 84e6380ba3SAndreas Gohr */ 85e6380ba3SAndreas Gohr protected $inParens = false; 86e6380ba3SAndreas Gohr 87e6380ba3SAndreas Gohr // caches preg escaped literals 88e6380ba3SAndreas Gohr protected static $literalCache = []; 89e6380ba3SAndreas Gohr 90e6380ba3SAndreas Gohr protected $currentProperty; 91e6380ba3SAndreas Gohr 92e6380ba3SAndreas Gohr /** 93e6380ba3SAndreas Gohr * @param string|null $sourceName name used for error messages 94e6380ba3SAndreas Gohr */ 95e6380ba3SAndreas Gohr public function __construct(?string $sourceName = null) 96e6380ba3SAndreas Gohr { 97e6380ba3SAndreas Gohr $this->eatWhiteDefault = true; 98e6380ba3SAndreas Gohr $this->sourceName = $sourceName; // name used for error messages 99e6380ba3SAndreas Gohr 100e6380ba3SAndreas Gohr $this->writeComments = false; 101e6380ba3SAndreas Gohr 102e6380ba3SAndreas Gohr if (!self::$operatorString) { 103e6380ba3SAndreas Gohr self::$operatorString = 104e6380ba3SAndreas Gohr '(' . implode('|', array_map( 105e6380ba3SAndreas Gohr [Util::class, 'pregQuote'], 106e6380ba3SAndreas Gohr array_keys(self::$precedence) 107e6380ba3SAndreas Gohr )) . ')'; 108e6380ba3SAndreas Gohr 109e6380ba3SAndreas Gohr $commentSingle = Util::pregQuote(self::$commentSingle); 110e6380ba3SAndreas Gohr $commentMultiLeft = Util::pregQuote(self::$commentMultiLeft); 111e6380ba3SAndreas Gohr $commentMultiRight = Util::pregQuote(self::$commentMultiRight); 112e6380ba3SAndreas Gohr 113e6380ba3SAndreas Gohr self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight; 114e6380ba3SAndreas Gohr self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais'; 115e6380ba3SAndreas Gohr } 116e6380ba3SAndreas Gohr } 117e6380ba3SAndreas Gohr 118e6380ba3SAndreas Gohr /** 119e6380ba3SAndreas Gohr * @throws Exception 120e6380ba3SAndreas Gohr */ 121e6380ba3SAndreas Gohr public function parse($buffer) 122e6380ba3SAndreas Gohr { 123e6380ba3SAndreas Gohr $this->count = 0; 124e6380ba3SAndreas Gohr $this->line = 1; 125e6380ba3SAndreas Gohr 126e6380ba3SAndreas Gohr $this->env = null; // block stack 127e6380ba3SAndreas Gohr $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer); 128e6380ba3SAndreas Gohr $this->pushSpecialBlock('root'); 129e6380ba3SAndreas Gohr $this->eatWhiteDefault = true; 130e6380ba3SAndreas Gohr $this->seenComments = []; 131e6380ba3SAndreas Gohr 132e6380ba3SAndreas Gohr // trim whitespace on head 133e6380ba3SAndreas Gohr // if (preg_match('/^\s+/', $this->buffer, $m)) { 134e6380ba3SAndreas Gohr // $this->line += substr_count($m[0], "\n"); 135e6380ba3SAndreas Gohr // $this->buffer = ltrim($this->buffer); 136e6380ba3SAndreas Gohr // } 137e6380ba3SAndreas Gohr $this->whitespace(); 138e6380ba3SAndreas Gohr 139e6380ba3SAndreas Gohr // parse the entire file 140e6380ba3SAndreas Gohr while (false !== $this->parseChunk()) { 141e6380ba3SAndreas Gohr // no-op 142e6380ba3SAndreas Gohr } 143e6380ba3SAndreas Gohr 144e6380ba3SAndreas Gohr if ($this->count != strlen($this->buffer)) { 145c13ef3baSAndreas Gohr $this->throwError(sprintf( 146c13ef3baSAndreas Gohr "parse error: count mismatches buffer length %d != %d", 147c13ef3baSAndreas Gohr $this->count, 148c13ef3baSAndreas Gohr strlen($this->buffer) 149c13ef3baSAndreas Gohr )); 150e6380ba3SAndreas Gohr } 151e6380ba3SAndreas Gohr 152e6380ba3SAndreas Gohr // TODO report where the block was opened 153e6380ba3SAndreas Gohr if (!property_exists($this->env, 'parent') || !is_null($this->env->parent)) { 154e6380ba3SAndreas Gohr $this->throwError('parse error: unclosed block'); 155e6380ba3SAndreas Gohr } 156e6380ba3SAndreas Gohr 157e6380ba3SAndreas Gohr return $this->env; 158e6380ba3SAndreas Gohr } 159e6380ba3SAndreas Gohr 160e6380ba3SAndreas Gohr /** 161e6380ba3SAndreas Gohr * Parse a single chunk off the head of the buffer and append it to the 162e6380ba3SAndreas Gohr * current parse environment. 163e6380ba3SAndreas Gohr * Returns false when the buffer is empty, or when there is an error. 164e6380ba3SAndreas Gohr * 165e6380ba3SAndreas Gohr * This function is called repeatedly until the entire document is 166e6380ba3SAndreas Gohr * parsed. 167e6380ba3SAndreas Gohr * 168e6380ba3SAndreas Gohr * This parser is most similar to a recursive descent parser. Single 169e6380ba3SAndreas Gohr * functions represent discrete grammatical rules for the language, and 170e6380ba3SAndreas Gohr * they are able to capture the text that represents those rules. 171e6380ba3SAndreas Gohr * 172e6380ba3SAndreas Gohr * Consider the function Lessc::keyword(). (all parse functions are 173e6380ba3SAndreas Gohr * structured the same) 174e6380ba3SAndreas Gohr * 175e6380ba3SAndreas Gohr * The function takes a single reference argument. When calling the 176e6380ba3SAndreas Gohr * function it will attempt to match a keyword on the head of the buffer. 177e6380ba3SAndreas Gohr * If it is successful, it will place the keyword in the referenced 178e6380ba3SAndreas Gohr * argument, advance the position in the buffer, and return true. If it 179e6380ba3SAndreas Gohr * fails then it won't advance the buffer and it will return false. 180e6380ba3SAndreas Gohr * 181e6380ba3SAndreas Gohr * All of these parse functions are powered by Lessc::match(), which behaves 182e6380ba3SAndreas Gohr * the same way, but takes a literal regular expression. Sometimes it is 183e6380ba3SAndreas Gohr * more convenient to use match instead of creating a new function. 184e6380ba3SAndreas Gohr * 185e6380ba3SAndreas Gohr * Because of the format of the functions, to parse an entire string of 186e6380ba3SAndreas Gohr * grammatical rules, you can chain them together using &&. 187e6380ba3SAndreas Gohr * 188e6380ba3SAndreas Gohr * But, if some of the rules in the chain succeed before one fails, then 189e6380ba3SAndreas Gohr * the buffer position will be left at an invalid state. In order to 190e6380ba3SAndreas Gohr * avoid this, Lessc::seek() is used to remember and set buffer positions. 191e6380ba3SAndreas Gohr * 192e6380ba3SAndreas Gohr * Before parsing a chain, use $s = $this->seek() to remember the current 193e6380ba3SAndreas Gohr * position into $s. Then if a chain fails, use $this->seek($s) to 194e6380ba3SAndreas Gohr * go back where we started. 195e6380ba3SAndreas Gohr * 196e6380ba3SAndreas Gohr * @throws Exception 197e6380ba3SAndreas Gohr */ 198e6380ba3SAndreas Gohr protected function parseChunk() 199e6380ba3SAndreas Gohr { 200e6380ba3SAndreas Gohr if (empty($this->buffer)) return false; 201e6380ba3SAndreas Gohr $s = $this->seek(); 202e6380ba3SAndreas Gohr 203e6380ba3SAndreas Gohr if ($this->whitespace()) { 204e6380ba3SAndreas Gohr return true; 205e6380ba3SAndreas Gohr } 206e6380ba3SAndreas Gohr 207e6380ba3SAndreas Gohr // setting a property 208e6380ba3SAndreas Gohr if ($this->keyword($key) && $this->assign() && 209e6380ba3SAndreas Gohr $this->propertyValue($value, $key) && $this->end()) { 210e6380ba3SAndreas Gohr $this->append(['assign', $key, $value], $s); 211e6380ba3SAndreas Gohr return true; 212e6380ba3SAndreas Gohr } else { 213e6380ba3SAndreas Gohr $this->seek($s); 214e6380ba3SAndreas Gohr } 215e6380ba3SAndreas Gohr 216e6380ba3SAndreas Gohr 217e6380ba3SAndreas Gohr // look for special css blocks 218e6380ba3SAndreas Gohr if ($this->literal('@', false)) { 219e6380ba3SAndreas Gohr $this->count--; 220e6380ba3SAndreas Gohr 221e6380ba3SAndreas Gohr // media 222e6380ba3SAndreas Gohr if ($this->literal('@media')) { 223e6380ba3SAndreas Gohr if (($this->mediaQueryList($mediaQueries) || true) 224e6380ba3SAndreas Gohr && $this->literal('{')) { 225e6380ba3SAndreas Gohr $media = $this->pushSpecialBlock('media'); 226e6380ba3SAndreas Gohr $media->queries = is_null($mediaQueries) ? [] : $mediaQueries; 227e6380ba3SAndreas Gohr return true; 228e6380ba3SAndreas Gohr } else { 229e6380ba3SAndreas Gohr $this->seek($s); 230e6380ba3SAndreas Gohr return false; 231e6380ba3SAndreas Gohr } 232e6380ba3SAndreas Gohr } 233e6380ba3SAndreas Gohr 234e6380ba3SAndreas Gohr if ($this->literal('@', false) && $this->keyword($dirName)) { 235e6380ba3SAndreas Gohr if ($this->isDirective($dirName, $this->blockDirectives)) { 236e6380ba3SAndreas Gohr if (($this->openString('{', $dirValue, null, [';']) || true) && 237e6380ba3SAndreas Gohr $this->literal('{')) { 238e6380ba3SAndreas Gohr $dir = $this->pushSpecialBlock('directive'); 239e6380ba3SAndreas Gohr $dir->name = $dirName; 240e6380ba3SAndreas Gohr if (isset($dirValue)) $dir->value = $dirValue; 241e6380ba3SAndreas Gohr return true; 242e6380ba3SAndreas Gohr } 243e6380ba3SAndreas Gohr } elseif ($this->isDirective($dirName, $this->lineDirectives)) { 244e6380ba3SAndreas Gohr if ($this->propertyValue($dirValue) && $this->end()) { 245e6380ba3SAndreas Gohr $this->append(['directive', $dirName, $dirValue]); 246e6380ba3SAndreas Gohr return true; 247e6380ba3SAndreas Gohr } 248e6380ba3SAndreas Gohr } elseif ($this->literal(':', true)) { 249e6380ba3SAndreas Gohr //Ruleset Definition 250e6380ba3SAndreas Gohr if (($this->openString('{', $dirValue, null, [';']) || true) && 251e6380ba3SAndreas Gohr $this->literal('{')) { 252e6380ba3SAndreas Gohr $dir = $this->pushBlock($this->fixTags(['@' . $dirName])); 253e6380ba3SAndreas Gohr $dir->name = $dirName; 254e6380ba3SAndreas Gohr if (isset($dirValue)) $dir->value = $dirValue; 255e6380ba3SAndreas Gohr return true; 256e6380ba3SAndreas Gohr } 257e6380ba3SAndreas Gohr } 258e6380ba3SAndreas Gohr } 259e6380ba3SAndreas Gohr 260e6380ba3SAndreas Gohr $this->seek($s); 261e6380ba3SAndreas Gohr } 262e6380ba3SAndreas Gohr 263e6380ba3SAndreas Gohr // setting a variable 264e6380ba3SAndreas Gohr if ($this->variable($var) && $this->assign() && 265e6380ba3SAndreas Gohr $this->propertyValue($value) && $this->end()) { 266e6380ba3SAndreas Gohr $this->append(['assign', $var, $value], $s); 267e6380ba3SAndreas Gohr return true; 268e6380ba3SAndreas Gohr } else { 269e6380ba3SAndreas Gohr $this->seek($s); 270e6380ba3SAndreas Gohr } 271e6380ba3SAndreas Gohr 272e6380ba3SAndreas Gohr if ($this->import($importValue)) { 273e6380ba3SAndreas Gohr $this->append($importValue, $s); 274e6380ba3SAndreas Gohr return true; 275e6380ba3SAndreas Gohr } 276e6380ba3SAndreas Gohr 277e6380ba3SAndreas Gohr // opening parametric mixin 278e6380ba3SAndreas Gohr if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && 279e6380ba3SAndreas Gohr ($this->guards($guards) || true) && 280e6380ba3SAndreas Gohr $this->literal('{')) { 281e6380ba3SAndreas Gohr $block = $this->pushBlock($this->fixTags([$tag])); 282e6380ba3SAndreas Gohr $block->args = $args; 283e6380ba3SAndreas Gohr $block->isVararg = $isVararg; 284e6380ba3SAndreas Gohr if (!empty($guards)) $block->guards = $guards; 285e6380ba3SAndreas Gohr return true; 286e6380ba3SAndreas Gohr } else { 287e6380ba3SAndreas Gohr $this->seek($s); 288e6380ba3SAndreas Gohr } 289e6380ba3SAndreas Gohr 290e6380ba3SAndreas Gohr // opening a simple block 291e6380ba3SAndreas Gohr if ($this->tags($tags) && $this->literal('{', false)) { 292e6380ba3SAndreas Gohr $tags = $this->fixTags($tags); 293e6380ba3SAndreas Gohr $this->pushBlock($tags); 294e6380ba3SAndreas Gohr return true; 295e6380ba3SAndreas Gohr } else { 296e6380ba3SAndreas Gohr $this->seek($s); 297e6380ba3SAndreas Gohr } 298e6380ba3SAndreas Gohr 299e6380ba3SAndreas Gohr // closing a block 300e6380ba3SAndreas Gohr if ($this->literal('}', false)) { 301e6380ba3SAndreas Gohr try { 302e6380ba3SAndreas Gohr $block = $this->pop(); 303e6380ba3SAndreas Gohr } catch (Exception $e) { 304e6380ba3SAndreas Gohr $this->seek($s); 305e6380ba3SAndreas Gohr $this->throwError($e->getMessage()); 306e6380ba3SAndreas Gohr } 307e6380ba3SAndreas Gohr 308e6380ba3SAndreas Gohr $hidden = false; 309e6380ba3SAndreas Gohr if (is_null($block->type)) { 310e6380ba3SAndreas Gohr $hidden = true; 311e6380ba3SAndreas Gohr if (!isset($block->args)) { 312e6380ba3SAndreas Gohr foreach ($block->tags as $tag) { 313e6380ba3SAndreas Gohr if (!is_string($tag) || $tag[0] != Constants::MPREFIX) { 314e6380ba3SAndreas Gohr $hidden = false; 315e6380ba3SAndreas Gohr break; 316e6380ba3SAndreas Gohr } 317e6380ba3SAndreas Gohr } 318e6380ba3SAndreas Gohr } 319e6380ba3SAndreas Gohr 320e6380ba3SAndreas Gohr foreach ($block->tags as $tag) { 321e6380ba3SAndreas Gohr if (is_string($tag)) { 322e6380ba3SAndreas Gohr $this->env->children[$tag][] = $block; 323e6380ba3SAndreas Gohr } 324e6380ba3SAndreas Gohr } 325e6380ba3SAndreas Gohr } 326e6380ba3SAndreas Gohr 327e6380ba3SAndreas Gohr if (!$hidden) { 328e6380ba3SAndreas Gohr $this->append(['block', $block], $s); 329e6380ba3SAndreas Gohr } 330e6380ba3SAndreas Gohr 331e6380ba3SAndreas Gohr // this is done here so comments aren't bundled into he block that 332e6380ba3SAndreas Gohr // was just closed 333e6380ba3SAndreas Gohr $this->whitespace(); 334e6380ba3SAndreas Gohr return true; 335e6380ba3SAndreas Gohr } 336e6380ba3SAndreas Gohr 337e6380ba3SAndreas Gohr // mixin 338e6380ba3SAndreas Gohr if ($this->mixinTags($tags) && 339e6380ba3SAndreas Gohr ($this->argumentDef($argv, $isVararg) || true) && 340e6380ba3SAndreas Gohr ($this->keyword($suffix) || true) && $this->end()) { 341e6380ba3SAndreas Gohr $tags = $this->fixTags($tags); 342e6380ba3SAndreas Gohr $this->append(['mixin', $tags, $argv, $suffix], $s); 343e6380ba3SAndreas Gohr return true; 344e6380ba3SAndreas Gohr } else { 345e6380ba3SAndreas Gohr $this->seek($s); 346e6380ba3SAndreas Gohr } 347e6380ba3SAndreas Gohr 348e6380ba3SAndreas Gohr // spare ; 349e6380ba3SAndreas Gohr if ($this->literal(';')) return true; 350e6380ba3SAndreas Gohr 351e6380ba3SAndreas Gohr return false; // got nothing, throw error 352e6380ba3SAndreas Gohr } 353e6380ba3SAndreas Gohr 354e6380ba3SAndreas Gohr protected function isDirective($dirname, $directives) 355e6380ba3SAndreas Gohr { 356e6380ba3SAndreas Gohr // TODO: cache pattern in parser 357e6380ba3SAndreas Gohr $pattern = implode( 358e6380ba3SAndreas Gohr '|', 359e6380ba3SAndreas Gohr array_map([Util::class, 'pregQuote'], $directives) 360e6380ba3SAndreas Gohr ); 361e6380ba3SAndreas Gohr $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; 362e6380ba3SAndreas Gohr 363e6380ba3SAndreas Gohr return preg_match($pattern, $dirname); 364e6380ba3SAndreas Gohr } 365e6380ba3SAndreas Gohr 366e6380ba3SAndreas Gohr protected function fixTags($tags) 367e6380ba3SAndreas Gohr { 368e6380ba3SAndreas Gohr // move @ tags out of variable namespace 369e6380ba3SAndreas Gohr foreach ($tags as &$tag) { 370e6380ba3SAndreas Gohr if ($tag[0] == Constants::VPREFIX) 371e6380ba3SAndreas Gohr $tag[0] = Constants::MPREFIX; 372e6380ba3SAndreas Gohr } 373e6380ba3SAndreas Gohr return $tags; 374e6380ba3SAndreas Gohr } 375e6380ba3SAndreas Gohr 376e6380ba3SAndreas Gohr // a list of expressions 377e6380ba3SAndreas Gohr protected function expressionList(&$exps) 378e6380ba3SAndreas Gohr { 379e6380ba3SAndreas Gohr $values = []; 380e6380ba3SAndreas Gohr 381e6380ba3SAndreas Gohr while ($this->expression($exp)) { 382e6380ba3SAndreas Gohr $values[] = $exp; 383e6380ba3SAndreas Gohr } 384e6380ba3SAndreas Gohr 385e6380ba3SAndreas Gohr if (count($values) == 0) return false; 386e6380ba3SAndreas Gohr 387e6380ba3SAndreas Gohr $exps = Lessc::compressList($values, ' '); 388e6380ba3SAndreas Gohr return true; 389e6380ba3SAndreas Gohr } 390e6380ba3SAndreas Gohr 391e6380ba3SAndreas Gohr /** 392e6380ba3SAndreas Gohr * Attempt to consume an expression. 393e6380ba3SAndreas Gohr * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code 394e6380ba3SAndreas Gohr */ 395e6380ba3SAndreas Gohr protected function expression(&$out) 396e6380ba3SAndreas Gohr { 397e6380ba3SAndreas Gohr if ($this->value($lhs)) { 398e6380ba3SAndreas Gohr $out = $this->expHelper($lhs, 0); 399e6380ba3SAndreas Gohr 400e6380ba3SAndreas Gohr // look for / shorthand 401e6380ba3SAndreas Gohr if (!empty($this->env->supressedDivision)) { 402e6380ba3SAndreas Gohr unset($this->env->supressedDivision); 403e6380ba3SAndreas Gohr $s = $this->seek(); 404e6380ba3SAndreas Gohr if ($this->literal('/') && $this->value($rhs)) { 405e6380ba3SAndreas Gohr $out = ['list', '', [$out, ['keyword', '/'], $rhs]]; 406e6380ba3SAndreas Gohr } else { 407e6380ba3SAndreas Gohr $this->seek($s); 408e6380ba3SAndreas Gohr } 409e6380ba3SAndreas Gohr } 410e6380ba3SAndreas Gohr 411e6380ba3SAndreas Gohr return true; 412e6380ba3SAndreas Gohr } 413e6380ba3SAndreas Gohr return false; 414e6380ba3SAndreas Gohr } 415e6380ba3SAndreas Gohr 416e6380ba3SAndreas Gohr /** 417e6380ba3SAndreas Gohr * recursively parse infix equation with $lhs at precedence $minP 418e6380ba3SAndreas Gohr */ 419e6380ba3SAndreas Gohr protected function expHelper($lhs, $minP) 420e6380ba3SAndreas Gohr { 421e6380ba3SAndreas Gohr $this->inExp = true; 422e6380ba3SAndreas Gohr $ss = $this->seek(); 423e6380ba3SAndreas Gohr 424e6380ba3SAndreas Gohr while (true) { 425e6380ba3SAndreas Gohr $whiteBefore = isset($this->buffer[$this->count - 1]) && 426e6380ba3SAndreas Gohr ctype_space($this->buffer[$this->count - 1]); 427e6380ba3SAndreas Gohr 428e6380ba3SAndreas Gohr // If there is whitespace before the operator, then we require 429e6380ba3SAndreas Gohr // whitespace after the operator for it to be an expression 430e6380ba3SAndreas Gohr $needWhite = $whiteBefore && !$this->inParens; 431e6380ba3SAndreas Gohr 432e6380ba3SAndreas Gohr if ( 433e6380ba3SAndreas Gohr $this->match(self::$operatorString . ($needWhite ? '\s' : ''), $m) && 434e6380ba3SAndreas Gohr self::$precedence[$m[1]] >= $minP 435e6380ba3SAndreas Gohr ) { 436e6380ba3SAndreas Gohr if ( 437e6380ba3SAndreas Gohr !$this->inParens && isset($this->env->currentProperty) && $m[1] == '/' && 438e6380ba3SAndreas Gohr empty($this->env->supressedDivision) 439e6380ba3SAndreas Gohr ) { 440e6380ba3SAndreas Gohr foreach (self::$supressDivisionProps as $pattern) { 441e6380ba3SAndreas Gohr if (preg_match($pattern, $this->env->currentProperty)) { 442e6380ba3SAndreas Gohr $this->env->supressedDivision = true; 443e6380ba3SAndreas Gohr break 2; 444e6380ba3SAndreas Gohr } 445e6380ba3SAndreas Gohr } 446e6380ba3SAndreas Gohr } 447e6380ba3SAndreas Gohr 448e6380ba3SAndreas Gohr 449e6380ba3SAndreas Gohr $whiteAfter = isset($this->buffer[$this->count - 1]) && 450e6380ba3SAndreas Gohr ctype_space($this->buffer[$this->count - 1]); 451e6380ba3SAndreas Gohr 452e6380ba3SAndreas Gohr if (!$this->value($rhs)) break; 453e6380ba3SAndreas Gohr 454e6380ba3SAndreas Gohr // peek for next operator to see what to do with rhs 455e6380ba3SAndreas Gohr if ( 456e6380ba3SAndreas Gohr $this->peek(self::$operatorString, $next) && 457e6380ba3SAndreas Gohr self::$precedence[$next[1]] > self::$precedence[$m[1]] 458e6380ba3SAndreas Gohr ) { 459e6380ba3SAndreas Gohr $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); 460e6380ba3SAndreas Gohr } 461e6380ba3SAndreas Gohr 462e6380ba3SAndreas Gohr $lhs = ['expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter]; 463e6380ba3SAndreas Gohr $ss = $this->seek(); 464e6380ba3SAndreas Gohr 465e6380ba3SAndreas Gohr continue; 466e6380ba3SAndreas Gohr } 467e6380ba3SAndreas Gohr 468e6380ba3SAndreas Gohr break; 469e6380ba3SAndreas Gohr } 470e6380ba3SAndreas Gohr 471e6380ba3SAndreas Gohr $this->seek($ss); 472e6380ba3SAndreas Gohr 473e6380ba3SAndreas Gohr return $lhs; 474e6380ba3SAndreas Gohr } 475e6380ba3SAndreas Gohr 476e6380ba3SAndreas Gohr // consume a list of values for a property 477e6380ba3SAndreas Gohr public function propertyValue(&$value, $keyName = null) 478e6380ba3SAndreas Gohr { 479e6380ba3SAndreas Gohr $values = []; 480e6380ba3SAndreas Gohr 481e6380ba3SAndreas Gohr if ($keyName !== null) $this->env->currentProperty = $keyName; 482e6380ba3SAndreas Gohr 483e6380ba3SAndreas Gohr $s = null; 484e6380ba3SAndreas Gohr while ($this->expressionList($v)) { 485e6380ba3SAndreas Gohr $values[] = $v; 486e6380ba3SAndreas Gohr $s = $this->seek(); 487e6380ba3SAndreas Gohr if (!$this->literal(',')) break; 488e6380ba3SAndreas Gohr } 489e6380ba3SAndreas Gohr 490e6380ba3SAndreas Gohr if ($s) $this->seek($s); 491e6380ba3SAndreas Gohr 492e6380ba3SAndreas Gohr if ($keyName !== null) unset($this->env->currentProperty); 493e6380ba3SAndreas Gohr 494e6380ba3SAndreas Gohr if (count($values) == 0) return false; 495e6380ba3SAndreas Gohr 496e6380ba3SAndreas Gohr $value = Lessc::compressList($values, ', '); 497e6380ba3SAndreas Gohr return true; 498e6380ba3SAndreas Gohr } 499e6380ba3SAndreas Gohr 500e6380ba3SAndreas Gohr protected function parenValue(&$out) 501e6380ba3SAndreas Gohr { 502e6380ba3SAndreas Gohr $s = $this->seek(); 503e6380ba3SAndreas Gohr 504e6380ba3SAndreas Gohr // speed shortcut 505e6380ba3SAndreas Gohr if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != '(') { 506e6380ba3SAndreas Gohr return false; 507e6380ba3SAndreas Gohr } 508e6380ba3SAndreas Gohr 509e6380ba3SAndreas Gohr $inParens = $this->inParens; 510e6380ba3SAndreas Gohr if ($this->literal('(') && 511e6380ba3SAndreas Gohr ($this->inParens = true) && $this->expression($exp) && 512e6380ba3SAndreas Gohr $this->literal(')')) { 513e6380ba3SAndreas Gohr $out = $exp; 514e6380ba3SAndreas Gohr $this->inParens = $inParens; 515e6380ba3SAndreas Gohr return true; 516e6380ba3SAndreas Gohr } else { 517e6380ba3SAndreas Gohr $this->inParens = $inParens; 518e6380ba3SAndreas Gohr $this->seek($s); 519e6380ba3SAndreas Gohr } 520e6380ba3SAndreas Gohr 521e6380ba3SAndreas Gohr return false; 522e6380ba3SAndreas Gohr } 523e6380ba3SAndreas Gohr 524e6380ba3SAndreas Gohr // a single value 525e6380ba3SAndreas Gohr protected function value(&$value) 526e6380ba3SAndreas Gohr { 527e6380ba3SAndreas Gohr $s = $this->seek(); 528e6380ba3SAndreas Gohr 529e6380ba3SAndreas Gohr // speed shortcut 530e6380ba3SAndreas Gohr if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == '-') { 531e6380ba3SAndreas Gohr // negation 532e6380ba3SAndreas Gohr if ($this->literal('-', false) && 533e6380ba3SAndreas Gohr (($this->variable($inner) && $inner = ['variable', $inner]) || 534e6380ba3SAndreas Gohr $this->unit($inner) || 535e6380ba3SAndreas Gohr $this->parenValue($inner))) { 536e6380ba3SAndreas Gohr $value = ['unary', '-', $inner]; 537e6380ba3SAndreas Gohr return true; 538e6380ba3SAndreas Gohr } else { 539e6380ba3SAndreas Gohr $this->seek($s); 540e6380ba3SAndreas Gohr } 541e6380ba3SAndreas Gohr } 542e6380ba3SAndreas Gohr 543e6380ba3SAndreas Gohr if ($this->parenValue($value)) return true; 544e6380ba3SAndreas Gohr if ($this->unit($value)) return true; 545e6380ba3SAndreas Gohr if ($this->color($value)) return true; 546e6380ba3SAndreas Gohr if ($this->func($value)) return true; 547e6380ba3SAndreas Gohr if ($this->stringValue($value)) return true; 548e6380ba3SAndreas Gohr 549e6380ba3SAndreas Gohr if ($this->keyword($word)) { 550e6380ba3SAndreas Gohr $value = ['keyword', $word]; 551e6380ba3SAndreas Gohr return true; 552e6380ba3SAndreas Gohr } 553e6380ba3SAndreas Gohr 554e6380ba3SAndreas Gohr // try a variable 555e6380ba3SAndreas Gohr if ($this->variable($var)) { 556e6380ba3SAndreas Gohr $value = ['variable', $var]; 557e6380ba3SAndreas Gohr return true; 558e6380ba3SAndreas Gohr } 559e6380ba3SAndreas Gohr 560e6380ba3SAndreas Gohr // unquote string (should this work on any type? 561e6380ba3SAndreas Gohr if ($this->literal('~') && $this->stringValue($str)) { 562e6380ba3SAndreas Gohr $value = ['escape', $str]; 563e6380ba3SAndreas Gohr return true; 564e6380ba3SAndreas Gohr } else { 565e6380ba3SAndreas Gohr $this->seek($s); 566e6380ba3SAndreas Gohr } 567e6380ba3SAndreas Gohr 568e6380ba3SAndreas Gohr // css hack: \0 569e6380ba3SAndreas Gohr if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { 570e6380ba3SAndreas Gohr $value = ['keyword', '\\' . $m[1]]; 571e6380ba3SAndreas Gohr return true; 572e6380ba3SAndreas Gohr } else { 573e6380ba3SAndreas Gohr $this->seek($s); 574e6380ba3SAndreas Gohr } 575e6380ba3SAndreas Gohr 576e6380ba3SAndreas Gohr return false; 577e6380ba3SAndreas Gohr } 578e6380ba3SAndreas Gohr 579e6380ba3SAndreas Gohr // an import statement 580e6380ba3SAndreas Gohr protected function import(&$out) 581e6380ba3SAndreas Gohr { 582e6380ba3SAndreas Gohr if (!$this->literal('@import')) return false; 583e6380ba3SAndreas Gohr 584e6380ba3SAndreas Gohr // @import "something.css" media; 585e6380ba3SAndreas Gohr // @import url("something.css") media; 586e6380ba3SAndreas Gohr // @import url(something.css) media; 587e6380ba3SAndreas Gohr 588e6380ba3SAndreas Gohr if ($this->propertyValue($value)) { 589e6380ba3SAndreas Gohr $out = ['import', $value]; 590e6380ba3SAndreas Gohr return true; 591e6380ba3SAndreas Gohr } 592e6380ba3SAndreas Gohr return false; 593e6380ba3SAndreas Gohr } 594e6380ba3SAndreas Gohr 595e6380ba3SAndreas Gohr protected function mediaQueryList(&$out) 596e6380ba3SAndreas Gohr { 597e6380ba3SAndreas Gohr if ($this->genericList($list, 'mediaQuery', ',', false)) { 598e6380ba3SAndreas Gohr $out = $list[2]; 599e6380ba3SAndreas Gohr return true; 600e6380ba3SAndreas Gohr } 601e6380ba3SAndreas Gohr return false; 602e6380ba3SAndreas Gohr } 603e6380ba3SAndreas Gohr 604e6380ba3SAndreas Gohr protected function mediaQuery(&$out) 605e6380ba3SAndreas Gohr { 606e6380ba3SAndreas Gohr $s = $this->seek(); 607e6380ba3SAndreas Gohr 608e6380ba3SAndreas Gohr $expressions = null; 609e6380ba3SAndreas Gohr $parts = []; 610e6380ba3SAndreas Gohr 611e6380ba3SAndreas Gohr if ( 612e6380ba3SAndreas Gohr ( 613e6380ba3SAndreas Gohr $this->literal('only') && ($only = true) || 614e6380ba3SAndreas Gohr $this->literal('not') && ($not = true) || 615e6380ba3SAndreas Gohr true 616e6380ba3SAndreas Gohr ) && 617e6380ba3SAndreas Gohr $this->keyword($mediaType) 618e6380ba3SAndreas Gohr ) { 619e6380ba3SAndreas Gohr $prop = ['mediaType']; 620e6380ba3SAndreas Gohr if (isset($only)) $prop[] = 'only'; 621e6380ba3SAndreas Gohr if (isset($not)) $prop[] = 'not'; 622e6380ba3SAndreas Gohr $prop[] = $mediaType; 623e6380ba3SAndreas Gohr $parts[] = $prop; 624e6380ba3SAndreas Gohr } else { 625e6380ba3SAndreas Gohr $this->seek($s); 626e6380ba3SAndreas Gohr } 627e6380ba3SAndreas Gohr 628e6380ba3SAndreas Gohr 629e6380ba3SAndreas Gohr if (!empty($mediaType) && !$this->literal('and')) { 630e6380ba3SAndreas Gohr // ~ 631e6380ba3SAndreas Gohr } else { 632e6380ba3SAndreas Gohr $this->genericList($expressions, 'mediaExpression', 'and', false); 633e6380ba3SAndreas Gohr if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); 634e6380ba3SAndreas Gohr } 635e6380ba3SAndreas Gohr 636e6380ba3SAndreas Gohr if (count($parts) == 0) { 637e6380ba3SAndreas Gohr $this->seek($s); 638e6380ba3SAndreas Gohr return false; 639e6380ba3SAndreas Gohr } 640e6380ba3SAndreas Gohr 641e6380ba3SAndreas Gohr $out = $parts; 642e6380ba3SAndreas Gohr return true; 643e6380ba3SAndreas Gohr } 644e6380ba3SAndreas Gohr 645e6380ba3SAndreas Gohr protected function mediaExpression(&$out) 646e6380ba3SAndreas Gohr { 647e6380ba3SAndreas Gohr $s = $this->seek(); 648e6380ba3SAndreas Gohr $value = null; 649e6380ba3SAndreas Gohr if ($this->literal('(') && 650e6380ba3SAndreas Gohr $this->keyword($feature) && 651e6380ba3SAndreas Gohr ($this->literal(':') && $this->expression($value) || true) && 652e6380ba3SAndreas Gohr $this->literal(')')) { 653e6380ba3SAndreas Gohr $out = ['mediaExp', $feature]; 654e6380ba3SAndreas Gohr if ($value) $out[] = $value; 655e6380ba3SAndreas Gohr return true; 656e6380ba3SAndreas Gohr } elseif ($this->variable($variable)) { 657e6380ba3SAndreas Gohr $out = ['variable', $variable]; 658e6380ba3SAndreas Gohr return true; 659e6380ba3SAndreas Gohr } 660e6380ba3SAndreas Gohr 661e6380ba3SAndreas Gohr $this->seek($s); 662e6380ba3SAndreas Gohr return false; 663e6380ba3SAndreas Gohr } 664e6380ba3SAndreas Gohr 665e6380ba3SAndreas Gohr // an unbounded string stopped by $end 666e6380ba3SAndreas Gohr protected function openString($end, &$out, $nestingOpen = null, $rejectStrs = null) 667e6380ba3SAndreas Gohr { 668e6380ba3SAndreas Gohr $oldWhite = $this->eatWhiteDefault; 669e6380ba3SAndreas Gohr $this->eatWhiteDefault = false; 670e6380ba3SAndreas Gohr 671e6380ba3SAndreas Gohr $stop = ["'", '"', '@{', $end]; 672e6380ba3SAndreas Gohr $stop = array_map([Util::class, 'pregQuote'], $stop); 673e6380ba3SAndreas Gohr // $stop[] = self::$commentMulti; 674e6380ba3SAndreas Gohr 675e6380ba3SAndreas Gohr if (!is_null($rejectStrs)) { 676e6380ba3SAndreas Gohr $stop = array_merge($stop, $rejectStrs); 677e6380ba3SAndreas Gohr } 678e6380ba3SAndreas Gohr 679e6380ba3SAndreas Gohr $patt = '(.*?)(' . implode('|', $stop) . ')'; 680e6380ba3SAndreas Gohr 681e6380ba3SAndreas Gohr $nestingLevel = 0; 682e6380ba3SAndreas Gohr 683e6380ba3SAndreas Gohr $content = []; 684e6380ba3SAndreas Gohr while ($this->match($patt, $m, false)) { 685e6380ba3SAndreas Gohr if (!empty($m[1])) { 686e6380ba3SAndreas Gohr $content[] = $m[1]; 687e6380ba3SAndreas Gohr if ($nestingOpen) { 688e6380ba3SAndreas Gohr $nestingLevel += substr_count($m[1], $nestingOpen); 689e6380ba3SAndreas Gohr } 690e6380ba3SAndreas Gohr } 691e6380ba3SAndreas Gohr 692e6380ba3SAndreas Gohr $tok = $m[2]; 693e6380ba3SAndreas Gohr 694e6380ba3SAndreas Gohr $this->count -= strlen($tok); 695e6380ba3SAndreas Gohr if ($tok == $end) { 696e6380ba3SAndreas Gohr if ($nestingLevel == 0) { 697e6380ba3SAndreas Gohr break; 698e6380ba3SAndreas Gohr } else { 699e6380ba3SAndreas Gohr $nestingLevel--; 700e6380ba3SAndreas Gohr } 701e6380ba3SAndreas Gohr } 702e6380ba3SAndreas Gohr 703e6380ba3SAndreas Gohr if (($tok == "'" || $tok == '"') && $this->stringValue($str)) { 704e6380ba3SAndreas Gohr $content[] = $str; 705e6380ba3SAndreas Gohr continue; 706e6380ba3SAndreas Gohr } 707e6380ba3SAndreas Gohr 708e6380ba3SAndreas Gohr if ($tok == '@{' && $this->interpolation($inter)) { 709e6380ba3SAndreas Gohr $content[] = $inter; 710e6380ba3SAndreas Gohr continue; 711e6380ba3SAndreas Gohr } 712e6380ba3SAndreas Gohr 713e6380ba3SAndreas Gohr if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { 714e6380ba3SAndreas Gohr break; 715e6380ba3SAndreas Gohr } 716e6380ba3SAndreas Gohr 717e6380ba3SAndreas Gohr $content[] = $tok; 718e6380ba3SAndreas Gohr $this->count += strlen($tok); 719e6380ba3SAndreas Gohr } 720e6380ba3SAndreas Gohr 721e6380ba3SAndreas Gohr $this->eatWhiteDefault = $oldWhite; 722e6380ba3SAndreas Gohr 723e6380ba3SAndreas Gohr if (count($content) == 0) return false; 724e6380ba3SAndreas Gohr 725e6380ba3SAndreas Gohr // trim the end 726e6380ba3SAndreas Gohr if (is_string(end($content))) { 727e6380ba3SAndreas Gohr $content[count($content) - 1] = rtrim(end($content)); 728e6380ba3SAndreas Gohr } 729e6380ba3SAndreas Gohr 730e6380ba3SAndreas Gohr $out = ['string', '', $content]; 731e6380ba3SAndreas Gohr return true; 732e6380ba3SAndreas Gohr } 733e6380ba3SAndreas Gohr 734e6380ba3SAndreas Gohr protected function stringValue(&$out) 735e6380ba3SAndreas Gohr { 736e6380ba3SAndreas Gohr $s = $this->seek(); 737e6380ba3SAndreas Gohr if ($this->literal('"', false)) { 738e6380ba3SAndreas Gohr $delim = '"'; 739e6380ba3SAndreas Gohr } elseif ($this->literal("'", false)) { 740e6380ba3SAndreas Gohr $delim = "'"; 741e6380ba3SAndreas Gohr } else { 742e6380ba3SAndreas Gohr return false; 743e6380ba3SAndreas Gohr } 744e6380ba3SAndreas Gohr 745e6380ba3SAndreas Gohr $content = []; 746e6380ba3SAndreas Gohr 747e6380ba3SAndreas Gohr // look for either ending delim , escape, or string interpolation 748e6380ba3SAndreas Gohr $patt = '([^\n]*?)(@\{|\\\\|' . Util::pregQuote($delim) . ')'; 749e6380ba3SAndreas Gohr 750e6380ba3SAndreas Gohr $oldWhite = $this->eatWhiteDefault; 751e6380ba3SAndreas Gohr $this->eatWhiteDefault = false; 752e6380ba3SAndreas Gohr 753e6380ba3SAndreas Gohr while ($this->match($patt, $m, false)) { 754e6380ba3SAndreas Gohr $content[] = $m[1]; 755e6380ba3SAndreas Gohr if ($m[2] == '@{') { 756e6380ba3SAndreas Gohr $this->count -= strlen($m[2]); 757e6380ba3SAndreas Gohr if ($this->interpolation($inter)) { 758e6380ba3SAndreas Gohr $content[] = $inter; 759e6380ba3SAndreas Gohr } else { 760e6380ba3SAndreas Gohr $this->count += strlen($m[2]); 761e6380ba3SAndreas Gohr $content[] = '@{'; // ignore it 762e6380ba3SAndreas Gohr } 763e6380ba3SAndreas Gohr } elseif ($m[2] == '\\') { 764e6380ba3SAndreas Gohr $content[] = $m[2]; 765e6380ba3SAndreas Gohr if ($this->literal($delim, false)) { 766e6380ba3SAndreas Gohr $content[] = $delim; 767e6380ba3SAndreas Gohr } 768e6380ba3SAndreas Gohr } else { 769e6380ba3SAndreas Gohr $this->count -= strlen($delim); 770e6380ba3SAndreas Gohr break; // delim 771e6380ba3SAndreas Gohr } 772e6380ba3SAndreas Gohr } 773e6380ba3SAndreas Gohr 774e6380ba3SAndreas Gohr $this->eatWhiteDefault = $oldWhite; 775e6380ba3SAndreas Gohr 776e6380ba3SAndreas Gohr if ($this->literal($delim)) { 777e6380ba3SAndreas Gohr $out = ['string', $delim, $content]; 778e6380ba3SAndreas Gohr return true; 779e6380ba3SAndreas Gohr } 780e6380ba3SAndreas Gohr 781e6380ba3SAndreas Gohr $this->seek($s); 782e6380ba3SAndreas Gohr return false; 783e6380ba3SAndreas Gohr } 784e6380ba3SAndreas Gohr 785e6380ba3SAndreas Gohr protected function interpolation(&$out) 786e6380ba3SAndreas Gohr { 787e6380ba3SAndreas Gohr $oldWhite = $this->eatWhiteDefault; 788e6380ba3SAndreas Gohr $this->eatWhiteDefault = true; 789e6380ba3SAndreas Gohr 790e6380ba3SAndreas Gohr $s = $this->seek(); 791e6380ba3SAndreas Gohr if ($this->literal('@{') && 792e6380ba3SAndreas Gohr $this->openString('}', $interp, null, ["'", '"', ';']) && 793e6380ba3SAndreas Gohr $this->literal('}', false)) { 794e6380ba3SAndreas Gohr $out = ['interpolate', $interp]; 795e6380ba3SAndreas Gohr $this->eatWhiteDefault = $oldWhite; 796e6380ba3SAndreas Gohr if ($this->eatWhiteDefault) $this->whitespace(); 797e6380ba3SAndreas Gohr return true; 798e6380ba3SAndreas Gohr } 799e6380ba3SAndreas Gohr 800e6380ba3SAndreas Gohr $this->eatWhiteDefault = $oldWhite; 801e6380ba3SAndreas Gohr $this->seek($s); 802e6380ba3SAndreas Gohr return false; 803e6380ba3SAndreas Gohr } 804e6380ba3SAndreas Gohr 805e6380ba3SAndreas Gohr protected function unit(&$unit) 806e6380ba3SAndreas Gohr { 807e6380ba3SAndreas Gohr // speed shortcut 808e6380ba3SAndreas Gohr if (isset($this->buffer[$this->count])) { 809e6380ba3SAndreas Gohr $char = $this->buffer[$this->count]; 810e6380ba3SAndreas Gohr if (!ctype_digit($char) && $char != '.') return false; 811e6380ba3SAndreas Gohr } 812e6380ba3SAndreas Gohr 813e6380ba3SAndreas Gohr if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { 814e6380ba3SAndreas Gohr $unit = ['number', $m[1], empty($m[2]) ? '' : $m[2]]; 815e6380ba3SAndreas Gohr return true; 816e6380ba3SAndreas Gohr } 817e6380ba3SAndreas Gohr return false; 818e6380ba3SAndreas Gohr } 819e6380ba3SAndreas Gohr 820e6380ba3SAndreas Gohr // a # color 821e6380ba3SAndreas Gohr protected function color(&$out) 822e6380ba3SAndreas Gohr { 823e6380ba3SAndreas Gohr if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { 824e6380ba3SAndreas Gohr if (strlen($m[1]) > 7) { 825e6380ba3SAndreas Gohr $out = ['string', '', [$m[1]]]; 826e6380ba3SAndreas Gohr } else { 827e6380ba3SAndreas Gohr $out = ['raw_color', $m[1]]; 828e6380ba3SAndreas Gohr } 829e6380ba3SAndreas Gohr return true; 830e6380ba3SAndreas Gohr } 831e6380ba3SAndreas Gohr 832e6380ba3SAndreas Gohr return false; 833e6380ba3SAndreas Gohr } 834e6380ba3SAndreas Gohr 835e6380ba3SAndreas Gohr /** 836e6380ba3SAndreas Gohr * consume an argument definition list surrounded by () 837e6380ba3SAndreas Gohr * each argument is a variable name with optional value 838e6380ba3SAndreas Gohr * or at the end a ... or a variable named followed by ... 839e6380ba3SAndreas Gohr * arguments are separated by , unless a ; is in the list, then ; is the 840e6380ba3SAndreas Gohr * delimiter. 841e6380ba3SAndreas Gohr * 842e6380ba3SAndreas Gohr * @throws Exception 843e6380ba3SAndreas Gohr */ 844e6380ba3SAndreas Gohr protected function argumentDef(&$args, &$isVararg) 845e6380ba3SAndreas Gohr { 846e6380ba3SAndreas Gohr $s = $this->seek(); 847e6380ba3SAndreas Gohr if (!$this->literal('(')) return false; 848e6380ba3SAndreas Gohr 849e6380ba3SAndreas Gohr $values = []; 850e6380ba3SAndreas Gohr $delim = ','; 851e6380ba3SAndreas Gohr $method = 'expressionList'; 852e6380ba3SAndreas Gohr 853e6380ba3SAndreas Gohr $value = null; 854e6380ba3SAndreas Gohr $rhs = null; 855e6380ba3SAndreas Gohr 856e6380ba3SAndreas Gohr $isVararg = false; 857e6380ba3SAndreas Gohr while (true) { 858e6380ba3SAndreas Gohr if ($this->literal('...')) { 859e6380ba3SAndreas Gohr $isVararg = true; 860e6380ba3SAndreas Gohr break; 861e6380ba3SAndreas Gohr } 862e6380ba3SAndreas Gohr 863e6380ba3SAndreas Gohr if ($this->$method($value)) { 864e6380ba3SAndreas Gohr if ($value[0] == 'variable') { 865e6380ba3SAndreas Gohr $arg = ['arg', $value[1]]; 866e6380ba3SAndreas Gohr $ss = $this->seek(); 867e6380ba3SAndreas Gohr 868e6380ba3SAndreas Gohr if ($this->assign() && $this->$method($rhs)) { 869e6380ba3SAndreas Gohr $arg[] = $rhs; 870e6380ba3SAndreas Gohr } else { 871e6380ba3SAndreas Gohr $this->seek($ss); 872e6380ba3SAndreas Gohr if ($this->literal('...')) { 873e6380ba3SAndreas Gohr $arg[0] = 'rest'; 874e6380ba3SAndreas Gohr $isVararg = true; 875e6380ba3SAndreas Gohr } 876e6380ba3SAndreas Gohr } 877e6380ba3SAndreas Gohr 878e6380ba3SAndreas Gohr $values[] = $arg; 879e6380ba3SAndreas Gohr if ($isVararg) break; 880e6380ba3SAndreas Gohr continue; 881e6380ba3SAndreas Gohr } else { 882e6380ba3SAndreas Gohr $values[] = ['lit', $value]; 883e6380ba3SAndreas Gohr } 884e6380ba3SAndreas Gohr } 885e6380ba3SAndreas Gohr 886e6380ba3SAndreas Gohr 887e6380ba3SAndreas Gohr if (!$this->literal($delim)) { 888e6380ba3SAndreas Gohr if ($delim == ',' && $this->literal(';')) { 889e6380ba3SAndreas Gohr // found new delim, convert existing args 890e6380ba3SAndreas Gohr $delim = ';'; 891e6380ba3SAndreas Gohr $method = 'propertyValue'; 892e6380ba3SAndreas Gohr 893e6380ba3SAndreas Gohr // transform arg list 894e6380ba3SAndreas Gohr if (isset($values[1])) { // 2 items 895e6380ba3SAndreas Gohr $newList = []; 896e6380ba3SAndreas Gohr foreach ($values as $i => $arg) { 897e6380ba3SAndreas Gohr switch ($arg[0]) { 898e6380ba3SAndreas Gohr case 'arg': 899e6380ba3SAndreas Gohr if ($i) { 900e6380ba3SAndreas Gohr $this->throwError('Cannot mix ; and , as delimiter types'); 901e6380ba3SAndreas Gohr } 902e6380ba3SAndreas Gohr $newList[] = $arg[2]; 903e6380ba3SAndreas Gohr break; 904e6380ba3SAndreas Gohr case 'lit': 905e6380ba3SAndreas Gohr $newList[] = $arg[1]; 906e6380ba3SAndreas Gohr break; 907e6380ba3SAndreas Gohr case 'rest': 908e6380ba3SAndreas Gohr $this->throwError('Unexpected rest before semicolon'); 909e6380ba3SAndreas Gohr } 910e6380ba3SAndreas Gohr } 911e6380ba3SAndreas Gohr 912e6380ba3SAndreas Gohr $newList = ['list', ', ', $newList]; 913e6380ba3SAndreas Gohr 914e6380ba3SAndreas Gohr switch ($values[0][0]) { 915e6380ba3SAndreas Gohr case 'arg': 916e6380ba3SAndreas Gohr $newArg = ['arg', $values[0][1], $newList]; 917e6380ba3SAndreas Gohr break; 918e6380ba3SAndreas Gohr case 'lit': 919e6380ba3SAndreas Gohr $newArg = ['lit', $newList]; 920e6380ba3SAndreas Gohr break; 921e6380ba3SAndreas Gohr } 922e6380ba3SAndreas Gohr } elseif ($values) { // 1 item 923e6380ba3SAndreas Gohr $newArg = $values[0]; 924e6380ba3SAndreas Gohr } 925e6380ba3SAndreas Gohr 926e6380ba3SAndreas Gohr if ($newArg) { 927e6380ba3SAndreas Gohr $values = [$newArg]; 928e6380ba3SAndreas Gohr } 929e6380ba3SAndreas Gohr } else { 930e6380ba3SAndreas Gohr break; 931e6380ba3SAndreas Gohr } 932e6380ba3SAndreas Gohr } 933e6380ba3SAndreas Gohr } 934e6380ba3SAndreas Gohr 935e6380ba3SAndreas Gohr if (!$this->literal(')')) { 936e6380ba3SAndreas Gohr $this->seek($s); 937e6380ba3SAndreas Gohr return false; 938e6380ba3SAndreas Gohr } 939e6380ba3SAndreas Gohr 940e6380ba3SAndreas Gohr $args = $values; 941e6380ba3SAndreas Gohr 942e6380ba3SAndreas Gohr return true; 943e6380ba3SAndreas Gohr } 944e6380ba3SAndreas Gohr 945e6380ba3SAndreas Gohr // consume a list of tags 946e6380ba3SAndreas Gohr // this accepts a hanging delimiter 947e6380ba3SAndreas Gohr protected function tags(&$tags, $simple = false, $delim = ',') 948e6380ba3SAndreas Gohr { 949e6380ba3SAndreas Gohr $tags = []; 950e6380ba3SAndreas Gohr while ($this->tag($tt, $simple)) { 951e6380ba3SAndreas Gohr $tags[] = $tt; 952e6380ba3SAndreas Gohr if (!$this->literal($delim)) break; 953e6380ba3SAndreas Gohr } 954e6380ba3SAndreas Gohr if (count($tags) == 0) return false; 955e6380ba3SAndreas Gohr 956e6380ba3SAndreas Gohr return true; 957e6380ba3SAndreas Gohr } 958e6380ba3SAndreas Gohr 959e6380ba3SAndreas Gohr // list of tags of specifying mixin path 960e6380ba3SAndreas Gohr // optionally separated by > (lazy, accepts extra >) 961e6380ba3SAndreas Gohr protected function mixinTags(&$tags) 962e6380ba3SAndreas Gohr { 963e6380ba3SAndreas Gohr $tags = []; 964e6380ba3SAndreas Gohr while ($this->tag($tt, true)) { 965e6380ba3SAndreas Gohr $tags[] = $tt; 966e6380ba3SAndreas Gohr $this->literal('>'); 967e6380ba3SAndreas Gohr } 968e6380ba3SAndreas Gohr 969e6380ba3SAndreas Gohr if (count($tags) == 0) return false; 970e6380ba3SAndreas Gohr 971e6380ba3SAndreas Gohr return true; 972e6380ba3SAndreas Gohr } 973e6380ba3SAndreas Gohr 974e6380ba3SAndreas Gohr // a bracketed value (contained within in a tag definition) 975e6380ba3SAndreas Gohr protected function tagBracket(&$parts, &$hasExpression) 976e6380ba3SAndreas Gohr { 977e6380ba3SAndreas Gohr // speed shortcut 978e6380ba3SAndreas Gohr if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != '[') { 979e6380ba3SAndreas Gohr return false; 980e6380ba3SAndreas Gohr } 981e6380ba3SAndreas Gohr 982e6380ba3SAndreas Gohr $s = $this->seek(); 983e6380ba3SAndreas Gohr 984e6380ba3SAndreas Gohr $hasInterpolation = false; 985e6380ba3SAndreas Gohr 986e6380ba3SAndreas Gohr if ($this->literal('[', false)) { 987e6380ba3SAndreas Gohr $attrParts = ['[']; 988e6380ba3SAndreas Gohr // keyword, string, operator 989e6380ba3SAndreas Gohr while (true) { 990e6380ba3SAndreas Gohr if ($this->literal(']', false)) { 991e6380ba3SAndreas Gohr $this->count--; 992e6380ba3SAndreas Gohr break; // get out early 993e6380ba3SAndreas Gohr } 994e6380ba3SAndreas Gohr 995e6380ba3SAndreas Gohr if ($this->match('\s+', $m)) { 996e6380ba3SAndreas Gohr $attrParts[] = ' '; 997e6380ba3SAndreas Gohr continue; 998e6380ba3SAndreas Gohr } 999e6380ba3SAndreas Gohr if ($this->stringValue($str)) { 1000e6380ba3SAndreas Gohr // escape parent selector, (yuck) 1001e6380ba3SAndreas Gohr foreach ($str[2] as &$chunk) { 1002e6380ba3SAndreas Gohr if (is_string($chunk)) { 1003e6380ba3SAndreas Gohr $chunk = str_replace(Constants::PARENT_SELECTOR, "$&$", $chunk); 1004e6380ba3SAndreas Gohr } 1005e6380ba3SAndreas Gohr } 1006e6380ba3SAndreas Gohr 1007e6380ba3SAndreas Gohr $attrParts[] = $str; 1008e6380ba3SAndreas Gohr $hasInterpolation = true; 1009e6380ba3SAndreas Gohr continue; 1010e6380ba3SAndreas Gohr } 1011e6380ba3SAndreas Gohr 1012e6380ba3SAndreas Gohr if ($this->keyword($word)) { 1013e6380ba3SAndreas Gohr $attrParts[] = $word; 1014e6380ba3SAndreas Gohr continue; 1015e6380ba3SAndreas Gohr } 1016e6380ba3SAndreas Gohr 1017e6380ba3SAndreas Gohr if ($this->interpolation($inter)) { 1018e6380ba3SAndreas Gohr $attrParts[] = $inter; 1019e6380ba3SAndreas Gohr $hasInterpolation = true; 1020e6380ba3SAndreas Gohr continue; 1021e6380ba3SAndreas Gohr } 1022e6380ba3SAndreas Gohr 1023e6380ba3SAndreas Gohr // operator, handles attr namespace too 1024e6380ba3SAndreas Gohr if ($this->match('[|-~\$\*\^=]+', $m)) { 1025e6380ba3SAndreas Gohr $attrParts[] = $m[0]; 1026e6380ba3SAndreas Gohr continue; 1027e6380ba3SAndreas Gohr } 1028e6380ba3SAndreas Gohr 1029e6380ba3SAndreas Gohr break; 1030e6380ba3SAndreas Gohr } 1031e6380ba3SAndreas Gohr 1032e6380ba3SAndreas Gohr if ($this->literal(']', false)) { 1033e6380ba3SAndreas Gohr $attrParts[] = ']'; 1034e6380ba3SAndreas Gohr foreach ($attrParts as $part) { 1035e6380ba3SAndreas Gohr $parts[] = $part; 1036e6380ba3SAndreas Gohr } 1037e6380ba3SAndreas Gohr $hasExpression = $hasExpression || $hasInterpolation; 1038e6380ba3SAndreas Gohr return true; 1039e6380ba3SAndreas Gohr } 1040e6380ba3SAndreas Gohr $this->seek($s); 1041e6380ba3SAndreas Gohr } 1042e6380ba3SAndreas Gohr 1043e6380ba3SAndreas Gohr $this->seek($s); 1044e6380ba3SAndreas Gohr return false; 1045e6380ba3SAndreas Gohr } 1046e6380ba3SAndreas Gohr 1047e6380ba3SAndreas Gohr // a space separated list of selectors 1048e6380ba3SAndreas Gohr protected function tag(&$tag, $simple = false) 1049e6380ba3SAndreas Gohr { 1050e6380ba3SAndreas Gohr if ($simple) 1051e6380ba3SAndreas Gohr $chars = '^@,:;{}\][>\(\) "\''; 1052e6380ba3SAndreas Gohr else $chars = '^@,;{}["\''; 1053e6380ba3SAndreas Gohr 1054e6380ba3SAndreas Gohr $s = $this->seek(); 1055e6380ba3SAndreas Gohr 1056e6380ba3SAndreas Gohr $hasExpression = false; 1057e6380ba3SAndreas Gohr $parts = []; 1058e6380ba3SAndreas Gohr while ($this->tagBracket($parts, $hasExpression)) { 1059e6380ba3SAndreas Gohr // no-op 1060e6380ba3SAndreas Gohr } 1061e6380ba3SAndreas Gohr 1062e6380ba3SAndreas Gohr $oldWhite = $this->eatWhiteDefault; 1063e6380ba3SAndreas Gohr $this->eatWhiteDefault = false; 1064e6380ba3SAndreas Gohr 1065e6380ba3SAndreas Gohr while (true) { 1066e6380ba3SAndreas Gohr if ($this->match('([' . $chars . '0-9][' . $chars . ']*)', $m)) { 1067e6380ba3SAndreas Gohr $parts[] = $m[1]; 1068e6380ba3SAndreas Gohr if ($simple) break; 1069e6380ba3SAndreas Gohr 1070e6380ba3SAndreas Gohr while ($this->tagBracket($parts, $hasExpression)) { 1071e6380ba3SAndreas Gohr // no-op 1072e6380ba3SAndreas Gohr } 1073e6380ba3SAndreas Gohr continue; 1074e6380ba3SAndreas Gohr } 1075e6380ba3SAndreas Gohr 1076e6380ba3SAndreas Gohr if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == '@') { 1077e6380ba3SAndreas Gohr if ($this->interpolation($interp)) { 1078e6380ba3SAndreas Gohr $hasExpression = true; 1079e6380ba3SAndreas Gohr $interp[2] = true; // don't unescape 1080e6380ba3SAndreas Gohr $parts[] = $interp; 1081e6380ba3SAndreas Gohr continue; 1082e6380ba3SAndreas Gohr } 1083e6380ba3SAndreas Gohr 1084e6380ba3SAndreas Gohr if ($this->literal('@')) { 1085e6380ba3SAndreas Gohr $parts[] = '@'; 1086e6380ba3SAndreas Gohr continue; 1087e6380ba3SAndreas Gohr } 1088e6380ba3SAndreas Gohr } 1089e6380ba3SAndreas Gohr 1090e6380ba3SAndreas Gohr if ($this->unit($unit)) { // for keyframes 1091e6380ba3SAndreas Gohr $parts[] = $unit[1]; 1092e6380ba3SAndreas Gohr $parts[] = $unit[2]; 1093e6380ba3SAndreas Gohr continue; 1094e6380ba3SAndreas Gohr } 1095e6380ba3SAndreas Gohr 1096e6380ba3SAndreas Gohr break; 1097e6380ba3SAndreas Gohr } 1098e6380ba3SAndreas Gohr 1099e6380ba3SAndreas Gohr $this->eatWhiteDefault = $oldWhite; 1100e6380ba3SAndreas Gohr if (!$parts) { 1101e6380ba3SAndreas Gohr $this->seek($s); 1102e6380ba3SAndreas Gohr return false; 1103e6380ba3SAndreas Gohr } 1104e6380ba3SAndreas Gohr 1105e6380ba3SAndreas Gohr if ($hasExpression) { 1106e6380ba3SAndreas Gohr $tag = ['exp', ['string', '', $parts]]; 1107e6380ba3SAndreas Gohr } else { 1108e6380ba3SAndreas Gohr $tag = trim(implode($parts)); 1109e6380ba3SAndreas Gohr } 1110e6380ba3SAndreas Gohr 1111e6380ba3SAndreas Gohr $this->whitespace(); 1112e6380ba3SAndreas Gohr return true; 1113e6380ba3SAndreas Gohr } 1114e6380ba3SAndreas Gohr 1115e6380ba3SAndreas Gohr // a css function 1116e6380ba3SAndreas Gohr protected function func(&$func) 1117e6380ba3SAndreas Gohr { 1118e6380ba3SAndreas Gohr $s = $this->seek(); 1119e6380ba3SAndreas Gohr 1120e6380ba3SAndreas Gohr if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) { 1121e6380ba3SAndreas Gohr $fname = $m[1]; 1122e6380ba3SAndreas Gohr 1123e6380ba3SAndreas Gohr $sPreArgs = $this->seek(); 1124e6380ba3SAndreas Gohr 1125e6380ba3SAndreas Gohr $args = []; 1126e6380ba3SAndreas Gohr while (true) { 1127e6380ba3SAndreas Gohr $ss = $this->seek(); 1128e6380ba3SAndreas Gohr // this ugly nonsense is for ie filter properties 1129e6380ba3SAndreas Gohr if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { 1130e6380ba3SAndreas Gohr $args[] = ['string', '', [$name, '=', $value]]; 1131e6380ba3SAndreas Gohr } else { 1132e6380ba3SAndreas Gohr $this->seek($ss); 1133e6380ba3SAndreas Gohr if ($this->expressionList($value)) { 1134e6380ba3SAndreas Gohr $args[] = $value; 1135e6380ba3SAndreas Gohr } 1136e6380ba3SAndreas Gohr } 1137e6380ba3SAndreas Gohr 1138e6380ba3SAndreas Gohr if (!$this->literal(',')) break; 1139e6380ba3SAndreas Gohr } 1140e6380ba3SAndreas Gohr $args = ['list', ',', $args]; 1141e6380ba3SAndreas Gohr 1142e6380ba3SAndreas Gohr if ($this->literal(')')) { 1143e6380ba3SAndreas Gohr $func = ['function', $fname, $args]; 1144e6380ba3SAndreas Gohr return true; 1145e6380ba3SAndreas Gohr } elseif ($fname == 'url') { 1146e6380ba3SAndreas Gohr // couldn't parse and in url? treat as string 1147e6380ba3SAndreas Gohr $this->seek($sPreArgs); 1148e6380ba3SAndreas Gohr if ($this->openString(')', $string) && $this->literal(')')) { 1149e6380ba3SAndreas Gohr $func = ['function', $fname, $string]; 1150e6380ba3SAndreas Gohr return true; 1151e6380ba3SAndreas Gohr } 1152e6380ba3SAndreas Gohr } 1153e6380ba3SAndreas Gohr } 1154e6380ba3SAndreas Gohr 1155e6380ba3SAndreas Gohr $this->seek($s); 1156e6380ba3SAndreas Gohr return false; 1157e6380ba3SAndreas Gohr } 1158e6380ba3SAndreas Gohr 1159e6380ba3SAndreas Gohr // consume a less variable 1160e6380ba3SAndreas Gohr protected function variable(&$name) 1161e6380ba3SAndreas Gohr { 1162e6380ba3SAndreas Gohr $s = $this->seek(); 1163e6380ba3SAndreas Gohr if ($this->literal(Constants::VPREFIX, false) && 1164e6380ba3SAndreas Gohr ($this->variable($sub) || $this->keyword($name))) { 1165e6380ba3SAndreas Gohr if (!empty($sub)) { 1166e6380ba3SAndreas Gohr $name = ['variable', $sub]; 1167e6380ba3SAndreas Gohr } else { 1168e6380ba3SAndreas Gohr $name = Constants::VPREFIX . $name; 1169e6380ba3SAndreas Gohr } 1170e6380ba3SAndreas Gohr return true; 1171e6380ba3SAndreas Gohr } 1172e6380ba3SAndreas Gohr 1173e6380ba3SAndreas Gohr $name = null; 1174e6380ba3SAndreas Gohr $this->seek($s); 1175e6380ba3SAndreas Gohr return false; 1176e6380ba3SAndreas Gohr } 1177e6380ba3SAndreas Gohr 1178e6380ba3SAndreas Gohr /** 1179e6380ba3SAndreas Gohr * Consume an assignment operator 1180e6380ba3SAndreas Gohr * Can optionally take a name that will be set to the current property name 1181e6380ba3SAndreas Gohr */ 1182e6380ba3SAndreas Gohr protected function assign($name = null) 1183e6380ba3SAndreas Gohr { 1184e6380ba3SAndreas Gohr if ($name) $this->currentProperty = $name; 1185e6380ba3SAndreas Gohr return $this->literal(':') || $this->literal('='); 1186e6380ba3SAndreas Gohr } 1187e6380ba3SAndreas Gohr 1188e6380ba3SAndreas Gohr // consume a keyword 1189e6380ba3SAndreas Gohr protected function keyword(&$word) 1190e6380ba3SAndreas Gohr { 1191e6380ba3SAndreas Gohr if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { 1192e6380ba3SAndreas Gohr $word = $m[1]; 1193e6380ba3SAndreas Gohr return true; 1194e6380ba3SAndreas Gohr } 1195e6380ba3SAndreas Gohr return false; 1196e6380ba3SAndreas Gohr } 1197e6380ba3SAndreas Gohr 1198e6380ba3SAndreas Gohr // consume an end of statement delimiter 1199e6380ba3SAndreas Gohr protected function end() 1200e6380ba3SAndreas Gohr { 1201e6380ba3SAndreas Gohr if ($this->literal(';', false)) { 1202e6380ba3SAndreas Gohr return true; 1203e6380ba3SAndreas Gohr } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { 1204e6380ba3SAndreas Gohr // if there is end of file or a closing block next then we don't need a ; 1205e6380ba3SAndreas Gohr return true; 1206e6380ba3SAndreas Gohr } 1207e6380ba3SAndreas Gohr return false; 1208e6380ba3SAndreas Gohr } 1209e6380ba3SAndreas Gohr 1210e6380ba3SAndreas Gohr protected function guards(&$guards) 1211e6380ba3SAndreas Gohr { 1212e6380ba3SAndreas Gohr $s = $this->seek(); 1213e6380ba3SAndreas Gohr 1214e6380ba3SAndreas Gohr if (!$this->literal('when')) { 1215e6380ba3SAndreas Gohr $this->seek($s); 1216e6380ba3SAndreas Gohr return false; 1217e6380ba3SAndreas Gohr } 1218e6380ba3SAndreas Gohr 1219e6380ba3SAndreas Gohr $guards = []; 1220e6380ba3SAndreas Gohr 1221e6380ba3SAndreas Gohr while ($this->guardGroup($g)) { 1222e6380ba3SAndreas Gohr $guards[] = $g; 1223e6380ba3SAndreas Gohr if (!$this->literal(',')) break; 1224e6380ba3SAndreas Gohr } 1225e6380ba3SAndreas Gohr 1226e6380ba3SAndreas Gohr if (count($guards) == 0) { 1227e6380ba3SAndreas Gohr $guards = null; 1228e6380ba3SAndreas Gohr $this->seek($s); 1229e6380ba3SAndreas Gohr return false; 1230e6380ba3SAndreas Gohr } 1231e6380ba3SAndreas Gohr 1232e6380ba3SAndreas Gohr return true; 1233e6380ba3SAndreas Gohr } 1234e6380ba3SAndreas Gohr 1235e6380ba3SAndreas Gohr // a bunch of guards that are and'd together 1236e6380ba3SAndreas Gohr // TODO rename to guardGroup 1237e6380ba3SAndreas Gohr protected function guardGroup(&$guardGroup) 1238e6380ba3SAndreas Gohr { 1239e6380ba3SAndreas Gohr $s = $this->seek(); 1240e6380ba3SAndreas Gohr $guardGroup = []; 1241e6380ba3SAndreas Gohr while ($this->guard($guard)) { 1242e6380ba3SAndreas Gohr $guardGroup[] = $guard; 1243e6380ba3SAndreas Gohr if (!$this->literal('and')) break; 1244e6380ba3SAndreas Gohr } 1245e6380ba3SAndreas Gohr 1246e6380ba3SAndreas Gohr if (count($guardGroup) == 0) { 1247e6380ba3SAndreas Gohr $guardGroup = null; 1248e6380ba3SAndreas Gohr $this->seek($s); 1249e6380ba3SAndreas Gohr return false; 1250e6380ba3SAndreas Gohr } 1251e6380ba3SAndreas Gohr 1252e6380ba3SAndreas Gohr return true; 1253e6380ba3SAndreas Gohr } 1254e6380ba3SAndreas Gohr 1255e6380ba3SAndreas Gohr protected function guard(&$guard) 1256e6380ba3SAndreas Gohr { 1257e6380ba3SAndreas Gohr $s = $this->seek(); 1258e6380ba3SAndreas Gohr $negate = $this->literal('not'); 1259e6380ba3SAndreas Gohr 1260e6380ba3SAndreas Gohr if ($this->literal('(') && $this->expression($exp) && $this->literal(')')) { 1261e6380ba3SAndreas Gohr $guard = $exp; 1262e6380ba3SAndreas Gohr if ($negate) $guard = ['negate', $guard]; 1263e6380ba3SAndreas Gohr return true; 1264e6380ba3SAndreas Gohr } 1265e6380ba3SAndreas Gohr 1266e6380ba3SAndreas Gohr $this->seek($s); 1267e6380ba3SAndreas Gohr return false; 1268e6380ba3SAndreas Gohr } 1269e6380ba3SAndreas Gohr 1270e6380ba3SAndreas Gohr /* raw parsing functions */ 1271e6380ba3SAndreas Gohr 1272e6380ba3SAndreas Gohr protected function literal($what, $eatWhitespace = null) 1273e6380ba3SAndreas Gohr { 1274e6380ba3SAndreas Gohr if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 1275e6380ba3SAndreas Gohr 1276e6380ba3SAndreas Gohr // shortcut on single letter 1277e6380ba3SAndreas Gohr if (!isset($what[1]) && isset($this->buffer[$this->count])) { 1278e6380ba3SAndreas Gohr if ($this->buffer[$this->count] == $what) { 1279e6380ba3SAndreas Gohr if (!$eatWhitespace) { 1280e6380ba3SAndreas Gohr $this->count++; 1281e6380ba3SAndreas Gohr return true; 1282e6380ba3SAndreas Gohr } 1283e6380ba3SAndreas Gohr // goes below... 1284e6380ba3SAndreas Gohr } else { 1285e6380ba3SAndreas Gohr return false; 1286e6380ba3SAndreas Gohr } 1287e6380ba3SAndreas Gohr } 1288e6380ba3SAndreas Gohr 1289e6380ba3SAndreas Gohr if (!isset(self::$literalCache[$what])) { 1290e6380ba3SAndreas Gohr self::$literalCache[$what] = Util::pregQuote($what); 1291e6380ba3SAndreas Gohr } 1292e6380ba3SAndreas Gohr 1293e6380ba3SAndreas Gohr return $this->match(self::$literalCache[$what], $m, $eatWhitespace); 1294e6380ba3SAndreas Gohr } 1295e6380ba3SAndreas Gohr 1296e6380ba3SAndreas Gohr protected function genericList(&$out, $parseItem, $delim = '', $flatten = true) 1297e6380ba3SAndreas Gohr { 1298e6380ba3SAndreas Gohr $s = $this->seek(); 1299e6380ba3SAndreas Gohr $items = []; 1300e6380ba3SAndreas Gohr $value = null; 1301e6380ba3SAndreas Gohr while ($this->$parseItem($value)) { 1302e6380ba3SAndreas Gohr $items[] = $value; 1303e6380ba3SAndreas Gohr if ($delim) { 1304e6380ba3SAndreas Gohr if (!$this->literal($delim)) break; 1305e6380ba3SAndreas Gohr } 1306e6380ba3SAndreas Gohr } 1307e6380ba3SAndreas Gohr 1308e6380ba3SAndreas Gohr if (count($items) == 0) { 1309e6380ba3SAndreas Gohr $this->seek($s); 1310e6380ba3SAndreas Gohr return false; 1311e6380ba3SAndreas Gohr } 1312e6380ba3SAndreas Gohr 1313e6380ba3SAndreas Gohr if ($flatten && count($items) == 1) { 1314e6380ba3SAndreas Gohr $out = $items[0]; 1315e6380ba3SAndreas Gohr } else { 1316e6380ba3SAndreas Gohr $out = ['list', $delim, $items]; 1317e6380ba3SAndreas Gohr } 1318e6380ba3SAndreas Gohr 1319e6380ba3SAndreas Gohr return true; 1320e6380ba3SAndreas Gohr } 1321e6380ba3SAndreas Gohr 1322e6380ba3SAndreas Gohr 1323e6380ba3SAndreas Gohr // advance counter to next occurrence of $what 1324e6380ba3SAndreas Gohr // $until - don't include $what in advance 1325e6380ba3SAndreas Gohr // $allowNewline, if string, will be used as valid char set 1326e6380ba3SAndreas Gohr protected function to($what, &$out, $until = false, $allowNewline = false) 1327e6380ba3SAndreas Gohr { 1328e6380ba3SAndreas Gohr if (is_string($allowNewline)) { 1329e6380ba3SAndreas Gohr $validChars = $allowNewline; 1330e6380ba3SAndreas Gohr } else { 1331e6380ba3SAndreas Gohr $validChars = $allowNewline ? '.' : "[^\n]"; 1332e6380ba3SAndreas Gohr } 1333e6380ba3SAndreas Gohr if (!$this->match('(' . $validChars . '*?)' . Lessc::preg_quote($what), $m, !$until)) return false; 1334e6380ba3SAndreas Gohr if ($until) $this->count -= strlen($what); // give back $what 1335e6380ba3SAndreas Gohr $out = $m[1]; 1336e6380ba3SAndreas Gohr return true; 1337e6380ba3SAndreas Gohr } 1338e6380ba3SAndreas Gohr 1339e6380ba3SAndreas Gohr // try to match something on head of buffer 1340e6380ba3SAndreas Gohr protected function match($regex, &$out, $eatWhitespace = null) 1341e6380ba3SAndreas Gohr { 1342e6380ba3SAndreas Gohr if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 1343e6380ba3SAndreas Gohr 1344e6380ba3SAndreas Gohr $r = '/' . $regex . ($eatWhitespace && !$this->writeComments ? '\s*' : '') . '/Ais'; 1345e6380ba3SAndreas Gohr if (preg_match($r, $this->buffer, $out, 0, $this->count)) { 1346e6380ba3SAndreas Gohr $this->count += strlen($out[0]); 1347e6380ba3SAndreas Gohr if ($eatWhitespace && $this->writeComments) $this->whitespace(); 1348e6380ba3SAndreas Gohr return true; 1349e6380ba3SAndreas Gohr } 1350e6380ba3SAndreas Gohr return false; 1351e6380ba3SAndreas Gohr } 1352e6380ba3SAndreas Gohr 1353e6380ba3SAndreas Gohr // match some whitespace 1354e6380ba3SAndreas Gohr protected function whitespace() 1355e6380ba3SAndreas Gohr { 1356e6380ba3SAndreas Gohr if ($this->writeComments) { 1357e6380ba3SAndreas Gohr $gotWhite = false; 1358e6380ba3SAndreas Gohr while (preg_match(self::$whitePattern, $this->buffer, $m, 0, $this->count)) { 1359e6380ba3SAndreas Gohr if (isset($m[1]) && empty($this->seenComments[$this->count])) { 1360e6380ba3SAndreas Gohr $this->append(['comment', $m[1]]); 1361e6380ba3SAndreas Gohr $this->seenComments[$this->count] = true; 1362e6380ba3SAndreas Gohr } 1363e6380ba3SAndreas Gohr $this->count += strlen($m[0]); 1364e6380ba3SAndreas Gohr $gotWhite = true; 1365e6380ba3SAndreas Gohr } 1366e6380ba3SAndreas Gohr return $gotWhite; 1367e6380ba3SAndreas Gohr } else { 1368e6380ba3SAndreas Gohr $this->match('', $m); 1369e6380ba3SAndreas Gohr return strlen($m[0]) > 0; 1370e6380ba3SAndreas Gohr } 1371e6380ba3SAndreas Gohr } 1372e6380ba3SAndreas Gohr 1373e6380ba3SAndreas Gohr // match something without consuming it 1374e6380ba3SAndreas Gohr protected function peek($regex, &$out = null, $from = null) 1375e6380ba3SAndreas Gohr { 1376e6380ba3SAndreas Gohr if (is_null($from)) $from = $this->count; 1377e6380ba3SAndreas Gohr $r = '/' . $regex . '/Ais'; 1378e6380ba3SAndreas Gohr return preg_match($r, $this->buffer, $out, 0, $from); 1379e6380ba3SAndreas Gohr } 1380e6380ba3SAndreas Gohr 1381e6380ba3SAndreas Gohr // seek to a spot in the buffer or return where we are on no argument 1382e6380ba3SAndreas Gohr protected function seek($where = null) 1383e6380ba3SAndreas Gohr { 1384e6380ba3SAndreas Gohr if ($where === null) return $this->count; 1385e6380ba3SAndreas Gohr else $this->count = $where; 1386e6380ba3SAndreas Gohr return true; 1387e6380ba3SAndreas Gohr } 1388e6380ba3SAndreas Gohr 1389e6380ba3SAndreas Gohr /* misc functions */ 1390e6380ba3SAndreas Gohr 1391e6380ba3SAndreas Gohr /** 1392e6380ba3SAndreas Gohr * Throw a parser exception 1393e6380ba3SAndreas Gohr * 1394e6380ba3SAndreas Gohr * This function tries to use the current parsing context to provide 1395e6380ba3SAndreas Gohr * additional info on where/why the error occurred. 1396e6380ba3SAndreas Gohr * 1397e6380ba3SAndreas Gohr * @param string $msg The error message to throw 1398e6380ba3SAndreas Gohr * @param int|null $count A line number counter to use instead of the current count 1399e6380ba3SAndreas Gohr * @param \Throwable|null $previous A previous exception to chain 1400e6380ba3SAndreas Gohr * @throws ParserException 1401e6380ba3SAndreas Gohr */ 1402*a646a37bSAndreas Gohr public function throwError(string $msg = 'parse error', ?int $count = null, ?\Throwable $previous = null) 1403e6380ba3SAndreas Gohr { 1404e6380ba3SAndreas Gohr $count = is_null($count) ? $this->count : $count; 1405e6380ba3SAndreas Gohr 1406e6380ba3SAndreas Gohr $line = $this->line + substr_count(substr($this->buffer, 0, $count), "\n"); 1407e6380ba3SAndreas Gohr 1408e6380ba3SAndreas Gohr if ($this->peek("(.*?)(\n|$)", $m, $count)) { 1409e6380ba3SAndreas Gohr $culprit = $m[1]; 1410e6380ba3SAndreas Gohr } 1411e6380ba3SAndreas Gohr 1412e6380ba3SAndreas Gohr throw new ParserException( 1413e6380ba3SAndreas Gohr $msg, 1414e6380ba3SAndreas Gohr $culprit, 1415e6380ba3SAndreas Gohr $this->sourceName, 1416e6380ba3SAndreas Gohr $line, 1417e6380ba3SAndreas Gohr $previous 1418e6380ba3SAndreas Gohr ); 1419e6380ba3SAndreas Gohr } 1420e6380ba3SAndreas Gohr 1421e6380ba3SAndreas Gohr protected function pushBlock($selectors = null, $type = null) 1422e6380ba3SAndreas Gohr { 1423e6380ba3SAndreas Gohr $b = new stdClass(); 1424e6380ba3SAndreas Gohr $b->parent = $this->env; 1425e6380ba3SAndreas Gohr 1426e6380ba3SAndreas Gohr $b->type = $type; 1427e6380ba3SAndreas Gohr $b->id = self::$nextBlockId++; 1428e6380ba3SAndreas Gohr 1429e6380ba3SAndreas Gohr $b->isVararg = false; // TODO: kill me from here 1430e6380ba3SAndreas Gohr $b->tags = $selectors; 1431e6380ba3SAndreas Gohr 1432e6380ba3SAndreas Gohr $b->props = []; 1433e6380ba3SAndreas Gohr $b->children = []; 1434e6380ba3SAndreas Gohr 1435e6380ba3SAndreas Gohr // add a reference to the parser so 1436e6380ba3SAndreas Gohr // we can access the parser to throw errors 1437e6380ba3SAndreas Gohr // or retrieve the sourceName of this block. 1438e6380ba3SAndreas Gohr $b->parser = $this; 1439e6380ba3SAndreas Gohr 1440e6380ba3SAndreas Gohr // so we know the position of this block 1441e6380ba3SAndreas Gohr $b->count = $this->count; 1442e6380ba3SAndreas Gohr 1443e6380ba3SAndreas Gohr $this->env = $b; 1444e6380ba3SAndreas Gohr return $b; 1445e6380ba3SAndreas Gohr } 1446e6380ba3SAndreas Gohr 1447e6380ba3SAndreas Gohr // push a block that doesn't multiply tags 1448e6380ba3SAndreas Gohr protected function pushSpecialBlock($type) 1449e6380ba3SAndreas Gohr { 1450e6380ba3SAndreas Gohr return $this->pushBlock(null, $type); 1451e6380ba3SAndreas Gohr } 1452e6380ba3SAndreas Gohr 1453e6380ba3SAndreas Gohr // append a property to the current block 1454e6380ba3SAndreas Gohr protected function append($prop, $pos = null) 1455e6380ba3SAndreas Gohr { 1456e6380ba3SAndreas Gohr if ($pos !== null) $prop[-1] = $pos; 1457e6380ba3SAndreas Gohr $this->env->props[] = $prop; 1458e6380ba3SAndreas Gohr } 1459e6380ba3SAndreas Gohr 1460e6380ba3SAndreas Gohr // pop something off the stack 1461e6380ba3SAndreas Gohr protected function pop() 1462e6380ba3SAndreas Gohr { 1463e6380ba3SAndreas Gohr $old = $this->env; 1464e6380ba3SAndreas Gohr $this->env = $this->env->parent; 1465e6380ba3SAndreas Gohr return $old; 1466e6380ba3SAndreas Gohr } 1467e6380ba3SAndreas Gohr 1468e6380ba3SAndreas Gohr // remove comments from $text 1469e6380ba3SAndreas Gohr // todo: make it work for all functions, not just url 1470e6380ba3SAndreas Gohr protected function removeComments($text) 1471e6380ba3SAndreas Gohr { 1472e6380ba3SAndreas Gohr $look = ['url(', '//', '/*', '"', "'"]; 1473e6380ba3SAndreas Gohr 1474e6380ba3SAndreas Gohr $out = ''; 1475e6380ba3SAndreas Gohr $min = null; 1476e6380ba3SAndreas Gohr while (true) { 1477e6380ba3SAndreas Gohr // find the next item 1478e6380ba3SAndreas Gohr foreach ($look as $token) { 1479e6380ba3SAndreas Gohr $pos = strpos($text, $token); 1480e6380ba3SAndreas Gohr if ($pos !== false) { 1481e6380ba3SAndreas Gohr if (!isset($min) || $pos < $min[1]) $min = [$token, $pos]; 1482e6380ba3SAndreas Gohr } 1483e6380ba3SAndreas Gohr } 1484e6380ba3SAndreas Gohr 1485e6380ba3SAndreas Gohr if (is_null($min)) break; 1486e6380ba3SAndreas Gohr 1487e6380ba3SAndreas Gohr $count = $min[1]; 1488e6380ba3SAndreas Gohr $skip = 0; 1489e6380ba3SAndreas Gohr $newlines = 0; 1490e6380ba3SAndreas Gohr switch ($min[0]) { 1491e6380ba3SAndreas Gohr case 'url(': 1492e6380ba3SAndreas Gohr if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) 1493e6380ba3SAndreas Gohr $count += strlen($m[0]) - strlen($min[0]); 1494e6380ba3SAndreas Gohr break; 1495e6380ba3SAndreas Gohr case '"': 1496e6380ba3SAndreas Gohr case "'": 1497e6380ba3SAndreas Gohr if (preg_match('/' . $min[0] . '.*?(?<!\\\\)' . $min[0] . '/', $text, $m, 0, $count)) 1498e6380ba3SAndreas Gohr $count += strlen($m[0]) - 1; 1499e6380ba3SAndreas Gohr break; 1500e6380ba3SAndreas Gohr case '//': 1501e6380ba3SAndreas Gohr $skip = strpos($text, "\n", $count); 1502e6380ba3SAndreas Gohr if ($skip === false) $skip = strlen($text) - $count; 1503e6380ba3SAndreas Gohr else $skip -= $count; 1504e6380ba3SAndreas Gohr break; 1505e6380ba3SAndreas Gohr case '/*': 1506e6380ba3SAndreas Gohr if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) { 1507e6380ba3SAndreas Gohr $skip = strlen($m[0]); 1508e6380ba3SAndreas Gohr $newlines = substr_count($m[0], "\n"); 1509e6380ba3SAndreas Gohr } 1510e6380ba3SAndreas Gohr break; 1511e6380ba3SAndreas Gohr } 1512e6380ba3SAndreas Gohr 1513e6380ba3SAndreas Gohr if ($skip == 0) $count += strlen($min[0]); 1514e6380ba3SAndreas Gohr 1515e6380ba3SAndreas Gohr $out .= substr($text, 0, $count) . str_repeat("\n", $newlines); 1516e6380ba3SAndreas Gohr $text = substr($text, $count + $skip); 1517e6380ba3SAndreas Gohr 1518e6380ba3SAndreas Gohr $min = null; 1519e6380ba3SAndreas Gohr } 1520e6380ba3SAndreas Gohr 1521e6380ba3SAndreas Gohr return $out . $text; 1522e6380ba3SAndreas Gohr } 1523e6380ba3SAndreas Gohr} 1524