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:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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') }
|
@@ -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') }
|
@@ -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') }
|
@@ -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') }
|
@@ -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') }
|
@@ -0,0 +1 @@
|
|||||||
|
input.test bar { background: url('../img/green.gif') }
|
@@ -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();
|
Reference in New Issue
Block a user