PluginProbe
Autoptimize / 3.1.16
Autoptimize v3.1.16
3.1.16 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 All 108 releases
autoptimize / classes / external / php / yui-php-cssmin-bundled / Minifier.php

Minifier.php in Autoptimize 3.1.16, at classes/external/php/yui-php-cssmin-bundled/Minifier.php

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