View source
<?php
namespace Drupal\advagg_js_minify\Asset;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Asset\AssetOptimizerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\State\StateInterface;
use Psr\Log\LoggerInterface;
class JsOptimizer implements AssetOptimizerInterface {
protected $cache;
protected $config;
protected $advaggConfig;
protected $advaggFiles;
protected $moduleHandler;
protected $logger;
public function __construct(CacheBackendInterface $minify_cache, ConfigFactoryInterface $config_factory, StateInterface $advagg_files, ModuleHandlerInterface $module_handler, LoggerInterface $logger) {
$this->cache = $minify_cache;
$this->config = $config_factory
->get('advagg_js_minify.settings');
$this->advaggConfig = $config_factory
->get('advagg.settings');
$this->advaggFiles = $advagg_files;
$this->moduleHandler = $module_handler;
$this->logger = $logger;
}
public function getConfiguration() {
$description = '';
$options = [
0 => t('Disabled'),
1 => t('JSMin+ ~1300ms'),
4 => t('JShrink ~1000ms'),
5 => t('JSqueeze ~600ms'),
];
if (function_exists('jsmin')) {
$options[3] = t('JSMin ~2ms');
$description .= t('JSMin is the very fast C complied version. Recommend using it.');
}
else {
$description .= t('You can use the much faster C version of JSMin (~2ms) by installing the <a href="@php_jsmin">JSMin PHP Extension</a> on this server.', [
'@php_jsmin' => 'https://github.com/sqmk/pecl-jsmin/',
]);
}
$minifiers = [
1 => 'jsminplus',
2 => 'packer',
4 => 'jshrink',
5 => 'jsqueeze',
];
if (function_exists('jsmin')) {
$minifiers[3] = 'jsmin';
}
$functions = [
1 => [
$this,
'minifyJsminplus',
],
2 => [
$this,
'minifyJspacker',
],
3 => [
$this,
'minifyJsmin',
],
4 => [
$this,
'minifyJshrink',
],
5 => [
$this,
'minifyJsqueeze',
],
];
$options_desc = [
$options,
$description,
];
$this->moduleHandler
->alter('advagg_js_minify_configuration', $options_desc, $minifiers, $functions);
list($options, $description) = $options_desc;
return [
$options,
$description,
$minifiers,
$functions,
];
}
public function optimize(array $js_asset) {
if ($js_asset['type'] !== 'file') {
throw new \Exception('Only file JavaScript assets can be optimized.');
}
if ($js_asset['type'] === 'file' && !$js_asset['preprocess']) {
throw new \Exception('Only file JavaScript assets with preprocessing enabled can be optimized.');
}
$data = file_get_contents($js_asset['data']);
if ($encoding = Unicode::encodingFromBOM($data)) {
$data = Unicode::substr(Unicode::convertToUtf8($data, $encoding), 1);
}
elseif (isset($js_asset['attributes']['charset'])) {
$data = Unicode::convertToUtf8($data, $js_asset['attributes']['charset']);
}
$minifier = $this->config
->get('minifier');
if ($file_settings = $this->config
->get('file_settings')) {
$file_settings = array_column($file_settings, 'minifier', 'path');
if (isset($file_settings[$js_asset['data']])) {
$minifier = $file_settings[$js_asset['data']];
}
}
if (empty($minifier) || $this->advaggConfig
->get('cache_level') < 0) {
return $data;
}
$semicolon_count = substr_count($data, ';');
if ($minifier != 2 && $semicolon_count > 10 && $semicolon_count > substr_count($data, "\n", strpos($data, ';')) * 5) {
if ($this->config
->get('add_license')) {
$url = file_create_url($js_asset['data']);
$data = "/* Source and licensing information for the line(s) below can be found at {$url}. */\n" . $data . "\n/* Source and licensing information for the above line(s) can be found at {$url}. */";
}
return $data;
}
$data_original = $data;
$before = strlen($data);
$info = $this->advaggFiles
->get($js_asset['data']);
$cid = 'js_minify:' . $minifier . ':' . $info['filename_hash'];
$cid .= !empty($info['content_hash']) ? ':' . $info['content_hash'] : '';
$cached_data = $this->cache
->get($cid);
if (!empty($cached_data->data)) {
$data = $cached_data->data;
}
else {
list(, , , $functions) = $this
->getConfiguration();
if (isset($functions[$minifier])) {
$run = $functions[$minifier];
if (is_callable($run)) {
call_user_func_array($run, [
&$data,
$js_asset,
]);
}
}
else {
return $data;
}
if (strpbrk(substr(trim($data), -1), ';})') === FALSE) {
$data = trim($data) . ';';
}
$this->cache
->set($cid, $data, REQUEST_TIME + 86400 * 7, [
'advagg_js',
$info['filename_hash'],
]);
$after = strlen($data);
$ratio = 0;
if ($before != 0) {
$ratio = ($before - $after) / $before;
}
if (empty($data) || empty($ratio) || $ratio < 0 || $ratio > $this->config
->get('ratio_max')) {
$data = $data_original;
}
elseif ($this->config
->get('add_license')) {
$url = file_create_url($js_asset['data']);
$data = "/* Source and licensing information for the line(s) below can be found at {$url}. */\n" . $data . "\n/* Source and licensing information for the above line(s) can be found at {$url}. */";
}
}
return $data;
}
public function clean($contents) {
$contents = preg_replace('/\\/\\/(#|@)\\s(sourceURL|sourceMappingURL)=\\s*(\\S*?)\\s*$/m', '', $contents);
return $contents;
}
public function minifyJsmin(&$contents, array $asset) {
if (!function_exists('jsmin')) {
$this->logger
->notice(t('The jsmin function does not exist. Using JSqueeze.'), []);
$contents = $this
->minifyJsqueeze($contents, $asset);
return;
}
if (version_compare(phpversion('jsmin'), '2.0.0', '<') && $this
->stringContainsMultibyteCharacters($contents)) {
$this->logger
->notice('The currently installed jsmin version does not handle multibyte characters, you may consider to upgrade the jsmin extension. Using JSqueeze fallback.', []);
$contents = $this
->minifyJsqueeze($contents, $asset);
return;
}
$contents = str_replace("\t", " ", $contents);
$minified = jsmin($contents);
$error = jsmin_last_error_msg();
if ($error != 'No error') {
$this->logger
->warning('JSMin had an error processing, usng JSqueeze fallback. Error details: ' . $error, []);
$contents = $this
->minifyJsqueeze($contents, $asset);
return;
}
if (ctype_cntrl(substr(trim($minified), -1)) || strpbrk(substr(trim($minified), -1), ';})') === FALSE) {
$contents = substr($minified, 0, strrpos($minified, ';'));
$this->logger
->notice(t('JSMin had an error minifying: @file, correcting.', [
'@file' => $asset['data'],
]));
}
else {
$contents = $minified;
}
$semicolons = substr_count($contents, ';', strlen($contents) - 5);
if ($semicolons > 2) {
$start = substr($contents, 0, -5);
$contents = $start . preg_replace("/([;)}]*)([\\w]*)([;)}]*)/", "\$1\$3", substr($contents, -5));
$this->logger
->notice(t('JSMin had an error minifying file: @file, attempting to correct.', [
'@file' => $asset['data'],
]));
}
}
public function minifyJsminplus(&$contents, array $asset, $log_errors = TRUE) {
$contents_before = $contents;
if (!class_exists('\\JSMinPlus')) {
include drupal_get_path('module', 'advagg_js_minify') . '/jsminplus.inc';
$nesting_level = ini_get('xdebug.max_nesting_level');
if (!empty($nesting_level) && $nesting_level < 200) {
ini_set('xdebug.max_nesting_level', 200);
}
}
ob_start();
try {
$contents = \JSMinPlus::minify($contents);
$error = trim(ob_get_contents());
if (!empty($error)) {
throw new \Exception($error);
}
} catch (\Exception $e) {
if ($log_errors) {
$this->logger
->warning($e
->getMessage() . '<pre>' . $contents_before . '</pre>', []);
}
$contents = $contents_before;
}
ob_end_clean();
}
public function minifyJspacker(&$contents, array $asset) {
if (!class_exists('\\JavaScriptPacker')) {
include drupal_get_path('module', 'advagg_js_minify') . '/jspacker.inc';
}
$contents = str_replace("}\n", "};\n", $contents);
$contents = str_replace("\nfunction", ";\nfunction", $contents);
$packer = new \JavaScriptPacker($contents, 62, TRUE, FALSE);
$contents = $packer
->pack();
}
public function minifyJshrink(&$contents, array $asset, $log_errors = TRUE) {
$contents_before = $contents;
if (!class_exists('\\JShrink\\Minifier')) {
include drupal_get_path('module', 'advagg_js_minify') . '/jshrink.inc';
$nesting_level = ini_get('xdebug.max_nesting_level');
if (!empty($nesting_level) && $nesting_level < 200) {
ini_set('xdebug.max_nesting_level', 200);
}
}
ob_start();
try {
$contents = \JShrink\Minifier::minify($contents, [
'flaggedComments' => FALSE,
]);
$error = trim(ob_get_contents());
if (!empty($error)) {
throw new \Exception($error);
}
} catch (\Exception $e) {
if ($log_errors) {
$this->logger
->warning($e
->getMessage() . '<pre>' . $contents_before . '</pre>', []);
}
$contents = $contents_before;
}
ob_end_clean();
}
public function minifyJsqueeze(&$contents, array $asset, $log_errors = TRUE) {
$contents_before = $contents;
if (!class_exists('\\Patchwork\\JSqueeze')) {
include drupal_get_path('module', 'advagg_js_minify') . '/jsqueeze.inc';
$nesting_level = ini_get('xdebug.max_nesting_level');
if (!empty($nesting_level) && $nesting_level < 200) {
ini_set('xdebug.max_nesting_level', 200);
}
}
ob_start();
try {
$jz = new \Patchwork\JSqueeze();
$contents = $jz
->squeeze($contents, TRUE, !\Drupal::config('advagg_js_minify.settings')
->get('add_license'), FALSE);
$error = trim(ob_get_contents());
if (!empty($error)) {
throw new \Exception($error);
}
} catch (\Exception $e) {
if ($log_errors) {
$this->logger
->warning('JSqueeze error, skipping file. ' . $e
->getMessage() . '<pre>' . $contents_before . '</pre>', []);
}
$contents = $contents_before;
}
ob_end_clean();
}
public function stringContainsMultibyteCharacters($string) {
if (strlen($string) == drupal_strlen($string)) {
return FALSE;
}
return TRUE;
}
}