PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.2.21
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.2.21
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / vendor / tubalmartin / cssmin / src / Minifier.php

Minifier.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 3.2.21, at vendor/tubalmartin/cssmin/src/Minifier.php

896 lines 31.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 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)
808 {
809 if ($this->linebreakPosition > 0) {
810 $l = strlen($css);
811 $offset = $this->linebreakPosition;
812 while (preg_match('/(?<!\\\\)\}(?!\n)/S', $css, $matches, PREG_OFFSET_CAPTURE, $offset)) {
813 $matchIndex = $matches[0][1];
814 $css = substr_replace($css, "\n", $matchIndex + 1, 0);
815 $offset = $matchIndex + 2 + $this->linebreakPosition;
816 $l += 1;
817 if ($offset > $l) {
818 break;
819 }
820 }
821 }
822
823 return $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)
882 {
883 return $matches[1] . $this->namedToHexColorsMap[strtolower($matches[2])] . $matches[3];
884 }
885
886 /**
887 * Makes a string lowercase
888 * @param array $matches
889 * @return string
890 */
891 private function strtolowerCallback($matches)
892 {
893 return strtolower($matches[0]);
894 }
895 }
896