From 511a4aaad215dbb8fe5480cb5fd20d90c75c0b16 Mon Sep 17 00:00:00 2001 From: Tim <162806658+timotei-litespeed@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:39:50 +0200 Subject: [PATCH] Improve CSS JS minification libraries (#779) * Improve CSS JS minification libraries * Fix css/js replacement leaving empty line * Remove extra function * Review fix --- autoload.php | 12 +- lib/css-min/colors.cls.php | 156 ---- lib/css-min/minifier.cls.php | 866 ------------------ lib/css-min/utils.cls.php | 150 --- lib/css_js_min/minify/LICENSE | 18 + lib/css_js_min/minify/css.cls.php | 860 +++++++++++++++++ .../minify/data/js}/keywords_after.txt | 0 .../minify/data/js}/keywords_before.txt | 0 .../minify/data/js}/keywords_reserved.txt | 0 .../minify/data/js}/operators.txt | 0 .../minify/data/js}/operators_after.txt | 0 .../minify/data/js}/operators_before.txt | 0 lib/css_js_min/minify/exception.cls.php | 25 + .../minify/js.cls.php} | 557 ++++------- lib/css_js_min/minify/minify.cls.php | 535 +++++++++++ lib/css_js_min/pathconverter/LICENSE | 18 + .../pathconverter/converter.cls.php | 228 +++++ lib/{css-min => }/urirewriter.cls.php | 2 +- src/optimize.cls.php | 10 +- src/optimizer.cls.php | 25 +- 20 files changed, 1883 insertions(+), 1579 deletions(-) delete mode 100644 lib/css-min/colors.cls.php delete mode 100644 lib/css-min/minifier.cls.php delete mode 100644 lib/css-min/utils.cls.php create mode 100644 lib/css_js_min/minify/LICENSE create mode 100644 lib/css_js_min/minify/css.cls.php rename lib/{jsmin_data => css_js_min/minify/data/js}/keywords_after.txt (100%) rename lib/{jsmin_data => css_js_min/minify/data/js}/keywords_before.txt (100%) rename lib/{jsmin_data => css_js_min/minify/data/js}/keywords_reserved.txt (100%) rename lib/{jsmin_data => css_js_min/minify/data/js}/operators.txt (100%) rename lib/{jsmin_data => css_js_min/minify/data/js}/operators_after.txt (100%) rename lib/{jsmin_data => css_js_min/minify/data/js}/operators_before.txt (100%) create mode 100644 lib/css_js_min/minify/exception.cls.php rename lib/{jsmin.cls.php => css_js_min/minify/js.cls.php} (74%) create mode 100644 lib/css_js_min/minify/minify.cls.php create mode 100644 lib/css_js_min/pathconverter/LICENSE create mode 100644 lib/css_js_min/pathconverter/converter.cls.php rename lib/{css-min => }/urirewriter.cls.php (99%) diff --git a/autoload.php b/autoload.php index 5616ae551..cec95e3e2 100644 --- a/autoload.php +++ b/autoload.php @@ -80,19 +80,19 @@ 'cli/purge.cls.php', // 3rd party libraries - 'lib/css-min/colors.cls.php', - 'lib/css-min/minifier.cls.php', - 'lib/css-min/urirewriter.cls.php', - 'lib/css-min/utils.cls.php', + 'lib/css_js_min/pathconverter/converter.cls.php', + 'lib/css_js_min/minify/exception.cls.php', + 'lib/css_js_min/minify/minify.cls.php', + 'lib/css_js_min/minify/css.cls.php', + 'lib/css_js_min/minify/js.cls.php', + 'lib/urirewriter.cls.php', 'lib/guest.cls.php', 'lib/html-min.cls.php', - 'lib/jsmin.cls.php', // 'lib/object-cache.php', // 'lib/php-compatibility.func.php', // upgrade purpose delay loaded funcs // 'src/data.upgrade.func.php', - ); foreach ($litespeed_php_files as $class) { $file = LSCWP_DIR . $class; diff --git a/lib/css-min/colors.cls.php b/lib/css-min/colors.cls.php deleted file mode 100644 index 8ad5c4e75..000000000 --- a/lib/css-min/colors.cls.php +++ /dev/null @@ -1,156 +0,0 @@ - 'azure', - '#f5f5dc' => 'beige', - '#ffe4c4' => 'bisque', - '#a52a2a' => 'brown', - '#ff7f50' => 'coral', - '#ffd700' => 'gold', - '#808080' => 'gray', - '#008000' => 'green', - '#4b0082' => 'indigo', - '#fffff0' => 'ivory', - '#f0e68c' => 'khaki', - '#faf0e6' => 'linen', - '#800000' => 'maroon', - '#000080' => 'navy', - '#fdf5e6' => 'oldlace', - '#808000' => 'olive', - '#ffa500' => 'orange', - '#da70d6' => 'orchid', - '#cd853f' => 'peru', - '#ffc0cb' => 'pink', - '#dda0dd' => 'plum', - '#800080' => 'purple', - '#f00' => 'red', - '#fa8072' => 'salmon', - '#a0522d' => 'sienna', - '#c0c0c0' => 'silver', - '#fffafa' => 'snow', - '#d2b48c' => 'tan', - '#008080' => 'teal', - '#ff6347' => 'tomato', - '#ee82ee' => 'violet', - '#f5deb3' => 'wheat' - ); - } - - public static function getNamedToHexMap() - { - // Named colors longer than hex counterpart - return array( - 'aliceblue' => '#f0f8ff', - 'antiquewhite' => '#faebd7', - 'aquamarine' => '#7fffd4', - 'black' => '#000', - 'blanchedalmond' => '#ffebcd', - 'blueviolet' => '#8a2be2', - 'burlywood' => '#deb887', - 'cadetblue' => '#5f9ea0', - 'chartreuse' => '#7fff00', - 'chocolate' => '#d2691e', - 'cornflowerblue' => '#6495ed', - 'cornsilk' => '#fff8dc', - 'darkblue' => '#00008b', - 'darkcyan' => '#008b8b', - 'darkgoldenrod' => '#b8860b', - 'darkgray' => '#a9a9a9', - 'darkgreen' => '#006400', - 'darkgrey' => '#a9a9a9', - 'darkkhaki' => '#bdb76b', - 'darkmagenta' => '#8b008b', - 'darkolivegreen' => '#556b2f', - 'darkorange' => '#ff8c00', - 'darkorchid' => '#9932cc', - 'darksalmon' => '#e9967a', - 'darkseagreen' => '#8fbc8f', - 'darkslateblue' => '#483d8b', - 'darkslategray' => '#2f4f4f', - 'darkslategrey' => '#2f4f4f', - 'darkturquoise' => '#00ced1', - 'darkviolet' => '#9400d3', - 'deeppink' => '#ff1493', - 'deepskyblue' => '#00bfff', - 'dodgerblue' => '#1e90ff', - 'firebrick' => '#b22222', - 'floralwhite' => '#fffaf0', - 'forestgreen' => '#228b22', - 'fuchsia' => '#f0f', - 'gainsboro' => '#dcdcdc', - 'ghostwhite' => '#f8f8ff', - 'goldenrod' => '#daa520', - 'greenyellow' => '#adff2f', - 'honeydew' => '#f0fff0', - 'indianred' => '#cd5c5c', - 'lavender' => '#e6e6fa', - 'lavenderblush' => '#fff0f5', - 'lawngreen' => '#7cfc00', - 'lemonchiffon' => '#fffacd', - 'lightblue' => '#add8e6', - 'lightcoral' => '#f08080', - 'lightcyan' => '#e0ffff', - 'lightgoldenrodyellow' => '#fafad2', - 'lightgray' => '#d3d3d3', - 'lightgreen' => '#90ee90', - 'lightgrey' => '#d3d3d3', - 'lightpink' => '#ffb6c1', - 'lightsalmon' => '#ffa07a', - 'lightseagreen' => '#20b2aa', - 'lightskyblue' => '#87cefa', - 'lightslategray' => '#778899', - 'lightslategrey' => '#778899', - 'lightsteelblue' => '#b0c4de', - 'lightyellow' => '#ffffe0', - 'limegreen' => '#32cd32', - 'mediumaquamarine' => '#66cdaa', - 'mediumblue' => '#0000cd', - 'mediumorchid' => '#ba55d3', - 'mediumpurple' => '#9370db', - 'mediumseagreen' => '#3cb371', - 'mediumslateblue' => '#7b68ee', - 'mediumspringgreen' => '#00fa9a', - 'mediumturquoise' => '#48d1cc', - 'mediumvioletred' => '#c71585', - 'midnightblue' => '#191970', - 'mintcream' => '#f5fffa', - 'mistyrose' => '#ffe4e1', - 'moccasin' => '#ffe4b5', - 'navajowhite' => '#ffdead', - 'olivedrab' => '#6b8e23', - 'orangered' => '#ff4500', - 'palegoldenrod' => '#eee8aa', - 'palegreen' => '#98fb98', - 'paleturquoise' => '#afeeee', - 'palevioletred' => '#db7093', - 'papayawhip' => '#ffefd5', - 'peachpuff' => '#ffdab9', - 'powderblue' => '#b0e0e6', - 'rebeccapurple' => '#663399', - 'rosybrown' => '#bc8f8f', - 'royalblue' => '#4169e1', - 'saddlebrown' => '#8b4513', - 'sandybrown' => '#f4a460', - 'seagreen' => '#2e8b57', - 'seashell' => '#fff5ee', - 'slateblue' => '#6a5acd', - 'slategray' => '#708090', - 'slategrey' => '#708090', - 'springgreen' => '#00ff7f', - 'steelblue' => '#4682b4', - 'turquoise' => '#40e0d0', - 'white' => '#fff', - 'whitesmoke' => '#f5f5f5', - 'yellow' => '#ff0', - 'yellowgreen' => '#9acd32' - ); - } -} diff --git a/lib/css-min/minifier.cls.php b/lib/css-min/minifier.cls.php deleted file mode 100644 index 4aa119948..000000000 --- a/lib/css-min/minifier.cls.php +++ /dev/null @@ -1,866 +0,0 @@ -raisePhpLimits = (bool) $raisePhpLimits; - $this->memoryLimit = 128 * 1048576; // 128MB in bytes - $this->pcreBacktrackLimit = 1000 * 1000; - $this->pcreRecursionLimit = 500 * 1000; - $this->hexToNamedColorsMap = Colors::getHexToNamedMap(); - $this->namedToHexColorsMap = Colors::getNamedToHexMap(); - $this->namedToHexColorsRegex = sprintf( - '/([:,( ])(%s)( |,|\)|;|$)/Si', - implode('|', array_keys($this->namedToHexColorsMap)) - ); - $this->numRegex = sprintf('-?\d*\.?\d+%s?', $this->unitsGroupRegex); - $this->setShortenZeroValuesRegexes(); - } - - /** - * Parses & minifies the given input CSS string - * @param string $css - * @return string - */ - public function run($css = '') - { - if (empty($css) || !is_string($css)) { - return ''; - } - - $this->resetRunProperties(); - - if ($this->raisePhpLimits) { - $this->doRaisePhpLimits(); - } - - return $this->minify($css); - } - - /** - * Sets whether to keep or remove sourcemap special comment. - * Sourcemap comments are removed by default. - * @param bool $keepSourceMapComment - */ - public function keepSourceMapComment($keepSourceMapComment = true) - { - $this->keepSourceMapComment = (bool) $keepSourceMapComment; - } - - /** - * Sets whether to keep or remove important comments. - * Important comments outside of a declaration block are kept by default. - * @param bool $removeImportantComments - */ - public function removeImportantComments($removeImportantComments = true) - { - $this->keepImportantComments = !(bool) $removeImportantComments; - } - - /** - * Sets the approximate column after which long lines will be split in the output - * with a linebreak. - * @param int $position - */ - public function setLineBreakPosition($position) - { - $this->linebreakPosition = (int) $position; - } - - /** - * Sets the memory limit for this script - * @param int|string $limit - */ - public function setMemoryLimit($limit) - { - $this->memoryLimit = Utils::normalizeInt($limit); - } - - /** - * Sets the maximum execution time for this script - * @param int|string $seconds - */ - public function setMaxExecutionTime($seconds) - { - $this->maxExecutionTime = (int) $seconds; - } - - /** - * Sets the PCRE backtrack limit for this script - * @param int $limit - */ - public function setPcreBacktrackLimit($limit) - { - $this->pcreBacktrackLimit = (int) $limit; - } - - /** - * Sets the PCRE recursion limit for this script - * @param int $limit - */ - public function setPcreRecursionLimit($limit) - { - $this->pcreRecursionLimit = (int) $limit; - } - - /** - * Builds regular expressions needed for shortening zero values - */ - private function setShortenZeroValuesRegexes() - { - $zeroRegex = '0'. $this->unitsGroupRegex; - $numOrPosRegex = '('. $this->numRegex .'|top|left|bottom|right|center) '; - $oneZeroSafeProperties = array( - '(?:line-)?height', - '(?:(?:min|max)-)?width', - 'top', - 'left', - 'background-position', - 'bottom', - 'right', - 'border(?:-(?:top|left|bottom|right))?(?:-width)?', - 'border-(?:(?:top|bottom)-(?:left|right)-)?radius', - 'column-(?:gap|width)', - 'margin(?:-(?:top|left|bottom|right))?', - 'outline-width', - 'padding(?:-(?:top|left|bottom|right))?' - ); - - // First zero regex - $regex = '/(^|;)('. implode('|', $oneZeroSafeProperties) .'):%s/Si'; - $this->shortenOneZeroesRegex = sprintf($regex, $zeroRegex); - - // Multiple zeroes regexes - $regex = '/(^|;)(margin|padding|border-(?:width|radius)|background-position):%s/Si'; - $this->shortenTwoZeroesRegex = sprintf($regex, $numOrPosRegex . $zeroRegex); - $this->shortenThreeZeroesRegex = sprintf($regex, $numOrPosRegex . $numOrPosRegex . $zeroRegex); - $this->shortenFourZeroesRegex = sprintf($regex, $numOrPosRegex . $numOrPosRegex . $numOrPosRegex . $zeroRegex); - } - - /** - * Resets properties whose value may change between runs - */ - private function resetRunProperties() - { - $this->comments = array(); - $this->ruleBodies = array(); - $this->preservedTokens = array(); - } - - /** - * Tries to configure PHP to use at least the suggested minimum settings - * @return void - */ - private function doRaisePhpLimits() - { - $phpLimits = array( - 'memory_limit' => $this->memoryLimit, - 'max_execution_time' => $this->maxExecutionTime, - 'pcre.backtrack_limit' => $this->pcreBacktrackLimit, - 'pcre.recursion_limit' => $this->pcreRecursionLimit - ); - - // If current settings are higher respect them. - foreach ($phpLimits as $name => $suggested) { - $current = Utils::normalizeInt(ini_get($name)); - - if ($current >= $suggested) { - continue; - } - - // memoryLimit exception: allow -1 for "no memory limit". - if ($name === 'memory_limit' && $current === -1) { - continue; - } - - // maxExecutionTime exception: allow 0 for "no memory limit". - if ($name === 'max_execution_time' && $current === 0) { - continue; - } - - ini_set($name, $suggested); - } - } - - /** - * Registers a preserved token - * @param string $token - * @return string The token ID string - */ - private function registerPreservedToken($token) - { - $tokenId = sprintf(self::PRESERVED_TOKEN, count($this->preservedTokens)); - $this->preservedTokens[$tokenId] = $token; - return $tokenId; - } - - /** - * Registers a candidate comment token - * @param string $comment - * @return string The comment token ID string - */ - private function registerCommentToken($comment) - { - $tokenId = sprintf(self::COMMENT_TOKEN, count($this->comments)); - $this->comments[$tokenId] = $comment; - return $tokenId; - } - - /** - * Registers a rule body token - * @param string $body the minified rule body - * @return string The rule body token ID string - */ - private function registerRuleBodyToken($body) - { - if (empty($body)) { - return ''; - } - - $tokenId = sprintf(self::RULE_BODY_TOKEN, count($this->ruleBodies)); - $this->ruleBodies[$tokenId] = $body; - return $tokenId; - } - - /** - * Parses & minifies the given input CSS string - * @param string $css - * @return string - */ - private function minify($css) - { - // Process data urls - $css = $this->processDataUrls($css); - - // Process comments - $css = preg_replace_callback( - '/(?processComments($css); - - // Process rule bodies - $css = $this->processRuleBodies($css); - - // Process at-rules and selectors - $css = $this->processAtRulesAndSelectors($css); - - // Restore preserved rule bodies before splitting - $css = strtr($css, $this->ruleBodies); - - // Some source control tools don't like it when files containing lines longer - // than, say 8000 characters, are checked in. The linebreak option is used in - // that case to split long lines after a specific column. - if ($this->linebreakPosition > 0) { - $l = strlen($css); - $offset = $this->linebreakPosition; - while (preg_match('/(?linebreakPosition; - $l += 1; - if ($offset > $l) { - break; - } - } - } - - // Restore preserved comments and strings - $css = strtr($css, $this->preservedTokens); - - return trim($css); - } - - /** - * Searches & replaces all data urls with tokens before we start compressing, - * to avoid performance issues running some of the subsequent regexes against large string chunks. - * @param string $css - * @return string - */ - private function processDataUrls($css) - { - $ret = ''; - $searchOffset = $substrOffset = 0; - - // Since we need to account for non-base64 data urls, we need to handle - // ' and ) being part of the data string. - while (preg_match('/url\(\s*(["\']?)data:/Si', $css, $m, PREG_OFFSET_CAPTURE, $searchOffset)) { - $matchStartIndex = $m[0][1]; - $dataStartIndex = $matchStartIndex + 4; // url( length - $searchOffset = $matchStartIndex + strlen($m[0][0]); - $terminator = $m[1][0]; // ', " or empty (not quoted) - $terminatorRegex = '/(?registerPreservedToken(trim($token)) .')'; - // No end terminator found, re-add the whole match. Should we throw/warn here? - } else { - $ret .= substr($css, $matchStartIndex, $searchOffset - $matchStartIndex); - } - - $substrOffset = $searchOffset; - } - - $ret .= substr($css, $substrOffset); - - return $ret; - } - - /** - * Registers all comments found as candidates to be preserved. - * @param array $matches - * @return string - */ - private function processCommentsCallback($matches) - { - return '/*'. $this->registerCommentToken($matches[1]) .'*/'; - } - - /** - * Preserves old IE Matrix string definition - * @param array $matches - * @return string - */ - private function processOldIeSpecificMatrixDefinitionCallback($matches) - { - return 'filter:progid:DXImageTransform.Microsoft.Matrix('. $this->registerPreservedToken($matches[1]) .')'; - } - - /** - * Preserves strings found - * @param array $matches - * @return string - */ - private function processStringsCallback($matches) - { - $match = $matches[0]; - $quote = substr($match, 0, 1); - $match = substr($match, 1, -1); - - // maybe the string contains a comment-like substring? - // one, maybe more? put'em back then - if (strpos($match, self::COMMENT_TOKEN_START) !== false) { - $match = strtr($match, $this->comments); - } - - // minify alpha opacity in filter strings - $match = str_ireplace('progid:DXImageTransform.Microsoft.Alpha(Opacity=', 'alpha(opacity=', $match); - - return $quote . $this->registerPreservedToken($match) . $quote; - } - - /** - * Preserves or removes comments found. - * @param string $css - * @return string - */ - private function processComments($css) - { - foreach ($this->comments as $commentId => $comment) { - $commentIdString = '/*'. $commentId .'*/'; - - // ! in the first position of the comment means preserve - // so push to the preserved tokens keeping the ! - if ($this->keepImportantComments && strpos($comment, '!') === 0) { - $preservedTokenId = $this->registerPreservedToken($comment); - // Put new lines before and after /*! important comments - $css = str_replace($commentIdString, "\n/*$preservedTokenId*/\n", $css); - continue; - } - - // # sourceMappingURL= in the first position of the comment means sourcemap - // so push to the preserved tokens if {$this->keepSourceMapComment} is truthy. - if ($this->keepSourceMapComment && strpos($comment, '# sourceMappingURL=') === 0) { - $preservedTokenId = $this->registerPreservedToken($comment); - // Add new line before the sourcemap comment - $css = str_replace($commentIdString, "\n/*$preservedTokenId*/", $css); - continue; - } - - // Keep empty comments after child selectors (IE7 hack) - // e.g. html >/**/ body - if (strlen($comment) === 0 && strpos($css, '>/*'.$commentId) !== false) { - $css = str_replace($commentId, $this->registerPreservedToken(''), $css); - continue; - } - - // in all other cases kill the comment - $css = str_replace($commentIdString, '', $css); - } - - // Normalize whitespace again - $css = preg_replace('/ +/S', ' ', $css); - - return $css; - } - - /** - * Finds, minifies & preserves all rule bodies. - * @param string $css the whole stylesheet. - * @return string - */ - private function processRuleBodies($css) - { - $ret = ''; - $searchOffset = $substrOffset = 0; - - while (($blockStartPos = strpos($css, '{', $searchOffset)) !== false) { - $blockEndPos = strpos($css, '}', $blockStartPos); - if ( ! $blockEndPos ) throw new \Exception( 'CSS parse error' ) ; - - $nextBlockStartPos = strpos($css, '{', $blockStartPos + 1); - $ret .= substr($css, $substrOffset, $blockStartPos - $substrOffset); - - if ($nextBlockStartPos !== false && $nextBlockStartPos < $blockEndPos) { - $ret .= substr($css, $blockStartPos, $nextBlockStartPos - $blockStartPos); - $searchOffset = $nextBlockStartPos; - } else { - $ruleBody = substr($css, $blockStartPos + 1, $blockEndPos - $blockStartPos - 1); - $ruleBodyToken = $this->registerRuleBodyToken($this->processRuleBody($ruleBody)); - $ret .= '{'. $ruleBodyToken .'}'; - $searchOffset = $blockEndPos + 1; - } - - $substrOffset = $searchOffset; - } - - $ret .= substr($css, $substrOffset); - - return $ret; - } - - /** - * Compresses non-group rule bodies. - * @param string $body The rule body without curly braces - * @return string - */ - private function processRuleBody($body) - { - $body = trim($body); - - // Remove spaces before the things that should not have spaces before them. - $body = preg_replace('/ ([:=,)*\/;\n])/S', '$1', $body); - - // Remove the spaces after the things that should not have spaces after them. - $body = preg_replace('/([:=,(*\/!;\n]) /S', '$1', $body); - - // Replace multiple semi-colons in a row by a single one - $body = preg_replace('/;;+/S', ';', $body); - - // Remove semicolon before closing brace except when: - // - The last property is prefixed with a `*` (lte IE7 hack) to avoid issues on Symbian S60 3.x browsers. - if (!preg_match('/\*[a-z0-9-]+:[^;]+;$/Si', $body)) { - $body = rtrim($body, ';'); - } - - // Remove important comments inside a rule body (because they make no sense here). - if (strpos($body, '/*') !== false) { - $body = preg_replace('/\n?\/\*[A-Z0-9_]+\*\/\n?/S', '', $body); - } - - // Empty rule body? Exit :) - if (empty($body)) { - return ''; - } - - // Shorten font-weight values - $body = preg_replace( - array('/(font-weight:)bold\b/Si', '/(font-weight:)normal\b/Si'), - array('${1}700', '${1}400'), - $body - ); - - // Shorten background property - $body = preg_replace('/(background:)(?:none|transparent)( !|;|$)/Si', '${1}0 0$2', $body); - - // Shorten opacity IE filter - $body = str_ireplace('progid:DXImageTransform.Microsoft.Alpha(Opacity=', 'alpha(opacity=', $body); - - // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space) - // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space) - // This makes it more likely that it'll get further compressed in the next step. - $body = preg_replace_callback( - '/(rgb|hsl)\(([0-9,.% -]+)\)(.|$)/Si', - array($this, 'shortenHslAndRgbToHexCallback'), - $body - ); - - // Shorten colors from #AABBCC to #ABC or shorter color name: - // - Look for hex colors which don't have a "=" in front of them (to avoid MSIE filters) - $body = preg_replace_callback( - '/(? #fff. - // Run at least 2 times to cover most cases - $body = preg_replace_callback( - array($this->namedToHexColorsRegex, $this->namedToHexColorsRegex), - array($this, 'shortenNamedColorsCallback'), - $body - ); - - // Replace positive sign from numbers before the leading space is removed. - // +1.2em to 1.2em, +.8px to .8px, +2% to 2% - $body = preg_replace('/([ :,(])\+(\.?\d+)/S', '$1$2', $body); - - // shorten ms to s - $body = preg_replace_callback('/([ :,(])(-?)(\d{3,})ms/Si', function ($matches) { - return $matches[1] . $matches[2] . ((int) $matches[3] / 1000) .'s'; - }, $body); - - // Remove leading zeros from integer and float numbers. - // 000.6 to .6, -0.8 to -.8, 0050 to 50, -01.05 to -1.05 - $body = preg_replace('/([ :,(])(-?)0+([1-9]?\.?\d+)/S', '$1$2$3', $body); - - // Remove trailing zeros from float numbers. - // -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px - $body = preg_replace('/([ :,(])(-?\d?\.\d+?)0+([^\d])/S', '$1$2$3', $body); - - // Remove trailing .0 -> -9.0 to -9 - $body = preg_replace('/([ :,(])(-?\d+)\.0([^\d])/S', '$1$2$3', $body); - - // Replace 0 length numbers with 0 - $body = preg_replace('/([ :,(])-?\.?0+([^\d])/S', '${1}0$2', $body); - - // Shorten zero values for safe properties only - $body = preg_replace( - array( - $this->shortenOneZeroesRegex, - $this->shortenTwoZeroesRegex, - $this->shortenThreeZeroesRegex, - $this->shortenFourZeroesRegex - ), - array( - '$1$2:0', - '$1$2:$3 0', - '$1$2:$3 $4 0', - '$1$2:$3 $4 $5 0' - ), - $body - ); - - // Replace 0 0 0; or 0 0 0 0; with 0 0 for background-position property. - $body = preg_replace('/(background-position):0(?: 0){2,3}( !|;|$)/Si', '$1:0 0$2', $body); - - // Shorten suitable shorthand properties with repeated values - $body = preg_replace( - array( - '/(margin|padding|border-(?:width|radius)):('.$this->numRegex.')(?: \2)+( !|;|$)/Si', - '/(border-(?:style|color)):([#a-z0-9]+)(?: \2)+( !|;|$)/Si' - ), - '$1:$2$3', - $body - ); - $body = preg_replace( - array( - '/(margin|padding|border-(?:width|radius)):'. - '('.$this->numRegex.') ('.$this->numRegex.') \2 \3( !|;|$)/Si', - '/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) \2 \3( !|;|$)/Si' - ), - '$1:$2 $3$4', - $body - ); - $body = preg_replace( - array( - '/(margin|padding|border-(?:width|radius)):'. - '('.$this->numRegex.') ('.$this->numRegex.') ('.$this->numRegex.') \3( !|;|$)/Si', - '/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) ([#a-z0-9]+) \3( !|;|$)/Si' - ), - '$1:$2 $3 $4$5', - $body - ); - - // Lowercase some common functions that can be values - $body = preg_replace_callback( - '/(?:attr|blur|brightness|circle|contrast|cubic-bezier|drop-shadow|ellipse|from|grayscale|'. - 'hsla?|hue-rotate|inset|invert|local|minmax|opacity|perspective|polygon|rgba?|rect|repeat|saturate|sepia|'. - 'steps|to|url|var|-webkit-gradient|'. - '(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|(?:repeating-)?(?:linear|radial)-gradient))\(/Si', - array($this, 'strtolowerCallback'), - $body - ); - - // Lowercase all uppercase properties - $body = preg_replace_callback('/(?:^|;)[A-Z-]+:/S', array($this, 'strtolowerCallback'), $body); - - return $body; - } - - /** - * Compresses At-rules and selectors. - * @param string $css the whole stylesheet with rule bodies tokenized. - * @return string - */ - private function processAtRulesAndSelectors($css) - { - $charset = ''; - $imports = ''; - $namespaces = ''; - - // Remove spaces before the things that should not have spaces before them. - $css = preg_replace('/ ([@{};>+)\]~=,\/\n])/S', '$1', $css); - - // Remove the spaces after the things that should not have spaces after them. - $css = preg_replace('/([{}:;>+(\[~=,\/\n]) /S', '$1', $css); - - // Shorten shortable double colon (CSS3) pseudo-elements to single colon (CSS2) - $css = preg_replace('/::(before|after|first-(?:line|letter))(\{|,)/Si', ':$1$2', $css); - - // Retain space for special IE6 cases - $css = preg_replace_callback('/:first-(line|letter)(\{|,)/Si', function ($matches) { - return ':first-'. strtolower($matches[1]) .' '. $matches[2]; - }, $css); - - // Find a fraction that may used in some @media queries such as: (min-aspect-ratio: 1/1) - // Add token to add the "/" back in later - $css = preg_replace('/\(([a-z-]+):([0-9]+)\/([0-9]+)\)/Si', '($1:$2'. self::QUERY_FRACTION .'$3)', $css); - - // Remove empty rule blocks up to 2 levels deep. - $css = preg_replace(array_fill(0, 2, '/(\{)[^{};\/\n]+\{\}/S'), '$1', $css); - $css = preg_replace('/[^{};\/\n]+\{\}/S', '', $css); - - // Two important comments next to each other? Remove extra newline. - if ($this->keepImportantComments) { - $css = str_replace("\n\n", "\n", $css); - } - - // Restore fraction - $css = str_replace(self::QUERY_FRACTION, '/', $css); - - // Lowercase some popular @directives - $css = preg_replace_callback( - '/(?charsetRegex, $css, $matches)) { - // Keep the first @charset at-rule found - $charset = $matches[0]; - // Delete all @charset at-rules - $css = preg_replace($this->charsetRegex, '', $css); - } - - // @import handling - $css = preg_replace_callback($this->importRegex, function ($matches) use (&$imports) { - // Keep all @import at-rules found for later - $imports .= $matches[0]; - // Delete all @import at-rules - return ''; - }, $css); - - // @namespace handling - $css = preg_replace_callback($this->namespaceRegex, function ($matches) use (&$namespaces) { - // Keep all @namespace at-rules found for later - $namespaces .= $matches[0]; - // Delete all @namespace at-rules - return ''; - }, $css); - - // Order critical at-rules: - // 1. @charset first - // 2. @imports below @charset - // 3. @namespaces below @imports - $css = $charset . $imports . $namespaces . $css; - - return $css; - } - - /** - * Converts hsl() & rgb() colors to HEX format. - * @param $matches - * @return string - */ - private function shortenHslAndRgbToHexCallback($matches) - { - $type = $matches[1]; - $values = explode(',', $matches[2]); - $terminator = $matches[3]; - - if ($type === 'hsl') { - $values = Utils::hslToRgb($values); - } - - $hexColors = Utils::rgbToHex($values); - - // Restore space after rgb() or hsl() function in some cases such as: - // background-image: linear-gradient(to bottom, rgb(210,180,140) 10%, rgb(255,0,0) 90%); - if (!empty($terminator) && !preg_match('/[ ,);]/S', $terminator)) { - $terminator = ' '. $terminator; - } - - return '#'. implode('', $hexColors) . $terminator; - } - - /** - * Compresses HEX color values of the form #AABBCC to #ABC or short color name. - * @param $matches - * @return string - */ - private function shortenHexColorsCallback($matches) - { - $hex = $matches[1]; - - // Shorten suitable 6 chars HEX colors - if (strlen($hex) === 6 && preg_match('/^([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3$/Si', $hex, $m)) { - $hex = $m[1] . $m[2] . $m[3]; - } - - // Lowercase - $hex = '#'. strtolower($hex); - - // Replace Hex colors with shorter color names - $color = array_key_exists($hex, $this->hexToNamedColorsMap) ? $this->hexToNamedColorsMap[$hex] : $hex; - - return $color . $matches[2]; - } - - /** - * Shortens all named colors with a shorter HEX counterpart for a set of safe properties - * e.g. white -> #fff - * @param array $matches - * @return string - */ - private function shortenNamedColorsCallback($matches) - { - return $matches[1] . $this->namedToHexColorsMap[strtolower($matches[2])] . $matches[3]; - } - - /** - * Makes a string lowercase - * @param array $matches - * @return string - */ - private function strtolowerCallback($matches) - { - return strtolower($matches[0]); - } -} diff --git a/lib/css-min/utils.cls.php b/lib/css-min/utils.cls.php deleted file mode 100644 index 36f805b6a..000000000 --- a/lib/css-min/utils.cls.php +++ /dev/null @@ -1,150 +0,0 @@ - 1 ? $vh - 1 : $vh); - - if ($vh * 6 < 1) { - return $v1 + ($v2 - $v1) * 6 * $vh; - } - - if ($vh * 2 < 1) { - return $v2; - } - - if ($vh * 3 < 2) { - return $v1 + ($v2 - $v1) * ((2 / 3) - $vh) * 6; - } - - return $v1; - } - - /** - * Convert strings like "64M" or "30" to int values - * @param mixed $size - * @return int - */ - public static function normalizeInt($size) - { - if (is_string($size)) { - $letter = substr($size, -1); - $size = intval($size); - switch ($letter) { - case 'M': - case 'm': - return (int) $size * 1048576; - case 'K': - case 'k': - return (int) $size * 1024; - case 'G': - case 'g': - return (int) $size * 1073741824; - } - } - return (int) $size; - } - - /** - * Converts a string containing and RGB percentage value into a RGB integer value i.e. '90%' -> 229.5 - * @param $rgbPercentage - * @return int - */ - public static function rgbPercentageToRgbInteger($rgbPercentage) - { - if (strpos($rgbPercentage, '%') !== false) { - $rgbPercentage = self::roundNumber(floatval(str_replace('%', '', $rgbPercentage)) * 2.55); - } - - return intval($rgbPercentage, 10); - } - - /** - * Converts a RGB color into a HEX color - * @param array $rgbColors - * @return array - */ - public static function rgbToHex($rgbColors) - { - $hexColors = array(); - - // Values outside the sRGB color space should be clipped (0-255) - for ($i = 0, $l = count($rgbColors); $i < $l; $i++) { - $hexColors[$i] = sprintf("%02x", self::clampNumberSrgb(self::rgbPercentageToRgbInteger($rgbColors[$i]))); - } - - return $hexColors; - } - - /** - * Rounds a number to its closest integer - * @param $n - * @return int - */ - public static function roundNumber($n) - { - return intval(round(floatval($n)), 10); - } -} diff --git a/lib/css_js_min/minify/LICENSE b/lib/css_js_min/minify/LICENSE new file mode 100644 index 000000000..0c0d08a79 --- /dev/null +++ b/lib/css_js_min/minify/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2012 Matthias Mullie + +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 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. diff --git a/lib/css_js_min/minify/css.cls.php b/lib/css_js_min/minify/css.cls.php new file mode 100644 index 000000000..6ec8893cf --- /dev/null +++ b/lib/css_js_min/minify/css.cls.php @@ -0,0 +1,860 @@ + + * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved + * @license MIT License + */ + +namespace LiteSpeed\Lib\CSS_JS_MIN\Minify; + +use LiteSpeed\Lib\CSS_JS_MIN\Minify\Minify; +use LiteSpeed\Lib\CSS_JS_MIN\Minify\Exception\FileImportException; +use LiteSpeed\Lib\CSS_JS_MIN\PathConverter\Converter; +use LiteSpeed\Lib\CSS_JS_MIN\PathConverter\ConverterInterface; + +defined( 'WPINC' ) || exit ; + +class CSS extends Minify +{ + /** + * @var int maximum inport size in kB + */ + protected $maxImportSize = 5; + + /** + * @var string[] valid import extensions + */ + protected $importExtensions = array( + 'gif' => 'data:image/gif', + 'png' => 'data:image/png', + 'jpe' => 'data:image/jpeg', + 'jpg' => 'data:image/jpeg', + 'jpeg' => 'data:image/jpeg', + 'svg' => 'data:image/svg+xml', + 'woff' => 'data:application/x-font-woff', + 'woff2' => 'data:application/x-font-woff2', + 'avif' => 'data:image/avif', + 'apng' => 'data:image/apng', + 'webp' => 'data:image/webp', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'xbm' => 'image/x-xbitmap', + ); + + /** + * Set the maximum size if files to be imported. + * + * Files larger than this size (in kB) will not be imported into the CSS. + * Importing files into the CSS as data-uri will save you some connections, + * but we should only import relatively small decorative images so that our + * CSS file doesn't get too bulky. + * + * @param int $size Size in kB + */ + public function setMaxImportSize($size) + { + $this->maxImportSize = $size; + } + + /** + * Set the type of extensions to be imported into the CSS (to save network + * connections). + * Keys of the array should be the file extensions & respective values + * should be the data type. + * + * @param string[] $extensions Array of file extensions + */ + public function setImportExtensions(array $extensions) + { + $this->importExtensions = $extensions; + } + + /** + * Move any import statements to the top. + * + * @param string $content Nearly finished CSS content + * + * @return string + */ + public function moveImportsToTop($content) + { + if (preg_match_all('/(;?)(@import (?url\()?(?P["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) { + // remove from content + foreach ($matches[0] as $import) { + $content = str_replace($import, '', $content); + } + + // add to top + $content = implode(';', $matches[2]) . ';' . trim($content, ';'); + } + + return $content; + } + + /** + * Combine CSS from import statements. + * + * \@import's will be loaded and their content merged into the original file, + * to save HTTP requests. + * + * @param string $source The file to combine imports for + * @param string $content The CSS content to combine imports for + * @param string[] $parents Parent paths, for circular reference checks + * + * @return string + * + * @throws FileImportException + */ + protected function combineImports($source, $content, $parents) + { + $importRegexes = array( + // @import url(xxx) + '/ + # import statement + @import + + # whitespace + \s+ + + # open url() + url\( + + # (optional) open path enclosure + (?P["\']?) + + # fetch path + (?P.+?) + + # (optional) close path enclosure + (?P=quotes) + + # close url() + \) + + # (optional) trailing whitespace + \s* + + # (optional) media statement(s) + (?P[^;]*) + + # (optional) trailing whitespace + \s* + + # (optional) closing semi-colon + ;? + + /ix', + + // @import 'xxx' + '/ + + # import statement + @import + + # whitespace + \s+ + + # open path enclosure + (?P["\']) + + # fetch path + (?P.+?) + + # close path enclosure + (?P=quotes) + + # (optional) trailing whitespace + \s* + + # (optional) media statement(s) + (?P[^;]*) + + # (optional) trailing whitespace + \s* + + # (optional) closing semi-colon + ;? + + /ix', + ); + + // find all relative imports in css + $matches = array(); + foreach ($importRegexes as $importRegex) { + if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) { + $matches = array_merge($matches, $regexMatches); + } + } + + $search = array(); + $replace = array(); + + // loop the matches + foreach ($matches as $match) { + // get the path for the file that will be imported + $importPath = dirname($source) . '/' . $match['path']; + + // only replace the import with the content if we can grab the + // content of the file + if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) { + continue; + } + + // check if current file was not imported previously in the same + // import chain. + if (in_array($importPath, $parents)) { + throw new FileImportException('Failed to import file "' . $importPath . '": circular reference detected.'); + } + + // grab referenced file & minify it (which may include importing + // yet other @import statements recursively) + $minifier = new self($importPath); + $minifier->setMaxImportSize($this->maxImportSize); + $minifier->setImportExtensions($this->importExtensions); + $importContent = $minifier->execute($source, $parents); + + // check if this is only valid for certain media + if (!empty($match['media'])) { + $importContent = '@media ' . $match['media'] . '{' . $importContent . '}'; + } + + // add to replacement array + $search[] = $match[0]; + $replace[] = $importContent; + } + + // replace the import statements + return str_replace($search, $replace, $content); + } + + /** + * Import files into the CSS, base64-ized. + * + * @url(image.jpg) images will be loaded and their content merged into the + * original file, to save HTTP requests. + * + * @param string $source The file to import files for + * @param string $content The CSS content to import files for + * + * @return string + */ + protected function importFiles($source, $content) + { + $regex = '/url\((["\']?)(.+?)\\1\)/i'; + if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { + $search = array(); + $replace = array(); + + // loop the matches + foreach ($matches as $match) { + $extension = substr(strrchr($match[2], '.'), 1); + if ($extension && !array_key_exists($extension, $this->importExtensions)) { + continue; + } + + // get the path for the file that will be imported + $path = $match[2]; + $path = dirname($source) . '/' . $path; + + // only replace the import with the content if we're able to get + // the content of the file, and it's relatively small + if ($this->canImportFile($path) && $this->canImportBySize($path)) { + // grab content && base64-ize + $importContent = $this->load($path); + $importContent = base64_encode($importContent); + + // build replacement + $search[] = $match[0]; + $replace[] = 'url(' . $this->importExtensions[$extension] . ';base64,' . $importContent . ')'; + } + } + + // replace the import statements + $content = str_replace($search, $replace, $content); + } + + return $content; + } + + /** + * Minify the data. + * Perform CSS optimizations. + * + * @param string[optional] $path Path to write the data to + * @param string[] $parents Parent paths, for circular reference checks + * + * @return string The minified data + */ + public function execute($path = null, $parents = array()) + { + $content = ''; + + // loop CSS data (raw data and files) + foreach ($this->data as $source => $css) { + /* + * Let's first take out strings & comments, since we can't just + * remove whitespace anywhere. If whitespace occurs inside a string, + * we should leave it alone. E.g.: + * p { content: "a test" } + */ + $this->extractStrings(); + $this->stripComments(); + $this->extractMath(); + $this->extractCustomProperties(); + $css = $this->replace($css); + + $css = $this->stripWhitespace($css); + $css = $this->convertLegacyColors($css); + $css = $this->cleanupModernColors($css); + $css = $this->shortenHEXColors($css); + $css = $this->shortenZeroes($css); + $css = $this->shortenFontWeights($css); + $css = $this->stripEmptyTags($css); + + // restore the string we've extracted earlier + $css = $this->restoreExtractedData($css); + + $source = is_int($source) ? '' : $source; + $parents = $source ? array_merge($parents, array($source)) : $parents; + $css = $this->combineImports($source, $css, $parents); + $css = $this->importFiles($source, $css); + + /* + * If we'll save to a new path, we'll have to fix the relative paths + * to be relative no longer to the source file, but to the new path. + * If we don't write to a file, fall back to same path so no + * conversion happens (because we still want it to go through most + * of the move code, which also addresses url() & @import syntax...) + */ + $converter = $this->getPathConverter($source, $path ?: $source); + $css = $this->move($converter, $css); + + // combine css + $content .= $css; + } + + $content = $this->moveImportsToTop($content); + + return $content; + } + + /** + * Moving a css file should update all relative urls. + * Relative references (e.g. ../images/image.gif) in a certain css file, + * will have to be updated when a file is being saved at another location + * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper). + * + * @param ConverterInterface $converter Relative path converter + * @param string $content The CSS content to update relative urls for + * + * @return string + */ + protected function move(ConverterInterface $converter, $content) + { + /* + * Relative path references will usually be enclosed by url(). @import + * is an exception, where url() is not necessary around the path (but is + * allowed). + * This *could* be 1 regular expression, where both regular expressions + * in this array are on different sides of a |. But we're using named + * patterns in both regexes, the same name on both regexes. This is only + * possible with a (?J) modifier, but that only works after a fairly + * recent PCRE version. That's why I'm doing 2 separate regular + * expressions & combining the matches after executing of both. + */ + $relativeRegexes = array( + // url(xxx) + '/ + # open url() + url\( + + \s* + + # open path enclosure + (?P["\'])? + + # fetch path + (?P.+?) + + # close path enclosure + (?(quotes)(?P=quotes)) + + \s* + + # close url() + \) + + /ix', + + // @import "xxx" + '/ + # import statement + @import + + # whitespace + \s+ + + # we don\'t have to check for @import url(), because the + # condition above will already catch these + + # open path enclosure + (?P["\']) + + # fetch path + (?P.+?) + + # close path enclosure + (?P=quotes) + + /ix', + ); + + // find all relative urls in css + $matches = array(); + foreach ($relativeRegexes as $relativeRegex) { + if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) { + $matches = array_merge($matches, $regexMatches); + } + } + + $search = array(); + $replace = array(); + + // loop all urls + foreach ($matches as $match) { + // determine if it's a url() or an @import match + $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url'); + + $url = $match['path']; + if ($this->canImportByPath($url)) { + // attempting to interpret GET-params makes no sense, so let's discard them for awhile + $params = strrchr($url, '?'); + $url = $params ? substr($url, 0, -strlen($params)) : $url; + + // fix relative url + $url = $converter->convert($url); + + // now that the path has been converted, re-apply GET-params + $url .= $params; + } + + /* + * Urls with control characters above 0x7e should be quoted. + * According to Mozilla's parser, whitespace is only allowed at the + * end of unquoted urls. + * Urls with `)` (as could happen with data: uris) should also be + * quoted to avoid being confused for the url() closing parentheses. + * And urls with a # have also been reported to cause issues. + * Urls with quotes inside should also remain escaped. + * + * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation + * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378 + * @see https://github.com/matthiasmullie/minify/issues/193 + */ + $url = trim($url); + if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) { + $url = $match['quotes'] . $url . $match['quotes']; + } + + // build replacement + $search[] = $match[0]; + if ($type === 'url') { + $replace[] = 'url(' . $url . ')'; + } elseif ($type === 'import') { + $replace[] = '@import "' . $url . '"'; + } + } + + // replace urls + return str_replace($search, $replace, $content); + } + + /** + * Shorthand HEX color codes. + * #FF0000FF -> #f00 -> red + * #FF00FF00 -> transparent. + * + * @param string $content The CSS content to shorten the HEX color codes for + * + * @return string + */ + protected function shortenHexColors($content) + { + // shorten repeating patterns within HEX .. + $content = preg_replace('/(?<=[: ])#([0-9a-f])\\1([0-9a-f])\\2([0-9a-f])\\3(?:([0-9a-f])\\4)?(?=[; }])/i', '#$1$2$3$4', $content); + + // remove alpha channel if it's pointless .. + $content = preg_replace('/(?<=[: ])#([0-9a-f]{6})ff(?=[; }])/i', '#$1', $content); + $content = preg_replace('/(?<=[: ])#([0-9a-f]{3})f(?=[; }])/i', '#$1', $content); + + // replace `transparent` with shortcut .. + $content = preg_replace('/(?<=[: ])#[0-9a-f]{6}00(?=[; }])/i', '#fff0', $content); + + $colors = array( + // make these more readable + '#00f' => 'blue', + '#dc143c' => 'crimson', + '#0ff' => 'cyan', + '#8b0000' => 'darkred', + '#696969' => 'dimgray', + '#ff69b4' => 'hotpink', + '#0f0' => 'lime', + '#fdf5e6' => 'oldlace', + '#87ceeb' => 'skyblue', + '#d8bfd8' => 'thistle', + // we can shorten some even more by replacing them with their color name + '#f0ffff' => 'azure', + '#f5f5dc' => 'beige', + '#ffe4c4' => 'bisque', + '#a52a2a' => 'brown', + '#ff7f50' => 'coral', + '#ffd700' => 'gold', + '#808080' => 'gray', + '#008000' => 'green', + '#4b0082' => 'indigo', + '#fffff0' => 'ivory', + '#f0e68c' => 'khaki', + '#faf0e6' => 'linen', + '#800000' => 'maroon', + '#000080' => 'navy', + '#808000' => 'olive', + '#ffa500' => 'orange', + '#da70d6' => 'orchid', + '#cd853f' => 'peru', + '#ffc0cb' => 'pink', + '#dda0dd' => 'plum', + '#800080' => 'purple', + '#f00' => 'red', + '#fa8072' => 'salmon', + '#a0522d' => 'sienna', + '#c0c0c0' => 'silver', + '#fffafa' => 'snow', + '#d2b48c' => 'tan', + '#008080' => 'teal', + '#ff6347' => 'tomato', + '#ee82ee' => 'violet', + '#f5deb3' => 'wheat', + // or the other way around + 'black' => '#000', + 'fuchsia' => '#f0f', + 'magenta' => '#f0f', + 'white' => '#fff', + 'yellow' => '#ff0', + // and also `transparent` + 'transparent' => '#fff0', + ); + + return preg_replace_callback( + '/(?<=[: ])(' . implode('|', array_keys($colors)) . ')(?=[; }])/i', + function ($match) use ($colors) { + return $colors[strtolower($match[0])]; + }, + $content + ); + } + + /** + * Convert RGB|HSL color codes. + * rgb(255,0,0,.5) -> rgb(255 0 0 / .5). + * rgb(255,0,0) -> #f00. + * + * @param string $content The CSS content to shorten the RGB color codes for + * + * @return string + */ + protected function convertLegacyColors($content) + { + /* + https://drafts.csswg.org/css-color/#color-syntax-legacy + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl + */ + + // convert legacy color syntax + $content = preg_replace('/(rgb)a?\(\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0,1]?(?:\.[0-9]*)?)\s*\)/i', '$1($2 $3 $4 / $5)', $content); + $content = preg_replace('/(rgb)a?\(\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*\)/i', '$1($2 $3 $4)', $content); + $content = preg_replace('/(hsl)a?\(\s*([0-9]+(?:deg|grad|rad|turn)?)\s*,\s*([0-9]{1,3}%)\s*,\s*([0-9]{1,3}%)\s*,\s*([0,1]?(?:\.[0-9]*)?)\s*\)/i', '$1($2 $3 $4 / $5)', $content); + $content = preg_replace('/(hsl)a?\(\s*([0-9]+(?:deg|grad|rad|turn)?)\s*,\s*([0-9]{1,3}%)\s*,\s*([0-9]{1,3}%)\s*\)/i', '$1($2 $3 $4)', $content); + + // convert `rgb` to `hex` + $dec = '([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])'; + return preg_replace_callback( + "/rgb\($dec $dec $dec\)/i", + function ($match) { + return sprintf('#%02x%02x%02x', $match[1], $match[2], $match[3]); + }, + $content + ); + } + + /** + * Cleanup RGB|HSL|HWB|LCH|LAB + * rgb(255 0 0 / 1) -> rgb(255 0 0). + * rgb(255 0 0 / 0) -> transparent. + * + * @param string $content The CSS content to cleanup HSL|HWB|LCH|LAB + * + * @return string + */ + protected function cleanupModernColors($content) + { + /* + https://drafts.csswg.org/css-color/#color-syntax-modern + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hwb + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lch + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lab + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklab + */ + $tag = '(rgb|hsl|hwb|(?:(?:ok)?(?:lch|lab)))'; + + // remove alpha channel if it's pointless .. + $content = preg_replace('/' . $tag . '\(\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+\/\s+1(?:(?:\.\d?)*|00%)?\s*\)/i', '$1($2 $3 $4)', $content); + + // replace `transparent` with shortcut .. + $content = preg_replace('/' . $tag . '\(\s*[^\s]+\s+[^\s]+\s+[^\s]+\s+\/\s+0(?:[\.0%]*)?\s*\)/i', '#fff0', $content); + + return $content; + } + + /** + * Shorten CSS font weights. + * + * @param string $content The CSS content to shorten the font weights for + * + * @return string + */ + protected function shortenFontWeights($content) + { + $weights = array( + 'normal' => 400, + 'bold' => 700, + ); + + $callback = function ($match) use ($weights) { + return $match[1] . $weights[$match[2]]; + }; + + return preg_replace_callback('/(font-weight\s*:\s*)(' . implode('|', array_keys($weights)) . ')(?=[;}])/', $callback, $content); + } + + /** + * Shorthand 0 values to plain 0, instead of e.g. -0em. + * + * @param string $content The CSS content to shorten the zero values for + * + * @return string + */ + protected function shortenZeroes($content) + { + // we don't want to strip units in `calc()` expressions: + // `5px - 0px` is valid, but `5px - 0` is not + // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but + // `10 * 0` is invalid + // we've extracted calcs earlier, so we don't need to worry about this + + // reusable bits of code throughout these regexes: + // before & after are used to make sure we don't match lose unintended + // 0-like values (e.g. in #000, or in http://url/1.0) + // units can be stripped from 0 values, or used to recognize non 0 + // values (where wa may be able to strip a .0 suffix) + $before = '(?<=[:(, ])'; + $after = '(?=[ ,);}])'; + $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)'; + + // strip units after zeroes (0px -> 0) + // NOTE: it should be safe to remove all units for a 0 value, but in + // practice, Webkit (especially Safari) seems to stumble over at least + // 0%, potentially other units as well. Only stripping 'px' for now. + // @see https://github.com/matthiasmullie/minify/issues/60 + $content = preg_replace('/' . $before . '(-?0*(\.0+)?)(?<=0)px' . $after . '/', '\\1', $content); + + // strip 0-digits (.0 -> 0) + $content = preg_replace('/' . $before . '\.0+' . $units . '?' . $after . '/', '0\\1', $content); + // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px + $content = preg_replace('/' . $before . '(-?[0-9]+\.[0-9]+)0+' . $units . '?' . $after . '/', '\\1\\2', $content); + // strip trailing 0: 50.00 -> 50, 50.00px -> 50px + $content = preg_replace('/' . $before . '(-?[0-9]+)\.0+' . $units . '?' . $after . '/', '\\1\\2', $content); + // strip leading 0: 0.1 -> .1, 01.1 -> 1.1 + $content = preg_replace('/' . $before . '(-?)0+([0-9]*\.[0-9]+)' . $units . '?' . $after . '/', '\\1\\2\\3', $content); + + // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0) + $content = preg_replace('/' . $before . '-?0+' . $units . '?' . $after . '/', '0\\1', $content); + + // IE doesn't seem to understand a unitless flex-basis value (correct - + // it goes against the spec), so let's add it in again (make it `%`, + // which is only 1 char: 0%, 0px, 0 anything, it's all just the same) + // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex + $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content); + $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content); + + return $content; + } + + /** + * Strip empty tags from source code. + * + * @param string $content + * + * @return string + */ + protected function stripEmptyTags($content) + { + $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content); + $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content); + + return $content; + } + + /** + * Strip comments from source code. + */ + protected function stripComments() + { + $this->stripMultilineComments(); + } + + /** + * Strip whitespace. + * + * @param string $content The CSS content to strip the whitespace for + * + * @return string + */ + protected function stripWhitespace($content) + { + // remove leading & trailing whitespace + $content = preg_replace('/^\s*/m', '', $content); + $content = preg_replace('/\s*$/m', '', $content); + + // replace newlines with a single space + $content = preg_replace('/\s+/', ' ', $content); + + // remove whitespace around meta characters + // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex + $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content); + $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content); + $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content); + $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content); + + // whitespace around + and - can only be stripped inside some pseudo- + // classes, like `:nth-child(3+2n)` + // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or + // selectors like `div.weird- p` + $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type'); + $content = preg_replace('/:(' . implode('|', $pseudos) . ')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content); + + // remove semicolon/whitespace followed by closing bracket + $content = str_replace(';}', '}', $content); + + return trim($content); + } + + /** + * Replace all occurrences of functions that may contain math, where + * whitespace around operators needs to be preserved (e.g. calc, clamp). + */ + protected function extractMath() + { + $functions = array('calc', 'clamp', 'min', 'max'); + $pattern = '/\b(' . implode('|', $functions) . ')(\(.+?)(?=$|;|})/m'; + + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $callback = function ($match) use ($minifier, $pattern, &$callback) { + $function = $match[1]; + $length = strlen($match[2]); + $expr = ''; + $opened = 0; + + // the regular expression for extracting math has 1 significant problem: + // it can't determine the correct closing parenthesis... + // instead, it'll match a larger portion of code to where it's certain that + // the calc() musts have ended, and we'll figure out which is the correct + // closing parenthesis here, by counting how many have opened + for ($i = 0; $i < $length; ++$i) { + $char = $match[2][$i]; + $expr .= $char; + if ($char === '(') { + ++$opened; + } elseif ($char === ')' && --$opened === 0) { + break; + } + } + + // now that we've figured out where the calc() starts and ends, extract it + $count = count($minifier->extracted); + $placeholder = 'math(' . $count . ')'; + $minifier->extracted[$placeholder] = $function . '(' . trim(substr($expr, 1, -1)) . ')'; + + // and since we've captured more code than required, we may have some leftover + // calc() in here too - go recursive on the remaining but of code to go figure + // that out and extract what is needed + $rest = $minifier->str_replace_first($function . $expr, '', $match[0]); + $rest = preg_replace_callback($pattern, $callback, $rest); + + return $placeholder . $rest; + }; + + $this->registerPattern($pattern, $callback); + } + + /** + * Replace custom properties, whose values may be used in scenarios where + * we wouldn't want them to be minified (e.g. inside calc). + */ + protected function extractCustomProperties() + { + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $this->registerPattern( + '/(?<=^|[;}{])\s*(--[^:;{}"\'\s]+)\s*:([^;{}]+)/m', + function ($match) use ($minifier) { + $placeholder = '--custom-' . count($minifier->extracted) . ':0'; + $minifier->extracted[$placeholder] = $match[1] . ':' . trim($match[2]); + + return $placeholder; + } + ); + } + + /** + * Check if file is small enough to be imported. + * + * @param string $path The path to the file + * + * @return bool + */ + protected function canImportBySize($path) + { + return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024; + } + + /** + * Check if file a file can be imported, going by the path. + * + * @param string $path + * + * @return bool + */ + protected function canImportByPath($path) + { + return preg_match('/^(data:|https?:|\\/)/', $path) === 0; + } + + /** + * Return a converter to update relative paths to be relative to the new + * destination. + * + * @param string $source + * @param string $target + * + * @return ConverterInterface + */ + protected function getPathConverter($source, $target) + { + return new Converter($source, $target); + } +} diff --git a/lib/jsmin_data/keywords_after.txt b/lib/css_js_min/minify/data/js/keywords_after.txt similarity index 100% rename from lib/jsmin_data/keywords_after.txt rename to lib/css_js_min/minify/data/js/keywords_after.txt diff --git a/lib/jsmin_data/keywords_before.txt b/lib/css_js_min/minify/data/js/keywords_before.txt similarity index 100% rename from lib/jsmin_data/keywords_before.txt rename to lib/css_js_min/minify/data/js/keywords_before.txt diff --git a/lib/jsmin_data/keywords_reserved.txt b/lib/css_js_min/minify/data/js/keywords_reserved.txt similarity index 100% rename from lib/jsmin_data/keywords_reserved.txt rename to lib/css_js_min/minify/data/js/keywords_reserved.txt diff --git a/lib/jsmin_data/operators.txt b/lib/css_js_min/minify/data/js/operators.txt similarity index 100% rename from lib/jsmin_data/operators.txt rename to lib/css_js_min/minify/data/js/operators.txt diff --git a/lib/jsmin_data/operators_after.txt b/lib/css_js_min/minify/data/js/operators_after.txt similarity index 100% rename from lib/jsmin_data/operators_after.txt rename to lib/css_js_min/minify/data/js/operators_after.txt diff --git a/lib/jsmin_data/operators_before.txt b/lib/css_js_min/minify/data/js/operators_before.txt similarity index 100% rename from lib/jsmin_data/operators_before.txt rename to lib/css_js_min/minify/data/js/operators_before.txt diff --git a/lib/css_js_min/minify/exception.cls.php b/lib/css_js_min/minify/exception.cls.php new file mode 100644 index 000000000..3ef6df98f --- /dev/null +++ b/lib/css_js_min/minify/exception.cls.php @@ -0,0 +1,25 @@ + + */ + +namespace LiteSpeed\Lib\CSS_JS_MIN\Minify\Exception; + +defined( 'WPINC' ) || exit ; + +abstract class Exception extends \Exception +{ +} + +abstract class BasicException extends Exception +{ +} + +class FileImportException extends BasicException +{ +} + +class IOException extends BasicException +{ +} \ No newline at end of file diff --git a/lib/jsmin.cls.php b/lib/css_js_min/minify/js.cls.php similarity index 74% rename from lib/jsmin.cls.php rename to lib/css_js_min/minify/js.cls.php index 0f41b5859..74aa59b02 100644 --- a/lib/jsmin.cls.php +++ b/lib/css_js_min/minify/js.cls.php @@ -1,23 +1,17 @@ - * @author Tijs Verkoyen * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ -namespace LiteSpeed\Lib ; +namespace LiteSpeed\Lib\CSS_JS_MIN\Minify; defined( 'WPINC' ) || exit ; -class JSMin +class JS extends Minify { /** * Var-matching regex based on http://stackoverflow.com/a/9337047/802993. @@ -25,26 +19,12 @@ class JSMin * Note that regular expressions using that bit must have the PCRE_UTF8 * pattern modifier (/u) set. * + * @internal + * * @var string */ const REGEX_VARIABLE = '\b[$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}][$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}0-9\x{0300}-\x{036f}\x{0483}-\x{0487}\x{0591}-\x{05bd}\x{05bf}\x{05c1}\x{05c2}\x{05c4}\x{05c5}\x{05c7}\x{0610}-\x{061a}\x{064b}-\x{0669}\x{0670}\x{06d6}-\x{06dc}\x{06df}-\x{06e4}\x{06e7}\x{06e8}\x{06ea}-\x{06ed}\x{06f0}-\x{06f9}\x{0711}\x{0730}-\x{074a}\x{07a6}-\x{07b0}\x{07c0}-\x{07c9}\x{07eb}-\x{07f3}\x{0816}-\x{0819}\x{081b}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082d}\x{0859}-\x{085b}\x{08e4}-\x{08fe}\x{0900}-\x{0903}\x{093a}-\x{093c}\x{093e}-\x{094f}\x{0951}-\x{0957}\x{0962}\x{0963}\x{0966}-\x{096f}\x{0981}-\x{0983}\x{09bc}\x{09be}-\x{09c4}\x{09c7}\x{09c8}\x{09cb}-\x{09cd}\x{09d7}\x{09e2}\x{09e3}\x{09e6}-\x{09ef}\x{0a01}-\x{0a03}\x{0a3c}\x{0a3e}-\x{0a42}\x{0a47}\x{0a48}\x{0a4b}-\x{0a4d}\x{0a51}\x{0a66}-\x{0a71}\x{0a75}\x{0a81}-\x{0a83}\x{0abc}\x{0abe}-\x{0ac5}\x{0ac7}-\x{0ac9}\x{0acb}-\x{0acd}\x{0ae2}\x{0ae3}\x{0ae6}-\x{0aef}\x{0b01}-\x{0b03}\x{0b3c}\x{0b3e}-\x{0b44}\x{0b47}\x{0b48}\x{0b4b}-\x{0b4d}\x{0b56}\x{0b57}\x{0b62}\x{0b63}\x{0b66}-\x{0b6f}\x{0b82}\x{0bbe}-\x{0bc2}\x{0bc6}-\x{0bc8}\x{0bca}-\x{0bcd}\x{0bd7}\x{0be6}-\x{0bef}\x{0c01}-\x{0c03}\x{0c3e}-\x{0c44}\x{0c46}-\x{0c48}\x{0c4a}-\x{0c4d}\x{0c55}\x{0c56}\x{0c62}\x{0c63}\x{0c66}-\x{0c6f}\x{0c82}\x{0c83}\x{0cbc}\x{0cbe}-\x{0cc4}\x{0cc6}-\x{0cc8}\x{0cca}-\x{0ccd}\x{0cd5}\x{0cd6}\x{0ce2}\x{0ce3}\x{0ce6}-\x{0cef}\x{0d02}\x{0d03}\x{0d3e}-\x{0d44}\x{0d46}-\x{0d48}\x{0d4a}-\x{0d4d}\x{0d57}\x{0d62}\x{0d63}\x{0d66}-\x{0d6f}\x{0d82}\x{0d83}\x{0dca}\x{0dcf}-\x{0dd4}\x{0dd6}\x{0dd8}-\x{0ddf}\x{0df2}\x{0df3}\x{0e31}\x{0e34}-\x{0e3a}\x{0e47}-\x{0e4e}\x{0e50}-\x{0e59}\x{0eb1}\x{0eb4}-\x{0eb9}\x{0ebb}\x{0ebc}\x{0ec8}-\x{0ecd}\x{0ed0}-\x{0ed9}\x{0f18}\x{0f19}\x{0f20}-\x{0f29}\x{0f35}\x{0f37}\x{0f39}\x{0f3e}\x{0f3f}\x{0f71}-\x{0f84}\x{0f86}\x{0f87}\x{0f8d}-\x{0f97}\x{0f99}-\x{0fbc}\x{0fc6}\x{102b}-\x{103e}\x{1040}-\x{1049}\x{1056}-\x{1059}\x{105e}-\x{1060}\x{1062}-\x{1064}\x{1067}-\x{106d}\x{1071}-\x{1074}\x{1082}-\x{108d}\x{108f}-\x{109d}\x{135d}-\x{135f}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}\x{1753}\x{1772}\x{1773}\x{17b4}-\x{17d3}\x{17dd}\x{17e0}-\x{17e9}\x{180b}-\x{180d}\x{1810}-\x{1819}\x{18a9}\x{1920}-\x{192b}\x{1930}-\x{193b}\x{1946}-\x{194f}\x{19b0}-\x{19c0}\x{19c8}\x{19c9}\x{19d0}-\x{19d9}\x{1a17}-\x{1a1b}\x{1a55}-\x{1a5e}\x{1a60}-\x{1a7c}\x{1a7f}-\x{1a89}\x{1a90}-\x{1a99}\x{1b00}-\x{1b04}\x{1b34}-\x{1b44}\x{1b50}-\x{1b59}\x{1b6b}-\x{1b73}\x{1b80}-\x{1b82}\x{1ba1}-\x{1bad}\x{1bb0}-\x{1bb9}\x{1be6}-\x{1bf3}\x{1c24}-\x{1c37}\x{1c40}-\x{1c49}\x{1c50}-\x{1c59}\x{1cd0}-\x{1cd2}\x{1cd4}-\x{1ce8}\x{1ced}\x{1cf2}-\x{1cf4}\x{1dc0}-\x{1de6}\x{1dfc}-\x{1dff}\x{200c}\x{200d}\x{203f}\x{2040}\x{2054}\x{20d0}-\x{20dc}\x{20e1}\x{20e5}-\x{20f0}\x{2cef}-\x{2cf1}\x{2d7f}\x{2de0}-\x{2dff}\x{302a}-\x{302f}\x{3099}\x{309a}\x{a620}-\x{a629}\x{a66f}\x{a674}-\x{a67d}\x{a69f}\x{a6f0}\x{a6f1}\x{a802}\x{a806}\x{a80b}\x{a823}-\x{a827}\x{a880}\x{a881}\x{a8b4}-\x{a8c4}\x{a8d0}-\x{a8d9}\x{a8e0}-\x{a8f1}\x{a900}-\x{a909}\x{a926}-\x{a92d}\x{a947}-\x{a953}\x{a980}-\x{a983}\x{a9b3}-\x{a9c0}\x{a9d0}-\x{a9d9}\x{aa29}-\x{aa36}\x{aa43}\x{aa4c}\x{aa4d}\x{aa50}-\x{aa59}\x{aa7b}\x{aab0}\x{aab2}-\x{aab4}\x{aab7}\x{aab8}\x{aabe}\x{aabf}\x{aac1}\x{aaeb}-\x{aaef}\x{aaf5}\x{aaf6}\x{abe3}-\x{abea}\x{abec}\x{abed}\x{abf0}-\x{abf9}\x{fb1e}\x{fe00}-\x{fe0f}\x{fe20}-\x{fe26}\x{fe33}\x{fe34}\x{fe4d}-\x{fe4f}\x{ff10}-\x{ff19}\x{ff3f}]*\b'; - /** - * Array of patterns to match. - * - * @var string[] - */ - protected $patterns = array(); - - /** - * This array will hold content of strings and regular expressions that have - * been extracted from the JS source code, so we can reliably match "code", - * without having to worry about potential "code-like" characters inside. - * - * @var string[] - */ - public $extracted = array(); - /** * Full list of JavaScript reserved words. * Will be loaded from /data/js/keywords_reserved.txt. @@ -131,19 +111,18 @@ class JSMin */ protected $operatorsAfter = array(); - /** - * {@inheritdoc} - */ public function __construct() { - $dataDir = __DIR__.'/jsmin_data/'; + call_user_func_array(array('\\LiteSpeed\\Lib\\CSS_JS_MIN\\Minify\\Minify', '__construct'), func_get_args()); + + $dataDir = __DIR__ . '/data/js/'; $options = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES; - $this->keywordsReserved = file($dataDir.'keywords_reserved.txt', $options); - $this->keywordsBefore = file($dataDir.'keywords_before.txt', $options); - $this->keywordsAfter = file($dataDir.'keywords_after.txt', $options); - $this->operators = file($dataDir.'operators.txt', $options); - $this->operatorsBefore = file($dataDir.'operators_before.txt', $options); - $this->operatorsAfter = file($dataDir.'operators_after.txt', $options); + $this->keywordsReserved = file($dataDir . 'keywords_reserved.txt', $options); + $this->keywordsBefore = file($dataDir . 'keywords_before.txt', $options); + $this->keywordsAfter = file($dataDir . 'keywords_after.txt', $options); + $this->operators = file($dataDir . 'operators.txt', $options); + $this->operatorsBefore = file($dataDir . 'operators_before.txt', $options); + $this->operatorsAfter = file($dataDir . 'operators_after.txt', $options); } /** @@ -154,16 +133,10 @@ public function __construct() * * @return string The minified data */ - public static function minify($js) - { - $jsmin = new JSMin(); - return $jsmin->min($js); - } - - public function min($js) + public function execute($path = null) { + $content = ''; - $output = ''; /* * Let's first take out strings, comments and regular expressions. * All of these can contain JS code-like characters, and we should make @@ -179,80 +152,31 @@ public function min($js) $this->stripComments(); $this->extractRegex(); - // take out strings, comments & regex (for which we've registered - // the regexes just a few lines earlier) - $js = $this->replace($js); + // loop files + foreach ($this->data as $source => $js) { + // take out strings, comments & regex (for which we've registered + // the regexes just a few lines earlier) + $js = $this->replace($js); - $js = $this->propertyNotation($js); - $js = $this->shortenBools($js); - $js = $this->stripWhitespace($js); + $js = $this->propertyNotation($js); + $js = $this->shortenBools($js); + $js = $this->stripWhitespace($js); - // combine js: separating the scripts by a ; - $output .= $js.";"; + // combine js: separating the scripts by a ; + $content .= $js . ';'; + } // clean up leftover `;`s from the combination of multiple scripts - $output = ltrim($output, ';'); - $output = (string) substr($output, 0, -1); + $content = ltrim($content, ';'); + $content = (string) substr($content, 0, -1); /* * Earlier, we extracted strings & regular expressions and replaced them * with placeholder text. This will restore them. */ - $output = $this->restoreExtractedData($output); - - return $output; - } - - /** - * Strings are a pattern we need to match, in order to ignore potential - * code-like content inside them, but we just want all of the string - * content to remain untouched. - * - * This method will replace all string content with simple STRING# - * placeholder text, so we've rid all strings from characters that may be - * misinterpreted. Original string content will be saved in $this->extracted - * and after doing all other minifying, we can restore the original content - * via restoreStrings(). - * - * @param string[optional] $chars - * @param string[optional] $placeholderPrefix - */ - protected function extractStrings($chars = '\'"', $placeholderPrefix = '') - { - // PHP only supports $this inside anonymous functions since 5.4 - $minifier = $this; - $callback = function ($match) use ($minifier, $placeholderPrefix) { - // check the second index here, because the first always contains a quote - if ($match[2] === '') { - /* - * Empty strings need no placeholder; they can't be confused for - * anything else anyway. - * But we still needed to match them, for the extraction routine - * to skip over this particular string. - */ - return $match[0]; - } - - $count = count($minifier->extracted); - $placeholder = $match[1].$placeholderPrefix.$count.$match[1]; - $minifier->extracted[$placeholder] = $match[1].$match[2].$match[1]; + $content = $this->restoreExtractedData($content); - return $placeholder; - }; - - /* - * The \\ messiness explained: - * * Don't count ' or " as end-of-string if it's escaped (has backslash - * in front of it) - * * Unless... that backslash itself is escaped (another leading slash), - * in which case it's no longer escaping the ' or " - * * So there can be either no backslash, or an even number - * * multiply all of that times 4, to account for the escaping that has - * to be done to pass the backslash into the PHP string without it being - * considered as escape-char (times 2) and to get it in the regex, - * escaped (times 2) - */ - $this->registerPattern('/(['.$chars.'])(.*?(?extracted); - $placeholder = '/*'.$count.'*/'; - $minifier->extracted[$placeholder] = $match[0]; - - return $placeholder; - } - - return ''; - }; - - // multi-line comments - $this->registerPattern('/\n?\/\*(.*?)\*\/\n?/s', $callback); + $this->stripMultilineComments(); // single-line comments $this->registerPattern('/\/\/.*$/m', ''); @@ -310,7 +213,7 @@ protected function extractRegex() $minifier = $this; $callback = function ($match) use ($minifier) { $count = count($minifier->extracted); - $placeholder = '"'.$count.'"'; + $placeholder = '"' . $count . '"'; $minifier->extracted[$placeholder] = $match[0]; return $placeholder; @@ -329,7 +232,7 @@ protected function extractRegex() // of the RegExp methods (a `\` followed by a variable or value is // likely part of a division, not a regex) $keywords = array('do', 'in', 'new', 'else', 'throw', 'yield', 'delete', 'return', 'typeof'); - $before = '(^|[=:,;\+\-\*\/\}\(\{\[&\|!]|'.implode('|', $keywords).')\s*'; + $before = '(^|[=:,;\+\-\*\?\/\}\(\{\[&\|!]|' . implode('|', $keywords) . ')\s*'; $propertiesAndMethods = array( // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Properties_2 'constructor', @@ -349,8 +252,8 @@ protected function extractRegex() ); $delimiters = array_fill(0, count($propertiesAndMethods), '/'); $propertiesAndMethods = array_map('preg_quote', $propertiesAndMethods, $delimiters); - $after = '(?=\s*([\.,;\)\}&\|+]|\/\/|$|\.('.implode('|', $propertiesAndMethods).')))'; - $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback); + $after = '(?=\s*([\.,;:\)\}&\|+]|\/\/|$|\.(' . implode('|', $propertiesAndMethods) . ')))'; + $this->registerPattern('/' . $before . '\K' . $pattern . $after . '/', $callback); // regular expressions following a `)` are rather annoying to detect... // quite often, `/` after `)` is a division operator & if it happens to @@ -364,8 +267,8 @@ protected function extractRegex() // if a regex following `)` is not followed by `.`, // it's quite likely not a regex $before = '\)\s*'; - $after = '(?=\s*\.('.implode('|', $propertiesAndMethods).'))'; - $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback); + $after = '(?=\s*\.(' . implode('|', $propertiesAndMethods) . '))'; + $this->registerPattern('/' . $before . '\K' . $pattern . $after . '/', $callback); // 1 more edge case: a regex can be followed by a lot more operators or // keywords if there's a newline (ASI) in between, where the operator @@ -373,205 +276,8 @@ protected function extractRegex() // (https://github.com/matthiasmullie/minify/issues/56) $operators = $this->getOperatorsForRegex($this->operatorsBefore, '/'); $operators += $this->getOperatorsForRegex($this->keywordsReserved, '/'); - $after = '(?=\s*\n\s*('.implode('|', $operators).'))'; - $this->registerPattern('/'.$pattern.$after.'/', $callback); - } - - /** - * We can't "just" run some regular expressions against JavaScript: it's a - * complex language. E.g. having an occurrence of // xyz would be a comment, - * unless it's used within a string. Of you could have something that looks - * like a 'string', but inside a comment. - * The only way to accurately replace these pieces is to traverse the JS one - * character at a time and try to find whatever starts first. - * - * @param string $content The content to replace patterns in - * - * @return string The (manipulated) content - */ - protected function replace($content) - { - $contentLength = strlen($content); - $output = ''; - $processedOffset = 0; - $positions = array_fill(0, count($this->patterns), -1); - $matches = array(); - - while ($processedOffset < $contentLength) { - // find first match for all patterns - foreach ($this->patterns as $i => $pattern) { - list($pattern, $replacement) = $pattern; - - // we can safely ignore patterns for positions we've unset earlier, - // because we know these won't show up anymore - if (array_key_exists($i, $positions) == false) { - continue; - } - - // no need to re-run matches that are still in the part of the - // content that hasn't been processed - if ($positions[$i] >= $processedOffset) { - continue; - } - - $match = null; - if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset)) { - $matches[$i] = $match; - - // we'll store the match position as well; that way, we - // don't have to redo all preg_matches after changing only - // the first (we'll still know where those others are) - $positions[$i] = $match[0][1]; - } else { - // if the pattern couldn't be matched, there's no point in - // executing it again in later runs on this same content; - // ignore this one until we reach end of content - unset($matches[$i], $positions[$i]); - } - } - - // no more matches to find: everything's been processed, break out - if (!$matches) { - // output the remaining content - $output .= substr($content, $processedOffset); - break; - } - - // see which of the patterns actually found the first thing (we'll - // only want to execute that one, since we're unsure if what the - // other found was not inside what the first found) - $matchOffset = min($positions); - $firstPattern = array_search($matchOffset, $positions); - $match = $matches[$firstPattern]; - - // execute the pattern that matches earliest in the content string - list(, $replacement) = $this->patterns[$firstPattern]; - - // add the part of the input between $processedOffset and the first match; - // that content wasn't matched by anything - $output .= substr($content, $processedOffset, $matchOffset - $processedOffset); - // add the replacement for the match - $output .= $this->executeReplacement($replacement, $match); - // advance $processedOffset past the match - $processedOffset = $matchOffset + strlen($match[0][0]); - } - - return $output; - } - - /** - * Replaces all occurrences of array['key'] by array.key. - * - * @param string $content - * - * @return string - */ - protected function propertyNotation($content) - { - // PHP only supports $this inside anonymous functions since 5.4 - $minifier = $this; - $keywords = $this->keywordsReserved; - $callback = function ($match) use ($minifier, $keywords) { - $property = trim($minifier->extracted[$match[1]], '\'"'); - - /* - * Check if the property is a reserved keyword. In this context (as - * property of an object literal/array) it shouldn't matter, but IE8 - * freaks out with "Expected identifier". - */ - if (in_array($property, $keywords)) { - return $match[0]; - } - - /* - * See if the property is in a variable-like format (e.g. - * array['key-here'] can't be replaced by array.key-here since '-' - * is not a valid character there. - */ - if (!preg_match('/^'.$minifier::REGEX_VARIABLE.'$/u', $property)) { - return $match[0]; - } - - return '.'.$property; - }; - - /* - * Figure out if previous character is a variable name (of the array - * we want to use property notation on) - this is to make sure - * standalone ['value'] arrays aren't confused for keys-of-an-array. - * We can (and only have to) check the last character, because PHP's - * regex implementation doesn't allow unfixed-length look-behind - * assertions. - */ - preg_match('/(\[[^\]]+\])[^\]]*$/', static::REGEX_VARIABLE, $previousChar); - $previousChar = $previousChar[1]; - - /* - * Make sure word preceding the ['value'] is not a keyword, e.g. - * return['x']. Because -again- PHP's regex implementation doesn't allow - * unfixed-length look-behind assertions, I'm just going to do a lot of - * separate look-behind assertions, one for each keyword. - */ - $keywords = $this->getKeywordsForRegex($keywords); - $keywords = '(?registerPattern('/' . $pattern . $after . '/', $callback); } /** @@ -615,8 +321,8 @@ protected function stripWhitespace($content) unset($operatorsBefore['+'], $operatorsBefore['-'], $operatorsAfter['+'], $operatorsAfter['-']); $content = preg_replace( array( - '/('.implode('|', $operatorsBefore).')\s+/', - '/\s+('.implode('|', $operatorsAfter).')/', + '/(' . implode('|', $operatorsBefore) . ')\s+/', + '/\s+(' . implode('|', $operatorsAfter) . ')/', ), '\\1', $content @@ -633,8 +339,8 @@ protected function stripWhitespace($content) ); // collapse whitespace around reserved words into single space - $content = preg_replace('/(^|[;\}\s])\K('.implode('|', $keywordsBefore).')\s+/', '\\2 ', $content); - $content = preg_replace('/\s+('.implode('|', $keywordsAfter).')(?=([;\{\s]|$))/', ' \\1', $content); + $content = preg_replace('/(^|[;\}\s])\K(' . implode('|', $keywordsBefore) . ')\s+/', '\\2 ', $content); + $content = preg_replace('/\s+(' . implode('|', $keywordsAfter) . ')(?=([;\{\s]|$))/', ' \\1', $content); /* * We didn't strip whitespace after a couple of operators because they @@ -644,8 +350,8 @@ protected function stripWhitespace($content) */ $operatorsDiffBefore = array_diff($operators, $operatorsBefore); $operatorsDiffAfter = array_diff($operators, $operatorsAfter); - $content = preg_replace('/('.implode('|', $operatorsDiffBefore).')[^\S\n]+/', '\\1', $content); - $content = preg_replace('/[^\S\n]+('.implode('|', $operatorsDiffAfter).')/', '\\1', $content); + $content = preg_replace('/(' . implode('|', $operatorsDiffBefore) . ')[^\S\n]+/', '\\1', $content); + $content = preg_replace('/[^\S\n]+(' . implode('|', $operatorsDiffAfter) . ')/', '\\1', $content); /* * Whitespace after `return` can be omitted in a few occasions @@ -677,9 +383,26 @@ protected function stripWhitespace($content) * to be the for-loop's body... Same goes for while loops. * I'm going to double that semicolon (if any) so after the next line, * which strips semicolons here & there, we're still left with this one. + * Note the special recursive construct in the three inner parts of the for: + * (\{([^\{\}]*(?-2))*[^\{\}]*\})? - it is intended to match inline + * functions bodies, e.g.: iextracted. - * - * @param string $content - * - * @return string - */ - protected function restoreExtractedData($content) - { - if (!$this->extracted) { - // nothing was extracted, nothing to restore - return $content; - } - - $content = strtr($content, $this->extracted); - - $this->extracted = array(); - - return $content; - } - /** * We'll strip whitespace around certain operators with regular expressions. * This will prepare the given array by escaping all characters. @@ -760,7 +460,7 @@ protected function getOperatorsForRegex(array $operators, $delimiter = '/') // don't confuse = with other assignment shortcuts (e.g. +=) $chars = preg_quote('+-*\=<>%&|', $delimiter); - $operators['='] = '(?keywordsReserved; + $callback = function ($match) use ($minifier, $keywords) { + $property = trim($minifier->extracted[$match[1]], '\'"'); + + /* + * Check if the property is a reserved keyword. In this context (as + * property of an object literal/array) it shouldn't matter, but IE8 + * freaks out with "Expected identifier". + */ + if (in_array($property, $keywords)) { + return $match[0]; + } + + /* + * See if the property is in a variable-like format (e.g. + * array['key-here'] can't be replaced by array.key-here since '-' + * is not a valid character there. + */ + if (!preg_match('/^' . $minifier::REGEX_VARIABLE . '$/u', $property)) { + return $match[0]; + } + + return '.' . $property; + }; + + /* + * Figure out if previous character is a variable name (of the array + * we want to use property notation on) - this is to make sure + * standalone ['value'] arrays aren't confused for keys-of-an-array. + * We can (and only have to) check the last character, because PHP's + * regex implementation doesn't allow unfixed-length look-behind + * assertions. + */ + preg_match('/(\[[^\]]+\])[^\]]*$/', static::REGEX_VARIABLE, $previousChar); + $previousChar = $previousChar[1]; + + /* + * Make sure word preceding the ['value'] is not a keyword, e.g. + * return['x']. Because -again- PHP's regex implementation doesn't allow + * unfixed-length look-behind assertions, I'm just going to do a lot of + * separate look-behind assertions, one for each keyword. + */ + $keywords = $this->getKeywordsForRegex($keywords); + $keywords = '(?patterns[] = array($pattern, $replacement); - } + return $content; + } } diff --git a/lib/css_js_min/minify/minify.cls.php b/lib/css_js_min/minify/minify.cls.php new file mode 100644 index 000000000..2137bf194 --- /dev/null +++ b/lib/css_js_min/minify/minify.cls.php @@ -0,0 +1,535 @@ + + * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved + * @license MIT License + */ + +namespace LiteSpeed\Lib\CSS_JS_MIN\Minify; + +use LiteSpeed\Lib\CSS_JS_MIN\Minify\Exception\IOException; + +defined( 'WPINC' ) || exit ; + +abstract class Minify +{ + /** + * The data to be minified. + * + * @var string[] + */ + protected $data = array(); + + /** + * Array of patterns to match. + * + * @var string[] + */ + protected $patterns = array(); + + /** + * This array will hold content of strings and regular expressions that have + * been extracted from the JS source code, so we can reliably match "code", + * without having to worry about potential "code-like" characters inside. + * + * @internal + * + * @var string[] + */ + public $extracted = array(); + + /** + * Init the minify class - optionally, code may be passed along already. + */ + public function __construct(/* $data = null, ... */) + { + // it's possible to add the source through the constructor as well ;) + if (func_num_args()) { + call_user_func_array(array($this, 'add'), func_get_args()); + } + } + + /** + * Add a file or straight-up code to be minified. + * + * @param string|string[] $data + * + * @return static + */ + public function add($data /* $data = null, ... */) + { + // bogus "usage" of parameter $data: scrutinizer warns this variable is + // not used (we're using func_get_args instead to support overloading), + // but it still needs to be defined because it makes no sense to have + // this function without argument :) + $args = array($data) + func_get_args(); + + // this method can be overloaded + foreach ($args as $data) { + if (is_array($data)) { + call_user_func_array(array($this, 'add'), $data); + continue; + } + + // redefine var + $data = (string) $data; + + // load data + $value = $this->load($data); + $key = ($data != $value) ? $data : count($this->data); + + // replace CR linefeeds etc. + // @see https://github.com/matthiasmullie/minify/pull/139 + $value = str_replace(array("\r\n", "\r"), "\n", $value); + + // store data + $this->data[$key] = $value; + } + + return $this; + } + + /** + * Add a file to be minified. + * + * @param string|string[] $data + * + * @return static + * + * @throws IOException + */ + public function addFile($data /* $data = null, ... */) + { + // bogus "usage" of parameter $data: scrutinizer warns this variable is + // not used (we're using func_get_args instead to support overloading), + // but it still needs to be defined because it makes no sense to have + // this function without argument :) + $args = array($data) + func_get_args(); + + // this method can be overloaded + foreach ($args as $path) { + if (is_array($path)) { + call_user_func_array(array($this, 'addFile'), $path); + continue; + } + + // redefine var + $path = (string) $path; + + // check if we can read the file + if (!$this->canImportFile($path)) { + throw new IOException('The file "' . $path . '" could not be opened for reading. Check if PHP has enough permissions.'); + } + + $this->add($path); + } + + return $this; + } + + /** + * Minify the data & (optionally) saves it to a file. + * + * @param string[optional] $path Path to write the data to + * + * @return string The minified data + */ + public function minify($path = null) + { + $content = $this->execute($path); + + // save to path + if ($path !== null) { + $this->save($content, $path); + } + + return $content; + } + + /** + * Minify & gzip the data & (optionally) saves it to a file. + * + * @param string[optional] $path Path to write the data to + * @param int[optional] $level Compression level, from 0 to 9 + * + * @return string The minified & gzipped data + */ + public function gzip($path = null, $level = 9) + { + $content = $this->execute($path); + $content = gzencode($content, $level, FORCE_GZIP); + + // save to path + if ($path !== null) { + $this->save($content, $path); + } + + return $content; + } + + + /** + * Minify the data. + * + * @param string[optional] $path Path to write the data to + * + * @return string The minified data + */ + abstract public function execute($path = null); + + /** + * Load data. + * + * @param string $data Either a path to a file or the content itself + * + * @return string + */ + protected function load($data) + { + // check if the data is a file + if ($this->canImportFile($data)) { + $data = file_get_contents($data); + + // strip BOM, if any + if (substr($data, 0, 3) == "\xef\xbb\xbf") { + $data = substr($data, 3); + } + } + + return $data; + } + + /** + * Save to file. + * + * @param string $content The minified data + * @param string $path The path to save the minified data to + * + * @throws IOException + */ + protected function save($content, $path) + { + $handler = $this->openFileForWriting($path); + + $this->writeToFile($handler, $content); + + @fclose($handler); + } + + /** + * Register a pattern to execute against the source content. + * + * If $replacement is a string, it must be plain text. Placeholders like $1 or \2 don't work. + * If you need that functionality, use a callback instead. + * + * @param string $pattern PCRE pattern + * @param string|callable $replacement Replacement value for matched pattern + */ + protected function registerPattern($pattern, $replacement = '') + { + // study the pattern, we'll execute it more than once + $pattern .= 'S'; + + $this->patterns[] = array($pattern, $replacement); + } + + /** + * Both JS and CSS use the same form of multi-line comment, so putting the common code here. + */ + protected function stripMultilineComments() + { + // First extract comments we want to keep, so they can be restored later + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $callback = function ($match) use ($minifier) { + $count = count($minifier->extracted); + $placeholder = '/*' . $count . '*/'; + $minifier->extracted[$placeholder] = $match[0]; + + return $placeholder; + }; + $this->registerPattern('/ + # optional newline + \n? + + # start comment + \/\* + + # comment content + (?: + # either starts with an ! + ! + | + # or, after some number of characters which do not end the comment + (?:(?!\*\/).)*? + + # there is either a @license or @preserve tag + @(?:license|preserve) + ) + + # then match to the end of the comment + .*?\*\/\n? + + /ixs', $callback); + + // Then strip all other comments + $this->registerPattern('/\/\*.*?\*\//s', ''); + } + + /** + * We can't "just" run some regular expressions against JavaScript: it's a + * complex language. E.g. having an occurrence of // xyz would be a comment, + * unless it's used within a string. Of you could have something that looks + * like a 'string', but inside a comment. + * The only way to accurately replace these pieces is to traverse the JS one + * character at a time and try to find whatever starts first. + * + * @param string $content The content to replace patterns in + * + * @return string The (manipulated) content + */ + protected function replace($content) + { + $contentLength = strlen($content); + $output = ''; + $processedOffset = 0; + $positions = array_fill(0, count($this->patterns), -1); + $matches = array(); + + while ($processedOffset < $contentLength) { + // find first match for all patterns + foreach ($this->patterns as $i => $pattern) { + list($pattern, $replacement) = $pattern; + + // we can safely ignore patterns for positions we've unset earlier, + // because we know these won't show up anymore + if (array_key_exists($i, $positions) == false) { + continue; + } + + // no need to re-run matches that are still in the part of the + // content that hasn't been processed + if ($positions[$i] >= $processedOffset) { + continue; + } + + $match = null; + if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset)) { + $matches[$i] = $match; + + // we'll store the match position as well; that way, we + // don't have to redo all preg_matches after changing only + // the first (we'll still know where those others are) + $positions[$i] = $match[0][1]; + } else { + // if the pattern couldn't be matched, there's no point in + // executing it again in later runs on this same content; + // ignore this one until we reach end of content + unset($matches[$i], $positions[$i]); + } + } + + // no more matches to find: everything's been processed, break out + if (!$matches) { + // output the remaining content + $output .= substr($content, $processedOffset); + break; + } + + // see which of the patterns actually found the first thing (we'll + // only want to execute that one, since we're unsure if what the + // other found was not inside what the first found) + $matchOffset = min($positions); + $firstPattern = array_search($matchOffset, $positions); + $match = $matches[$firstPattern]; + + // execute the pattern that matches earliest in the content string + list(, $replacement) = $this->patterns[$firstPattern]; + + // add the part of the input between $processedOffset and the first match; + // that content wasn't matched by anything + $output .= substr($content, $processedOffset, $matchOffset - $processedOffset); + // add the replacement for the match + $output .= $this->executeReplacement($replacement, $match); + // advance $processedOffset past the match + $processedOffset = $matchOffset + strlen($match[0][0]); + } + + return $output; + } + + /** + * If $replacement is a callback, execute it, passing in the match data. + * If it's a string, just pass it through. + * + * @param string|callable $replacement Replacement value + * @param array $match Match data, in PREG_OFFSET_CAPTURE form + * + * @return string + */ + protected function executeReplacement($replacement, $match) + { + if (!is_callable($replacement)) { + return $replacement; + } + // convert $match from the PREG_OFFSET_CAPTURE form to the form the callback expects + foreach ($match as &$matchItem) { + $matchItem = $matchItem[0]; + } + + return $replacement($match); + } + + /** + * Strings are a pattern we need to match, in order to ignore potential + * code-like content inside them, but we just want all of the string + * content to remain untouched. + * + * This method will replace all string content with simple STRING# + * placeholder text, so we've rid all strings from characters that may be + * misinterpreted. Original string content will be saved in $this->extracted + * and after doing all other minifying, we can restore the original content + * via restoreStrings(). + * + * @param string[optional] $chars + * @param string[optional] $placeholderPrefix + */ + protected function extractStrings($chars = '\'"', $placeholderPrefix = '') + { + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $callback = function ($match) use ($minifier, $placeholderPrefix) { + // check the second index here, because the first always contains a quote + if ($match[2] === '') { + /* + * Empty strings need no placeholder; they can't be confused for + * anything else anyway. + * But we still needed to match them, for the extraction routine + * to skip over this particular string. + */ + return $match[0]; + } + + $count = count($minifier->extracted); + $placeholder = $match[1] . $placeholderPrefix . $count . $match[1]; + $minifier->extracted[$placeholder] = $match[1] . $match[2] . $match[1]; + + return $placeholder; + }; + + /* + * The \\ messiness explained: + * * Don't count ' or " as end-of-string if it's escaped (has backslash + * in front of it) + * * Unless... that backslash itself is escaped (another leading slash), + * in which case it's no longer escaping the ' or " + * * So there can be either no backslash, or an even number + * * multiply all of that times 4, to account for the escaping that has + * to be done to pass the backslash into the PHP string without it being + * considered as escape-char (times 2) and to get it in the regex, + * escaped (times 2) + */ + $this->registerPattern('/([' . $chars . '])(.*?(?extracted. + * + * @param string $content + * + * @return string + */ + protected function restoreExtractedData($content) + { + if (!$this->extracted) { + // nothing was extracted, nothing to restore + return $content; + } + + $content = strtr($content, $this->extracted); + + $this->extracted = array(); + + return $content; + } + + /** + * Check if the path is a regular file and can be read. + * + * @param string $path + * + * @return bool + */ + protected function canImportFile($path) + { + $parsed = parse_url($path); + if ( + // file is elsewhere + isset($parsed['host']) + // file responds to queries (may change, or need to bypass cache) + || isset($parsed['query']) + ) { + return false; + } + + try { + return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path); + } + // catch openbasedir exceptions which are not caught by @ on is_file() + catch (\Exception $e) { + return false; + } + } + + /** + * Attempts to open file specified by $path for writing. + * + * @param string $path The path to the file + * + * @return resource Specifier for the target file + * + * @throws IOException + */ + protected function openFileForWriting($path) + { + if ($path === '' || ($handler = @fopen($path, 'w')) === false) { + throw new IOException('The file "' . $path . '" could not be opened for writing. Check if PHP has enough permissions.'); + } + + return $handler; + } + + /** + * Attempts to write $content to the file specified by $handler. $path is used for printing exceptions. + * + * @param resource $handler The resource to write to + * @param string $content The content to write + * @param string $path The path to the file (for exception printing only) + * + * @throws IOException + */ + protected function writeToFile($handler, $content, $path = '') + { + if ( + !is_resource($handler) + || ($result = @fwrite($handler, $content)) === false + || ($result < strlen($content)) + ) { + throw new IOException('The file "' . $path . '" could not be written to. Check your disk space and file permissions.'); + } + } + + protected static function str_replace_first($search, $replace, $subject) + { + $pos = strpos($subject, $search); + if ($pos !== false) { + return substr_replace($subject, $replace, $pos, strlen($search)); + } + + return $subject; + } +} diff --git a/lib/css_js_min/pathconverter/LICENSE b/lib/css_js_min/pathconverter/LICENSE new file mode 100644 index 000000000..491295ad3 --- /dev/null +++ b/lib/css_js_min/pathconverter/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2015 Matthias Mullie + +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 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. diff --git a/lib/css_js_min/pathconverter/converter.cls.php b/lib/css_js_min/pathconverter/converter.cls.php new file mode 100644 index 000000000..7cf95094b --- /dev/null +++ b/lib/css_js_min/pathconverter/converter.cls.php @@ -0,0 +1,228 @@ + + * @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved + * @license MIT License + */ + +namespace LiteSpeed\Lib\CSS_JS_MIN\PathConverter; + +defined( 'WPINC' ) || exit ; + +interface ConverterInterface +{ + /** + * Convert file paths. + * + * @param string $path The path to be converted + * + * @return string The new path + */ + public function convert($path); +} + +class Converter implements ConverterInterface +{ + /** + * @var string + */ + protected $from; + + /** + * @var string + */ + protected $to; + + /** + * @param string $from The original base path (directory, not file!) + * @param string $to The new base path (directory, not file!) + * @param string $root Root directory (defaults to `getcwd`) + */ + public function __construct($from, $to, $root = '') + { + $shared = $this->shared($from, $to); + if ($shared === '') { + // when both paths have nothing in common, one of them is probably + // absolute while the other is relative + $root = $root ?: getcwd(); + $from = strpos($from, $root) === 0 ? $from : preg_replace('/\/+/', '/', $root.'/'.$from); + $to = strpos($to, $root) === 0 ? $to : preg_replace('/\/+/', '/', $root.'/'.$to); + + // or traveling the tree via `..` + // attempt to resolve path, or assume it's fine if it doesn't exist + $from = @realpath($from) ?: $from; + $to = @realpath($to) ?: $to; + } + + $from = $this->dirname($from); + $to = $this->dirname($to); + + $from = $this->normalize($from); + $to = $this->normalize($to); + + $this->from = $from; + $this->to = $to; + } + + /** + * Normalize path. + * + * @param string $path + * + * @return string + */ + protected function normalize($path) + { + // deal with different operating systems' directory structure + $path = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $path), '/'); + + // remove leading current directory. + if (substr($path, 0, 2) === './') { + $path = substr($path, 2); + } + + // remove references to current directory in the path. + $path = str_replace('/./', '/', $path); + + /* + * Example: + * /home/forkcms/frontend/cache/compiled_templates/../../core/layout/css/../images/img.gif + * to + * /home/forkcms/frontend/core/layout/images/img.gif + */ + do { + $path = preg_replace('/[^\/]+(? $chunk) { + if (isset($path2[$i]) && $path1[$i] == $path2[$i]) { + $shared[] = $chunk; + } else { + break; + } + } + + return implode('/', $shared); + } + + /** + * Convert paths relative from 1 file to another. + * + * E.g. + * ../images/img.gif relative to /home/forkcms/frontend/core/layout/css + * should become: + * ../../core/layout/images/img.gif relative to + * /home/forkcms/frontend/cache/minified_css + * + * @param string $path The relative path that needs to be converted + * + * @return string The new relative path + */ + public function convert($path) + { + // quit early if conversion makes no sense + if ($this->from === $this->to) { + return $path; + } + + $path = $this->normalize($path); + // if we're not dealing with a relative path, just return absolute + if (strpos($path, '/') === 0) { + return $path; + } + + // normalize paths + $path = $this->normalize($this->from.'/'.$path); + + // strip shared ancestor paths + $shared = $this->shared($path, $this->to); + $path = mb_substr($path, mb_strlen($shared)); + $to = mb_substr($this->to, mb_strlen($shared)); + + // add .. for every directory that needs to be traversed to new path + $to = str_repeat('../', count(array_filter(explode('/', $to)))); + + return $to.ltrim($path, '/'); + } + + /** + * Attempt to get the directory name from a path. + * + * @param string $path + * + * @return string + */ + protected function dirname($path) + { + if (@is_file($path)) { + return dirname($path); + } + + if (@is_dir($path)) { + return rtrim($path, '/'); + } + + // no known file/dir, start making assumptions + + // ends in / = dir + if (mb_substr($path, -1) === '/') { + return rtrim($path, '/'); + } + + // has a dot in the name, likely a file + if (preg_match('/.*\..*$/', basename($path)) !== 0) { + return dirname($path); + } + + // you're on your own here! + return $path; + } +} + +class NoConverter implements ConverterInterface +{ + /** + * {@inheritdoc} + */ + public function convert($path) + { + return $path; + } +} diff --git a/lib/css-min/urirewriter.cls.php b/lib/urirewriter.cls.php similarity index 99% rename from lib/css-min/urirewriter.cls.php rename to lib/urirewriter.cls.php index 97d5e8be7..84c7ff7cc 100644 --- a/lib/css-min/urirewriter.cls.php +++ b/lib/urirewriter.cls.php @@ -6,7 +6,7 @@ * @author Stephen Clay */ -namespace LiteSpeed\Lib\CSS_MIN ; +namespace LiteSpeed\Lib; defined( 'WPINC' ) || exit ; diff --git a/src/optimize.cls.php b/src/optimize.cls.php index 266582321..50025f909 100644 --- a/src/optimize.cls.php +++ b/src/optimize.cls.php @@ -874,8 +874,9 @@ private function _parse_js() $src_list = array(); $html_list = array(); - $content = preg_replace('##sU', '', $this->content); - preg_match_all('#]*)>(.*)#isU', $content, $matches, PREG_SET_ORDER); + // V7 added: (?:\r\n?|\n?) to fix replacement leaving empty new line + $content = preg_replace('#(?:\r\n?|\n?)#sU', '', $this->content); + preg_match_all('#]*)>(.*)(?:\r\n?|\n?)#isU', $content, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $attrs = empty($match[1]) ? array() : Utility::parse_attr($match[1]); @@ -1059,8 +1060,9 @@ private function _parse_css() // $dom->load( $content );return $val; // $items = $dom->find( 'link' ); - $content = preg_replace(array('##sU', '#]*)>.*#isU', '#]*)>.*#isU'), '', $this->content); - preg_match_all('#]+)/?>|]*)>([^<]+)#isU', $content, $matches, PREG_SET_ORDER); + // V7 added: (?:\r\n?|\n?) to fix replacement leaving empty new line + $content = preg_replace(array('#(?:\r\n?|\n?)#sU', '#]*)>.*(?:\r\n?|\n?)#isU', '#]*)>.*(?:\r\n?|\n?)#isU'), '', $this->content); + preg_match_all('#]+)/?>|]*)>([^<]+)(?:\r\n?|\n?)#isU', $content, $matches, PREG_SET_ORDER); foreach ($matches as $match) { // to avoid multiple replacement diff --git a/src/optimizer.cls.php b/src/optimizer.cls.php index 412d922f4..f9e256006 100644 --- a/src/optimizer.cls.php +++ b/src/optimizer.cls.php @@ -136,6 +136,15 @@ public function serve($request_url, $file_type, $minify, $src_list) File::save($tmp_static_file, $content, true, true); } + // if CSS - run the minification on the saved file. + // Will move imports to the top of file and remove extra spaces. + if ($file_type == 'css') { + $obj = new Lib\CSS_JS_MIN\Minify\CSS(); + $file_content_combined = $obj->moveImportsToTop(File::read($tmp_static_file)); + + File::save($tmp_static_file, $file_content_combined); + } + // validate md5 $filecon_md5 = md5_file($tmp_static_file); @@ -254,7 +263,7 @@ public function load_file($src, $file_type = 'css') if ($file_type == 'css') { $dirname = dirname($this_url) . '/'; - $con = Lib\CSS_MIN\UriRewriter::prepend($con, $dirname); + $con = Lib\UriRewriter::prepend($con, $dirname); } } else { Debug2::debug2('[CSS] Load local [' . $file_type . '] ' . $real_file[0]); @@ -263,7 +272,7 @@ public function load_file($src, $file_type = 'css') if ($file_type == 'css') { $dirname = dirname($real_file[0]); - $con = Lib\CSS_MIN\UriRewriter::rewrite($con, $dirname); + $con = Lib\UriRewriter::rewrite($con, $dirname); } } @@ -279,8 +288,10 @@ public function load_file($src, $file_type = 'css') public static function minify_css($data) { try { - $obj = new Lib\CSS_MIN\Minifier(); - return $obj->run($data); + $obj = new Lib\CSS_JS_MIN\Minify\CSS(); + $obj->add($data); + + return $obj->minify(); } catch (\Exception $e) { Debug2::debug('******[Optmer] minify_css failed: ' . $e->getMessage()); error_log('****** LiteSpeed Optimizer minify_css failed: ' . $e->getMessage()); @@ -308,8 +319,10 @@ public static function minify_js($data, $js_type = '') } try { - $data = Lib\JSMin::minify($data); - return $data; + $obj = new Lib\CSS_JS_MIN\Minify\JS(); + $obj->add($data); + + return $obj->minify(); } catch (\Exception $e) { Debug2::debug('******[Optmer] minify_js failed: ' . $e->getMessage()); // error_log( '****** LiteSpeed Optimizer minify_js failed: ' . $e->getMessage() );