Initial commit

This commit is contained in:
2018-04-02 08:07:38 +02:00
commit 7330c1ed3e
2054 changed files with 405203 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
<?php
namespace SGalinski\Scriptmerger;
/***************************************************************
* Copyright notice
*
* (c) Stefan Galinski <stefan@sgalinski.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This class contains the basic stuff required by both processors, css and javascript.
*/
abstract class ScriptmergerBase {
/**
* directories for minified, compressed and merged files
*
* @var array
*/
protected $tempDirectories = '';
/**
* @var array
*/
protected $configuration;
/**
* Constructor
*/
public function __construct() {
$this->tempDirectories = array(
'main' => PATH_site . 'typo3temp/scriptmerger/',
'temp' => PATH_site . 'typo3temp/scriptmerger/temp/',
'minified' => PATH_site . 'typo3temp/scriptmerger/uncompressed/',
'compressed' => PATH_site . 'typo3temp/scriptmerger/compressed/',
'merged' => PATH_site . 'typo3temp/scriptmerger/uncompressed/'
);
foreach ($this->tempDirectories as $directory) {
if (!is_dir($directory)) {
GeneralUtility::mkdir($directory);
}
}
}
/**
* Injects the extension configuration
*
* @param array $configuration
* @return void
*/
public function injectExtensionConfiguration(array $configuration) {
$this->configuration = $configuration;
}
/**
* Controller for the dedicated script type processing
*
* @return void
*/
abstract function process();
/**
* Gets a file from an external resource (e.g. http://) and caches them
*
* @param string $source Source address
* @param boolean $returnContent
* @return string cache file or content (depends on the parameter)
*/
protected function getExternalFile($source, $returnContent = FALSE) {
$filename = basename($source);
$hash = md5($source);
$cacheFile = $this->tempDirectories['temp'] . $filename . '-' . $hash;
$externalFileCacheLifetime = intval($this->configuration['externalFileCacheLifetime']);
$cacheLifetime = ($externalFileCacheLifetime > 0) ? $externalFileCacheLifetime : 3600;
// check the age of the cache file (also fails with non-existent file)
$content = '';
if ((int) @filemtime($cacheFile) <= ($GLOBALS['EXEC_TIME'] - $cacheLifetime)) {
if ($source{0} === '/' && $source{1} === '/') {
$protocol = stripos($_SERVER['SERVER_PROTOCOL'], 'https') === TRUE ? 'https:' : 'http:';
$source = $protocol . $source;
}
$content = GeneralUtility::getURL($source);
if ($content !== FALSE) {
$this->writeFile($cacheFile, $content);
} else {
$cacheFile = '';
}
} elseif ($returnContent) {
$content = file_get_contents($cacheFile);
}
$returnValue = $cacheFile;
if ($returnContent) {
$returnValue = $content;
}
return $returnValue;
}
/**
* Writes $content to the file $file
*
* @param string $file file path to write to
* @param string $content Content to write
* @return boolean TRUE if the file was successfully opened and written to.
*/
protected function writeFile($file, $content) {
$result = GeneralUtility::writeFile($file, $content);
// hook here for other file system operations like syncing to other servers etc.
$hooks = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['scriptmerger']['writeFilePostHook'];
if (is_array($hooks)) {
foreach ($hooks as $classReference) {
$hookObject = GeneralUtility::getUserObj($classReference);
$hookObject->writeFilePostHook($file, $content, $this);
}
}
return $result;
}
}
?>

View File

@@ -0,0 +1,97 @@
<?php
namespace SGalinski\Scriptmerger;
/***************************************************************
* Copyright notice
*
* (c) Stefan Galinski <stefan@sgalinski.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* This class contains the basic stuff required for preserving conditional comments
*/
class ScriptmergerConditionalCommentPreserver {
/**
* holds the conditional comments
*
* @var array
*/
protected $conditionalComments = array();
/**
* Callback function to replace conditional comments with placeholders
*
* @param array $hits
* @return string
*/
protected function save($hits) {
$this->conditionalComments[] = $hits[0];
return '###conditionalComment' . (count($this->conditionalComments) - 1) . '###';
}
/**
* Callback function to restore placeholders for conditional comments
*
* @param array $hits
* @return string
*/
protected function restore($hits) {
$results = array();
preg_match('/\d+/is', $hits[0], $results);
$result = '';
if (count($results) > 0) {
$result = $this->conditionalComments[$results[0]];
}
return $result;
}
/**
* This method parses the output content and saves any found conditional comments
* into the "conditionalComments" class property. The output content is cleaned
* up of the found results.
*
* @return void
*/
public function read() {
$pattern = '/<!--\[if.+?<!\[endif\]-->/is';
$GLOBALS['TSFE']->content = preg_replace_callback(
$pattern,
array($this, 'save'),
$GLOBALS['TSFE']->content
);
}
/**
* This method writes the conditional comments back into the final output content.
*
* @return void
*/
public function writeBack() {
$pattern = '/###conditionalComment\d+###/is';
$GLOBALS['TSFE']->content = preg_replace_callback(
$pattern,
array($this, 'restore'),
$GLOBALS['TSFE']->content
);
}
}
?>

View File

@@ -0,0 +1,453 @@
<?php
namespace SGalinski\Scriptmerger;
/***************************************************************
* Copyright notice
*
* (c) Stefan Galinski <stefan@sgalinski.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
/**
* This class contains the parsing and replacing functionality for css files
*/
class ScriptmergerCss extends ScriptmergerBase {
/**
* holds the javascript code
*
* Structure:
* - $relation (rel attribute)
* - $media (media attribute)
* - $file
* |-content => string
* |-basename => string (base name of $file without file prefix)
* |-minify-ignore => bool
* |-merge-ignore => bool
*
* @var array
*/
protected $css = array();
/**
* Injects the extension configuration
*
* @param array $configuration
* @return void
*/
public function injectExtensionConfiguration(array $configuration) {
parent::injectExtensionConfiguration($configuration);
if (!class_exists('Minify_ImportProcessor', FALSE)) {
require_once(ExtensionManagementUtility::extPath('scriptmerger') . 'Resources/Minify/ImportProcessor.php');
}
\Minify_ImportProcessor::$extensionConfiguration = $this->configuration;
}
/**
* Controller for the css parsing and replacement
*
* @return void
*/
public function process() {
// fetch all remaining css contents
$this->getFiles();
// minify, compress and merging
foreach ($this->css as $relation => $cssByRelation) {
foreach ($cssByRelation as $media => $cssByMedia) {
$mergedContent = '';
$positionOfMergedFile = NULL;
foreach ($cssByMedia as $index => $cssProperties) {
$newFile = '';
// file should be minified
if ($this->configuration['css.']['minify.']['enable'] === '1' &&
!$cssProperties['minify-ignore']
) {
$newFile = $this->minifyFile($cssProperties);
}
// file should be merged
if ($this->configuration['css.']['merge.']['enable'] === '1' &&
!$cssProperties['merge-ignore']
) {
if ($positionOfMergedFile === NULL) {
$positionOfMergedFile = $cssProperties['position-key'];
}
$mergedContent .= $cssProperties['content'] . LF;
unset($this->css[$relation][$media][$index]);
continue;
}
// file should be compressed instead?
if ($this->configuration['css.']['compress.']['enable'] === '1' &&
function_exists('gzcompress') && !$cssProperties['compress-ignore']
) {
$newFile = $this->compressFile($cssProperties);
}
// minification or compression was used
if ($newFile !== '') {
$this->css[$relation][$media][$index]['file'] = $newFile;
$this->css[$relation][$media][$index]['content'] =
$cssProperties['content'];
$this->css[$relation][$media][$index]['basename'] =
$cssProperties['basename'];
}
}
// save merged content inside a new file
if ($this->configuration['css.']['merge.']['enable'] === '1' && $mergedContent !== '') {
if ($this->configuration['css.']['uniqueCharset.']['enable'] === '1') {
$mergedContent = $this->uniqueCharset($mergedContent);
}
// create property array
$properties = array(
'content' => $mergedContent,
'basename' => 'head-' . md5($mergedContent) . '.merged'
);
// write merged file in any case
$newFile = $this->tempDirectories['merged'] . $properties['basename'] . '.css';
if (!file_exists($newFile)) {
$this->writeFile($newFile, $properties['content']);
}
// file should be compressed
if ($this->configuration['css.']['compress.']['enable'] === '1' &&
function_exists('gzcompress')
) {
$newFile = $this->compressFile($properties);
}
// add new entry
$this->css[$relation][$media][] = array(
'file' => $newFile,
'content' => $properties['content'],
'basename' => $properties['basename'],
'position-key' => $positionOfMergedFile,
);
}
}
}
// write the conditional comments and possibly merged css files back to the document
$this->writeToDocument();
}
/**
* Some browser fail on parsing merged CSS files if multiple charset definitions are found.
* Therefor we replace all charset definition's with an empty string and add a single charset
* definition to the beginning of the content. At least Webkit engines fail badly.
*
* @param string $content
* @return string
*/
protected function uniqueCharset($content) {
if (!empty($this->configuration['css.']['uniqueCharset.']['value'])) {
$content = preg_replace('/@charset[^;]+;/', '', $content);
$content = $this->configuration['css.']['uniqueCharset.']['value'] . $content;
}
return $content;
}
/**
* This method parses the output content and saves any found css files or inline code
* into the "css" class property. The output content is cleaned up of the found results.
*
* @return void
*/
protected function getFiles() {
// filter pattern for the inDoc styles (fetches the content)
$filterInDocumentPattern = '/' .
'<style.*?>' . // This expression removes the opening style tag
'(?:.*?\/\*<!\[CDATA\[\*\/)?' . // and the optionally prefixed CDATA string.
'\s*(.*?)' . // We save the pure css content,
'(?:\s*\/\*\]\]>\*\/)?' . // remove the possible closing CDATA string
'\s*<\/style>' . // and closing style tag
'/is';
// parse all available css code inside link and style tags
$cssTags = array();
$pattern = '/' .
'<(link|sty)' . // Parse any link and style tags.
'(?=.+?(?:media="(.*?)"|>))' . // Fetch the media attribute
'(?=.+?(?:href="(.*?)"|>))' . // and the href attribute
'(?=.+?(?:rel="(.*?)"|>))' . // and the rel attribute
'(?=.+?(?:title="(.*?)"|>))' . // and the title attribute of the tag.
'(?=.+?(?:data-ignore="(.*?)"|>))' . // and the data-ignore attribute of the tag.
'(?:[^>]+?\.css[^>]+?\/?>' . // Continue parsing from \1 to the closing tag.
'|le[^>]*?>[^>]+?<\/style>)\s*' .
'/is';
preg_match_all($pattern, $GLOBALS['TSFE']->content, $cssTags);
$amountOfResults = count($cssTags[0]);
if (!$amountOfResults) {
return;
}
$function = create_function('', 'static $i = 0; return \'###MERGER\' . $i++ . \'MERGER###\';');
$GLOBALS['TSFE']->content = preg_replace_callback(
$pattern, $function, $GLOBALS['TSFE']->content, $amountOfResults
);
for ($i = 0; $i < $amountOfResults; ++$i) {
$content = '';
$media = (trim($cssTags[2][$i]) === '') ? 'all' : $cssTags[2][$i];
$media = implode(',', array_map('trim', explode(',', $media)));
$relation = (trim($cssTags[4][$i]) === '') ? 'stylesheet' : $cssTags[4][$i];
$source = $cssTags[3][$i];
$title = trim($cssTags[5][$i]);
$ignoreDataFlagSet = intval($cssTags[6][$i]);
// add basic entry
$this->css[$relation][$media][$i]['minify-ignore'] = FALSE;
$this->css[$relation][$media][$i]['compress-ignore'] = FALSE;
$this->css[$relation][$media][$i]['merge-ignore'] = FALSE;
$this->css[$relation][$media][$i]['addInDocument'] = FALSE;
$this->css[$relation][$media][$i]['useOriginalCodeLine'] = FALSE;
$this->css[$relation][$media][$i]['file'] = $source;
$this->css[$relation][$media][$i]['content'] = '';
$this->css[$relation][$media][$i]['basename'] = '';
$this->css[$relation][$media][$i]['title'] = $title;
$this->css[$relation][$media][$i]['position-key'] = $i;
$this->css[$relation][$media][$i]['original'] = $cssTags[0][$i];
// styles which are added inside the document must be parsed again to fetch the pure css code
$cssTags[1][$i] = (strtolower($cssTags[1][$i]) === 'sty' ? 'style' : strtolower($cssTags[1][$i]));
if ($cssTags[1][$i] === 'style') {
$cssContent = array();
preg_match_all($filterInDocumentPattern, $cssTags[0][$i], $cssContent);
// we doesn't need to continue if it was an empty style tag
if ($cssContent[1][0] === '') {
unset($this->css[$relation][$media][$i]);
continue;
}
// ignore this file if the content could not be fetched
$doNotRemoveInDoc = $this->configuration['css.']['doNotRemoveInDoc'] === '1';
if ($doNotRemoveInDoc || $ignoreDataFlagSet) {
$this->css[$relation][$media][$i]['minify-ignore'] = TRUE;
$this->css[$relation][$media][$i]['compress-ignore'] = TRUE;
$this->css[$relation][$media][$i]['merge-ignore'] = TRUE;
$this->css[$relation][$media][$i]['addInDocument'] = TRUE;
$this->css[$relation][$media][$i]['useOriginalCodeLine'] = TRUE;
$this->css[$relation][$media][$i]['content'] = $cssContent[1][0];
continue;
}
// save the content into a temporary file
$hash = md5($cssContent[1][0]);
$source = $this->tempDirectories['temp'] . 'inDocument-' . $hash;
$tempFile = $source . '.css';
if (!file_exists($source . '.css')) {
$this->writeFile($tempFile, $cssContent[1][0]);
}
// try to resolve any @import occurrences
/** @noinspection PhpUndefinedClassInspection */
$content = \Minify_ImportProcessor::process($tempFile);
$this->css[$relation][$media][$i]['file'] = $tempFile;
$this->css[$relation][$media][$i]['content'] = $content;
$this->css[$relation][$media][$i]['basename'] = basename($source);
} elseif ($source !== '') {
// try to fetch the content of the css file
$file = $source;
if ($GLOBALS['TSFE']->absRefPrefix !== '' && strpos($file, $GLOBALS['TSFE']->absRefPrefix) === 0) {
$file = substr($file, strlen($GLOBALS['TSFE']->absRefPrefix) - 1);
}
if (file_exists(PATH_site . $file)) {
$content = \Minify_ImportProcessor::process(PATH_site . $file);
} else {
$tempFile = $this->getExternalFile($source);
$content = \Minify_ImportProcessor::process($tempFile);
}
// ignore this file if the content could not be fetched
if ($content === '' || $ignoreDataFlagSet) {
$this->css[$relation][$media][$i]['minify-ignore'] = TRUE;
$this->css[$relation][$media][$i]['compress-ignore'] = TRUE;
$this->css[$relation][$media][$i]['merge-ignore'] = TRUE;
$this->css[$relation][$media][$i]['useOriginalCodeLine'] = TRUE;
continue;
}
// check if the file should be ignored for some processes
$ignoreAmount = 0;
if ($this->configuration['css.']['minify.']['ignore'] !== '') {
if (preg_match($this->configuration['css.']['minify.']['ignore'], $source)) {
$this->css[$relation][$media][$i]['minify-ignore'] = TRUE;
++$ignoreAmount;
}
}
if ($this->configuration['css.']['compress.']['ignore'] !== '') {
if (preg_match($this->configuration['css.']['compress.']['ignore'], $source)) {
$this->css[$relation][$media][$i]['compress-ignore'] = TRUE;
++$ignoreAmount;
}
}
if ($this->configuration['css.']['merge.']['ignore'] !== '') {
if (preg_match($this->configuration['css.']['merge.']['ignore'], $source)) {
$this->css[$relation][$media][$i]['merge-ignore'] = TRUE;
++$ignoreAmount;
}
}
if ($ignoreAmount === 3) {
$this->css[$relation][$media][$i]['useOriginalCodeLine'] = TRUE;
}
// set the css file with it's content
$this->css[$relation][$media][$i]['content'] = $content;
}
// get base name for later usage
// base name without file prefix and prefixed hash of the content
$filename = basename($source);
$hash = md5($content);
$this->css[$relation][$media][$i]['basename'] =
substr($filename, 0, strrpos($filename, '.')) . '-' . $hash;
}
}
/**
* This method minifies a css file. It's based upon the Minify_CSS class
* of the project minify.
*
* @param array $properties properties of an entry (copy-by-reference is used!)
* @return string new filename
*/
protected function minifyFile(&$properties) {
// get new filename
$newFile = $this->tempDirectories['minified'] .
$properties['basename'] . '.min.css';
// stop further processing if the file already exists
if (file_exists($newFile)) {
$properties['basename'] .= '.min';
$properties['content'] = file_get_contents($newFile);
return $newFile;
}
// minify content
if (!class_exists('Minify_CSS', FALSE)) {
require_once(ExtensionManagementUtility::extPath('scriptmerger') . 'Resources/Minify/CSS.php');
}
/** @noinspection PhpUndefinedClassInspection */
$properties['content'] = \Minify_CSS::minify($properties['content']);
// save content inside the new file
$this->writeFile($newFile, $properties['content']);
// save new part of the base name
$properties['basename'] .= '.min';
return $newFile;
}
/**
* This method compresses a css file.
*
* @param array $properties properties of an entry (copy-by-reference is used!)
* @return string new filename
*/
protected function compressFile(&$properties) {
$newFile = $this->tempDirectories['compressed'] . $properties['basename'] . '.gz.css';
if (file_exists($newFile)) {
return $newFile;
}
$this->writeFile($newFile, gzencode($properties['content'], 5));
return $newFile;
}
/**
* This method writes the css back to the document.
*
* @return void
*/
protected function writeToDocument() {
$pattern = '';
if (trim($this->configuration['css.']['mergedFilePosition']) !== '') {
$pattern = '/' . preg_quote($this->configuration['css.']['mergedFilePosition'], '/') . '/i';
}
$contentShouldBeAddedInline = $this->configuration['css.']['addContentInDocument'] === '1';
foreach ($this->css as $relation => $cssByRelation) {
$cssByRelation = array_reverse($cssByRelation);
foreach ($cssByRelation as $media => $cssByMedia) {
$cssByMedia = array_reverse($cssByMedia);
foreach ($cssByMedia as $cssProperties) {
if ($cssProperties['useOriginalCodeLine']) {
$content = $cssProperties['original'];
} elseif ($cssProperties['addInDocument'] || $contentShouldBeAddedInline) {
$content = LF . "\t" .
'<style media="' . $media . '" type="text/css">' . LF .
"\t" . '/* <![CDATA[ */' . LF .
"\t" . $cssProperties['content'] . LF .
"\t" . '/* ]]> */' . LF .
"\t" . '</style>' . LF;
} else {
$file = $cssProperties['file'];
if (file_exists($file)) {
$file = $GLOBALS['TSFE']->absRefPrefix .
(PATH_site === '/' ? $file : str_replace(PATH_site, '', $file));
}
$title = (trim($cssProperties['title']) !== '' ?
'title="' . $cssProperties['title'] . '"' : '');
$content = LF . "\t" . '<link rel="' . $relation . '" type="text/css" ' .
'media="' . $media . '" ' . $title . ' href="' . $file . '" />' . LF;
}
if ($pattern === '' || $cssProperties['merge-ignore']) {
$GLOBALS['TSFE']->content = str_replace(
'###MERGER' . $cssProperties['position-key'] . 'MERGER###',
$content,
$GLOBALS['TSFE']->content
);
continue;
} else {
$GLOBALS['TSFE']->content = preg_replace(
$pattern, $content . '\0', $GLOBALS['TSFE']->content, 1
);
}
}
}
}
// remove any empty markers
$pattern = '/###MERGER[0-9]*?MERGER###/is';
$GLOBALS['TSFE']->content = preg_replace($pattern, '', $GLOBALS['TSFE']->content);
}
}
?>

View File

@@ -0,0 +1,490 @@
<?php
namespace SGalinski\Scriptmerger;
/***************************************************************
* Copyright notice
*
* (c) Stefan Galinski <stefan@sgalinski.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use JShrink\Minifier;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This class contains the parsing and replacing functionality for javascript files
*/
class ScriptmergerJavascript extends ScriptmergerBase {
/**
* holds the javascript code
*
* Structure:
* - $file
* |-content => string
* |-basename => string (base name of $file without file prefix)
* |-minify-ignore => bool
* |-merge-ignore => bool
*
* @var array
*/
protected $javascript = array();
/**
* Controller for the processing of the javascript files.
*
* @return void
*/
public function process() {
// fetch all javascript content
$this->getFiles();
// minify, compress and merging
foreach ($this->javascript as $section => $javascriptBySection) {
$mergedContent = '';
$positionOfMergedFile = NULL;
foreach ($javascriptBySection as $index => $javascriptProperties) {
$newFile = '';
// file should be minified
if ($this->configuration['javascript.']['minify.']['enable'] === '1' &&
!$javascriptProperties['minify-ignore']
) {
$newFile = $this->minifyFile($javascriptProperties);
}
// file should be merged
if ($this->configuration['javascript.']['merge.']['enable'] === '1' &&
!$javascriptProperties['merge-ignore']
) {
if ($positionOfMergedFile === NULL) {
$positionOfMergedFile = $javascriptProperties['position-key'];
}
$mergedContent .= $javascriptProperties['content'] . LF;
unset($this->javascript[$section][$index]);
continue;
}
// file should be compressed instead?
if ($this->configuration['javascript.']['compress.']['enable'] === '1' &&
function_exists('gzcompress') && !$javascriptProperties['compress-ignore']
) {
$newFile = $this->compressFile($javascriptProperties);
}
// minification or compression was used
if ($newFile !== '') {
$this->javascript[$section][$index]['file'] = $newFile;
$this->javascript[$section][$index]['content'] =
$javascriptProperties['content'];
$this->javascript[$section][$index]['basename'] =
$javascriptProperties['basename'];
}
}
// save merged content inside a new file
if ($this->configuration['javascript.']['merge.']['enable'] === '1' && $mergedContent !== '') {
// create property array
$properties = array(
'content' => $mergedContent,
'basename' => $section . '-' . md5($mergedContent) . '.merged'
);
// write merged file in any case
$newFile = $this->tempDirectories['merged'] . $properties['basename'] . '.js';
if (!file_exists($newFile)) {
$this->writeFile($newFile, $properties['content']);
}
// file should be compressed
if ($this->configuration['javascript.']['compress.']['enable'] === '1' &&
function_exists('gzcompress')
) {
$newFile = $this->compressFile($properties);
}
// add new entry
$this->javascript[$section][] = array(
'file' => $newFile,
'content' => $properties['content'],
'basename' => $properties['basename'],
'position-key' => $positionOfMergedFile,
);
}
}
// write javascript content back to the document
$this->writeToDocument();
}
/**
* This method parses the output content and saves any found javascript files or inline code
* into the "javascript" class property. The output content is cleaned up of the found results.
*
* @return array js files
*/
protected function getFiles() {
// init
$javascriptTags = array(
'head' => array(),
'body' => array()
);
// create search pattern
$searchScriptsPattern = '/' .
'<script' . // This expression includes any script nodes.
'(?=.+?(?:src="(.*?)"|>))' . // It fetches the src attribute.
'(?=.+?(?:data-ignore="(.*?)"|>))' . // and the data-ignore attribute of the tag.
'[^>]*?>' . // Finally we finish the parsing of the opening tag
'.*?<\/script>\s*' . // until the closing tag.
'/is';
// filter pattern for the inDoc scripts (fetches the content)
$filterInDocumentPattern = '/' .
'<script.*?>' . // The expression removes the opening script tag
'(?:.*?\/\*<!\[CDATA\[\*\/)?' . // and the optionally prefixed CDATA string.
'(?:.*?<!--)?' . // senseless <!-- construct
'\s*(.*?)' . // We save the pure js content,
'(?:\s*\/\/\s*-->)?' . // senseless <!-- construct
'(?:\s*\/\*\]\]>\*\/)?' . // remove the possible closing CDATA string
'\s*<\/script>' . // and closing script tag
'/is';
// parse scripts in the head
$head = array();
preg_match('/<head>.+?<\/head>/is', $GLOBALS['TSFE']->content, $head);
$head = $oldHead = $head[0];
preg_match_all($searchScriptsPattern, $head, $javascriptTags['head']);
$amountOfScriptTags = count($javascriptTags['head'][0]);
if ($amountOfScriptTags) {
$function = create_function('', 'static $i = 0; return \'###MERGER-head\' . $i++ . \'MERGER###\';');
$head = preg_replace_callback($searchScriptsPattern, $function, $head, $amountOfScriptTags);
$GLOBALS['TSFE']->content = str_replace($oldHead, $head, $GLOBALS['TSFE']->content);
}
// parse scripts in the body
if ($this->configuration['javascript.']['parseBody'] === '1') {
$body = array();
preg_match('/<body.*>.+?<\/body>/is', $GLOBALS['TSFE']->content, $body);
$body = $oldBody = $body[0];
preg_match_all($searchScriptsPattern, $body, $javascriptTags['body']);
$amountOfScriptTags = count($javascriptTags['body'][0]);
if ($amountOfScriptTags) {
$function = create_function('', 'static $i = 0; return \'###MERGER-body\' . $i++ . \'MERGER###\';');
$body = preg_replace_callback($searchScriptsPattern, $function, $body, $amountOfScriptTags);
$GLOBALS['TSFE']->content = str_replace($oldBody, $body, $GLOBALS['TSFE']->content);
}
}
foreach ($javascriptTags as $section => $results) {
$amountOfResults = count($results[0]);
for ($i = 0; $i < $amountOfResults; ++$i) {
// get source attribute
$source = trim($results[1][$i]);
$isSourceFromMainAttribute = FALSE;
if ($source !== '') {
preg_match('/^<script([^>]*)>/', trim($results[0][$i]), $scriptAttribute);
$isSourceFromMainAttribute = (strpos($scriptAttribute[1], $source) !== FALSE);
}
$ignoreDataFlagSet = intval($results[2][$i]);
// add basic entry
$this->javascript[$section][$i]['minify-ignore'] = FALSE;
$this->javascript[$section][$i]['compress-ignore'] = FALSE;
$this->javascript[$section][$i]['merge-ignore'] = FALSE;
$this->javascript[$section][$i]['addInDocument'] = FALSE;
$this->javascript[$section][$i]['useOriginalCodeLine'] = FALSE;
$this->javascript[$section][$i]['file'] = $source;
$this->javascript[$section][$i]['content'] = '';
$this->javascript[$section][$i]['basename'] = '';
$this->javascript[$section][$i]['position-key'] = $i;
$this->javascript[$section][$i]['original'] = $results[0][$i];
if ($isSourceFromMainAttribute) {
// try to fetch the content of the css file
$file = $source;
if ($GLOBALS['TSFE']->absRefPrefix !== '' && strpos($file, $GLOBALS['TSFE']->absRefPrefix) === 0) {
$file = substr($file, strlen($GLOBALS['TSFE']->absRefPrefix) - 1);
}
$file = PATH_site . $file;
if (file_exists($file)) {
$content = file_get_contents($file);
} else {
$content = $this->getExternalFile($source, TRUE);
}
// ignore this file if the content could not be fetched
if (trim($content) === '' || $ignoreDataFlagSet) {
$this->javascript[$section][$i]['minify-ignore'] = TRUE;
$this->javascript[$section][$i]['compress-ignore'] = TRUE;
$this->javascript[$section][$i]['merge-ignore'] = TRUE;
$this->javascript[$section][$i]['useOriginalCodeLine'] = TRUE;
continue;
}
// check if the file should be ignored for some processes
$amountOfIgnores = 0;
if ($this->configuration['javascript.']['minify.']['ignore'] !== '' &&
preg_match($this->configuration['javascript.']['minify.']['ignore'], $source)
) {
$this->javascript[$section][$i]['minify-ignore'] = TRUE;
++$amountOfIgnores;
}
if ($this->configuration['javascript.']['compress.']['ignore'] !== '' &&
preg_match($this->configuration['javascript.']['compress.']['ignore'], $source)
) {
$this->javascript[$section][$i]['compress-ignore'] = TRUE;
++$amountOfIgnores;
}
if ($this->configuration['javascript.']['merge.']['ignore'] !== '' &&
preg_match($this->configuration['javascript.']['merge.']['ignore'], $source)
) {
$this->javascript[$section][$i]['merge-ignore'] = TRUE;
++$amountOfIgnores;
}
if ($amountOfIgnores === 3) {
$this->javascript[$section][$i]['useOriginalCodeLine'] = TRUE;
}
// set the javascript file with it's content
$this->javascript[$section][$i]['file'] = $source;
$this->javascript[$section][$i]['content'] = $content;
// get base name for later usage
// base name without file prefix and prefixed hash of the content
$filename = basename($source);
$hash = md5($content);
$this->javascript[$section][$i]['basename'] =
substr($filename, 0, strrpos($filename, '.')) . '-' . $hash;
} else {
// scripts which are added inside the document must be parsed again
// to fetch the pure js code
$javascriptContent = array();
preg_match_all($filterInDocumentPattern, $results[0][$i], $javascriptContent);
// we doesn't need to continue if it was an empty script tag
if ($javascriptContent[1][0] === '') {
unset($this->javascript[$section][$i]);
continue;
}
$doNotRemoveinDocInBody =
($this->configuration['javascript.']['doNotRemoveInDocInBody'] === '1' && $section === 'body');
$doNotRemoveinDocInHead =
($this->configuration['javascript.']['doNotRemoveInDocInHead'] === '1' && $section === 'head');
if ($doNotRemoveinDocInHead || $doNotRemoveinDocInBody || $ignoreDataFlagSet) {
$this->javascript[$section][$i]['minify-ignore'] = TRUE;
$this->javascript[$section][$i]['compress-ignore'] = TRUE;
$this->javascript[$section][$i]['merge-ignore'] = TRUE;
$this->javascript[$section][$i]['useOriginalCodeLine'] = TRUE;
$this->javascript[$section][$i]['addInDocument'] = TRUE;
$this->javascript[$section][$i]['content'] = $javascriptContent[1][0];
continue;
}
// save the content into a temporary file
$hash = md5($javascriptContent[1][0]);
$source = $this->tempDirectories['temp'] . 'inDocument-' . $hash;
if (!file_exists($source . '.js')) {
$this->writeFile($source . '.js', $javascriptContent[1][0]);
}
$this->javascript[$section][$i]['file'] = $source . '.js';
$this->javascript[$section][$i]['content'] = $javascriptContent[1][0];
$this->javascript[$section][$i]['basename'] = basename($source);
}
}
}
}
/**
* This method minifies a javascript file. It's based upon the JSMin+ class
* of the project minify. Alternatively the old JSMin class can be used, but it's
* definitely not the preferred solution!
*
* @param array $properties properties of an entry (copy-by-reference is used!)
* @return string new filename
*/
protected function minifyFile(&$properties) {
// stop further processing if the file already exists
$newFile = $this->tempDirectories['minified'] . $properties['basename'] . '.min.js';
if (file_exists($newFile)) {
$properties['basename'] .= '.min';
$properties['content'] = file_get_contents($newFile);
return $newFile;
}
// check for conditional compilation code to fix an issue with jsmin+
$hasConditionalCompilation = FALSE;
if ($this->configuration['javascript.']['minify.']['useJSMinPlus'] === '1') {
$hasConditionalCompilation = preg_match('/\/\*@cc_on/is', $properties['content']);
}
// minify content (the ending semicolon must be added to prevent minimisation bugs)
$hasErrors = FALSE;
$minifiedContent = '';
try {
if (!$hasConditionalCompilation && $this->configuration['javascript.']['minify.']['useJShrink'] === '1') {
if (!class_exists('JShrink\Minifier', FALSE)) {
require_once(ExtensionManagementUtility::extPath('scriptmerger') . 'Resources/JShrink/Minifier.php');
}
$minifiedContent = Minifier::minify($properties['content']);
} elseif (!$hasConditionalCompilation && $this->configuration['javascript.']['minify.']['useJSMinPlus'] === '1') {
if (!class_exists('JSMinPlus', FALSE)) {
require_once(ExtensionManagementUtility::extPath('scriptmerger') . 'Resources/jsminplus.php');
}
$minifiedContent = \JSMinPlus::minify($properties['content']);
} else {
if (!class_exists('JSMin', FALSE)) {
require_once(ExtensionManagementUtility::extPath('scriptmerger') . 'Resources/jsmin.php');
}
/** @noinspection PhpUndefinedClassInspection */
$minifiedContent = \JSMin::minify($properties['content']);
}
} catch (\Exception $exception) {
$hasErrors = TRUE;
}
// check if the minified content has more than two characters or more than 50 lines and no errors occurred
if (!$hasErrors && (strlen($minifiedContent) > 2 || count(explode(LF, $minifiedContent)) > 50)) {
$properties['content'] = $minifiedContent . ';';
} else {
$message = 'This javascript file could not be minified: "' . $properties['file'] . '"! ' .
'You should exclude it from the minification process!';
GeneralUtility::sysLog($message, 'scriptmerger', GeneralUtility::SYSLOG_SEVERITY_ERROR);
}
$this->writeFile($newFile, $properties['content']);
$properties['basename'] .= '.min';
return $newFile;
}
/**
* This method compresses a javascript file.
*
* @param array $properties properties of an entry (copy-by-reference is used!)
* @return string new filename
*/
protected function compressFile(&$properties) {
$newFile = $this->tempDirectories['compressed'] . $properties['basename'] . '.gz.js';
if (file_exists($newFile)) {
return $newFile;
}
$this->writeFile($newFile, gzencode($properties['content'], 5));
return $newFile;
}
/**
* This method writes the javascript back to the document.
*
* @return void
*/
protected function writeToDocument() {
$shouldBeAddedInDoc = $this->configuration['javascript.']['addContentInDocument'] === '1';
foreach ($this->javascript as $section => $javascriptBySection) {
ksort($javascriptBySection);
if (!is_array($javascriptBySection)) {
continue;
}
// addBeforeBody was deprecated in version 4.0.0 and can be removed later on
$pattern = '';
if ($section === 'body' || $this->configuration['javascript.']['addBeforeBody'] === '1') {
$pattern = '/' . preg_quote($this->configuration['javascript.']['mergedBodyFilePosition'], '/') . '/i';
} elseif (trim($this->configuration['javascript.']['mergedHeadFilePosition']) !== '') {
$pattern = '/' . preg_quote($this->configuration['javascript.']['mergedHeadFilePosition'], '/') . '/i';
}
foreach ($javascriptBySection as $javascriptProperties) {
if ($javascriptProperties['useOriginalCodeLine']) {
$content = $javascriptProperties['original'];
} elseif ($javascriptProperties['addInDocument'] || $shouldBeAddedInDoc) {
$content = "\t" .
'<script type="text/javascript">' . LF .
"\t" . '/* <![CDATA[ */' . LF .
"\t" . $javascriptProperties['content'] . LF .
"\t" . '/* ]]> */' . LF .
"\t" . '</script>' . LF;
} else {
$file = $javascriptProperties['file'];
if (file_exists($file)) {
$file = $GLOBALS['TSFE']->absRefPrefix .
(PATH_site === '/' ? $file : str_replace(PATH_site, '', $file));
}
if ($this->configuration['javascript.']['deferLoading'] === '1') {
$content = '
<script type="text/javascript" defer="defer">
function downloadJSAtOnload() {
var element = document.createElement("script");
element.src = "' . $file . '";
document.body.appendChild(element);
}
if (window.addEventListener) {
window.addEventListener("load", downloadJSAtOnload, false);
} else if (window.attachEvent) {
window.attachEvent("onload", downloadJSAtOnload);
} else {
window.onload = downloadJSAtOnload;
}
</script>';
} else {
$content = "\t" .
'<script type="text/javascript" src="' . $file . '"></script>' . LF;
}
}
if ($pattern === '' || $javascriptProperties['merge-ignore']) {
// add body scripts back to their original place if they were ignored
$GLOBALS['TSFE']->content = str_replace(
'###MERGER-' . $section . $javascriptProperties['position-key'] . 'MERGER###',
$content,
$GLOBALS['TSFE']->content
);
} else {
$GLOBALS['TSFE']->content = preg_replace($pattern, $content . '\0', $GLOBALS['TSFE']->content, 1);
}
}
}
// remove any empty markers
$pattern = '/###MERGER-(head|body)[0-9]*?MERGER###/is';
$GLOBALS['TSFE']->content = preg_replace($pattern, '', $GLOBALS['TSFE']->content);
}
}
?>

