1<?php
2
3/*
4 * This file is part of the Assetic package, an OpenSky project.
5 *
6 * (c) 2010-2014 OpenSky Project Inc
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Assetic\Filter;
13
14use Assetic\Asset\AssetInterface;
15use Assetic\Exception\FilterException;
16use Assetic\Filter\Sass\BaseSassFilter;
17use Assetic\Util\FilesystemUtils;
18
19/**
20 * Loads Compass files.
21 *
22 * @link http://compass-style.org/
23 * @author Maxime Thirouin <maxime.thirouin@gmail.com>
24 */
25class CompassFilter extends BaseSassFilter
26{
27    private $compassPath;
28    private $rubyPath;
29    private $scss;
30
31    // sass options
32    private $unixNewlines;
33    private $debugInfo;
34    private $cacheLocation;
35    private $noCache;
36
37    // compass options
38    private $force;
39    private $style;
40    private $quiet;
41    private $boring;
42    private $noLineComments;
43    private $imagesDir;
44    private $javascriptsDir;
45    private $fontsDir;
46    private $relativeAssets;
47
48    // compass configuration file options
49    private $plugins = array();
50    private $httpPath;
51    private $httpImagesPath;
52    private $httpFontsPath;
53    private $httpGeneratedImagesPath;
54    private $generatedImagesPath;
55    private $httpJavascriptsPath;
56    private $homeEnv = true;
57
58    public function __construct($compassPath = '/usr/bin/compass', $rubyPath = null)
59    {
60        $this->compassPath = $compassPath;
61        $this->rubyPath = $rubyPath;
62        $this->cacheLocation = FilesystemUtils::getTemporaryDirectory();
63
64        if ('cli' !== php_sapi_name()) {
65            $this->boring = true;
66        }
67    }
68
69    public function setScss($scss)
70    {
71        $this->scss = $scss;
72    }
73
74    // sass options setters
75    public function setUnixNewlines($unixNewlines)
76    {
77        $this->unixNewlines = $unixNewlines;
78    }
79
80    public function setDebugInfo($debugInfo)
81    {
82        $this->debugInfo = $debugInfo;
83    }
84
85    public function setCacheLocation($cacheLocation)
86    {
87        $this->cacheLocation = $cacheLocation;
88    }
89
90    public function setNoCache($noCache)
91    {
92        $this->noCache = $noCache;
93    }
94
95    // compass options setters
96    public function setForce($force)
97    {
98        $this->force = $force;
99    }
100
101    public function setStyle($style)
102    {
103        $this->style = $style;
104    }
105
106    public function setQuiet($quiet)
107    {
108        $this->quiet = $quiet;
109    }
110
111    public function setBoring($boring)
112    {
113        $this->boring = $boring;
114    }
115
116    public function setNoLineComments($noLineComments)
117    {
118        $this->noLineComments = $noLineComments;
119    }
120
121    public function setImagesDir($imagesDir)
122    {
123        $this->imagesDir = $imagesDir;
124    }
125
126    public function setJavascriptsDir($javascriptsDir)
127    {
128        $this->javascriptsDir = $javascriptsDir;
129    }
130
131    public function setFontsDir($fontsDir)
132    {
133        $this->fontsDir = $fontsDir;
134    }
135
136    // compass configuration file options setters
137    public function setPlugins(array $plugins)
138    {
139        $this->plugins = $plugins;
140    }
141
142    public function addPlugin($plugin)
143    {
144        $this->plugins[] = $plugin;
145    }
146
147    public function setHttpPath($httpPath)
148    {
149        $this->httpPath = $httpPath;
150    }
151
152    public function setHttpImagesPath($httpImagesPath)
153    {
154        $this->httpImagesPath = $httpImagesPath;
155    }
156
157    public function setHttpFontsPath($httpFontsPath)
158    {
159        $this->httpFontsPath = $httpFontsPath;
160    }
161
162    public function setHttpGeneratedImagesPath($httpGeneratedImagesPath)
163    {
164        $this->httpGeneratedImagesPath = $httpGeneratedImagesPath;
165    }
166
167    public function setGeneratedImagesPath($generatedImagesPath)
168    {
169        $this->generatedImagesPath = $generatedImagesPath;
170    }
171
172    public function setHttpJavascriptsPath($httpJavascriptsPath)
173    {
174        $this->httpJavascriptsPath = $httpJavascriptsPath;
175    }
176
177    public function setHomeEnv($homeEnv)
178    {
179        $this->homeEnv = $homeEnv;
180    }
181
182    public function setRelativeAssets($relativeAssets)
183    {
184        $this->relativeAssets = $relativeAssets;
185    }
186
187    public function filterLoad(AssetInterface $asset)
188    {
189        $loadPaths = $this->loadPaths;
190        if ($dir = $asset->getSourceDirectory()) {
191            $loadPaths[] = $dir;
192        }
193
194        $tempDir = $this->cacheLocation ? $this->cacheLocation : FilesystemUtils::getTemporaryDirectory();
195
196        $compassProcessArgs = array(
197            $this->compassPath,
198            'compile',
199            $tempDir,
200        );
201        if (null !== $this->rubyPath) {
202            $compassProcessArgs = array_merge(explode(' ', $this->rubyPath), $compassProcessArgs);
203        }
204
205        $pb = $this->createProcessBuilder($compassProcessArgs);
206
207        if ($this->force) {
208            $pb->add('--force');
209        }
210
211        if ($this->style) {
212            $pb->add('--output-style')->add($this->style);
213        }
214
215        if ($this->quiet) {
216            $pb->add('--quiet');
217        }
218
219        if ($this->boring) {
220            $pb->add('--boring');
221        }
222
223        if ($this->noLineComments) {
224            $pb->add('--no-line-comments');
225        }
226
227        // these three options are not passed into the config file
228        // because like this, compass adapts this to be xxx_dir or xxx_path
229        // whether it's an absolute path or not
230        if ($this->imagesDir) {
231            $pb->add('--images-dir')->add($this->imagesDir);
232        }
233
234        if ($this->relativeAssets) {
235            $pb->add('--relative-assets');
236        }
237
238        if ($this->javascriptsDir) {
239            $pb->add('--javascripts-dir')->add($this->javascriptsDir);
240        }
241
242        if ($this->fontsDir) {
243            $pb->add('--fonts-dir')->add($this->fontsDir);
244        }
245
246        // options in config file
247        $optionsConfig = array();
248
249        if (!empty($loadPaths)) {
250            $optionsConfig['additional_import_paths'] = $loadPaths;
251        }
252
253        if ($this->unixNewlines) {
254            $optionsConfig['sass_options']['unix_newlines'] = true;
255        }
256
257        if ($this->debugInfo) {
258            $optionsConfig['sass_options']['debug_info'] = true;
259        }
260
261        if ($this->cacheLocation) {
262            $optionsConfig['sass_options']['cache_location'] = $this->cacheLocation;
263        }
264
265        if ($this->noCache) {
266            $optionsConfig['sass_options']['no_cache'] = true;
267        }
268
269        if ($this->httpPath) {
270            $optionsConfig['http_path'] = $this->httpPath;
271        }
272
273        if ($this->httpImagesPath) {
274            $optionsConfig['http_images_path'] = $this->httpImagesPath;
275        }
276
277        if ($this->httpFontsPath) {
278            $optionsConfig['http_fonts_path'] = $this->httpFontsPath;
279        }
280
281        if ($this->httpGeneratedImagesPath) {
282            $optionsConfig['http_generated_images_path'] = $this->httpGeneratedImagesPath;
283        }
284
285        if ($this->generatedImagesPath) {
286            $optionsConfig['generated_images_path'] = $this->generatedImagesPath;
287        }
288
289        if ($this->httpJavascriptsPath) {
290            $optionsConfig['http_javascripts_path'] = $this->httpJavascriptsPath;
291        }
292
293        // options in configuration file
294        if (count($optionsConfig)) {
295            $config = array();
296            foreach ($this->plugins as $plugin) {
297                $config[] = sprintf("require '%s'", addcslashes($plugin, '\\'));
298            }
299            foreach ($optionsConfig as $name => $value) {
300                if (!is_array($value)) {
301                    $config[] = sprintf('%s = "%s"', $name, addcslashes($value, '\\'));
302                } elseif (!empty($value)) {
303                    $config[] = sprintf('%s = %s', $name, $this->formatArrayToRuby($value));
304                }
305            }
306
307            $configFile = tempnam($tempDir, 'assetic_compass');
308            file_put_contents($configFile, implode("\n", $config)."\n");
309            $pb->add('--config')->add($configFile);
310        }
311
312        $pb->add('--sass-dir')->add('')->add('--css-dir')->add('');
313
314        // compass choose the type (sass or scss from the filename)
315        if (null !== $this->scss) {
316            $type = $this->scss ? 'scss' : 'sass';
317        } elseif ($path = $asset->getSourcePath()) {
318            // FIXME: what if the extension is something else?
319            $type = pathinfo($path, PATHINFO_EXTENSION);
320        } else {
321            $type = 'scss';
322        }
323
324        $tempName = tempnam($tempDir, 'assetic_compass');
325        unlink($tempName); // FIXME: don't use tempnam() here
326
327        // input
328        $input = $tempName.'.'.$type;
329
330        // work-around for https://github.com/chriseppstein/compass/issues/748
331        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
332            $input = str_replace('\\', '/', $input);
333        }
334
335        $pb->add($input);
336        file_put_contents($input, $asset->getContent());
337
338        // output
339        $output = $tempName.'.css';
340
341        if ($this->homeEnv) {
342            // it's not really usefull but... https://github.com/chriseppstein/compass/issues/376
343            $pb->setEnv('HOME', FilesystemUtils::getTemporaryDirectory());
344            $this->mergeEnv($pb);
345        }
346
347        $proc = $pb->getProcess();
348        $code = $proc->run();
349
350        if (0 !== $code) {
351            unlink($input);
352            if (isset($configFile)) {
353                unlink($configFile);
354            }
355
356            throw FilterException::fromProcess($proc)->setInput($asset->getContent());
357        }
358
359        $asset->setContent(file_get_contents($output));
360
361        unlink($input);
362        unlink($output);
363        if (isset($configFile)) {
364            unlink($configFile);
365        }
366    }
367
368    public function filterDump(AssetInterface $asset)
369    {
370    }
371
372    private function formatArrayToRuby($array)
373    {
374        $output = array();
375
376        // does we have an associative array ?
377        if (count(array_filter(array_keys($array), "is_numeric")) != count($array)) {
378            foreach ($array as $name => $value) {
379                $output[] = sprintf('    :%s => "%s"', $name, addcslashes($value, '\\'));
380            }
381            $output = "{\n".implode(",\n", $output)."\n}";
382        } else {
383            foreach ($array as $name => $value) {
384                $output[] = sprintf('    "%s"', addcslashes($value, '\\'));
385            }
386            $output = "[\n".implode(",\n", $output)."\n]";
387        }
388
389        return $output;
390    }
391}
392