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\Factory\AssetFactory;
17use Assetic\Util\FilesystemUtils;
18use Assetic\Util\LessUtils;
19
20/**
21 * Loads LESS files.
22 *
23 * @link http://lesscss.org/
24 * @author Kris Wallsmith <kris.wallsmith@gmail.com>
25 */
26class LessFilter extends BaseNodeFilter implements DependencyExtractorInterface
27{
28    private $nodeBin;
29
30    /**
31     * @var array
32     */
33    private $treeOptions;
34
35    /**
36     * @var array
37     */
38    private $parserOptions;
39
40    /**
41     * Load Paths
42     *
43     * A list of paths which less will search for includes.
44     *
45     * @var array
46     */
47    protected $loadPaths = array();
48
49    /**
50     * Constructor.
51     *
52     * @param string $nodeBin   The path to the node binary
53     * @param array  $nodePaths An array of node paths
54     */
55    public function __construct($nodeBin = '/usr/bin/node', array $nodePaths = array())
56    {
57        $this->nodeBin = $nodeBin;
58        $this->setNodePaths($nodePaths);
59        $this->treeOptions = array();
60        $this->parserOptions = array();
61    }
62
63    /**
64     * @param bool $compress
65     */
66    public function setCompress($compress)
67    {
68        $this->addTreeOption('compress', $compress);
69    }
70
71    public function setLoadPaths(array $loadPaths)
72    {
73        $this->loadPaths = $loadPaths;
74    }
75
76    /**
77     * Adds a path where less will search for includes
78     *
79     * @param string $path Load path (absolute)
80     */
81    public function addLoadPath($path)
82    {
83        $this->loadPaths[] = $path;
84    }
85
86    /**
87     * @param string $code
88     * @param string $value
89     */
90    public function addTreeOption($code, $value)
91    {
92        $this->treeOptions[$code] = $value;
93    }
94
95    /**
96     * @param string $code
97     * @param string $value
98     */
99    public function addParserOption($code, $value)
100    {
101        $this->parserOptions[$code] = $value;
102    }
103
104    public function filterLoad(AssetInterface $asset)
105    {
106        static $format = <<<'EOF'
107var less = require('less');
108var sys  = require(process.binding('natives').util ? 'util' : 'sys');
109
110less.render(%s, %s, function(error, css) {
111    if (error) {
112        less.writeError(error);
113        process.exit(2);
114    }
115    try {
116        if (typeof css == 'string') {
117            sys.print(css);
118        } else {
119            sys.print(css.css);
120        }
121    } catch (e) {
122        less.writeError(error);
123        process.exit(3);
124    }
125});
126
127EOF;
128
129        // parser options
130        $parserOptions = $this->parserOptions;
131        if ($dir = $asset->getSourceDirectory()) {
132            $parserOptions['paths'] = array($dir);
133            $parserOptions['filename'] = basename($asset->getSourcePath());
134        }
135
136        foreach ($this->loadPaths as $loadPath) {
137            $parserOptions['paths'][] = $loadPath;
138        }
139
140        $pb = $this->createProcessBuilder();
141
142        $pb->add($this->nodeBin)->add($input = FilesystemUtils::createTemporaryFile('less'));
143        file_put_contents($input, sprintf($format,
144            json_encode($asset->getContent()),
145            json_encode(array_merge($parserOptions, $this->treeOptions))
146        ));
147
148        $proc = $pb->getProcess();
149        $code = $proc->run();
150        unlink($input);
151
152        if (0 !== $code) {
153            throw FilterException::fromProcess($proc)->setInput($asset->getContent());
154        }
155
156        $asset->setContent($proc->getOutput());
157    }
158
159    public function filterDump(AssetInterface $asset)
160    {
161    }
162
163    /**
164     * @todo support for import-once
165     * @todo support for import (less) "lib.css"
166     */
167    public function getChildren(AssetFactory $factory, $content, $loadPath = null)
168    {
169        $loadPaths = $this->loadPaths;
170        if (null !== $loadPath) {
171            $loadPaths[] = $loadPath;
172        }
173
174        if (empty($loadPaths)) {
175            return array();
176        }
177
178        $children = array();
179        foreach (LessUtils::extractImports($content) as $reference) {
180            if ('.css' === substr($reference, -4)) {
181                // skip normal css imports
182                // todo: skip imports with media queries
183                continue;
184            }
185
186            if ('.less' !== substr($reference, -5)) {
187                $reference .= '.less';
188            }
189
190            foreach ($loadPaths as $loadPath) {
191                if (file_exists($file = $loadPath.'/'.$reference)) {
192                    $coll = $factory->createAsset($file, array(), array('root' => $loadPath));
193                    foreach ($coll as $leaf) {
194                        $leaf->ensureFilter($this);
195                        $children[] = $leaf;
196                        goto next_reference;
197                    }
198                }
199            }
200
201            next_reference:
202        }
203
204        return $children;
205    }
206}
207