| Total Complexity | 69 |
| Total Lines | 869 |
| Duplicated Lines | 0 % |
| Changes | 1 | ||
| Bugs | 0 | Features | 0 |
Complex classes like Minifier often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Minifier, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 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 import At-rules with unquoted URLs so URI reserved characters such as a semicolon may be used safely. |
||
| 333 | $css = preg_replace_callback( |
||
| 334 | '/@import url\(([^\'"]+?)\)( |;)/Si', |
||
| 335 | array($this, 'processImportUnquotedUrlAtRulesCallback'), |
||
| 336 | $css |
||
| 337 | ); |
||
| 338 | |||
| 339 | // Process comments |
||
| 340 | $css = $this->processComments($css); |
||
| 341 | |||
| 342 | // Process rule bodies |
||
| 343 | $css = $this->processRuleBodies($css); |
||
| 344 | |||
| 345 | // Process at-rules and selectors |
||
| 346 | $css = $this->processAtRulesAndSelectors($css); |
||
| 347 | |||
| 348 | // Restore preserved rule bodies before splitting |
||
| 349 | $css = strtr($css, $this->ruleBodies); |
||
| 350 | |||
| 351 | // Split long lines in output if required |
||
| 352 | $css = $this->processLongLineSplitting($css); |
||
| 353 | |||
| 354 | // Restore preserved comments and strings |
||
| 355 | $css = strtr($css, $this->preservedTokens); |
||
| 356 | |||
| 357 | return trim($css); |
||
| 358 | } |
||
| 359 | |||
| 360 | /** |
||
| 361 | * Searches & replaces all data urls with tokens before we start compressing, |
||
| 362 | * to avoid performance issues running some of the subsequent regexes against large string chunks. |
||
| 363 | * @param string $css |
||
| 364 | * @return string |
||
| 365 | */ |
||
| 366 | private function processDataUrls($css) |
||
| 367 | { |
||
| 368 | $ret = ''; |
||
| 369 | $searchOffset = $substrOffset = 0; |
||
| 370 | |||
| 371 | // Since we need to account for non-base64 data urls, we need to handle |
||
| 372 | // ' and ) being part of the data string. |
||
| 373 | while (preg_match('/url\(\s*(["\']?)data:/Si', $css, $m, PREG_OFFSET_CAPTURE, $searchOffset)) { |
||
| 374 | $matchStartIndex = $m[0][1]; |
||
| 375 | $dataStartIndex = $matchStartIndex + 4; // url( length |
||
| 376 | $searchOffset = $matchStartIndex + strlen($m[0][0]); |
||
| 377 | $terminator = $m[1][0]; // ', " or empty (not quoted) |
||
| 378 | $terminatorRegex = '/(?<!\\\\)'. (strlen($terminator) === 0 ? '' : $terminator.'\s*') .'(\))/S'; |
||
| 379 | |||
| 380 | $ret .= substr($css, $substrOffset, $matchStartIndex - $substrOffset); |
||
| 381 | |||
| 382 | // Terminator found |
||
| 383 | if (preg_match($terminatorRegex, $css, $matches, PREG_OFFSET_CAPTURE, $searchOffset)) { |
||
| 384 | $matchEndIndex = $matches[1][1]; |
||
| 385 | $searchOffset = $matchEndIndex + 1; |
||
| 386 | $token = substr($css, $dataStartIndex, $matchEndIndex - $dataStartIndex); |
||
| 387 | |||
| 388 | // Remove all spaces only for base64 encoded URLs. |
||
| 389 | if (stripos($token, 'base64,') !== false) { |
||
| 390 | $token = preg_replace('/\s+/S', '', $token); |
||
| 391 | } |
||
| 392 | |||
| 393 | $ret .= 'url('. $this->registerPreservedToken(trim($token)) .')'; |
||
| 394 | // No end terminator found, re-add the whole match. Should we throw/warn here? |
||
| 395 | } else { |
||
| 396 | $ret .= substr($css, $matchStartIndex, $searchOffset - $matchStartIndex); |
||
| 397 | } |
||
| 398 | |||
| 399 | $substrOffset = $searchOffset; |
||
| 400 | } |
||
| 401 | |||
| 402 | $ret .= substr($css, $substrOffset); |
||
| 403 | |||
| 404 | return $ret; |
||
| 405 | } |
||
| 406 | |||
| 407 | /** |
||
| 408 | * Registers all comments found as candidates to be preserved. |
||
| 409 | * @param array $matches |
||
| 410 | * @return string |
||
| 411 | */ |
||
| 412 | private function processCommentsCallback($matches) |
||
| 413 | { |
||
| 414 | return '/*'. $this->registerCommentToken($matches[1]) .'*/'; |
||
| 415 | } |
||
| 416 | |||
| 417 | /** |
||
| 418 | * Preserves old IE Matrix string definition |
||
| 419 | * @param array $matches |
||
| 420 | * @return string |
||
| 421 | */ |
||
| 422 | private function processOldIeSpecificMatrixDefinitionCallback($matches) |
||
| 423 | { |
||
| 424 | return 'filter:progid:DXImageTransform.Microsoft.Matrix('. $this->registerPreservedToken($matches[1]) .')'; |
||
| 425 | } |
||
| 426 | |||
| 427 | /** |
||
| 428 | * Preserves strings found |
||
| 429 | * @param array $matches |
||
| 430 | * @return string |
||
| 431 | */ |
||
| 432 | private function processStringsCallback($matches) |
||
| 433 | { |
||
| 434 | $match = $matches[0]; |
||
| 435 | $quote = substr($match, 0, 1); |
||
| 436 | $match = substr($match, 1, -1); |
||
| 437 | |||
| 438 | // maybe the string contains a comment-like substring? |
||
| 439 | // one, maybe more? put'em back then |
||
| 440 | if (strpos($match, self::COMMENT_TOKEN_START) !== false) { |
||
| 441 | $match = strtr($match, $this->comments); |
||
| 442 | } |
||
| 443 | |||
| 444 | // minify alpha opacity in filter strings |
||
| 445 | $match = str_ireplace('progid:DXImageTransform.Microsoft.Alpha(Opacity=', 'alpha(opacity=', $match); |
||
| 446 | |||
| 447 | return $quote . $this->registerPreservedToken($match) . $quote; |
||
|
|
|||
| 448 | } |
||
| 449 | |||
| 450 | /** |
||
| 451 | * Searches & replaces all import at-rule unquoted urls with tokens so URI reserved characters such as a semicolon |
||
| 452 | * may be used safely in a URL. |
||
| 453 | * @param array $matches |
||
| 454 | * @return string |
||
| 455 | */ |
||
| 456 | private function processImportUnquotedUrlAtRulesCallback($matches) |
||
| 457 | { |
||
| 458 | return '@import url('. $this->registerPreservedToken($matches[1]) .')'. $matches[2]; |
||
| 459 | } |
||
| 460 | |||
| 461 | /** |
||
| 462 | * Preserves or removes comments found. |
||
| 463 | * @param string $css |
||
| 464 | * @return string |
||
| 465 | */ |
||
| 466 | private function processComments($css) |
||
| 467 | { |
||
| 468 | foreach ($this->comments as $commentId => $comment) { |
||
| 469 | $commentIdString = '/*'. $commentId .'*/'; |
||
| 470 | |||
| 471 | // ! in the first position of the comment means preserve |
||
| 472 | // so push to the preserved tokens keeping the ! |
||
| 473 | if ($this->keepImportantComments && strpos($comment, '!') === 0) { |
||
| 474 | $preservedTokenId = $this->registerPreservedToken($comment); |
||
| 475 | // Put new lines before and after /*! important comments |
||
| 476 | $css = str_replace($commentIdString, "\n/*$preservedTokenId*/\n", $css); |
||
| 477 | continue; |
||
| 478 | } |
||
| 479 | |||
| 480 | // # sourceMappingURL= in the first position of the comment means sourcemap |
||
| 481 | // so push to the preserved tokens if {$this->keepSourceMapComment} is truthy. |
||
| 482 | if ($this->keepSourceMapComment && strpos($comment, '# sourceMappingURL=') === 0) { |
||
| 483 | $preservedTokenId = $this->registerPreservedToken($comment); |
||
| 484 | // Add new line before the sourcemap comment |
||
| 485 | $css = str_replace($commentIdString, "\n/*$preservedTokenId*/", $css); |
||
| 486 | continue; |
||
| 487 | } |
||
| 488 | |||
| 489 | // Keep empty comments after child selectors (IE7 hack) |
||
| 490 | // e.g. html >/**/ body |
||
| 491 | if (strlen($comment) === 0 && strpos($css, '>/*'.$commentId) !== false) { |
||
| 492 | $css = str_replace($commentId, $this->registerPreservedToken(''), $css); |
||
| 493 | continue; |
||
| 494 | } |
||
| 495 | |||
| 496 | // in all other cases kill the comment |
||
| 497 | $css = str_replace($commentIdString, '', $css); |
||
| 498 | } |
||
| 499 | |||
| 500 | // Normalize whitespace again |
||
| 501 | $css = preg_replace('/ +/S', ' ', $css); |
||
| 502 | |||
| 503 | return $css; |
||
| 504 | } |
||
| 505 | |||
| 506 | /** |
||
| 507 | * Finds, minifies & preserves all rule bodies. |
||
| 508 | * @param string $css the whole stylesheet. |
||
| 509 | * @return string |
||
| 510 | */ |
||
| 511 | private function processRuleBodies($css) |
||
| 512 | { |
||
| 513 | $ret = ''; |
||
| 514 | $searchOffset = $substrOffset = 0; |
||
| 515 | |||
| 516 | while (($blockStartPos = strpos($css, '{', $searchOffset)) !== false) { |
||
| 517 | $blockEndPos = strpos($css, '}', $blockStartPos); |
||
| 518 | $nextBlockStartPos = strpos($css, '{', $blockStartPos + 1); |
||
| 519 | $ret .= substr($css, $substrOffset, $blockStartPos - $substrOffset); |
||
| 520 | |||
| 521 | if ($nextBlockStartPos !== false && $nextBlockStartPos < $blockEndPos) { |
||
| 522 | $ret .= substr($css, $blockStartPos, $nextBlockStartPos - $blockStartPos); |
||
| 523 | $searchOffset = $nextBlockStartPos; |
||
| 524 | } else { |
||
| 525 | $ruleBody = substr($css, $blockStartPos + 1, $blockEndPos - $blockStartPos - 1); |
||
| 526 | $ruleBodyToken = $this->registerRuleBodyToken($this->processRuleBody($ruleBody)); |
||
| 527 | $ret .= '{'. $ruleBodyToken .'}'; |
||
| 528 | $searchOffset = $blockEndPos + 1; |
||
| 529 | } |
||
| 530 | |||
| 531 | $substrOffset = $searchOffset; |
||
| 532 | } |
||
| 533 | |||
| 534 | $ret .= substr($css, $substrOffset); |
||
| 535 | |||
| 536 | return $ret; |
||
| 537 | } |
||
| 538 | |||
| 539 | /** |
||
| 540 | * Compresses non-group rule bodies. |
||
| 541 | * @param string $body The rule body without curly braces |
||
| 542 | * @return string |
||
| 543 | */ |
||
| 544 | private function processRuleBody($body) |
||
| 545 | { |
||
| 546 | $body = trim($body); |
||
| 547 | |||
| 548 | // Remove spaces before the things that should not have spaces before them. |
||
| 549 | $body = preg_replace('/ ([:=,)*\/;\n])/S', '$1', $body); |
||
| 550 | |||
| 551 | // Remove the spaces after the things that should not have spaces after them. |
||
| 552 | $body = preg_replace('/([:=,(*\/!;\n]) /S', '$1', $body); |
||
| 553 | |||
| 554 | // Replace multiple semi-colons in a row by a single one |
||
| 555 | $body = preg_replace('/;;+/S', ';', $body); |
||
| 556 | |||
| 557 | // Remove semicolon before closing brace except when: |
||
| 558 | // - The last property is prefixed with a `*` (lte IE7 hack) to avoid issues on Symbian S60 3.x browsers. |
||
| 559 | if (!preg_match('/\*[a-z0-9-]+:[^;]+;$/Si', $body)) { |
||
| 560 | $body = rtrim($body, ';'); |
||
| 561 | } |
||
| 562 | |||
| 563 | // Remove important comments inside a rule body (because they make no sense here). |
||
| 564 | if (strpos($body, '/*') !== false) { |
||
| 565 | $body = preg_replace('/\n?\/\*[A-Z0-9_]+\*\/\n?/S', '', $body); |
||
| 566 | } |
||
| 567 | |||
| 568 | // Empty rule body? Exit :) |
||
| 569 | if (empty($body)) { |
||
| 570 | return ''; |
||
| 571 | } |
||
| 572 | |||
| 573 | // Shorten font-weight values |
||
| 574 | $body = preg_replace( |
||
| 575 | array('/(font-weight:)bold\b/Si', '/(font-weight:)normal\b/Si'), |
||
| 576 | array('${1}700', '${1}400'), |
||
| 577 | $body |
||
| 578 | ); |
||
| 579 | |||
| 580 | // Shorten background property |
||
| 581 | $body = preg_replace('/(background:)(?:none|transparent)( !|;|$)/Si', '${1}0 0$2', $body); |
||
| 582 | |||
| 583 | // Shorten opacity IE filter |
||
| 584 | $body = str_ireplace('progid:DXImageTransform.Microsoft.Alpha(Opacity=', 'alpha(opacity=', $body); |
||
| 585 | |||
| 586 | // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space) |
||
| 587 | // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space) |
||
| 588 | // This makes it more likely that it'll get further compressed in the next step. |
||
| 589 | $body = preg_replace_callback( |
||
| 590 | '/(rgb|hsl)\(([0-9,.% -]+)\)(.|$)/Si', |
||
| 591 | array($this, 'shortenHslAndRgbToHexCallback'), |
||
| 592 | $body |
||
| 593 | ); |
||
| 594 | |||
| 595 | // Shorten colors from #AABBCC to #ABC or shorter color name: |
||
| 596 | // - Look for hex colors which don't have a "=" in front of them (to avoid MSIE filters) |
||
| 597 | $body = preg_replace_callback( |
||
| 598 | '/(?<!=)#([0-9a-f]{3,6})( |,|\)|;|$)/Si', |
||
| 599 | array($this, 'shortenHexColorsCallback'), |
||
| 600 | $body |
||
| 601 | ); |
||
| 602 | |||
| 603 | // Shorten long named colors with a shorter HEX counterpart: white -> #fff. |
||
| 604 | // Run at least 2 times to cover most cases |
||
| 605 | $body = preg_replace_callback( |
||
| 606 | array($this->namedToHexColorsRegex, $this->namedToHexColorsRegex), |
||
| 607 | array($this, 'shortenNamedColorsCallback'), |
||
| 608 | $body |
||
| 609 | ); |
||
| 610 | |||
| 611 | // Replace positive sign from numbers before the leading space is removed. |
||
| 612 | // +1.2em to 1.2em, +.8px to .8px, +2% to 2% |
||
| 613 | $body = preg_replace('/([ :,(])\+(\.?\d+)/S', '$1$2', $body); |
||
| 614 | |||
| 615 | // shorten ms to s |
||
| 616 | $body = preg_replace_callback('/([ :,(])(-?)(\d{3,})ms/Si', function ($matches) { |
||
| 617 | return $matches[1] . $matches[2] . ((int) $matches[3] / 1000) .'s'; |
||
| 618 | }, $body); |
||
| 619 | |||
| 620 | // Remove leading zeros from integer and float numbers. |
||
| 621 | // 000.6 to .6, -0.8 to -.8, 0050 to 50, -01.05 to -1.05 |
||
| 622 | $body = preg_replace('/([ :,(])(-?)0+([1-9]?\.?\d+)/S', '$1$2$3', $body); |
||
| 623 | |||
| 624 | // Remove trailing zeros from float numbers. |
||
| 625 | // -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px |
||
| 626 | $body = preg_replace('/([ :,(])(-?\d?\.\d+?)0+([^\d])/S', '$1$2$3', $body); |
||
| 627 | |||
| 628 | // Remove trailing .0 -> -9.0 to -9 |
||
| 629 | $body = preg_replace('/([ :,(])(-?\d+)\.0([^\d])/S', '$1$2$3', $body); |
||
| 630 | |||
| 631 | // Replace 0 length numbers with 0 |
||
| 632 | $body = preg_replace('/([ :,(])-?\.?0+([^\d])/S', '${1}0$2', $body); |
||
| 633 | |||
| 634 | // Shorten zero values for safe properties only |
||
| 635 | $body = preg_replace( |
||
| 636 | array( |
||
| 637 | $this->shortenOneZeroesRegex, |
||
| 638 | $this->shortenTwoZeroesRegex, |
||
| 639 | $this->shortenThreeZeroesRegex, |
||
| 640 | $this->shortenFourZeroesRegex |
||
| 641 | ), |
||
| 642 | array( |
||
| 643 | '$1$2:0', |
||
| 644 | '$1$2:$3 0', |
||
| 645 | '$1$2:$3 $4 0', |
||
| 646 | '$1$2:$3 $4 $5 0' |
||
| 647 | ), |
||
| 648 | $body |
||
| 649 | ); |
||
| 650 | |||
| 651 | // Replace 0 0 0; or 0 0 0 0; with 0 0 for background-position property. |
||
| 652 | $body = preg_replace('/(background-position):0(?: 0){2,3}( !|;|$)/Si', '$1:0 0$2', $body); |
||
| 653 | |||
| 654 | // Shorten suitable shorthand properties with repeated values |
||
| 655 | $body = preg_replace( |
||
| 656 | array( |
||
| 657 | '/(margin|padding|border-(?:width|radius)):('.$this->numRegex.')(?: \2)+( !|;|$)/Si', |
||
| 658 | '/(border-(?:style|color)):([#a-z0-9]+)(?: \2)+( !|;|$)/Si' |
||
| 659 | ), |
||
| 660 | '$1:$2$3', |
||
| 661 | $body |
||
| 662 | ); |
||
| 663 | $body = preg_replace( |
||
| 664 | array( |
||
| 665 | '/(margin|padding|border-(?:width|radius)):'. |
||
| 666 | '('.$this->numRegex.') ('.$this->numRegex.') \2 \3( !|;|$)/Si', |
||
| 667 | '/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) \2 \3( !|;|$)/Si' |
||
| 668 | ), |
||
| 669 | '$1:$2 $3$4', |
||
| 670 | $body |
||
| 671 | ); |
||
| 672 | $body = preg_replace( |
||
| 673 | array( |
||
| 674 | '/(margin|padding|border-(?:width|radius)):'. |
||
| 675 | '('.$this->numRegex.') ('.$this->numRegex.') ('.$this->numRegex.') \3( !|;|$)/Si', |
||
| 676 | '/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) ([#a-z0-9]+) \3( !|;|$)/Si' |
||
| 677 | ), |
||
| 678 | '$1:$2 $3 $4$5', |
||
| 679 | $body |
||
| 680 | ); |
||
| 681 | |||
| 682 | // Lowercase some common functions that can be values |
||
| 683 | $body = preg_replace_callback( |
||
| 684 | '/(?:attr|blur|brightness|circle|contrast|cubic-bezier|drop-shadow|ellipse|from|grayscale|'. |
||
| 685 | 'hsla?|hue-rotate|inset|invert|local|minmax|opacity|perspective|polygon|rgba?|rect|repeat|saturate|sepia|'. |
||
| 686 | 'steps|to|url|var|-webkit-gradient|'. |
||
| 687 | '(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|(?:repeating-)?(?:linear|radial)-gradient))\(/Si', |
||
| 688 | array($this, 'strtolowerCallback'), |
||
| 689 | $body |
||
| 690 | ); |
||
| 691 | |||
| 692 | // Lowercase all uppercase properties |
||
| 693 | $body = preg_replace_callback('/(?:^|;)[A-Z-]+:/S', array($this, 'strtolowerCallback'), $body); |
||
| 694 | |||
| 695 | return $body; |
||
| 696 | } |
||
| 697 | |||
| 698 | /** |
||
| 699 | * Compresses At-rules and selectors. |
||
| 700 | * @param string $css the whole stylesheet with rule bodies tokenized. |
||
| 701 | * @return string |
||
| 702 | */ |
||
| 703 | private function processAtRulesAndSelectors($css) |
||
| 704 | { |
||
| 705 | $charset = ''; |
||
| 706 | $imports = ''; |
||
| 707 | $namespaces = ''; |
||
| 708 | |||
| 709 | // Remove spaces before the things that should not have spaces before them. |
||
| 710 | $css = preg_replace('/ ([@{};>+)\]~=,\/\n])/S', '$1', $css); |
||
| 711 | |||
| 712 | // Remove the spaces after the things that should not have spaces after them. |
||
| 713 | $css = preg_replace('/([{}:;>+(\[~=,\/\n]) /S', '$1', $css); |
||
| 714 | |||
| 715 | // Shorten shortable double colon (CSS3) pseudo-elements to single colon (CSS2) |
||
| 716 | $css = preg_replace('/::(before|after|first-(?:line|letter))(\{|,)/Si', ':$1$2', $css); |
||
| 717 | |||
| 718 | // Retain space for special IE6 cases |
||
| 719 | $css = preg_replace_callback('/:first-(line|letter)(\{|,)/Si', function ($matches) { |
||
| 720 | return ':first-'. strtolower($matches[1]) .' '. $matches[2]; |
||
| 721 | }, $css); |
||
| 722 | |||
| 723 | // Find a fraction that may used in some @media queries such as: (min-aspect-ratio: 1/1) |
||
| 724 | // Add token to add the "/" back in later |
||
| 725 | $css = preg_replace('/\(([a-z-]+):([0-9]+)\/([0-9]+)\)/Si', '($1:$2'. self::QUERY_FRACTION .'$3)', $css); |
||
| 726 | |||
| 727 | // Remove empty rule blocks up to 2 levels deep. |
||
| 728 | $css = preg_replace(array_fill(0, 2, '/(\{)[^{};\/\n]+\{\}/S'), '$1', $css); |
||
| 729 | $css = preg_replace('/[^{};\/\n]+\{\}/S', '', $css); |
||
| 730 | |||
| 731 | // Two important comments next to each other? Remove extra newline. |
||
| 732 | if ($this->keepImportantComments) { |
||
| 733 | $css = str_replace("\n\n", "\n", $css); |
||
| 734 | } |
||
| 735 | |||
| 736 | // Restore fraction |
||
| 737 | $css = str_replace(self::QUERY_FRACTION, '/', $css); |
||
| 738 | |||
| 739 | // Lowercase some popular @directives |
||
| 740 | $css = preg_replace_callback( |
||
| 741 | '/(?<!\\\\)@(?:charset|document|font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframes|media|'. |
||
| 742 | 'namespace|page|supports|viewport)/Si', |
||
| 743 | array($this, 'strtolowerCallback'), |
||
| 744 | $css |
||
| 745 | ); |
||
| 746 | |||
| 747 | // Lowercase some popular media types |
||
| 748 | $css = preg_replace_callback( |
||
| 749 | '/[ ,](?:all|aural|braille|handheld|print|projection|screen|tty|tv|embossed|speech)[ ,;{]/Si', |
||
| 750 | array($this, 'strtolowerCallback'), |
||
| 751 | $css |
||
| 752 | ); |
||
| 753 | |||
| 754 | // Lowercase some common pseudo-classes & pseudo-elements |
||
| 755 | $css = preg_replace_callback( |
||
| 756 | '/(?<!\\\\):(?:active|after|before|checked|default|disabled|empty|enabled|first-(?:child|of-type)|'. |
||
| 757 | 'focus(?:-within)?|hover|indeterminate|in-range|invalid|lang\(|last-(?:child|of-type)|left|link|not\(|'. |
||
| 758 | 'nth-(?:child|of-type)\(|nth-last-(?:child|of-type)\(|only-(?:child|of-type)|optional|out-of-range|'. |
||
| 759 | 'read-(?:only|write)|required|right|root|:selection|target|valid|visited)/Si', |
||
| 760 | array($this, 'strtolowerCallback'), |
||
| 761 | $css |
||
| 762 | ); |
||
| 763 | |||
| 764 | // @charset handling |
||
| 765 | if (preg_match($this->charsetRegex, $css, $matches)) { |
||
| 766 | // Keep the first @charset at-rule found |
||
| 767 | $charset = $matches[0]; |
||
| 768 | // Delete all @charset at-rules |
||
| 769 | $css = preg_replace($this->charsetRegex, '', $css); |
||
| 770 | } |
||
| 771 | |||
| 772 | // @import handling |
||
| 773 | $css = preg_replace_callback($this->importRegex, function ($matches) use (&$imports) { |
||
| 774 | // Keep all @import at-rules found for later |
||
| 775 | $imports .= $matches[0]; |
||
| 776 | // Delete all @import at-rules |
||
| 777 | return ''; |
||
| 778 | }, $css); |
||
| 779 | |||
| 780 | // @namespace handling |
||
| 781 | $css = preg_replace_callback($this->namespaceRegex, function ($matches) use (&$namespaces) { |
||
| 782 | // Keep all @namespace at-rules found for later |
||
| 783 | $namespaces .= $matches[0]; |
||
| 784 | // Delete all @namespace at-rules |
||
| 785 | return ''; |
||
| 786 | }, $css); |
||
| 787 | |||
| 788 | // Order critical at-rules: |
||
| 789 | // 1. @charset first |
||
| 790 | // 2. @imports below @charset |
||
| 791 | // 3. @namespaces below @imports |
||
| 792 | $css = $charset . $imports . $namespaces . $css; |
||
| 793 | |||
| 794 | return $css; |
||
| 795 | } |
||
| 796 | |||
| 797 | /** |
||
| 798 | * Splits long lines after a specific column. |
||
| 799 | * |
||
| 800 | * Some source control tools don't like it when files containing lines longer |
||
| 801 | * than, say 8000 characters, are checked in. The linebreak option is used in |
||
| 802 | * that case to split long lines after a specific column. |
||
| 803 | * |
||
| 804 | * @param string $css the whole stylesheet. |
||
| 805 | * @return string |
||
| 806 | */ |
||
| 807 | private function processLongLineSplitting($css) |
||
| 824 | } |
||
| 825 | |||
| 826 | /** |
||
| 827 | * Converts hsl() & rgb() colors to HEX format. |
||
| 828 | * @param $matches |
||
| 829 | * @return string |
||
| 830 | */ |
||
| 831 | private function shortenHslAndRgbToHexCallback($matches) |
||
| 832 | { |
||
| 833 | $type = $matches[1]; |
||
| 834 | $values = explode(',', $matches[2]); |
||
| 835 | $terminator = $matches[3]; |
||
| 836 | |||
| 837 | if ($type === 'hsl') { |
||
| 838 | $values = Utils::hslToRgb($values); |
||
| 839 | } |
||
| 840 | |||
| 841 | $hexColors = Utils::rgbToHex($values); |
||
| 842 | |||
| 843 | // Restore space after rgb() or hsl() function in some cases such as: |
||
| 844 | // background-image: linear-gradient(to bottom, rgb(210,180,140) 10%, rgb(255,0,0) 90%); |
||
| 845 | if (!empty($terminator) && !preg_match('/[ ,);]/S', $terminator)) { |
||
| 846 | $terminator = ' '. $terminator; |
||
| 847 | } |
||
| 848 | |||
| 849 | return '#'. implode('', $hexColors) . $terminator; |
||
| 850 | } |
||
| 851 | |||
| 852 | /** |
||
| 853 | * Compresses HEX color values of the form #AABBCC to #ABC or short color name. |
||
| 854 | * @param $matches |
||
| 855 | * @return string |
||
| 856 | */ |
||
| 857 | private function shortenHexColorsCallback($matches) |
||
| 858 | { |
||
| 859 | $hex = $matches[1]; |
||
| 860 | |||
| 861 | // Shorten suitable 6 chars HEX colors |
||
| 862 | if (strlen($hex) === 6 && preg_match('/^([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3$/Si', $hex, $m)) { |
||
| 863 | $hex = $m[1] . $m[2] . $m[3]; |
||
| 864 | } |
||
| 865 | |||
| 866 | // Lowercase |
||
| 867 | $hex = '#'. strtolower($hex); |
||
| 868 | |||
| 869 | // Replace Hex colors with shorter color names |
||
| 870 | $color = array_key_exists($hex, $this->hexToNamedColorsMap) ? $this->hexToNamedColorsMap[$hex] : $hex; |
||
| 871 | |||
| 872 | return $color . $matches[2]; |
||
| 873 | } |
||
| 874 | |||
| 875 | /** |
||
| 876 | * Shortens all named colors with a shorter HEX counterpart for a set of safe properties |
||
| 877 | * e.g. white -> #fff |
||
| 878 | * @param array $matches |
||
| 879 | * @return string |
||
| 880 | */ |
||
| 881 | private function shortenNamedColorsCallback($matches) |
||
| 884 | } |
||
| 885 | |||
| 886 | /** |
||
| 887 | * Makes a string lowercase |
||
| 888 | * @param array $matches |
||
| 889 | * @return string |
||
| 890 | */ |
||
| 891 | private function strtolowerCallback($matches) |
||
| 894 | } |
||
| 895 | } |
||
| 896 |