Scripts
CSS & JS Minifier
CSS & JavaScript minifier in PHP. CSS * Verwijdert comments * Verwijdert witruimte * Importeert @import-ed CSS bestanden * Embed kleine statische bestanden in minified bestand (base64-geencodeerd) * Verkort hexadecimale kleurcodes * Verkort nulwaardes (zoals -0px) JavaScript * Verwijdert comments * Verwijdert witruimte Documentatie, voorbeeldcode & online test op http://www.minifier.org Code op https://github.com/matthiasmullie/minify Voor gebruik met composer: https://packagist.org/packages/matthiasmullie/minify
Minify.php
<?php
namespace MatthiasMullie\Minify;
/**
* Abstract minifier class.
*
* Please report bugs on https://github.com/matthiasmullie/minify/issues
*
* @author Matthias Mullie <[email protected]>
*
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved.
* @license MIT License
*/
abstract class Minify
{
/**
* The data to be minified
*
* @var string[]
*/
protected $data = array();
/**
* Array of patterns to match.
*
* @var string[]
*/
protected $patterns = array();
/**
* This array will hold content of strings and regular expressions that have
* been extracted from the JS source code, so we can reliably match "code",
* without having to worry about potential "code-like" characters inside.
*
* @var string[]
*/
public $extracted = array();
/**
* Init the minify class - optionally, code may be passed along already.
*/
public function __construct(/* $data = null, ... */)
{
// it's possible to add the source through the constructor as well ;)
if (func_num_args()) {
call_user_func_array(array($this, 'add'), func_get_args());
}
}
/**
* Add a file or straight-up code to be minified.
*
* @param string $data
*/
public function add($data /* $data = null, ... */)
{
// bogus "usage" of parameter $data: scrutinizer warns this variable is
// not used (we're using func_get_args instead to support overloading),
// but it still needs to be defined because it makes no sense to have
// this function without argument :)
$args = array($data) + func_get_args();
// this method can be overloaded
foreach ($args as $data) {
// redefine var
$data = (string) $data;
// load data
$value = $this->load($data);
$key = ($data != $value) ? $data : count($this->data);
// store data
$this->data[$key] = $value;
}
}
/**
* Load data.
*
* @param string $data Either a path to a file or the content itself.
* @return string
*/
protected function load($data)
{
// check if the data is a file
if (@file_exists($data) && is_file($data)) {
$data = @file_get_contents($data);
// strip BOM, if any
if (substr($data, 0, 3) == "\xef\xbb\xbf") {
$data = substr($data, 3);
}
}
return $data;
}
/**
* Save to file
*
* @param string $content The minified data.
* @param string $path The path to save the minified data to.
* @throws Exception
*/
protected function save($content, $path)
{
// create file & open for writing
if (($handler = @fopen($path, 'w')) === false) {
throw new Exception('The file "' . $path . '" could not be opened. Check if PHP has enough permissions.');
}
// write to file
if (@fwrite($handler, $content) === false) {
throw new Exception('The file "' . $path . '" could not be written to. Check if PHP has enough permissions.');
}
// close the file
@fclose($handler);
}
/**
* Minify the data.
*
* @param string[optional] $path Path to write the data to.
* @return string The minified data.
*/
abstract public function minify($path = null);
/**
* Register a pattern to execute against the source content.
*
* @param string $pattern PCRE pattern.
* @param string|callable $replacement Replacement value for matched pattern.
* @throws Exception
*/
protected function registerPattern($pattern, $replacement = '')
{
// study the pattern, we'll execute it more than once
$pattern .= 'S';
$this->patterns[] = array($pattern, $replacement);
}
/**
* We can't "just" run some regular expressions against JavaScript: it's a
* complex language. E.g. having an occurrence of // xyz would be a comment,
* unless it's used within a string. Of you could have something that looks
* like a 'string', but inside a comment.
* The only way to accurately replace these pieces is to traverse the JS one
* character at a time and try to find whatever starts first.
*
* @param string $content The content to replace patterns in.
* @return string The (manipulated) content.
*/
protected function replace($content)
{
$processed = '';
while ($content) {
// execute all patterns and find the first match
$matches = array();
foreach ($this->patterns as $i => $pattern) {
list($pattern, $replacement) = $pattern;
$match = null;
if (preg_match($pattern, $content, $match)) {
$matches[$i] = $match;
}
}
// no more matches to find: everything's been processed, break out
if (!$matches) {
$processed .= $content;
break;
}
// see which of the patterns actually found the first thing (we'll
// only want to execute that one, since we're unsure if what the
// other found was not inside what the first found)
$positions = array();
foreach ($matches as $i => $match) {
$positions[$i] = strpos($content, $match[0]);
}
$discardLength = min($positions);
$firstPattern = array_search($discardLength, $positions);
$match = $matches[$firstPattern][0];
// execute the pattern that matches earliest in the content string
list($pattern, $replacement) = $this->patterns[$firstPattern];
$replacement = $this->replacePattern($pattern, $replacement, $content);
// figure out which part of the string was unmatched; that's the
// part we'll execute the patterns on again next
$content = substr($content, $discardLength);
$unmatched = (string) substr($content, strpos($content, $match) + strlen($match));
// move the replaced part to $processed and prepare $content to
// again match batch of patterns against
$processed .= substr($replacement, 0, strlen($replacement) - strlen($unmatched));
$content = $unmatched;
}
return $processed;
}
/**
* This is where a pattern is matched against $content and the matches
* are replaced by their respective value.
* This function will be called plenty of times, where $content will always
* move up 1 character.
*
* @param string $pattern Pattern to match.
* @param string|callable $replacement Replacement value.
* @param string $content Content to match pattern against.
* @return string
*/
protected function replacePattern($pattern, $replacement, $content)
{
if (is_callable($replacement)) {
return preg_replace_callback($pattern, $replacement, $content, 1, $count);
} else {
return preg_replace($pattern, $replacement, $content, 1, $count);
}
}
/**
* Strings are a pattern we need to match, in order to ignore potential
* code-like content inside them, but we just want all of the string
* content to remain untouched.
*
* This method will replace all string content with simple STRING#
* placeholder text, so we've rid all strings from characters that may be
* misinterpreted. Original string content will be saved in $this->extracted
* and after doing all other minifying, we can restore the original content
* via restoreStrings()
*/
protected function extractStrings()
{
// PHP only supports $this inside anonymous functions since 5.4
$minifier = $this;
$callback = function ($match) use ($minifier) {
$count = count($minifier->extracted);
$placeholder = $match[1] . 'STRING' . $count . $match[1];
$minifier->extracted[$placeholder] = $match[1] . $match[2] . $match[1];
return $placeholder;
};
$this->registerPattern('/([\'"])(.*?)(?<!\\\\)\\1/s', $callback);
}
/**
* This method will restore all extracted data (strings, regexes) that were
* replaced with placeholder text in extract*(). The original content was
* saved in $this->extracted.
*
* @param string $content
* @return string
*/
protected function restoreExtractedData($content)
{
$content = str_replace(array_keys($this->extracted), $this->extracted, $content);
$this->extracted = array();
return $content;
}
}
CSS.php
<?php
namespace MatthiasMullie\Minify;
/**
* CSS minifier.
*
* Please report bugs on https://github.com/matthiasmullie/minify/issues
*
* @author Matthias Mullie <[email protected]>
* @author Tijs Verkoyen <[email protected]>
*
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved.
* @license MIT License
*/
class CSS extends Minify
{
/**
* @var int
*/
protected $maxImportSize = 5;
/**
* @var string[]
*/
protected $importExtensions = array(
'gif' => 'data:image/gif',
'png' => 'data:image/png',
'jpg' => 'data:image/jpg',
'jpeg' => 'data:image/jpeg',
'svg' => 'data:image/svg+xml',
'woff' => 'data:application/x-font-woff',
);
/**
* Set the maximum size if files to be imported.
*
* Files larger than this size (in kB) will not be imported into the CSS.
* Importing files into the CSS as data-uri will save you some connections,
* but we should only import relatively small decorative images so that our
* CSS file doesn't get too bulky.
*
* @param int $size Size in kB
*/
public function setMaxImportSize($size) {
$this->maxImportSize = $size;
}
/**
* Set the type of extensions to be imported into the CSS (to save network
* connections).
* Keys of the array should be the file extensions & respective values
* should be the data type.
*
* @param string[] $extensions Array of file extensions
*/
public function setImportExtensions(array $extensions) {
$this->importExtensions = $extensions;
}
/**
* Combine CSS from import statements.
* @import's will be loaded and their content merged into the original file,
* to save HTTP requests.
*
* @param string $source The file to combine imports for.
* @param string $content The CSS content to combine imports for.
* @return string
*/
protected function combineImports($source, $content)
{
$importRegexes = array(
// @import url(xxx)
'/
# import statement
@import
# whitespace
\s+
# open url()
url\(
# (optional) open path enclosure
(?P<quotes>["\']?)
# fetch path
(?P<path>
# do not fetch data uris or external sources
(?!(
["\']?
(data|https?):
))
.+?
)
# (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 'xxx'
'/
# import statement
@import
# whitespace
\s+
# open path enclosure
(?P<quotes>["\'])
# fetch path
(?P<path>
# do not fetch data uris or external sources
(?!(
["\']?
(data|https?):
))
.+?
)
# close path enclosure
(?P=quotes)
# (optional) trailing whitespace
\s*
# (optional) media statement(s)
(?P<media>[^;]*)
# (optional) trailing whitespace
\s*
# (optional) closing semi-colon
;?
/ix'
);
// find all relative imports in css
$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();
// loop the matches
foreach ($matches as $match) {
// get the path for the file that will be imported
$importPath = dirname($source) . '/' . $match['path'];
// only replace the import with the content if we can grab the
// content of the file
if (@file_exists($importPath) && is_file($importPath)) {
// grab referenced file & minify it (which may include importing
// yet other @import statements recursively)
$minifier = new static($importPath);
$importContent = $minifier->minify($source);
// check if this is only valid for certain media
if ($match['media']) {
$importContent = '@media ' . $match['media'] . '{' . $importContent . '}';
}
// add to replacement array
$search[] = $match[0];
$replace[] = $importContent;
}
}
// replace the import statements
$content = str_replace($search, $replace, $content);
return $content;
}
/**
* Convert relative paths based upon 1 path to another.
*
* E.g.
* ../images/img.gif (relative to /home/forkcms/frontend/core/layout/css)
* should become:
* ../../core/layout/images/img.gif (relative to
* /home/forkcms/frontend/cache/minified_css)
*
* @param string $path The relative path that needs to be converted.
* @param string $from The original base path.
* @param string $to The new base path.
* @return string The new relative path.
*/
protected function convertRelativePath($path, $from, $to)
{
$from = $from ? realpath($from) : '';
$to = $to ? realpath($to) : '';
// make sure we're dealing with directories
$from = @is_file($from) ? dirname($from) : $from;
$to = @is_file($to) ? dirname($to) : $to;
// deal with different operating systems' directory structure
$path = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $path), '/');
$from = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $from), '/');
$to = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $to), '/');
// if we're not dealing with a relative path, just return absolute
if (strpos($path, '/') === 0) {
return $path;
}
// get full path to file referenced from $from
$path = $from . '/' . $path;
/*
* Example:
* $path = /home/forkcms/frontend/cache/compiled_templates/../../core/layout/css/../images/img.gif
* $to = /home/forkcms/frontend/cache/minified_css
*/
// normalize paths
do {
list($path, $to) = preg_replace('/[^\/]+(?<!\.\.)\/\.\.\//', '', array($path, $to), -1, $count);
} while ($count);
/*
* Example:
* $path = /home/forkcms/frontend/core/layout/images/img.gif
* $to = /home/forkcms/frontend/cache/minified_css
*/
$path = explode('/', $path);
$to = explode('/', $to);
// compare paths & strip identical ancestors
foreach ($path as $i => $chunk) {
if (isset($to[$i]) && $path[$i] == $to[$i]) {
unset($path[$i], $to[$i]);
} else {
break;
}
}
/*
* At this point:
* $path = array('core', 'layout', 'images', 'img.gif')
* $to = array('cache', 'minified_css')
*/
$path = implode('/', $path);
// add .. for every directory that needs to be traversed for new path
$to = str_repeat('../', count($to));
/*
* At this point:
* $path = core/layout/images/img.gif
* $to = ../../
*/
// Tada!
return $to . $path;
}
/**
* Import files into the CSS, base64-ized.
* @url(image.jpg) images will be loaded and their content merged into the
* original file, to save HTTP requests.
*
* @param string $source The file to import files for.
* @param string $content The CSS content to import files for.
* @return string
*/
protected function importFiles($source, $content)
{
$extensions = array_keys($this->importExtensions);
$regex = '/url\((["\']?)((?!["\']?data:).*?\.(' . implode('|', $extensions) . '))\\1\)/i';
if ($extensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
$search = array();
$replace = array();
// loop the matches
foreach ($matches as $match) {
// get the path for the file that will be imported
$path = $match[2];
$path = dirname($source) . '/' . $path;
$extension = $match[3];
// only replace the import with the content if we're able to get
// the content of the file, and it's relatively small
$import = @file_exists($path);
$import = $import && is_file($path);
$import = $import && filesize($path) <= $this->maxImportSize * 1024;
if (!$import) {
continue;
}
// grab content && base64-ize
$importContent = $this->load($path);
$importContent = base64_encode($importContent);
// build replacement
$search[] = $match[0];
$replace[] = 'url(' . $this->importExtensions[$extension] . ';base64,' . $importContent . ')';
}
// replace the import statements
$content = str_replace($search, $replace, $content);
}
return $content;
}
/**
* Minify the data.
* Perform CSS optimizations.
*
* @param string[optional] $path Path to write the data to.
* @return string The minified data.
*/
public function minify($path = null)
{
$content = '';
// loop files
foreach ($this->data as $source => $css) {
// if we'll save to a new path, we'll have to fix the relative paths
if ($source !== 0) {
$css = $this->move($source, $path, $css);
}
// combine css
$content .= $css;
}
/*
* Let's first take out strings & comments, since we can't just remove
* whitespace anywhere. If whitespace occurs inside a string, we should
* leave it alone. E.g.:
* p { content: "a test" }
*/
$this->extractStrings();
$this->stripComments();
$content = $this->replace($content);
$content = $this->stripWhitespace($content);
$content = $this->shortenHex($content);
$content = $this->shortenZeroes($content);
// restore the string we've extracted earlier
$content = $this->restoreExtractedData($content);
$content = $this->importFiles($path, $content);
$content = $this->combineImports($path, $content);
// save to path
if ($path !== null) {
$this->save($content, $path);
}
return $content;
}
/**
* Moving a css file should update all relative urls.
* Relative references (e.g. ../images/image.gif) in a certain css file,
* will have to be updated when a file is being saved at another location
* (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper)
*
* @param string $source The file to update relative urls for.
* @param string $destination The path the data will be written to.
* @param string $content The CSS content to update relative urls for.
* @return string
*/
protected function move($source, $destination, $content)
{
/*
* Relative path references will usually be enclosed by url(). @import
* is an exception, where url() is not necessary around the path (but is
* allowed).
* This *could* be 1 regular expression, where both regular expressions
* in this array are on different sides of a |. But we're using named
* patterns in both regexes, the same name on both regexes. This is only
* possible with a (?J) modifier, but that only works after a fairly
* recent PCRE version. That's why I'm doing 2 separate regular
* expressions & combining the matches after executing of both.
*/
$relativeRegexes = array(
// url(xxx)
'/
# open url()
url\(
# open path enclosure
(?P<quotes>["\'])?
# fetch path
(?P<path>
# do not fetch data uris or external sources
(?!(
["\']?
(data|https?):
))
.+?
)
# close path enclosure
(?(quotes)(?P=quotes))
# close url()
\)
/ix',
// @import "xxx"
'/
# 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>
# do not fetch data uris or external sources
(?!(
["\']?
(data|https?):
))
.+?
)
# close path enclosure
(?P=quotes)
/ix'
);
// find all relative urls in css
$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();
// loop all urls
foreach ($matches as $match) {
// determine if it's a url() or an @import match
$type = (strpos($match[0], '@import') === 0 ? 'import' : 'url');
// fix relative url
$url = $this->convertRelativePath($match['path'], dirname($source), dirname($destination));
// build replacement
$search[] = $match[0];
if ($type == 'url') {
$replace[] = 'url(' . $url . ')';
} elseif ($type == 'import') {
$replace[] = '@import "' . $url . '"';
}
}
// replace urls
$content = str_replace($search, $replace, $content);
return $content;
}
/**
* Shorthand hex color codes.
* #FF0000 -> #F00
*
* @param string $content The CSS content to shorten the hex color codes for.
* @return string
*/
protected function shortenHex($content)
{
$content = preg_replace('/(?<![\'"])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?![\'"])/i', '#$1$2$3', $content);
return $content;
}
/**
* Shorthand 0 values to plain 0, instead of e.g. -0em.
*
* @param string $content The CSS content to shorten the zero values for.
* @return string
*/
protected function shortenZeroes($content)
{
$content = preg_replace('/(?<![0-9])-?0(%|px|em)?/i', '0', $content);
return $content;
}
/**
* Strip comments from source code.
*/
protected function stripComments()
{
$this->registerPattern('/\/\*.*?\*\//s', '');
}
/**
* Strip whitespace.
*
* @param string $content The CSS content to strip the whitespace for.
* @return string
*/
protected function stripWhitespace($content)
{
// remove leading & trailing whitespace
$content = preg_replace('/^\s*/m', '', $content);
$content = preg_replace('/\s*$/m', '', $content);
// replace newlines with a single space
$content = preg_replace('/\s+/', ' ', $content);
// remove whitespace around meta characters
// inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex
$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);
// whitespace around + and - can only be stripped in selectors, like
// :nth-child(3+2n), not in things like calc(3px + 2px) or shorthands
// like 3px -2px
$content = preg_replace('/\s*([+-])\s*(?=[^}]*{)/', '$1', $content);
// remove semicolon/whitespace followed by closing bracket
$content = preg_replace('/;}/', '}', $content);
return trim($content);
}
}
Exception.php
<?php
namespace MatthiasMullie\Minify;
/**
* @author Matthias Mullie <[email protected]>
*/
class Exception extends \Exception
{
}
JS.php
<?php
namespace MatthiasMullie\Minify;
/**
* JavaScript minifier.
*
* Please report bugs on https://github.com/matthiasmullie/minify/issues
*
* @author Matthias Mullie <[email protected]>
* @author Tijs Verkoyen <[email protected]>
*
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved.
* @license MIT License
*/
class JS extends Minify
{
/**
* List of JavaScript operators that accept a <variable, value, ...> after
* them. We'll insert semicolons if they're missing at EOL, but some
* end of lines are not the end of a statement, like with these operators.
*
* Note: Most operators are fine, we've only removed !, ++ and --.
* There can't be a newline separating ! and whatever it is negating.
* ++ & -- have to be joined with the value they're in-/decrementing.
*
* Will be loaded from /data/js/operators_before.txt
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators
* @var string[]
*/
protected $operatorsBefore = array();
/**
* List of JavaScript operators that accept a <variable, value, ...> before
* them. We'll insert semicolons if they're missing at EOL, but some end of
* lines are not the end of a statement, like when continued by one of these
* operators on the newline.
*
* Note: Most operators are fine, we've only removed ), ], ++ and --.
* ++ & -- have to be joined with the value they're in-/decrementing.
* ) & ] are "special" in that they have lots or usecases. () for example
* is used for function calls, for grouping, in if () and for (), ...
*
* Will be loaded from /data/js/operators_after.txt
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators
* @var string[]
*/
protected $operatorsAfter = array();
/**
* List of JavaScript reserved words that accept a <variable, value, ...>
* after them. We'll insert semicolons if they're missing at EOL, but some
* end of lines are not the end of a statement, like with these keywords.
*
* E.g.: we shouldn't insert a ; after this else
* else
* console.log('this is quite fine')
*
* Will be loaded from /data/js/reserved_before.txt
*
* @see https://mathiasbynens.be/notes/reserved-keywords
* @var string[]
*/
protected $keywordsBefore = array();
/**
* List of JavaScript reserved words that accept a <variable, value, ...>
* before them. We'll insert semicolons if they're missing at EOL, but some
* end of lines are not the end of a statement, like when continued by one
* of these keywords on the newline.
*
* E.g.: we shouldn't insert a ; before this instanceof
* variable
* instanceof String
*
* Will be loaded from /data/js/reserved_after.txt
*
* @see https://mathiasbynens.be/notes/reserved-keywords
* @var string[]
*/
protected $keywordsAfter = array();
/**
* {@inheritDoc}
*/
public function __construct()
{
call_user_func_array(array('parent', '__construct'), func_get_args());
$dataDir = __DIR__ . '/../data/js/';
$options = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
$this->keywordsBefore = file($dataDir . 'keywords_before.txt', $options);
$this->keywordsAfter = file($dataDir . 'keywords_after.txt', $options);
$this->operatorsBefore = file($dataDir . 'operators_before.txt', $options);
$this->operatorsAfter = file($dataDir . 'operators_after.txt', $options);
}
/**
* Minify the data.
* Perform JS optimizations.
*
* @param string[optional] $path Path to write the data to.
* @return string The minified data.
*/
public function minify($path = null)
{
$content = '';
// loop files
foreach ($this->data as $source => $js) {
// combine js (separate sources with semicolon)
$content .= $js . ';';
}
/*
* Let's first take out strings, comments and regular expressions.
* All of these can contain JS code-like characters, and we should make
* sure any further magic ignores anything inside of these.
*
* Consider this example, where we should not strip any whitespace:
* var str = "a test";
*
* Comments will be removed altogether, strings and regular expressions
* will be replaced by placeholder text, which we'll restore later.
*/
$this->extractStrings();
$this->stripComments();
$this->extractRegex();
$content = $this->replace($content);
$content = $this->stripWhitespace($content);
/*
* Earlier, we extracted strings & regular expressions and replaced them
* with placeholder text. This will restore them.
*/
$content = $this->restoreExtractedData($content);
// save to path
if ($path !== null) {
$this->save($content, $path);
}
return $content;
}
/**
* Strip comments from source code.
*/
protected function stripComments()
{
// single-line comments
$this->registerPattern('/\/\/.*$[\r\n]*/m', '');
// multi-line comments
$this->registerPattern('/\/\*.*?\*\//s', '');
}
/**
* JS kan have /-delimited regular expressions, like: /ab+c/.match(string)
*
* The content inside the regex can contain characters that may be confused
* for JS code: e.g. it could contain whitespace it needs to match & we
* don't want to strip whitespace in there.
*
* The regex can be pretty simple: we don't have to care about comments,
* (which also use slashes) because stripComments() will have stripped those
* already.
*
* This method will replace all string content with simple REGEX#
* placeholder text, so we've rid all regular expressions from characters
* that may be misinterpreted. Original regex content will be saved in
* $this->extracted and after doing all other minifying, we can restore the
* original content via restoreRegex()
*/
protected function extractRegex()
{
// PHP only supports $this inside anonymous functions since 5.4
$minifier = $this;
$callback = function ($match) use ($minifier) {
$count = count($minifier->extracted);
$placeholder = '/REGEX' . $count . '/';
$minifier->extracted[$placeholder] = '/' . $match[1] . '/';
return $placeholder;
};
// it's a regex if we can find an opening (not preceded by variable,
// value or similar) & (non-escaped) closing /,
$before = $this->getOperatorsForRegex($this->operatorsBefore, '/');
$this->registerPattern('/^\s*\K\/(.*?(?<!\\\\)(\\\\\\\\)*)\//', $callback);
$this->registerPattern('/(?:' . implode('|', $before) . ')\s*\K\/(.*?(?<!\\\\)(\\\\\\\\)*)\//', $callback);
}
/**
* Strip whitespace.
*
* We won't strip *all* whitespace, but as much as possible. The thing that
* we'll preserve are newlines we're unsure about.
* JavaScript doesn't require statements to be terminated with a semicolon.
* It will automatically fix missing semicolons with ASI (automatic semi-
* colon insertion) at the end of line causing errors (without semicolon.)
*
* Because it's sometimes hard to tell if a newline is part of a statement
* that should be terminated or not, we'll just leave some of them alone.
*
* @param string $content The content to strip the whitespace for.
* @return string
*/
protected function stripWhitespace($content)
{
// uniform line endings, make them all line feed
$content = str_replace(array("\r\n", "\r"), "\n", $content);
// collapse all non-line feed whitespace into a single space
$content = preg_replace('/[^\S\n]+/', ' ', $content);
// strip leading & trailing whitespace
$content = str_replace(array(" \n", "\n "), "\n", $content);
// collapse consecutive line feeds into just 1
$content = preg_replace('/\n+/', "\n", $content);
// strip whitespace that ends in (or next line begin with) an operator
// that allows statements to be broken up over multiple lines
$before = $this->getOperatorsForRegex($this->operatorsBefore, '/');
$after = $this->getOperatorsForRegex($this->operatorsAfter, '/');
$content = preg_replace('/(' . implode('|', $before) . ')\s+/', '\\1', $content);
$content = preg_replace('/\s+(' . implode('|', $after) . ')/', '\\1', $content);
// make sure + and - can't be mistaken for, or joined into ++ and --
$content = preg_replace('/(?<![\+\-])\s*([\+\-])/', '\\1', $content);
$content = preg_replace('/([\+\-])\s*(?!\\1)/', '\\1', $content);
// collapse whitespace around reserved words into single space
$before = $this->getKeywordsForRegex($this->keywordsBefore, '/');
$after = $this->getKeywordsForRegex($this->keywordsAfter, '/');
$content = preg_replace('/(' . implode('|', $before) . ')\s+/', '\\1 ', $content);
$content = preg_replace('/\s+(' . implode('|', $after) . ')/', ' \\1', $content);
/*
* We didn't strip whitespace after a couple of operators because they
* could be used in different contexts and we can't be sure it's ok to
* strip the newlines. However, we can safely strip any non-line feed
* whitespace that follows them.
*/
$operators = $this->getOperatorsForRegex($this->operatorsBefore + $this->operatorsAfter, '/');
$content = preg_replace('/([\}\)\]])[^\S\n]+(?!' . implode('|', $operators) . ')/', '\\1', $content);
/*
* We also don't really want to terminate statements followed by closing
* curly braces (which we've ignored completely up until now): ASI will
* kick in here & we're all about minifying.
*/
$content = preg_replace('/;\}/s', '}', $content);
// get rid of remaining whitespace af beginning/end, as well as
// semicolon, which doesn't make sense there: ASI will kick in here too
return trim($content, "\n ;");
}
/**
* We'll strip whitespace around certain operators with regular expressions.
* This will prepare the given array by escaping all characters.
*
* @param string[] $operators
* @param string $delimiter
* @return string[]
*/
protected function getOperatorsForRegex(array $operators, $delimiter = '/')
{
// escape operators for use in regex
$delimiter = array_fill(0, count($operators), $delimiter);
$escaped = array_map('preg_quote', $operators, $delimiter);
$operators = array_combine($operators, $escaped);
// ignore + & - for now, they'll get special treatment
unset($operators['+'], $operators['-']);
// dot can not just immediately follow a number; it can be confused
// between decimal point, or calling a method on it, e.g. 42 .toString()
$operators['.'] = '(?<![0-9]\s)\.';
return $operators;
}
/**
* We'll strip whitespace around certain keywords with regular expressions.
* This will prepare the given array by escaping all characters.
*
* @param string[] $keywords
* @param string $delimiter
* @return string[]
*/
protected function getKeywordsForRegex(array $keywords, $delimiter = '/')
{
// escape keywords for use in regex
$delimiter = array_fill(0, count($keywords), $delimiter);
$escaped = array_map('preg_quote', $keywords, $delimiter);
// add word boundaries
array_walk($keywords, function ($value) {
return '\b' . $value . '\b';
});
$keywords = array_combine($keywords, $escaped);
return $keywords;
}
}
Reacties
0