summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Less
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Less')
-rw-r--r--library/Icinga/Less/Call.php77
-rw-r--r--library/Icinga/Less/ColorProp.php109
-rw-r--r--library/Icinga/Less/ColorPropOrVariable.php71
-rw-r--r--library/Icinga/Less/DeferredColorProp.php136
-rw-r--r--library/Icinga/Less/LightMode.php128
-rw-r--r--library/Icinga/Less/LightModeCall.php38
-rw-r--r--library/Icinga/Less/LightModeDefinition.php75
-rw-r--r--library/Icinga/Less/LightModeTrait.php30
-rw-r--r--library/Icinga/Less/LightModeVisitor.php26
-rw-r--r--library/Icinga/Less/Visitor.php233
10 files changed, 923 insertions, 0 deletions
diff --git a/library/Icinga/Less/Call.php b/library/Icinga/Less/Call.php
new file mode 100644
index 0000000..0a78cb5
--- /dev/null
+++ b/library/Icinga/Less/Call.php
@@ -0,0 +1,77 @@
+<?php
+
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use Less_Tree_Call;
+use Less_Tree_Color;
+use Less_Tree_Value;
+use Less_Tree_Variable;
+
+class Call extends Less_Tree_Call
+{
+ public static function fromCall(Less_Tree_Call $call)
+ {
+ return new static($call->name, $call->args, $call->index, $call->currentFileInfo);
+ }
+
+ public function compile($env = null)
+ {
+ if (! $env) {
+ // Not sure how to trigger this, but if there is no $env, there is nothing we can do
+ return parent::compile($env);
+ }
+
+ foreach ($this->args as $arg) {
+ if (! is_array($arg->value)) {
+ continue;
+ }
+
+ $name = null;
+ if ($arg->value[0] instanceof Less_Tree_Variable) {
+ // This is the case when defining a variable with a callable LESS rules such as fade, fadeout..
+ // Example: `@foo: #fff; @foo-bar: fade(@foo, 10);`
+ $name = $arg->value[0]->name;
+ } elseif ($arg->value[0] instanceof ColorPropOrVariable) {
+ // This is the case when defining a CSS rule using the LESS functions and passing
+ // a variable as an argument to them. Example: `... { color: fade(@foo, 10%); }`
+ $name = $arg->value[0]->getVariable()->name;
+ }
+
+ if ($name) {
+ foreach ($env->frames as $frame) {
+ if (($v = $frame->variable($name))) {
+ // Variables from the frame stack are always of type LESS Tree Rule
+ $vr = $v->value;
+ if ($vr instanceof Less_Tree_Value) {
+ // Get the actual color prop, otherwise this may cause an invalid argument error
+ $vr = $vr->compile($env);
+ }
+
+ if ($vr instanceof DeferredColorProp) {
+ if (! $vr->hasReference()) {
+ // Should never happen, though just for safety's sake
+ $vr->compile($env);
+ }
+
+ // Get the uppermost variable of the variable references
+ while (! $vr instanceof ColorProp) {
+ $vr = $vr->getRef();
+ }
+ } elseif ($vr instanceof Less_Tree_Color) {
+ $vr = ColorProp::fromColor($vr);
+ $vr->setName($name);
+ }
+
+ $arg->value[0] = $vr;
+
+ break;
+ }
+ }
+ }
+ }
+
+ return parent::compile($env);
+ }
+}
diff --git a/library/Icinga/Less/ColorProp.php b/library/Icinga/Less/ColorProp.php
new file mode 100644
index 0000000..3f83c5e
--- /dev/null
+++ b/library/Icinga/Less/ColorProp.php
@@ -0,0 +1,109 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | 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) {
+ if ($k === 'name') {
+ $self->setName($v); // Removes the @ char from the name
+ } else {
+ $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)
+ {
+ if ($name[0] === '@') {
+ $name = substr($name, 1);
+ }
+
+ $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);
+ }
+}
diff --git a/library/Icinga/Less/ColorPropOrVariable.php b/library/Icinga/Less/ColorPropOrVariable.php
new file mode 100644
index 0000000..7918674
--- /dev/null
+++ b/library/Icinga/Less/ColorPropOrVariable.php
@@ -0,0 +1,71 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | 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.
+ $result = $vv->compile($env);
+ if ($result instanceof DeferredColorProp) {
+ $v->name = $result->name;
+ } else {
+ $v->name = '@' . $result->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($v->name);
+ }
+
+ return $compiled;
+ }
+}
diff --git a/library/Icinga/Less/DeferredColorProp.php b/library/Icinga/Less/DeferredColorProp.php
new file mode 100644
index 0000000..c9c39ad
--- /dev/null
+++ b/library/Icinga/Less/DeferredColorProp.php
@@ -0,0 +1,136 @@
+<?php
+
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use Less_Exception_Compiler;
+use Less_Tree_Call;
+use Less_Tree_Color;
+use Less_Tree_Keyword;
+use Less_Tree_Value;
+use Less_Tree_Variable;
+
+class DeferredColorProp extends Less_Tree_Variable
+{
+ /** @var DeferredColorProp|ColorProp */
+ protected $reference;
+
+ protected $resolved = false;
+
+ public function __construct($name, $variable, $index = null, $currentFileInfo = null)
+ {
+ parent::__construct($name, $index, $currentFileInfo);
+
+ if ($variable instanceof Less_Tree_Variable) {
+ $this->reference = self::fromVariable($variable);
+ }
+ }
+
+ public function isResolved()
+ {
+ return $this->resolved;
+ }
+
+ public function getName()
+ {
+ $name = $this->name;
+ if ($this->name[0] === '@') {
+ $name = substr($this->name, 1);
+ }
+
+ return $name;
+ }
+
+ public function hasReference()
+ {
+ return $this->reference !== null;
+ }
+
+ public function getRef()
+ {
+ return $this->reference;
+ }
+
+ public function setReference($ref)
+ {
+ $this->reference = $ref;
+
+ return $this;
+ }
+
+ public static function fromVariable(Less_Tree_Variable $variable)
+ {
+ $static = new static($variable->name, $variable->index, $variable->currentFileInfo);
+ $static->evaluating = $variable->evaluating;
+ $static->type = $variable->type;
+
+ return $static;
+ }
+
+ public function compile($env)
+ {
+ if (! $this->hasReference()) {
+ // This is never supposed to happen, however, we might have a deferred color prop
+ // without a reference. In this case we can simply use the parent method.
+ return parent::compile($env);
+ }
+
+ if ($this->isResolved()) {
+ // The dependencies are already resolved, no need to traverse the frame stack over again!
+ return $this;
+ }
+
+ if ($this->evaluating) { // Just like the parent method
+ throw new Less_Exception_Compiler(
+ "Recursive variable definition for " . $this->name,
+ null,
+ $this->index,
+ $this->currentFileInfo
+ );
+ }
+
+ $this->evaluating = true;
+
+ foreach ($env->frames as $frame) {
+ if (($v = $frame->variable($this->getRef()->name))) {
+ $rv = $v->value;
+ if ($rv instanceof Less_Tree_Value) {
+ $rv = $rv->compile($env);
+ }
+
+ // As we are at it anyway, let's cast the tree color to our color prop as well!
+ if ($rv instanceof Less_Tree_Color) {
+ $rv = ColorProp::fromColor($rv);
+ $rv->setName($this->getRef()->getName());
+ }
+
+ $this->evaluating = false;
+ $this->resolved = true;
+ $this->setReference($rv);
+
+ break;
+ }
+ }
+
+ return $this;
+ }
+
+ public function genCSS($output)
+ {
+ if (! $this->hasReference()) {
+ return; // Nothing to generate
+ }
+
+ $css = (new Less_Tree_Call(
+ 'var',
+ [
+ new Less_Tree_Keyword('--' . $this->getName()),
+ $this->getRef() // Each of the references will be generated recursively
+ ],
+ $this->index
+ ))->toCSS();
+
+ $output->add($css);
+ }
+}
diff --git a/library/Icinga/Less/LightMode.php b/library/Icinga/Less/LightMode.php
new file mode 100644
index 0000000..b4b72a0
--- /dev/null
+++ b/library/Icinga/Less/LightMode.php
@@ -0,0 +1,128 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use ArrayIterator;
+use InvalidArgumentException;
+use IteratorAggregate;
+use Less_Environment;
+use Traversable;
+
+/**
+ * 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(): Traversable
+ {
+ return new ArrayIterator(array_keys($this->modes));
+ }
+}
diff --git a/library/Icinga/Less/LightModeCall.php b/library/Icinga/Less/LightModeCall.php
new file mode 100644
index 0000000..d899e3c
--- /dev/null
+++ b/library/Icinga/Less/LightModeCall.php
@@ -0,0 +1,38 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | 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))
+ );
+ }
+}
diff --git a/library/Icinga/Less/LightModeDefinition.php b/library/Icinga/Less/LightModeDefinition.php
new file mode 100644
index 0000000..929e95c
--- /dev/null
+++ b/library/Icinga/Less/LightModeDefinition.php
@@ -0,0 +1,75 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | 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;
+ }
+}
diff --git a/library/Icinga/Less/LightModeTrait.php b/library/Icinga/Less/LightModeTrait.php
new file mode 100644
index 0000000..d328265
--- /dev/null
+++ b/library/Icinga/Less/LightModeTrait.php
@@ -0,0 +1,30 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | 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;
+ }
+}
diff --git a/library/Icinga/Less/LightModeVisitor.php b/library/Icinga/Less/LightModeVisitor.php
new file mode 100644
index 0000000..35758b4
--- /dev/null
+++ b/library/Icinga/Less/LightModeVisitor.php
@@ -0,0 +1,26 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | 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);
+ }
+}
diff --git a/library/Icinga/Less/Visitor.php b/library/Icinga/Less/Visitor.php
new file mode 100644
index 0000000..c04a0eb
--- /dev/null
+++ b/library/Icinga/Less/Visitor.php
@@ -0,0 +1,233 @@
+<?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');
+ $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;
+ }
+}