1
0
mirror of https://github.com/mrclay/minify.git synced 2025-08-13 17:44:00 +02:00

Merge pull request #11 from SimonSimCity/master

Updated ImportProcessor to write relative for imported stuff like images
This commit is contained in:
Steve Clay
2011-11-23 07:26:28 -08:00
8 changed files with 319 additions and 265 deletions

View File

@@ -1,157 +1,216 @@
<?php <?php
/** /**
* Class Minify_ImportProcessor * Class Minify_ImportProcessor
* @package Minify * @package Minify
*/ */
/** /**
* Linearize a CSS/JS file by including content specified by CSS import * Linearize a CSS/JS file by including content specified by CSS import
* declarations. In CSS files, relative URIs are fixed. * declarations. In CSS files, relative URIs are fixed.
* *
* @imports will be processed regardless of where they appear in the source * @imports will be processed regardless of where they appear in the source
* files; i.e. @imports commented out or in string content will still be * files; i.e. @imports commented out or in string content will still be
* processed! * processed!
* *
* This has a unit test but should be considered "experimental". * This has a unit test but should be considered "experimental".
* *
* @package Minify * @package Minify
* @author Stephen Clay <steve@mrclay.org> * @author Stephen Clay <steve@mrclay.org>
*/ * @author Simon Schick <simonsimcity@gmail.com>
class Minify_ImportProcessor { */
class Minify_ImportProcessor {
public static $filesIncluded = array();
public static $filesIncluded = array();
public static function process($file)
{ public static function process($file)
self::$filesIncluded = array(); {
self::$_isCss = (strtolower(substr($file, -4)) === '.css'); self::$filesIncluded = array();
$obj = new Minify_ImportProcessor(dirname($file)); self::$_isCss = (strtolower(substr($file, -4)) === '.css');
return $obj->_getContent($file); $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 current directory
private $_currentDir = null;
// allows _importCB to write the fetched content back to the obj
private $_importedContent = ''; // allows callback funcs to know the directory of the file that inherits this one
private $_previewsDir = null;
private static $_isCss = null;
// allows _importCB to write the fetched content back to the obj
private function __construct($currentDir) private $_importedContent = '';
{
$this->_currentDir = $currentDir; private static $_isCss = null;
}
/**
private function _getContent($file) * @param String $currentDir
{ * @param String $previewsDir Is only used internally
$file = realpath($file); */
if (! $file private function __construct($currentDir, $previewsDir = "")
|| in_array($file, self::$filesIncluded) {
|| false === ($content = @file_get_contents($file)) $this->_currentDir = $currentDir;
) { $this->_previewsDir = $previewsDir;
// file missing, already included, or failed read }
return '';
} private function _getContent($file, $is_imported = false)
self::$filesIncluded[] = realpath($file); {
$this->_currentDir = dirname($file); $file = realpath($file);
if (! $file
// remove UTF-8 BOM if present || in_array($file, self::$filesIncluded)
if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) { || false === ($content = @file_get_contents($file))
$content = substr($content, 3); ) {
} // file missing, already included, or failed read
// ensure uniform EOLs return '';
$content = str_replace("\r\n", "\n", $content); }
self::$filesIncluded[] = realpath($file);
// process @imports $this->_currentDir = dirname($file);
$content = preg_replace_callback(
'/ // remove UTF-8 BOM if present
@import\\s+ if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) {
(?:url\\(\\s*)? # maybe url( $content = substr($content, 3);
[\'"]? # maybe quote }
(.*?) # 1 = URI // ensure uniform EOLs
[\'"]? # maybe end quote $content = str_replace("\r\n", "\n", $content);
(?:\\s*\\))? # maybe )
([a-zA-Z,\\s]*)? # 2 = media list // process @imports
; # end token $content = preg_replace_callback(
/x' '/
,array($this, '_importCB') @import\\s+
,$content (?:url\\(\\s*)? # maybe url(
); [\'"]? # maybe quote
(.*?) # 1 = URI
if (self::$_isCss) { [\'"]? # maybe end quote
// rewrite remaining relative URIs (?:\\s*\\))? # maybe )
$content = preg_replace_callback( ([a-zA-Z,\\s]*)? # 2 = media list
'/url\\(\\s*([^\\)\\s]+)\\s*\\)/' ; # end token
,array($this, '_urlCB') /x'
,$content ,array($this, '_importCB')
); ,$content
} );
return $this->_importedContent . $content; // You only need to rework the import-path if the script is imported
} if (self::$_isCss && $is_imported) {
// rewrite remaining relative URIs
private function _importCB($m) $content = preg_replace_callback(
{ '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
$url = $m[1]; ,array($this, '_urlCB')
$mediaList = preg_replace('/\\s+/', '', $m[2]); ,$content
);
if (strpos($url, '://') > 0) { }
// protocol, leave in place for CSS, comment for JS
return self::$_isCss return $this->_importedContent . $content;
? $m[0] }
: "/* Minify_ImportProcessor will not include remote content */";
} private function _importCB($m)
if ('/' === $url[0]) { {
// protocol-relative or root path $url = $m[1];
$url = ltrim($url, '/'); $mediaList = preg_replace('/\\s+/', '', $m[2]);
$file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR
. strtr($url, '/', DIRECTORY_SEPARATOR); if (strpos($url, '://') > 0) {
} else { // protocol, leave in place for CSS, comment for JS
// relative to current path return self::$_isCss
$file = $this->_currentDir . DIRECTORY_SEPARATOR ? $m[0]
. strtr($url, '/', DIRECTORY_SEPARATOR); : "/* Minify_ImportProcessor will not include remote content */";
} }
$obj = new Minify_ImportProcessor(dirname($file)); if ('/' === $url[0]) {
$content = $obj->_getContent($file); // protocol-relative or root path
if ('' === $content) { $url = ltrim($url, '/');
// failed. leave in place for CSS, comment for JS $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR
return self::$_isCss . strtr($url, '/', DIRECTORY_SEPARATOR);
? $m[0] } else {
: "/* Minify_ImportProcessor could not fetch '{$file}' */";; // relative to current path
} $file = $this->_currentDir . DIRECTORY_SEPARATOR
return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList)) . strtr($url, '/', DIRECTORY_SEPARATOR);
? $content }
: "@media {$mediaList} {\n{$content}\n}\n"; $obj = new Minify_ImportProcessor(dirname($file), $this->_currentDir);
} $content = $obj->_getContent($file, true);
if ('' === $content) {
private function _urlCB($m) // failed. leave in place for CSS, comment for JS
{ return self::$_isCss
// $m[1] is either quoted or not ? $m[0]
$quote = ($m[1][0] === "'" || $m[1][0] === '"') : "/* Minify_ImportProcessor could not fetch '{$file}' */";
? $m[1][0] }
: ''; return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList))
$url = ($quote === '') ? $content
? $m[1] : "@media {$mediaList} {\n{$content}\n}\n";
: substr($m[1], 1, strlen($m[1]) - 2); }
if ('/' !== $url[0]) {
if (strpos($url, '//') > 0) { private function _urlCB($m)
// probably starts with protocol, do not alter {
} else { // $m[1] is either quoted or not
// prepend path with current dir separator (OS-independent) $quote = ($m[1][0] === "'" || $m[1][0] === '"')
$path = $this->_currentDir ? $m[1][0]
. DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR); : '';
// strip doc root $url = ($quote === '')
$path = substr($path, strlen(realpath($_SERVER['DOCUMENT_ROOT']))); ? $m[1]
// fix to absolute URL : substr($m[1], 1, strlen($m[1]) - 2);
$url = strtr($path, '/\\', '//'); if ('/' !== $url[0]) {
// remove /./ and /../ where possible if (strpos($url, '//') > 0) {
$url = str_replace('/./', '/', $url); // probably starts with protocol, do not alter
// inspired by patch from Oleg Cherniy } else {
do { // prepend path with current dir separator (OS-independent)
$url = preg_replace('@/(?!\\.\\.?)[^/]+/\\.\\.@', '/', $url, 1, $changed); $path = $this->_currentDir
} while ($changed); . 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})"; }
} }
} 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;
}
}

View File

@@ -1,3 +1,3 @@
/* @import url('bad.css' ) bad; */ /* @import url('bad.css' ) bad; */
adjacent2 foo { background: red url(/red.gif); } adjacent2 foo { background: red url(/red.gif); }
adjacent2 bar { background: url('../green.gif') } adjacent2 bar { background: url('../green.gif') }

View File

@@ -1,4 +1,4 @@
@import url( adjacent.css ) all; @import url( adjacent.css ) all;
@import '../input.css'; @import '../input.css';
tv foo { background: red url(/red.gif); } tv foo { background: red url(/red.gif); }
tv bar { background: url('../green.gif') } tv bar { background: url('../green.gif') }

View File

@@ -1,4 +1,4 @@
@import url(../css/styles.css); @import url(../../css/styles.css);
@import url(http://example.com/hello.css); @import url(http://example.com/hello.css);
adjacent foo { background: red url(/red.gif); } adjacent foo { background: red url(/red.gif); }
adjacent bar { background: url('../green.gif') } adjacent bar { background: url('../green.gif') }

View File

@@ -1,4 +1,5 @@
@import url( adjacent.css ) screen; @import url(adjacent.css) screen;
@import "1/tv.css" tv, projection; @import "1/tv.css" tv, projection;
input foo { background: red url(/red.gif); } @import "../lib/css/example.css";
input foo { background: red url(/red.gif); }
input bar { background: url('../green.gif') } input bar { background: url('../green.gif') }

View File

@@ -1,50 +1,52 @@
@media screen { @media screen {
@charset "utf-8"; @charset "utf-8";
/* some CSS to try to exercise things in general */ /* some CSS to try to exercise things in general */
@import url(/more.css); @import url(/more.css);
body, td, th { body, td, th {
font-family: Verdana , "Bitstream Vera Sans" , Arial Narrow, sans-serif ; font-family: Verdana , "Bitstream Vera Sans" , Arial Narrow, sans-serif ;
font-size : 12px; font-size : 12px;
} }
.nav { .nav {
margin-left: 20%; margin-left: 20%;
} }
#main-nav { #main-nav {
background-color: red; background-color: red;
border: 1px solid #00ff77; border: 1px solid #00ff77;
} }
div#content div#content
h1 + p { h1 + p {
padding-top: 0; padding-top: 0;
margin-top: 0; margin-top: 0;
} }
@media all and (min-width: 640px) { @media all and (min-width: 640px) {
#media-queries-1 { background-color: #0f0; } #media-queries-1 { background-color: #0f0; }
} }
@media screen and (max-width: 2000px) { @media screen and (max-width: 2000px) {
#media-queries-2 { background-color: #0f0; } #media-queries-2 { background-color: #0f0; }
} }
@import url(http://example.com/hello.css); @import url(http://example.com/hello.css);
adjacent foo { background: red url(/red.gif); } adjacent foo { background: red url(/red.gif); }
adjacent bar { background: url('%TEST_FILES_URI%/green.gif') } adjacent bar { background: url('../green.gif') }
} }
@media tv,projection { @media tv,projection {
/* @import url('%TEST_FILES_URI%/importProcessor/1/bad.css') bad; */ /* @import url('1/bad.css') bad; */
adjacent2 foo { background: red url(/red.gif); } adjacent2 foo { background: red url(/red.gif); }
adjacent2 bar { background: url('%TEST_FILES_URI%/importProcessor/green.gif') } adjacent2 bar { background: url('green.gif') }
@import '../input.css'; @import '../input.css';
tv foo { background: red url(/red.gif); } tv foo { background: red url(/red.gif); }
tv bar { background: url('%TEST_FILES_URI%/importProcessor/green.gif') } tv bar { background: url('green.gif') }
} }
input foo { background: red url(/red.gif); } input.test bar { background: url('../lib/img/green.gif') }
input bar { background: url('%TEST_FILES_URI%/green.gif') }
input foo { background: red url(/red.gif); }
input bar { background: url('../green.gif') }

