mirror of
https://github.com/Icinga/icingaweb2.git
synced 2026-04-13 04:56:13 -04:00
`ReflectionProperty::setAccessible()` has had no effect since PHP 8.1, as all properties are accessible via reflection by default. The method is deprecated as of PHP 8.5.
232 lines
7.3 KiB
PHP
232 lines
7.3 KiB
PHP
<?php
|
|
/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
|
|
|
|
namespace Icinga\Less;
|
|
|
|
use Less_Parser;
|
|
use Less_Tree_Expression;
|
|
use Less_Tree_Rule;
|
|
use Less_Tree_Value;
|
|
use Less_Tree_Variable;
|
|
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), print,
|
|
(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') {
|
|
// We need to use our own tree call class , so that we can precompile the arguments before making
|
|
// the actual LESS function calls. Otherwise, it will produce lots of invalid argument exceptions!
|
|
$c = Call::fromCall($c);
|
|
}
|
|
|
|
return $c;
|
|
}
|
|
|
|
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;
|
|
|
|
if ($r->value instanceof Less_Tree_Value) {
|
|
if ($r->value->value[0] instanceof Less_Tree_Expression) {
|
|
if ($r->value->value[0]->value[0] instanceof Less_Tree_Variable) {
|
|
// Transform the variable definition rule into our own class
|
|
$r->value->value[0]->value[0] = new DeferredColorProp($r->name, $r->value->value[0]->value[0]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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->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');
|
|
$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;
|
|
}
|
|
}
|