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