Skip to content

Instantly share code, notes, and snippets.

@JPKCom
Forked from dstorozhuk/CssServer.php
Created May 2, 2017 09:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JPKCom/c31fcb52b9c150eb35cd4f137c4b34ba to your computer and use it in GitHub Desktop.
Save JPKCom/c31fcb52b9c150eb35cd4f137c4b34ba to your computer and use it in GitHub Desktop.
Extends the Leafo\ScssPhp\Server. Add source map. Part of Drupal 8 module.
<?php
/**
* Created by PhpStorm.
* User: dima
* Date: 11/12/16
* Time: 11:58 AM
*/
namespace Drupal\bootstrap_sass_helper;
use Drupal\Core\Site\Settings;
use Leafo\ScssPhp\Compiler;
use Leafo\ScssPhp\Server ;
use Leafo\ScssPhp\Version;
use Symfony\Component\HttpFoundation\Response;
class CssServer extends Server {
private $debug = FALSE;
private $cacheFileSalt = '';
private $displayErrors = FALSE;
protected $inputName = '';
const cacheDir = 'public://scss_cache';
/**
* @inheritdoc
*/
protected function compile($in, $out) {
$start = microtime(true);
$scss = new Compiler();
$pi = pathinfo($in);
$scss->addImportPath($pi['dirname'] . '/');
if ($this->debug) {
$scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
}
$css = $scss->compile(file_get_contents($in), $in);
$elapsed = round((microtime(true) - $start), 4);
$v = Version::VERSION;
$t = date('r');
$css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
$etag = md5($css);
// Add debug source maps.
if ($this->debug) {
$cssDrupalPath = join('/',[static::cacheDir, md5($this->cacheFileSalt . $in) . '.css']);
$mapPath = file_create_url($cssDrupalPath);
$css .= " /*# sourceMappingURL=". $mapPath .".map */";
$css = preg_replace('#\\n[[:space:]]*\\n#',"\n", $css);
}
file_put_contents($out, $css);
// Add debug source maps.
if ($this->debug) {
$this->generateMap($out);
}
file_put_contents(
$this->metadataName($out),
serialize([
'etag' => $etag,
'imports' => $scss->getParsedFiles(),
'vars' => crc32(serialize($scss->getVariables())),
])
);
return [$css, $etag];
}
/**
* Class constructor.
*
* CssServer constructor.
* @param string $dir
* @param null $scss
* @param string $salt
*/
function __construct($dir, $scss = NULL, $salt = '') {
$cacheDir = $this->getCacheDir();
$this->debug = Settings::get('bootstrap_sass_helper.debug', FALSE);
$this->cacheFileSalt = $salt;
parent::__construct($dir, $cacheDir, $scss);
}
/**
* Serve the CSS.
*
* @param string $salt
* Not used. Exist just to match the Just to match the parent class method.
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function serve($salt = '') {
$protocol = isset($_SERVER['SERVER_PROTOCOL'])
? $_SERVER['SERVER_PROTOCOL']
: 'HTTP/1.0';
$headers = [];
$content = '';
$code = 200;
if ($input = $this->findInput()) {
$output = $this->cacheName($this->cacheFileSalt . $input);
$etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
if ($this->needsCompile($output, $etag)) {
try {
list($css, $etag) = $this->compile($input, $output);
$lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT';
$content = $css;
$headers['Last-Modified'] = $lastModified;
$headers['Content-type'] = 'text/css';
$headers['ETag'] = $etag;
}
catch (\Exception $e) {
if ($this->displayErrors) {
$headers['Content-type'] = 'text/css';
$content = $this->createErrorCSS($e);
}
else {
$headers[$protocol] = '500 Internal Server Error';
$headers['Content-type'] = 'text/plain';
$code = 500;
$content = 'Parse error: ' . $e->getMessage() . "\n";
}
}
return new Response($content, $code, $headers);
}
$headers['X-SCSS-Cache'] = TRUE;
$headers['Content-type'] = 'text/css';
$headers['ETag'] = $etag;
if ($etag === $noneMatch) {
$headers[$protocol] = '304 Not Modified';
$code = 304;
return new Response($content, $code, $headers);
}
$modifiedSince = $this->getIfModifiedSinceHeader();
$mtime = filemtime($output);
if (strtotime($modifiedSince) === $mtime) {
$headers[$protocol] = '304 Not Modified';
$code = 304;
return new Response($content, $code, $headers);
}
$lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
$headers['Last-Modified'] = $lastModified;
$content = file_get_contents($output);
return new Response($content, $code, $headers);
}
$headers[$protocol] = '404 Not Found';
$headers['Content-type'] = 'text/plain';
$code = 404;
$v = Version::VERSION;
$content = "/* INPUT NOT FOUND scss $v */\n";
return new Response($content, $code, $headers);
}
/**
* Generate source map.
*
* @param string $css
* Path to cached css file.
*
* @return bool|string
* Path to map or false if not able to generate.
*/
public function generateMap($css) {
$handle = fopen($css, 'r');
$lNumber = 0;
$compiled = file_get_contents($css);
$sourceMapGenerator = \Kwf_SourceMaps_SourceMap::createEmptyMap($compiled);
while (false !== ($line = fgets($handle))) {
preg_match_all('#[[:space:]]*/\* line ([0-9]+), ([^[:space:]]+) \*/#', $line, $matches);
$lNumber++;
if (count($matches) < 3) {
continue;
}
for ($i = 0; $i < count($matches[1]); $i++) {
if (!isset($matches[2][$i])) {
break;
}
$originalLine = $matches[1][$i];
$originalFile = $matches[2][$i];
// TODO set SCSS folder path relative to CSS folder
$originalFile = preg_replace('#(.*/)?web/#', '/', $originalFile);
$sourceMapGenerator->addMapping($lNumber, 0, $originalLine, 0, $originalFile);
}
}
$map = $css . '.map';
$sMap = $sourceMapGenerator->getMapContents();
return (false === file_put_contents($map, $sMap)) ? false : $map;
}
public static function getCacheDir() {
return drupal_realpath(static::cacheDir);
}
public function setInputName($name) {
$this->inputName = $name;
}
private function getInputName() {
return $this->inputName;
}
protected function inputName() {
return $this->getInputName();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment