Generate light mode calls from their definitions
This commit is contained in:
parent
5f46493148
commit
19f57644e8
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Less;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Less_Environment;
|
||||
|
||||
/**
|
||||
* Registry for light modes and the environments in which they are defined
|
||||
*/
|
||||
class LightMode
|
||||
{
|
||||
protected $envs = [];
|
||||
|
||||
protected $modes = [];
|
||||
|
||||
public function add($mode, $module = null)
|
||||
{
|
||||
if (array_key_exists($mode, $this->modes)) {
|
||||
throw new InvalidArgumentException("$mode already exists");
|
||||
}
|
||||
|
||||
$this->modes[$mode] = $module ?: null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isModule($mode)
|
||||
{
|
||||
return isset($this->modes[$mode]);
|
||||
}
|
||||
|
||||
public function list()
|
||||
{
|
||||
$modes = [];
|
||||
|
||||
foreach ($this->modes as $mode => $module) {
|
||||
$modes[$module][] = $mode;
|
||||
}
|
||||
|
||||
$byModule = $modes;
|
||||
unset($byModule[null]);
|
||||
|
||||
return [isset($modes[null]) ? $modes[null] : [], $byModule];
|
||||
}
|
||||
|
||||
public function getEnv($mode)
|
||||
{
|
||||
if (! isset($this->envs[$mode])) {
|
||||
throw new InvalidArgumentException("$mode does not exist");
|
||||
}
|
||||
|
||||
return $this->envs[$mode];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
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,61 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Less;
|
||||
|
||||
use Less_Environment;
|
||||
use Less_Tree_DetachedRuleset;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
$this->getLightMode()->setEnv($this->getName(), $env->copyEvalEnv($env->frames));
|
||||
|
||||
return $drs;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
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,29 @@
|
|||
<?php
|
||||
|
||||
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)
|
||||
{
|
||||
if ($this->getLightMode()->isModule($c->variable)) {
|
||||
return LightModeCall::fromRulesetCall($c)->setLightMode($this->getLightMode());
|
||||
}
|
||||
|
||||
return $c;
|
||||
}
|
||||
|
||||
public function run($node)
|
||||
{
|
||||
return $this->visitObj($node);
|
||||
}
|
||||
}
|
|
@ -2,17 +2,31 @@
|
|||
|
||||
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
|
||||
* Replace compiled Less colors with CSS var() function calls and inject light mode calls
|
||||
*
|
||||
* This basically works by replacing every visited Less variable with {@link ColorPropOrVariable},
|
||||
* 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;
|
||||
|
||||
/**
|
||||
|
@ -29,9 +43,21 @@ class Visitor extends Less_VisitorReplacing
|
|||
*
|
||||
* If that's the case, don't try to replace compiled Less colors with CSS var() function calls.
|
||||
*
|
||||
* @var bool|string
|
||||
* @var false|string
|
||||
*/
|
||||
protected $definingVar = false;
|
||||
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)
|
||||
{
|
||||
|
@ -53,13 +79,25 @@ class Visitor extends Less_VisitorReplacing
|
|||
}
|
||||
}
|
||||
|
||||
public function visitDetachedRuleset($rs)
|
||||
public function visitDetachedRuleset($drs)
|
||||
{
|
||||
// A detached ruleset is a variable definition in the first place,
|
||||
// so just reset that we define a variable.
|
||||
$this->definingVar = false;
|
||||
if ($this->variableOrigin->name === '@' . static::LIGHT_MODE_NAME) {
|
||||
$this->variableOrigin->name .= '-' . substr(sha1(uniqid(mt_rand(), true)), 0, 7);
|
||||
|
||||
return $rs;
|
||||
$this->lightMode->add($this->variableOrigin->name, $this->moduleSelector);
|
||||
|
||||
if ($this->moduleSelector !== false) {
|
||||
$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)
|
||||
|
@ -89,11 +127,12 @@ class Visitor extends Less_VisitorReplacing
|
|||
public function visitRule($r)
|
||||
{
|
||||
if ($r->name[0] === '@' && $r->variable) {
|
||||
if ($this->definingVar !== false) {
|
||||
if ($this->definingVariable !== false) {
|
||||
throw new LogicException('Already defining a variable');
|
||||
}
|
||||
|
||||
$this->definingVar = spl_object_hash($r);
|
||||
$this->definingVariable = spl_object_hash($r);
|
||||
$this->variableOrigin = $r;
|
||||
}
|
||||
|
||||
return $r;
|
||||
|
@ -101,14 +140,42 @@ class Visitor extends Less_VisitorReplacing
|
|||
|
||||
public function visitRuleOut($r)
|
||||
{
|
||||
if ($this->definingVar !== false && $this->definingVar === spl_object_hash($r)) {
|
||||
$this->definingVar = false;
|
||||
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->definingVar !== false) {
|
||||
if ($this->callingVar !== false || $this->definingVariable !== false) {
|
||||
return $v;
|
||||
}
|
||||
|
||||
|
@ -118,6 +185,43 @@ class Visitor extends Less_VisitorReplacing
|
|||
|
||||
public function run($node)
|
||||
{
|
||||
return $this->visitObj($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 module CSS selector.
|
||||
$calls = [];
|
||||
list($modes, $moduleModes) = $this->lightMode->list();
|
||||
if (! empty($modes)) {
|
||||
$calls[] = implode("();\n", $modes) . '();';
|
||||
}
|
||||
foreach ($moduleModes as $module => $modes) {
|
||||
$calls[] = "$module {\n" . implode("();\n", $modes) . "();\n}";
|
||||
}
|
||||
|
||||
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($parser::class, '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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue