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