3 * Class Minify_ImportProcessor
8 * Linearize a CSS/JS file by including content specified by CSS import
9 * declarations. In CSS files, relative URIs are fixed.
11 * @imports will be processed regardless of where they appear in the source
12 * files; i.e. @imports commented out or in string content will still be
15 * This has a unit test but should be considered "experimental".
18 * @author Stephen Clay <steve@mrclay.org>
19 * @author Simon Schick <simonsimcity@gmail.com>
21 class Minify_ImportProcessor {
23 public static $filesIncluded = array();
25 public static function process($file)
27 self::$filesIncluded = array();
28 self::$_isCss = (strtolower(substr($file, -4)) === '.css');
29 $obj = new Minify_ImportProcessor(dirname($file));
30 return $obj->_getContent($file);
33 // allows callback funcs to know the current directory
34 private $_currentDir = null;
36 // allows callback funcs to know the directory of the file that inherits this one
37 private $_previewsDir = null;
39 // allows _importCB to write the fetched content back to the obj
40 private $_importedContent = '';
42 private static $_isCss = null;
45 * @param String $currentDir
46 * @param String $previewsDir Is only used internally
48 private function __construct($currentDir, $previewsDir = "")
50 $this->_currentDir = $currentDir;
51 $this->_previewsDir = $previewsDir;
54 private function _getContent($file, $is_imported = false)
56 $file = realpath($file);
58 || in_array($file, self::$filesIncluded)
59 || false === ($content = @file_get_contents($file))
61 // file missing, already included, or failed read
64 self::$filesIncluded[] = realpath($file);
65 $this->_currentDir = dirname($file);
67 // remove UTF-8 BOM if present
68 if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) {
69 $content = substr($content, 3);
71 // ensure uniform EOLs
72 $content = str_replace("\r\n", "\n", $content);
75 $content = preg_replace_callback(
78 (?:url\\(\\s*)? # maybe url(
81 [\'"]? # maybe end quote
82 (?:\\s*\\))? # maybe )
83 ([a-zA-Z,\\s]*)? # 2 = media list
86 ,array($this, '_importCB')
90 // You only need to rework the import-path if the script is imported
91 if (self::$_isCss && $is_imported) {
92 // rewrite remaining relative URIs
93 $content = preg_replace_callback(
94 '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
95 ,array($this, '_urlCB')
100 return $this->_importedContent . $content;
103 private function _importCB($m)
106 $mediaList = preg_replace('/\\s+/', '', $m[2]);
108 if (strpos($url, '://') > 0) {
109 // protocol, leave in place for CSS, comment for JS
112 : "/* Minify_ImportProcessor will not include remote content */";
114 if ('/' === $url[0]) {
115 // protocol-relative or root path
116 $url = ltrim($url, '/');
117 $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR
118 . strtr($url, '/', DIRECTORY_SEPARATOR);
120 // relative to current path
121 $file = $this->_currentDir . DIRECTORY_SEPARATOR
122 . strtr($url, '/', DIRECTORY_SEPARATOR);
124 $obj = new Minify_ImportProcessor(dirname($file), $this->_currentDir);
125 $content = $obj->_getContent($file, true);
126 if ('' === $content) {
127 // failed. leave in place for CSS, comment for JS
130 : "/* Minify_ImportProcessor could not fetch '{$file}' */";
132 return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList))
134 : "@media {$mediaList} {\n{$content}\n}\n";
137 private function _urlCB($m)
139 // $m[1] is either quoted or not
140 $quote = ($m[1][0] === "'" || $m[1][0] === '"')
143 $url = ($quote === '')
145 : substr($m[1], 1, strlen($m[1]) - 2);
146 if ('/' !== $url[0]) {
147 if (strpos($url, '//') > 0) {
148 // probably starts with protocol, do not alter
150 // prepend path with current dir separator (OS-independent)
151 $path = $this->_currentDir
152 . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR);
153 // update the relative path by the directory of the file that imported this one
154 $url = self::getPathDiff(realpath($this->_previewsDir), $path);
157 return "url({$quote}{$url}{$quote})";
161 * @param string $from
166 private function getPathDiff($from, $to, $ps = DIRECTORY_SEPARATOR)
168 $realFrom = $this->truepath($from);
169 $realTo = $this->truepath($to);
171 $arFrom = explode($ps, rtrim($realFrom, $ps));
172 $arTo = explode($ps, rtrim($realTo, $ps));
173 while (count($arFrom) && count($arTo) && ($arFrom[0] == $arTo[0]))
175 array_shift($arFrom);
178 return str_pad("", count($arFrom) * 3, '..' . $ps) . implode($ps, $arTo);
182 * This function is to replace PHP's extremely buggy realpath().
183 * @param string $path The original path, can be relative etc.
184 * @return string The resolved path, it might not exist.
185 * @see http://stackoverflow.com/questions/4049856/replace-phps-realpath
187 function truepath($path)
189 // whether $path is unix or not
190 $unipath = strlen($path) == 0 || $path{0} != '/';
191 // attempts to detect if path is relative in which case, add cwd
192 if (strpos($path, ':') === false && $unipath)
193 $path = $this->_currentDir . DIRECTORY_SEPARATOR . $path;
195 // resolve path parts (single dot, double dot and double delimiters)
196 $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
197 $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
198 $absolutes = array();
199 foreach ($parts as $part) {
203 array_pop($absolutes);
205 $absolutes[] = $part;
208 $path = implode(DIRECTORY_SEPARATOR, $absolutes);
209 // resolve any symlinks
210 if (file_exists($path) && linkinfo($path) > 0)
211 $path = readlink($path);
212 // put initial separator that could have been lost
213 $path = !$unipath ? '/' . $path : $path;