|
|
<?php |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace MatthiasMullie\Minify; |
|
|
|
|
|
use MatthiasMullie\Minify\Exceptions\FileImportException; |
|
|
use MatthiasMullie\PathConverter\ConverterInterface; |
|
|
use MatthiasMullie\PathConverter\Converter; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CSS extends Minify |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
protected $maxImportSize = 5; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected $importExtensions = array( |
|
|
'gif' => 'data:image/gif', |
|
|
'png' => 'data:image/png', |
|
|
'jpe' => 'data:image/jpeg', |
|
|
'jpg' => 'data:image/jpeg', |
|
|
'jpeg' => 'data:image/jpeg', |
|
|
'svg' => 'data:image/svg+xml', |
|
|
'woff' => 'data:application/x-font-woff', |
|
|
'tif' => 'image/tiff', |
|
|
'tiff' => 'image/tiff', |
|
|
'xbm' => 'image/x-xbitmap', |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setMaxImportSize($size) |
|
|
{ |
|
|
$this->maxImportSize = $size; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setImportExtensions(array $extensions) |
|
|
{ |
|
|
$this->importExtensions = $extensions; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function moveImportsToTop($content) |
|
|
{ |
|
|
if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) { |
|
|
|
|
|
foreach ($matches[0] as $import) { |
|
|
$content = str_replace($import, '', $content); |
|
|
} |
|
|
|
|
|
|
|
|
$content = implode(';', $matches[2]).';'.trim($content, ';'); |
|
|
} |
|
|
|
|
|
return $content; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function combineImports($source, $content, $parents) |
|
|
{ |
|
|
$importRegexes = array( |
|
|
|
|
|
'/ |
|
|
# import statement |
|
|
@import |
|
|
|
|
|
# whitespace |
|
|
\s+ |
|
|
|
|
|
# open url() |
|
|
url\( |
|
|
|
|
|
# (optional) open path enclosure |
|
|
(?P<quotes>["\']?) |
|
|
|
|
|
# fetch path |
|
|
(?P<path>.+?) |
|
|
|
|
|
# (optional) close path enclosure |
|
|
(?P=quotes) |
|
|
|
|
|
# close url() |
|
|
\) |
|
|
|
|
|
# (optional) trailing whitespace |
|
|
\s* |
|
|
|
|
|
# (optional) media statement(s) |
|
|
(?P<media>[^;]*) |
|
|
|
|
|
# (optional) trailing whitespace |
|
|
\s* |
|
|
|
|
|
# (optional) closing semi-colon |
|
|
;? |
|
|
|
|
|
/ix', |
|
|
|
|
|
|
|
|
'/ |
|
|
|
|
|
# import statement |
|
|
@import |
|
|
|
|
|
# whitespace |
|
|
\s+ |
|
|
|
|
|
# open path enclosure |
|
|
(?P<quotes>["\']) |
|
|
|
|
|
# fetch path |
|
|
(?P<path>.+?) |
|
|
|
|
|
# close path enclosure |
|
|
(?P=quotes) |
|
|
|
|
|
# (optional) trailing whitespace |
|
|
\s* |
|
|
|
|
|
# (optional) media statement(s) |
|
|
(?P<media>[^;]*) |
|
|
|
|
|
# (optional) trailing whitespace |
|
|
\s* |
|
|
|
|
|
# (optional) closing semi-colon |
|
|
;? |
|
|
|
|
|
/ix', |
|
|
); |
|
|
|
|
|
|
|
|
$matches = array(); |
|
|
foreach ($importRegexes as $importRegex) { |
|
|
if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) { |
|
|
$matches = array_merge($matches, $regexMatches); |
|
|
} |
|
|
} |
|
|
|
|
|
$search = array(); |
|
|
$replace = array(); |
|
|
|
|
|
|
|
|
foreach ($matches as $match) { |
|
|
|
|
|
$importPath = dirname($source).'/'.$match['path']; |
|
|
|
|
|
|
|
|
|
|
|
if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) { |
|
|
continue; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (in_array($importPath, $parents)) { |
|
|
throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$minifier = new static($importPath); |
|
|
$minifier->setMaxImportSize($this->maxImportSize); |
|
|
$minifier->setImportExtensions($this->importExtensions); |
|
|
$importContent = $minifier->execute($source, $parents); |
|
|
|
|
|
|
|
|
if (!empty($match['media'])) { |
|
|
$importContent = '@media '.$match['media'].'{'.$importContent.'}'; |
|
|
} |
|
|
|
|
|
|
|
|
$search[] = $match[0]; |
|
|
$replace[] = $importContent; |
|
|
} |
|
|
|
|
|
|
|
|
return str_replace($search, $replace, $content); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function importFiles($source, $content) |
|
|
{ |
|
|
$regex = '/url\((["\']?)(.+?)\\1\)/i'; |
|
|
if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { |
|
|
$search = array(); |
|
|
$replace = array(); |
|
|
|
|
|
|
|
|
foreach ($matches as $match) { |
|
|
$extension = substr(strrchr($match[2], '.'), 1); |
|
|
if ($extension && !array_key_exists($extension, $this->importExtensions)) { |
|
|
continue; |
|
|
} |
|
|
|
|
|
|
|
|
$path = $match[2]; |
|
|
$path = dirname($source).'/'.$path; |
|
|
|
|
|
|
|
|
|
|
|
if ($this->canImportFile($path) && $this->canImportBySize($path)) { |
|
|
|
|
|
$importContent = $this->load($path); |
|
|
$importContent = base64_encode($importContent); |
|
|
|
|
|
|
|
|
$search[] = $match[0]; |
|
|
$replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
$content = str_replace($search, $replace, $content); |
|
|
} |
|
|
|
|
|
return $content; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function execute($path = null, $parents = array()) |
|
|
{ |
|
|
$content = ''; |
|
|
|
|
|
|
|
|
foreach ($this->data as $source => $css) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$this->extractStrings(); |
|
|
$this->stripComments(); |
|
|
$this->extractCalcs(); |
|
|
$css = $this->replace($css); |
|
|
|
|
|
$css = $this->stripWhitespace($css); |
|
|
$css = $this->shortenColors($css); |
|
|
$css = $this->shortenZeroes($css); |
|
|
$css = $this->shortenFontWeights($css); |
|
|
$css = $this->stripEmptyTags($css); |
|
|
|
|
|
|
|
|
$css = $this->restoreExtractedData($css); |
|
|
|
|
|
$source = is_int($source) ? '' : $source; |
|
|
$parents = $source ? array_merge($parents, array($source)) : $parents; |
|
|
$css = $this->combineImports($source, $css, $parents); |
|
|
$css = $this->importFiles($source, $css); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$converter = $this->getPathConverter($source, $path ?: $source); |
|
|
$css = $this->move($converter, $css); |
|
|
|
|
|
|
|
|
$content .= $css; |
|
|
} |
|
|
|
|
|
$content = $this->moveImportsToTop($content); |
|
|
|
|
|
return $content; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function move(ConverterInterface $converter, $content) |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$relativeRegexes = array( |
|
|
|
|
|
'/ |
|
|
# open url() |
|
|
url\( |
|
|
|
|
|
\s* |
|
|
|
|
|
# open path enclosure |
|
|
(?P<quotes>["\'])? |
|
|
|
|
|
# fetch path |
|
|
(?P<path>.+?) |
|
|
|
|
|
# close path enclosure |
|
|
(?(quotes)(?P=quotes)) |
|
|
|
|
|
\s* |
|
|
|
|
|
# close url() |
|
|
\) |
|
|
|
|
|
/ix', |
|
|
|
|
|
|
|
|
'/ |
|
|
# import statement |
|
|
@import |
|
|
|
|
|
# whitespace |
|
|
\s+ |
|
|
|
|
|
# we don\'t have to check for @import url(), because the |
|
|
# condition above will already catch these |
|
|
|
|
|
# open path enclosure |
|
|
(?P<quotes>["\']) |
|
|
|
|
|
# fetch path |
|
|
(?P<path>.+?) |
|
|
|
|
|
# close path enclosure |
|
|
(?P=quotes) |
|
|
|
|
|
/ix', |
|
|
); |
|
|
|
|
|
|
|
|
$matches = array(); |
|
|
foreach ($relativeRegexes as $relativeRegex) { |
|
|
if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) { |
|
|
$matches = array_merge($matches, $regexMatches); |
|
|
} |
|
|
} |
|
|
|
|
|
$search = array(); |
|
|
$replace = array(); |
|
|
|
|
|
|
|
|
foreach ($matches as $match) { |
|
|
|
|
|
$type = (strpos($match[0], '@import') === 0 ? 'import' : 'url'); |
|
|
|
|
|
$url = $match['path']; |
|
|
if ($this->canImportByPath($url)) { |
|
|
|
|
|
$params = strrchr($url, '?'); |
|
|
$url = $params ? substr($url, 0, -strlen($params)) : $url; |
|
|
|
|
|
|
|
|
$url = $converter->convert($url); |
|
|
|
|
|
|
|
|
$url .= $params; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$url = trim($url); |
|
|
if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) { |
|
|
$url = $match['quotes'] . $url . $match['quotes']; |
|
|
} |
|
|
|
|
|
|
|
|
$search[] = $match[0]; |
|
|
if ($type === 'url') { |
|
|
$replace[] = 'url('.$url.')'; |
|
|
} elseif ($type === 'import') { |
|
|
$replace[] = '@import "'.$url.'"'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return str_replace($search, $replace, $content); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function shortenColors($content) |
|
|
{ |
|
|
$content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?:([0-9a-z])\\4)?(?=[; }])/i', '#$1$2$3$4', $content); |
|
|
|
|
|
|
|
|
$content = preg_replace('/(?<=[: ])#([0-9a-z]{6})ff?(?=[; }])/i', '#$1', $content); |
|
|
$content = preg_replace('/(?<=[: ])#([0-9a-z]{3})f?(?=[; }])/i', '#$1', $content); |
|
|
|
|
|
$colors = array( |
|
|
|
|
|
'#F0FFFF' => 'azure', |
|
|
'#F5F5DC' => 'beige', |
|
|
'#A52A2A' => 'brown', |
|
|
'#FF7F50' => 'coral', |
|
|
'#FFD700' => 'gold', |
|
|
'#808080' => 'gray', |
|
|
'#008000' => 'green', |
|
|
'#4B0082' => 'indigo', |
|
|
'#FFFFF0' => 'ivory', |
|
|
'#F0E68C' => 'khaki', |
|
|
'#FAF0E6' => 'linen', |
|
|
'#800000' => 'maroon', |
|
|
'#000080' => 'navy', |
|
|
'#808000' => 'olive', |
|
|
'#CD853F' => 'peru', |
|
|
'#FFC0CB' => 'pink', |
|
|
'#DDA0DD' => 'plum', |
|
|
'#800080' => 'purple', |
|
|
'#F00' => 'red', |
|
|
'#FA8072' => 'salmon', |
|
|
'#A0522D' => 'sienna', |
|
|
'#C0C0C0' => 'silver', |
|
|
'#FFFAFA' => 'snow', |
|
|
'#D2B48C' => 'tan', |
|
|
'#FF6347' => 'tomato', |
|
|
'#EE82EE' => 'violet', |
|
|
'#F5DEB3' => 'wheat', |
|
|
|
|
|
'WHITE' => '#fff', |
|
|
'BLACK' => '#000', |
|
|
); |
|
|
|
|
|
return preg_replace_callback( |
|
|
'/(?<=[: ])('.implode('|', array_keys($colors)).')(?=[; }])/i', |
|
|
function ($match) use ($colors) { |
|
|
return $colors[strtoupper($match[0])]; |
|
|
}, |
|
|
$content |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function shortenFontWeights($content) |
|
|
{ |
|
|
$weights = array( |
|
|
'normal' => 400, |
|
|
'bold' => 700, |
|
|
); |
|
|
|
|
|
$callback = function ($match) use ($weights) { |
|
|
return $match[1].$weights[$match[2]]; |
|
|
}; |
|
|
|
|
|
return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function shortenZeroes($content) |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$before = '(?<=[:(, ])'; |
|
|
$after = '(?=[ ,);}])'; |
|
|
$units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content); |
|
|
|
|
|
|
|
|
$content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content); |
|
|
|
|
|
$content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content); |
|
|
|
|
|
$content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content); |
|
|
|
|
|
$content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content); |
|
|
|
|
|
|
|
|
$content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content); |
|
|
$content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content); |
|
|
|
|
|
return $content; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function stripEmptyTags($content) |
|
|
{ |
|
|
$content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content); |
|
|
$content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content); |
|
|
|
|
|
return $content; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function stripComments() |
|
|
{ |
|
|
|
|
|
$minifier = $this; |
|
|
$callback = function ($match) use ($minifier) { |
|
|
$count = count($minifier->extracted); |
|
|
$placeholder = '/*'.$count.'*/'; |
|
|
$minifier->extracted[$placeholder] = $match[0]; |
|
|
|
|
|
return $placeholder; |
|
|
}; |
|
|
$this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback); |
|
|
|
|
|
$this->registerPattern('/\/\*.*?\*\//s', ''); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function stripWhitespace($content) |
|
|
{ |
|
|
|
|
|
$content = preg_replace('/^\s*/m', '', $content); |
|
|
$content = preg_replace('/\s*$/m', '', $content); |
|
|
|
|
|
|
|
|
$content = preg_replace('/\s+/', ' ', $content); |
|
|
|
|
|
|
|
|
|
|
|
$content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content); |
|
|
$content = preg_replace('/([\[(:>\+])\s+/', '$1', $content); |
|
|
$content = preg_replace('/\s+([\]\)>\+])/', '$1', $content); |
|
|
$content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type'); |
|
|
$content = preg_replace('/:('.implode('|', $pseudos).')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content); |
|
|
|
|
|
|
|
|
$content = str_replace(';}', '}', $content); |
|
|
|
|
|
return trim($content); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function extractCalcs() |
|
|
{ |
|
|
|
|
|
$minifier = $this; |
|
|
$callback = function ($match) use ($minifier) { |
|
|
$length = strlen($match[1]); |
|
|
$expr = ''; |
|
|
$opened = 0; |
|
|
|
|
|
for ($i = 0; $i < $length; $i++) { |
|
|
$char = $match[1][$i]; |
|
|
$expr .= $char; |
|
|
if ($char === '(') { |
|
|
$opened++; |
|
|
} elseif ($char === ')' && --$opened === 0) { |
|
|
break; |
|
|
} |
|
|
} |
|
|
$rest = str_replace($expr, '', $match[1]); |
|
|
$expr = trim(substr($expr, 1, -1)); |
|
|
|
|
|
$count = count($minifier->extracted); |
|
|
$placeholder = 'calc('.$count.')'; |
|
|
$minifier->extracted[$placeholder] = 'calc('.$expr.')'; |
|
|
|
|
|
return $placeholder.$rest; |
|
|
}; |
|
|
|
|
|
$this->registerPattern('/calc(\(.+?)(?=$|;|calc\()/', $callback); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function canImportBySize($path) |
|
|
{ |
|
|
return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function canImportByPath($path) |
|
|
{ |
|
|
return preg_match('/^(data:|https?:|\\/)/', $path) === 0; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function getPathConverter($source, $target) |
|
|
{ |
|
|
return new Converter($source, $target); |
|
|
} |
|
|
} |
|
|
|