5 * @copyright 2012-2017 Leaf Corcoran
7 * @license http://opensource.org/licenses/MIT MIT
9 * @link http://leafo.github.io/scssphp
12 namespace Leafo\ScssPhp
;
14 use Leafo\ScssPhp\Compiler
;
15 use Leafo\ScssPhp\Exception\ServerException
;
16 use Leafo\ScssPhp\Version
;
21 * @author Leaf Corcoran <leafot@gmail.com>
28 private $showErrorsAsCSS;
41 * @var \Leafo\ScssPhp\Compiler
46 * Join path components
48 * @param string $left Path component, left of the directory separator
49 * @param string $right Path component, right of the directory separator
53 protected function join($left, $right)
55 return rtrim($left, '/\\') . DIRECTORY_SEPARATOR
. ltrim($right, '/\\');
59 * Get name of requested .scss file
63 protected function inputName()
66 case isset($_GET['p']):
68 case isset($_SERVER['PATH_INFO']):
69 return $_SERVER['PATH_INFO'];
70 case isset($_SERVER['DOCUMENT_URI']):
71 return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
76 * Get path to requested .scss file
80 protected function findInput()
82 if (($input = $this->inputName())
83 && strpos($input, '..') === false
84 && substr($input, -5) === '.scss'
86 $name = $this->join($this->dir
, $input);
88 if (is_file($name) && is_readable($name)) {
97 * Get path to cached .css file
101 protected function cacheName($fname)
103 return $this->join($this->cacheDir
, md5($fname) . '.css');
107 * Get path to meta data
111 protected function metadataName($out)
113 return $out . '.meta';
117 * Determine whether .scss file needs to be re-compiled.
119 * @param string $out Output path
120 * @param string $etag ETag
122 * @return boolean True if compile required.
124 protected function needsCompile($out, &$etag)
126 if (! is_file($out)) {
130 $mtime = filemtime($out);
132 $metadataName = $this->metadataName($out);
134 if (is_readable($metadataName)) {
135 $metadata = unserialize(file_get_contents($metadataName));
137 foreach ($metadata['imports'] as $import => $originalMtime) {
138 $currentMtime = filemtime($import);
140 if ($currentMtime !== $originalMtime ||
$currentMtime > $mtime) {
145 $metaVars = crc32(serialize($this->scss
->getVariables()));
147 if ($metaVars !== $metadata['vars']) {
151 $etag = $metadata['etag'];
160 * Get If-Modified-Since header from client request
162 * @return string|null
164 protected function getIfModifiedSinceHeader()
166 $modifiedSince = null;
168 if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
169 $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
171 if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
172 $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
176 return $modifiedSince;
180 * Get If-None-Match header from client request
182 * @return string|null
184 protected function getIfNoneMatchHeader()
188 if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
189 $noneMatch = $_SERVER['HTTP_IF_NONE_MATCH'];
198 * @param string $in Input path (.scss)
199 * @param string $out Output path (.css)
203 protected function compile($in, $out)
205 $start = microtime(true);
206 $css = $this->scss
->compile(file_get_contents($in), $in);
207 $elapsed = round((microtime(true) - $start), 4);
209 $v = Version
::VERSION
;
211 $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
214 file_put_contents($out, $css);
216 $this->metadataName($out),
219 'imports' => $this->scss
->getParsedFiles(),
220 'vars' => crc32(serialize($this->scss
->getVariables())),
224 return [$css, $etag];
228 * Format error as a pseudo-element in CSS
230 * @param \Exception $error
234 protected function createErrorCSS(\Exception
$error)
236 $message = str_replace(
239 $error->getfile() . ":\n\n" . $error->getMessage()
242 return "body { display: none !important; }
247 display: block !important;
255 * Render errors as a pseudo-element within valid CSS, displaying the errors on any
256 * page that includes this CSS.
258 * @param boolean $show
260 public function showErrorsAsCSS($show = true)
262 $this->showErrorsAsCSS
= $show;
268 * @param string $in Input file (.scss)
269 * @param string $out Output file (.css) optional
271 * @return string|bool
273 * @throws \Leafo\ScssPhp\Exception\ServerException
275 public function compileFile($in, $out = null)
277 if (! is_readable($in)) {
278 throw new ServerException('load error: failed to find ' . $in);
283 $this->scss
->addImportPath($pi['dirname'] . '/');
285 $compiled = $this->scss
->compile(file_get_contents($in), $in);
288 return file_put_contents($out, $compiled);
295 * Check if file need compiling
297 * @param string $in Input file (.scss)
298 * @param string $out Output file (.css)
302 public function checkedCompile($in, $out)
304 if (! is_file($out) ||
filemtime($in) > filemtime($out)) {
305 $this->compileFile($in, $out);
314 * Compile requested scss and serve css. Outputs HTTP response.
316 * @param string $salt Prefix a string to the filename for creating the cache name hash
318 public function serve($salt = '')
320 $protocol = isset($_SERVER['SERVER_PROTOCOL'])
321 ?
$_SERVER['SERVER_PROTOCOL']
324 if ($input = $this->findInput()) {
325 $output = $this->cacheName($salt . $input);
326 $etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
328 if ($this->needsCompile($output, $etag)) {
330 list($css, $etag) = $this->compile($input, $output);
332 $lastModified = gmdate('r', filemtime($output));
334 header('Last-Modified: ' . $lastModified);
335 header('Content-type: text/css');
336 header('ETag: "' . $etag . '"');
339 } catch (\Exception
$e) {
340 if ($this->showErrorsAsCSS
) {
341 header('Content-type: text/css');
343 echo $this->createErrorCSS($e);
345 header($protocol . ' 500 Internal Server Error');
346 header('Content-type: text/plain');
348 echo 'Parse error: ' . $e->getMessage() . "\n";
355 header('X-SCSS-Cache: true');
356 header('Content-type: text/css');
357 header('ETag: "' . $etag . '"');
359 if ($etag === $noneMatch) {
360 header($protocol . ' 304 Not Modified');
365 $modifiedSince = $this->getIfModifiedSinceHeader();
366 $mtime = filemtime($output);
368 if (strtotime($modifiedSince) === $mtime) {
369 header($protocol . ' 304 Not Modified');
374 $lastModified = gmdate('r', $mtime);
375 header('Last-Modified: ' . $lastModified);
377 echo file_get_contents($output);
382 header($protocol . ' 404 Not Found');
383 header('Content-type: text/plain');
385 $v = Version
::VERSION
;
386 echo "/* INPUT NOT FOUND scss $v */\n";
390 * Based on explicit input/output files does a full change check on cache before compiling.
394 * @param boolean $force
396 * @return string Compiled CSS results
398 * @throws \Leafo\ScssPhp\Exception\ServerException
400 public function checkedCachedCompile($in, $out, $force = false)
402 if (! is_file($in) ||
! is_readable($in)) {
403 throw new ServerException('Invalid or unreadable input file specified.');
406 if (is_dir($out) ||
! is_writable(file_exists($out) ?
$out : dirname($out))) {
407 throw new ServerException('Invalid or unwritable output file specified.');
410 if ($force ||
$this->needsCompile($out, $etag)) {
411 list($css, $etag) = $this->compile($in, $out);
413 $css = file_get_contents($out);
420 * Execute scssphp on a .scss file or a scssphp cache structure
422 * The scssphp cache structure contains information about a specific
423 * scss file having been parsed. It can be used as a hint for future
424 * calls to determine whether or not a rebuild is required.
426 * The cache structure contains two important keys that may be used
429 * compiled: The final compiled CSS
430 * updated: The time (in seconds) the CSS was last compiled
432 * The cache structure is a plain-ol' PHP associative array and can
433 * be serialized and unserialized without a hitch.
435 * @param mixed $in Input
436 * @param boolean $force Force rebuild?
438 * @return array scssphp cache structure
440 public function cachedCompile($in, $force = false)
445 if (is_string($in)) {
447 } elseif (is_array($in) and isset($in['root'])) {
448 if ($force or ! isset($in['files'])) {
449 // If we are forcing a recompile or if for some reason the
450 // structure does not contain any file information we should
451 // specify the root to trigger a rebuild.
453 } elseif (isset($in['files']) and is_array($in['files'])) {
454 foreach ($in['files'] as $fname => $ftime) {
455 if (! file_exists($fname) or filemtime($fname) > $ftime) {
456 // One of the files we knew about previously has changed
457 // so we should look at our incoming root again.
464 // TODO: Throw an exception? We got neither a string nor something
465 // that looks like a compatible lessphp cache structure.
469 if ($root !== null) {
470 // If we have a root value which means we should rebuild.
472 $out['root'] = $root;
473 $out['compiled'] = $this->compileFile($root);
474 $out['files'] = $this->scss
->getParsedFiles();
475 $out['updated'] = time();
478 // No changes, pass back the structure
479 // we were given initially.
487 * @param string $dir Root directory to .scss files
488 * @param string $cacheDir Cache directory
489 * @param \Leafo\ScssPhp\Compiler|null $scss SCSS compiler instance
491 public function __construct($dir, $cacheDir = null, $scss = null)
495 if (! isset($cacheDir)) {
496 $cacheDir = $this->join($dir, 'scss_cache');
499 $this->cacheDir
= $cacheDir;
501 if (! is_dir($this->cacheDir
)) {
502 throw new ServerException('Cache directory doesn\'t exist: ' . $cacheDir);
505 if (! isset($scss)) {
506 $scss = new Compiler();
507 $scss->setImportPaths($this->dir
);
511 $this->showErrorsAsCSS
= false;
513 date_default_timezone_set('UTC');