View File

@@ -0,0 +1,94 @@
<?php
namespace SGalinski\Scriptmerger;
/***************************************************************
* Copyright notice
*
* (c) Stefan Galinski (stefan@sgalinski.de)
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
* A copy is found in the textfile GPL.txt and important notices to the license
* from the author is found in LICENSE.txt distributed with these scripts.
*
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* This class contains a hook method for the "clear all cache" action in the TYPO3 backend.
*/
class user_ScriptmergerCacheHook {
/**
* Contains the temporary directories of this extension.
*
* @var array
*/
protected $tempDirectories = array();
/**
* Initializes some class variables...
*/
public function __construct() {
$this->tempDirectories = array(
PATH_site . 'typo3temp/scriptmerger/' => 0,
PATH_site . 'typo3temp/scriptmerger/temp/' => 0,
PATH_site . 'typo3temp/scriptmerger/uncompressed/' => 1209600,
PATH_site . 'typo3temp/scriptmerger/compressed/' => 1209600
);
}
/**
* Clear cache post processor
*
* This method deletes all temporary files that are older than one month and
* if the deletion of the whole cache is requested.
*
* @param array $params
* @return void
*/
public function clearCachePostProc(&$params) {
if ($params['cacheCmd'] !== 'all') {
return;
}
$now = $GLOBALS['EXEC_TIME'];
foreach ($this->tempDirectories as $tempDirectory => $maxAge) {
if (!is_dir($tempDirectory)) {
continue;
}
$handle = opendir($tempDirectory);
while (FALSE !== ($file = readdir($handle))) {
if ($file === '.' || $file === '..') {
continue;
}
if (is_file($tempDirectory . $file)) {
// get last modification time
$lastAccess = fileatime($tempDirectory . $file);
$age = $now - $lastAccess;
if ($age >= $maxAge) {
unlink($tempDirectory . $file);
}
}
}
}
}
}
?>

View File

@@ -0,0 +1,222 @@
<?php
namespace SGalinski\Scriptmerger;
/***************************************************************
* Copyright notice
*
* (c) Stefan Galinski <stefan@sgalinski.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* This class contains the output hooks that trigger the scriptmerger process
*/
class user_ScriptmergerOutputHook {
/**
* holds the extension configuration
*
* @var array
*/
protected $extensionConfiguration = array();
/**
* This method fetches and prepares the extension configuration.
*
* @return void
*/
protected function prepareExtensionConfiguration() {
$this->extensionConfiguration = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['scriptmerger']);
$tsSetup = $GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_scriptmerger.'];
if (is_array($tsSetup)) {
foreach ($tsSetup as $key => $value) {
$this->extensionConfiguration[$key] = $value;
}
}
// no compression allowed if content should be added inside the document
if ($this->extensionConfiguration['css.']['addContentInDocument'] === '1') {
$this->extensionConfiguration['css.']['compress.']['enable'] = '0';
}
if ($this->extensionConfiguration['javascript.']['addContentInDocument'] === '1') {
$this->extensionConfiguration['javascript.']['compress.']['enable'] = '0';
}
// prepare ignore expressions
if ($this->extensionConfiguration['css.']['minify.']['ignore'] !== '') {
$this->extensionConfiguration['css.']['minify.']['ignore'] = '/.*(' .
str_replace(',', '|', $this->extensionConfiguration['css.']['minify.']['ignore']) .
').*/isU';
}
if ($this->extensionConfiguration['css.']['compress.']['ignore'] !== '') {
$this->extensionConfiguration['css.']['compress.']['ignore'] = '/.*(' .
str_replace(',', '|', $this->extensionConfiguration['css.']['compress.']['ignore']) .
').*/isU';
}
if ($this->extensionConfiguration['css.']['merge.']['ignore'] !== '') {
$this->extensionConfiguration['css.']['merge.']['ignore'] = '/.*(' .
str_replace(',', '|', $this->extensionConfiguration['css.']['merge.']['ignore']) .
').*/isU';
}
if ($this->extensionConfiguration['javascript.']['minify.']['ignore'] !== '') {
$this->extensionConfiguration['javascript.']['minify.']['ignore'] = '/.*(' .
str_replace(',', '|', $this->extensionConfiguration['javascript.']['minify.']['ignore']) .
').*/isU';
}
if ($this->extensionConfiguration['javascript.']['compress.']['ignore'] !== '') {
$this->extensionConfiguration['javascript.']['compress.']['ignore'] = '/.*(' .
str_replace(',', '|', $this->extensionConfiguration['javascript.']['compress.']['ignore']) .
').*/isU';
}
if ($this->extensionConfiguration['javascript.']['merge.']['ignore'] !== '') {
$this->extensionConfiguration['javascript.']['merge.']['ignore'] = '/.*(' .
str_replace(',', '|', $this->extensionConfiguration['javascript.']['merge.']['ignore']) .
').*/isU';
}
}
/**
* This hook is executed if the page contains *_INT objects! It's called always as the
* last hook before the final output. This isn't the case if you are using a
* static file cache like nc_staticfilecache.
*
* @return bool
*/
public function contentPostProcOutput() {
/** @var $tsfe TypoScriptFrontendController */
$tsfe = $GLOBALS['TSFE'];
if (!$tsfe->isINTincScript() || intval(GeneralUtility::_GP('disableScriptmerger')) === 1) {
return TRUE;
}
$this->prepareExtensionConfiguration();
$this->process();
return TRUE;
}
/**
* The hook is only executed if the page does not contains any *_INT objects. It's called
* always if the page was not already cached or on first hit!
*
* @return bool
*/
public function contentPostProcAll() {
/** @var $tsfe TypoScriptFrontendController */
$tsfe = $GLOBALS['TSFE'];
if ($tsfe->isINTincScript() || intval(GeneralUtility::_GP('disableScriptmerger')) === 1) {
return TRUE;
}
$this->prepareExtensionConfiguration();
$this->process();
return TRUE;
}
/**
* Contains the process logic of the whole plugin!
*
* @return void
*/
protected function process() {
$javascriptEnabled = $this->extensionConfiguration['javascript.']['enable'] === '1';
$cssEnabled = $this->extensionConfiguration['css.']['enable'] === '1';
if ($cssEnabled || $javascriptEnabled) {
/** @var ScriptmergerConditionalCommentPreserver $conditionalCommentPreserver */
$conditionalCommentPreserver = GeneralUtility::makeInstance(
'SGalinski\Scriptmerger\ScriptmergerConditionalCommentPreserver'
);
$conditionalCommentPreserver->read();
if ($cssEnabled) {
/** @var ScriptmergerCss $cssProcessor */
$cssProcessor = GeneralUtility::makeInstance('SGalinski\Scriptmerger\ScriptmergerCss');
$cssProcessor->injectExtensionConfiguration($this->extensionConfiguration);
$cssProcessor->process();
}
if ($javascriptEnabled) {
/** @var ScriptmergerJavascript $javascriptProcessor */
$javascriptProcessor = GeneralUtility::makeInstance('SGalinski\Scriptmerger\ScriptmergerJavascript');
$javascriptProcessor->injectExtensionConfiguration($this->extensionConfiguration);
$javascriptProcessor->process();
}
$conditionalCommentPreserver->writeBack();
}
if (is_array($this->extensionConfiguration['urlRegularExpressions.']) &&
count($this->extensionConfiguration['urlRegularExpressions.'])
) {
$this->executeUserDefinedRegularExpressionsOnContent(
$this->extensionConfiguration['urlRegularExpressions.']
);
}
}
/**
* Executes user defined regular expressions on the href/src urls for e.g. use an cookie-less asset domain.
*
* @param array $expressions
* @return void
*/
protected function executeUserDefinedRegularExpressionsOnContent($expressions) {
foreach ($expressions as $index => $expression) {
if (strpos($index, '.') !== FALSE || !isset($expressions[$index . '.']['replacement'])) {
continue;
}
$replacement = trim($expressions[$index . '.']['replacement']);
if ($replacement === '') {
continue;
}
if ($expressions[$index . '.']['useWholeContent'] === '1') {
$GLOBALS['TSFE']->content = preg_replace($expression, $replacement, $GLOBALS['TSFE']->content);
} else {
$userExpression = trim(str_replace('/', '\/', $expression));
$expression = '/<(?:img|link|style|script|meta|input)' .
'(?=[^>]+?(?:content|href|src)="(' . $userExpression . ')")[^>]*?>/iU';
preg_match_all($expression, $GLOBALS['TSFE']->content, $matches);
if (is_array($matches[1])) {
foreach ($matches[1] as $match) {
if (trim($match) === '') {
continue;
}
$changedUrl = preg_replace('/' . $userExpression . '/is', $replacement, $match);
$GLOBALS['TSFE']->content = str_replace($match, $changedUrl, $GLOBALS['TSFE']->content);
}
}
}
}
}
}
?>

View File

@@ -0,0 +1,63 @@
plugin.tx_scriptmerger {
css {
enable = 1
addContentInDocument = 0
doNotRemoveInDoc = 0
mergedFilePosition =
minify {
enable = 1
ignore = \.min\.
}
compress {
enable = 1
ignore = \.gz\.
}
merge {
enable = 1
ignore =
}
uniqueCharset {
enable = 1
value = @charset "UTF-8";
}
postUrlProcessing {
pattern =
replacement =
}
}
javascript {
enable = 1
parseBody = 0
doNotRemoveInDocInBody = 1
doNotRemoveInDocInHead = 0
mergedHeadFilePosition =
mergedBodyFilePosition = </body>
addContentInDocument = 0
deferLoading = 0
minify {
enable = 1
useJSMinPlus = 1
useJShrink = 0
ignore = \?,\.min\.
}
compress {
enable = 1
ignore = \?,\.gz\.
}
merge {
enable = 1
ignore = \?
}
}
externalFileCacheLifetime = 3600
}

View File

@@ -0,0 +1,76 @@
config {
compressJs = 0
compressCss = 0
#concatenateJs = 0
#concatenateCss = 0
}
plugin.tx_scriptmerger {
css {
enable = {$plugin.tx_scriptmerger.css.enable}
addContentInDocument = {$plugin.tx_scriptmerger.css.addContentInDocument}
doNotRemoveInDoc = {$plugin.tx_scriptmerger.css.doNotRemoveInDoc}
mergedFilePosition = {$plugin.tx_scriptmerger.css.mergedFilePosition}
minify {
enable = {$plugin.tx_scriptmerger.css.minify.enable}
ignore = {$plugin.tx_scriptmerger.css.minify.ignore}
}
compress {
enable = {$plugin.tx_scriptmerger.css.compress.enable}
ignore = {$plugin.tx_scriptmerger.css.compress.ignore}
}
merge {
enable = {$plugin.tx_scriptmerger.css.merge.enable}
ignore = {$plugin.tx_scriptmerger.css.merge.ignore}
}
uniqueCharset {
enable = {$plugin.tx_scriptmerger.css.uniqueCharset.enable}
value = {$plugin.tx_scriptmerger.css.uniqueCharset.value}
}
postUrlProcessing {
pattern = {$plugin.tx_scriptmerger.css.postUrlProcessing.pattern}
replacement = {$plugin.tx_scriptmerger.css.postUrlProcessing.replacement}
}
}
javascript {
enable = {$plugin.tx_scriptmerger.javascript.enable}
parseBody = {$plugin.tx_scriptmerger.javascript.parseBody}
doNotRemoveInDocInBody = {$plugin.tx_scriptmerger.javascript.doNotRemoveInDocInBody}
doNotRemoveInDocInHead = {$plugin.tx_scriptmerger.javascript.doNotRemoveInDocInHead}
mergedHeadFilePosition = {$plugin.tx_scriptmerger.javascript.mergedHeadFilePosition}
mergedBodyFilePosition = {$plugin.tx_scriptmerger.javascript.mergedBodyFilePosition}
addContentInDocument = {$plugin.tx_scriptmerger.javascript.addContentInDocument}
deferLoading = {$plugin.tx_scriptmerger.javascript.deferLoading}
minify {
enable = {$plugin.tx_scriptmerger.javascript.minify.enable}
useJSMinPlus = {$plugin.tx_scriptmerger.javascript.minify.useJSMinPlus}
useJShrink = {$plugin.tx_scriptmerger.javascript.minify.useJShrink}
ignore = {$plugin.tx_scriptmerger.javascript.minify.ignore}
}
compress {
enable = {$plugin.tx_scriptmerger.javascript.compress.enable}
ignore = {$plugin.tx_scriptmerger.javascript.compress.ignore}
}
merge {
enable = {$plugin.tx_scriptmerger.javascript.merge.enable}
ignore = {$plugin.tx_scriptmerger.javascript.merge.ignore}
}
}
externalFileCacheLifetime = {$plugin.tx_scriptmerger.externalFileCacheLifetime}
urlRegularExpressions {
#pattern = http://domain.tld((filadmin|typo3temp/).+)
#pattern.replacement = http://assets.domain.tld/$1
#pattern.useWholeContent = 0
}
}

View File

@@ -0,0 +1,17 @@
.. ==================================================
.. FOR YOUR INFORMATION
.. --------------------------------------------------
.. -*- coding: utf-8 -*- with BOM.
Caching
-------
The extension adds a folder inside the typo3temp directory that contains several subfolders for the different
processes. If you are interested in the naming conventions of the files, you are encouraged to read the
source code. Each file inside this folder contains a hash which is created with the md5 algorithm
with the original file contents as a source. This is extremely useful, because we can detect changes to script
files or stylesheets by a simple comparison and automatically include the current version in the next rendering process.
The scriptmerger registers itself to a hook which is called after clearing of the caches. The registered class
simply removes any files inside the mentioned directories above which have an access date that is older than
two weeks.

View File

