diff --git a/library/Icinga/Less/Call.php b/library/Icinga/Less/Call.php new file mode 100644 index 000000000..0a78cb541 --- /dev/null +++ b/library/Icinga/Less/Call.php @@ -0,0 +1,77 @@ +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 index f10115d75..3f83c5eee 100644 --- a/library/Icinga/Less/ColorProp.php +++ b/library/Icinga/Less/ColorProp.php @@ -1,5 +1,5 @@ color = $color; foreach ($color as $k => $v) { - $self->$k = $v; + if ($k === 'name') { + $self->setName($v); // Removes the @ char from the name + } else { + $self->$k = $v; + } } return $self; @@ -79,6 +83,10 @@ class ColorProp extends Less_Tree_Color */ public function setName($name) { + if ($name[0] === '@') { + $name = substr($name, 1); + } + $this->name = $name; return $this; diff --git a/library/Icinga/Less/ColorPropOrVariable.php b/library/Icinga/Less/ColorPropOrVariable.php index 371cca47f..791867495 100644 --- a/library/Icinga/Less/ColorPropOrVariable.php +++ b/library/Icinga/Less/ColorPropOrVariable.php @@ -1,5 +1,5 @@ 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; + $result = $vv->compile($env); + if ($result instanceof DeferredColorProp) { + $v->name = $result->name; + } else { + $v->name = '@' . $result->value; + } } $compiled = $v->compile($env); @@ -58,7 +63,7 @@ class ColorPropOrVariable extends Less_Tree if ($compiled instanceof Less_Tree_Color) { return ColorProp::fromColor($compiled) ->setIndex($v->index) - ->setName(substr($v->name, 1)); + ->setName($v->name); } return $compiled; diff --git a/library/Icinga/Less/DeferredColorProp.php b/library/Icinga/Less/DeferredColorProp.php new file mode 100644 index 000000000..c9c39adf5 --- /dev/null +++ b/library/Icinga/Less/DeferredColorProp.php @@ -0,0 +1,136 @@ +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 index f12cc03bf..b4b72a074 100644 --- a/library/Icinga/Less/LightMode.php +++ b/library/Icinga/Less/LightMode.php @@ -1,5 +1,5 @@ name === 'var') { - if ($this->callingVar !== false) { - throw new LogicException('Already calling var'); - } - - $this->callingVar = spl_object_hash($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 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) { @@ -136,6 +130,15 @@ CSS; $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; @@ -178,7 +181,7 @@ CSS; public function visitVariable($v) { - if ($this->callingVar !== false || $this->definingVariable !== false) { + if ($this->definingVariable !== false) { return $v; } @@ -186,6 +189,16 @@ CSS; ->setVariable($v); } + public function visitColor($c) + { + if ($this->definingVariable !== false) { + // Make sure that all less tree colors do have a proper name + $c->name = $this->variableOrigin->name; + } + + return $c; + } + public function run($node) { $this->lightMode = new LightMode(); diff --git a/test/php/library/Icinga/Util/LessParserTest.php b/test/php/library/Icinga/Util/LessParserTest.php index e32e32056..7f36c52cb 100644 --- a/test/php/library/Icinga/Util/LessParserTest.php +++ b/test/php/library/Icinga/Util/LessParserTest.php @@ -60,6 +60,93 @@ LESS ); } + public function testNestedVariables() + { + $this->assertEquals( + <<compileLess(<<assertEquals( + <<compileLess(<<assertEquals( + <<compileLess(<<assertEquals(