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