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;
16
17/**
18 * CleanCss filter.
19 *
20 * @link https://github.com/jakubpawlowicz/clean-css
21 * @author Jakub Pawlowicz <http://JakubPawlowicz.com>
22 */
23class CleanCssFilter extends BaseNodeFilter
24{
25    private $cleanCssBin;
26    private $nodeBin;
27
28    private $keepLineBreaks;
29    private $compatibility;
30    private $debug;
31    private $rootPath;
32    private $skipImport = true;
33    private $timeout;
34    private $semanticMerging;
35    private $roundingPrecision;
36    private $removeSpecialComments;
37    private $onlyKeepFirstSpecialComment;
38    private $skipAdvanced;
39    private $skipAggresiveMerging;
40    private $skipImportFrom;
41    private $mediaMerging;
42    private $skipRebase;
43    private $skipRestructuring;
44    private $skipShorthandCompacting;
45    private $sourceMap;
46    private $sourceMapInlineSources;
47
48
49    /**
50     * @param string $cleanCssBin  Absolute path to the cleancss executable
51     * @param string $nodeBin      Absolute path to the folder containg node.js executable
52     */
53    public function __construct($cleanCssBin = '/usr/bin/cleancss', $nodeBin = null)
54    {
55        $this->cleanCssBin = $cleanCssBin;
56        $this->nodeBin = $nodeBin;
57    }
58
59    /**
60     * Keep line breaks
61     * @param bool $keepLineBreaks True to enable
62     */
63    public function setKeepLineBreaks($keepLineBreaks)
64    {
65        $this->keepLineBreaks = $keepLineBreaks;
66    }
67
68    /**
69     * Remove all special comments
70     * @param bool $removeSpecialComments True to enable
71     */ // i.e.  /*! comment */
72    public function setRemoveSpecialComments($removeSpecialComments)
73    {
74        $this->removeSpecialComments = $removeSpecialComments;
75    }
76
77    /**
78     * Remove all special comments except the first one
79     * @param bool $onlyKeepFirstSpecialComment True to enable
80     */
81    public function setOnlyKeepFirstSpecialComment($onlyKeepFirstSpecialComment)
82    {
83        $this->onlyKeepFirstSpecialComment = $onlyKeepFirstSpecialComment;
84    }
85    /**
86     * Enables unsafe mode by assuming BEM-like semantic stylesheets (warning, this may break your styling!)
87     * @param bool $semanticMerging True to enable
88     */
89    public function setSemanticMerging($semanticMerging)
90    {
91        $this->semanticMerging = $semanticMerging;
92    }
93
94    /**
95     * A root path to which resolve absolute @import rules
96     * @param string $rootPath
97     */
98    public function setRootPath($rootPath)
99    {
100        $this->rootPath = $rootPath;
101    }
102
103    /**
104     * Disable @import processing
105     * @param bool $skipImport True to enable
106     */
107    public function setSkipImport($skipImport)
108    {
109        $this->skipImport = $skipImport;
110    }
111    /**
112     * Per connection timeout when fetching remote @imports; defaults to 5 seconds
113     * @param int $timeout
114     */
115    public function setTimeout($timeout)
116    {
117        $this->timeout = $timeout;
118    }
119
120    /**
121     * Disable URLs rebasing
122     * @param bool $skipRebase True to enable
123     */
124    public function setSkipRebase($skipRebase)
125    {
126        $this->skipRebase = $skipRebase;
127    }
128
129    /**
130     * Disable restructuring optimizations
131     * @param bool $skipRestructuring True to enable
132     */
133    public function setSkipRestructuring($skipRestructuring)
134    {
135        $this->skipRestructuring = $skipRestructuring;
136    }
137
138    /**
139     * Disable shorthand compacting
140     * @param bool $skipShorthandCompacting True to enable
141     */
142    public function setSkipShorthandCompacting($skipShorthandCompacting)
143    {
144        $this->skipShorthandCompacting = $skipShorthandCompacting;
145    }
146
147    /**
148     * Enables building input's source map
149     * @param bool $sourceMap True to enable
150     */
151    public function setSourceMap($sourceMap)
152    {
153        $this->sourceMap = $sourceMap;
154    }
155
156    /**
157     * Enables inlining sources inside source maps
158     * @param bool $sourceMapInlineSources True to enable
159     */
160    public function setSourceMapInlineSources($sourceMapInlineSources)
161    {
162        $this->sourceMapInlineSources = $sourceMapInlineSources;
163    }
164
165    /**
166     * Disable advanced optimizations - selector & property merging, reduction, etc.
167     * @param bool $skipAdvanced True to enable
168     */
169    public function setSkipAdvanced($skipAdvanced)
170    {
171        $this->skipAdvanced = $skipAdvanced;
172    }
173
174    /**
175     * Disable properties merging based on their order
176     * @param bool $skipAggresiveMerging True to enable
177     */
178    public function setSkipAggresiveMerging($skipAggresiveMerging)
179    {
180        $this->skipAggresiveMerging = $skipAggresiveMerging;
181    }
182
183    /**
184     * Disable @import processing for specified rules
185     * @param string $skipImportFrom
186     */
187    public function setSkipImportFrom($skipImportFrom)
188    {
189        $this->skipImportFrom = $skipImportFrom;
190    }
191
192    /**
193     * Disable @media merging
194     * @param bool $mediaMerging True to enable
195     */
196    public function setMediaMerging($mediaMerging)
197    {
198        $this->mediaMerging = $mediaMerging;
199    }
200
201    /**
202     * Rounds to `N` decimal places. Defaults to 2. -1 disables rounding.
203     * @param int $roundingPrecision
204     */
205    public function setRoundingPrecision($roundingPrecision)
206    {
207        $this->roundingPrecision = $roundingPrecision;
208    }
209
210    /**
211     * Force compatibility mode (see https://github.com/jakubpawlowicz/clean-css/blob/master/README.md#how-to-set-compatibility-mode for advanced examples)
212     * @param string $compatibility
213     */
214    public function setCompatibility($compatibility)
215    {
216        $this->compatibility = $compatibility;
217    }
218
219    /**
220     * Shows debug information (minification time & compression efficiency)
221     * @param bool $debug True to enable
222     */
223    public function setDebug($debug)
224    {
225        $this->debug = $debug;
226    }
227
228
229    /**
230     * @see Assetic\Filter\FilterInterface::filterLoad()
231     */
232    public function filterLoad(AssetInterface $asset)
233    {
234    }
235
236
237    /**
238     * Run the asset through CleanCss
239     *
240     * @see Assetic\Filter\FilterInterface::filterDump()
241     */
242    public function filterDump(AssetInterface $asset)
243    {
244        $pb = $this->createProcessBuilder($this->nodeBin
245            ? array($this->nodeBin, $this->cleanCssBin)
246            : array($this->cleanCssBin));
247
248        if ($this->keepLineBreaks) {
249            $pb->add('--keep-line-breaks');
250        }
251
252        if ($this->compatibility) {
253            $pb->add('--compatibility ' .$this->compatibility);
254        }
255
256        if ($this->debug) {
257            $pb->add('--debug');
258        }
259
260        if ($this->rootPath) {
261            $pb->add('--root ' .$this->rootPath);
262        }
263
264        if ($this->skipImport) {
265            $pb->add('--skip-import');
266        }
267
268        if ($this->timeout) {
269            $pb->add('--timeout ' .$this->timeout);
270        }
271
272        if ($this->roundingPrecision) {
273            $pb->add('--rounding-precision ' .$this->roundingPrecision);
274        }
275
276        if ($this->removeSpecialComments) {
277            $pb->add('--s0');
278        }
279
280        if ($this->onlyKeepFirstSpecialComment) {
281            $pb->add('--s1');
282        }
283
284        if ($this->semanticMerging) {
285            $pb->add('--semantic-merging');
286        }
287
288        if ($this->skipAdvanced) {
289            $pb->add('--skip-advanced');
290        }
291
292        if ($this->skipAggresiveMerging) {
293            $pb->add('--skip-aggressive-merging');
294        }
295
296        if ($this->skipImportFrom) {
297            $pb->add('--skip-import-from ' .$this->skipImportFrom);
298        }
299
300        if ($this->mediaMerging) {
301            $pb->add('--skip-media-merging');
302        }
303
304        if ($this->skipRebase) {
305            $pb->add('--skip-rebase');
306        }
307
308        if ($this->skipRestructuring) {
309            $pb->add('--skip-restructuring');
310        }
311
312        if ($this->skipShorthandCompacting) {
313            $pb->add('--skip-shorthand-compacting');
314        }
315
316        if ($this->sourceMap) {
317            $pb->add('--source-map');
318        }
319
320        if ($this->sourceMapInlineSources) {
321            $pb->add('--source-map-inline-sources');
322        }
323        // input and output files
324        $input = tempnam(sys_get_temp_dir(), 'input');
325
326        file_put_contents($input, $asset->getContent());
327        $pb->add($input);
328
329        $proc = $pb->getProcess();
330        $code = $proc->run();
331        unlink($input);
332
333        if (127 === $code) {
334            throw new \RuntimeException('Path to node executable could not be resolved.');
335        }
336
337        if (0 !== $code) {
338            throw FilterException::fromProcess($proc)->setInput($asset->getContent());
339        }
340
341        $asset->setContent($proc->getOutput());
342    }
343}
344