PluginProbe
Autoptimize / 3.1.7
Autoptimize v3.1.7
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 2.7.8 All 107 releases
autoptimize / classes / external / php / yui-php-cssmin-bundled / Minifier.php

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

936 lines 32.5 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 $body = preg_replace_callback(
609 '/(rgb|hsl)\(([0-9,.% -]+)\)(.|$)/Si',
610 array($this, 'shortenHslAndRgbToHexCallback'),
611 $body
612 );
613
614 // Shorten colors from #AABBCC to #ABC or shorter color name:
615 // - Look for hex colors which don't have a "=" in front of them (to avoid MSIE filters)
616 $body = preg_replace_callback(
617 '/(?<!=)#([0-9a-f]{3,6})( |,|\)|;|$)/Si',
618 array($this, 'shortenHexColorsCallback'),
619 $body
620 );
621
622 // Tokenize unquoted font names in order to hide them from
623 // color name replacements.
624 $body = preg_replace_callback(
625 $this->unquotedFontsRegex,
626 array($this, 'preserveUnquotedFontTokens'),
627 $body
628 );
629
630 // Shorten long named colors with a shorter HEX counterpart: white -> #fff.
631 // Run at least 2 times to cover most cases
632 $body = preg_replace_callback(
633 array($this->namedToHexColorsRegex, $this->namedToHexColorsRegex),
634 array($this, 'shortenNamedColorsCallback'),
635 $body
636 );
637
638 // Restore unquoted font tokens now after colors have been changed.
639 $body = $this->restoreUnquotedFontTokens($body);
640
641 // Replace positive sign from numbers before the leading space is removed.
642 // +1.2em to 1.2em, +.8px to .8px, +2% to 2%
643 $body = preg_replace('/([ :,(])\+(\.?\d+)/S', '$1$2', $body);
644
645 // shorten ms to s
646 $body = preg_replace_callback('/([ :,(])(-?)(\d{3,})ms/Si', function ($matches) {
647 return $matches[1] . $matches[2] . ((int) $matches[3] / 1000) .'s';
648 }, $body);
649
650 // Remove leading zeros from integer and float numbers.
651 // 000.6 to .6, -0.8 to -.8, 0050 to 50, -01.05 to -1.05
652 $body = preg_replace('/([ :,(])(-?)0+([1-9]?\.?\d+)/S', '$1$2$3', $body);
653
654 // Remove trailing zeros from float numbers.
655 // -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px
656 $body = preg_replace('/([ :,(])(-?\d?\.\d+?)0+([^\d])/S', '$1$2$3', $body);
657
658 // Remove trailing .0 -> -9.0 to -9
659 $body = preg_replace('/([ :,(])(-?\d+)\.0([^\d])/S', '$1$2$3', $body);
660
661 // Replace 0 length numbers with 0
662 $body = preg_replace('/([ :,(])-?\.?0+([^\d])/S', '${1}0$2', $body);
663
664 // Shorten zero values for safe properties only
665 $body = preg_replace(
666 array(
667 $this->shortenOneZeroesRegex,
668 $this->shortenTwoZeroesRegex,
669 $this->shortenThreeZeroesRegex,
670 $this->shortenFourZeroesRegex
671 ),
672 array(
673 '$1$2:0',
674 '$1$2:$3 0',
675 '$1$2:$3 $4 0',
676 '$1$2:$3 $4 $5 0'
677 ),
678 $body
679 );
680
681 // Replace 0 0 0; or 0 0 0 0; with 0 0 for background-position property.
682 $body = preg_replace('/(background-position):0(?: 0){2,3}( !|;|$)/Si', '$1:0 0$2', $body);
683
684 // Shorten suitable shorthand properties with repeated values
685 $body = preg_replace(
686 array(
687 '/(margin|padding|border-(?:width|radius)):('.$this->numRegex.')(?: \2)+( !|;|$)/Si',
688 '/(border-(?:style|color)):([#a-z0-9]+)(?: \2)+( !|;|$)/Si'
689 ),
690 '$1:$2$3',
691 $body
692 );
693 $body = preg_replace(
694 array(
695 '/(margin|padding|border-(?:width|radius)):'.
696 '('.$this->numRegex.') ('.$this->numRegex.') \2 \3( !|;|$)/Si',
697 '/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) \2 \3( !|;|$)/Si'
698 ),
699 '$1:$2 $3$4',
700 $body
701 );
702 $body = preg_replace(
703 array(
704 '/(margin|padding|border-(?:width|radius)):'.
705 '('.$this->numRegex.') ('.$this->numRegex.') ('.$this->numRegex.') \3( !|;|$)/Si',
706 '/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) ([#a-z0-9]+) \3( !|;|$)/Si'
707 ),
708 '$1:$2 $3 $4$5',
709 $body
710 );
711
712 // Lowercase some common functions that can be values
713 $body = preg_replace_callback(
714 '/(?:attr|blur|brightness|circle|contrast|cubic-bezier|drop-shadow|ellipse|from|grayscale|'.
715 'hsla?|hue-rotate|inset|invert|local|minmax|opacity|perspective|polygon|rgba?|rect|repeat|saturate|sepia|'.
716 'steps|to|url|var|-webkit-gradient|'.
717 '(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|(?:repeating-)?(?:linear|radial)-gradient))\(/Si',
718 array($this, 'strtolowerCallback'),
719 $body
720 );
721
722 // Lowercase all uppercase properties
723 $body = preg_replace_callback('/(?:^|;)[A-Z-]+:/S', array($this, 'strtolowerCallback'), $body);
724
725 return $body;
726 }
727
728 private function preserveUnquotedFontTokens($matches)
729 {
730 return $this->registerUnquotedFontToken($matches[0]);
731 }
732
733 private function restoreUnquotedFontTokens($body)
734 {
735 return strtr($body, $this->unquotedFontTokens);
736 }
737
738 /**
739 * Compresses At-rules and selectors.
740 * @param string $css the whole stylesheet with rule bodies tokenized.
741 * @return string
742 */
743 private function processAtRulesAndSelectors($css)
744 {
745 $charset = '';
746 $imports = '';
747 $namespaces = '';
748
749 // Remove spaces before the things that should not have spaces before them.
750 $css = preg_replace('/ ([@{};>+)\]~=,\/\n])/S', '$1', $css);
751
752 // Remove the spaces after the things that should not have spaces after them.
753 $css = preg_replace('/([{}:;>+(\[~=,\/\n]) /S', '$1', $css);
754
755 // Shorten shortable double colon (CSS3) pseudo-elements to single colon (CSS2)
756 $css = preg_replace('/::(before|after|first-(?:line|letter))(\{|,)/Si', ':$1$2', $css);
757
758 // Retain space for special IE6 cases
759 $css = preg_replace_callback('/:first-(line|letter)(\{|,)/Si', function ($matches) {
760 return ':first-'. strtolower($matches[1]) .' '. $matches[2];
761 }, $css);
762
763 // Find a fraction that may used in some @media queries such as: (min-aspect-ratio: 1/1)
764 // Add token to add the "/" back in later
765 $css = preg_replace('/\(([a-z-]+):([0-9]+)\/([0-9]+)\)/Si', '($1:$2'. self::QUERY_FRACTION .'$3)', $css);
766
767 // Remove empty rule blocks up to 2 levels deep.
768 $css = preg_replace(array_fill(0, 2, '/(\{)[^{};\/\n]+\{\}/S'), '$1', $css);
769 $css = preg_replace('/[^{};\/\n]+\{\}/S', '', $css);
770
771 // Two important comments next to each other? Remove extra newline.
772 if ($this->keepImportantComments) {
773 $css = str_replace("\n\n", "\n", $css);
774 }
775
776 // Restore fraction
777 $css = str_replace(self::QUERY_FRACTION, '/', $css);
778
779 // Lowercase some popular @directives
780 $css = preg_replace_callback(
781 '/(?<!\\\\)@(?:charset|document|font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframes|media|'.
782 'namespace|page|supports|viewport)/Si',
783 array($this, 'strtolowerCallback'),
784 $css
785 );
786
787 // Lowercase some popular media types
788 $css = preg_replace_callback(
789 '/[ ,](?:all|aural|braille|handheld|print|projection|screen|tty|tv|embossed|speech)[ ,;{]/Si',
790 array($this, 'strtolowerCallback'),
791 $css
792 );
793
794 // Lowercase some common pseudo-classes & pseudo-elements
795 $css = preg_replace_callback(
796 '/(?<!\\\\):(?:active|after|before|checked|default|disabled|empty|enabled|first-(?:child|of-type)|'.
797 'focus(?:-within)?|hover|indeterminate|in-range|invalid|lang\(|last-(?:child|of-type)|left|link|not\(|'.
798 'nth-(?:child|of-type)\(|nth-last-(?:child|of-type)\(|only-(?:child|of-type)|optional|out-of-range|'.
799 'read-(?:only|write)|required|right|root|:selection|target|valid|visited)/Si',
800 array($this, 'strtolowerCallback'),
801 $css
802 );
803
804 // @charset handling
805 if (preg_match($this->charsetRegex, $css, $matches)) {
806 // Keep the first @charset at-rule found
807 $charset = $matches[0];
808 // Delete all @charset at-rules
809 $css = preg_replace($this->charsetRegex, '', $css);
810 }
811
812 // @import handling
813 $css = preg_replace_callback($this->importRegex, function ($matches) use (&$imports) {
814 // Keep all @import at-rules found for later
815 $imports .= $matches[0];
816 // Delete all @import at-rules
817 return '';
818 }, $css);
819
820 // @namespace handling
821 $css = preg_replace_callback($this->namespaceRegex, function ($matches) use (&$namespaces) {
822 // Keep all @namespace at-rules found for later
823 $namespaces .= $matches[0];
824 // Delete all @namespace at-rules
825 return '';
826 }, $css);
827
828 // Order critical at-rules:
829 // 1. @charset first
830 // 2. @imports below @charset
831 // 3. @namespaces below @imports
832 $css = $charset . $imports . $namespaces . $css;
833
834 return $css;
835 }
836
837 /**
838 * Splits long lines after a specific column.
839 *
840 * Some source control tools don't like it when files containing lines longer
841 * than, say 8000 characters, are checked in. The linebreak option is used in
842 * that case to split long lines after a specific column.
843 *
844 * @param string $css the whole stylesheet.
845 * @return string
846 */
847 private function processLongLineSplitting($css)
848 {
849 if ($this->linebreakPosition > 0) {
850 $l = strlen($css);
851 $offset = $this->linebreakPosition;
852 while (preg_match('/(?<!\\\\)\}(?!\n)/S', $css, $matches, PREG_OFFSET_CAPTURE, $offset)) {
853 $matchIndex = $matches[0][1];
854 $css = substr_replace($css, "\n", $matchIndex + 1, 0);
855 $offset = $matchIndex + 2 + $this->linebreakPosition;
856 $l += 1;
857 if ($offset > $l) {
858 break;
859 }
860 }
861 }
862
863 return $css;
864 }
865
866 /**
867 * Converts hsl() & rgb() colors to HEX format.
868 * @param $matches
869 * @return string
870 */
871 private function shortenHslAndRgbToHexCallback($matches)
872 {
873 $type = $matches[1];
874 $values = explode(',', $matches[2]);
875 $terminator = $matches[3];
876
877 if ($type === 'hsl') {
878 $values = Utils::hslToRgb($values);
879 }
880
881 $hexColors = Utils::rgbToHex($values);
882
883 // Restore space after rgb() or hsl() function in some cases such as:
884 // background-image: linear-gradient(to bottom, rgb(210,180,140) 10%, rgb(255,0,0) 90%);
885 if (!empty($terminator) && !preg_match('/[ ,);]/S', $terminator)) {
886 $terminator = ' '. $terminator;
887 }
888
889 return '#'. implode('', $hexColors) . $terminator;
890 }
891
892 /**
893 * Compresses HEX color values of the form #AABBCC to #ABC or short color name.
894 * @param $matches
895 * @return string
896 */
897 private function shortenHexColorsCallback($matches)
898 {
899 $hex = $matches[1];
900
901 // Shorten suitable 6 chars HEX colors
902 if (strlen($hex) === 6 && preg_match('/^([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3$/Si', $hex, $m)) {
903 $hex = $m[1] . $m[2] . $m[3];
904 }
905
906 // Lowercase
907 $hex = '#'. strtolower($hex);
908
909 // Replace Hex colors with shorter color names
910 $color = array_key_exists($hex, $this->hexToNamedColorsMap) ? $this->hexToNamedColorsMap[$hex] : $hex;
911
912 return $color . $matches[2];
913 }
914
915 /**
916 * Shortens all named colors with a shorter HEX counterpart for a set of safe properties
917 * e.g. white -> #fff
918 * @param array $matches
919 * @return string
920 */
921 private function shortenNamedColorsCallback($matches)
922 {
923 return $matches[1] . $this->namedToHexColorsMap[strtolower($matches[2])] . $matches[3];
924 }
925
926 /**
927 * Makes a string lowercase
928 * @param array $matches
929 * @return string
930 */
931 private function strtolowerCallback($matches)
932 {
933 return strtolower($matches[0]);
934 }
935 }
936