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