diff --git a/min/lib/Minify/ImportProcessor.php b/min/lib/Minify/ImportProcessor.php index 10d9a09..bdfae54 100644 --- a/min/lib/Minify/ImportProcessor.php +++ b/min/lib/Minify/ImportProcessor.php @@ -1,157 +1,216 @@ - - */ -class Minify_ImportProcessor { - - public static $filesIncluded = array(); - - public static function process($file) - { - self::$filesIncluded = array(); - self::$_isCss = (strtolower(substr($file, -4)) === '.css'); - $obj = new Minify_ImportProcessor(dirname($file)); - return $obj->_getContent($file); - } - - // allows callback funcs to know the current directory - private $_currentDir = null; - - // allows _importCB to write the fetched content back to the obj - private $_importedContent = ''; - - private static $_isCss = null; - - private function __construct($currentDir) - { - $this->_currentDir = $currentDir; - } - - private function _getContent($file) - { - $file = realpath($file); - if (! $file - || in_array($file, self::$filesIncluded) - || false === ($content = @file_get_contents($file)) - ) { - // file missing, already included, or failed read - return ''; - } - self::$filesIncluded[] = realpath($file); - $this->_currentDir = dirname($file); - - // remove UTF-8 BOM if present - if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) { - $content = substr($content, 3); - } - // ensure uniform EOLs - $content = str_replace("\r\n", "\n", $content); - - // process @imports - $content = preg_replace_callback( - '/ - @import\\s+ - (?:url\\(\\s*)? # maybe url( - [\'"]? # maybe quote - (.*?) # 1 = URI - [\'"]? # maybe end quote - (?:\\s*\\))? # maybe ) - ([a-zA-Z,\\s]*)? # 2 = media list - ; # end token - /x' - ,array($this, '_importCB') - ,$content - ); - - if (self::$_isCss) { - // rewrite remaining relative URIs - $content = preg_replace_callback( - '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' - ,array($this, '_urlCB') - ,$content - ); - } - - return $this->_importedContent . $content; - } - - private function _importCB($m) - { - $url = $m[1]; - $mediaList = preg_replace('/\\s+/', '', $m[2]); - - if (strpos($url, '://') > 0) { - // protocol, leave in place for CSS, comment for JS - return self::$_isCss - ? $m[0] - : "/* Minify_ImportProcessor will not include remote content */"; - } - if ('/' === $url[0]) { - // protocol-relative or root path - $url = ltrim($url, '/'); - $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR - . strtr($url, '/', DIRECTORY_SEPARATOR); - } else { - // relative to current path - $file = $this->_currentDir . DIRECTORY_SEPARATOR - . strtr($url, '/', DIRECTORY_SEPARATOR); - } - $obj = new Minify_ImportProcessor(dirname($file)); - $content = $obj->_getContent($file); - if ('' === $content) { - // failed. leave in place for CSS, comment for JS - return self::$_isCss - ? $m[0] - : "/* Minify_ImportProcessor could not fetch '{$file}' */";; - } - return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList)) - ? $content - : "@media {$mediaList} {\n{$content}\n}\n"; - } - - private function _urlCB($m) - { - // $m[1] is either quoted or not - $quote = ($m[1][0] === "'" || $m[1][0] === '"') - ? $m[1][0] - : ''; - $url = ($quote === '') - ? $m[1] - : substr($m[1], 1, strlen($m[1]) - 2); - if ('/' !== $url[0]) { - if (strpos($url, '//') > 0) { - // probably starts with protocol, do not alter - } else { - // prepend path with current dir separator (OS-independent) - $path = $this->_currentDir - . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR); - // strip doc root - $path = substr($path, strlen(realpath($_SERVER['DOCUMENT_ROOT']))); - // fix to absolute URL - $url = strtr($path, '/\\', '//'); - // remove /./ and /../ where possible - $url = str_replace('/./', '/', $url); - // inspired by patch from Oleg Cherniy - do { - $url = preg_replace('@/(?!\\.\\.?)[^/]+/\\.\\.@', '/', $url, 1, $changed); - } while ($changed); - } - } - return "url({$quote}{$url}{$quote})"; - } -} + + * @author Simon Schick + */ +class Minify_ImportProcessor { + + public static $filesIncluded = array(); + + public static function process($file) + { + self::$filesIncluded = array(); + self::$_isCss = (strtolower(substr($file, -4)) === '.css'); + $obj = new Minify_ImportProcessor(dirname($file)); + return $obj->_getContent($file); + } + + // allows callback funcs to know the current directory + private $_currentDir = null; + + // allows callback funcs to know the directory of the file that inherits this one + private $_previewsDir = null; + + // allows _importCB to write the fetched content back to the obj + private $_importedContent = ''; + + private static $_isCss = null; + + /** + * @param String $currentDir + * @param String $previewsDir Is only used internally + */ + private function __construct($currentDir, $previewsDir = "") + { + $this->_currentDir = $currentDir; + $this->_previewsDir = $previewsDir; + } + + private function _getContent($file, $is_imported = false) + { + $file = realpath($file); + if (! $file + || in_array($file, self::$filesIncluded) + || false === ($content = @file_get_contents($file)) + ) { + // file missing, already included, or failed read + return ''; + } + self::$filesIncluded[] = realpath($file); + $this->_currentDir = dirname($file); + + // remove UTF-8 BOM if present + if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) { + $content = substr($content, 3); + } + // ensure uniform EOLs + $content = str_replace("\r\n", "\n", $content); + + // process @imports + $content = preg_replace_callback( + '/ + @import\\s+ + (?:url\\(\\s*)? # maybe url( + [\'"]? # maybe quote + (.*?) # 1 = URI + [\'"]? # maybe end quote + (?:\\s*\\))? # maybe ) + ([a-zA-Z,\\s]*)? # 2 = media list + ; # end token + /x' + ,array($this, '_importCB') + ,$content + ); + + // You only need to rework the import-path if the script is imported + if (self::$_isCss && $is_imported) { + // rewrite remaining relative URIs + $content = preg_replace_callback( + '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' + ,array($this, '_urlCB') + ,$content + ); + } + + return $this->_importedContent . $content; + } + + private function _importCB($m) + { + $url = $m[1]; + $mediaList = preg_replace('/\\s+/', '', $m[2]); + + if (strpos($url, '://') > 0) { + // protocol, leave in place for CSS, comment for JS + return self::$_isCss + ? $m[0] + : "/* Minify_ImportProcessor will not include remote content */"; + } + if ('/' === $url[0]) { + // protocol-relative or root path + $url = ltrim($url, '/'); + $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR + . strtr($url, '/', DIRECTORY_SEPARATOR); + } else { + // relative to current path + $file = $this->_currentDir . DIRECTORY_SEPARATOR + . strtr($url, '/', DIRECTORY_SEPARATOR); + } + $obj = new Minify_ImportProcessor(dirname($file), $this->_currentDir); + $content = $obj->_getContent($file, true); + if ('' === $content) { + // failed. leave in place for CSS, comment for JS + return self::$_isCss + ? $m[0] + : "/* Minify_ImportProcessor could not fetch '{$file}' */"; + } + return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList)) + ? $content + : "@media {$mediaList} {\n{$content}\n}\n"; + } + + private function _urlCB($m) + { + // $m[1] is either quoted or not + $quote = ($m[1][0] === "'" || $m[1][0] === '"') + ? $m[1][0] + : ''; + $url = ($quote === '') + ? $m[1] + : substr($m[1], 1, strlen($m[1]) - 2); + if ('/' !== $url[0]) { + if (strpos($url, '//') > 0) { + // probably starts with protocol, do not alter + } else { + // prepend path with current dir separator (OS-independent) + $path = $this->_currentDir + . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR); + // update the relative path by the directory of the file that imported this one + $url = self::getPathDiff(realpath($this->_previewsDir), $path); + } + } + return "url({$quote}{$url}{$quote})"; + } + + /** + * @param string $from + * @param string $to + * @param string $ps + * @return string + */ + private function getPathDiff($from, $to, $ps = DIRECTORY_SEPARATOR) + { + $realFrom = $this->truepath($from); + $realTo = $this->truepath($to); + + $arFrom = explode($ps, rtrim($realFrom, $ps)); + $arTo = explode($ps, rtrim($realTo, $ps)); + while (count($arFrom) && count($arTo) && ($arFrom[0] == $arTo[0])) + { + array_shift($arFrom); + array_shift($arTo); + } + return str_pad("", count($arFrom) * 3, '..' . $ps) . implode($ps, $arTo); + } + + /** + * This function is to replace PHP's extremely buggy realpath(). + * @param string $path The original path, can be relative etc. + * @return string The resolved path, it might not exist. + * @see http://stackoverflow.com/questions/4049856/replace-phps-realpath + */ + function truepath($path) + { + // whether $path is unix or not + $unipath = strlen($path) == 0 || $path{0} != '/'; + // attempts to detect if path is relative in which case, add cwd + if (strpos($path, ':') === false && $unipath) + $path = $this->_currentDir . DIRECTORY_SEPARATOR . $path; + + // resolve path parts (single dot, double dot and double delimiters) + $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path); + $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen'); + $absolutes = array(); + foreach ($parts as $part) { + if ('.' == $part) + continue; + if ('..' == $part) { + array_pop($absolutes); + } else { + $absolutes[] = $part; + } + } + $path = implode(DIRECTORY_SEPARATOR, $absolutes); + // resolve any symlinks + if (file_exists($path) && linkinfo($path) > 0) + $path = readlink($path); + // put initial separator that could have been lost + $path = !$unipath ? '/' . $path : $path; + return $path; + } +} diff --git a/min_unit_tests/_test_files/importProcessor/1/adjacent.css b/min_unit_tests/_test_files/importProcessor/css/1/adjacent.css similarity index 98% rename from min_unit_tests/_test_files/importProcessor/1/adjacent.css rename to min_unit_tests/_test_files/importProcessor/css/1/adjacent.css index da52c69..80ceaaf 100644 --- a/min_unit_tests/_test_files/importProcessor/1/adjacent.css +++ b/min_unit_tests/_test_files/importProcessor/css/1/adjacent.css @@ -1,3 +1,3 @@ -/* @import url('bad.css' ) bad; */ -adjacent2 foo { background: red url(/red.gif); } +/* @import url('bad.css' ) bad; */ +adjacent2 foo { background: red url(/red.gif); } adjacent2 bar { background: url('../green.gif') } \ No newline at end of file diff --git a/min_unit_tests/_test_files/importProcessor/1/tv.css b/min_unit_tests/_test_files/importProcessor/css/1/tv.css similarity index 97% rename from min_unit_tests/_test_files/importProcessor/1/tv.css rename to min_unit_tests/_test_files/importProcessor/css/1/tv.css index 69fa2f5..8542d74 100644 --- a/min_unit_tests/_test_files/importProcessor/1/tv.css +++ b/min_unit_tests/_test_files/importProcessor/css/1/tv.css @@ -1,4 +1,4 @@ -@import url( adjacent.css ) all; -@import '../input.css'; -tv foo { background: red url(/red.gif); } +@import url( adjacent.css ) all; +@import '../input.css'; +tv foo { background: red url(/red.gif); } tv bar { background: url('../green.gif') } \ No newline at end of file diff --git a/min_unit_tests/_test_files/importProcessor/adjacent.css b/min_unit_tests/_test_files/importProcessor/css/adjacent.css similarity index 78% rename from min_unit_tests/_test_files/importProcessor/adjacent.css rename to min_unit_tests/_test_files/importProcessor/css/adjacent.css index b452f47..d666a25 100644 --- a/min_unit_tests/_test_files/importProcessor/adjacent.css +++ b/min_unit_tests/_test_files/importProcessor/css/adjacent.css @@ -1,4 +1,4 @@ -@import url(../css/styles.css); -@import url(http://example.com/hello.css); -adjacent foo { background: red url(/red.gif); } +@import url(../../css/styles.css); +@import url(http://example.com/hello.css); +adjacent foo { background: red url(/red.gif); } adjacent bar { background: url('../green.gif') } \ No newline at end of file diff --git a/min_unit_tests/_test_files/importProcessor/input.css b/min_unit_tests/_test_files/importProcessor/css/input.css similarity index 63% rename from min_unit_tests/_test_files/importProcessor/input.css rename to min_unit_tests/_test_files/importProcessor/css/input.css index bb7b959..1534733 100644 --- a/min_unit_tests/_test_files/importProcessor/input.css +++ b/min_unit_tests/_test_files/importProcessor/css/input.css @@ -1,4 +1,5 @@ -@import url( adjacent.css ) screen; -@import "1/tv.css" tv, projection; -input foo { background: red url(/red.gif); } +@import url(adjacent.css) screen; +@import "1/tv.css" tv, projection; +@import "../lib/css/example.css"; +input foo { background: red url(/red.gif); } input bar { background: url('../green.gif') } \ No newline at end of file diff --git a/min_unit_tests/_test_files/importProcessor/output.css b/min_unit_tests/_test_files/importProcessor/css/output.css similarity index 71% rename from min_unit_tests/_test_files/importProcessor/output.css rename to min_unit_tests/_test_files/importProcessor/css/output.css index a652c99..2acb048 100644 --- a/min_unit_tests/_test_files/importProcessor/output.css +++ b/min_unit_tests/_test_files/importProcessor/css/output.css @@ -1,50 +1,52 @@ -@media screen { -@charset "utf-8"; - -/* some CSS to try to exercise things in general */ - -@import url(/more.css); - - body, td, th { - font-family: Verdana , "Bitstream Vera Sans" , Arial Narrow, sans-serif ; - - font-size : 12px; -} - -.nav { - margin-left: 20%; -} -#main-nav { - background-color: red; - border: 1px solid #00ff77; -} - -div#content -h1 + p { - padding-top: 0; - margin-top: 0; -} - -@media all and (min-width: 640px) { - #media-queries-1 { background-color: #0f0; } -} - -@media screen and (max-width: 2000px) { - #media-queries-2 { background-color: #0f0; } -} -@import url(http://example.com/hello.css); -adjacent foo { background: red url(/red.gif); } -adjacent bar { background: url('%TEST_FILES_URI%/green.gif') } -} - -@media tv,projection { -/* @import url('%TEST_FILES_URI%/importProcessor/1/bad.css') bad; */ -adjacent2 foo { background: red url(/red.gif); } -adjacent2 bar { background: url('%TEST_FILES_URI%/importProcessor/green.gif') } -@import '../input.css'; -tv foo { background: red url(/red.gif); } -tv bar { background: url('%TEST_FILES_URI%/importProcessor/green.gif') } -} - -input foo { background: red url(/red.gif); } -input bar { background: url('%TEST_FILES_URI%/green.gif') } \ No newline at end of file +@media screen { +@charset "utf-8"; + +/* some CSS to try to exercise things in general */ + +@import url(/more.css); + + body, td, th { + font-family: Verdana , "Bitstream Vera Sans" , Arial Narrow, sans-serif ; + + font-size : 12px; +} + +.nav { + margin-left: 20%; +} +#main-nav { + background-color: red; + border: 1px solid #00ff77; +} + +div#content +h1 + p { + padding-top: 0; + margin-top: 0; +} + +@media all and (min-width: 640px) { + #media-queries-1 { background-color: #0f0; } +} + +@media screen and (max-width: 2000px) { + #media-queries-2 { background-color: #0f0; } +} +@import url(http://example.com/hello.css); +adjacent foo { background: red url(/red.gif); } +adjacent bar { background: url('../green.gif') } +} + +@media tv,projection { +/* @import url('1/bad.css') bad; */ +adjacent2 foo { background: red url(/red.gif); } +adjacent2 bar { background: url('green.gif') } +@import '../input.css'; +tv foo { background: red url(/red.gif); } +tv bar { background: url('green.gif') } +} + +input.test bar { background: url('../lib/img/green.gif') } + +input foo { background: red url(/red.gif); } +input bar { background: url('../green.gif') } \ No newline at end of file diff --git a/min_unit_tests/_test_files/importProcessor/lib/css/example.css b/min_unit_tests/_test_files/importProcessor/lib/css/example.css new file mode 100644 index 0000000..7e3ceb7 --- /dev/null +++ b/min_unit_tests/_test_files/importProcessor/lib/css/example.css @@ -0,0 +1 @@ +input.test bar { background: url('../img/green.gif') } diff --git a/min_unit_tests/test_Minify_ImportProcessor.php b/min_unit_tests/test_Minify_ImportProcessor.php index 59abfca..cc0e827 100644 --- a/min_unit_tests/test_Minify_ImportProcessor.php +++ b/min_unit_tests/test_Minify_ImportProcessor.php @@ -1,48 +1,39 @@ -