@@ -0,0 +1,302 @@
.. ==================================================
.. FOR YOUR INFORMATION
.. --------------------------------------------------
.. -*- coding: utf-8 -*- with BOM.
.. include:: ../../Includes.txt
Constants
=========
Properties
^^^^^^^^^^
===================================================== ========================================== ======================= =============================================
Property Data type :ref:`t3tsref:stdwrap` Default
===================================================== ========================================== ======================= =============================================
externalFileCacheLifetime_ :ref:`t3tsref:data-type-integer` no 3600
css.enable_ :ref:`t3tsref:data-type-boolean` no 1
css.addContentInDocument_ :ref:`t3tsref:data-type-boolean` no 0
css.doNotRemoveInDoc_ :ref:`t3tsref:data-type-boolean` no 0
css.mergedFilePosition_ :ref:`t3tsref:data-type-string` no *empty*
css.minify.enable_ :ref:`t3tsref:data-type-boolean` no 1
css.minify.ignore_ :ref:`t3tsref:data-type-string` no \.min\.
css.compress.enable_ :ref:`t3tsref:data-type-boolean` no 1
css.compress.ignore_ :ref:`t3tsref:data-type-string` no \.gz\.
css.merge.enable_ :ref:`t3tsref:data-type-boolean` no 1
css.merge.ignore_ :ref:`t3tsref:data-type-string` no *empty*
css.uniqueCharset.enable_ :ref:`t3tsref:data-type-boolean` no 1
css.uniqueCharset.value_ :ref:`t3tsref:data-type-string` no @charset "UTF-8";
css.postUrlProcessing.pattern_ :ref:`t3tsref:data-type-string` no *empty*
css.postUrlProcessing.replacement_ :ref:`t3tsref:data-type-string` no *empty*
javascript.enable_ :ref:`t3tsref:data-type-boolean` no 1
javascript.addContentInDocument_ :ref:`t3tsref:data-type-boolean` no 0
javascript.parseBody_ :ref:`t3tsref:data-type-boolean` no 0
javascript.doNotRemoveInDocInBody_ :ref:`t3tsref:data-type-boolean` no 1
javascript.doNotRemoveInDocInHead_ :ref:`t3tsref:data-type-boolean` no 0
javascript.mergedHeadFilePosition_ :ref:`t3tsref:data-type-string` no *empty*
javascript.mergedBodyFilePosition_ :ref:`t3tsref:data-type-string` no </body>
javascript.deferLoading_ :ref:`t3tsref:data-type-boolean` no 0
javascript.minify.enable_ :ref:`t3tsref:data-type-boolean` no 1
javascript.minify.ignore_ :ref:`t3tsref:data-type-string` no \?,\.min\.
javascript.minify.useJSMinPlus_ :ref:`t3tsref:data-type-boolean` no 1
javascript.minify.useJShrink_ :ref:`t3tsref:data-type-boolean` no 0
javascript.compress.enable_ :ref:`t3tsref:data-type-boolean` no 1
javascript.compress.ignore_ :ref:`t3tsref:data-type-string` no \?,\.gz\.
javascript.merge.enable_ :ref:`t3tsref:data-type-boolean` no 1
javascript.merge.ignore_ :ref:`t3tsref:data-type-string` no \?
===================================================== ========================================== ======================= =============================================
Property Details
^^^^^^^^^^^^^^^^
externalFileCacheLifetime
"""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.externalFileCacheLifetime =` :ref:`t3tsref:data-type-integer`
“Time to live” in seconds for the cache files of external CSS and JS
css.enable
""""""""""
:typoscript:`plugin.tx_scriptmerger.css.enable =` :ref:`t3tsref:data-type-boolean`
Enable all css processes
css.addContentInDocument
""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.addContentInDocument =` :ref:`t3tsref:data-type-boolean`
Embed the resulting css directly into the document in favor of a
linked resource (this automatically disables the compression step).
css.doNotRemoveInDoc
""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.doNotRemoveInDoc=` :ref:`t3tsref:data-type-boolean`
This option can be used to prevent embedded scripts to be merged, minified or compressed.
css.mergedFilePosition
""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.mergedFilePosition=` :ref:`t3tsref:data-type-string`
Use this option to define the final position of the merged files and any other ones that were processed by
the scriptmerger. The value is used inside a regular expression, but you cannot use any wildcards or such stuff.
A possible value could be "</head>". If empty, the position of the first merged file is reused.
css.minify.enable
"""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.minify.enable =` :ref:`t3tsref:data-type-boolean`
Enable the minification process
css.minify.ignore
"""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.minify.ignore =` :ref:`t3tsref:data-type-string`
A comma-separated list of files which should be ignored from the minification process.
Be careful, because you need to quote the characters yourself as the entries are considered as regular expressions.
If a file is added to all three ignore options, it's not touched at all.
css.compress.enable
"""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.compress.enable =` :ref:`t3tsref:data-type-boolean`
Enable the compression process
css.compress.ignore
"""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.compress.ignore =` :ref:`t3tsref:data-type-string`
A comma-separated list of files which should be ignored from the compression process.
Be careful, because you need to quote the characters yourself as the entries are considered as regular expressions.
If a file is added to all three ignore options, it's not touched at all.
css.merge.enable
""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.merge.enable =` :ref:`t3tsref:data-type-boolean`
Enable the merging process
css.merge.ignore
""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.merge.ignore =` :ref:`t3tsref:data-type-string`
A comma-separated list of files which should be ignored from the merging process.
Be careful, because you need to quote the characters yourself as the entries are considered as regular expressions.
If a file is added to all three ignore options, it's not touched at all. Also this setting will trigger the
process to readd the file at the same position it was taken from.
css.uniqueCharset.enable
""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.uniqueCharset.enable =` :ref:`t3tsref:data-type-boolean`
Enables the replacement of multiple @charset definitions by the given value option
css.uniqueCharset.value
"""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.uniqueCharset.value =` :ref:`t3tsref:data-type-string`
@charset definition that is added on the top of the merged css files
css.postUrlProcessing.pattern
"""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.postUrlProcessing.pattern =` :ref:`t3tsref:data-type-string`
Regular expression pattern (e.g. /(fileadmin)/i)
The pattern and replacement values can be used to fix broken urls inside the combined css file.
css.postUrlProcessing.replacement
"""""""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.css.postUrlProcessing.replacement =` :ref:`t3tsref:data-type-string`
Regular expression replacement (e.g. prefix/$i)
javascript.minify.enable
""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.minify.enable =` :ref:`t3tsref:data-type-boolean`
Enable the minification process
javascript.minify.ignore
""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.minify.ignore =` :ref:`t3tsref:data-type-string`
A comma-separated list of files which should be ignored from the minification process.
Be careful, because you need to quote the characters yourself as the entries are considered as regular expressions.
If a file is added to all three ignore options, it's not touched at all.
javascript.minify.useJSMinPlus
""""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.minify.useJSMinPlus =` :ref:`t3tsref:data-type-boolean`
Use JSMin+ instead of JSMin or JShrink.
javascript.minify.useJShrink
""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.minify.useJShrink =` :ref:`t3tsref:data-type-boolean`
Use JShrink instead of JSMin or JSMin+.
javascript.compress.enable
""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.compress.enable =` :ref:`t3tsref:data-type-boolean`
Enable the compression process.
javascript.compress.ignore
""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.compress.ignore =` :ref:`t3tsref:data-type-string`
A comma-separated list of files which should be ignored from the compression process.
Be careful, because you need to quote the characters yourself as the entries are considered as regular expressions.
If a file is added to all three ignore options, it's not touched at all.
javascript.merge.enable
"""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.merge.enable =` :ref:`t3tsref:data-type-boolean`
Enable the merging process
javascript.merge.ignore
"""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.merge.ignore =` :ref:`t3tsref:data-type-string`
A comma-separated list of files which should be ignored from the merging process.
Be careful, because you need to quote the characters yourself as the entries are considered as regular expressions.
If a file is added to all three ignore options, it's not touched at all. Also this setting will trigger the
process to readd the file at the same position it was taken from.
javascript.enable
"""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.enable=` :ref:`t3tsref:data-type-boolean`
Enable all javascript processes (by default only for the head section)
javascript.addContentInDocument
"""""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.addContentInDocument=` :ref:`t3tsref:data-type-boolean`
Embed the resulting javascript code directly into the document in favor of a
linked resource (this automatically disables the compression step).
javascript.parseBody
""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.parseBody=` :ref:`t3tsref:data-type-boolean`
Enable this option to enable the minification, processing and merging processes for the body section.
The resulting files are always included directly before the closing body tag.
javascript.doNotRemoveInDocInBody
"""""""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.doNotRemoveInDocInBody=` :ref:`t3tsref:data-type-boolean`
This option can be used to prevent embedded scripts inside the document of the body section to be merged, minified
or compressed as this is in many cases a possible error source in the final result. Therefore the option is
enabled by default.
javascript.doNotRemoveInDocInHead
"""""""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.doNotRemoveInDocInHead=` :ref:`t3tsref:data-type-boolean`
This option can be used to prevent embedded scripts inside the document of the head section to be merged,
minified or compressed.
javascript.mergedHeadFilePosition
"""""""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.mergedHeadFilePosition=` :ref:`t3tsref:data-type-string`
Use this option to define the final position of the merged files and any other ones in the head section that
were processed by the scriptmerger. The value is used inside a regular expression, but you cannot use any
wildcards or such stuff. A possible value could be "</head>". If empty, the position of the first merged file
is reused.
javascript.mergedBodyFilePosition
"""""""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.mergedBodyFilePosition=` :ref:`t3tsref:data-type-string`
Use this option to define the final position of the merged files and any other ones in the body section that
were processed by the scriptmerger. The value is used inside a regular expression, but you cannot use any
wildcards or such stuff.
javascript.deferLoading
"""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.javascript.deferLoading=` :ref:`t3tsref:data-type-boolean`
If you want to load your javascript always after the page onload event, then you are encouraged to activate this option.
Additional Information
^^^^^^^^^^^^^^^^^^^^^^
You can ignore any script or stylesheet by adding the data-ignore attribute with the value 1 to their tag.

View File

@@ -0,0 +1,40 @@
.. ==================================================
.. FOR YOUR INFORMATION
.. --------------------------------------------------
.. -*- coding: utf-8 -*- with BOM.
.. include:: ../../Includes.txt
Example
=======
The following configuration was used on one a production site. On this site all javascript and css files were
merged, compressed and minified. Furthermore I excluded any mootools scripts from being minified as they are
already provided as minified source. Also the deployJava script was ignored in the merging procecess, because this
script was added inside the body and the merging would cause an invalid loading order afterwards. On page
type 101,the scriptmerger plugin was completely disabled, because the type contains a template which was used by a
pdf generator extension that had several problems with the minified and merged scripts.
You see that it possible to get an advanced configuration with just some small changes to the configuration!
.. code-block:: typoscript
plugin.tx_scriptmerger {
javascript {
parseBody = 1
minify {
ignore = \?,\.min\.,mootools-
}
merge {
ignore = \?,deployJava
}
}
}
[globalVar = GP:type = 101]
plugin.tx_scriptmerger {
css.enable = 0
javascript.enable = 0
}
[global]

View File

@@ -0,0 +1,18 @@
.. ==================================================
.. FOR YOUR INFORMATION
.. --------------------------------------------------
.. -*- coding: utf-8 -*- with BOM.
Configuration
-------------
This chapter describes the TypoScript extension parameters.
.. toctree::
:maxdepth: 5
:titlesonly:
:glob:
Constants/Index
Setup/Index
Example/Index

View File

@@ -0,0 +1,53 @@
.. ==================================================
.. FOR YOUR INFORMATION
.. --------------------------------------------------
.. -*- coding: utf-8 -*- with BOM.
.. include:: ../../Includes.txt
Setup
=====
Properties
^^^^^^^^^^
===================================================== ========================================== ======================= =============================================
Property Data type :ref:`t3tsref:stdwrap` Default
===================================================== ========================================== ======================= =============================================
urlRegularExpressions.pattern1_ :ref:`t3tsref:data-type-string` no *empty*
urlRegularExpressions.pattern1.replacement_ :ref:`t3tsref:data-type-string` no *empty*
urlRegularExpressions.pattern1.useWholeContent_ :ref:`t3tsref:data-type-boolean` no 0
===================================================== ========================================== ======================= =============================================
Property Details
^^^^^^^^^^^^^^^^
urlRegularExpressions
"""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.urlRegularExpressions =` :ref:`t3tsref:data-type-array`
Custom regular expressions that process any links on the site. Can be used to change links to use a CDN or an
special cookie-free asset domain. It's possible to define multiple expressions.
urlRegularExpressions.pattern1
""""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.urlRegularExpressions.pattern1 =` :ref:`t3tsref:data-type-string`
Regular expression (e.g. http://domain.tld((filadmin\|typo3temp/).+))
urlRegularExpressions.pattern1.replacement
""""""""""""""""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.urlRegularExpressions.pattern1.replacement =` :ref:`t3tsref:data-type-string`
Replacement expression (e.g. http://assets.domain.tld/$1)
urlRegularExpressions.pattern1.useWholeContent
""""""""""""""""""""""""""""""""""""""""""""""
:typoscript:`plugin.tx_scriptmerger.urlRegularExpressions.pattern1.useWholeContent =` :ref:`t3tsref:data-type-boolean`
Use the whole page content as the source for the regular expression instead of only URLs. In this case you must
handle the quoting and modifier stuff yourself (e.g. /http:\/\/de\./is)

View File

@@ -0,0 +1,21 @@
.. ==================================================
.. FOR YOUR INFORMATION
.. --------------------------------------------------
.. -*- coding: utf-8 -*- with BOM.
.. This is 'Includes.txt'. It is included at the very top of each and
every ReST source file in this documentation project (= manual).
.. ==================================================
.. DEFINE SOME TEXT ROLES
.. --------------------------------------------------
.. role:: typoscript(code)
.. role:: ts(typoscript)
:class: typoscript
.. role:: php(code)
.. highlight:: guess

View File

@@ -0,0 +1,61 @@
.. ==================================================
.. FOR YOUR INFORMATION
.. --------------------------------------------------
.. -*- coding: utf-8 -*- with BOM.
.. _start:
================
EXT:scriptmerger
================
.. only:: html
:Classification:
scriptmerger
:Author:
Stefan Galinski
:Version:
|release|
:Language:
en
:Description:
This extension minimizes the http requests by concatenating your css and javascript.
Furthermore the result can be minified and compressed. This whole process is highly configurable
and is partly based on the "minify", "jsminplus", "jshrink" and "jsmin" projects.
:Keywords:
performance, compression, merging, javascript, stylesheets
:Author:
Stefan Galinski
:Email:
stefan@sgalinski.de
:License:
This document is published under the Open Content License
available from http://www.opencontent.org/opl.shtml
:Rendered:
|today|
The content of this document is related to TYPO3,
a GNU/GPL CMS/Framework available from `www.typo3.org <http://www.typo3.org/>`_.
**Table of Contents**
.. toctree::
:maxdepth: 2
:titlesonly:
:glob:
Introduction/Index
Installation/Index
Configuration/Index
Caching/Index
KnownProblemsAndHints/Index

View File

@@ -0,0 +1,15 @@
.. ==================================================
.. FOR YOUR INFORMATION
.. --------------------------------------------------
.. -*- coding: utf-8 -*- with BOM.
Installation
------------
The installation process of the extension isn't that simple, but don't give up too fast. First you need
to download and install the extension with the extension manager and include the static extension template
into your root page like you possibly did many times before for other extensions. As an additional step it's
required to include the contents of the example .htaccess file in the extension directory in your own site-wide
.htaccess - in most cases directly after the RewriteBase setting. Now you can finally test the output of your page
and tweak the scriptmerger configuration as it's possible that you will experience javascript errors or other
issues, because of a wrong order of the executed files.

View File

@@ -0,0 +1,22 @@
.. ==================================================
.. FOR YOUR INFORMATION
.. --------------------------------------------------
.. -*- coding: utf-8 -*- with BOM.
Introduction
------------
This extension provides a significant speedup of your pages. The
speedup depends on the amount of css and javascript files that you
are using on your site. The performance advantage is gained by
concatenating, minimizing and compressing of all css and javascript
files. Furthermore the extension supports different media types and
relations (rel attribute) for css scripts and different output formats
of the result files. Currently you can write them back into the
document as links to external files, as embedded content or deferred scripts (js only).
The minification process and @import rule replacement logic is based
upon the extension „Minify“ that you can find at `Google Code <http://code.google.com/p/minify/>`_.
Furthermore `JSMinPlus <http://crisp.tweakblogs.net/blog/1665/a-new-javascript-minifier-jsmin+.html>`_
and `JShrink <https://github.com/tedivm/JShrink>`_ are used as better alternatives for JSMin in the javascript
minification process. You can still switch to JSMin if you experience trouble.

View File

@@ -0,0 +1,46 @@
.. ==================================================
.. FOR YOUR INFORMATION
.. --------------------------------------------------
.. -*- coding: utf-8 -*- with BOM.
Known Problems And Hints
------------------------
- It's known that the used @import replacement utility from the „Minify“
project doesn't resolves URL's. It's not a real problem, because such
@import rules will simply not be replaced.
- The defer, async and rev attributes are completely ignored.
- Embedded css and javascript in the document is automatically added to an external, cachable file. If you want
to prevent this behaviour, just ignore the tags with the "data-ignore" attribute set to "1".
- If you experience problems with the compression, make sure you have
mod\_headers installed and activated. You can do this in e.g. ubuntu
with a simple „sudo a2enmod headers && sudo service apache2 restart“ command.
- You must have mod\_expires installed to use the expire dates feature in the .htaccess for the
images, css and javascript files. You can activate the module in e.g. ubuntu by this command:
„sudo a2enmod expires && sudo service apache2 restart“
- If you have some missing images after the merging process, you can use the "postUrlProcessing" option
to manually fix them. See the configuration chapter for additional information.
- Often problems are caused by an incorrect ordering after the concatenation process if
some files are ignored. You can fix that by defining a better position for your merged files. See the
configuration chapter for more information.
- You can disable the scriptmerger for the request by an URL parameter. Just add the following to your query string:
“?no\_cache=1&disableScriptmerger=1”.
- If you want to post-process the written files, because you want to push them to a CDN or something like that, you
can use the proviced internal hook. Just register some class in your ext_localconf.php into this array
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['scriptmerger']['writeFilePostHook'] and add the method "writeFilePostHook"
inside this class with the required logic.
- You can ignore any scripts and stylesheets by adding the data-ignore attribute with the value 1. This tags are
not touched in any way by the scriptmerger.
Your problem isn't listed here and you still experience issues with your minified, compressed and/or concatenated
contents? Then please report this at the project tracker
on `forge <http://forge.typo3.org/projects/extension-scriptmerger/issues>`_.

View File

@@ -0,0 +1,470 @@
<?php
/**
* JShrink
*
* Copyright (c) 2009-2012, Robert Hafner <tedivm@tedivm.com>.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Robert Hafner nor the names of his
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package JShrink
* @author Robert Hafner <tedivm@tedivm.com>
* @copyright 2009-2012 Robert Hafner <tedivm@tedivm.com>
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
* @link https://github.com/tedivm/JShrink
* @version Release: 0.5.1
*/
namespace JShrink;
/**
* Minifier
*
* Usage - Minifier::minify($js);
* Usage - Minifier::minify($js, $options);
* Usage - Minifier::minify($js, array('flaggedComments' => false));
*
* @package JShrink
* @author Robert Hafner <tedivm@tedivm.com>
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
*/
class Minifier
{
/**
* The input javascript to be minified.
*
* @var string
*/
protected $input;
/**
* The location of the character (in the input string) that is next to be
* processed.
*
* @var int
*/
protected $index = 0;
/**
* The first of the characters currently being looked at.
*
* @var string
*/
protected $a = '';
/**
* The next character being looked at (after a);
*
* @var string
*/
protected $b = '';
/**
* This character is only active when certain look ahead actions take place.
*
* @var string
*/
protected $c;
/**
* Contains the options for the current minification process.
*
* @var array
*/
protected $options;
/**
* Contains the default options for minification. This array is merged with
* the one passed in by the user to create the request specific set of
* options (stored in the $options attribute).
*
* @var array
*/
static protected $defaultOptions = array('flaggedComments' => true);
/**
* Contains a copy of the JShrink object used to run minification. This is
* only used internally, and is only stored for performance reasons. There
* is no internal data shared between minification requests.
*/
static protected $jshrink;
/**
* Minifier::minify takes a string containing javascript and removes
* unneeded characters in order to shrink the code without altering it's
* functionality.
*/
static public function minify($js, $options = array())
{
try{
ob_start();
$currentOptions = array_merge(self::$defaultOptions, $options);
if(!isset(self::$jshrink))
self::$jshrink = new Minifier();
self::$jshrink->breakdownScript($js, $currentOptions);
return ob_get_clean();
}catch(Exception $e){
if(isset(self::$jshrink))
self::$jshrink->clean();
ob_end_clean();
throw $e;
}
}
/**
* Processes a javascript string and outputs only the required characters,
* stripping out all unneeded characters.
*
* @param string $js The raw javascript to be minified
* @param array $currentOptions Various runtime options in an associative array
*/
protected function breakdownScript($js, $currentOptions)
{
// reset work attributes in case this isn't the first run.
$this->clean();
$this->options = $currentOptions;
$js = str_replace("\r\n", "\n", $js);
$this->input = str_replace("\r", "\n", $js);
$this->a = $this->getReal();
// the only time the length can be higher than 1 is if a conditional
// comment needs to be displayed and the only time that can happen for
// $a is on the very first run
while(strlen($this->a) > 1)
{
echo $this->a;
$this->a = $this->getReal();
}
$this->b = $this->getReal();
while($this->a !== false && !is_null($this->a) && $this->a !== '')
{
// now we give $b the same check for conditional comments we gave $a
// before we began looping
if(strlen($this->b) > 1)
{
echo $this->a . $this->b;
$this->a = $this->getReal();
$this->b = $this->getReal();
continue;
}
switch($this->a)
{
// new lines
case "\n":
// if the next line is something that can't stand alone
// preserve the newline
if(strpos('(-+{[@', $this->b) !== false)
{
echo $this->a;
$this->saveString();
break;
}
// if its a space we move down to the string test below
if($this->b === ' ')
break;
// otherwise we treat the newline like a space
case ' ':
if(self::isAlphaNumeric($this->b))
echo $this->a;
$this->saveString();
break;
default:
switch($this->b)
{
case "\n":
if(strpos('}])+-"\'', $this->a) !== false)
{
echo $this->a;
$this->saveString();
break;
}else{
if(self::isAlphaNumeric($this->a))
{
echo $this->a;
$this->saveString();
}
}
break;
case ' ':
if(!self::isAlphaNumeric($this->a))
break;
default:
// check for some regex that breaks stuff
if($this->a == '/' && ($this->b == '\'' || $this->b == '"'))
{
$this->saveRegex();
continue;
}
echo $this->a;
$this->saveString();
break;
}
}
// do reg check of doom
$this->b = $this->getReal();
if(($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false))
$this->saveRegex();
}
$this->clean();
}
/**
* Returns the next string for processing based off of the current index.
*
* @return string
*/
protected function getChar()
{
if(isset($this->c))
{
$char = $this->c;
unset($this->c);
}else{
$tchar = substr($this->input, $this->index, 1);
if(isset($tchar) && $tchar !== false)
{
$char = $tchar;
$this->index++;
}else{
return false;
}
}
if($char !== "\n" && ord($char) < 32)
return ' ';
return $char;
}
/**
* This function gets the next "real" character. It is essentially a wrapper
* around the getChar function that skips comments. This has significant
* performance benefits as the skipping is done using native functions (ie,
* c code) rather than in script php.
*
* @return string Next 'real' character to be processed.
*/
protected function getReal()
{
$startIndex = $this->index;
$char = $this->getChar();
if($char == '/')
{
$this->c = $this->getChar();
if($this->c == '/')
{
$thirdCommentString = substr($this->input, $this->index, 1);
// kill rest of line
$char = $this->getNext("\n");
if($thirdCommentString == '@')
{
$endPoint = ($this->index) - $startIndex;
unset($this->c);
$char = "\n" . substr($this->input, $startIndex, $endPoint);
}else{
$char = $this->getChar();
$char = $this->getChar();
}
}elseif($this->c == '*'){
$this->getChar(); // current C
$thirdCommentString = $this->getChar();
if($thirdCommentString == '@')
{
// conditional comment
// we're gonna back up a bit and and send the comment back,
// where the first char will be echoed and the rest will be
// treated like a string
$this->index = $this->index-2;
return '/';
}elseif($this->getNext('*/')){
// kill everything up to the next */
$this->getChar(); // get *
$this->getChar(); // get /
$char = $this->getChar(); // get next real character
// if YUI-style comments are enabled we reinsert it into the stream
if($this->options['flaggedComments'] && $thirdCommentString == '!')
{
$endPoint = ($this->index - 1) - $startIndex;
echo "\n" . substr($this->input, $startIndex, $endPoint) . "\n";
}
}else{
$char = false;
}
if($char === false)
throw new \RuntimeException('Stray comment. ' . $this->index);
// if we're here c is part of the comment and therefore tossed
if(isset($this->c))
unset($this->c);
}
}
return $char;
}
/**
* Pushes the index ahead to the next instance of the supplied string. If it
* is found the first character of the string is returned.
*
* @return string|false Returns the first character of the string or false.
*/
protected function getNext($string)
{
$pos = strpos($this->input, $string, $this->index);
if($pos === false)
return false;
$this->index = $pos;
return substr($this->input, $this->index, 1);
}
/**
* When a javascript string is detected this function crawls for the end of
* it and saves the whole string.
*
*/
protected function saveString()
{
$this->a = $this->b;
if($this->a == "'" || $this->a == '"') // is the character a quote
{
// save literal string
$stringType = $this->a;
while(1)
{
echo $this->a;
$this->a = $this->getChar();
switch($this->a)
{
case $stringType:
break 2;
case "\n":
throw new \RuntimeException('Unclosed string. ' . $this->index);
break;
case '\\':
echo $this->a;
$this->a = $this->getChar();
}
}
}
}
/**
* When a regular expression is detected this funcion crawls for the end of
* it and saves the whole regex.
*/
protected function saveRegex()
{
echo $this->a . $this->b;
while(($this->a = $this->getChar()) !== false)
{
if($this->a == '/')
break;
if($this->a == '\\')
{
echo $this->a;
$this->a = $this->getChar();
}
if($this->a == "\n")
throw new \RuntimeException('Stray regex pattern. ' . $this->index);
echo $this->a;
}
$this->b = $this->getReal();
}
/**
* Resets attributes that do not need to be stored between requests so that
* the next request is ready to go.
*/
protected function clean()
{
unset($this->input);
$this->index = 0;
$this->a = $this->b = '';
unset($this->c);
unset($this->options);
}
/**
* Checks to see if a character is alphanumeric.
*
* @return bool
*/
static protected function isAlphaNumeric($char)
{
return preg_match('/^[\w\$]$/', $char) === 1 || $char == '/';
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* Class Minify_CSS
* @package Minify
*/
/**
* Minify CSS
*
* This class uses Minify_CSS_Compressor and Minify_CSS_UriRewriter to
* minify CSS and rewrite relative URIs.
*
* @package Minify
* @author Stephen Clay <steve@mrclay.org>
* @author http://code.google.com/u/1stvamp/ (Issue 64 patch)
*/
class Minify_CSS {
/**
* Minify a CSS string
*
* @param string $css
*
* @param array $options available options:
*
* 'preserveComments': (default true) multi-line comments that begin
* with "/*!" will be preserved with newlines before and after to
* enhance readability.
*
* 'removeCharsets': (default true) remove all @charset at-rules
*
* 'prependRelativePath': (default null) if given, this string will be
* prepended to all relative URIs in import/url declarations
*
* 'currentDir': (default null) if given, this is assumed to be the
* directory of the current CSS file. Using this, minify will rewrite
* all relative URIs in import/url declarations to correctly point to
* the desired files. For this to work, the files *must* exist and be
* visible by the PHP process.
*
* 'symlinks': (default = array()) If the CSS file is stored in
* a symlink-ed directory, provide an array of link paths to
* target paths, where the link paths are within the document root. Because
* paths need to be normalized for this to work, use "//" to substitute
* the doc root in the link paths (the array keys). E.g.:
* <code>
* array('//symlink' => '/real/target/path') // unix
* array('//static' => 'D:\\staticStorage') // Windows
* </code>
*
* 'docRoot': (default = $_SERVER['DOCUMENT_ROOT'])
* see Minify_CSS_UriRewriter::rewrite
*
* @return string
*/
public static function minify($css, $options = array())
{
$options = array_merge(array(
'removeCharsets' => true,
'preserveComments' => true,
'currentDir' => null,
'docRoot' => $_SERVER['DOCUMENT_ROOT'],
'prependRelativePath' => null,
'symlinks' => array(),
), $options);
if ($options['removeCharsets']) {
$css = preg_replace('/@charset[^;]+;\\s*/', '', $css);
}
require_once 'CSS/Compressor.php';
if (! $options['preserveComments']) {
$css = Minify_CSS_Compressor::process($css, $options);
} else {
require_once 'CommentPreserver.php';
$css = Minify_CommentPreserver::process(
$css
,array('Minify_CSS_Compressor', 'process')
,array($options)
);
}
if (! $options['currentDir'] && ! $options['prependRelativePath']) {
return $css;
}
require_once 'CSS/UriRewriter.php';
if ($options['currentDir']) {
return Minify_CSS_UriRewriter::rewrite(
$css
,$options['currentDir']
,$options['docRoot']
,$options['symlinks']
);
} else {
return Minify_CSS_UriRewriter::prepend(
$css
,$options['prependRelativePath']
);
}
}
}

View File

@@ -0,0 +1,249 @@
<?php
/**
* Class Minify_CSS_Compressor
* @package Minify
*/
/**
* Compress CSS
*
* This is a heavy regex-based removal of whitespace, unnecessary
* comments and tokens, and some CSS value minimization, where practical.
* Many steps have been taken to avoid breaking comment-based hacks,
* including the ie5/mac filter (and its inversion), but expect tricky
* hacks involving comment tokens in 'content' value strings to break
* minimization badly. A test suite is available.
*
* @package Minify
* @author Stephen Clay <steve@mrclay.org>
* @author http://code.google.com/u/1stvamp/ (Issue 64 patch)
*/
class Minify_CSS_Compressor {
/**
* Minify a CSS string
*
* @param string $css
*
* @param array $options (currently ignored)
*
* @return string
*/
public static function process($css, $options = array())
{
$obj = new Minify_CSS_Compressor($options);
return $obj->_process($css);
}
/**
* @var array options
*/
protected $_options = null;
/**
* @var bool Are we "in" a hack?
*
* I.e. are some browsers targeted until the next comment?
*/
protected $_inHack = false;
/**
* Constructor
*
* @param array $options (currently ignored)
*/
private function __construct($options) {
$this->_options = $options;
}
/**
* Minify a CSS string
*
* @param string $css
*
* @return string
*/
protected function _process($css)
{
$css = str_replace("\r\n", "\n", $css);
// preserve empty comment after '>'
// http://www.webdevout.net/css-hacks#in_css-selectors
$css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
// preserve empty comment between property and value
// http://css-discuss.incutio.com/?page=BoxModelHack
$css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
$css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);
// apply callback to all valid comments (and strip out surrounding ws
$css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@'
,array($this, '_commentCB'), $css);
// remove ws around { } and last semicolon in declaration block
$css = preg_replace('/\\s*{\\s*/', '{', $css);
$css = preg_replace('/;?\\s*}\\s*/', '}', $css);
// remove ws surrounding semicolons
$css = preg_replace('/\\s*;\\s*/', ';', $css);
// remove ws around urls
$css = preg_replace('/
url\\( # url(
\\s*
([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis)
\\s*
\\) # )
/x', 'url($1)', $css);
// remove ws between rules and colons
$css = preg_replace('/
\\s*
([{;]) # 1 = beginning of block or rule separator
\\s*
([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter)
\\s*
:
\\s*
(\\b|[#\'"-]) # 3 = first character of a value
/x', '$1$2:$3', $css);
// remove ws in selectors
$css = preg_replace_callback('/
(?: # non-capture
\\s*
[^~>+,\\s]+ # selector part
\\s*
[,>+~] # combinators
)+
\\s*
[^~>+,\\s]+ # selector part
{ # open declaration block
/x'
,array($this, '_selectorsCB'), $css);
// minimize hex colors
$css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i'
, '$1#$2$3$4$5', $css);
// remove spaces between font families
$css = preg_replace_callback('/font-family:([^;}]+)([;}])/'
,array($this, '_fontFamilyCB'), $css);
$css = preg_replace('/@import\\s+url/', '@import url', $css);
// replace any ws involving newlines with a single newline
$css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);
// separate common descendent selectors w/ newlines (to limit line lengths)
$css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);
// Use newline after 1st numeric value (to limit line lengths).
$css = preg_replace('/
((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value
\\s+
/x'
,"$1\n", $css);
// prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/
$css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);
return trim($css);
}
/**
* Replace what looks like a set of selectors
*
* @param array $m regex matches
*
* @return string
*/
protected function _selectorsCB($m)
{
// remove ws around the combinators
return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
}
/**
* Process a comment and return a replacement
*
* @param array $m regex matches
*
* @return string
*/
protected function _commentCB($m)
{
$hasSurroundingWs = (trim($m[0]) !== $m[1]);
$m = $m[1];
// $m is the comment content w/o the surrounding tokens,
// but the return value will replace the entire comment.
if ($m === 'keep') {
return '/**/';
}
if ($m === '" "') {
// component of http://tantek.com/CSS/Examples/midpass.html
return '/*" "*/';
}
if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {
// component of http://tantek.com/CSS/Examples/midpass.html
return '/*";}}/* */';
}
if ($this->_inHack) {
// inversion: feeding only to one browser
if (preg_match('@
^/ # comment started like /*/
\\s*
(\\S[\\s\\S]+?) # has at least some non-ws content
\\s*
/\\* # ends like /*/ or /**/
@x', $m, $n)) {
// end hack mode after this comment, but preserve the hack and comment content
$this->_inHack = false;
return "/*/{$n[1]}/**/";
}
}
if (substr($m, -1) === '\\') { // comment ends like \*/
// begin hack mode and preserve hack
$this->_inHack = true;
return '/*\\*/';
}
if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */
// begin hack mode and preserve hack
$this->_inHack = true;
return '/*/*/';
}
if ($this->_inHack) {
// a regular comment ends hack mode but should be preserved
$this->_inHack = false;
return '/**/';
}
// Issue 107: if there's any surrounding whitespace, it may be important, so
// replace the comment with a single space
return $hasSurroundingWs // remove all other comments
? ' '
: '';
}
/**
* Process a font-family listing and return a replacement
*
* @param array $m regex matches
*
* @return string
*/
protected function _fontFamilyCB($m)
{
// Issue 210: must not eliminate WS between words in unquoted families
$pieces = preg_split('/(\'[^\']+\'|"[^"]+")/', $m[1], null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$out = 'font-family:';
while (null !== ($piece = array_shift($pieces))) {
if ($piece[0] !== '"' && $piece[0] !== "'") {
$piece = preg_replace('/\\s+/', ' ', $piece);
$piece = preg_replace('/\\s?,\\s?/', ',', $piece);
}
$out .= $piece;
}
return $out . $m[2];
}
}

View File

@@ -0,0 +1,290 @@
<?php
/**
* Class Minify_CSS_UriRewriter
* @package Minify
*/
/**
* Rewrite file-relative URIs as root-relative in CSS files
*
* @package Minify
* @author Stephen Clay <steve@mrclay.org>
*/
class Minify_CSS_UriRewriter {
/**
* rewrite() and rewriteRelative() append debugging information here
* @var string
*/
public static $debugText = '';
/**
* In CSS content, rewrite file relative URIs as root relative
*
* @param string $css
*
* @param string $currentDir The directory of the current CSS file.
*
* @param string $docRoot The document root of the web site in which
* the CSS file resides (default = $_SERVER['DOCUMENT_ROOT']).
*
* @param array $symlinks (default = array()) If the CSS file is stored in
* a symlink-ed directory, provide an array of link paths to
* target paths, where the link paths are within the document root. Because
* paths need to be normalized for this to work, use "//" to substitute
* the doc root in the link paths (the array keys). E.g.:
* <code>
* array('//symlink' => '/real/target/path') // unix
* array('//static' => 'D:\\staticStorage') // Windows
* </code>
*
* @return string
*/
public static function rewrite($css, $currentDir, $docRoot = null, $symlinks = array())
{
self::$_docRoot = self::_realpath(
$docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT']
);
self::$_currentDir = self::_realpath($currentDir);
self::$_symlinks = array();
// normalize symlinks
foreach ($symlinks as $link => $target) {
$link = ($link === '//')
? self::$_docRoot
: str_replace('//', self::$_docRoot . '/', $link);
$link = strtr($link, '/', DIRECTORY_SEPARATOR);
self::$_symlinks[$link] = self::_realpath($target);
}
self::$debugText .= "docRoot : " . self::$_docRoot . "\n"
. "currentDir : " . self::$_currentDir . "\n";
if (self::$_symlinks) {
self::$debugText .= "symlinks : " . var_export(self::$_symlinks, 1) . "\n";
}
self::$debugText .= "\n";
$css = self::_trimUrls($css);
// rewrite
$css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/'
,array(self::$className, '_processUriCB'), $css);
$css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
,array(self::$className, '_processUriCB'), $css);
return $css;
}
/**
* In CSS content, prepend a path to relative URIs
*
* @param string $css
*
* @param string $path The path to prepend.
*
* @return string
*/
public static function prepend($css, $path)
{
self::$_prependPath = $path;
$css = self::_trimUrls($css);
// append
$css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/'
,array(self::$className, '_processUriCB'), $css);
$css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
,array(self::$className, '_processUriCB'), $css);
self::$_prependPath = null;
return $css;
}
/**
* Get a root relative URI from a file relative URI
*
* <code>
* Minify_CSS_UriRewriter::rewriteRelative(
* '../img/hello.gif'
* , '/home/user/www/css' // path of CSS file
* , '/home/user/www' // doc root
* );
* // returns '/img/hello.gif'
*
* // example where static files are stored in a symlinked directory
* Minify_CSS_UriRewriter::rewriteRelative(
* 'hello.gif'
* , '/var/staticFiles/theme'
* , '/home/user/www'
* , array('/home/user/www/static' => '/var/staticFiles')
* );
* // returns '/static/theme/hello.gif'
* </code>
*
* @param string $uri file relative URI
*
* @param string $realCurrentDir realpath of the current file's directory.
*
* @param string $realDocRoot realpath of the site document root.
*
* @param array $symlinks (default = array()) If the file is stored in
* a symlink-ed directory, provide an array of link paths to
* real target paths, where the link paths "appear" to be within the document
* root. E.g.:
* <code>
* array('/home/foo/www/not/real/path' => '/real/target/path') // unix
* array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path') // Windows
* </code>
*
* @return string
*/
public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = array())
{
// prepend path with current dir separator (OS-independent)
$path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR)
. DIRECTORY_SEPARATOR . strtr($uri, '/', DIRECTORY_SEPARATOR);
self::$debugText .= "file-relative URI : {$uri}\n"
. "path prepended : {$path}\n";
// "unresolve" a symlink back to doc root
foreach ($symlinks as $link => $target) {
if (0 === strpos($path, $target)) {
// replace $target with $link
$path = $link . substr($path, strlen($target));
self::$debugText .= "symlink unresolved : {$path}\n";
break;
}
}
// strip doc root
$path = substr($path, strlen($realDocRoot));
self::$debugText .= "docroot stripped : {$path}\n";
// fix to root-relative URI
$uri = strtr($path, '/\\', '//');
$uri = self::removeDots($uri);
self::$debugText .= "traversals removed : {$uri}\n\n";
return $uri;
}
/**
* Remove instances of "./" and "../" where possible from a root-relative URI
* @param string $uri
* @return string
*/
public static function removeDots($uri)
{
$uri = str_replace('/./', '/', $uri);
// inspired by patch from Oleg Cherniy
do {
$uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed);
} while ($changed);
return $uri;
}
/**
* Defines which class to call as part of callbacks, change this
* if you extend Minify_CSS_UriRewriter
* @var string
*/
protected static $className = 'Minify_CSS_UriRewriter';
/**
* Get realpath with any trailing slash removed. If realpath() fails,
* just remove the trailing slash.
*
* @param string $path
*
* @return mixed path with no trailing slash
*/
protected static function _realpath($path)
{
$realPath = realpath($path);
if ($realPath !== false) {
$path = $realPath;
}
return rtrim($path, '/\\');
}
/**
* @var string directory of this stylesheet
*/
private static $_currentDir = '';
/**
* @var string DOC_ROOT
*/
private static $_docRoot = '';
/**
* @var array directory replacements to map symlink targets back to their
* source (within the document root) E.g. '/var/www/symlink' => '/var/realpath'
*/
private static $_symlinks = array();
/**
* @var string path to prepend
*/
private static $_prependPath = null;
private static function _trimUrls($css)
{
return preg_replace('/
url\\( # url(
\\s*
([^\\)]+?) # 1 = URI (assuming does not contain ")")
\\s*
\\) # )
/x', 'url($1)', $css);
}
private static function _processUriCB($m)
{
// $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
$isImport = ($m[0][0] === '@');
// determine URI and the quote character (if any)
if ($isImport) {
$quoteChar = $m[1];
$uri = $m[2];
} else {
// $m[1] is either quoted or not
$quoteChar = ($m[1][0] === "'" || $m[1][0] === '"')
? $m[1][0]
: '';
$uri = ($quoteChar === '')
? $m[1]
: substr($m[1], 1, strlen($m[1]) - 2);
}
// analyze URI
if ('/' !== $uri[0] // root-relative
&& false === strpos($uri, '//') // protocol (non-data)
&& 0 !== strpos($uri, 'data:') // data protocol
) {
// URI is file-relative: rewrite depending on options
if (self::$_prependPath === null) {
$uri = self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks);
} else {
$uri = self::$_prependPath . $uri;
if ($uri[0] === '/') {
$root = '';
$rootRelative = $uri;
$uri = $root . self::removeDots($rootRelative);
} elseif (preg_match('@^((https?\:)?//([^/]+))/@', $uri, $m) && (false !== strpos($m[3], '.'))) {
$root = $m[1];
$rootRelative = substr($uri, strlen($root));
$uri = $root . self::removeDots($rootRelative);
}
}
}
return $isImport
? "@import {$quoteChar}{$uri}{$quoteChar}"
: "url({$quoteChar}{$uri}{$quoteChar})";
}
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* Class Minify_CommentPreserver
* @package Minify
*/
/**
* Process a string in pieces preserving C-style comments that begin with "/*!"
*
* @package Minify
* @author Stephen Clay <steve@mrclay.org>
*/
class Minify_CommentPreserver {
/**
* String to be prepended to each preserved comment
*
* @var string
*/
public static $prepend = "\n";
/**
* String to be appended to each preserved comment
*
* @var string
*/
public static $append = "\n";
/**
* Process a string outside of C-style comments that begin with "/*!"
*
* On each non-empty string outside these comments, the given processor
* function will be called. The comments will be surrounded by
* Minify_CommentPreserver::$preprend and Minify_CommentPreserver::$append.
*
* @param string $content
* @param callback $processor function
* @param array $args array of extra arguments to pass to the processor
* function (default = array())
* @return string
*/
public static function process($content, $processor, $args = array())
{
$ret = '';
while (true) {
list($beforeComment, $comment, $afterComment) = self::_nextComment($content);
if ('' !== $beforeComment) {
$callArgs = $args;
array_unshift($callArgs, $beforeComment);
$ret .= call_user_func_array($processor, $callArgs);
}
if (false === $comment) {
break;
}
$ret .= $comment;
$content = $afterComment;
}
return $ret;
}
/**
* Extract comments that YUI Compressor preserves.
*
* @param string $in input
*
* @return array 3 elements are returned. If a YUI comment is found, the
* 2nd element is the comment and the 1st and 3rd are the surrounding
* strings. If no comment is found, the entire string is returned as the
* 1st element and the other two are false.
*/
private static function _nextComment($in)
{
if (
false === ($start = strpos($in, '/*!'))
|| false === ($end = strpos($in, '*/', $start + 3))
) {
return array($in, false, false);
}
$ret = array(
substr($in, 0, $start)
,self::$prepend . '/*!' . substr($in, $start + 3, $end - $start - 1) . self::$append
);
$endChars = (strlen($in) - $end - 2);
$ret[] = (0 === $endChars)
? ''
: substr($in, -$endChars);
return $ret;
}
}

View File

@@ -0,0 +1,189 @@
<?php
/**
* Class Minify_ImportProcessor
* @package Minify
*/
/**
* Linearize a CSS/JS file by including content specified by CSS import
* declarations. In CSS files, relative URIs are fixed.
*
* @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
* processed!
*
* This has a unit test but should be considered "experimental".
*
* @package Minify
* @author Stephen Clay <steve@mrclay.org>
*/
class Minify_ImportProcessor {
public static $filesIncluded = array();
public static function process($file)
{
self::$filesIncluded = array();
self::$_isCss = (strtolower(substr($file, -4)) === '.css');
$obj = new Minify_ImportProcessor(dirname($file));
return $obj->_getContent($file);
}
// allows callback funcs to know the current directory
private $_currentDir = NULL;
// allows _importCB to write the fetched content back to the obj
private $_importedContent = '';
private static $_isCss = NULL;
private function __construct($currentDir)
{
$this->_currentDir = $currentDir;
}
// ##################### BEGIN TYPO3 modification
/**
* @var array
*/
public static $extensionConfiguration = array();
// ##################### END TYPO3 modification
private function _getContent($file)
{
$file = realpath($file);
if (! $file
|| in_array($file, self::$filesIncluded)
|| FALSE === ($content = @file_get_contents($file))
) {
// file missing, already included, or failed read
return '';
}
self::$filesIncluded[] = $file;
$this->_currentDir = dirname($file);
// ##################### BEGIN TYPO3 modification
if (strpos($this->_currentDir, realpath(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_DOCUMENT_ROOT'))) === FALSE) {
$realPathToTYPO3 = str_replace('typo3/', '', realpath(PATH_typo3));
$this->_currentDir = str_replace($realPathToTYPO3, '', $this->_currentDir);
$this->_currentDir = realpath(PATH_site) . '/typo3' . $this->_currentDir;
}
// ##################### END TYPO3 modification
// remove UTF-8 BOM if present
if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) {
$content = substr($content, 3);
}
// ensure uniform EOLs
$content = str_replace("\r\n", "\n", $content);
// process @imports
$content = preg_replace_callback(
'/
@import\\s+
(?:url\\(\\s*)? # maybe url(
[\'"]? # maybe quote
(.*?) # 1 = URI
[\'"]? # maybe end quote
(?:\\s*\\))? # maybe )
([a-zA-Z,\\s]*)? # 2 = media list
; # end token
/x'
,array($this, '_importCB')
,$content
);
if (self::$_isCss) {
// rewrite remaining relative URIs
$content = preg_replace_callback(
'/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
,array($this, '_urlCB')
,$content
);
}
return $this->_importedContent . $content;
}
private function _importCB($m)
{
$url = $m[1];
$mediaList = preg_replace('/\\s+/', '', $m[2]);
if (strpos($url, '://') > 0) {
// protocol, leave in place for CSS, comment for JS
return self::$_isCss
? $m[0]
: "/* Minify_ImportProcessor will not include remote content */";
}
if ('/' === $url[0]) {
// protocol-relative or root path
$url = ltrim($url, '/');
$file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR
. strtr($url, '/', DIRECTORY_SEPARATOR);
} else {
// relative to current path
$file = $this->_currentDir . DIRECTORY_SEPARATOR
. strtr($url, '/', DIRECTORY_SEPARATOR);
}
$obj = new Minify_ImportProcessor(dirname($file));
$content = $obj->_getContent($file);
if ('' === $content) {
// failed. leave in place for CSS, comment for JS
return self::$_isCss
? $m[0]
: "/* Minify_ImportProcessor could not fetch '{$file}' */";
}
return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList))
? $content
: "@media {$mediaList} {\n{$content}\n}\n";
}
private function _urlCB($m)
{
// $m[1] is either quoted or not
$quote = ($m[1][0] === "'" || $m[1][0] === '"')
? $m[1][0]
: '';
$url = ($quote === '')
? $m[1]
: substr($m[1], 1, strlen($m[1]) - 2);
if ('/' !== $url[0]) {
if (strpos($url, '//') > 0) {
// probably starts with protocol, do not alter
// ##################### BEGIN TYPO3 modification
} elseif (strpos($url, 'data:image') !== FALSE) {
// probably starts with an inline image, do not alter
// ##################### END TYPO3 modification
} else {
// prepend path with current dir separator (OS-independent)
$path = $this->_currentDir
. DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR);
// strip doc root
$path = substr($path, strlen(realpath($_SERVER['DOCUMENT_ROOT'])));
// fix to absolute URL
$url = strtr($path, '/\\', '//');
// remove /./ and /../ where possible
$url = str_replace('/./', '/', $url);
// inspired by patch from Oleg Cherniy
do {
$url = preg_replace('@/[^/]+/\\.\\./@', '/', $url, 1, $changed);
} while ($changed);
}
}
// ##################### BEGIN TYPO3 modification
if (trim(self::$extensionConfiguration['css.']['postUrlProcessing.']['pattern']) !== '') {
$pattern = self::$extensionConfiguration['css.']['postUrlProcessing.']['pattern'];
$replacement = self::$extensionConfiguration['css.']['postUrlProcessing.']['replacement'];
$url = preg_replace($pattern, $replacement, $url);
}
// ##################### END TYPO3 modification
return "url({$quote}{$url}{$quote})";
}
}

View File

@@ -0,0 +1,386 @@
<?php
/**
* jsmin.php - PHP implementation of Douglas Crockford's JSMin.
*
* This is pretty much a direct port of jsmin.c to PHP with just a few
* PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and
* outputs to stdout, this library accepts a string as input and returns another
* string as output.
*
* PHP 5 or higher is required.
*
* Permission is hereby granted to use this version of the library under the
* same terms as jsmin.c, which has the following license:
*
* --
* Copyright (c) 2002 Douglas Crockford (www.crockford.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* The Software shall be used for Good, not Evil.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* --
*
* @package JSMin
* @author Ryan Grove <ryan@wonko.com>
* @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
* @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
* @copyright 2012 Adam Goforth <aag@adamgoforth.com> (Updates)
* @license http://opensource.org/licenses/mit-license.php MIT License
* @version 1.1.2 (2012-05-01)
* @link https://github.com/rgrove/jsmin-php
*/
class JSMin {
const ORD_LF = 10;
const ORD_SPACE = 32;
const ACTION_KEEP_A = 1;
const ACTION_DELETE_A = 2;
const ACTION_DELETE_A_B = 3;
protected $a = '';
protected $b = '';
protected $input = '';
protected $inputIndex = 0;
protected $inputLength = 0;
protected $lookAhead = null;
protected $output = '';
// -- Public Static Methods --------------------------------------------------
/**
* Minify Javascript
*
* @uses __construct()
* @uses min()
* @param string $js Javascript to be minified
* @return string
*/
public static function minify($js) {
$jsmin = new JSMin($js);
return $jsmin->min();
}
// -- Public Instance Methods ------------------------------------------------
/**
* Constructor
*
* @param string $input Javascript to be minified
*/
public function __construct($input) {
$this->input = str_replace("\r\n", "\n", $input);
$this->inputLength = strlen($this->input);
}
// -- Protected Instance Methods ---------------------------------------------
/**
* Action -- do something! What to do is determined by the $command argument.
*
* action treats a string as a single character. Wow!
* action recognizes a regular expression if it is preceded by ( or , or =.
*
* @uses next()
* @uses get()
* @throws JSMinException If parser errors are found:
* - Unterminated string literal
* - Unterminated regular expression set in regex literal
* - Unterminated regular expression literal
* @param int $command One of class constants:
* ACTION_KEEP_A Output A. Copy B to A. Get the next B.
* ACTION_DELETE_A Copy B to A. Get the next B. (Delete A).
* ACTION_DELETE_A_B Get the next B. (Delete B).
*/
protected function action($command) {
switch($command) {
case self::ACTION_KEEP_A:
$this->output .= $this->a;
case self::ACTION_DELETE_A:
$this->a = $this->b;
if ($this->a === "'" || $this->a === '"') {
for (;;) {
$this->output .= $this->a;
$this->a = $this->get();
if ($this->a === $this->b) {
break;
}
if (ord($this->a) <= self::ORD_LF) {
throw new JSMinException('Unterminated string literal.');
}
if ($this->a === '\\') {
$this->output .= $this->a;
$this->a = $this->get();
}
}
}
case self::ACTION_DELETE_A_B:
$this->b = $this->next();
if ($this->b === '/' && (
$this->a === '(' || $this->a === ',' || $this->a === '=' ||
$this->a === ':' || $this->a === '[' || $this->a === '!' ||
$this->a === '&' || $this->a === '|' || $this->a === '?' ||
$this->a === '{' || $this->a === '}' || $this->a === ';' ||
$this->a === "\n" )) {
$this->output .= $this->a . $this->b;
for (;;) {
$this->a = $this->get();
if ($this->a === '[') {
/*
inside a regex [...] set, which MAY contain a '/' itself. Example: mootools Form.Validator near line 460:
return Form.Validator.getValidator('IsEmpty').test(element) || (/^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]\.?){0,63}[a-z0-9!#$%&'*+/=?^_`{|}~-]@(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\])$/i).test(element.get('value'));
*/
for (;;) {
$this->output .= $this->a;
$this->a = $this->get();
if ($this->a === ']') {
break;
} elseif ($this->a === '\\') {
$this->output .= $this->a;
$this->a = $this->get();
} elseif (ord($this->a) <= self::ORD_LF) {
throw new JSMinException('Unterminated regular expression set in regex literal.');
}
}
} elseif ($this->a === '/') {
break;
} elseif ($this->a === '\\') {
$this->output .= $this->a;
$this->a = $this->get();
} elseif (ord($this->a) <= self::ORD_LF) {
throw new JSMinException('Unterminated regular expression literal.');
}
$this->output .= $this->a;
}
$this->b = $this->next();
}
}
}
/**
* Get next char. Convert ctrl char to space.
*
* @return string|null
*/
protected function get() {
$c = $this->lookAhead;
$this->lookAhead = null;
if ($c === null) {
if ($this->inputIndex < $this->inputLength) {
$c = substr($this->input, $this->inputIndex, 1);
$this->inputIndex += 1;
} else {
$c = null;
}
}
if ($c === "\r") {
return "\n";
}
if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) {
return $c;
}
return ' ';
}
/**
* Is $c a letter, digit, underscore, dollar sign, or non-ASCII character.
*
* @return bool
*/
protected function isAlphaNum($c) {
return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1;
}
/**
* Perform minification, return result
*
* @uses action()
* @uses isAlphaNum()
* @uses get()
* @uses peek()
* @return string
*/
protected function min() {
if (0 == strncmp($this->peek(), "\xef", 1)) {
$this->get();
$this->get();
$this->get();
}
$this->a = "\n";
$this->action(self::ACTION_DELETE_A_B);
while ($this->a !== null) {
switch ($this->a) {
case ' ':
if ($this->isAlphaNum($this->b)) {
$this->action(self::ACTION_KEEP_A);
} else {
$this->action(self::ACTION_DELETE_A);
}
break;
case "\n":
switch ($this->b) {
case '{':
case '[':
case '(':
case '+':
case '-':
case '!':
case '~':
$this->action(self::ACTION_KEEP_A);
break;
case ' ':
$this->action(self::ACTION_DELETE_A_B);
break;
default:
if ($this->isAlphaNum($this->b)) {
$this->action(self::ACTION_KEEP_A);
}
else {
$this->action(self::ACTION_DELETE_A);
}
}
break;
default:
switch ($this->b) {
case ' ':
if ($this->isAlphaNum($this->a)) {
$this->action(self::ACTION_KEEP_A);
break;
}
$this->action(self::ACTION_DELETE_A_B);
break;
case "\n":
switch ($this->a) {
case '}':
case ']':
case ')':
case '+':
case '-':
case '"':
case "'":
$this->action(self::ACTION_KEEP_A);
break;
default:
if ($this->isAlphaNum($this->a)) {
$this->action(self::ACTION_KEEP_A);
}
else {
$this->action(self::ACTION_DELETE_A_B);
}
}
break;
default:
$this->action(self::ACTION_KEEP_A);
break;
}
}
}
return $this->output;
}
/**
* Get the next character, skipping over comments. peek() is used to see
* if a '/' is followed by a '/' or '*'.
*
* @uses get()
* @uses peek()
* @throws JSMinException On unterminated comment.
* @return string
*/
protected function next() {
$c = $this->get();
if ($c === '/') {
switch($this->peek()) {
case '/':
for (;;) {
$c = $this->get();
if (ord($c) <= self::ORD_LF) {
return $c;
}
}
case '*':
$this->get();
for (;;) {
switch($this->get()) {
case '*':
if ($this->peek() === '/') {
$this->get();
return ' ';
}
break;
case null:
throw new JSMinException('Unterminated comment.');
}
}
default:
return $c;
}
}
return $c;
}
/**
* Get next char. If is ctrl character, translate to a space or newline.
*
* @uses get()
* @return string|null
*/
protected function peek() {
$this->lookAhead = $this->get();
return $this->lookAhead;
}
}
// -- Exceptions ---------------------------------------------------------------
class JSMinException extends Exception {}
?>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
{
"name": "sgalinski/scriptmerger",
"type": "typo3-cms-extension",
"description": "CSS/Javascript Minificator, Compressor And Concatenator - This extension minimizes the http requests by concatenating your css and javascript. Furthermore the result can be minified and compressed. This whole process is highly configurable and is partly based on the \"minify\", \"jsminplus\" and \"jsmin\" projects.",
"homepage": "https://www.sgalinski.de",
"license": ["GPL-2.0+"],
"version": "5.0.1",
"support": {
"issues": "https://forge.typo3.org/projects/extension-scriptmerger/issues"
},
"require": {
"typo3/cms-core": "*"
},
"autoload": {
"psr-4": {
"SGalinski\\Scriptmerger\\": "Classes/"
}
}
}