View File

@@ -0,0 +1 @@
input.test bar { background: url('../img/green.gif') }

View File

@@ -1,48 +1,39 @@
<?php <?php
require_once '_inc.php'; require_once '_inc.php';
require_once 'Minify/ImportProcessor.php'; require_once 'Minify/ImportProcessor.php';
function test_Minify_ImportProcessor() function test_Minify_ImportProcessor()
{ {
global $thisDir; global $thisDir;
$linDir = $thisDir . '/_test_files/importProcessor'; $linDir = $thisDir . '/_test_files/importProcessor';
$testFilesUri = substr( $expected = file_get_contents($linDir . '/css/output.css');
realpath($thisDir . '/_test_files')
,strlen(realpath($_SERVER['DOCUMENT_ROOT'])) $actual = Minify_ImportProcessor::process($linDir . '/css/input.css');
);
$testFilesUri = str_replace('\\', '/', $testFilesUri); $passed = assertTrue($expected === $actual, 'ImportProcessor');
$expected = str_replace( if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) {
'%TEST_FILES_URI%' echo "\n---Output: " .countBytes($actual). " bytes\n\n{$actual}\n\n";
,$testFilesUri if (!$passed) {
,file_get_contents($linDir . '/output.css') echo "---Expected: " .countBytes($expected). " bytes\n\n{$expected}\n\n\n";
); }
}
$actual = Minify_ImportProcessor::process($linDir . '/input.css');
$expectedIncludes = array (
$passed = assertTrue($expected === $actual, 'ImportProcessor'); realpath($linDir . '/css/input.css')
,realpath($linDir . '/css/adjacent.css')
if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { ,realpath($linDir . '/../css/styles.css')
echo "\n---Output: " .countBytes($actual). " bytes\n\n{$actual}\n\n"; ,realpath($linDir . '/css/1/tv.css')
if (!$passed) { ,realpath($linDir . '/css/1/adjacent.css')
echo "---Expected: " .countBytes($expected). " bytes\n\n{$expected}\n\n\n"; ,realpath($linDir . '/lib/css/example.css')
} );
}
$passed = assertTrue($expectedIncludes === Minify_ImportProcessor::$filesIncluded
$expectedIncludes = array ( , 'ImportProcessor : included right files in right order');
realpath($linDir . '/input.css') }
,realpath($linDir . '/adjacent.css')
,realpath($linDir . '/../css/styles.css')
,realpath($linDir . '/1/tv.css')
,realpath($linDir . '/1/adjacent.css')
);
$passed = assertTrue($expectedIncludes === Minify_ImportProcessor::$filesIncluded
, 'ImportProcessor : included right files in right order');
}
test_Minify_ImportProcessor(); test_Minify_ImportProcessor();