litespeed
8 years ago
css_min.class.php
8 years ago
css_min.colors.class.php
8 years ago
css_min.utils.class.php
8 years ago
html_min.class.php
8 years ago
js_min.class.php
8 years ago
litespeed-php-compatibility.func.php
8 years ago
object-cache.php
8 years ago
url_rewritter.class.php
8 years ago
css_min.class.php
863 lines
| 1 | <?php |
| 2 | |
| 3 | /*! |
| 4 | * CssMin |
| 5 | * Author: Tubal Martin - http://tubalmartin.me/ |
| 6 | * Repo: https://github.com/tubalmartin/YUI-CSS-compressor-PHP-port |
| 7 | * |
| 8 | * This is a PHP port of the CSS minification tool distributed with YUICompressor, |
| 9 | * itself a port of the cssmin utility by Isaac Schlueter - http://foohack.com/ |
| 10 | * Permission is hereby granted to use the PHP version under the same |
| 11 | * conditions as the YUICompressor. |
| 12 | */ |
| 13 | |
| 14 | /*! |
| 15 | * YUI Compressor |
| 16 | * http://developer.yahoo.com/yui/compressor/ |
| 17 | * Author: Julien Lecomte - http://www.julienlecomte.net/ |
| 18 | * Copyright (c) 2013 Yahoo! Inc. All rights reserved. |
| 19 | * The copyrights embodied in the content of this file are licensed |
| 20 | * by Yahoo! Inc. under the BSD (revised) open source license. |
| 21 | */ |
| 22 | |
| 23 | namespace tubalmartin\CssMin; |
| 24 | |
| 25 | class Minifier |
| 26 | { |
| 27 | const QUERY_FRACTION = '_CSSMIN_QF_'; |
| 28 | const COMMENT_TOKEN = '_CSSMIN_CMT_%d_'; |
| 29 | const COMMENT_TOKEN_START = '_CSSMIN_CMT_'; |
| 30 | const RULE_BODY_TOKEN = '_CSSMIN_RBT_%d_'; |
| 31 | const PRESERVED_TOKEN = '_CSSMIN_PTK_%d_'; |
| 32 | |
| 33 | // Token lists |
| 34 | private $comments = array(); |
| 35 | private $ruleBodies = array(); |
| 36 | private $preservedTokens = array(); |
| 37 | |
| 38 | // Output options |
| 39 | private $keepImportantComments = true; |
| 40 | private $keepSourceMapComment = false; |
| 41 | private $linebreakPosition = 0; |
| 42 | |
| 43 | // PHP ini limits |
| 44 | private $raisePhpLimits; |
| 45 | private $memoryLimit; |
| 46 | private $maxExecutionTime = 60; // 1 min |
| 47 | private $pcreBacktrackLimit; |
| 48 | private $pcreRecursionLimit; |
| 49 | |
| 50 | // Color maps |
| 51 | private $hexToNamedColorsMap; |
| 52 | private $namedToHexColorsMap; |
| 53 | |
| 54 | // Regexes |
| 55 | private $numRegex; |
| 56 | private $charsetRegex = '/@charset [^;]+;/Si'; |
| 57 | private $importRegex = '/@import [^;]+;/Si'; |
| 58 | private $namespaceRegex = '/@namespace [^;]+;/Si'; |
| 59 | private $namedToHexColorsRegex; |
| 60 | private $shortenOneZeroesRegex; |
| 61 | private $shortenTwoZeroesRegex; |
| 62 | private $shortenThreeZeroesRegex; |
| 63 | private $shortenFourZeroesRegex; |
| 64 | private $unitsGroupRegex = '(?:ch|cm|em|ex|gd|in|mm|px|pt|pc|q|rem|vh|vmax|vmin|vw|%)'; |
| 65 | |
| 66 | /** |
| 67 | * @param bool|int $raisePhpLimits If true, PHP settings will be raised if needed |
| 68 | */ |
| 69 | public function __construct($raisePhpLimits = true) |
| 70 | { |
| 71 | $this->raisePhpLimits = (bool) $raisePhpLimits; |
| 72 | $this->memoryLimit = 128 * 1048576; // 128MB in bytes |
| 73 | $this->pcreBacktrackLimit = 1000 * 1000; |
| 74 | $this->pcreRecursionLimit = 500 * 1000; |
| 75 | $this->hexToNamedColorsMap = Colors::getHexToNamedMap(); |
| 76 | $this->namedToHexColorsMap = Colors::getNamedToHexMap(); |
| 77 | $this->namedToHexColorsRegex = sprintf( |
| 78 | '/([:,( ])(%s)( |,|\)|;|$)/Si', |
| 79 | implode('|', array_keys($this->namedToHexColorsMap)) |
| 80 | ); |
| 81 | $this->numRegex = sprintf('-?\d*\.?\d+%s?', $this->unitsGroupRegex); |
| 82 | $this->setShortenZeroValuesRegexes(); |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * Parses & minifies the given input CSS string |
| 87 | * @param string $css |
| 88 | * @return string |
| 89 | */ |
| 90 | public function run($css = '') |
| 91 | { |
| 92 | if (empty($css) || !is_string($css)) { |
| 93 | return ''; |
| 94 | } |
| 95 | |
| 96 | $this->resetRunProperties(); |
| 97 | |
| 98 | if ($this->raisePhpLimits) { |
| 99 | $this->doRaisePhpLimits(); |
| 100 | } |
| 101 | |
| 102 | return $this->minify($css); |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Sets whether to keep or remove sourcemap special comment. |
| 107 | * Sourcemap comments are removed by default. |
| 108 | * @param bool $keepSourceMapComment |
| 109 | */ |
| 110 | public function keepSourceMapComment($keepSourceMapComment = true) |
| 111 | { |
| 112 | $this->keepSourceMapComment = (bool) $keepSourceMapComment; |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Sets whether to keep or remove important comments. |
| 117 | * Important comments outside of a declaration block are kept by default. |
| 118 | * @param bool $removeImportantComments |
| 119 | */ |
| 120 | public function removeImportantComments($removeImportantComments = true) |
| 121 | { |
| 122 | $this->keepImportantComments = !(bool) $removeImportantComments; |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Sets the approximate column after which long lines will be splitted in the output |
| 127 | * with a linebreak. |
| 128 | * @param int $position |
| 129 | */ |
| 130 | public function setLineBreakPosition($position) |
| 131 | { |
| 132 | $this->linebreakPosition = (int) $position; |
| 133 | } |
| 134 | |
| 135 | /** |
| 136 | * Sets the memory limit for this script |
| 137 | * @param int|string $limit |
| 138 | */ |
| 139 | public function setMemoryLimit($limit) |
| 140 | { |
| 141 | $this->memoryLimit = Utils::normalizeInt($limit); |
| 142 | } |
| 143 | |
| 144 | /** |
| 145 | * Sets the maximum execution time for this script |
| 146 | * @param int|string $seconds |
| 147 | */ |
| 148 | public function setMaxExecutionTime($seconds) |
| 149 | { |
| 150 | $this->maxExecutionTime = (int) $seconds; |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | * Sets the PCRE backtrack limit for this script |
| 155 | * @param int $limit |
| 156 | */ |
| 157 | public function setPcreBacktrackLimit($limit) |
| 158 | { |
| 159 | $this->pcreBacktrackLimit = (int) $limit; |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * Sets the PCRE recursion limit for this script |
| 164 | * @param int $limit |
| 165 | */ |
| 166 | public function setPcreRecursionLimit($limit) |
| 167 | { |
| 168 | $this->pcreRecursionLimit = (int) $limit; |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Builds regular expressions needed for shortening zero values |
| 173 | */ |
| 174 | private function setShortenZeroValuesRegexes() |
| 175 | { |
| 176 | $zeroRegex = '0'. $this->unitsGroupRegex; |
| 177 | $numOrPosRegex = '('. $this->numRegex .'|top|left|bottom|right|center) '; |
| 178 | $oneZeroSafeProperties = array( |
| 179 | '(?:line-)?height', |
| 180 | '(?:(?:min|max)-)?width', |
| 181 | 'top', |
| 182 | 'left', |
| 183 | 'background-position', |
| 184 | 'bottom', |
| 185 | 'right', |
| 186 | 'border(?:-(?:top|left|bottom|right))?(?:-width)?', |
| 187 | 'border-(?:(?:top|bottom)-(?:left|right)-)?radius', |
| 188 | 'column-(?:gap|width)', |
| 189 | 'margin(?:-(?:top|left|bottom|right))?', |
| 190 | 'outline-width', |
| 191 | 'padding(?:-(?:top|left|bottom|right))?' |
| 192 | ); |
| 193 | |
| 194 | // First zero regex |
| 195 | $regex = '/(^|;)('. implode('|', $oneZeroSafeProperties) .'):%s/Si'; |
| 196 | $this->shortenOneZeroesRegex = sprintf($regex, $zeroRegex); |
| 197 | |
| 198 | // Multiple zeroes regexes |
| 199 | $regex = '/(^|;)(margin|padding|border-(?:width|radius)|background-position):%s/Si'; |
| 200 | $this->shortenTwoZeroesRegex = sprintf($regex, $numOrPosRegex . $zeroRegex); |
| 201 | $this->shortenThreeZeroesRegex = sprintf($regex, $numOrPosRegex . $numOrPosRegex . $zeroRegex); |
| 202 | $this->shortenFourZeroesRegex = sprintf($regex, $numOrPosRegex . $numOrPosRegex . $numOrPosRegex . $zeroRegex); |
| 203 | } |
| 204 | |
| 205 | /** |
| 206 | * Resets properties whose value may change between runs |
| 207 | */ |
| 208 | private function resetRunProperties() |
| 209 | { |
| 210 | $this->comments = array(); |
| 211 | $this->ruleBodies = array(); |
| 212 | $this->preservedTokens = array(); |
| 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Tries to configure PHP to use at least the suggested minimum settings |
| 217 | * @return void |
| 218 | */ |
| 219 | private function doRaisePhpLimits() |
| 220 | { |
| 221 | $phpLimits = array( |
| 222 | 'memory_limit' => $this->memoryLimit, |
| 223 | 'max_execution_time' => $this->maxExecutionTime, |
| 224 | 'pcre.backtrack_limit' => $this->pcreBacktrackLimit, |
| 225 | 'pcre.recursion_limit' => $this->pcreRecursionLimit |
| 226 | ); |
| 227 | |
| 228 | // If current settings are higher respect them. |
| 229 | foreach ($phpLimits as $name => $suggested) { |
| 230 | $current = Utils::normalizeInt(ini_get($name)); |
| 231 | |
| 232 | if ($current >= $suggested) { |
| 233 | continue; |
| 234 | } |
| 235 | |
| 236 | // memoryLimit exception: allow -1 for "no memory limit". |
| 237 | if ($name === 'memory_limit' && $current === -1) { |
| 238 | continue; |
| 239 | } |
| 240 | |
| 241 | // maxExecutionTime exception: allow 0 for "no memory limit". |
| 242 | if ($name === 'max_execution_time' && $current === 0) { |
| 243 | continue; |
| 244 | } |
| 245 | |
| 246 | ini_set($name, $suggested); |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | /** |
| 251 | * Registers a preserved token |
| 252 | * @param string $token |
| 253 | * @return string The token ID string |
| 254 | */ |
| 255 | private function registerPreservedToken($token) |
| 256 | { |
| 257 | $tokenId = sprintf(self::PRESERVED_TOKEN, count($this->preservedTokens)); |
| 258 | $this->preservedTokens[$tokenId] = $token; |
| 259 | return $tokenId; |
| 260 | } |
| 261 | |
| 262 | /** |
| 263 | * Registers a candidate comment token |
| 264 | * @param string $comment |
| 265 | * @return string The comment token ID string |
| 266 | */ |
| 267 | private function registerCommentToken($comment) |
| 268 | { |
| 269 | $tokenId = sprintf(self::COMMENT_TOKEN, count($this->comments)); |
| 270 | $this->comments[$tokenId] = $comment; |
| 271 | return $tokenId; |
| 272 | } |
| 273 | |
| 274 | /** |
| 275 | * Registers a rule body token |
| 276 | * @param string $body the minified rule body |
| 277 | * @return string The rule body token ID string |
| 278 | */ |
| 279 | private function registerRuleBodyToken($body) |
| 280 | { |
| 281 | if (empty($body)) { |
| 282 | return ''; |
| 283 | } |
| 284 | |
| 285 | $tokenId = sprintf(self::RULE_BODY_TOKEN, count($this->ruleBodies)); |
| 286 | $this->ruleBodies[$tokenId] = $body; |
| 287 | return $tokenId; |
| 288 | } |
| 289 | |
| 290 | /** |
| 291 | * Parses & minifies the given input CSS string |
| 292 | * @param string $css |
| 293 | * @return string |
| 294 | */ |
| 295 | private function minify($css) |
| 296 | { |
| 297 | // Process data urls |
| 298 | $css = $this->processDataUrls($css); |
| 299 | |
| 300 | // Process comments |
| 301 | $css = preg_replace_callback( |
| 302 | '/(?<!\\\\)\/\*(.*?)\*(?<!\\\\)\//Ss', |
| 303 | array($this, 'processCommentsCallback'), |
| 304 | $css |
| 305 | ); |
| 306 | |
| 307 | // IE7: Process Microsoft matrix filters (whitespaces between Matrix parameters). Can contain strings inside. |
| 308 | $css = preg_replace_callback( |
| 309 | '/filter:\s*progid:DXImageTransform\.Microsoft\.Matrix\(([^)]+)\)/Ss', |
| 310 | array($this, 'processOldIeSpecificMatrixDefinitionCallback'), |
| 311 | $css |
| 312 | ); |
| 313 | |
| 314 | // Process quoted unquotable attribute selectors to unquote them. Covers most common cases. |
| 315 | // Likelyhood of a quoted attribute selector being a substring in a string: Very very low. |
| 316 | $css = preg_replace( |
| 317 | '/\[\s*([a-z][a-z-]+)\s*([\*\|\^\$~]?=)\s*[\'"](-?[a-z_][a-z0-9-_]+)[\'"]\s*\]/Ssi', |
| 318 | '[$1$2$3]', |
| 319 | $css |
| 320 | ); |
| 321 | |
| 322 | // Process strings so their content doesn't get accidentally minified |
| 323 | $css = preg_replace_callback( |
| 324 | '/(?:"(?:[^\\\\"]|\\\\.|\\\\)*")|'."(?:'(?:[^\\\\']|\\\\.|\\\\)*')/S", |
| 325 | array($this, 'processStringsCallback'), |
| 326 | $css |
| 327 | ); |
| 328 | |
| 329 | // Normalize all whitespace strings to single spaces. Easier to work with that way. |
| 330 | $css = preg_replace('/\s+/S', ' ', $css); |
| 331 | |
| 332 | // Process comments |
| 333 | $css = $this->processComments($css); |
| 334 | |
| 335 | // Process rule bodies |
| 336 | $css = $this->processRuleBodies($css); |
| 337 | |
| 338 | // Process at-rules and selectors |
| 339 | $css = $this->processAtRulesAndSelectors($css); |
| 340 | |
| 341 | // Restore preserved rule bodies before splitting |
| 342 | $css = strtr($css, $this->ruleBodies); |
| 343 | |
| 344 | // Some source control tools don't like it when files containing lines longer |
| 345 | // than, say 8000 characters, are checked in. The linebreak option is used in |
| 346 | // that case to split long lines after a specific column. |
| 347 | if ($this->linebreakPosition > 0) { |
| 348 | $l = strlen($css); |
| 349 | $offset = $this->linebreakPosition; |
| 350 | while (preg_match('/(?<!\\\\)\}(?!\n)/S', $css, $matches, PREG_OFFSET_CAPTURE, $offset)) { |
| 351 | $matchIndex = $matches[0][1]; |
| 352 | $css = substr_replace($css, "\n", $matchIndex + 1, 0); |
| 353 | $offset = $matchIndex + 2 + $this->linebreakPosition; |
| 354 | $l += 1; |
| 355 | if ($offset > $l) { |
| 356 | break; |
| 357 | } |
| 358 | } |
| 359 | } |
| 360 | |
| 361 | // Restore preserved comments and strings |
| 362 | $css = strtr($css, $this->preservedTokens); |
| 363 | |
| 364 | return trim($css); |
| 365 | } |
| 366 | |
| 367 | /** |
| 368 | * Searches & replaces all data urls with tokens before we start compressing, |
| 369 | * to avoid performance issues running some of the subsequent regexes against large string chunks. |
| 370 | * @param string $css |
| 371 | * @return string |
| 372 | */ |
| 373 | private function processDataUrls($css) |
| 374 | { |
| 375 | $ret = ''; |
| 376 | $searchOffset = $substrOffset = 0; |
| 377 | |
| 378 | // Since we need to account for non-base64 data urls, we need to handle |
| 379 | // ' and ) being part of the data string. |
| 380 | while (preg_match('/url\(\s*(["\']?)data:/Si', $css, $m, PREG_OFFSET_CAPTURE, $searchOffset)) { |
| 381 | $matchStartIndex = $m[0][1]; |
| 382 | $dataStartIndex = $matchStartIndex + 4; // url( length |
| 383 | $searchOffset = $matchStartIndex + strlen($m[0][0]); |
| 384 | $terminator = $m[1][0]; // ', " or empty (not quoted) |
| 385 | $terminatorRegex = '/(?<!\\\\)'. (strlen($terminator) === 0 ? '' : $terminator.'\s*') .'(\))/S'; |
| 386 | |
| 387 | $ret .= substr($css, $substrOffset, $matchStartIndex - $substrOffset); |
| 388 | |
| 389 | // Terminator found |
| 390 | if (preg_match($terminatorRegex, $css, $matches, PREG_OFFSET_CAPTURE, $searchOffset)) { |
| 391 | $matchEndIndex = $matches[1][1]; |
| 392 | $searchOffset = $matchEndIndex + 1; |
| 393 | $token = substr($css, $dataStartIndex, $matchEndIndex - $dataStartIndex); |
| 394 | |
| 395 | // Remove all spaces only for base64 encoded URLs. |
| 396 | if (stripos($token, 'base64,') !== false) { |
| 397 | $token = preg_replace('/\s+/S', '', $token); |
| 398 | } |
| 399 | |
| 400 | $ret .= 'url('. $this->registerPreservedToken(trim($token)) .')'; |
| 401 | // No end terminator found, re-add the whole match. Should we throw/warn here? |
| 402 | } else { |
| 403 | $ret .= substr($css, $matchStartIndex, $searchOffset - $matchStartIndex); |
| 404 | } |
| 405 | |
| 406 | $substrOffset = $searchOffset; |
| 407 | } |
| 408 | |
| 409 | $ret .= substr($css, $substrOffset); |
| 410 | |
| 411 | return $ret; |
| 412 | } |
| 413 | |
| 414 | /** |
| 415 | * Registers all comments found as candidates to be preserved. |
| 416 | * @param array $matches |
| 417 | * @return string |
| 418 | */ |
| 419 | private function processCommentsCallback($matches) |
| 420 | { |
| 421 | return '/*'. $this->registerCommentToken($matches[1]) .'*/'; |
| 422 | } |
| 423 | |
| 424 | /** |
| 425 | * Preserves old IE Matrix string definition |
| 426 | * @param array $matches |
| 427 | * @return string |
| 428 | */ |
| 429 | private function processOldIeSpecificMatrixDefinitionCallback($matches) |
| 430 | { |
| 431 | return 'filter:progid:DXImageTransform.Microsoft.Matrix('. $this->registerPreservedToken($matches[1]) .')'; |
| 432 | } |
| 433 | |
| 434 | /** |
| 435 | * Preserves strings found |
| 436 | * @param array $matches |
| 437 | * @return string |
| 438 | */ |
| 439 | private function processStringsCallback($matches) |
| 440 | { |
| 441 | $match = $matches[0]; |
| 442 | $quote = substr($match, 0, 1); |
| 443 | $match = substr($match, 1, -1); |
| 444 | |
| 445 | // maybe the string contains a comment-like substring? |
| 446 | // one, maybe more? put'em back then |
| 447 | if (strpos($match, self::COMMENT_TOKEN_START) !== false) { |
| 448 | $match = strtr($match, $this->comments); |
| 449 | } |
| 450 | |
| 451 | // minify alpha opacity in filter strings |
| 452 | $match = str_ireplace('progid:DXImageTransform.Microsoft.Alpha(Opacity=', 'alpha(opacity=', $match); |
| 453 | |
| 454 | return $quote . $this->registerPreservedToken($match) . $quote; |
| 455 | } |
| 456 | |
| 457 | /** |
| 458 | * Preserves or removes comments found. |
| 459 | * @param string $css |
| 460 | * @return string |
| 461 | */ |
| 462 | private function processComments($css) |
| 463 | { |
| 464 | foreach ($this->comments as $commentId => $comment) { |
| 465 | $commentIdString = '/*'. $commentId .'*/'; |
| 466 | |
| 467 | // ! in the first position of the comment means preserve |
| 468 | // so push to the preserved tokens keeping the ! |
| 469 | if ($this->keepImportantComments && strpos($comment, '!') === 0) { |
| 470 | $preservedTokenId = $this->registerPreservedToken($comment); |
| 471 | // Put new lines before and after /*! important comments |
| 472 | $css = str_replace($commentIdString, "\n/*$preservedTokenId*/\n", $css); |
| 473 | continue; |
| 474 | } |
| 475 | |
| 476 | // # sourceMappingURL= in the first position of the comment means sourcemap |
| 477 | // so push to the preserved tokens if {$this->keepSourceMapComment} is truthy. |
| 478 | if ($this->keepSourceMapComment && strpos($comment, '# sourceMappingURL=') === 0) { |
| 479 | $preservedTokenId = $this->registerPreservedToken($comment); |
| 480 | // Add new line before the sourcemap comment |
| 481 | $css = str_replace($commentIdString, "\n/*$preservedTokenId*/", $css); |
| 482 | continue; |
| 483 | } |
| 484 | |
| 485 | // Keep empty comments after child selectors (IE7 hack) |
| 486 | // e.g. html >/**/ body |
| 487 | if (strlen($comment) === 0 && strpos($css, '>/*'.$commentId) !== false) { |
| 488 | $css = str_replace($commentId, $this->registerPreservedToken(''), $css); |
| 489 | continue; |
| 490 | } |
| 491 | |
| 492 | // in all other cases kill the comment |
| 493 | $css = str_replace($commentIdString, '', $css); |
| 494 | } |
| 495 | |
| 496 | // Normalize whitespace again |
| 497 | $css = preg_replace('/ +/S', ' ', $css); |
| 498 | |
| 499 | return $css; |
| 500 | } |
| 501 | |
| 502 | /** |
| 503 | * Finds, minifies & preserves all rule bodies. |
| 504 | * @param string $css the whole stylesheet. |
| 505 | * @return string |
| 506 | */ |
| 507 | private function processRuleBodies($css) |
| 508 | { |
| 509 | $ret = ''; |
| 510 | $searchOffset = $substrOffset = 0; |
| 511 | |
| 512 | while (($blockStartPos = strpos($css, '{', $searchOffset)) !== false) { |
| 513 | $blockEndPos = strpos($css, '}', $blockStartPos); |
| 514 | $nextBlockStartPos = strpos($css, '{', $blockStartPos + 1); |
| 515 | $ret .= substr($css, $substrOffset, $blockStartPos - $substrOffset); |
| 516 | |
| 517 | if ($nextBlockStartPos !== false && $nextBlockStartPos < $blockEndPos) { |
| 518 | $ret .= substr($css, $blockStartPos, $nextBlockStartPos - $blockStartPos); |
| 519 | $searchOffset = $nextBlockStartPos; |
| 520 | } else { |
| 521 | $ruleBody = substr($css, $blockStartPos + 1, $blockEndPos - $blockStartPos - 1); |
| 522 | $ruleBodyToken = $this->registerRuleBodyToken($this->processRuleBody($ruleBody)); |
| 523 | $ret .= '{'. $ruleBodyToken .'}'; |
| 524 | $searchOffset = $blockEndPos + 1; |
| 525 | } |
| 526 | |
| 527 | $substrOffset = $searchOffset; |
| 528 | } |
| 529 | |
| 530 | $ret .= substr($css, $substrOffset); |
| 531 | |
| 532 | return $ret; |
| 533 | } |
| 534 | |
| 535 | /** |
| 536 | * Compresses non-group rule bodies. |
| 537 | * @param string $body The rule body without curly braces |
| 538 | * @return string |
| 539 | */ |
| 540 | private function processRuleBody($body) |
| 541 | { |
| 542 | $body = trim($body); |
| 543 | |
| 544 | // Remove spaces before the things that should not have spaces before them. |
| 545 | $body = preg_replace('/ ([:=,)*\/;\n])/S', '$1', $body); |
| 546 | |
| 547 | // Remove the spaces after the things that should not have spaces after them. |
| 548 | $body = preg_replace('/([:=,(*\/!;\n]) /S', '$1', $body); |
| 549 | |
| 550 | // Replace multiple semi-colons in a row by a single one |
| 551 | $body = preg_replace('/;;+/S', ';', $body); |
| 552 | |
| 553 | // Remove semicolon before closing brace except when: |
| 554 | // - The last property is prefixed with a `*` (lte IE7 hack) to avoid issues on Symbian S60 3.x browsers. |
| 555 | if (!preg_match('/\*[a-z0-9-]+:[^;]+;$/Si', $body)) { |
| 556 | $body = rtrim($body, ';'); |
| 557 | } |
| 558 | |
| 559 | // Remove important comments inside a rule body (because they make no sense here). |
| 560 | if (strpos($body, '/*') !== false) { |
| 561 | $body = preg_replace('/\n?\/\*[A-Z0-9_]+\*\/\n?/S', '', $body); |
| 562 | } |
| 563 | |
| 564 | // Empty rule body? Exit :) |
| 565 | if (empty($body)) { |
| 566 | return ''; |
| 567 | } |
| 568 | |
| 569 | // Shorten font-weight values |
| 570 | $body = preg_replace( |
| 571 | array('/(font-weight:)bold\b/Si', '/(font-weight:)normal\b/Si'), |
| 572 | array('${1}700', '${1}400'), |
| 573 | $body |
| 574 | ); |
| 575 | |
| 576 | // Shorten background property |
| 577 | $body = preg_replace('/(background:)(?:none|transparent)( !|;|$)/Si', '${1}0 0$2', $body); |
| 578 | |
| 579 | // Shorten opacity IE filter |
| 580 | $body = str_ireplace('progid:DXImageTransform.Microsoft.Alpha(Opacity=', 'alpha(opacity=', $body); |
| 581 | |
| 582 | // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space) |
| 583 | // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space) |
| 584 | // This makes it more likely that it'll get further compressed in the next step. |
| 585 | $body = preg_replace_callback( |
| 586 | '/(rgb|hsl)\(([0-9,.% -]+)\)(.|$)/Si', |
| 587 | array($this, 'shortenHslAndRgbToHexCallback'), |
| 588 | $body |
| 589 | ); |
| 590 | |
| 591 | // Shorten colors from #AABBCC to #ABC or shorter color name: |
| 592 | // - Look for hex colors which don't have a "=" in front of them (to avoid MSIE filters) |
| 593 | $body = preg_replace_callback( |
| 594 | '/(?<!=)#([0-9a-f]{3,6})( |,|\)|;|$)/Si', |
| 595 | array($this, 'shortenHexColorsCallback'), |
| 596 | $body |
| 597 | ); |
| 598 | |
| 599 | // Shorten long named colors with a shorter HEX counterpart: white -> #fff. |
| 600 | // Run at least 2 times to cover most cases |
| 601 | $body = preg_replace_callback( |
| 602 | array($this->namedToHexColorsRegex, $this->namedToHexColorsRegex), |
| 603 | array($this, 'shortenNamedColorsCallback'), |
| 604 | $body |
| 605 | ); |
| 606 | |
| 607 | // Replace positive sign from numbers before the leading space is removed. |
| 608 | // +1.2em to 1.2em, +.8px to .8px, +2% to 2% |
| 609 | $body = preg_replace('/([ :,(])\+(\.?\d+)/S', '$1$2', $body); |
| 610 | |
| 611 | // shorten ms to s |
| 612 | $body = preg_replace_callback('/([ :,(])(-?)(\d{3,})ms/Si', function ($matches) { |
| 613 | return $matches[1] . $matches[2] . ((int) $matches[3] / 1000) .'s'; |
| 614 | }, $body); |
| 615 | |
| 616 | // Remove leading zeros from integer and float numbers. |
| 617 | // 000.6 to .6, -0.8 to -.8, 0050 to 50, -01.05 to -1.05 |
| 618 | $body = preg_replace('/([ :,(])(-?)0+([1-9]?\.?\d+)/S', '$1$2$3', $body); |
| 619 | |
| 620 | // Remove trailing zeros from float numbers. |
| 621 | // -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px |
| 622 | $body = preg_replace('/([ :,(])(-?\d?\.\d+?)0+([^\d])/S', '$1$2$3', $body); |
| 623 | |
| 624 | // Remove trailing .0 -> -9.0 to -9 |
| 625 | $body = preg_replace('/([ :,(])(-?\d+)\.0([^\d])/S', '$1$2$3', $body); |
| 626 | |
| 627 | // Replace 0 length numbers with 0 |
| 628 | $body = preg_replace('/([ :,(])-?\.?0+([^\d])/S', '${1}0$2', $body); |
| 629 | |
| 630 | // Shorten zero values for safe properties only |
| 631 | $body = preg_replace( |
| 632 | array( |
| 633 | $this->shortenOneZeroesRegex, |
| 634 | $this->shortenTwoZeroesRegex, |
| 635 | $this->shortenThreeZeroesRegex, |
| 636 | $this->shortenFourZeroesRegex |
| 637 | ), |
| 638 | array( |
| 639 | '$1$2:0', |
| 640 | '$1$2:$3 0', |
| 641 | '$1$2:$3 $4 0', |
| 642 | '$1$2:$3 $4 $5 0' |
| 643 | ), |
| 644 | $body |
| 645 | ); |
| 646 | |
| 647 | // Replace 0 0 0; or 0 0 0 0; with 0 0 for background-position property. |
| 648 | $body = preg_replace('/(background-position):0(?: 0){2,3}( !|;|$)/Si', '$1:0 0$2', $body); |
| 649 | |
| 650 | // Shorten suitable shorthand properties with repeated values |
| 651 | $body = preg_replace( |
| 652 | array( |
| 653 | '/(margin|padding|border-(?:width|radius)):('.$this->numRegex.')(?: \2)+( !|;|$)/Si', |
| 654 | '/(border-(?:style|color)):([#a-z0-9]+)(?: \2)+( !|;|$)/Si' |
| 655 | ), |
| 656 | '$1:$2$3', |
| 657 | $body |
| 658 | ); |
| 659 | $body = preg_replace( |
| 660 | array( |
| 661 | '/(margin|padding|border-(?:width|radius)):'. |
| 662 | '('.$this->numRegex.') ('.$this->numRegex.') \2 \3( !|;|$)/Si', |
| 663 | '/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) \2 \3( !|;|$)/Si' |
| 664 | ), |
| 665 | '$1:$2 $3$4', |
| 666 | $body |
| 667 | ); |
| 668 | $body = preg_replace( |
| 669 | array( |
| 670 | '/(margin|padding|border-(?:width|radius)):'. |
| 671 | '('.$this->numRegex.') ('.$this->numRegex.') ('.$this->numRegex.') \3( !|;|$)/Si', |
| 672 | '/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) ([#a-z0-9]+) \3( !|;|$)/Si' |
| 673 | ), |
| 674 | '$1:$2 $3 $4$5', |
| 675 | $body |
| 676 | ); |
| 677 | |
| 678 | // Lowercase some common functions that can be values |
| 679 | $body = preg_replace_callback( |
| 680 | '/(?:attr|blur|brightness|circle|contrast|cubic-bezier|drop-shadow|ellipse|from|grayscale|'. |
| 681 | 'hsla?|hue-rotate|inset|invert|local|minmax|opacity|perspective|polygon|rgba?|rect|repeat|saturate|sepia|'. |
| 682 | 'steps|to|url|var|-webkit-gradient|'. |
| 683 | '(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|(?:repeating-)?(?:linear|radial)-gradient))\(/Si', |
| 684 | array($this, 'strtolowerCallback'), |
| 685 | $body |
| 686 | ); |
| 687 | |
| 688 | // Lowercase all uppercase properties |
| 689 | $body = preg_replace_callback('/(?:^|;)[A-Z-]+:/S', array($this, 'strtolowerCallback'), $body); |
| 690 | |
| 691 | return $body; |
| 692 | } |
| 693 | |
| 694 | /** |
| 695 | * Compresses At-rules and selectors. |
| 696 | * @param string $css the whole stylesheet with rule bodies tokenized. |
| 697 | * @return string |
| 698 | */ |
| 699 | private function processAtRulesAndSelectors($css) |
| 700 | { |
| 701 | $charset = ''; |
| 702 | $imports = ''; |
| 703 | $namespaces = ''; |
| 704 | |
| 705 | // Remove spaces before the things that should not have spaces before them. |
| 706 | $css = preg_replace('/ ([@{};>+)\]~=,\/\n])/S', '$1', $css); |
| 707 | |
| 708 | // Remove the spaces after the things that should not have spaces after them. |
| 709 | $css = preg_replace('/([{}:;>+(\[~=,\/\n]) /S', '$1', $css); |
| 710 | |
| 711 | // Shorten shortable double colon (CSS3) pseudo-elements to single colon (CSS2) |
| 712 | $css = preg_replace('/::(before|after|first-(?:line|letter))(\{|,)/Si', ':$1$2', $css); |
| 713 | |
| 714 | // Retain space for special IE6 cases |
| 715 | $css = preg_replace_callback('/:first-(line|letter)(\{|,)/Si', function ($matches) { |
| 716 | return ':first-'. strtolower($matches[1]) .' '. $matches[2]; |
| 717 | }, $css); |
| 718 | |
| 719 | // Find a fraction that may used in some @media queries such as: (min-aspect-ratio: 1/1) |
| 720 | // Add token to add the "/" back in later |
| 721 | $css = preg_replace('/\(([a-z-]+):([0-9]+)\/([0-9]+)\)/Si', '($1:$2'. self::QUERY_FRACTION .'$3)', $css); |
| 722 | |
| 723 | // Remove empty rule blocks up to 2 levels deep. |
| 724 | $css = preg_replace(array_fill(0, 2, '/(\{)[^{};\/\n]+\{\}/S'), '$1', $css); |
| 725 | $css = preg_replace('/[^{};\/\n]+\{\}/S', '', $css); |
| 726 | |
| 727 | // Two important comments next to each other? Remove extra newline. |
| 728 | if ($this->keepImportantComments) { |
| 729 | $css = str_replace("\n\n", "\n", $css); |
| 730 | } |
| 731 | |
| 732 | // Restore fraction |
| 733 | $css = str_replace(self::QUERY_FRACTION, '/', $css); |
| 734 | |
| 735 | // Lowercase some popular @directives |
| 736 | $css = preg_replace_callback( |
| 737 | '/(?<!\\\\)@(?:charset|document|font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframes|media|'. |
| 738 | 'namespace|page|supports|viewport)/Si', |
| 739 | array($this, 'strtolowerCallback'), |
| 740 | $css |
| 741 | ); |
| 742 | |
| 743 | // Lowercase some popular media types |
| 744 | $css = preg_replace_callback( |
| 745 | '/[ ,](?:all|aural|braille|handheld|print|projection|screen|tty|tv|embossed|speech)[ ,;{]/Si', |
| 746 | array($this, 'strtolowerCallback'), |
| 747 | $css |
| 748 | ); |
| 749 | |
| 750 | // Lowercase some common pseudo-classes & pseudo-elements |
| 751 | $css = preg_replace_callback( |
| 752 | '/(?<!\\\\):(?:active|after|before|checked|default|disabled|empty|enabled|first-(?:child|of-type)|'. |
| 753 | 'focus(?:-within)?|hover|indeterminate|in-range|invalid|lang\(|last-(?:child|of-type)|left|link|not\(|'. |
| 754 | 'nth-(?:child|of-type)\(|nth-last-(?:child|of-type)\(|only-(?:child|of-type)|optional|out-of-range|'. |
| 755 | 'read-(?:only|write)|required|right|root|:selection|target|valid|visited)/Si', |
| 756 | array($this, 'strtolowerCallback'), |
| 757 | $css |
| 758 | ); |
| 759 | |
| 760 | // @charset handling |
| 761 | if (preg_match($this->charsetRegex, $css, $matches)) { |
| 762 | // Keep the first @charset at-rule found |
| 763 | $charset = $matches[0]; |
| 764 | // Delete all @charset at-rules |
| 765 | $css = preg_replace($this->charsetRegex, '', $css); |
| 766 | } |
| 767 | |
| 768 | // @import handling |
| 769 | $css = preg_replace_callback($this->importRegex, function ($matches) use (&$imports) { |
| 770 | // Keep all @import at-rules found for later |
| 771 | $imports .= $matches[0]; |
| 772 | // Delete all @import at-rules |
| 773 | return ''; |
| 774 | }, $css); |
| 775 | |
| 776 | // @namespace handling |
| 777 | $css = preg_replace_callback($this->namespaceRegex, function ($matches) use (&$namespaces) { |
| 778 | // Keep all @namespace at-rules found for later |
| 779 | $namespaces .= $matches[0]; |
| 780 | // Delete all @namespace at-rules |
| 781 | return ''; |
| 782 | }, $css); |
| 783 | |
| 784 | // Order critical at-rules: |
| 785 | // 1. @charset first |
| 786 | // 2. @imports below @charset |
| 787 | // 3. @namespaces below @imports |
| 788 | $css = $charset . $imports . $namespaces . $css; |
| 789 | |
| 790 | return $css; |
| 791 | } |
| 792 | |
| 793 | /** |
| 794 | * Converts hsl() & rgb() colors to HEX format. |
| 795 | * @param $matches |
| 796 | * @return string |
| 797 | */ |
| 798 | private function shortenHslAndRgbToHexCallback($matches) |
| 799 | { |
| 800 | $type = $matches[1]; |
| 801 | $values = explode(',', $matches[2]); |
| 802 | $terminator = $matches[3]; |
| 803 | |
| 804 | if ($type === 'hsl') { |
| 805 | $values = Utils::hslToRgb($values); |
| 806 | } |
| 807 | |
| 808 | $hexColors = Utils::rgbToHex($values); |
| 809 | |
| 810 | // Restore space after rgb() or hsl() function in some cases such as: |
| 811 | // background-image: linear-gradient(to bottom, rgb(210,180,140) 10%, rgb(255,0,0) 90%); |
| 812 | if (!empty($terminator) && !preg_match('/[ ,);]/S', $terminator)) { |
| 813 | $terminator = ' '. $terminator; |
| 814 | } |
| 815 | |
| 816 | return '#'. implode('', $hexColors) . $terminator; |
| 817 | } |
| 818 | |
| 819 | /** |
| 820 | * Compresses HEX color values of the form #AABBCC to #ABC or short color name. |
| 821 | * @param $matches |
| 822 | * @return string |
| 823 | */ |
| 824 | private function shortenHexColorsCallback($matches) |
| 825 | { |
| 826 | $hex = $matches[1]; |
| 827 | |
| 828 | // Shorten suitable 6 chars HEX colors |
| 829 | if (strlen($hex) === 6 && preg_match('/^([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3$/Si', $hex, $m)) { |
| 830 | $hex = $m[1] . $m[2] . $m[3]; |
| 831 | } |
| 832 | |
| 833 | // Lowercase |
| 834 | $hex = '#'. strtolower($hex); |
| 835 | |
| 836 | // Replace Hex colors with shorter color names |
| 837 | $color = array_key_exists($hex, $this->hexToNamedColorsMap) ? $this->hexToNamedColorsMap[$hex] : $hex; |
| 838 | |
| 839 | return $color . $matches[2]; |
| 840 | } |
| 841 | |
| 842 | /** |
| 843 | * Shortens all named colors with a shorter HEX counterpart for a set of safe properties |
| 844 | * e.g. white -> #fff |
| 845 | * @param array $matches |
| 846 | * @return string |
| 847 | */ |
| 848 | private function shortenNamedColorsCallback($matches) |
| 849 | { |
| 850 | return $matches[1] . $this->namedToHexColorsMap[strtolower($matches[2])] . $matches[3]; |
| 851 | } |
| 852 | |
| 853 | /** |
| 854 | * Makes a string lowercase |
| 855 | * @param array $matches |
| 856 | * @return string |
| 857 | */ |
| 858 | private function strtolowerCallback($matches) |
| 859 | { |
| 860 | return strtolower($matches[0]); |
| 861 | } |
| 862 | } |
| 863 |