View File

@@ -0,0 +1,28 @@
### BEGIN EXT:scriptmerger ###
# Removal of ETag (saves some bytes; the last modified header is still there)
FileETag MTime Size
<IfModule mod_headers.c>
FileETag none
</IfModule>
# Client caching of JS/CSS files (because they are merged with an applied hash!)
# Note: Unsetting of the last modified header causes a permant 200 Ok status
<FilesMatch "\.(merge|min|gz)(\.gz)?\.(js|css)">
<IfModule mod_expires.c>
ExpiresActive on
ExpiresDefault "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
Header append Cache-Control "public"
</IfModule>
</FilesMatch>
# deliver the uncompressed file if gzip encoding isn't accepted
AddEncoding x-gzip .gz
RewriteCond %{HTTP:accept-encoding} !.*(x-)?gzip.* [NC,OR]
RewriteCond %{HTTP:accept-encoding} .*(x-)?gzip;q=0.* [NC]
RewriteRule ^typo3temp/scriptmerger/compressed/(.+)\.gz\.(js|css) typo3temp/scriptmerger/uncompressed/$1.$2 [L,NC]
### END EXT:scriptmerger ###

View File

@@ -0,0 +1,14 @@
location ~ \.(merge|min|gz)(\.gz)?\.(js|css) {
expires 365d;
add_header Cache-Control "public";
}
# active only if compression is disabled in the scriptmerger configuration
gzip on;
gzip_disable "msie6";
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1100;
gzip_buffers 16 8k;
gzip_types text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/json application/xml+rss;

View File

@@ -0,0 +1,14 @@
<?php
$extensionPath = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('scriptmerger');
return array(
'minify_importprocessor' => $extensionPath . 'Resources/Minify/ImportProcessor.php',
'minify_commentpreserver' => $extensionPath . 'Resources/Minify/CommentPreserver.php',
'minify_css' => $extensionPath . 'Resources/Minify/CSS.php',
'minify_css_compressor' => $extensionPath . 'Resources/Minify/CSS/Compressor.php',
'minify_css_urirewriter' => $extensionPath . 'Resources/Minify/CSS/UriRewriter.php',
'jsminplus' => $extensionPath . 'Resources/jsminplus.php',
'jsmin' => $extensionPath . 'Resources/jsmin.php',
'jshrink\\minifier' => $extensionPath . 'Resources/JShrink/Minifier.php',
);

View File

@@ -0,0 +1,44 @@
<?php
/***************************************************************
* Extension Manager/Repository config file for ext "scriptmerger".
*
* Auto generated 02-02-2016 16:19
*
* Manual updates:
* Only the data in the array - everything else is removed by next
* writing. "version" and "dependencies" must not be touched!
***************************************************************/
$EM_CONF[$_EXTKEY] = array (
'title' => 'CSS/Javascript Minificator, Compressor And Concatenator',
'description' => 'This extension minimizes the http requests by concatenating your css and javascript. Furthermore the result can be minified and compressed. This whole process is highly configurable and is partly based on the "minify", "jsminplus" and "jsmin" projects.',
'category' => 'fe',
'version' => '5.0.1',
'state' => 'stable',
'uploadfolder' => false,
'createDirs' => 'typo3temp/scriptmerger/',
'clearcacheonload' => false,
'author' => 'Stefan Galinski',
'author_email' => 'stefan@sgalinski.de',
'author_company' => '',
'constraints' =>
array (
'depends' =>
array (
'php' => '5.3.0-5.6.99',
'typo3' => '6.2.0-7.6.99',
),
'conflicts' =>
array (
'speedy' => '',
'queo_speedup' => '',
'js_css_optimizer' => '',
'minify' => '',
),
'suggests' =>
array (
),
),
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -0,0 +1,24 @@
<?php
if (!defined('TYPO3_MODE')) {
die ('Access denied.');
}
// post processing hook to clear any existing cache files if the button in
// the backend is clicked (contains an age check)
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'][] =
'EXT:scriptmerger/Classes/user_ScriptmergerCacheHook.php:SGalinski\Scriptmerger\user_ScriptmergerCacheHook->clearCachePostProc';
// register the minify, compress and merge processes
if (TYPO3_MODE == 'FE') {
$TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-output'][] =
'EXT:scriptmerger/Classes/user_ScriptmergerOutputHook.php:SGalinski\Scriptmerger\user_ScriptmergerOutputHook->contentPostProcOutput';
$TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-all'][] =
'EXT:scriptmerger/Classes/user_ScriptmergerOutputHook.php:SGalinski\Scriptmerger\user_ScriptmergerOutputHook->contentPostProcAll';
}
// needs to be disabled for the frontend, otherwise the default exclude rule prevents any script files from merging
$TYPO3_CONF_VARS['FE']['versionNumberInFilename'] = '';
$TYPO3_CONF_VARS['FE']['compressionLevel'] = '0';
?>

View File

@@ -0,0 +1,10 @@
<?php
if (!defined('TYPO3_MODE')) {
die('Access denied.');
}
/** @noinspection PhpUndefinedVariableInspection */
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile($_EXTKEY, 'Configuration/', 'Scriptmerger');
?>