1<?php
2/**
3 * This file provides the part of lessphp API (https://github.com/leafo/lessphp)
4 * to be a drop-in replacement for following products:
5 *  - Drupal 7, by the less module v3.0+ (https://drupal.org/project/less)
6 *  - Symfony 2
7 */
8
9// Register autoloader for non-composer installations
10if ( !class_exists( 'Less_Parser' ) ) {
11	require_once __DIR__ . '/lib/Less/Autoloader.php';
12	Less_Autoloader::register();
13}
14
15class lessc {
16
17	static public $VERSION = Less_Version::less_version;
18
19	public $importDir = '';
20	protected $allParsedFiles = array();
21	protected $libFunctions = array();
22	protected $registeredVars = array();
23	private $formatterName;
24	private $options = array();
25
26	public function __construct( $lessc = null, $sourceName = null ) {
27	}
28
29	public function setImportDir( $dirs ) {
30		$this->importDir = (array)$dirs;
31	}
32
33	public function addImportDir( $dir ) {
34		$this->importDir = (array)$this->importDir;
35		$this->importDir[] = $dir;
36	}
37
38	public function setFormatter( $name ) {
39		$this->formatterName = $name;
40	}
41
42	public function setPreserveComments( $preserve ) {
43	}
44
45	public function registerFunction( $name, $func ) {
46		$this->libFunctions[$name] = $func;
47	}
48
49	public function unregisterFunction( $name ) {
50		unset( $this->libFunctions[$name] );
51	}
52
53	public function setVariables( $variables ) {
54		foreach ( $variables as $name => $value ) {
55			$this->setVariable( $name, $value );
56		}
57	}
58
59	public function setVariable( $name, $value ) {
60		$this->registeredVars[$name] = $value;
61	}
62
63	public function unsetVariable( $name ) {
64		unset( $this->registeredVars[$name] );
65	}
66
67	public function setOptions( $options ) {
68		foreach ( $options as $name => $value ) {
69			$this->setOption( $name, $value );
70		}
71	}
72
73	public function setOption( $name, $value ) {
74		$this->options[$name] = $value;
75	}
76
77	public function parse( $buffer, $presets = array() ) {
78		$this->setVariables( $presets );
79
80		$parser = new Less_Parser( $this->getOptions() );
81		$parser->setImportDirs( $this->getImportDirs() );
82		foreach ( $this->libFunctions as $name => $func ) {
83			$parser->registerFunction( $name, $func );
84		}
85		$parser->parse( $buffer );
86		if ( count( $this->registeredVars ) ) {
87			$parser->ModifyVars( $this->registeredVars );
88		}
89
90		return $parser->getCss();
91	}
92
93	protected function getOptions() {
94		$options = array( 'relativeUrls' => false );
95		switch ( $this->formatterName ) {
96			case 'compressed':
97				$options['compress'] = true;
98				break;
99		}
100		if ( is_array( $this->options ) ) {
101			$options = array_merge( $options, $this->options );
102		}
103		return $options;
104	}
105
106	protected function getImportDirs() {
107		$dirs_ = (array)$this->importDir;
108		$dirs = array();
109		foreach ( $dirs_ as $dir ) {
110			$dirs[$dir] = '';
111		}
112		return $dirs;
113	}
114
115	public function compile( $string, $name = null ) {
116		$oldImport = $this->importDir;
117		$this->importDir = (array)$this->importDir;
118
119		$this->allParsedFiles = array();
120
121		$parser = new Less_Parser( $this->getOptions() );
122		$parser->SetImportDirs( $this->getImportDirs() );
123		if ( count( $this->registeredVars ) ) {
124			$parser->ModifyVars( $this->registeredVars );
125		}
126		foreach ( $this->libFunctions as $name => $func ) {
127			$parser->registerFunction( $name, $func );
128		}
129		$parser->parse( $string );
130		$out = $parser->getCss();
131
132		$parsed = Less_Parser::AllParsedFiles();
133		foreach ( $parsed as $file ) {
134			$this->addParsedFile( $file );
135		}
136
137		$this->importDir = $oldImport;
138
139		return $out;
140	}
141
142	public function compileFile( $fname, $outFname = null ) {
143		if ( !is_readable( $fname ) ) {
144			throw new Exception( 'load error: failed to find '.$fname );
145		}
146
147		$pi = pathinfo( $fname );
148
149		$oldImport = $this->importDir;
150
151		$this->importDir = (array)$this->importDir;
152		$this->importDir[] = Less_Parser::AbsPath( $pi['dirname'] ).'/';
153
154		$this->allParsedFiles = array();
155		$this->addParsedFile( $fname );
156
157		$parser = new Less_Parser( $this->getOptions() );
158		$parser->SetImportDirs( $this->getImportDirs() );
159		if ( count( $this->registeredVars ) ) {
160			$parser->ModifyVars( $this->registeredVars );
161		}
162		foreach ( $this->libFunctions as $name => $func ) {
163			$parser->registerFunction( $name, $func );
164		}
165		$parser->parseFile( $fname );
166		$out = $parser->getCss();
167
168		$parsed = Less_Parser::AllParsedFiles();
169		foreach ( $parsed as $file ) {
170			$this->addParsedFile( $file );
171		}
172
173		$this->importDir = $oldImport;
174
175		if ( $outFname !== null ) {
176			return file_put_contents( $outFname, $out );
177		}
178
179		return $out;
180	}
181
182	public function checkedCompile( $in, $out ) {
183		if ( !is_file( $out ) || filemtime( $in ) > filemtime( $out ) ) {
184			$this->compileFile( $in, $out );
185			return true;
186		}
187		return false;
188	}
189
190	/**
191	 * Execute lessphp on a .less file or a lessphp cache structure
192	 *
193	 * The lessphp cache structure contains information about a specific
194	 * less file having been parsed. It can be used as a hint for future
195	 * calls to determine whether or not a rebuild is required.
196	 *
197	 * The cache structure contains two important keys that may be used
198	 * externally:
199	 *
200	 * compiled: The final compiled CSS
201	 * updated: The time (in seconds) the CSS was last compiled
202	 *
203	 * The cache structure is a plain-ol' PHP associative array and can
204	 * be serialized and unserialized without a hitch.
205	 *
206	 * @param mixed $in Input
207	 * @param bool $force Force rebuild?
208	 * @return array lessphp cache structure
209	 */
210	public function cachedCompile( $in, $force = false ) {
211		// assume no root
212		$root = null;
213
214		if ( is_string( $in ) ) {
215			$root = $in;
216		} elseif ( is_array( $in ) and isset( $in['root'] ) ) {
217			if ( $force or !isset( $in['files'] ) ) {
218				// If we are forcing a recompile or if for some reason the
219				// structure does not contain any file information we should
220				// specify the root to trigger a rebuild.
221				$root = $in['root'];
222			} elseif ( isset( $in['files'] ) and is_array( $in['files'] ) ) {
223				foreach ( $in['files'] as $fname => $ftime ) {
224					if ( !file_exists( $fname ) or filemtime( $fname ) > $ftime ) {
225						// One of the files we knew about previously has changed
226						// so we should look at our incoming root again.
227						$root = $in['root'];
228						break;
229					}
230				}
231			}
232		} else {
233			// TODO: Throw an exception? We got neither a string nor something
234			// that looks like a compatible lessphp cache structure.
235			return null;
236		}
237
238		if ( $root !== null ) {
239			// If we have a root value which means we should rebuild.
240			$out = array();
241			$out['root'] = $root;
242			$out['compiled'] = $this->compileFile( $root );
243			$out['files'] = $this->allParsedFiles();
244			$out['updated'] = time();
245			return $out;
246		} else {
247			// No changes, pass back the structure
248			// we were given initially.
249			return $in;
250		}
251	}
252
253	public function ccompile( $in, $out, $less = null ) {
254		if ( $less === null ) {
255			$less = new self;
256		}
257		return $less->checkedCompile( $in, $out );
258	}
259
260	public static function cexecute( $in, $force = false, $less = null ) {
261		if ( $less === null ) {
262			$less = new self;
263		}
264		return $less->cachedCompile( $in, $force );
265	}
266
267	public function allParsedFiles() {
268		return $this->allParsedFiles;
269	}
270
271	protected function addParsedFile( $file ) {
272		$this->allParsedFiles[Less_Parser::AbsPath( $file )] = filemtime( $file );
273	}
274}
275