summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Web/LessCompiler.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Web/LessCompiler.php')
-rw-r--r--library/Icinga/Web/LessCompiler.php257
1 files changed, 257 insertions, 0 deletions
diff --git a/library/Icinga/Web/LessCompiler.php b/library/Icinga/Web/LessCompiler.php
new file mode 100644
index 0000000..1f72560
--- /dev/null
+++ b/library/Icinga/Web/LessCompiler.php
@@ -0,0 +1,257 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Application\Logger;
+use Icinga\Util\LessParser;
+use Less_Exception_Parser;
+
+/**
+ * Compile LESS into CSS
+ *
+ * Comments will be removed always. lessc is messing them up.
+ */
+class LessCompiler
+{
+ /**
+ * lessphp compiler
+ *
+ * @var LessParser
+ */
+ protected $lessc;
+
+ /**
+ * 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
+ *
+ * @var string
+ */
+ protected $source;
+
+ /**
+ * Path of the LESS theme
+ *
+ * @var string
+ */
+ protected $theme;
+
+ /**
+ * Path of the LESS theme mode
+ *
+ * @var string
+ */
+ protected $themeMode;
+
+ /**
+ * Create a new LESS compiler
+ */
+ public function __construct()
+ {
+ $this->lessc = new LessParser();
+ // Discourage usage of import because we're caching based on an explicit list of LESS files to compile
+ $this->lessc->importDisabled = true;
+ }
+
+ /**
+ * Add a Web 2 LESS file
+ *
+ * @param string $lessFile Path to the LESS file
+ *
+ * @return $this
+ */
+ public function addLessFile($lessFile)
+ {
+ $this->lessFiles[] = realpath($lessFile);
+ return $this;
+ }
+
+ /**
+ * Add a module LESS file
+ *
+ * @param string $moduleName Name of the module
+ * @param string $lessFile Path to the LESS file
+ *
+ * @return $this
+ */
+ public function addModuleLessFile($moduleName, $lessFile)
+ {
+ if (! isset($this->moduleLessFiles[$moduleName])) {
+ $this->moduleLessFiles[$moduleName] = array();
+ }
+ $this->moduleLessFiles[$moduleName][] = realpath($lessFile);
+ return $this;
+ }
+
+ /**
+ * Get the list of LESS files added to the compiler
+ *
+ * @return string[]
+ */
+ public function getLessFiles()
+ {
+ $lessFiles = $this->lessFiles;
+
+ foreach ($this->moduleLessFiles as $moduleLessFiles) {
+ $lessFiles = array_merge($lessFiles, $moduleLessFiles);
+ }
+
+ if ($this->theme !== null) {
+ $lessFiles[] = $this->theme;
+ }
+
+ if ($this->themeMode !== null) {
+ $lessFiles[] = $this->themeMode;
+ }
+
+ return $lessFiles;
+ }
+
+ /**
+ * Set the path to the LESS theme
+ *
+ * @param ?string $theme Path to the LESS theme
+ *
+ * @return $this
+ */
+ public function setTheme($theme)
+ {
+ if ($theme === null || (is_file($theme) && is_readable($theme))) {
+ $this->theme = $theme;
+ } else {
+ Logger::error('Can\t load theme %s. Make sure that the theme exists and is readable', $theme);
+ }
+ return $this;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Instruct the compiler to minify CSS
+ *
+ * @return $this
+ */
+ public function compress()
+ {
+ $this->lessc->setFormatter('compressed');
+ return $this;
+ }
+
+ /**
+ * Render to CSS
+ *
+ * @return string
+ */
+ public function render()
+ {
+ foreach ($this->lessFiles as $lessFile) {
+ $this->source .= file_get_contents($lessFile);
+ }
+
+ $moduleCss = '';
+ $exportedVars = [];
+ foreach ($this->moduleLessFiles as $moduleName => $moduleLessFiles) {
+ $moduleCss .= '.icinga-module.module-' . $moduleName . ' {';
+
+ foreach ($moduleLessFiles as $moduleLessFile) {
+ $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;
+ }
+
+ $moduleCss .= '}';
+ }
+
+ $this->source .= $moduleCss;
+
+ $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;
+
+ if ($this->theme !== null) {
+ $this->source .= file_get_contents($this->theme);
+ }
+
+ if ($this->themeMode !== null) {
+ $this->source .= file_get_contents($this->themeMode);
+ }
+
+ try {
+ return preg_replace(
+ '/(\.icinga-module\.module-[^\s]+) (#layout\.[^\s]+)/m',
+ '\2 \1',
+ $this->lessc->compile($this->source)
+ );
+ } catch (Less_Exception_Parser $e) {
+ $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);
+ }
+ }
+}