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\Functions\AbstractFunctionCollection; 16*e6380ba3SAndreas Gohruse LesserPHP\Utils\Color; 17*e6380ba3SAndreas Gohruse LesserPHP\Utils\Util; 18*e6380ba3SAndreas Gohruse stdClass; 19*e6380ba3SAndreas Gohr 20*e6380ba3SAndreas Gohr/** 21*e6380ba3SAndreas Gohr * The LESS compiler and parser. 22*e6380ba3SAndreas Gohr * 23*e6380ba3SAndreas Gohr * Converting LESS to CSS is a three stage process. The incoming file is parsed 24*e6380ba3SAndreas Gohr * by `Parser` into a syntax tree, then it is compiled into another tree 25*e6380ba3SAndreas Gohr * representing the CSS structure by `Lessc`. The CSS tree is fed into a 26*e6380ba3SAndreas Gohr * formatter, which then outputs CSS as a string. 27*e6380ba3SAndreas Gohr * 28*e6380ba3SAndreas Gohr * During the first compile, all values are *reduced*, which means that their 29*e6380ba3SAndreas Gohr * types are brought to the lowest form before being dumped as strings. This 30*e6380ba3SAndreas Gohr * handles math equations, variable dereferences, and the like. 31*e6380ba3SAndreas Gohr * 32*e6380ba3SAndreas Gohr * The `compile` function of `Lessc` is the entry point. 33*e6380ba3SAndreas Gohr * 34*e6380ba3SAndreas Gohr * In summary: 35*e6380ba3SAndreas Gohr * 36*e6380ba3SAndreas Gohr * The `Lessc` class creates an instance of the parser, feeds it LESS code, 37*e6380ba3SAndreas Gohr * then transforms the resulting tree to a CSS tree. This class also holds the 38*e6380ba3SAndreas Gohr * evaluation context, such as all available mixins and variables at any given 39*e6380ba3SAndreas Gohr * time. 40*e6380ba3SAndreas Gohr * 41*e6380ba3SAndreas Gohr * The `Parser` class is only concerned with parsing its input. 42*e6380ba3SAndreas Gohr * 43*e6380ba3SAndreas Gohr * The `Formatter` takes a CSS tree, and dumps it to a formatted string, 44*e6380ba3SAndreas Gohr * handling things like indentation. 45*e6380ba3SAndreas Gohr */ 46*e6380ba3SAndreas Gohrclass Lessc 47*e6380ba3SAndreas Gohr{ 48*e6380ba3SAndreas Gohr /** @var string A list of directories used to search for imported files */ 49*e6380ba3SAndreas Gohr protected $importDir = []; 50*e6380ba3SAndreas Gohr 51*e6380ba3SAndreas Gohr /** @var bool Should imports be disabled? */ 52*e6380ba3SAndreas Gohr protected bool $importDisabled = false; 53*e6380ba3SAndreas Gohr 54*e6380ba3SAndreas Gohr /** @var null|int Round numbers to this precision FIXME currently not settable */ 55*e6380ba3SAndreas Gohr protected ?int $numberPrecision = null; 56*e6380ba3SAndreas Gohr 57*e6380ba3SAndreas Gohr /** @var bool Should comments be preserved in the output? */ 58*e6380ba3SAndreas Gohr protected bool $preserveComments = false; 59*e6380ba3SAndreas Gohr 60*e6380ba3SAndreas Gohr /** @var array List of all functions registered to be available in LESS */ 61*e6380ba3SAndreas Gohr protected array $libFunctions = []; 62*e6380ba3SAndreas Gohr 63*e6380ba3SAndreas Gohr /** @var array List of all registered variables */ 64*e6380ba3SAndreas Gohr protected array $registeredVars = []; 65*e6380ba3SAndreas Gohr 66*e6380ba3SAndreas Gohr /** @var FormatterClassic|null The formatter for the output */ 67*e6380ba3SAndreas Gohr protected ?FormatterClassic $formatter = null; 68*e6380ba3SAndreas Gohr 69*e6380ba3SAndreas Gohr /** @var stdClass|null Environment FIXME should probably be its own proper class */ 70*e6380ba3SAndreas Gohr protected ?stdClass $env = null; 71*e6380ba3SAndreas Gohr 72*e6380ba3SAndreas Gohr /** @var stdClass|null The currently parsed block FIXME should probably be its own proper class */ 73*e6380ba3SAndreas Gohr protected ?stdClass $scope; 74*e6380ba3SAndreas Gohr 75*e6380ba3SAndreas Gohr /** @var array [file => mtime] list of all files that have been parsed, to avoid circular imports */ 76*e6380ba3SAndreas Gohr protected array $allParsedFiles = []; 77*e6380ba3SAndreas Gohr 78*e6380ba3SAndreas Gohr /** @var Parser|null The currently used Parser instance. Used when creating error messages */ 79*e6380ba3SAndreas Gohr protected ?Parser $sourceParser = null; 80*e6380ba3SAndreas Gohr 81*e6380ba3SAndreas Gohr /** @var int The position in the current parsing step (in $sourceParser) */ 82*e6380ba3SAndreas Gohr protected int $sourceLoc = -1; 83*e6380ba3SAndreas Gohr 84*e6380ba3SAndreas Gohr /** @var int counter to uniquely identify imports */ 85*e6380ba3SAndreas Gohr protected static int $nextImportId = 0; 86*e6380ba3SAndreas Gohr 87*e6380ba3SAndreas Gohr // region public API 88*e6380ba3SAndreas Gohr 89*e6380ba3SAndreas Gohr /** 90*e6380ba3SAndreas Gohr * Initialize the LESS Parser 91*e6380ba3SAndreas Gohr */ 92*e6380ba3SAndreas Gohr public function __construct() 93*e6380ba3SAndreas Gohr { 94*e6380ba3SAndreas Gohr $this->registerLibraryFunctions(); 95*e6380ba3SAndreas Gohr } 96*e6380ba3SAndreas Gohr 97*e6380ba3SAndreas Gohr /** 98*e6380ba3SAndreas Gohr * Compile the given LESS string into CSS 99*e6380ba3SAndreas Gohr * 100*e6380ba3SAndreas Gohr * @param string $string LESS code 101*e6380ba3SAndreas Gohr * @param string|null $name optional filename to show in errors 102*e6380ba3SAndreas Gohr * @throws Exception 103*e6380ba3SAndreas Gohr * @throws ParserException 104*e6380ba3SAndreas Gohr */ 105*e6380ba3SAndreas Gohr public function compile(string $string, ?string $name = null): string 106*e6380ba3SAndreas Gohr { 107*e6380ba3SAndreas Gohr $locale = setlocale(LC_NUMERIC, 0); 108*e6380ba3SAndreas Gohr setlocale(LC_NUMERIC, 'C'); 109*e6380ba3SAndreas Gohr 110*e6380ba3SAndreas Gohr $parser = $this->makeParser($name); 111*e6380ba3SAndreas Gohr $root = $parser->parse($string); 112*e6380ba3SAndreas Gohr 113*e6380ba3SAndreas Gohr $this->env = null; 114*e6380ba3SAndreas Gohr $this->scope = null; 115*e6380ba3SAndreas Gohr $this->allParsedFiles = []; 116*e6380ba3SAndreas Gohr 117*e6380ba3SAndreas Gohr if ($this->formatter === null) $this->setFormatter(); 118*e6380ba3SAndreas Gohr 119*e6380ba3SAndreas Gohr if (!empty($this->registeredVars)) { 120*e6380ba3SAndreas Gohr $this->injectVariables($this->registeredVars); 121*e6380ba3SAndreas Gohr } 122*e6380ba3SAndreas Gohr 123*e6380ba3SAndreas Gohr $this->sourceParser = $parser; // used for error messages 124*e6380ba3SAndreas Gohr 125*e6380ba3SAndreas Gohr try { 126*e6380ba3SAndreas Gohr $this->compileBlock($root); 127*e6380ba3SAndreas Gohr } catch (Exception $e) { 128*e6380ba3SAndreas Gohr setlocale(LC_NUMERIC, $locale); 129*e6380ba3SAndreas Gohr $position = $this->sourceLoc !== -1 ? $this->sourceLoc : $root->count; 130*e6380ba3SAndreas Gohr $this->sourceParser->throwError($e->getMessage(), $position, $e); 131*e6380ba3SAndreas Gohr } 132*e6380ba3SAndreas Gohr 133*e6380ba3SAndreas Gohr ob_start(); 134*e6380ba3SAndreas Gohr $this->formatter->block($this->scope); 135*e6380ba3SAndreas Gohr $out = ob_get_clean(); 136*e6380ba3SAndreas Gohr setlocale(LC_NUMERIC, $locale); 137*e6380ba3SAndreas Gohr return $out; 138*e6380ba3SAndreas Gohr } 139*e6380ba3SAndreas Gohr 140*e6380ba3SAndreas Gohr /** 141*e6380ba3SAndreas Gohr * Parse the given File and return the compiled CSS. 142*e6380ba3SAndreas Gohr * 143*e6380ba3SAndreas Gohr * If an output file is specified, the compiled CSS will be written to that file. 144*e6380ba3SAndreas Gohr * 145*e6380ba3SAndreas Gohr * @param string $fname LESS file 146*e6380ba3SAndreas Gohr * @param string|null $outFname optional output file 147*e6380ba3SAndreas Gohr * @return int|string number of bytes written to file, or CSS if no output file 148*e6380ba3SAndreas Gohr * @throws Exception 149*e6380ba3SAndreas Gohr * @throws ParserException 150*e6380ba3SAndreas Gohr */ 151*e6380ba3SAndreas Gohr public function compileFile(string $fname, ?string $outFname = null) 152*e6380ba3SAndreas Gohr { 153*e6380ba3SAndreas Gohr if (!is_readable($fname)) { 154*e6380ba3SAndreas Gohr throw new Exception('load error: failed to find ' . $fname); 155*e6380ba3SAndreas Gohr } 156*e6380ba3SAndreas Gohr 157*e6380ba3SAndreas Gohr $pi = pathinfo($fname); 158*e6380ba3SAndreas Gohr 159*e6380ba3SAndreas Gohr $oldImport = $this->importDir; 160*e6380ba3SAndreas Gohr 161*e6380ba3SAndreas Gohr $this->importDir = (array)$this->importDir; 162*e6380ba3SAndreas Gohr $this->importDir[] = $pi['dirname'] . '/'; 163*e6380ba3SAndreas Gohr 164*e6380ba3SAndreas Gohr $this->addParsedFile($fname); 165*e6380ba3SAndreas Gohr 166*e6380ba3SAndreas Gohr $out = $this->compile(file_get_contents($fname), $fname); 167*e6380ba3SAndreas Gohr 168*e6380ba3SAndreas Gohr $this->importDir = $oldImport; 169*e6380ba3SAndreas Gohr 170*e6380ba3SAndreas Gohr if ($outFname !== null) { 171*e6380ba3SAndreas Gohr return file_put_contents($outFname, $out); 172*e6380ba3SAndreas Gohr } 173*e6380ba3SAndreas Gohr 174*e6380ba3SAndreas Gohr return $out; 175*e6380ba3SAndreas Gohr } 176*e6380ba3SAndreas Gohr 177*e6380ba3SAndreas Gohr // endregion 178*e6380ba3SAndreas Gohr 179*e6380ba3SAndreas Gohr // region configuration API 180*e6380ba3SAndreas Gohr 181*e6380ba3SAndreas Gohr /** 182*e6380ba3SAndreas Gohr * Should comments be preserved in the output? 183*e6380ba3SAndreas Gohr * 184*e6380ba3SAndreas Gohr * Default is false 185*e6380ba3SAndreas Gohr * 186*e6380ba3SAndreas Gohr * @param bool $preserve 187*e6380ba3SAndreas Gohr */ 188*e6380ba3SAndreas Gohr public function setPreserveComments(bool $preserve): void 189*e6380ba3SAndreas Gohr { 190*e6380ba3SAndreas Gohr $this->preserveComments = $preserve; 191*e6380ba3SAndreas Gohr } 192*e6380ba3SAndreas Gohr 193*e6380ba3SAndreas Gohr /** 194*e6380ba3SAndreas Gohr * Register a custom function to be available in LESS 195*e6380ba3SAndreas Gohr * 196*e6380ba3SAndreas Gohr * @param string $name name of function 197*e6380ba3SAndreas Gohr * @param callable $func callback 198*e6380ba3SAndreas Gohr */ 199*e6380ba3SAndreas Gohr public function registerFunction(string $name, callable $func): void 200*e6380ba3SAndreas Gohr { 201*e6380ba3SAndreas Gohr $this->libFunctions[$name] = $func; 202*e6380ba3SAndreas Gohr } 203*e6380ba3SAndreas Gohr 204*e6380ba3SAndreas Gohr /** 205*e6380ba3SAndreas Gohr * Remove a function from being available in LESS 206*e6380ba3SAndreas Gohr * 207*e6380ba3SAndreas Gohr * @param string $name The name of the function to unregister 208*e6380ba3SAndreas Gohr */ 209*e6380ba3SAndreas Gohr public function unregisterFunction(string $name): void 210*e6380ba3SAndreas Gohr { 211*e6380ba3SAndreas Gohr if (isset($this->libFunctions[$name])) { 212*e6380ba3SAndreas Gohr unset($this->libFunctions[$name]); 213*e6380ba3SAndreas Gohr } 214*e6380ba3SAndreas Gohr } 215*e6380ba3SAndreas Gohr 216*e6380ba3SAndreas Gohr /** 217*e6380ba3SAndreas Gohr * Add additional variables to the parser 218*e6380ba3SAndreas Gohr * 219*e6380ba3SAndreas Gohr * Given variables are merged with any already set variables 220*e6380ba3SAndreas Gohr * 221*e6380ba3SAndreas Gohr * @param array $variables [name => value, ...] 222*e6380ba3SAndreas Gohr */ 223*e6380ba3SAndreas Gohr public function setVariables($variables): void 224*e6380ba3SAndreas Gohr { 225*e6380ba3SAndreas Gohr $this->registeredVars = array_merge($this->registeredVars, $variables); 226*e6380ba3SAndreas Gohr } 227*e6380ba3SAndreas Gohr 228*e6380ba3SAndreas Gohr /** 229*e6380ba3SAndreas Gohr * Get the currently set variables 230*e6380ba3SAndreas Gohr * 231*e6380ba3SAndreas Gohr * @return array [name => value, ...] 232*e6380ba3SAndreas Gohr */ 233*e6380ba3SAndreas Gohr public function getVariables(): array 234*e6380ba3SAndreas Gohr { 235*e6380ba3SAndreas Gohr return $this->registeredVars; 236*e6380ba3SAndreas Gohr } 237*e6380ba3SAndreas Gohr 238*e6380ba3SAndreas Gohr /** 239*e6380ba3SAndreas Gohr * Remove a currently set variable 240*e6380ba3SAndreas Gohr * 241*e6380ba3SAndreas Gohr * @param string $name 242*e6380ba3SAndreas Gohr */ 243*e6380ba3SAndreas Gohr public function unsetVariable(string $name): void 244*e6380ba3SAndreas Gohr { 245*e6380ba3SAndreas Gohr if (isset($this->registeredVars[$name])) { 246*e6380ba3SAndreas Gohr unset($this->registeredVars[$name]); 247*e6380ba3SAndreas Gohr } 248*e6380ba3SAndreas Gohr } 249*e6380ba3SAndreas Gohr 250*e6380ba3SAndreas Gohr /** 251*e6380ba3SAndreas Gohr * Set the directories to search for imports 252*e6380ba3SAndreas Gohr * 253*e6380ba3SAndreas Gohr * Overwrites any previously set directories 254*e6380ba3SAndreas Gohr * 255*e6380ba3SAndreas Gohr * @param string|string[] $dirs 256*e6380ba3SAndreas Gohr */ 257*e6380ba3SAndreas Gohr public function setImportDir($dirs): void 258*e6380ba3SAndreas Gohr { 259*e6380ba3SAndreas Gohr $this->importDir = (array)$dirs; 260*e6380ba3SAndreas Gohr } 261*e6380ba3SAndreas Gohr 262*e6380ba3SAndreas Gohr /** 263*e6380ba3SAndreas Gohr * Add an additional directory to search for imports 264*e6380ba3SAndreas Gohr */ 265*e6380ba3SAndreas Gohr public function addImportDir(string $dir): void 266*e6380ba3SAndreas Gohr { 267*e6380ba3SAndreas Gohr $this->importDir = (array)$this->importDir; 268*e6380ba3SAndreas Gohr $this->importDir[] = $dir; 269*e6380ba3SAndreas Gohr } 270*e6380ba3SAndreas Gohr 271*e6380ba3SAndreas Gohr /** 272*e6380ba3SAndreas Gohr * Enable or disable import statements 273*e6380ba3SAndreas Gohr * 274*e6380ba3SAndreas Gohr * There is usually no need to disable imports 275*e6380ba3SAndreas Gohr * 276*e6380ba3SAndreas Gohr * @param bool $enable 277*e6380ba3SAndreas Gohr * @return void 278*e6380ba3SAndreas Gohr */ 279*e6380ba3SAndreas Gohr public function enableImports(bool $enable): void 280*e6380ba3SAndreas Gohr { 281*e6380ba3SAndreas Gohr $this->importDisabled = !$enable; 282*e6380ba3SAndreas Gohr } 283*e6380ba3SAndreas Gohr 284*e6380ba3SAndreas Gohr /** 285*e6380ba3SAndreas Gohr * Set the formatter to use for output 286*e6380ba3SAndreas Gohr * 287*e6380ba3SAndreas Gohr * @param FormatterClassic|null $formatter Null for the default LessJs formatter 288*e6380ba3SAndreas Gohr * @return void 289*e6380ba3SAndreas Gohr */ 290*e6380ba3SAndreas Gohr public function setFormatter(?FormatterClassic $formatter = null) 291*e6380ba3SAndreas Gohr { 292*e6380ba3SAndreas Gohr if ($formatter === null) { 293*e6380ba3SAndreas Gohr $formatter = new FormatterLessJs(); 294*e6380ba3SAndreas Gohr } 295*e6380ba3SAndreas Gohr 296*e6380ba3SAndreas Gohr $this->formatter = $formatter; 297*e6380ba3SAndreas Gohr } 298*e6380ba3SAndreas Gohr 299*e6380ba3SAndreas Gohr // endregion 300*e6380ba3SAndreas Gohr 301*e6380ba3SAndreas Gohr 302*e6380ba3SAndreas Gohr /** 303*e6380ba3SAndreas Gohr * Register all the default functions 304*e6380ba3SAndreas Gohr */ 305*e6380ba3SAndreas Gohr protected function registerLibraryFunctions() 306*e6380ba3SAndreas Gohr { 307*e6380ba3SAndreas Gohr $files = glob(__DIR__ . '/Functions/*.php'); 308*e6380ba3SAndreas Gohr foreach ($files as $file) { 309*e6380ba3SAndreas Gohr $name = basename($file, '.php'); 310*e6380ba3SAndreas Gohr if (substr($name, 0, 8) == 'Abstract') continue; 311*e6380ba3SAndreas Gohr $class = '\\LesserPHP\\Functions\\' . $name; 312*e6380ba3SAndreas Gohr $funcObj = new $class($this); 313*e6380ba3SAndreas Gohr if ($funcObj instanceof AbstractFunctionCollection) { 314*e6380ba3SAndreas Gohr foreach ($funcObj->getFunctions() as $name => $callback) { 315*e6380ba3SAndreas Gohr $this->registerFunction($name, $callback); 316*e6380ba3SAndreas Gohr } 317*e6380ba3SAndreas Gohr } 318*e6380ba3SAndreas Gohr } 319*e6380ba3SAndreas Gohr } 320*e6380ba3SAndreas Gohr 321*e6380ba3SAndreas Gohr /** 322*e6380ba3SAndreas Gohr * attempts to find the path of an import url, returns null for css files 323*e6380ba3SAndreas Gohr * 324*e6380ba3SAndreas Gohr * @internal parser internal method 325*e6380ba3SAndreas Gohr */ 326*e6380ba3SAndreas Gohr public function findImport(string $url): ?string 327*e6380ba3SAndreas Gohr { 328*e6380ba3SAndreas Gohr foreach ((array)$this->importDir as $dir) { 329*e6380ba3SAndreas Gohr $full = $dir . (substr($dir, -1) != '/' ? '/' : '') . $url; 330*e6380ba3SAndreas Gohr if ($this->fileExists($file = $full . '.less') || $this->fileExists($file = $full)) { 331*e6380ba3SAndreas Gohr return $file; 332*e6380ba3SAndreas Gohr } 333*e6380ba3SAndreas Gohr } 334*e6380ba3SAndreas Gohr 335*e6380ba3SAndreas Gohr return null; 336*e6380ba3SAndreas Gohr } 337*e6380ba3SAndreas Gohr 338*e6380ba3SAndreas Gohr /** 339*e6380ba3SAndreas Gohr * Check if a given file exists and is actually a file 340*e6380ba3SAndreas Gohr * 341*e6380ba3SAndreas Gohr * @param string $name file path 342*e6380ba3SAndreas Gohr * @return bool 343*e6380ba3SAndreas Gohr */ 344*e6380ba3SAndreas Gohr protected function fileExists(string $name): bool 345*e6380ba3SAndreas Gohr { 346*e6380ba3SAndreas Gohr return is_file($name); 347*e6380ba3SAndreas Gohr } 348*e6380ba3SAndreas Gohr 349*e6380ba3SAndreas Gohr /** 350*e6380ba3SAndreas Gohr * @internal parser internal method 351*e6380ba3SAndreas Gohr */ 352*e6380ba3SAndreas Gohr public static function compressList($items, $delim) 353*e6380ba3SAndreas Gohr { 354*e6380ba3SAndreas Gohr if (!isset($items[1]) && isset($items[0])) return $items[0]; 355*e6380ba3SAndreas Gohr else return ['list', $delim, $items]; 356*e6380ba3SAndreas Gohr } 357*e6380ba3SAndreas Gohr 358*e6380ba3SAndreas Gohr 359*e6380ba3SAndreas Gohr /** 360*e6380ba3SAndreas Gohr * @throws Exception 361*e6380ba3SAndreas Gohr */ 362*e6380ba3SAndreas Gohr protected function tryImport($importPath, $parentBlock, $out) 363*e6380ba3SAndreas Gohr { 364*e6380ba3SAndreas Gohr if ($importPath[0] == 'function' && $importPath[1] == 'url') { 365*e6380ba3SAndreas Gohr $importPath = $this->flattenList($importPath[2]); 366*e6380ba3SAndreas Gohr } 367*e6380ba3SAndreas Gohr 368*e6380ba3SAndreas Gohr $str = $this->coerceString($importPath); 369*e6380ba3SAndreas Gohr if ($str === null) return false; 370*e6380ba3SAndreas Gohr 371*e6380ba3SAndreas Gohr $url = $this->compileValue($this->unwrap($str)); 372*e6380ba3SAndreas Gohr 373*e6380ba3SAndreas Gohr // don't import if it ends in css 374*e6380ba3SAndreas Gohr if (substr_compare($url, '.css', -4, 4) === 0) return false; 375*e6380ba3SAndreas Gohr 376*e6380ba3SAndreas Gohr $realPath = $this->findImport($url); 377*e6380ba3SAndreas Gohr 378*e6380ba3SAndreas Gohr if ($realPath === null) return false; 379*e6380ba3SAndreas Gohr 380*e6380ba3SAndreas Gohr if ($this->importDisabled) { 381*e6380ba3SAndreas Gohr return [false, '/* import disabled */']; 382*e6380ba3SAndreas Gohr } 383*e6380ba3SAndreas Gohr 384*e6380ba3SAndreas Gohr if (isset($this->allParsedFiles[realpath($realPath)])) { 385*e6380ba3SAndreas Gohr return [false, null]; 386*e6380ba3SAndreas Gohr } 387*e6380ba3SAndreas Gohr 388*e6380ba3SAndreas Gohr $this->addParsedFile($realPath); 389*e6380ba3SAndreas Gohr $parser = $this->makeParser($realPath); 390*e6380ba3SAndreas Gohr $root = $parser->parse(file_get_contents($realPath)); 391*e6380ba3SAndreas Gohr 392*e6380ba3SAndreas Gohr // set the parents of all the block props 393*e6380ba3SAndreas Gohr foreach ($root->props as $prop) { 394*e6380ba3SAndreas Gohr if ($prop[0] == 'block') { 395*e6380ba3SAndreas Gohr $prop[1]->parent = $parentBlock; 396*e6380ba3SAndreas Gohr } 397*e6380ba3SAndreas Gohr } 398*e6380ba3SAndreas Gohr 399*e6380ba3SAndreas Gohr // copy mixins into scope, set their parents 400*e6380ba3SAndreas Gohr // bring blocks from import into current block 401*e6380ba3SAndreas Gohr // TODO: need to mark the source parser these came from this file 402*e6380ba3SAndreas Gohr foreach ($root->children as $childName => $child) { 403*e6380ba3SAndreas Gohr if (isset($parentBlock->children[$childName])) { 404*e6380ba3SAndreas Gohr $parentBlock->children[$childName] = array_merge( 405*e6380ba3SAndreas Gohr $parentBlock->children[$childName], 406*e6380ba3SAndreas Gohr $child 407*e6380ba3SAndreas Gohr ); 408*e6380ba3SAndreas Gohr } else { 409*e6380ba3SAndreas Gohr $parentBlock->children[$childName] = $child; 410*e6380ba3SAndreas Gohr } 411*e6380ba3SAndreas Gohr } 412*e6380ba3SAndreas Gohr 413*e6380ba3SAndreas Gohr $pi = pathinfo($realPath); 414*e6380ba3SAndreas Gohr $dir = $pi['dirname']; 415*e6380ba3SAndreas Gohr 416*e6380ba3SAndreas Gohr [$top, $bottom] = $this->sortProps($root->props, true); 417*e6380ba3SAndreas Gohr $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir); 418*e6380ba3SAndreas Gohr 419*e6380ba3SAndreas Gohr return [true, $bottom, $parser, $dir]; 420*e6380ba3SAndreas Gohr } 421*e6380ba3SAndreas Gohr 422*e6380ba3SAndreas Gohr /** 423*e6380ba3SAndreas Gohr * @throws Exception 424*e6380ba3SAndreas Gohr */ 425*e6380ba3SAndreas Gohr protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) 426*e6380ba3SAndreas Gohr { 427*e6380ba3SAndreas Gohr $oldSourceParser = $this->sourceParser; 428*e6380ba3SAndreas Gohr 429*e6380ba3SAndreas Gohr $oldImport = $this->importDir; 430*e6380ba3SAndreas Gohr 431*e6380ba3SAndreas Gohr // TODO: this is because the importDir api is stupid 432*e6380ba3SAndreas Gohr $this->importDir = (array)$this->importDir; 433*e6380ba3SAndreas Gohr array_unshift($this->importDir, $importDir); 434*e6380ba3SAndreas Gohr 435*e6380ba3SAndreas Gohr foreach ($props as $prop) { 436*e6380ba3SAndreas Gohr $this->compileProp($prop, $block, $out); 437*e6380ba3SAndreas Gohr } 438*e6380ba3SAndreas Gohr 439*e6380ba3SAndreas Gohr $this->importDir = $oldImport; 440*e6380ba3SAndreas Gohr $this->sourceParser = $oldSourceParser; 441*e6380ba3SAndreas Gohr } 442*e6380ba3SAndreas Gohr 443*e6380ba3SAndreas Gohr /** 444*e6380ba3SAndreas Gohr * Recursively compiles a block. 445*e6380ba3SAndreas Gohr * 446*e6380ba3SAndreas Gohr * A block is analogous to a CSS block in most cases. A single LESS document 447*e6380ba3SAndreas Gohr * is encapsulated in a block when parsed, but it does not have parent tags 448*e6380ba3SAndreas Gohr * so all of it's children appear on the root level when compiled. 449*e6380ba3SAndreas Gohr * 450*e6380ba3SAndreas Gohr * Blocks are made up of props and children. 451*e6380ba3SAndreas Gohr * 452*e6380ba3SAndreas Gohr * Props are property instructions, array tuples which describe an action 453*e6380ba3SAndreas Gohr * to be taken, eg. write a property, set a variable, mixin a block. 454*e6380ba3SAndreas Gohr * 455*e6380ba3SAndreas Gohr * The children of a block are just all the blocks that are defined within. 456*e6380ba3SAndreas Gohr * This is used to look up mixins when performing a mixin. 457*e6380ba3SAndreas Gohr * 458*e6380ba3SAndreas Gohr * Compiling the block involves pushing a fresh environment on the stack, 459*e6380ba3SAndreas Gohr * and iterating through the props, compiling each one. 460*e6380ba3SAndreas Gohr * 461*e6380ba3SAndreas Gohr * @throws Exception 462*e6380ba3SAndreas Gohr * @see compileProp() 463*e6380ba3SAndreas Gohr */ 464*e6380ba3SAndreas Gohr protected function compileBlock($block) 465*e6380ba3SAndreas Gohr { 466*e6380ba3SAndreas Gohr switch ($block->type) { 467*e6380ba3SAndreas Gohr case 'root': 468*e6380ba3SAndreas Gohr $this->compileRoot($block); 469*e6380ba3SAndreas Gohr break; 470*e6380ba3SAndreas Gohr case null: 471*e6380ba3SAndreas Gohr $this->compileCSSBlock($block); 472*e6380ba3SAndreas Gohr break; 473*e6380ba3SAndreas Gohr case 'media': 474*e6380ba3SAndreas Gohr $this->compileMedia($block); 475*e6380ba3SAndreas Gohr break; 476*e6380ba3SAndreas Gohr case 'directive': 477*e6380ba3SAndreas Gohr $name = '@' . $block->name; 478*e6380ba3SAndreas Gohr if (!empty($block->value)) { 479*e6380ba3SAndreas Gohr $name .= ' ' . $this->compileValue($this->reduce($block->value)); 480*e6380ba3SAndreas Gohr } 481*e6380ba3SAndreas Gohr 482*e6380ba3SAndreas Gohr $this->compileNestedBlock($block, [$name]); 483*e6380ba3SAndreas Gohr break; 484*e6380ba3SAndreas Gohr default: 485*e6380ba3SAndreas Gohr throw new Exception("unknown block type: $block->type\n"); 486*e6380ba3SAndreas Gohr } 487*e6380ba3SAndreas Gohr } 488*e6380ba3SAndreas Gohr 489*e6380ba3SAndreas Gohr /** 490*e6380ba3SAndreas Gohr * @throws Exception 491*e6380ba3SAndreas Gohr */ 492*e6380ba3SAndreas Gohr protected function compileCSSBlock($block) 493*e6380ba3SAndreas Gohr { 494*e6380ba3SAndreas Gohr $env = $this->pushEnv(); 495*e6380ba3SAndreas Gohr 496*e6380ba3SAndreas Gohr $selectors = $this->compileSelectors($block->tags); 497*e6380ba3SAndreas Gohr $env->selectors = $this->multiplySelectors($selectors); 498*e6380ba3SAndreas Gohr $out = $this->makeOutputBlock(null, $env->selectors); 499*e6380ba3SAndreas Gohr 500*e6380ba3SAndreas Gohr $this->scope->children[] = $out; 501*e6380ba3SAndreas Gohr $this->compileProps($block, $out); 502*e6380ba3SAndreas Gohr 503*e6380ba3SAndreas Gohr $block->scope = $env; // mixins carry scope with them! 504*e6380ba3SAndreas Gohr $this->popEnv(); 505*e6380ba3SAndreas Gohr } 506*e6380ba3SAndreas Gohr 507*e6380ba3SAndreas Gohr /** 508*e6380ba3SAndreas Gohr * @throws Exception 509*e6380ba3SAndreas Gohr */ 510*e6380ba3SAndreas Gohr protected function compileMedia($media) 511*e6380ba3SAndreas Gohr { 512*e6380ba3SAndreas Gohr $env = $this->pushEnv($media); 513*e6380ba3SAndreas Gohr $parentScope = $this->mediaParent($this->scope); 514*e6380ba3SAndreas Gohr 515*e6380ba3SAndreas Gohr $query = $this->compileMediaQuery($this->multiplyMedia($env)); 516*e6380ba3SAndreas Gohr 517*e6380ba3SAndreas Gohr $this->scope = $this->makeOutputBlock($media->type, [$query]); 518*e6380ba3SAndreas Gohr $parentScope->children[] = $this->scope; 519*e6380ba3SAndreas Gohr 520*e6380ba3SAndreas Gohr $this->compileProps($media, $this->scope); 521*e6380ba3SAndreas Gohr 522*e6380ba3SAndreas Gohr if (count($this->scope->lines) > 0) { 523*e6380ba3SAndreas Gohr $orphanSelelectors = $this->findClosestSelectors(); 524*e6380ba3SAndreas Gohr if (!is_null($orphanSelelectors)) { 525*e6380ba3SAndreas Gohr $orphan = $this->makeOutputBlock(null, $orphanSelelectors); 526*e6380ba3SAndreas Gohr $orphan->lines = $this->scope->lines; 527*e6380ba3SAndreas Gohr array_unshift($this->scope->children, $orphan); 528*e6380ba3SAndreas Gohr $this->scope->lines = []; 529*e6380ba3SAndreas Gohr } 530*e6380ba3SAndreas Gohr } 531*e6380ba3SAndreas Gohr 532*e6380ba3SAndreas Gohr $this->scope = $this->scope->parent; 533*e6380ba3SAndreas Gohr $this->popEnv(); 534*e6380ba3SAndreas Gohr } 535*e6380ba3SAndreas Gohr 536*e6380ba3SAndreas Gohr protected function mediaParent($scope) 537*e6380ba3SAndreas Gohr { 538*e6380ba3SAndreas Gohr while (!empty($scope->parent)) { 539*e6380ba3SAndreas Gohr if (!empty($scope->type) && $scope->type != 'media') { 540*e6380ba3SAndreas Gohr break; 541*e6380ba3SAndreas Gohr } 542*e6380ba3SAndreas Gohr $scope = $scope->parent; 543*e6380ba3SAndreas Gohr } 544*e6380ba3SAndreas Gohr 545*e6380ba3SAndreas Gohr return $scope; 546*e6380ba3SAndreas Gohr } 547*e6380ba3SAndreas Gohr 548*e6380ba3SAndreas Gohr /** 549*e6380ba3SAndreas Gohr * @throws Exception 550*e6380ba3SAndreas Gohr */ 551*e6380ba3SAndreas Gohr protected function compileNestedBlock($block, $selectors) 552*e6380ba3SAndreas Gohr { 553*e6380ba3SAndreas Gohr $this->pushEnv($block); 554*e6380ba3SAndreas Gohr $this->scope = $this->makeOutputBlock($block->type, $selectors); 555*e6380ba3SAndreas Gohr $this->scope->parent->children[] = $this->scope; 556*e6380ba3SAndreas Gohr 557*e6380ba3SAndreas Gohr $this->compileProps($block, $this->scope); 558*e6380ba3SAndreas Gohr 559*e6380ba3SAndreas Gohr $this->scope = $this->scope->parent; 560*e6380ba3SAndreas Gohr $this->popEnv(); 561*e6380ba3SAndreas Gohr } 562*e6380ba3SAndreas Gohr 563*e6380ba3SAndreas Gohr /** 564*e6380ba3SAndreas Gohr * @throws Exception 565*e6380ba3SAndreas Gohr */ 566*e6380ba3SAndreas Gohr protected function compileRoot($root) 567*e6380ba3SAndreas Gohr { 568*e6380ba3SAndreas Gohr $this->pushEnv(); 569*e6380ba3SAndreas Gohr $this->scope = $this->makeOutputBlock($root->type); 570*e6380ba3SAndreas Gohr $this->compileProps($root, $this->scope); 571*e6380ba3SAndreas Gohr $this->popEnv(); 572*e6380ba3SAndreas Gohr } 573*e6380ba3SAndreas Gohr 574*e6380ba3SAndreas Gohr /** 575*e6380ba3SAndreas Gohr * @throws Exception 576*e6380ba3SAndreas Gohr */ 577*e6380ba3SAndreas Gohr protected function compileProps($block, $out) 578*e6380ba3SAndreas Gohr { 579*e6380ba3SAndreas Gohr foreach ($this->sortProps($block->props) as $prop) { 580*e6380ba3SAndreas Gohr $this->compileProp($prop, $block, $out); 581*e6380ba3SAndreas Gohr } 582*e6380ba3SAndreas Gohr $out->lines = $this->deduplicate($out->lines); 583*e6380ba3SAndreas Gohr } 584*e6380ba3SAndreas Gohr 585*e6380ba3SAndreas Gohr /** 586*e6380ba3SAndreas Gohr * Deduplicate lines in a block. Comments are not deduplicated. If a 587*e6380ba3SAndreas Gohr * duplicate rule is detected, the comments immediately preceding each 588*e6380ba3SAndreas Gohr * occurrence are consolidated. 589*e6380ba3SAndreas Gohr */ 590*e6380ba3SAndreas Gohr protected function deduplicate($lines) 591*e6380ba3SAndreas Gohr { 592*e6380ba3SAndreas Gohr $unique = []; 593*e6380ba3SAndreas Gohr $comments = []; 594*e6380ba3SAndreas Gohr 595*e6380ba3SAndreas Gohr foreach ($lines as $line) { 596*e6380ba3SAndreas Gohr if (strpos($line, '/*') === 0) { 597*e6380ba3SAndreas Gohr $comments[] = $line; 598*e6380ba3SAndreas Gohr continue; 599*e6380ba3SAndreas Gohr } 600*e6380ba3SAndreas Gohr if (!in_array($line, $unique)) { 601*e6380ba3SAndreas Gohr $unique[] = $line; 602*e6380ba3SAndreas Gohr } 603*e6380ba3SAndreas Gohr array_splice($unique, array_search($line, $unique), 0, $comments); 604*e6380ba3SAndreas Gohr $comments = []; 605*e6380ba3SAndreas Gohr } 606*e6380ba3SAndreas Gohr return array_merge($unique, $comments); 607*e6380ba3SAndreas Gohr } 608*e6380ba3SAndreas Gohr 609*e6380ba3SAndreas Gohr protected function sortProps($props, $split = false) 610*e6380ba3SAndreas Gohr { 611*e6380ba3SAndreas Gohr $vars = []; 612*e6380ba3SAndreas Gohr $imports = []; 613*e6380ba3SAndreas Gohr $other = []; 614*e6380ba3SAndreas Gohr $stack = []; 615*e6380ba3SAndreas Gohr 616*e6380ba3SAndreas Gohr foreach ($props as $prop) { 617*e6380ba3SAndreas Gohr switch ($prop[0]) { 618*e6380ba3SAndreas Gohr case 'comment': 619*e6380ba3SAndreas Gohr $stack[] = $prop; 620*e6380ba3SAndreas Gohr break; 621*e6380ba3SAndreas Gohr case 'assign': 622*e6380ba3SAndreas Gohr $stack[] = $prop; 623*e6380ba3SAndreas Gohr if (isset($prop[1][0]) && $prop[1][0] == Constants::VPREFIX) { 624*e6380ba3SAndreas Gohr $vars = array_merge($vars, $stack); 625*e6380ba3SAndreas Gohr } else { 626*e6380ba3SAndreas Gohr $other = array_merge($other, $stack); 627*e6380ba3SAndreas Gohr } 628*e6380ba3SAndreas Gohr $stack = []; 629*e6380ba3SAndreas Gohr break; 630*e6380ba3SAndreas Gohr case 'import': 631*e6380ba3SAndreas Gohr $id = self::$nextImportId++; 632*e6380ba3SAndreas Gohr $prop[] = $id; 633*e6380ba3SAndreas Gohr $stack[] = $prop; 634*e6380ba3SAndreas Gohr $imports = array_merge($imports, $stack); 635*e6380ba3SAndreas Gohr $other[] = ['import_mixin', $id]; 636*e6380ba3SAndreas Gohr $stack = []; 637*e6380ba3SAndreas Gohr break; 638*e6380ba3SAndreas Gohr default: 639*e6380ba3SAndreas Gohr $stack[] = $prop; 640*e6380ba3SAndreas Gohr $other = array_merge($other, $stack); 641*e6380ba3SAndreas Gohr $stack = []; 642*e6380ba3SAndreas Gohr break; 643*e6380ba3SAndreas Gohr } 644*e6380ba3SAndreas Gohr } 645*e6380ba3SAndreas Gohr $other = array_merge($other, $stack); 646*e6380ba3SAndreas Gohr 647*e6380ba3SAndreas Gohr if ($split) { 648*e6380ba3SAndreas Gohr return [array_merge($vars, $imports, $vars), $other]; 649*e6380ba3SAndreas Gohr } else { 650*e6380ba3SAndreas Gohr return array_merge($vars, $imports, $vars, $other); 651*e6380ba3SAndreas Gohr } 652*e6380ba3SAndreas Gohr } 653*e6380ba3SAndreas Gohr 654*e6380ba3SAndreas Gohr /** 655*e6380ba3SAndreas Gohr * @throws Exception 656*e6380ba3SAndreas Gohr */ 657*e6380ba3SAndreas Gohr protected function compileMediaQuery($queries) 658*e6380ba3SAndreas Gohr { 659*e6380ba3SAndreas Gohr $compiledQueries = []; 660*e6380ba3SAndreas Gohr foreach ($queries as $query) { 661*e6380ba3SAndreas Gohr $parts = []; 662*e6380ba3SAndreas Gohr foreach ($query as $q) { 663*e6380ba3SAndreas Gohr switch ($q[0]) { 664*e6380ba3SAndreas Gohr case 'mediaType': 665*e6380ba3SAndreas Gohr $parts[] = implode(' ', array_slice($q, 1)); 666*e6380ba3SAndreas Gohr break; 667*e6380ba3SAndreas Gohr case 'mediaExp': 668*e6380ba3SAndreas Gohr if (isset($q[2])) { 669*e6380ba3SAndreas Gohr $parts[] = "($q[1]: " . 670*e6380ba3SAndreas Gohr $this->compileValue($this->reduce($q[2])) . ')'; 671*e6380ba3SAndreas Gohr } else { 672*e6380ba3SAndreas Gohr $parts[] = "($q[1])"; 673*e6380ba3SAndreas Gohr } 674*e6380ba3SAndreas Gohr break; 675*e6380ba3SAndreas Gohr case 'variable': 676*e6380ba3SAndreas Gohr $parts[] = $this->compileValue($this->reduce($q)); 677*e6380ba3SAndreas Gohr break; 678*e6380ba3SAndreas Gohr } 679*e6380ba3SAndreas Gohr } 680*e6380ba3SAndreas Gohr 681*e6380ba3SAndreas Gohr if (count($parts) > 0) { 682*e6380ba3SAndreas Gohr $compiledQueries[] = implode(' and ', $parts); 683*e6380ba3SAndreas Gohr } 684*e6380ba3SAndreas Gohr } 685*e6380ba3SAndreas Gohr 686*e6380ba3SAndreas Gohr $out = '@media'; 687*e6380ba3SAndreas Gohr if (!empty($parts)) { 688*e6380ba3SAndreas Gohr $out .= ' ' . 689*e6380ba3SAndreas Gohr implode($this->formatter->selectorSeparator, $compiledQueries); 690*e6380ba3SAndreas Gohr } 691*e6380ba3SAndreas Gohr return $out; 692*e6380ba3SAndreas Gohr } 693*e6380ba3SAndreas Gohr 694*e6380ba3SAndreas Gohr protected function multiplyMedia($env, $childQueries = null) 695*e6380ba3SAndreas Gohr { 696*e6380ba3SAndreas Gohr if (is_null($env) || 697*e6380ba3SAndreas Gohr !empty($env->block->type) && $env->block->type != 'media') { 698*e6380ba3SAndreas Gohr return $childQueries; 699*e6380ba3SAndreas Gohr } 700*e6380ba3SAndreas Gohr 701*e6380ba3SAndreas Gohr // plain old block, skip 702*e6380ba3SAndreas Gohr if (empty($env->block->type)) { 703*e6380ba3SAndreas Gohr return $this->multiplyMedia($env->parent, $childQueries); 704*e6380ba3SAndreas Gohr } 705*e6380ba3SAndreas Gohr 706*e6380ba3SAndreas Gohr $out = []; 707*e6380ba3SAndreas Gohr $queries = $env->block->queries; 708*e6380ba3SAndreas Gohr if (is_null($childQueries)) { 709*e6380ba3SAndreas Gohr $out = $queries; 710*e6380ba3SAndreas Gohr } else { 711*e6380ba3SAndreas Gohr foreach ($queries as $parent) { 712*e6380ba3SAndreas Gohr foreach ($childQueries as $child) { 713*e6380ba3SAndreas Gohr $out[] = array_merge($parent, $child); 714*e6380ba3SAndreas Gohr } 715*e6380ba3SAndreas Gohr } 716*e6380ba3SAndreas Gohr } 717*e6380ba3SAndreas Gohr 718*e6380ba3SAndreas Gohr return $this->multiplyMedia($env->parent, $out); 719*e6380ba3SAndreas Gohr } 720*e6380ba3SAndreas Gohr 721*e6380ba3SAndreas Gohr protected function expandParentSelectors(&$tag, $replace): int 722*e6380ba3SAndreas Gohr { 723*e6380ba3SAndreas Gohr $parts = explode("$&$", $tag); 724*e6380ba3SAndreas Gohr $count = 0; 725*e6380ba3SAndreas Gohr foreach ($parts as &$part) { 726*e6380ba3SAndreas Gohr $part = str_replace(Constants::PARENT_SELECTOR, $replace, $part, $c); 727*e6380ba3SAndreas Gohr $count += $c; 728*e6380ba3SAndreas Gohr } 729*e6380ba3SAndreas Gohr $tag = implode(Constants::PARENT_SELECTOR, $parts); 730*e6380ba3SAndreas Gohr return $count; 731*e6380ba3SAndreas Gohr } 732*e6380ba3SAndreas Gohr 733*e6380ba3SAndreas Gohr protected function findClosestSelectors() 734*e6380ba3SAndreas Gohr { 735*e6380ba3SAndreas Gohr $env = $this->env; 736*e6380ba3SAndreas Gohr $selectors = null; 737*e6380ba3SAndreas Gohr while ($env !== null) { 738*e6380ba3SAndreas Gohr if (isset($env->selectors)) { 739*e6380ba3SAndreas Gohr $selectors = $env->selectors; 740*e6380ba3SAndreas Gohr break; 741*e6380ba3SAndreas Gohr } 742*e6380ba3SAndreas Gohr $env = $env->parent; 743*e6380ba3SAndreas Gohr } 744*e6380ba3SAndreas Gohr 745*e6380ba3SAndreas Gohr return $selectors; 746*e6380ba3SAndreas Gohr } 747*e6380ba3SAndreas Gohr 748*e6380ba3SAndreas Gohr 749*e6380ba3SAndreas Gohr // multiply $selectors against the nearest selectors in env 750*e6380ba3SAndreas Gohr protected function multiplySelectors($selectors) 751*e6380ba3SAndreas Gohr { 752*e6380ba3SAndreas Gohr // find parent selectors 753*e6380ba3SAndreas Gohr 754*e6380ba3SAndreas Gohr $parentSelectors = $this->findClosestSelectors(); 755*e6380ba3SAndreas Gohr if (is_null($parentSelectors)) { 756*e6380ba3SAndreas Gohr // kill parent reference in top level selector 757*e6380ba3SAndreas Gohr foreach ($selectors as &$s) { 758*e6380ba3SAndreas Gohr $this->expandParentSelectors($s, ''); 759*e6380ba3SAndreas Gohr } 760*e6380ba3SAndreas Gohr 761*e6380ba3SAndreas Gohr return $selectors; 762*e6380ba3SAndreas Gohr } 763*e6380ba3SAndreas Gohr 764*e6380ba3SAndreas Gohr $out = []; 765*e6380ba3SAndreas Gohr foreach ($parentSelectors as $parent) { 766*e6380ba3SAndreas Gohr foreach ($selectors as $child) { 767*e6380ba3SAndreas Gohr $count = $this->expandParentSelectors($child, $parent); 768*e6380ba3SAndreas Gohr 769*e6380ba3SAndreas Gohr // don't prepend the parent tag if & was used 770*e6380ba3SAndreas Gohr if ($count > 0) { 771*e6380ba3SAndreas Gohr $out[] = trim($child); 772*e6380ba3SAndreas Gohr } else { 773*e6380ba3SAndreas Gohr $out[] = trim($parent . ' ' . $child); 774*e6380ba3SAndreas Gohr } 775*e6380ba3SAndreas Gohr } 776*e6380ba3SAndreas Gohr } 777*e6380ba3SAndreas Gohr 778*e6380ba3SAndreas Gohr return $out; 779*e6380ba3SAndreas Gohr } 780*e6380ba3SAndreas Gohr 781*e6380ba3SAndreas Gohr /** 782*e6380ba3SAndreas Gohr * reduces selector expressions 783*e6380ba3SAndreas Gohr * @throws Exception 784*e6380ba3SAndreas Gohr */ 785*e6380ba3SAndreas Gohr protected function compileSelectors($selectors) 786*e6380ba3SAndreas Gohr { 787*e6380ba3SAndreas Gohr $out = []; 788*e6380ba3SAndreas Gohr 789*e6380ba3SAndreas Gohr foreach ($selectors as $s) { 790*e6380ba3SAndreas Gohr if (is_array($s)) { 791*e6380ba3SAndreas Gohr [, $value] = $s; 792*e6380ba3SAndreas Gohr $out[] = trim($this->compileValue($this->reduce($value))); 793*e6380ba3SAndreas Gohr } else { 794*e6380ba3SAndreas Gohr $out[] = $s; 795*e6380ba3SAndreas Gohr } 796*e6380ba3SAndreas Gohr } 797*e6380ba3SAndreas Gohr 798*e6380ba3SAndreas Gohr return $out; 799*e6380ba3SAndreas Gohr } 800*e6380ba3SAndreas Gohr 801*e6380ba3SAndreas Gohr protected function eq($left, $right) 802*e6380ba3SAndreas Gohr { 803*e6380ba3SAndreas Gohr return $left == $right; 804*e6380ba3SAndreas Gohr } 805*e6380ba3SAndreas Gohr 806*e6380ba3SAndreas Gohr /** 807*e6380ba3SAndreas Gohr * @return bool 808*e6380ba3SAndreas Gohr * @throws Exception 809*e6380ba3SAndreas Gohr */ 810*e6380ba3SAndreas Gohr protected function patternMatch($block, $orderedArgs, $keywordArgs) 811*e6380ba3SAndreas Gohr { 812*e6380ba3SAndreas Gohr // match the guards if it has them 813*e6380ba3SAndreas Gohr // any one of the groups must have all its guards pass for a match 814*e6380ba3SAndreas Gohr if (!empty($block->guards)) { 815*e6380ba3SAndreas Gohr $groupPassed = false; 816*e6380ba3SAndreas Gohr foreach ($block->guards as $guardGroup) { 817*e6380ba3SAndreas Gohr foreach ($guardGroup as $guard) { 818*e6380ba3SAndreas Gohr $this->pushEnv(); 819*e6380ba3SAndreas Gohr $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); 820*e6380ba3SAndreas Gohr 821*e6380ba3SAndreas Gohr $negate = false; 822*e6380ba3SAndreas Gohr if ($guard[0] == 'negate') { 823*e6380ba3SAndreas Gohr $guard = $guard[1]; 824*e6380ba3SAndreas Gohr $negate = true; 825*e6380ba3SAndreas Gohr } 826*e6380ba3SAndreas Gohr 827*e6380ba3SAndreas Gohr $passed = $this->reduce($guard) == Constants::TRUE; 828*e6380ba3SAndreas Gohr if ($negate) $passed = !$passed; 829*e6380ba3SAndreas Gohr 830*e6380ba3SAndreas Gohr $this->popEnv(); 831*e6380ba3SAndreas Gohr 832*e6380ba3SAndreas Gohr if ($passed) { 833*e6380ba3SAndreas Gohr $groupPassed = true; 834*e6380ba3SAndreas Gohr } else { 835*e6380ba3SAndreas Gohr $groupPassed = false; 836*e6380ba3SAndreas Gohr break; 837*e6380ba3SAndreas Gohr } 838*e6380ba3SAndreas Gohr } 839*e6380ba3SAndreas Gohr 840*e6380ba3SAndreas Gohr if ($groupPassed) break; 841*e6380ba3SAndreas Gohr } 842*e6380ba3SAndreas Gohr 843*e6380ba3SAndreas Gohr if (!$groupPassed) { 844*e6380ba3SAndreas Gohr return false; 845*e6380ba3SAndreas Gohr } 846*e6380ba3SAndreas Gohr } 847*e6380ba3SAndreas Gohr 848*e6380ba3SAndreas Gohr if (empty($block->args)) { 849*e6380ba3SAndreas Gohr return $block->isVararg || empty($orderedArgs) && empty($keywordArgs); 850*e6380ba3SAndreas Gohr } 851*e6380ba3SAndreas Gohr 852*e6380ba3SAndreas Gohr $remainingArgs = $block->args; 853*e6380ba3SAndreas Gohr if ($keywordArgs) { 854*e6380ba3SAndreas Gohr $remainingArgs = []; 855*e6380ba3SAndreas Gohr foreach ($block->args as $arg) { 856*e6380ba3SAndreas Gohr if ($arg[0] == 'arg' && isset($keywordArgs[$arg[1]])) { 857*e6380ba3SAndreas Gohr continue; 858*e6380ba3SAndreas Gohr } 859*e6380ba3SAndreas Gohr 860*e6380ba3SAndreas Gohr $remainingArgs[] = $arg; 861*e6380ba3SAndreas Gohr } 862*e6380ba3SAndreas Gohr } 863*e6380ba3SAndreas Gohr 864*e6380ba3SAndreas Gohr $i = -1; // no args 865*e6380ba3SAndreas Gohr // try to match by arity or by argument literal 866*e6380ba3SAndreas Gohr foreach ($remainingArgs as $i => $arg) { 867*e6380ba3SAndreas Gohr switch ($arg[0]) { 868*e6380ba3SAndreas Gohr case 'lit': 869*e6380ba3SAndreas Gohr if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) { 870*e6380ba3SAndreas Gohr return false; 871*e6380ba3SAndreas Gohr } 872*e6380ba3SAndreas Gohr break; 873*e6380ba3SAndreas Gohr case 'arg': 874*e6380ba3SAndreas Gohr // no arg and no default value 875*e6380ba3SAndreas Gohr if (!isset($orderedArgs[$i]) && !isset($arg[2])) { 876*e6380ba3SAndreas Gohr return false; 877*e6380ba3SAndreas Gohr } 878*e6380ba3SAndreas Gohr break; 879*e6380ba3SAndreas Gohr case 'rest': 880*e6380ba3SAndreas Gohr $i--; // rest can be empty 881*e6380ba3SAndreas Gohr break 2; 882*e6380ba3SAndreas Gohr } 883*e6380ba3SAndreas Gohr } 884*e6380ba3SAndreas Gohr 885*e6380ba3SAndreas Gohr if ($block->isVararg) { 886*e6380ba3SAndreas Gohr return true; // not having enough is handled above 887*e6380ba3SAndreas Gohr } else { 888*e6380ba3SAndreas Gohr $numMatched = $i + 1; 889*e6380ba3SAndreas Gohr // greater than because default values always match 890*e6380ba3SAndreas Gohr return $numMatched >= count($orderedArgs); 891*e6380ba3SAndreas Gohr } 892*e6380ba3SAndreas Gohr } 893*e6380ba3SAndreas Gohr 894*e6380ba3SAndreas Gohr /** 895*e6380ba3SAndreas Gohr * @throws Exception 896*e6380ba3SAndreas Gohr */ 897*e6380ba3SAndreas Gohr protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip = []) 898*e6380ba3SAndreas Gohr { 899*e6380ba3SAndreas Gohr $matches = null; 900*e6380ba3SAndreas Gohr foreach ($blocks as $block) { 901*e6380ba3SAndreas Gohr // skip seen blocks that don't have arguments 902*e6380ba3SAndreas Gohr if (isset($skip[$block->id]) && !isset($block->args)) { 903*e6380ba3SAndreas Gohr continue; 904*e6380ba3SAndreas Gohr } 905*e6380ba3SAndreas Gohr 906*e6380ba3SAndreas Gohr if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) { 907*e6380ba3SAndreas Gohr $matches[] = $block; 908*e6380ba3SAndreas Gohr } 909*e6380ba3SAndreas Gohr } 910*e6380ba3SAndreas Gohr 911*e6380ba3SAndreas Gohr return $matches; 912*e6380ba3SAndreas Gohr } 913*e6380ba3SAndreas Gohr 914*e6380ba3SAndreas Gohr /** 915*e6380ba3SAndreas Gohr * attempt to find blocks matched by path and args 916*e6380ba3SAndreas Gohr * @throws Exception 917*e6380ba3SAndreas Gohr */ 918*e6380ba3SAndreas Gohr protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen = []) 919*e6380ba3SAndreas Gohr { 920*e6380ba3SAndreas Gohr if ($searchIn == null) return null; 921*e6380ba3SAndreas Gohr if (isset($seen[$searchIn->id])) return null; 922*e6380ba3SAndreas Gohr $seen[$searchIn->id] = true; 923*e6380ba3SAndreas Gohr 924*e6380ba3SAndreas Gohr $name = $path[0]; 925*e6380ba3SAndreas Gohr 926*e6380ba3SAndreas Gohr if (isset($searchIn->children[$name])) { 927*e6380ba3SAndreas Gohr $blocks = $searchIn->children[$name]; 928*e6380ba3SAndreas Gohr if (count($path) == 1) { 929*e6380ba3SAndreas Gohr $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen); 930*e6380ba3SAndreas Gohr if (!empty($matches)) { 931*e6380ba3SAndreas Gohr // This will return all blocks that match in the closest 932*e6380ba3SAndreas Gohr // scope that has any matching block, like lessjs 933*e6380ba3SAndreas Gohr return $matches; 934*e6380ba3SAndreas Gohr } 935*e6380ba3SAndreas Gohr } else { 936*e6380ba3SAndreas Gohr $matches = []; 937*e6380ba3SAndreas Gohr foreach ($blocks as $subBlock) { 938*e6380ba3SAndreas Gohr $subMatches = $this->findBlocks( 939*e6380ba3SAndreas Gohr $subBlock, 940*e6380ba3SAndreas Gohr array_slice($path, 1), 941*e6380ba3SAndreas Gohr $orderedArgs, 942*e6380ba3SAndreas Gohr $keywordArgs, 943*e6380ba3SAndreas Gohr $seen 944*e6380ba3SAndreas Gohr ); 945*e6380ba3SAndreas Gohr 946*e6380ba3SAndreas Gohr if (!is_null($subMatches)) { 947*e6380ba3SAndreas Gohr foreach ($subMatches as $sm) { 948*e6380ba3SAndreas Gohr $matches[] = $sm; 949*e6380ba3SAndreas Gohr } 950*e6380ba3SAndreas Gohr } 951*e6380ba3SAndreas Gohr } 952*e6380ba3SAndreas Gohr 953*e6380ba3SAndreas Gohr return count($matches) > 0 ? $matches : null; 954*e6380ba3SAndreas Gohr } 955*e6380ba3SAndreas Gohr } 956*e6380ba3SAndreas Gohr if ($searchIn->parent === $searchIn) return null; 957*e6380ba3SAndreas Gohr return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen); 958*e6380ba3SAndreas Gohr } 959*e6380ba3SAndreas Gohr 960*e6380ba3SAndreas Gohr /** 961*e6380ba3SAndreas Gohr * sets all argument names in $args to either the default value 962*e6380ba3SAndreas Gohr * or the one passed in through $values 963*e6380ba3SAndreas Gohr * 964*e6380ba3SAndreas Gohr * @throws Exception 965*e6380ba3SAndreas Gohr */ 966*e6380ba3SAndreas Gohr protected function zipSetArgs($args, $orderedValues, $keywordValues) 967*e6380ba3SAndreas Gohr { 968*e6380ba3SAndreas Gohr $assignedValues = []; 969*e6380ba3SAndreas Gohr 970*e6380ba3SAndreas Gohr $i = 0; 971*e6380ba3SAndreas Gohr foreach ($args as $a) { 972*e6380ba3SAndreas Gohr if ($a[0] == 'arg') { 973*e6380ba3SAndreas Gohr if (isset($keywordValues[$a[1]])) { 974*e6380ba3SAndreas Gohr // has keyword arg 975*e6380ba3SAndreas Gohr $value = $keywordValues[$a[1]]; 976*e6380ba3SAndreas Gohr } elseif (isset($orderedValues[$i])) { 977*e6380ba3SAndreas Gohr // has ordered arg 978*e6380ba3SAndreas Gohr $value = $orderedValues[$i]; 979*e6380ba3SAndreas Gohr $i++; 980*e6380ba3SAndreas Gohr } elseif (isset($a[2])) { 981*e6380ba3SAndreas Gohr // has default value 982*e6380ba3SAndreas Gohr $value = $a[2]; 983*e6380ba3SAndreas Gohr } else { 984*e6380ba3SAndreas Gohr throw new Exception('Failed to assign arg ' . $a[1]); 985*e6380ba3SAndreas Gohr } 986*e6380ba3SAndreas Gohr 987*e6380ba3SAndreas Gohr $value = $this->reduce($value); 988*e6380ba3SAndreas Gohr $this->set($a[1], $value); 989*e6380ba3SAndreas Gohr $assignedValues[] = $value; 990*e6380ba3SAndreas Gohr } else { 991*e6380ba3SAndreas Gohr // a lit 992*e6380ba3SAndreas Gohr $i++; 993*e6380ba3SAndreas Gohr } 994*e6380ba3SAndreas Gohr } 995*e6380ba3SAndreas Gohr 996*e6380ba3SAndreas Gohr // check for a rest 997*e6380ba3SAndreas Gohr $last = end($args); 998*e6380ba3SAndreas Gohr if ($last !== false && $last[0] === 'rest') { 999*e6380ba3SAndreas Gohr $rest = array_slice($orderedValues, count($args) - 1); 1000*e6380ba3SAndreas Gohr $this->set($last[1], $this->reduce(['list', ' ', $rest])); 1001*e6380ba3SAndreas Gohr } 1002*e6380ba3SAndreas Gohr 1003*e6380ba3SAndreas Gohr // wow is this the only true use of PHP's + operator for arrays? 1004*e6380ba3SAndreas Gohr $this->env->arguments = $assignedValues + $orderedValues; 1005*e6380ba3SAndreas Gohr } 1006*e6380ba3SAndreas Gohr 1007*e6380ba3SAndreas Gohr /** 1008*e6380ba3SAndreas Gohr * compile a prop and update $lines or $blocks appropriately 1009*e6380ba3SAndreas Gohr * @throws Exception 1010*e6380ba3SAndreas Gohr */ 1011*e6380ba3SAndreas Gohr protected function compileProp($prop, $block, $out) 1012*e6380ba3SAndreas Gohr { 1013*e6380ba3SAndreas Gohr // set error position context 1014*e6380ba3SAndreas Gohr $this->sourceLoc = $prop[-1] ?? -1; 1015*e6380ba3SAndreas Gohr 1016*e6380ba3SAndreas Gohr switch ($prop[0]) { 1017*e6380ba3SAndreas Gohr case 'assign': 1018*e6380ba3SAndreas Gohr [, $name, $value] = $prop; 1019*e6380ba3SAndreas Gohr if ($name[0] == Constants::VPREFIX) { 1020*e6380ba3SAndreas Gohr $this->set($name, $value); 1021*e6380ba3SAndreas Gohr } else { 1022*e6380ba3SAndreas Gohr $out->lines[] = $this->formatter->property( 1023*e6380ba3SAndreas Gohr $name, 1024*e6380ba3SAndreas Gohr $this->compileValue($this->reduce($value)) 1025*e6380ba3SAndreas Gohr ); 1026*e6380ba3SAndreas Gohr } 1027*e6380ba3SAndreas Gohr break; 1028*e6380ba3SAndreas Gohr case 'block': 1029*e6380ba3SAndreas Gohr [, $child] = $prop; 1030*e6380ba3SAndreas Gohr $this->compileBlock($child); 1031*e6380ba3SAndreas Gohr break; 1032*e6380ba3SAndreas Gohr case 'ruleset': 1033*e6380ba3SAndreas Gohr case 'mixin': 1034*e6380ba3SAndreas Gohr [, $path, $args, $suffix] = $prop; 1035*e6380ba3SAndreas Gohr 1036*e6380ba3SAndreas Gohr $orderedArgs = []; 1037*e6380ba3SAndreas Gohr $keywordArgs = []; 1038*e6380ba3SAndreas Gohr foreach ((array)$args as $arg) { 1039*e6380ba3SAndreas Gohr switch ($arg[0]) { 1040*e6380ba3SAndreas Gohr case 'arg': 1041*e6380ba3SAndreas Gohr if (!isset($arg[2])) { 1042*e6380ba3SAndreas Gohr $orderedArgs[] = $this->reduce(['variable', $arg[1]]); 1043*e6380ba3SAndreas Gohr } else { 1044*e6380ba3SAndreas Gohr $keywordArgs[$arg[1]] = $this->reduce($arg[2]); 1045*e6380ba3SAndreas Gohr } 1046*e6380ba3SAndreas Gohr break; 1047*e6380ba3SAndreas Gohr 1048*e6380ba3SAndreas Gohr case 'lit': 1049*e6380ba3SAndreas Gohr $orderedArgs[] = $this->reduce($arg[1]); 1050*e6380ba3SAndreas Gohr break; 1051*e6380ba3SAndreas Gohr default: 1052*e6380ba3SAndreas Gohr throw new Exception('Unknown arg type: ' . $arg[0]); 1053*e6380ba3SAndreas Gohr } 1054*e6380ba3SAndreas Gohr } 1055*e6380ba3SAndreas Gohr 1056*e6380ba3SAndreas Gohr $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); 1057*e6380ba3SAndreas Gohr 1058*e6380ba3SAndreas Gohr if ($mixins === null) { 1059*e6380ba3SAndreas Gohr $block->parser->throwError("{$prop[1][0]} is undefined", $block->count); 1060*e6380ba3SAndreas Gohr } 1061*e6380ba3SAndreas Gohr 1062*e6380ba3SAndreas Gohr if (strpos($prop[1][0], "$") === 0) { 1063*e6380ba3SAndreas Gohr //Use Ruleset Logic - Only last element 1064*e6380ba3SAndreas Gohr $mixins = [array_pop($mixins)]; 1065*e6380ba3SAndreas Gohr } 1066*e6380ba3SAndreas Gohr 1067*e6380ba3SAndreas Gohr foreach ($mixins as $mixin) { 1068*e6380ba3SAndreas Gohr if ($mixin === $block && !$orderedArgs) { 1069*e6380ba3SAndreas Gohr continue; 1070*e6380ba3SAndreas Gohr } 1071*e6380ba3SAndreas Gohr 1072*e6380ba3SAndreas Gohr $haveScope = false; 1073*e6380ba3SAndreas Gohr if (isset($mixin->parent->scope)) { 1074*e6380ba3SAndreas Gohr $haveScope = true; 1075*e6380ba3SAndreas Gohr $mixinParentEnv = $this->pushEnv(); 1076*e6380ba3SAndreas Gohr $mixinParentEnv->storeParent = $mixin->parent->scope; 1077*e6380ba3SAndreas Gohr } 1078*e6380ba3SAndreas Gohr 1079*e6380ba3SAndreas Gohr $haveArgs = false; 1080*e6380ba3SAndreas Gohr if (isset($mixin->args)) { 1081*e6380ba3SAndreas Gohr $haveArgs = true; 1082*e6380ba3SAndreas Gohr $this->pushEnv(); 1083*e6380ba3SAndreas Gohr $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs); 1084*e6380ba3SAndreas Gohr } 1085*e6380ba3SAndreas Gohr 1086*e6380ba3SAndreas Gohr $oldParent = $mixin->parent; 1087*e6380ba3SAndreas Gohr if ($mixin != $block) $mixin->parent = $block; 1088*e6380ba3SAndreas Gohr 1089*e6380ba3SAndreas Gohr foreach ($this->sortProps($mixin->props) as $subProp) { 1090*e6380ba3SAndreas Gohr if ($suffix !== null && 1091*e6380ba3SAndreas Gohr $subProp[0] == 'assign' && 1092*e6380ba3SAndreas Gohr is_string($subProp[1]) && 1093*e6380ba3SAndreas Gohr $subProp[1][0] != Constants::VPREFIX) { 1094*e6380ba3SAndreas Gohr $subProp[2] = ['list', ' ', [$subProp[2], ['keyword', $suffix]]]; 1095*e6380ba3SAndreas Gohr } 1096*e6380ba3SAndreas Gohr 1097*e6380ba3SAndreas Gohr $this->compileProp($subProp, $mixin, $out); 1098*e6380ba3SAndreas Gohr } 1099*e6380ba3SAndreas Gohr 1100*e6380ba3SAndreas Gohr $mixin->parent = $oldParent; 1101*e6380ba3SAndreas Gohr 1102*e6380ba3SAndreas Gohr if ($haveArgs) $this->popEnv(); 1103*e6380ba3SAndreas Gohr if ($haveScope) $this->popEnv(); 1104*e6380ba3SAndreas Gohr } 1105*e6380ba3SAndreas Gohr 1106*e6380ba3SAndreas Gohr break; 1107*e6380ba3SAndreas Gohr case 'raw': 1108*e6380ba3SAndreas Gohr case 'comment': 1109*e6380ba3SAndreas Gohr $out->lines[] = $prop[1]; 1110*e6380ba3SAndreas Gohr break; 1111*e6380ba3SAndreas Gohr case 'directive': 1112*e6380ba3SAndreas Gohr [, $name, $value] = $prop; 1113*e6380ba3SAndreas Gohr $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)) . ';'; 1114*e6380ba3SAndreas Gohr break; 1115*e6380ba3SAndreas Gohr case 'import': 1116*e6380ba3SAndreas Gohr [, $importPath, $importId] = $prop; 1117*e6380ba3SAndreas Gohr $importPath = $this->reduce($importPath); 1118*e6380ba3SAndreas Gohr 1119*e6380ba3SAndreas Gohr if (!isset($this->env->imports)) { 1120*e6380ba3SAndreas Gohr $this->env->imports = []; 1121*e6380ba3SAndreas Gohr } 1122*e6380ba3SAndreas Gohr 1123*e6380ba3SAndreas Gohr $result = $this->tryImport($importPath, $block, $out); 1124*e6380ba3SAndreas Gohr 1125*e6380ba3SAndreas Gohr $this->env->imports[$importId] = $result === false ? 1126*e6380ba3SAndreas Gohr [false, '@import ' . $this->compileValue($importPath) . ';'] : 1127*e6380ba3SAndreas Gohr $result; 1128*e6380ba3SAndreas Gohr 1129*e6380ba3SAndreas Gohr break; 1130*e6380ba3SAndreas Gohr case 'import_mixin': 1131*e6380ba3SAndreas Gohr [, $importId] = $prop; 1132*e6380ba3SAndreas Gohr $import = $this->env->imports[$importId]; 1133*e6380ba3SAndreas Gohr if ($import[0] === false) { 1134*e6380ba3SAndreas Gohr if (isset($import[1])) { 1135*e6380ba3SAndreas Gohr $out->lines[] = $import[1]; 1136*e6380ba3SAndreas Gohr } 1137*e6380ba3SAndreas Gohr } else { 1138*e6380ba3SAndreas Gohr [, $bottom, $parser, $importDir] = $import; 1139*e6380ba3SAndreas Gohr $this->compileImportedProps($bottom, $block, $out, $parser, $importDir); 1140*e6380ba3SAndreas Gohr } 1141*e6380ba3SAndreas Gohr 1142*e6380ba3SAndreas Gohr break; 1143*e6380ba3SAndreas Gohr default: 1144*e6380ba3SAndreas Gohr $block->parser->throwError("unknown op: $prop[0]\n", $block->count); 1145*e6380ba3SAndreas Gohr } 1146*e6380ba3SAndreas Gohr } 1147*e6380ba3SAndreas Gohr 1148*e6380ba3SAndreas Gohr 1149*e6380ba3SAndreas Gohr /** 1150*e6380ba3SAndreas Gohr * Compiles a primitive value into a CSS property value. 1151*e6380ba3SAndreas Gohr * 1152*e6380ba3SAndreas Gohr * Values in lessphp are typed by being wrapped in arrays, their format is 1153*e6380ba3SAndreas Gohr * typically: 1154*e6380ba3SAndreas Gohr * 1155*e6380ba3SAndreas Gohr * array(type, contents [, additional_contents]*) 1156*e6380ba3SAndreas Gohr * 1157*e6380ba3SAndreas Gohr * The input is expected to be reduced. This function will not work on 1158*e6380ba3SAndreas Gohr * things like expressions and variables. 1159*e6380ba3SAndreas Gohr * @throws Exception 1160*e6380ba3SAndreas Gohr * @internal parser internal method 1161*e6380ba3SAndreas Gohr */ 1162*e6380ba3SAndreas Gohr public function compileValue($value) 1163*e6380ba3SAndreas Gohr { 1164*e6380ba3SAndreas Gohr switch ($value[0]) { 1165*e6380ba3SAndreas Gohr case 'list': 1166*e6380ba3SAndreas Gohr // [1] - delimiter 1167*e6380ba3SAndreas Gohr // [2] - array of values 1168*e6380ba3SAndreas Gohr return implode($value[1], array_map([$this, 'compileValue'], $value[2])); 1169*e6380ba3SAndreas Gohr case 'raw_color': 1170*e6380ba3SAndreas Gohr if (!empty($this->formatter->compressColors)) { 1171*e6380ba3SAndreas Gohr return $this->compileValue(Color::coerceColor($value)); 1172*e6380ba3SAndreas Gohr } 1173*e6380ba3SAndreas Gohr return $value[1]; 1174*e6380ba3SAndreas Gohr case 'keyword': 1175*e6380ba3SAndreas Gohr // [1] - the keyword 1176*e6380ba3SAndreas Gohr return $value[1]; 1177*e6380ba3SAndreas Gohr case 'number': 1178*e6380ba3SAndreas Gohr [, $num, $unit] = $value; 1179*e6380ba3SAndreas Gohr // [1] - the number 1180*e6380ba3SAndreas Gohr // [2] - the unit 1181*e6380ba3SAndreas Gohr if ($this->numberPrecision !== null) { 1182*e6380ba3SAndreas Gohr $num = round($num, $this->numberPrecision); 1183*e6380ba3SAndreas Gohr } 1184*e6380ba3SAndreas Gohr return $num . $unit; 1185*e6380ba3SAndreas Gohr case 'string': 1186*e6380ba3SAndreas Gohr // [1] - contents of string (includes quotes) 1187*e6380ba3SAndreas Gohr [, $delim, $content] = $value; 1188*e6380ba3SAndreas Gohr foreach ($content as &$part) { 1189*e6380ba3SAndreas Gohr if (is_array($part)) { 1190*e6380ba3SAndreas Gohr $part = $this->compileValue($part); 1191*e6380ba3SAndreas Gohr } 1192*e6380ba3SAndreas Gohr } 1193*e6380ba3SAndreas Gohr return $delim . implode($content) . $delim; 1194*e6380ba3SAndreas Gohr case 'color': 1195*e6380ba3SAndreas Gohr // [1] - red component (either number or a %) 1196*e6380ba3SAndreas Gohr // [2] - green component 1197*e6380ba3SAndreas Gohr // [3] - blue component 1198*e6380ba3SAndreas Gohr // [4] - optional alpha component 1199*e6380ba3SAndreas Gohr [, $r, $g, $b] = $value; 1200*e6380ba3SAndreas Gohr $r = round($r); 1201*e6380ba3SAndreas Gohr $g = round($g); 1202*e6380ba3SAndreas Gohr $b = round($b); 1203*e6380ba3SAndreas Gohr 1204*e6380ba3SAndreas Gohr if (count($value) == 5 && $value[4] != 1) { // rgba 1205*e6380ba3SAndreas Gohr return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $value[4] . ')'; 1206*e6380ba3SAndreas Gohr } 1207*e6380ba3SAndreas Gohr 1208*e6380ba3SAndreas Gohr $h = sprintf('#%02x%02x%02x', $r, $g, $b); 1209*e6380ba3SAndreas Gohr 1210*e6380ba3SAndreas Gohr if (!empty($this->formatter->compressColors)) { 1211*e6380ba3SAndreas Gohr // Converting hex color to short notation (e.g. #003399 to #039) 1212*e6380ba3SAndreas Gohr if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { 1213*e6380ba3SAndreas Gohr $h = '#' . $h[1] . $h[3] . $h[5]; 1214*e6380ba3SAndreas Gohr } 1215*e6380ba3SAndreas Gohr } 1216*e6380ba3SAndreas Gohr 1217*e6380ba3SAndreas Gohr return $h; 1218*e6380ba3SAndreas Gohr 1219*e6380ba3SAndreas Gohr case 'function': 1220*e6380ba3SAndreas Gohr [, $name, $args] = $value; 1221*e6380ba3SAndreas Gohr return $name . '(' . $this->compileValue($args) . ')'; 1222*e6380ba3SAndreas Gohr default: // assumed to be unit 1223*e6380ba3SAndreas Gohr throw new Exception("unknown value type: $value[0]"); 1224*e6380ba3SAndreas Gohr } 1225*e6380ba3SAndreas Gohr } 1226*e6380ba3SAndreas Gohr 1227*e6380ba3SAndreas Gohr /** 1228*e6380ba3SAndreas Gohr * Utility func to unquote a string 1229*e6380ba3SAndreas Gohr * 1230*e6380ba3SAndreas Gohr * @todo this not really a good name for this function 1231*e6380ba3SAndreas Gohr * @internal parser internal method 1232*e6380ba3SAndreas Gohr */ 1233*e6380ba3SAndreas Gohr public function unwrap(array $arg): array 1234*e6380ba3SAndreas Gohr { 1235*e6380ba3SAndreas Gohr switch ($arg[0]) { 1236*e6380ba3SAndreas Gohr case 'list': 1237*e6380ba3SAndreas Gohr $items = $arg[2]; 1238*e6380ba3SAndreas Gohr if (isset($items[0])) { 1239*e6380ba3SAndreas Gohr return self::unwrap($items[0]); 1240*e6380ba3SAndreas Gohr } 1241*e6380ba3SAndreas Gohr throw new Exception('unrecognised input'); 1242*e6380ba3SAndreas Gohr case 'string': 1243*e6380ba3SAndreas Gohr $arg[1] = ''; 1244*e6380ba3SAndreas Gohr return $arg; 1245*e6380ba3SAndreas Gohr case 'keyword': 1246*e6380ba3SAndreas Gohr return $arg; 1247*e6380ba3SAndreas Gohr default: 1248*e6380ba3SAndreas Gohr return ['keyword', $this->compileValue($arg)]; 1249*e6380ba3SAndreas Gohr } 1250*e6380ba3SAndreas Gohr } 1251*e6380ba3SAndreas Gohr 1252*e6380ba3SAndreas Gohr /** 1253*e6380ba3SAndreas Gohr * Convert the rgb, rgba, hsl color literals of function type 1254*e6380ba3SAndreas Gohr * as returned by the parser into values of color type. 1255*e6380ba3SAndreas Gohr * 1256*e6380ba3SAndreas Gohr * @throws Exception 1257*e6380ba3SAndreas Gohr */ 1258*e6380ba3SAndreas Gohr protected function funcToColor($func) 1259*e6380ba3SAndreas Gohr { 1260*e6380ba3SAndreas Gohr $fname = $func[1]; 1261*e6380ba3SAndreas Gohr if ($func[2][0] != 'list') return false; // need a list of arguments 1262*e6380ba3SAndreas Gohr $rawComponents = $func[2][2]; 1263*e6380ba3SAndreas Gohr 1264*e6380ba3SAndreas Gohr if ($fname == 'hsl' || $fname == 'hsla') { 1265*e6380ba3SAndreas Gohr $hsl = ['hsl']; 1266*e6380ba3SAndreas Gohr $i = 0; 1267*e6380ba3SAndreas Gohr foreach ($rawComponents as $c) { 1268*e6380ba3SAndreas Gohr $val = $this->reduce($c); 1269*e6380ba3SAndreas Gohr $val = isset($val[1]) ? floatval($val[1]) : 0; 1270*e6380ba3SAndreas Gohr 1271*e6380ba3SAndreas Gohr if ($i == 0) $clamp = 360; 1272*e6380ba3SAndreas Gohr elseif ($i < 3) $clamp = 100; 1273*e6380ba3SAndreas Gohr else $clamp = 1; 1274*e6380ba3SAndreas Gohr 1275*e6380ba3SAndreas Gohr $hsl[] = Util::clamp($val, $clamp); 1276*e6380ba3SAndreas Gohr $i++; 1277*e6380ba3SAndreas Gohr } 1278*e6380ba3SAndreas Gohr 1279*e6380ba3SAndreas Gohr while (count($hsl) < 4) $hsl[] = 0; 1280*e6380ba3SAndreas Gohr return Color::toRGB($hsl); 1281*e6380ba3SAndreas Gohr } elseif ($fname == 'rgb' || $fname == 'rgba') { 1282*e6380ba3SAndreas Gohr $components = []; 1283*e6380ba3SAndreas Gohr $i = 1; 1284*e6380ba3SAndreas Gohr foreach ($rawComponents as $c) { 1285*e6380ba3SAndreas Gohr $c = $this->reduce($c); 1286*e6380ba3SAndreas Gohr if ($i < 4) { 1287*e6380ba3SAndreas Gohr if ($c[0] == 'number' && $c[2] == '%') { 1288*e6380ba3SAndreas Gohr $components[] = 255 * ($c[1] / 100); 1289*e6380ba3SAndreas Gohr } else { 1290*e6380ba3SAndreas Gohr $components[] = floatval($c[1]); 1291*e6380ba3SAndreas Gohr } 1292*e6380ba3SAndreas Gohr } elseif ($i == 4) { 1293*e6380ba3SAndreas Gohr if ($c[0] == 'number' && $c[2] == '%') { 1294*e6380ba3SAndreas Gohr $components[] = 1.0 * ($c[1] / 100); 1295*e6380ba3SAndreas Gohr } else { 1296*e6380ba3SAndreas Gohr $components[] = floatval($c[1]); 1297*e6380ba3SAndreas Gohr } 1298*e6380ba3SAndreas Gohr } else break; 1299*e6380ba3SAndreas Gohr 1300*e6380ba3SAndreas Gohr $i++; 1301*e6380ba3SAndreas Gohr } 1302*e6380ba3SAndreas Gohr while (count($components) < 3) $components[] = 0; 1303*e6380ba3SAndreas Gohr array_unshift($components, 'color'); 1304*e6380ba3SAndreas Gohr return Color::fixColor($components); 1305*e6380ba3SAndreas Gohr } 1306*e6380ba3SAndreas Gohr 1307*e6380ba3SAndreas Gohr return false; 1308*e6380ba3SAndreas Gohr } 1309*e6380ba3SAndreas Gohr 1310*e6380ba3SAndreas Gohr /** 1311*e6380ba3SAndreas Gohr * @throws Exception 1312*e6380ba3SAndreas Gohr * @internal parser internal method 1313*e6380ba3SAndreas Gohr */ 1314*e6380ba3SAndreas Gohr public function reduce($value, $forExpression = false) 1315*e6380ba3SAndreas Gohr { 1316*e6380ba3SAndreas Gohr switch ($value[0]) { 1317*e6380ba3SAndreas Gohr case 'interpolate': 1318*e6380ba3SAndreas Gohr $reduced = $this->reduce($value[1]); 1319*e6380ba3SAndreas Gohr $var = $this->compileValue($reduced); 1320*e6380ba3SAndreas Gohr $res = $this->reduce(['variable', Constants::VPREFIX . $var]); 1321*e6380ba3SAndreas Gohr 1322*e6380ba3SAndreas Gohr if ($res[0] == 'raw_color') { 1323*e6380ba3SAndreas Gohr $res = Color::coerceColor($res); 1324*e6380ba3SAndreas Gohr } 1325*e6380ba3SAndreas Gohr 1326*e6380ba3SAndreas Gohr if (empty($value[2])) $res = $this->unwrap($res); 1327*e6380ba3SAndreas Gohr 1328*e6380ba3SAndreas Gohr return $res; 1329*e6380ba3SAndreas Gohr case 'variable': 1330*e6380ba3SAndreas Gohr $key = $value[1]; 1331*e6380ba3SAndreas Gohr if (is_array($key)) { 1332*e6380ba3SAndreas Gohr $key = $this->reduce($key); 1333*e6380ba3SAndreas Gohr $key = Constants::VPREFIX . $this->compileValue($this->unwrap($key)); 1334*e6380ba3SAndreas Gohr } 1335*e6380ba3SAndreas Gohr 1336*e6380ba3SAndreas Gohr $seen =& $this->env->seenNames; 1337*e6380ba3SAndreas Gohr 1338*e6380ba3SAndreas Gohr if (!empty($seen[$key])) { 1339*e6380ba3SAndreas Gohr throw new Exception("infinite loop detected: $key"); 1340*e6380ba3SAndreas Gohr } 1341*e6380ba3SAndreas Gohr 1342*e6380ba3SAndreas Gohr $seen[$key] = true; 1343*e6380ba3SAndreas Gohr $out = $this->reduce($this->get($key)); 1344*e6380ba3SAndreas Gohr $seen[$key] = false; 1345*e6380ba3SAndreas Gohr return $out; 1346*e6380ba3SAndreas Gohr case 'list': 1347*e6380ba3SAndreas Gohr foreach ($value[2] as &$item) { 1348*e6380ba3SAndreas Gohr $item = $this->reduce($item, $forExpression); 1349*e6380ba3SAndreas Gohr } 1350*e6380ba3SAndreas Gohr return $value; 1351*e6380ba3SAndreas Gohr case 'expression': 1352*e6380ba3SAndreas Gohr return $this->evaluate($value); 1353*e6380ba3SAndreas Gohr case 'string': 1354*e6380ba3SAndreas Gohr foreach ($value[2] as &$part) { 1355*e6380ba3SAndreas Gohr if (is_array($part)) { 1356*e6380ba3SAndreas Gohr $strip = $part[0] == 'variable'; 1357*e6380ba3SAndreas Gohr $part = $this->reduce($part); 1358*e6380ba3SAndreas Gohr if ($strip) $part = $this->unwrap($part); 1359*e6380ba3SAndreas Gohr } 1360*e6380ba3SAndreas Gohr } 1361*e6380ba3SAndreas Gohr return $value; 1362*e6380ba3SAndreas Gohr case 'escape': 1363*e6380ba3SAndreas Gohr [, $inner] = $value; 1364*e6380ba3SAndreas Gohr return $this->unwrap($this->reduce($inner)); 1365*e6380ba3SAndreas Gohr case 'function': 1366*e6380ba3SAndreas Gohr $color = $this->funcToColor($value); 1367*e6380ba3SAndreas Gohr if ($color) return $color; 1368*e6380ba3SAndreas Gohr 1369*e6380ba3SAndreas Gohr [, $name, $args] = $value; 1370*e6380ba3SAndreas Gohr 1371*e6380ba3SAndreas Gohr $f = $this->libFunctions[$name] ?? null; 1372*e6380ba3SAndreas Gohr 1373*e6380ba3SAndreas Gohr if (is_callable($f)) { 1374*e6380ba3SAndreas Gohr if ($args[0] == 'list') 1375*e6380ba3SAndreas Gohr $args = self::compressList($args[2], $args[1]); 1376*e6380ba3SAndreas Gohr 1377*e6380ba3SAndreas Gohr $ret = call_user_func($f, $this->reduce($args, true), $this); 1378*e6380ba3SAndreas Gohr 1379*e6380ba3SAndreas Gohr if (is_null($ret)) { 1380*e6380ba3SAndreas Gohr return ['string', '', [$name, '(', $args, ')']]; 1381*e6380ba3SAndreas Gohr } 1382*e6380ba3SAndreas Gohr 1383*e6380ba3SAndreas Gohr // convert to a typed value if the result is a php primitive 1384*e6380ba3SAndreas Gohr if (is_numeric($ret)) $ret = ['number', $ret, '']; 1385*e6380ba3SAndreas Gohr elseif (!is_array($ret)) $ret = ['keyword', $ret]; 1386*e6380ba3SAndreas Gohr 1387*e6380ba3SAndreas Gohr return $ret; 1388*e6380ba3SAndreas Gohr } 1389*e6380ba3SAndreas Gohr 1390*e6380ba3SAndreas Gohr // plain function, reduce args 1391*e6380ba3SAndreas Gohr $value[2] = $this->reduce($value[2]); 1392*e6380ba3SAndreas Gohr return $value; 1393*e6380ba3SAndreas Gohr case 'unary': 1394*e6380ba3SAndreas Gohr [, $op, $exp] = $value; 1395*e6380ba3SAndreas Gohr $exp = $this->reduce($exp); 1396*e6380ba3SAndreas Gohr 1397*e6380ba3SAndreas Gohr if ($exp[0] == 'number') { 1398*e6380ba3SAndreas Gohr switch ($op) { 1399*e6380ba3SAndreas Gohr case '+': 1400*e6380ba3SAndreas Gohr return $exp; 1401*e6380ba3SAndreas Gohr case '-': 1402*e6380ba3SAndreas Gohr $exp[1] *= -1; 1403*e6380ba3SAndreas Gohr return $exp; 1404*e6380ba3SAndreas Gohr } 1405*e6380ba3SAndreas Gohr } 1406*e6380ba3SAndreas Gohr return ['string', '', [$op, $exp]]; 1407*e6380ba3SAndreas Gohr } 1408*e6380ba3SAndreas Gohr 1409*e6380ba3SAndreas Gohr if ($forExpression) { 1410*e6380ba3SAndreas Gohr switch ($value[0]) { 1411*e6380ba3SAndreas Gohr case 'keyword': 1412*e6380ba3SAndreas Gohr if ($color = Color::coerceColor($value)) { 1413*e6380ba3SAndreas Gohr return $color; 1414*e6380ba3SAndreas Gohr } 1415*e6380ba3SAndreas Gohr break; 1416*e6380ba3SAndreas Gohr case 'raw_color': 1417*e6380ba3SAndreas Gohr return Color::coerceColor($value); 1418*e6380ba3SAndreas Gohr } 1419*e6380ba3SAndreas Gohr } 1420*e6380ba3SAndreas Gohr 1421*e6380ba3SAndreas Gohr return $value; 1422*e6380ba3SAndreas Gohr } 1423*e6380ba3SAndreas Gohr 1424*e6380ba3SAndreas Gohr 1425*e6380ba3SAndreas Gohr // make something string like into a string 1426*e6380ba3SAndreas Gohr protected function coerceString($value) 1427*e6380ba3SAndreas Gohr { 1428*e6380ba3SAndreas Gohr switch ($value[0]) { 1429*e6380ba3SAndreas Gohr case 'string': 1430*e6380ba3SAndreas Gohr return $value; 1431*e6380ba3SAndreas Gohr case 'keyword': 1432*e6380ba3SAndreas Gohr return ['string', '', [$value[1]]]; 1433*e6380ba3SAndreas Gohr } 1434*e6380ba3SAndreas Gohr return null; 1435*e6380ba3SAndreas Gohr } 1436*e6380ba3SAndreas Gohr 1437*e6380ba3SAndreas Gohr // turn list of length 1 into value type 1438*e6380ba3SAndreas Gohr protected function flattenList($value) 1439*e6380ba3SAndreas Gohr { 1440*e6380ba3SAndreas Gohr if ($value[0] == 'list' && count($value[2]) == 1) { 1441*e6380ba3SAndreas Gohr return $this->flattenList($value[2][0]); 1442*e6380ba3SAndreas Gohr } 1443*e6380ba3SAndreas Gohr return $value; 1444*e6380ba3SAndreas Gohr } 1445*e6380ba3SAndreas Gohr 1446*e6380ba3SAndreas Gohr /** 1447*e6380ba3SAndreas Gohr * evaluate an expression 1448*e6380ba3SAndreas Gohr * @throws Exception 1449*e6380ba3SAndreas Gohr */ 1450*e6380ba3SAndreas Gohr protected function evaluate($exp) 1451*e6380ba3SAndreas Gohr { 1452*e6380ba3SAndreas Gohr [, $op, $left, $right, $whiteBefore, $whiteAfter] = $exp; 1453*e6380ba3SAndreas Gohr 1454*e6380ba3SAndreas Gohr $left = $this->reduce($left, true); 1455*e6380ba3SAndreas Gohr $right = $this->reduce($right, true); 1456*e6380ba3SAndreas Gohr 1457*e6380ba3SAndreas Gohr if ($leftColor = Color::coerceColor($left)) { 1458*e6380ba3SAndreas Gohr $left = $leftColor; 1459*e6380ba3SAndreas Gohr } 1460*e6380ba3SAndreas Gohr 1461*e6380ba3SAndreas Gohr if ($rightColor = Color::coerceColor($right)) { 1462*e6380ba3SAndreas Gohr $right = $rightColor; 1463*e6380ba3SAndreas Gohr } 1464*e6380ba3SAndreas Gohr 1465*e6380ba3SAndreas Gohr $ltype = $left[0]; 1466*e6380ba3SAndreas Gohr $rtype = $right[0]; 1467*e6380ba3SAndreas Gohr 1468*e6380ba3SAndreas Gohr // operators that work on all types 1469*e6380ba3SAndreas Gohr if ($op == 'and') { 1470*e6380ba3SAndreas Gohr return Util::toBool($left == Constants::TRUE && $right == Constants::TRUE); 1471*e6380ba3SAndreas Gohr } 1472*e6380ba3SAndreas Gohr 1473*e6380ba3SAndreas Gohr if ($op == '=') { 1474*e6380ba3SAndreas Gohr return Util::toBool($this->eq($left, $right)); 1475*e6380ba3SAndreas Gohr } 1476*e6380ba3SAndreas Gohr 1477*e6380ba3SAndreas Gohr if ($op == '+' && !is_null($str = $this->stringConcatenate($left, $right))) { 1478*e6380ba3SAndreas Gohr return $str; 1479*e6380ba3SAndreas Gohr } 1480*e6380ba3SAndreas Gohr 1481*e6380ba3SAndreas Gohr // type based operators 1482*e6380ba3SAndreas Gohr $fname = sprintf('op_%s_%s', $ltype, $rtype); 1483*e6380ba3SAndreas Gohr if (is_callable([$this, $fname])) { 1484*e6380ba3SAndreas Gohr $out = $this->$fname($op, $left, $right); 1485*e6380ba3SAndreas Gohr if (!is_null($out)) return $out; 1486*e6380ba3SAndreas Gohr } 1487*e6380ba3SAndreas Gohr 1488*e6380ba3SAndreas Gohr // make the expression look it did before being parsed 1489*e6380ba3SAndreas Gohr $paddedOp = $op; 1490*e6380ba3SAndreas Gohr if ($whiteBefore) $paddedOp = ' ' . $paddedOp; 1491*e6380ba3SAndreas Gohr if ($whiteAfter) $paddedOp .= ' '; 1492*e6380ba3SAndreas Gohr 1493*e6380ba3SAndreas Gohr return ['string', '', [$left, $paddedOp, $right]]; 1494*e6380ba3SAndreas Gohr } 1495*e6380ba3SAndreas Gohr 1496*e6380ba3SAndreas Gohr protected function stringConcatenate($left, $right) 1497*e6380ba3SAndreas Gohr { 1498*e6380ba3SAndreas Gohr if ($strLeft = $this->coerceString($left)) { 1499*e6380ba3SAndreas Gohr if ($right[0] == 'string') { 1500*e6380ba3SAndreas Gohr $right[1] = ''; 1501*e6380ba3SAndreas Gohr } 1502*e6380ba3SAndreas Gohr $strLeft[2][] = $right; 1503*e6380ba3SAndreas Gohr return $strLeft; 1504*e6380ba3SAndreas Gohr } 1505*e6380ba3SAndreas Gohr 1506*e6380ba3SAndreas Gohr if ($strRight = $this->coerceString($right)) { 1507*e6380ba3SAndreas Gohr array_unshift($strRight[2], $left); 1508*e6380ba3SAndreas Gohr return $strRight; 1509*e6380ba3SAndreas Gohr } 1510*e6380ba3SAndreas Gohr } 1511*e6380ba3SAndreas Gohr 1512*e6380ba3SAndreas Gohr 1513*e6380ba3SAndreas Gohr /** 1514*e6380ba3SAndreas Gohr * @throws Exception 1515*e6380ba3SAndreas Gohr */ 1516*e6380ba3SAndreas Gohr protected function op_number_color($op, $lft, $rgt) 1517*e6380ba3SAndreas Gohr { 1518*e6380ba3SAndreas Gohr if ($op == '+' || $op == '*') { 1519*e6380ba3SAndreas Gohr return $this->op_color_number($op, $rgt, $lft); 1520*e6380ba3SAndreas Gohr } 1521*e6380ba3SAndreas Gohr } 1522*e6380ba3SAndreas Gohr 1523*e6380ba3SAndreas Gohr /** 1524*e6380ba3SAndreas Gohr * @throws Exception 1525*e6380ba3SAndreas Gohr */ 1526*e6380ba3SAndreas Gohr protected function op_color_number($op, $lft, $rgt) 1527*e6380ba3SAndreas Gohr { 1528*e6380ba3SAndreas Gohr if ($rgt[0] == '%') $rgt[1] /= 100; 1529*e6380ba3SAndreas Gohr 1530*e6380ba3SAndreas Gohr return $this->op_color_color( 1531*e6380ba3SAndreas Gohr $op, 1532*e6380ba3SAndreas Gohr $lft, 1533*e6380ba3SAndreas Gohr array_fill(1, count($lft) - 1, $rgt[1]) 1534*e6380ba3SAndreas Gohr ); 1535*e6380ba3SAndreas Gohr } 1536*e6380ba3SAndreas Gohr 1537*e6380ba3SAndreas Gohr /** 1538*e6380ba3SAndreas Gohr * @throws Exception 1539*e6380ba3SAndreas Gohr */ 1540*e6380ba3SAndreas Gohr protected function op_color_color($op, $left, $right) 1541*e6380ba3SAndreas Gohr { 1542*e6380ba3SAndreas Gohr $out = ['color']; 1543*e6380ba3SAndreas Gohr $max = count($left) > count($right) ? count($left) : count($right); 1544*e6380ba3SAndreas Gohr foreach (range(1, $max - 1) as $i) { 1545*e6380ba3SAndreas Gohr $lval = $left[$i] ?? 0; 1546*e6380ba3SAndreas Gohr $rval = $right[$i] ?? 0; 1547*e6380ba3SAndreas Gohr switch ($op) { 1548*e6380ba3SAndreas Gohr case '+': 1549*e6380ba3SAndreas Gohr $out[] = $lval + $rval; 1550*e6380ba3SAndreas Gohr break; 1551*e6380ba3SAndreas Gohr case '-': 1552*e6380ba3SAndreas Gohr $out[] = $lval - $rval; 1553*e6380ba3SAndreas Gohr break; 1554*e6380ba3SAndreas Gohr case '*': 1555*e6380ba3SAndreas Gohr $out[] = $lval * $rval; 1556*e6380ba3SAndreas Gohr break; 1557*e6380ba3SAndreas Gohr case '%': 1558*e6380ba3SAndreas Gohr $out[] = $lval % $rval; 1559*e6380ba3SAndreas Gohr break; 1560*e6380ba3SAndreas Gohr case '/': 1561*e6380ba3SAndreas Gohr if ($rval == 0) throw new Exception("evaluate error: can't divide by zero"); 1562*e6380ba3SAndreas Gohr $out[] = $lval / $rval; 1563*e6380ba3SAndreas Gohr break; 1564*e6380ba3SAndreas Gohr default: 1565*e6380ba3SAndreas Gohr throw new Exception('evaluate error: color op number failed on op ' . $op); 1566*e6380ba3SAndreas Gohr } 1567*e6380ba3SAndreas Gohr } 1568*e6380ba3SAndreas Gohr return Color::fixColor($out); 1569*e6380ba3SAndreas Gohr } 1570*e6380ba3SAndreas Gohr 1571*e6380ba3SAndreas Gohr 1572*e6380ba3SAndreas Gohr /** 1573*e6380ba3SAndreas Gohr * operator on two numbers 1574*e6380ba3SAndreas Gohr * @throws Exception 1575*e6380ba3SAndreas Gohr */ 1576*e6380ba3SAndreas Gohr protected function op_number_number($op, $left, $right) 1577*e6380ba3SAndreas Gohr { 1578*e6380ba3SAndreas Gohr $unit = empty($left[2]) ? $right[2] : $left[2]; 1579*e6380ba3SAndreas Gohr 1580*e6380ba3SAndreas Gohr $value = 0; 1581*e6380ba3SAndreas Gohr switch ($op) { 1582*e6380ba3SAndreas Gohr case '+': 1583*e6380ba3SAndreas Gohr $value = $left[1] + $right[1]; 1584*e6380ba3SAndreas Gohr break; 1585*e6380ba3SAndreas Gohr case '*': 1586*e6380ba3SAndreas Gohr $value = $left[1] * $right[1]; 1587*e6380ba3SAndreas Gohr break; 1588*e6380ba3SAndreas Gohr case '-': 1589*e6380ba3SAndreas Gohr $value = $left[1] - $right[1]; 1590*e6380ba3SAndreas Gohr break; 1591*e6380ba3SAndreas Gohr case '%': 1592*e6380ba3SAndreas Gohr $value = $left[1] % $right[1]; 1593*e6380ba3SAndreas Gohr break; 1594*e6380ba3SAndreas Gohr case '/': 1595*e6380ba3SAndreas Gohr if ($right[1] == 0) throw new Exception('parse error: divide by zero'); 1596*e6380ba3SAndreas Gohr $value = $left[1] / $right[1]; 1597*e6380ba3SAndreas Gohr break; 1598*e6380ba3SAndreas Gohr case '<': 1599*e6380ba3SAndreas Gohr return Util::toBool($left[1] < $right[1]); 1600*e6380ba3SAndreas Gohr case '>': 1601*e6380ba3SAndreas Gohr return Util::toBool($left[1] > $right[1]); 1602*e6380ba3SAndreas Gohr case '>=': 1603*e6380ba3SAndreas Gohr return Util::toBool($left[1] >= $right[1]); 1604*e6380ba3SAndreas Gohr case '=<': 1605*e6380ba3SAndreas Gohr return Util::toBool($left[1] <= $right[1]); 1606*e6380ba3SAndreas Gohr default: 1607*e6380ba3SAndreas Gohr throw new Exception('parse error: unknown number operator: ' . $op); 1608*e6380ba3SAndreas Gohr } 1609*e6380ba3SAndreas Gohr 1610*e6380ba3SAndreas Gohr return ['number', $value, $unit]; 1611*e6380ba3SAndreas Gohr } 1612*e6380ba3SAndreas Gohr 1613*e6380ba3SAndreas Gohr 1614*e6380ba3SAndreas Gohr /* environment functions */ 1615*e6380ba3SAndreas Gohr 1616*e6380ba3SAndreas Gohr protected function makeOutputBlock($type, $selectors = null) 1617*e6380ba3SAndreas Gohr { 1618*e6380ba3SAndreas Gohr $b = new stdclass; 1619*e6380ba3SAndreas Gohr $b->lines = []; 1620*e6380ba3SAndreas Gohr $b->children = []; 1621*e6380ba3SAndreas Gohr $b->selectors = $selectors; 1622*e6380ba3SAndreas Gohr $b->type = $type; 1623*e6380ba3SAndreas Gohr $b->parent = $this->scope; 1624*e6380ba3SAndreas Gohr return $b; 1625*e6380ba3SAndreas Gohr } 1626*e6380ba3SAndreas Gohr 1627*e6380ba3SAndreas Gohr // the state of execution 1628*e6380ba3SAndreas Gohr protected function pushEnv($block = null) 1629*e6380ba3SAndreas Gohr { 1630*e6380ba3SAndreas Gohr $e = new stdclass; 1631*e6380ba3SAndreas Gohr $e->parent = $this->env; 1632*e6380ba3SAndreas Gohr $e->store = []; 1633*e6380ba3SAndreas Gohr $e->block = $block; 1634*e6380ba3SAndreas Gohr 1635*e6380ba3SAndreas Gohr $this->env = $e; 1636*e6380ba3SAndreas Gohr return $e; 1637*e6380ba3SAndreas Gohr } 1638*e6380ba3SAndreas Gohr 1639*e6380ba3SAndreas Gohr // pop something off the stack 1640*e6380ba3SAndreas Gohr protected function popEnv() 1641*e6380ba3SAndreas Gohr { 1642*e6380ba3SAndreas Gohr $old = $this->env; 1643*e6380ba3SAndreas Gohr $this->env = $this->env->parent; 1644*e6380ba3SAndreas Gohr return $old; 1645*e6380ba3SAndreas Gohr } 1646*e6380ba3SAndreas Gohr 1647*e6380ba3SAndreas Gohr // set something in the current env 1648*e6380ba3SAndreas Gohr protected function set($name, $value) 1649*e6380ba3SAndreas Gohr { 1650*e6380ba3SAndreas Gohr $this->env->store[$name] = $value; 1651*e6380ba3SAndreas Gohr } 1652*e6380ba3SAndreas Gohr 1653*e6380ba3SAndreas Gohr 1654*e6380ba3SAndreas Gohr /** 1655*e6380ba3SAndreas Gohr * get the highest occurrence entry for a name 1656*e6380ba3SAndreas Gohr * @throws Exception 1657*e6380ba3SAndreas Gohr */ 1658*e6380ba3SAndreas Gohr protected function get($name) 1659*e6380ba3SAndreas Gohr { 1660*e6380ba3SAndreas Gohr $current = $this->env; 1661*e6380ba3SAndreas Gohr 1662*e6380ba3SAndreas Gohr // track scope to evaluate 1663*e6380ba3SAndreas Gohr $scope_secondary = []; 1664*e6380ba3SAndreas Gohr 1665*e6380ba3SAndreas Gohr $isArguments = $name == Constants::VPREFIX . 'arguments'; 1666*e6380ba3SAndreas Gohr while ($current) { 1667*e6380ba3SAndreas Gohr if ($isArguments && isset($current->arguments)) { 1668*e6380ba3SAndreas Gohr return ['list', ' ', $current->arguments]; 1669*e6380ba3SAndreas Gohr } 1670*e6380ba3SAndreas Gohr 1671*e6380ba3SAndreas Gohr if (isset($current->store[$name])) 1672*e6380ba3SAndreas Gohr return $current->store[$name]; 1673*e6380ba3SAndreas Gohr // has secondary scope? 1674*e6380ba3SAndreas Gohr if (isset($current->storeParent)) 1675*e6380ba3SAndreas Gohr $scope_secondary[] = $current->storeParent; 1676*e6380ba3SAndreas Gohr 1677*e6380ba3SAndreas Gohr $current = $current->parent ?? null; 1678*e6380ba3SAndreas Gohr } 1679*e6380ba3SAndreas Gohr 1680*e6380ba3SAndreas Gohr while (count($scope_secondary)) { 1681*e6380ba3SAndreas Gohr // pop one off 1682*e6380ba3SAndreas Gohr $current = array_shift($scope_secondary); 1683*e6380ba3SAndreas Gohr while ($current) { 1684*e6380ba3SAndreas Gohr if ($isArguments && isset($current->arguments)) { 1685*e6380ba3SAndreas Gohr return ['list', ' ', $current->arguments]; 1686*e6380ba3SAndreas Gohr } 1687*e6380ba3SAndreas Gohr 1688*e6380ba3SAndreas Gohr if (isset($current->store[$name])) { 1689*e6380ba3SAndreas Gohr return $current->store[$name]; 1690*e6380ba3SAndreas Gohr } 1691*e6380ba3SAndreas Gohr 1692*e6380ba3SAndreas Gohr // has secondary scope? 1693*e6380ba3SAndreas Gohr if (isset($current->storeParent)) { 1694*e6380ba3SAndreas Gohr $scope_secondary[] = $current->storeParent; 1695*e6380ba3SAndreas Gohr } 1696*e6380ba3SAndreas Gohr 1697*e6380ba3SAndreas Gohr $current = $current->parent ?? null; 1698*e6380ba3SAndreas Gohr } 1699*e6380ba3SAndreas Gohr } 1700*e6380ba3SAndreas Gohr 1701*e6380ba3SAndreas Gohr throw new Exception("variable $name is undefined"); 1702*e6380ba3SAndreas Gohr } 1703*e6380ba3SAndreas Gohr 1704*e6380ba3SAndreas Gohr /** 1705*e6380ba3SAndreas Gohr * inject array of unparsed strings into environment as variables 1706*e6380ba3SAndreas Gohr * @throws Exception 1707*e6380ba3SAndreas Gohr */ 1708*e6380ba3SAndreas Gohr protected function injectVariables($args) 1709*e6380ba3SAndreas Gohr { 1710*e6380ba3SAndreas Gohr $this->pushEnv(); 1711*e6380ba3SAndreas Gohr $parser = new Parser(__METHOD__); 1712*e6380ba3SAndreas Gohr foreach ($args as $name => $strValue) { 1713*e6380ba3SAndreas Gohr if ($name[0] != '@') $name = '@' . $name; 1714*e6380ba3SAndreas Gohr $parser->count = 0; 1715*e6380ba3SAndreas Gohr $parser->buffer = (string)$strValue; 1716*e6380ba3SAndreas Gohr if (!$parser->propertyValue($value)) { 1717*e6380ba3SAndreas Gohr throw new Exception("failed to parse passed in variable $name: $strValue"); 1718*e6380ba3SAndreas Gohr } 1719*e6380ba3SAndreas Gohr 1720*e6380ba3SAndreas Gohr $this->set($name, $value); 1721*e6380ba3SAndreas Gohr } 1722*e6380ba3SAndreas Gohr } 1723*e6380ba3SAndreas Gohr 1724*e6380ba3SAndreas Gohr /** 1725*e6380ba3SAndreas Gohr * Create a new parser instance 1726*e6380ba3SAndreas Gohr * 1727*e6380ba3SAndreas Gohr * @param string|null $name A name to identify the parser in error messages 1728*e6380ba3SAndreas Gohr */ 1729*e6380ba3SAndreas Gohr protected function makeParser(?string $name): Parser 1730*e6380ba3SAndreas Gohr { 1731*e6380ba3SAndreas Gohr $parser = new Parser($name); 1732*e6380ba3SAndreas Gohr $parser->writeComments = $this->preserveComments; 1733*e6380ba3SAndreas Gohr 1734*e6380ba3SAndreas Gohr return $parser; 1735*e6380ba3SAndreas Gohr } 1736*e6380ba3SAndreas Gohr 1737*e6380ba3SAndreas Gohr /** 1738*e6380ba3SAndreas Gohr * Add the given file to the list of parsed files 1739*e6380ba3SAndreas Gohr * 1740*e6380ba3SAndreas Gohr * @param $file 1741*e6380ba3SAndreas Gohr */ 1742*e6380ba3SAndreas Gohr protected function addParsedFile($file): void 1743*e6380ba3SAndreas Gohr { 1744*e6380ba3SAndreas Gohr $this->allParsedFiles[realpath($file)] = filemtime($file); 1745*e6380ba3SAndreas Gohr } 1746*e6380ba3SAndreas Gohr} 1747