diff --git a/composer.json b/composer.json index f7e6c2e..478bb6b 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ }, "require-dev": { "leafo/lessphp": "~0.4.0", + "leafo/scssphp": "^0.3.0", "meenie/javascript-packer": "~1.1", "phpunit/phpunit": "4.8.*", "tedivm/jshrink": "~1.1.0" diff --git a/lib/Minify/ScssCssSource.php b/lib/Minify/ScssCssSource.php new file mode 100644 index 0000000..038a03f --- /dev/null +++ b/lib/Minify/ScssCssSource.php @@ -0,0 +1,175 @@ +cache = $cache; + } + + /** + * Get last modified of all parsed files + * + * @return int + */ + public function getLastModified() + { + $cache = $this->getCache(); + + return $cache['updated']; + } + + /** + * Get content + * + * @return string + */ + public function getContent() + { + $cache = $this->getCache(); + + return $cache['content']; + } + + /** + * Make a unique cache id for for this source. + * + * @param string $prefix + * + * @return string + */ + private function getCacheId($prefix = 'minify') + { + $md5 = md5($this->filepath); + + return "{$prefix}_scss_{$md5}"; + } + + /** + * Get SCSS cache object + * + * Runs the compilation if needed + * + * Implements Leafo\ScssPhp\Server logic because we need to get parsed files without parsing actual content + * + * @return array + */ + private function getCache() + { + // cache for single run + // so that getLastModified and getContent in single request do not add additional cache roundtrips (i.e memcache) + if (isset($this->parsed)) { + return $this->parsed; + } + + // check from cache first + $cache = null; + $cacheId = $this->getCacheId(); + if ($this->cache->isValid($cacheId, 0)) { + if ($cache = $this->cache->fetch($cacheId)) { + $cache = unserialize($cache); + } + } + + $input = $cache ? $cache : $this->filepath; + + if ($this->cacheIsStale($cache)) { + $cache = $this->compile($this->filepath); + } + + if (!is_array($input) || $cache['updated'] > $input['updated']) { + $this->cache->store($cacheId, serialize($cache)); + } + + return $this->parsed = $cache; + } + + /** + * Determine whether .scss file needs to be re-compiled. + * + * @param array $cache Cache object + * + * @return boolean True if compile required. + */ + private function cacheIsStale($cache) + { + if (!$cache) { + return true; + } + + $updated = $cache['updated']; + foreach ($cache['files'] as $import => $mtime) { + $filemtime = filemtime($import); + + if ($filemtime !== $mtime || $filemtime > $updated) { + return true; + } + } + + return false; + } + + /** + * Compile .scss file + * + * @param string $filename Input path (.scss) + * + * @see Server::compile() + * @return array meta data result of the compile + */ + private function compile($filename) + { + $start = microtime(true); + $scss = new Compiler(); + + // set import path directory the input filename resides + // otherwise @import statements will not find the files + // and will treat the @import line as css import + $scss->setImportPaths(dirname($filename)); + + $css = $scss->compile(file_get_contents($filename), $filename); + $elapsed = round((microtime(true) - $start), 4); + + $v = Version::VERSION; + $ts = date('r', $start); + $css = "/* compiled by scssphp $v on $ts (${elapsed}s) */\n\n" . $css; + + $imports = $scss->getParsedFiles(); + + $updated = 0; + foreach ($imports as $mtime) { + $updated = max($updated, $mtime); + } + + return array( + 'elapsed' => $elapsed, // statistic, can be dropped + 'updated' => $updated, + 'content' => $css, + 'files' => $imports, + ); + } +} diff --git a/lib/Minify/Source/Factory.php b/lib/Minify/Source/Factory.php index 7033179..71d7f08 100644 --- a/lib/Minify/Source/Factory.php +++ b/lib/Minify/Source/Factory.php @@ -67,9 +67,13 @@ class Minify_Source_Factory { throw new InvalidArgumentException("fileChecker option is not callable"); } - $this->setHandler('~\.less$~i', function ($spec) use ($cache) { - return new Minify_LessCssSource($spec, $cache); - }); + $this->setHandler('~\.less$~i', function ($spec) use ($cache) { + return new Minify_LessCssSource($spec, $cache); + }); + + $this->setHandler('~\.scss~i', function ($spec) use ($cache) { + return new Minify_ScssCssSource($spec, $cache); + }); $this->setHandler('~\.(js|css)$~i', function ($spec) { return new Minify_Source($spec); diff --git a/tests/ScssSourceTest.php b/tests/ScssSourceTest.php new file mode 100644 index 0000000..a1581ef --- /dev/null +++ b/tests/ScssSourceTest.php @@ -0,0 +1,40 @@ +realDocRoot = $_SERVER['DOCUMENT_ROOT']; + $_SERVER['DOCUMENT_ROOT'] = self::$document_root; + } + + /** + * @link https://github.com/mrclay/minify/issues/500 + */ + public function testTimestamp() + { + $baseDir = self::$test_files; + + $mainLess = "$baseDir/main.scss"; + $includedLess = "$baseDir/_included.scss"; + + // touch timestamp with 1s difference + touch($mainLess); + sleep(1); + touch($includedLess); + + $mtime1 = filemtime($mainLess); + var_dump($mtime1); + $mtime2 = filemtime($includedLess); + var_dump($mtime2); + + $max = max($mtime1, $mtime2); + + $options = array( + 'groupsConfigFile' => "$baseDir/htmlHelper_groupsConfig.php", + ); + $res = Minify_HTML_Helper::getUri('scss', $options); + + $this->assertEquals("/min/g=scss&{$max}", $res); + } +} \ No newline at end of file diff --git a/tests/_test_files/_included.scss b/tests/_test_files/_included.scss new file mode 100644 index 0000000..df8d371 --- /dev/null +++ b/tests/_test_files/_included.scss @@ -0,0 +1,8 @@ +/* lesstest2.scss */ + + +a.included { + color: $primary-color; + font-size: 13px; + text-decoration: none; +} diff --git a/tests/_test_files/htmlHelper_groupsConfig.php b/tests/_test_files/htmlHelper_groupsConfig.php index cf67275..cbf8a9d 100644 --- a/tests/_test_files/htmlHelper_groupsConfig.php +++ b/tests/_test_files/htmlHelper_groupsConfig.php @@ -9,4 +9,8 @@ return array( 'less' => array( '//_test_files/main.less', ), + + 'scss' => array( + '//_test_files/main.scss', + ), ); diff --git a/tests/_test_files/main.scss b/tests/_test_files/main.scss new file mode 100644 index 0000000..80bc0ea --- /dev/null +++ b/tests/_test_files/main.scss @@ -0,0 +1,29 @@ +/*! preserving comment */ + +// Variable +$primary-color: hotpink; + +// Mixin +@mixin border-radius($radius) { + -webkit-border-radius: $radius; + -moz-border-radius: $radius; + border-radius: $radius; +} + +.my-element { + color: $primary-color; + width: 100%; + overflow: hidden; +} + +.my-other-element { + @include border-radius(6px); +} + +/* import include -> */ +@import "_included"; +/* <- import included */ + +/* + a normal comment. +*/ \ No newline at end of file