Merge pull request #4613 from Icinga/less-parser-injections
Replace compiled Less colors with CSS var() function calls and generate light mode calls
This commit is contained in:
commit
6e034b753e
|
@ -56,10 +56,12 @@ if (in_array($path, $special)) {
|
||||||
|
|
||||||
switch ($path) {
|
switch ($path) {
|
||||||
case 'css/icinga.css':
|
case 'css/icinga.css':
|
||||||
Stylesheet::send();
|
$forIe11 = (bool) preg_match('/Trident\/7.0;.*rv:11/', $_SERVER['HTTP_USER_AGENT']);
|
||||||
|
Stylesheet::send(false, $forIe11);
|
||||||
exit;
|
exit;
|
||||||
case 'css/icinga.min.css':
|
case 'css/icinga.min.css':
|
||||||
Stylesheet::send(true);
|
$forIe11 = (bool) preg_match('/Trident\/7.0;.*rv:11/', $_SERVER['HTTP_USER_AGENT']);
|
||||||
|
Stylesheet::send(true, $forIe11);
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
case 'js/icinga.dev.js':
|
case 'js/icinga.dev.js':
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2022 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Less;
|
||||||
|
|
||||||
|
use Less_Tree_Call;
|
||||||
|
use Less_Tree_Color;
|
||||||
|
use Less_Tree_Keyword;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ColorProp renders Less colors as CSS var() function calls
|
||||||
|
*
|
||||||
|
* It extends {@link Less_Tree_Color} so that Less functions that take a Less_Tree_Color as an argument do not fail.
|
||||||
|
*/
|
||||||
|
class ColorProp extends Less_Tree_Color
|
||||||
|
{
|
||||||
|
/** @var Less_Tree_Color Color with which we created the ColorProp */
|
||||||
|
protected $color;
|
||||||
|
|
||||||
|
/** @var int */
|
||||||
|
protected $index;
|
||||||
|
|
||||||
|
/** @var string Color variable name */
|
||||||
|
protected $name;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Less_Tree_Color $color
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public static function fromColor(Less_Tree_Color $color)
|
||||||
|
{
|
||||||
|
$self = new static();
|
||||||
|
$self->color = $color;
|
||||||
|
|
||||||
|
foreach ($color as $k => $v) {
|
||||||
|
$self->$k = $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getIndex()
|
||||||
|
{
|
||||||
|
return $this->index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $index
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setIndex($index)
|
||||||
|
{
|
||||||
|
$this->index = $index;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setName($name)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function genCSS($output)
|
||||||
|
{
|
||||||
|
$css = (new Less_Tree_Call(
|
||||||
|
'var',
|
||||||
|
[
|
||||||
|
new Less_Tree_Keyword('--' . $this->getName()),
|
||||||
|
// Use the Less_Tree_Color with which we created the ColorProp so that we don't get into genCSS() loops.
|
||||||
|
$this->color
|
||||||
|
],
|
||||||
|
$this->getIndex()
|
||||||
|
))->toCSS();
|
||||||
|
|
||||||
|
$output->add($css);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2022 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Less;
|
||||||
|
|
||||||
|
use Less_Tree;
|
||||||
|
use Less_Tree_Color;
|
||||||
|
use Less_Tree_Variable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile a Less variable to {@link ColorProp} if it is a color
|
||||||
|
*/
|
||||||
|
class ColorPropOrVariable extends Less_Tree
|
||||||
|
{
|
||||||
|
public $type = 'Variable';
|
||||||
|
|
||||||
|
/** @var Less_Tree_Variable */
|
||||||
|
protected $variable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Less_Tree_Variable
|
||||||
|
*/
|
||||||
|
public function getVariable()
|
||||||
|
{
|
||||||
|
return $this->variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Less_Tree_Variable $variable
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setVariable(Less_Tree_Variable $variable)
|
||||||
|
{
|
||||||
|
$this->variable = $variable;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function compile($env)
|
||||||
|
{
|
||||||
|
$v = $this->getVariable();
|
||||||
|
|
||||||
|
if ($v->name[1] === '@') {
|
||||||
|
// Evaluate variable variable as in Less_Tree_Variable:28.
|
||||||
|
$vv = new Less_Tree_Variable(substr($v->name, 1), $v->index + 1, $v->currentFileInfo);
|
||||||
|
// Overwrite the name so that the variable variable is not evaluated again.
|
||||||
|
$v->name = '@' . $vv->compile($env)->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$compiled = $v->compile($env);
|
||||||
|
|
||||||
|
if ($compiled instanceof ColorProp) {
|
||||||
|
// We may already have a ColorProp, which is the case with mixin calls.
|
||||||
|
return $compiled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($compiled instanceof Less_Tree_Color) {
|
||||||
|
return ColorProp::fromColor($compiled)
|
||||||
|
->setIndex($v->index)
|
||||||
|
->setName(substr($v->name, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $compiled;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2022 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Less;
|
||||||
|
|
||||||
|
use ArrayIterator;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use IteratorAggregate;
|
||||||
|
use Less_Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry for light modes and the environments in which they are defined
|
||||||
|
*/
|
||||||
|
class LightMode implements IteratorAggregate
|
||||||
|
{
|
||||||
|
/** @var array Mode environments as mode-environment pairs */
|
||||||
|
protected $envs = [];
|
||||||
|
|
||||||
|
/** @var array Assoc list of modes */
|
||||||
|
protected $modes = [];
|
||||||
|
|
||||||
|
/** @var array Mode selectors as mode-selector pairs */
|
||||||
|
protected $selectors = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $mode
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException If the mode already exists
|
||||||
|
*/
|
||||||
|
public function add($mode)
|
||||||
|
{
|
||||||
|
if (array_key_exists($mode, $this->modes)) {
|
||||||
|
throw new InvalidArgumentException("$mode already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->modes[$mode] = true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $mode
|
||||||
|
*
|
||||||
|
* @return Less_Environment
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException If there is no environment for the given mode
|
||||||
|
*/
|
||||||
|
public function getEnv($mode)
|
||||||
|
{
|
||||||
|
if (! isset($this->envs[$mode])) {
|
||||||
|
throw new InvalidArgumentException("$mode does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->envs[$mode];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $mode
|
||||||
|
* @param Less_Environment $env
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException If an environment for given the mode already exists
|
||||||
|
*/
|
||||||
|
public function setEnv($mode, Less_Environment $env)
|
||||||
|
{
|
||||||
|
if (array_key_exists($mode, $this->envs)) {
|
||||||
|
throw new InvalidArgumentException("$mode already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->envs[$mode] = $env;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $mode
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasSelector($mode)
|
||||||
|
{
|
||||||
|
return isset($this->selectors[$mode]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $mode
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException If there is no selector for the given mode
|
||||||
|
*/
|
||||||
|
public function getSelector($mode)
|
||||||
|
{
|
||||||
|
if (! isset($this->selectors[$mode])) {
|
||||||
|
throw new InvalidArgumentException("$mode does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->selectors[$mode];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $mode
|
||||||
|
* @param string $selector
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException If a selector for given the mode already exists
|
||||||
|
*/
|
||||||
|
public function setSelector($mode, $selector)
|
||||||
|
{
|
||||||
|
if (array_key_exists($mode, $this->selectors)) {
|
||||||
|
throw new InvalidArgumentException("$mode already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->selectors[$mode] = $selector;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIterator()
|
||||||
|
{
|
||||||
|
return new ArrayIterator(array_keys($this->modes));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2022 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Less;
|
||||||
|
|
||||||
|
use Less_Environment;
|
||||||
|
use Less_Tree_Ruleset;
|
||||||
|
use Less_Tree_RulesetCall;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the environment where the light mode was defined to evaluate the call
|
||||||
|
*/
|
||||||
|
class LightModeCall extends Less_Tree_RulesetCall
|
||||||
|
{
|
||||||
|
use LightModeTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Less_Tree_RulesetCall $c
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public static function fromRulesetCall(Less_Tree_RulesetCall $c)
|
||||||
|
{
|
||||||
|
return new static($c->variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Less_Environment $env
|
||||||
|
*
|
||||||
|
* @return Less_Tree_Ruleset
|
||||||
|
*/
|
||||||
|
public function compile($env)
|
||||||
|
{
|
||||||
|
return parent::compile(
|
||||||
|
$env->copyEvalEnv(array_merge($env->frames, $this->getLightMode()->getEnv($this->variable)->frames))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2022 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Less;
|
||||||
|
|
||||||
|
use Less_Environment;
|
||||||
|
use Less_Exception_Compiler;
|
||||||
|
use Less_Tree_DetachedRuleset;
|
||||||
|
use Less_Tree_Ruleset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the environment in which the light mode is defined
|
||||||
|
*/
|
||||||
|
class LightModeDefinition extends Less_Tree_DetachedRuleset
|
||||||
|
{
|
||||||
|
use LightModeTrait;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Less_Tree_DetachedRuleset $drs
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public static function fromDetachedRuleset(Less_Tree_DetachedRuleset $drs)
|
||||||
|
{
|
||||||
|
return new static($drs->ruleset, $drs->frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setName($name)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Less_Environment $env
|
||||||
|
*
|
||||||
|
* @return Less_Tree_DetachedRuleset
|
||||||
|
*/
|
||||||
|
public function compile($env)
|
||||||
|
{
|
||||||
|
$drs = parent::compile($env);
|
||||||
|
|
||||||
|
/** @var $frame Less_Tree_Ruleset */
|
||||||
|
foreach ($env->frames as $frame) {
|
||||||
|
if ($frame->variable($this->getName())) {
|
||||||
|
if (! empty($frame->first_oelements) && ! isset($frame->first_oelements['.icinga-module'])) {
|
||||||
|
throw new Less_Exception_Compiler('Light mode definition not allowed in selectors');
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getLightMode()->setEnv($this->getName(), $env->copyEvalEnv($env->frames));
|
||||||
|
|
||||||
|
return $drs;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2022 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Less;
|
||||||
|
|
||||||
|
trait LightModeTrait
|
||||||
|
{
|
||||||
|
/** @var LightMode */
|
||||||
|
private $lightMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return LightMode
|
||||||
|
*/
|
||||||
|
public function getLightMode()
|
||||||
|
{
|
||||||
|
return $this->lightMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param LightMode $lightMode
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setLightMode(LightMode $lightMode)
|
||||||
|
{
|
||||||
|
$this->lightMode = $lightMode;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2022 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Less;
|
||||||
|
|
||||||
|
use Less_VisitorReplacing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that light mode calls have access to the environment in which the mode was defined
|
||||||
|
*/
|
||||||
|
class LightModeVisitor extends Less_VisitorReplacing
|
||||||
|
{
|
||||||
|
use LightModeTrait;
|
||||||
|
|
||||||
|
public $isPreVisitor = true;
|
||||||
|
|
||||||
|
public function visitRulesetCall($c)
|
||||||
|
{
|
||||||
|
return LightModeCall::fromRulesetCall($c)->setLightMode($this->getLightMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run($node)
|
||||||
|
{
|
||||||
|
return $this->visitObj($node);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,230 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2022 Icinga Development Team | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Less;
|
||||||
|
|
||||||
|
use Less_Parser;
|
||||||
|
use Less_Tree_Rule;
|
||||||
|
use Less_VisitorReplacing;
|
||||||
|
use LogicException;
|
||||||
|
use ReflectionProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace compiled Less colors with CSS var() function calls and inject light mode calls
|
||||||
|
*
|
||||||
|
* Color replacing basically works by replacing every visited Less variable with {@link ColorPropOrVariable},
|
||||||
|
* which is later compiled to {@link ColorProp} if it is a color.
|
||||||
|
*
|
||||||
|
* Light mode calls are generated from light mode definitions.
|
||||||
|
*/
|
||||||
|
class Visitor extends Less_VisitorReplacing
|
||||||
|
{
|
||||||
|
const LIGHT_MODE_CSS = <<<'CSS'
|
||||||
|
@media (min-height: @prefer-light-color-scheme),
|
||||||
|
(prefers-color-scheme: light) and (min-height: @enable-color-preference) {
|
||||||
|
%s
|
||||||
|
}
|
||||||
|
CSS;
|
||||||
|
|
||||||
|
const LIGHT_MODE_NAME = 'light-mode';
|
||||||
|
|
||||||
|
public $isPreEvalVisitor = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether calling var() CSS function
|
||||||
|
*
|
||||||
|
* If that's the case, don't try to replace compiled Less colors with CSS var() function calls.
|
||||||
|
*
|
||||||
|
* @var bool|string
|
||||||
|
*/
|
||||||
|
protected $callingVar = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether defining a variable
|
||||||
|
*
|
||||||
|
* If that's the case, don't try to replace compiled Less colors with CSS var() function calls.
|
||||||
|
*
|
||||||
|
* @var false|string
|
||||||
|
*/
|
||||||
|
protected $definingVariable = false;
|
||||||
|
|
||||||
|
/** @var Less_Tree_Rule If defining a variable, determines the origin rule of the variable */
|
||||||
|
protected $variableOrigin;
|
||||||
|
|
||||||
|
/** @var LightMode Light mode registry */
|
||||||
|
protected $lightMode;
|
||||||
|
|
||||||
|
/** @var false|string Whether parsing module Less */
|
||||||
|
protected $moduleScope = false;
|
||||||
|
|
||||||
|
/** @var null|string CSS module selector if any */
|
||||||
|
protected $moduleSelector;
|
||||||
|
|
||||||
|
public function visitCall($c)
|
||||||
|
{
|
||||||
|
if ($c->name === 'var') {
|
||||||
|
if ($this->callingVar !== false) {
|
||||||
|
throw new LogicException('Already calling var');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->callingVar = spl_object_hash($c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function visitCallOut($c)
|
||||||
|
{
|
||||||
|
if ($this->callingVar !== false && $this->callingVar === spl_object_hash($c)) {
|
||||||
|
$this->callingVar = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function visitDetachedRuleset($drs)
|
||||||
|
{
|
||||||
|
if ($this->variableOrigin->name === '@' . static::LIGHT_MODE_NAME) {
|
||||||
|
$this->variableOrigin->name .= '-' . substr(sha1(uniqid(mt_rand(), true)), 0, 7);
|
||||||
|
|
||||||
|
$this->lightMode->add($this->variableOrigin->name);
|
||||||
|
|
||||||
|
if ($this->moduleSelector !== false) {
|
||||||
|
$this->lightMode->setSelector($this->variableOrigin->name, $this->moduleSelector);
|
||||||
|
}
|
||||||
|
|
||||||
|
$drs = LightModeDefinition::fromDetachedRuleset($drs)
|
||||||
|
->setLightMode($this->lightMode)
|
||||||
|
->setName($this->variableOrigin->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since a detached ruleset is a variable definition in the first place,
|
||||||
|
// just reset that we define a variable.
|
||||||
|
$this->definingVariable = false;
|
||||||
|
|
||||||
|
return $drs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function visitMixinCall($c)
|
||||||
|
{
|
||||||
|
// Less_Tree_Mixin_Call::accept() does not visit arguments, but we have to replace them if necessary.
|
||||||
|
foreach ($c->arguments as $a) {
|
||||||
|
$a['value'] = $this->visitObj($a['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function visitMixinDefinition($m)
|
||||||
|
{
|
||||||
|
// Less_Tree_Mixin_Definition::accept() does not visit params, but we have to replace them if necessary.
|
||||||
|
foreach ($m->params as $p) {
|
||||||
|
if (! isset($p['value'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$p['value'] = $this->visitObj($p['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function visitRule($r)
|
||||||
|
{
|
||||||
|
if ($r->name[0] === '@' && $r->variable) {
|
||||||
|
if ($this->definingVariable !== false) {
|
||||||
|
throw new LogicException('Already defining a variable');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->definingVariable = spl_object_hash($r);
|
||||||
|
$this->variableOrigin = $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function visitRuleOut($r)
|
||||||
|
{
|
||||||
|
if ($this->definingVariable !== false && $this->definingVariable === spl_object_hash($r)) {
|
||||||
|
$this->definingVariable = false;
|
||||||
|
$this->variableOrigin = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function visitRuleset($rs)
|
||||||
|
{
|
||||||
|
// Method is required, otherwise visitRulesetOut will not be called.
|
||||||
|
return $rs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function visitRulesetOut($rs)
|
||||||
|
{
|
||||||
|
if ($this->moduleScope !== false
|
||||||
|
&& isset($rs->selectors)
|
||||||
|
&& spl_object_hash($rs->selectors[0]) === $this->moduleScope
|
||||||
|
) {
|
||||||
|
$this->moduleSelector = null;
|
||||||
|
$this->moduleScope = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function visitSelector($s)
|
||||||
|
{
|
||||||
|
if ($s->_oelements_len === 2 && $s->_oelements[0] === '.icinga-module') {
|
||||||
|
$this->moduleSelector = implode('', $s->_oelements);
|
||||||
|
$this->moduleScope = spl_object_hash($s);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function visitVariable($v)
|
||||||
|
{
|
||||||
|
if ($this->callingVar !== false || $this->definingVariable !== false) {
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (new ColorPropOrVariable())
|
||||||
|
->setVariable($v);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run($node)
|
||||||
|
{
|
||||||
|
$this->lightMode = new LightMode();
|
||||||
|
|
||||||
|
$evald = $this->visitObj($node);
|
||||||
|
|
||||||
|
// The visitor has registered all light modes in visitDetachedRuleset, but has not called them yet.
|
||||||
|
// Now the light mode calls are prepared with the appropriate CSS selectors.
|
||||||
|
$calls = [];
|
||||||
|
foreach ($this->lightMode as $mode) {
|
||||||
|
if ($this->lightMode->hasSelector($mode)) {
|
||||||
|
$calls[] = "{$this->lightMode->getSelector($mode)} {\n$mode();\n}";
|
||||||
|
} else {
|
||||||
|
$calls[] = "$mode();";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($calls)) {
|
||||||
|
// Place and parse light mode calls into a new anonymous file,
|
||||||
|
// leaving the original Less in which the light modes were defined untouched.
|
||||||
|
$parser = (new Less_Parser())
|
||||||
|
->parse(sprintf(static::LIGHT_MODE_CSS, implode("\n", $calls)));
|
||||||
|
|
||||||
|
// Because Less variables are block scoped,
|
||||||
|
// we can't just access the light mode definitions in the calls above.
|
||||||
|
// The LightModeVisitor ensures that all calls have access to the environment in which the mode was defined.
|
||||||
|
// Finally, the rules are merged so that the light mode calls are also rendered to CSS.
|
||||||
|
$rules = new ReflectionProperty(get_class($parser), 'rules');
|
||||||
|
$rules->setAccessible(true);
|
||||||
|
$evald->rules = array_merge(
|
||||||
|
$evald->rules,
|
||||||
|
(new LightModeVisitor())
|
||||||
|
->setLightMode($this->lightMode)
|
||||||
|
->visitArray($rules->getValue($parser))
|
||||||
|
);
|
||||||
|
// The LightModeVisitor is used explicitly here instead of using it as a plugin
|
||||||
|
// since we only need to process the newly created rules for the light mode calls.
|
||||||
|
}
|
||||||
|
|
||||||
|
return $evald;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
namespace Icinga\Util;
|
namespace Icinga\Util;
|
||||||
|
|
||||||
|
use Icinga\Less\Visitor;
|
||||||
use Less_Tree_Anonymous;
|
use Less_Tree_Anonymous;
|
||||||
use Less_Tree_Expression;
|
use Less_Tree_Expression;
|
||||||
use Less_Tree_Quoted;
|
use Less_Tree_Quoted;
|
||||||
|
@ -13,9 +14,16 @@ require_once 'lessphp/lessc.inc.php';
|
||||||
|
|
||||||
class LessParser extends lessc
|
class LessParser extends lessc
|
||||||
{
|
{
|
||||||
public function __construct()
|
/**
|
||||||
|
* @param bool $disableModes Disable replacing compiled Less colors with CSS var() function calls and don't inject
|
||||||
|
* light mode calls
|
||||||
|
*/
|
||||||
|
public function __construct($disableModes = false)
|
||||||
{
|
{
|
||||||
$this->registerFunction('extract-variable-default', [$this, 'extractVariableDefault']);
|
$this->registerFunction('extract-variable-default', [$this, 'extractVariableDefault']);
|
||||||
|
if (! $disableModes) {
|
||||||
|
$this->setOption('plugins', [new Visitor()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,7 +41,7 @@ class LessParser extends lessc
|
||||||
* background: drop-shadow(5px 0 3px @mixin-parameter);
|
* background: drop-shadow(5px 0 3px @mixin-parameter);
|
||||||
*
|
*
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @param bool $valAsDefault
|
* @param bool $valAsDefault
|
||||||
*
|
*
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -58,10 +58,13 @@ class LessCompiler
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new LESS compiler
|
* Create a new LESS compiler
|
||||||
|
*
|
||||||
|
* @param bool $disableModes Disable replacing compiled Less colors with CSS var() function calls and don't inject
|
||||||
|
* light mode calls
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct($disableModes = false)
|
||||||
{
|
{
|
||||||
$this->lessc = new LessParser();
|
$this->lessc = new LessParser($disableModes);
|
||||||
// Discourage usage of import because we're caching based on an explicit list of LESS files to compile
|
// Discourage usage of import because we're caching based on an explicit list of LESS files to compile
|
||||||
$this->lessc->importDisabled = true;
|
$this->lessc->importDisabled = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,12 +113,15 @@ class StyleSheet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the StyleSheet
|
* Create the StyleSheet
|
||||||
|
*
|
||||||
|
* @param bool $disableModes Disable replacing compiled Less colors with CSS var() function calls and don't inject
|
||||||
|
* light mode calls
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct($disableModes = false)
|
||||||
{
|
{
|
||||||
$app = Icinga::app();
|
$app = Icinga::app();
|
||||||
$this->app = $app;
|
$this->app = $app;
|
||||||
$this->lessCompiler = new LessCompiler();
|
$this->lessCompiler = new LessCompiler($disableModes);
|
||||||
$this->pubPath = $app->getBaseDir('public');
|
$this->pubPath = $app->getBaseDir('public');
|
||||||
$this->collect();
|
$this->collect();
|
||||||
}
|
}
|
||||||
|
@ -221,11 +224,13 @@ class StyleSheet
|
||||||
*
|
*
|
||||||
* Does not cache the stylesheet if the HTTP header Cache-Control or Pragma is set to no-cache.
|
* Does not cache the stylesheet if the HTTP header Cache-Control or Pragma is set to no-cache.
|
||||||
*
|
*
|
||||||
* @param bool $minified Whether to compress the stylesheet
|
* @param bool $minified Whether to compress the stylesheet
|
||||||
|
* @param bool $disableModes Disable replacing compiled Less colors with CSS var() function calls and don't inject
|
||||||
|
* light mode calls
|
||||||
*/
|
*/
|
||||||
public static function send($minified = false)
|
public static function send($minified = false, $disableModes = false)
|
||||||
{
|
{
|
||||||
$styleSheet = new self();
|
$styleSheet = new self($disableModes);
|
||||||
|
|
||||||
$request = $styleSheet->app->getRequest();
|
$request = $styleSheet->app->getRequest();
|
||||||
$response = $styleSheet->app->getResponse();
|
$response = $styleSheet->app->getResponse();
|
||||||
|
|
|
@ -0,0 +1,517 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Tests\Icinga\Util;
|
||||||
|
|
||||||
|
use Icinga\Test\BaseTestCase;
|
||||||
|
use Icinga\Util\LessParser;
|
||||||
|
use Less_Exception_Compiler;
|
||||||
|
|
||||||
|
class LessParserTest extends BaseTestCase
|
||||||
|
{
|
||||||
|
protected $lessc;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->lessc = new LessParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function compileLess($less)
|
||||||
|
{
|
||||||
|
return $this->lessc->compile($less);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSimpleVariables()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.black {
|
||||||
|
color: var(--black, #000000);
|
||||||
|
}
|
||||||
|
.notBlack {
|
||||||
|
color: var(--notBlack, #ffffff);
|
||||||
|
}
|
||||||
|
.alsoNotBlack {
|
||||||
|
color: var(--also-not-black, #008000);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@black: black;
|
||||||
|
@notBlack: white;
|
||||||
|
@also-not-black: green;
|
||||||
|
|
||||||
|
.black {
|
||||||
|
color: @black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notBlack {
|
||||||
|
color: @notBlack;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alsoNotBlack {
|
||||||
|
color: @also-not-black;
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariablesUsedInFunctions()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.light-black {
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
.dark-white {
|
||||||
|
color: var(--dark-white, #808080);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@black: black;
|
||||||
|
@dark-white: darken(white, 50%);
|
||||||
|
|
||||||
|
.light-black {
|
||||||
|
color: lighten(@black, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-white {
|
||||||
|
color: @dark-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariableInterpolation()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.a-rule {
|
||||||
|
width: calc(1337px - 50%);
|
||||||
|
color: var(--property-value, #ffa500);
|
||||||
|
}
|
||||||
|
.another-rule {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@pixels: 1337px;
|
||||||
|
@property: color;
|
||||||
|
@property-value: orange;
|
||||||
|
@selector: another-rule;
|
||||||
|
|
||||||
|
.a-rule {
|
||||||
|
width: ~"calc(@{pixels} - 50%)";
|
||||||
|
@{property}: @property-value;
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{selector} {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariableVariables()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.section .element {
|
||||||
|
color: var(--primary, #008000);
|
||||||
|
}
|
||||||
|
.lazy-eval {
|
||||||
|
color: var(--a, #000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@primary: green;
|
||||||
|
|
||||||
|
.section {
|
||||||
|
@color: primary;
|
||||||
|
|
||||||
|
.element {
|
||||||
|
color: @@color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lazy-eval {
|
||||||
|
color: @@var;
|
||||||
|
}
|
||||||
|
|
||||||
|
@var: a;
|
||||||
|
@a: black;
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariablesInsideMediaQueries()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
@media screen {
|
||||||
|
.link {
|
||||||
|
color: var(--link-color, #000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@link-color: black;
|
||||||
|
|
||||||
|
@media screen {
|
||||||
|
.link {
|
||||||
|
color: @link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariablesInsideMixins()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.mixin2 {
|
||||||
|
color: var(--link-color, #000000);
|
||||||
|
}
|
||||||
|
.mixin-user {
|
||||||
|
color: var(--link-color, #000000);
|
||||||
|
}
|
||||||
|
.mixin-user .nested {
|
||||||
|
color: var(--link-color, #000000) !important;
|
||||||
|
}
|
||||||
|
.mixin2-user {
|
||||||
|
color: var(--link-color, #000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@link-color: black;
|
||||||
|
|
||||||
|
.mixin() {
|
||||||
|
color: @link-color;
|
||||||
|
|
||||||
|
.nested {
|
||||||
|
color: @link-color !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mixin2 {
|
||||||
|
color: @link-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mixin-user {
|
||||||
|
.mixin();
|
||||||
|
}
|
||||||
|
|
||||||
|
.mixin2-user {
|
||||||
|
.mixin2();
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariablesInsideNamespacedMixins()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.mixin-user {
|
||||||
|
color: var(--link-color, #000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@link-color: black;
|
||||||
|
|
||||||
|
#namespace {
|
||||||
|
.mixin() {
|
||||||
|
color: @link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mixin-user {
|
||||||
|
#namespace.mixin();
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariablesInsideMixinsAndGuardedNamespaces()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.mixin-user {
|
||||||
|
color: var(--link-color, #000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@mode: huge;
|
||||||
|
@link-color: black;
|
||||||
|
|
||||||
|
#namespace when (@mode = huge) {
|
||||||
|
.mixin() {
|
||||||
|
color: @link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mixin-user {
|
||||||
|
#namespace.mixin();
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariablesInsideParametricMixins()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.button {
|
||||||
|
background-color: var(--button-bg-color, #000000);
|
||||||
|
}
|
||||||
|
.light-button {
|
||||||
|
background-color: var(--base-bg-color, #ffffff);
|
||||||
|
}
|
||||||
|
.very-special-button {
|
||||||
|
background-color: var(--special-bg-color, #ff0000);
|
||||||
|
color: var(--special-fg-color, #4169e1);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@base-bg-color: white;
|
||||||
|
@base-fg-color: black;
|
||||||
|
@button-bg-color: black;
|
||||||
|
@special-bg-color: red;
|
||||||
|
@special-fg-color: royalblue;
|
||||||
|
|
||||||
|
.button(@bg-color) {
|
||||||
|
background-color: @bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-with-defaults(@bg-color: @base-bg-color) {
|
||||||
|
background-color: @bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-button(@bg-color: @base-bg-color, @fg-color: @base-fg-color) {
|
||||||
|
background-color: @bg-color;
|
||||||
|
color: @fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
.button(@button-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-button {
|
||||||
|
.button-with-defaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
.very-special-button {
|
||||||
|
.special-button(@fg-color: @special-fg-color, @bg-color: @special-bg-color);
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testArgumentsParameterOfMixins()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.big-block {
|
||||||
|
-webkit-box-shadow: 2px 5px 1px var(--shadow-color, #000000);
|
||||||
|
-moz-box-shadow: 2px 5px 1px var(--shadow-color, #000000);
|
||||||
|
box-shadow: 2px 5px 1px var(--shadow-color, #000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@shadow-color: black;
|
||||||
|
|
||||||
|
.box-shadow(@x: 0, @y: 0, @blur: 1px, @color: #fff) {
|
||||||
|
-webkit-box-shadow: @arguments;
|
||||||
|
-moz-box-shadow: @arguments;
|
||||||
|
box-shadow: @arguments;
|
||||||
|
}
|
||||||
|
.big-block {
|
||||||
|
.box-shadow(2px, 5px, @color: @shadow-color);
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRestParameterOfMixins()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.my-button {
|
||||||
|
color: var(--button-fg-color, #000000);
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 0 1px var(--shadow-color, #000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@button-fg-color: black;
|
||||||
|
@shadow-color: black;
|
||||||
|
|
||||||
|
.button(@fg-color, @box-shadow...) {
|
||||||
|
color: @fg-color;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: @box-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-button {
|
||||||
|
.button(@button-fg-color, 0, 0, 1px, @shadow-color);
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariablesInsideDetachedRulesets()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.ruleset-user {
|
||||||
|
background-color: var(--base-bg-color, #000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@base-bg-color: black;
|
||||||
|
|
||||||
|
@detached-ruleset: {
|
||||||
|
background-color: @base-bg-color;
|
||||||
|
};
|
||||||
|
|
||||||
|
.ruleset-user {
|
||||||
|
@detached-ruleset();
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRulesetUsagesInsideRulesets()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
.ruleset-user {
|
||||||
|
color: black;
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@detached-ruleset: {
|
||||||
|
color: black;
|
||||||
|
@another-detached-ruleset();
|
||||||
|
};
|
||||||
|
|
||||||
|
@another-detached-ruleset: {
|
||||||
|
border-color: white;
|
||||||
|
};
|
||||||
|
|
||||||
|
.ruleset-user {
|
||||||
|
@detached-ruleset();
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLightModeCollection()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
<<<CSS
|
||||||
|
@media (min-height: 999999px), (prefers-color-scheme: light) and (min-height: 999999px) {
|
||||||
|
:root {
|
||||||
|
--my-color: orange;
|
||||||
|
}
|
||||||
|
:root {
|
||||||
|
--my-other-color: green;
|
||||||
|
}
|
||||||
|
.icinga-module.module-test {
|
||||||
|
--greenish-color: lime;
|
||||||
|
--blueish-color: navy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CSS
|
||||||
|
,
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
@prefer-light-color-scheme: 999999px;
|
||||||
|
@enable-color-preference: 999999px;
|
||||||
|
|
||||||
|
@light-mode: {
|
||||||
|
:root {
|
||||||
|
--my-color: orange;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@light-mode: {
|
||||||
|
:root {
|
||||||
|
--my-other-color: green;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
.icinga-module.module-test {
|
||||||
|
@light-mode: {
|
||||||
|
@more-light-colors();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@more-light-colors: {
|
||||||
|
--greenish-color: lime;
|
||||||
|
--blueish-color: navy;
|
||||||
|
};
|
||||||
|
LESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLightModeDefinitionRestrictedInSelectors()
|
||||||
|
{
|
||||||
|
$this->expectException(Less_Exception_Compiler::class);
|
||||||
|
|
||||||
|
$this->compileLess(<<<LESS
|
||||||
|
.selector {
|
||||||
|
@light-mode: {
|
||||||
|
:root {
|
||||||
|
--my-color: orange;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
LESS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue