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 |