2013-10-16 12:00:09 +02:00
|
|
|
<?php
|
2016-02-08 15:41:00 +01:00
|
|
|
/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
|
2013-10-16 12:00:09 +02:00
|
|
|
|
|
|
|
namespace Icinga\Web;
|
|
|
|
|
2015-11-27 16:40:17 +01:00
|
|
|
use Icinga\Application\Logger;
|
2021-01-15 16:32:06 +01:00
|
|
|
use Icinga\Util\LessParser;
|
2022-02-10 10:50:39 +01:00
|
|
|
use Less_Exception_Parser;
|
2013-10-16 12:00:09 +02:00
|
|
|
|
|
|
|
/**
|
2015-11-27 16:40:17 +01:00
|
|
|
* Compile LESS into CSS
|
|
|
|
*
|
|
|
|
* Comments will be removed always. lessc is messing them up.
|
2013-10-16 12:00:09 +02:00
|
|
|
*/
|
|
|
|
class LessCompiler
|
|
|
|
{
|
|
|
|
/**
|
2015-11-27 16:40:17 +01:00
|
|
|
* lessphp compiler
|
2013-10-16 12:00:09 +02:00
|
|
|
*
|
2021-01-15 16:32:06 +01:00
|
|
|
* @var LessParser
|
2013-10-16 12:00:09 +02:00
|
|
|
*/
|
2015-11-27 16:40:17 +01:00
|
|
|
protected $lessc;
|
2013-10-16 12:00:09 +02:00
|
|
|
|
|
|
|
/**
|
2015-11-27 16:40:17 +01:00
|
|
|
* Array of LESS files
|
|
|
|
*
|
|
|
|
* @var string[]
|
|
|
|
*/
|
|
|
|
protected $lessFiles = array();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Array of module LESS files indexed by module names
|
|
|
|
*
|
|
|
|
* @var array[]
|
|
|
|
*/
|
|
|
|
protected $moduleLessFiles = array();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* LESS source
|
2013-10-16 12:00:09 +02:00
|
|
|
*
|
2015-11-27 16:40:17 +01:00
|
|
|
* @var string
|
2013-10-16 12:00:09 +02:00
|
|
|
*/
|
2015-11-27 16:40:17 +01:00
|
|
|
protected $source;
|
2013-10-16 12:00:09 +02:00
|
|
|
|
2015-11-27 16:40:17 +01:00
|
|
|
/**
|
|
|
|
* Path of the LESS theme
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $theme;
|
2013-10-16 14:48:22 +02:00
|
|
|
|
2021-06-11 16:53:30 +02:00
|
|
|
/**
|
|
|
|
* Path of the LESS theme mode
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $themeMode;
|
|
|
|
|
2013-10-16 12:00:09 +02:00
|
|
|
/**
|
2015-11-27 16:40:17 +01:00
|
|
|
* Create a new LESS compiler
|
2022-02-09 21:32:50 +01:00
|
|
|
*
|
|
|
|
* @param bool $disableModes Disable replacing compiled Less colors with CSS var() function calls and don't inject
|
|
|
|
* light mode calls
|
2013-10-16 12:00:09 +02:00
|
|
|
*/
|
2022-02-09 21:32:50 +01:00
|
|
|
public function __construct($disableModes = false)
|
2013-10-16 12:00:09 +02:00
|
|
|
{
|
2022-02-09 21:32:50 +01:00
|
|
|
$this->lessc = new LessParser($disableModes);
|
2015-11-27 16:40:17 +01:00
|
|
|
// Discourage usage of import because we're caching based on an explicit list of LESS files to compile
|
|
|
|
$this->lessc->importDisabled = true;
|
2013-10-16 12:00:09 +02:00
|
|
|
}
|
|
|
|
|
2015-07-22 12:45:10 +02:00
|
|
|
/**
|
2015-11-27 16:40:17 +01:00
|
|
|
* Add a Web 2 LESS file
|
|
|
|
*
|
|
|
|
* @param string $lessFile Path to the LESS file
|
2015-07-22 12:45:10 +02:00
|
|
|
*
|
|
|
|
* @return $this
|
|
|
|
*/
|
2015-11-27 16:40:17 +01:00
|
|
|
public function addLessFile($lessFile)
|
2015-07-22 12:45:10 +02:00
|
|
|
{
|
2016-03-29 11:39:41 +02:00
|
|
|
$this->lessFiles[] = realpath($lessFile);
|
2014-02-18 19:08:21 +01:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2013-10-16 12:00:09 +02:00
|
|
|
/**
|
2015-11-27 16:40:17 +01:00
|
|
|
* Add a module LESS file
|
2013-10-16 12:00:09 +02:00
|
|
|
*
|
2015-11-27 16:40:17 +01:00
|
|
|
* @param string $moduleName Name of the module
|
|
|
|
* @param string $lessFile Path to the LESS file
|
|
|
|
*
|
|
|
|
* @return $this
|
2013-10-16 12:00:09 +02:00
|
|
|
*/
|
2015-11-27 16:40:17 +01:00
|
|
|
public function addModuleLessFile($moduleName, $lessFile)
|
2013-10-16 12:00:09 +02:00
|
|
|
{
|
2015-11-27 16:40:17 +01:00
|
|
|
if (! isset($this->moduleLessFiles[$moduleName])) {
|
|
|
|
$this->moduleLessFiles[$moduleName] = array();
|
2014-02-18 19:08:21 +01:00
|
|
|
}
|
2016-03-29 11:39:41 +02:00
|
|
|
$this->moduleLessFiles[$moduleName][] = realpath($lessFile);
|
2014-02-18 19:08:21 +01:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2015-11-27 16:40:17 +01:00
|
|
|
/**
|
|
|
|
* Get the list of LESS files added to the compiler
|
|
|
|
*
|
|
|
|
* @return string[]
|
|
|
|
*/
|
|
|
|
public function getLessFiles()
|
2014-02-18 19:08:21 +01:00
|
|
|
{
|
2016-03-29 11:39:41 +02:00
|
|
|
$lessFiles = $this->lessFiles;
|
|
|
|
|
|
|
|
foreach ($this->moduleLessFiles as $moduleLessFiles) {
|
|
|
|
$lessFiles = array_merge($lessFiles, $moduleLessFiles);
|
|
|
|
}
|
|
|
|
|
2015-11-27 16:40:17 +01:00
|
|
|
if ($this->theme !== null) {
|
|
|
|
$lessFiles[] = $this->theme;
|
2014-02-18 19:08:21 +01:00
|
|
|
}
|
2021-06-11 16:53:30 +02:00
|
|
|
|
|
|
|
if ($this->themeMode !== null) {
|
|
|
|
$lessFiles[] = $this->themeMode;
|
|
|
|
}
|
|
|
|
|
2015-11-27 16:40:17 +01:00
|
|
|
return $lessFiles;
|
2014-02-18 19:08:21 +01:00
|
|
|
}
|
|
|
|
|
2013-10-16 12:00:09 +02:00
|
|
|
/**
|
2015-11-27 16:40:17 +01:00
|
|
|
* Set the path to the LESS theme
|
|
|
|
*
|
2022-03-01 16:11:28 +01:00
|
|
|
* @param ?string $theme Path to the LESS theme
|
2013-10-16 12:00:09 +02:00
|
|
|
*
|
2015-11-27 16:40:17 +01:00
|
|
|
* @return $this
|
2013-10-16 12:00:09 +02:00
|
|
|
*/
|
2015-11-27 16:40:17 +01:00
|
|
|
public function setTheme($theme)
|
2013-10-16 12:00:09 +02:00
|
|
|
{
|
2022-03-01 16:11:28 +01:00
|
|
|
if ($theme === null || (is_file($theme) && is_readable($theme))) {
|
2015-12-07 13:51:30 +01:00
|
|
|
$this->theme = $theme;
|
|
|
|
} else {
|
|
|
|
Logger::error('Can\t load theme %s. Make sure that the theme exists and is readable', $theme);
|
|
|
|
}
|
2015-11-27 16:40:17 +01:00
|
|
|
return $this;
|
2013-10-16 12:00:09 +02:00
|
|
|
}
|
|
|
|
|
2021-06-11 16:53:30 +02:00
|
|
|
/**
|
|
|
|
* Set the path to the LESS theme mode
|
|
|
|
*
|
|
|
|
* @param string $themeMode Path to the LESS theme mode
|
|
|
|
*
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setThemeMode($themeMode)
|
|
|
|
{
|
|
|
|
if (is_file($themeMode) && is_readable($themeMode)) {
|
|
|
|
$this->themeMode = $themeMode;
|
|
|
|
} else {
|
|
|
|
Logger::error('Can\t load theme mode %s. Make sure that the theme mode exists and is readable', $themeMode);
|
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2013-10-16 12:00:09 +02:00
|
|
|
/**
|
2015-11-27 16:40:17 +01:00
|
|
|
* Instruct the compiler to minify CSS
|
2013-10-16 12:00:09 +02:00
|
|
|
*
|
2015-11-27 16:40:17 +01:00
|
|
|
* @return $this
|
2013-10-16 12:00:09 +02:00
|
|
|
*/
|
2015-11-27 16:40:17 +01:00
|
|
|
public function compress()
|
2013-10-16 12:00:09 +02:00
|
|
|
{
|
2015-11-27 16:40:17 +01:00
|
|
|
$this->lessc->setFormatter('compressed');
|
|
|
|
return $this;
|
2013-10-16 12:00:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-11-27 16:40:17 +01:00
|
|
|
* Render to CSS
|
|
|
|
*
|
|
|
|
* @return string
|
2013-10-16 12:00:09 +02:00
|
|
|
*/
|
2015-11-27 16:40:17 +01:00
|
|
|
public function render()
|
2013-10-16 12:00:09 +02:00
|
|
|
{
|
2015-11-27 16:40:17 +01:00
|
|
|
foreach ($this->lessFiles as $lessFile) {
|
|
|
|
$this->source .= file_get_contents($lessFile);
|
|
|
|
}
|
|
|
|
|
|
|
|
$moduleCss = '';
|
2020-11-19 11:30:34 +01:00
|
|
|
$exportedVars = [];
|
2015-11-27 16:40:17 +01:00
|
|
|
foreach ($this->moduleLessFiles as $moduleName => $moduleLessFiles) {
|
|
|
|
$moduleCss .= '.icinga-module.module-' . $moduleName . ' {';
|
2019-10-01 15:27:51 +02:00
|
|
|
|
2015-11-27 16:40:17 +01:00
|
|
|
foreach ($moduleLessFiles as $moduleLessFile) {
|
2020-11-19 11:30:34 +01:00
|
|
|
$content = file_get_contents($moduleLessFile);
|
|
|
|
|
|
|
|
$pattern = '/^@exports:\s*{((?:\s*@[^:}]+:[^;]*;\s+)+)};$/m';
|
|
|
|
if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
|
|
|
|
foreach ($matches as $match) {
|
|
|
|
$content = str_replace($match[0], '', $content);
|
|
|
|
foreach (explode("\n", trim($match[1])) as $line) {
|
|
|
|
list($name, $value) = explode(':', $line, 2);
|
|
|
|
$exportedVars[trim($name)] = trim($value, ' ;');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$moduleCss .= $content;
|
2013-10-16 12:00:09 +02:00
|
|
|
}
|
2020-11-19 11:30:34 +01:00
|
|
|
|
2015-11-27 16:40:17 +01:00
|
|
|
$moduleCss .= '}';
|
2013-10-16 12:00:09 +02:00
|
|
|
}
|
2015-11-27 16:40:17 +01:00
|
|
|
|
|
|
|
$this->source .= $moduleCss;
|
|
|
|
|
2020-11-19 11:30:34 +01:00
|
|
|
$varExports = '';
|
|
|
|
foreach ($exportedVars as $name => $value) {
|
|
|
|
$varExports .= sprintf("%s: %s;\n", $name, $value);
|
|
|
|
}
|
|
|
|
|
|
|
|
// exported vars are injected at the beginning to avoid that they are
|
|
|
|
// able to override other variables, that's what themes are for
|
|
|
|
$this->source = $varExports . "\n\n" . $this->source;
|
|
|
|
|
2015-11-27 16:40:17 +01:00
|
|
|
if ($this->theme !== null) {
|
|
|
|
$this->source .= file_get_contents($this->theme);
|
|
|
|
}
|
|
|
|
|
2021-06-11 16:53:30 +02:00
|
|
|
if ($this->themeMode !== null) {
|
|
|
|
$this->source .= file_get_contents($this->themeMode);
|
|
|
|
}
|
|
|
|
|
2021-11-02 17:06:45 +01:00
|
|
|
try {
|
|
|
|
return preg_replace(
|
|
|
|
'/(\.icinga-module\.module-[^\s]+) (#layout\.[^\s]+)/m',
|
|
|
|
'\2 \1',
|
|
|
|
$this->lessc->compile($this->source)
|
|
|
|
);
|
2022-02-10 10:50:39 +01:00
|
|
|
} catch (Less_Exception_Parser $e) {
|
2021-11-02 17:06:45 +01:00
|
|
|
$excerpt = substr($this->source, $e->index - 500, 1000);
|
|
|
|
|
|
|
|
$lines = [];
|
|
|
|
$found = false;
|
|
|
|
$pos = $e->index - 500;
|
|
|
|
foreach (explode("\n", $excerpt) as $i => $line) {
|
|
|
|
if ($i === 0) {
|
|
|
|
$pos += strlen($line);
|
|
|
|
$lines[] = '.. ' . $line;
|
|
|
|
} else {
|
|
|
|
$pos += strlen($line) + 1;
|
|
|
|
$sep = ' ';
|
|
|
|
if (! $found && $pos > $e->index) {
|
|
|
|
$found = true;
|
|
|
|
$sep = '!! ';
|
|
|
|
}
|
|
|
|
|
|
|
|
$lines[] = $sep . $line;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$lines[] = '..';
|
|
|
|
$excerpt = join("\n", $lines);
|
|
|
|
|
|
|
|
return sprintf("%s\n%s\n\n\n%s", $e->getMessage(), $e->getTraceAsString(), $excerpt);
|
|
|
|
}
|
2013-10-16 12:00:09 +02:00
|
|
|
}
|
|
|
|
}
|