Files
2025-12-19 16:21:27 -08:00

191 lines
5.3 KiB
PHP

<?php
namespace Garfix\JsMinify;
use RuntimeException;
/**
* This minifier removes whitespace and comments from a Javascript string.
*
* See docs for information.
*
* @author Patrick van Bergen
*/
class Minifier
{
const FLAGGED_COMMENTS = 'flaggedComments';
protected static $defaultOptions = [
self::FLAGGED_COMMENTS => true
];
protected $options = [];
/**
* Processes a javascript string and returns only the required characters,
* stripping out all unneeded whitespace and comments.
*
* @param string $js The raw javascript to be minified
* @param array $options Various runtime options in an associative array
* @throws RuntimeException
*/
public static function minify($js, $options = [])
{
$minifier = new Minifier($options);
return $minifier->minifyUsingCallbacks($js);
}
public function __construct($options)
{
$this->options = array_merge(static::$defaultOptions, $options);
}
/**
* @param string $js The raw javascript to be minified
* @return string
* @throws RuntimeException
*/
public function minifyUsingCallbacks($js)
{
$e = MinifierExpressions::get();
// treat all newlines as unix newlines, to keep working with newlines simple
$shrunkText = str_replace(["\r\n", "\r"], ["\n", "\n"], $js);
// remove comments
$exp1 = "~(?:" . implode("|", $e->tokenExpressions) . ")~su";
$shrunkText = preg_replace_callback($exp1, [$this, 'removeComments'], $shrunkText);
MinifierError::checkRegexpError();
// rewrite blocks by inserting whitespace placeholders
$exp2 = "~(?:" . implode("|", $e->allExpressions) . ")~su";
$shrunkText = preg_replace_callback($exp2, [$this, 'processBlocks'], $shrunkText);
MinifierError::checkRegexpError();
// remove all remaining space (without the newlines)
$shrunkText = preg_replace("~" . $e->someWhitespace . "~", '', $shrunkText);
MinifierError::checkRegexpError();
// reduce consecutive newlines to single one
$shrunkText = preg_replace("~[\\n]+~", "\n", $shrunkText);
MinifierError::checkRegexpError();
// remove newlines that may safely be removed
foreach ($e->safeNewlines as $safeNewline) {
$shrunkText = preg_replace("~" . $safeNewline . "~", "", $shrunkText);
MinifierError::checkRegexpError();
}
// replace whitespace placeholders by their original whitespace
$shrunkText = str_replace($e->whitespaceArrayPlaceholders, $e->whitespaceArrayNewline, $shrunkText);
MinifierError::checkRegexpError();
// remove leading and trailing whitespace
return trim($shrunkText);
}
/**
* Removes all comments that need to be removed
* The newlines that are added here may later be removed again
*
* @param $matches
* @return mixed|string
*/
protected function removeComments($matches)
{
$e = MinifierExpressions::get();
// the fully matching text
$match = $matches[0];
if (!empty($matches['lineComment'])) {
// not empty because this might glue words together
return "\n";
}
if (!empty($matches['starComment'])) {
// create a version without leading and trailing whitespace
$trimmed = trim($match, $e->whitespaceCharsNewline);
switch ($trimmed[2]) {
case '@':
// IE conditional comment
return $match;
case '!':
if ($this->options[self::FLAGGED_COMMENTS]) {
// option says: leave flagged comments in
return $match;
}
}
// multi line comment; not empty because this might glue words together
return "\n";
}
// leave other matches unchanged
return $match;
}
/**
* Updates the code for all blocks (they contain whitespace that should be conserved)
* No early returns: all code must reach `end` and have the whitespace replaced by placeholders
*
* @param $matches
* @return array|mixed|string|string[]
*/
protected function processBlocks($matches)
{
$e = MinifierExpressions::get();
// the fully matching text
$match = $matches[0];
// create a version without leading and trailing whitespace
$trimmed = trim($match, $e->whitespaceCharsNewline);
// Should be handled before optional whitespace
if (!empty($matches['requiredSpace'])) {
$match = strpos($matches['requiredSpace'], "\n") === false ? " " : "\n";
goto end;
}
// + followed by +, or - followed by -
if (!empty($matches['plus']) || !empty($matches['min'])) {
$match = ' ';
goto end;
}
if (!empty($matches['doubleQuote'])) {
// remove line continuation
$match = str_replace("\\\n", "", $match);
goto end;
}
if (!empty($matches['starComment'])) {
switch ($trimmed[2]) {
case '@':
// IE conditional comment
$match = $trimmed;
goto end;
case '!':
if ($this->options[self::FLAGGED_COMMENTS]) {
// ensure newlines before and after
$match = "\n" . $trimmed . "\n";
goto end;
}
}
// simple multi line comment; will have been removed in the first step
goto end;
}
if (!empty($matches['regexp'])) {
// regular expression
// only if the space after the regexp contains a newline, keep it
preg_match("~^" . $e->regexp . "(?P<post>" . $e->optionalWhitespaceNewline . ")" . "$~su",
$match, $newMatches);
$postfix = strpos($newMatches['post'], "\n") === false ? "" : "\n";
$match = $trimmed . $postfix;
goto end;
}
end:
return str_replace($e->whitespaceArrayNewline, $e->whitespaceArrayPlaceholders, $match);
}
}