PluginProbe ʕ •ᴥ•ʔ
10Web Booster – Website speed optimization, Cache & Page Speed optimizer / trunk
10Web Booster – Website speed optimization, Cache & Page Speed optimizer vtrunk
2.33.0 2.30.5 2.30.7 2.30.9 2.31.10 2.31.8 2.32.11 2.32.21 2.32.3 2.32.4 2.32.7 2.6.31 2.6.40 2.6.42 2.6.7 2.7.37 2.7.44 2.7.47 2.8.18 2.8.19 2.8.32 2.8.34 2.8.35 2.9.23 2.9.24 2.9.25 2.9.27 v2.27.4 trunk 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.17 2.0.18 2.0.21 2.0.22 2.0.25 2.0.26 2.0.27 2.0.3 2.0.7 2.0.9 2.10.46 2.10.65 2.10.66 2.10.68 2.11.41 2.11.42 2.11.43 2.12.15 2.12.21 2.12.22 2.12.23 2.12.26 2.13.37 2.13.40 2.13.41 2.13.42 2.13.44 2.13.45 2.13.47 2.14.49 2.14.50 2.15.18 2.17.21 2.17.23 2.18.17 2.19.44 2.19.45 2.19.46 2.19.49 2.2.12 2.2.15 2.2.16 2.2.18 2.2.8 2.20.31 2.20.32 2.20.33 2.21.11 2.21.12 2.21.16 2.21.25 2.22.32 2.23.13 2.23.15 2.23.16 2.23.18 2.24.12 2.24.14 2.24.18 2.25.14 2.26.6 2.28.10 2.28.13 2.28.14 2.28.7 2.29.1 2.29.2 2.29.3 2.3.0 2.3.1 2.3.2 2.3.3 2.30.18
tenweb-speed-optimizer / vendor / matthiasmullie / minify / src / CSS.php
tenweb-speed-optimizer / vendor / matthiasmullie / minify / src Last commit date
Exceptions 3 years ago CSS.php 2 years ago Exception.php 3 years ago JS.php 2 years ago Minify.php 2 years ago
CSS.php
871 lines
1 <?php
2
3 /**
4 * CSS Minifier.
5 *
6 * Please report bugs on https://github.com/matthiasmullie/minify/issues
7 *
8 * @author Matthias Mullie <minify@mullie.eu>
9 * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
10 * @license MIT License
11 */
12
13 namespace MatthiasMullie\Minify;
14
15 use MatthiasMullie\Minify\Exceptions\FileImportException;
16 use MatthiasMullie\PathConverter\Converter;
17 use MatthiasMullie\PathConverter\ConverterInterface;
18
19 /**
20 * CSS minifier.
21 *
22 * Please report bugs on https://github.com/matthiasmullie/minify/issues
23 *
24 * @author Matthias Mullie <minify@mullie.eu>
25 * @author Tijs Verkoyen <minify@verkoyen.eu>
26 * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
27 * @license MIT License
28 */
29 class CSS extends Minify
30 {
31 /**
32 * @var int maximum inport size in kB
33 */
34 protected $maxImportSize = 5;
35
36 /**
37 * @var string[] valid import extensions
38 */
39 protected $importExtensions = array(
40 'gif' => 'data:image/gif',
41 'png' => 'data:image/png',
42 'jpe' => 'data:image/jpeg',
43 'jpg' => 'data:image/jpeg',
44 'jpeg' => 'data:image/jpeg',
45 'svg' => 'data:image/svg+xml',
46 'woff' => 'data:application/x-font-woff',
47 'woff2' => 'data:application/x-font-woff2',
48 'avif' => 'data:image/avif',
49 'apng' => 'data:image/apng',
50 'webp' => 'data:image/webp',
51 'tif' => 'image/tiff',
52 'tiff' => 'image/tiff',
53 'xbm' => 'image/x-xbitmap',
54 );
55
56 /**
57 * Set the maximum size if files to be imported.
58 *
59 * Files larger than this size (in kB) will not be imported into the CSS.
60 * Importing files into the CSS as data-uri will save you some connections,
61 * but we should only import relatively small decorative images so that our
62 * CSS file doesn't get too bulky.
63 *
64 * @param int $size Size in kB
65 */
66 public function setMaxImportSize($size)
67 {
68 $this->maxImportSize = $size;
69 }
70
71 /**
72 * Set the type of extensions to be imported into the CSS (to save network
73 * connections).
74 * Keys of the array should be the file extensions & respective values
75 * should be the data type.
76 *
77 * @param string[] $extensions Array of file extensions
78 */
79 public function setImportExtensions(array $extensions)
80 {
81 $this->importExtensions = $extensions;
82 }
83
84 /**
85 * Move any import statements to the top.
86 *
87 * @param string $content Nearly finished CSS content
88 *
89 * @return string
90 */
91 protected function moveImportsToTop($content)
92 {
93 if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) {
94 // remove from content
95 foreach ($matches[0] as $import) {
96 $content = str_replace($import, '', $content);
97 }
98
99 // add to top
100 $content = implode(';', $matches[2]) . ';' . trim($content, ';');
101 }
102
103 return $content;
104 }
105
106 /**
107 * Combine CSS from import statements.
108 *
109 * \@import's will be loaded and their content merged into the original file,
110 * to save HTTP requests.
111 *
112 * @param string $source The file to combine imports for
113 * @param string $content The CSS content to combine imports for
114 * @param string[] $parents Parent paths, for circular reference checks
115 *
116 * @return string
117 *
118 * @throws FileImportException
119 */
120 protected function combineImports($source, $content, $parents)
121 {
122 $importRegexes = array(
123 // @import url(xxx)
124 '/
125 # import statement
126 @import
127
128 # whitespace
129 \s+
130
131 # open url()
132 url\(
133
134 # (optional) open path enclosure
135 (?P<quotes>["\']?)
136
137 # fetch path
138 (?P<path>.+?)
139
140 # (optional) close path enclosure
141 (?P=quotes)
142
143 # close url()
144 \)
145
146 # (optional) trailing whitespace
147 \s*
148
149 # (optional) media statement(s)
150 (?P<media>[^;]*)
151
152 # (optional) trailing whitespace
153 \s*
154
155 # (optional) closing semi-colon
156 ;?
157
158 /ix',
159
160 // @import 'xxx'
161 '/
162
163 # import statement
164 @import
165
166 # whitespace
167 \s+
168
169 # open path enclosure
170 (?P<quotes>["\'])
171
172 # fetch path
173 (?P<path>.+?)
174
175 # close path enclosure
176 (?P=quotes)
177
178 # (optional) trailing whitespace
179 \s*
180
181 # (optional) media statement(s)
182 (?P<media>[^;]*)
183
184 # (optional) trailing whitespace
185 \s*
186
187 # (optional) closing semi-colon
188 ;?
189
190 /ix',
191 );
192
193 // find all relative imports in css
194 $matches = array();
195 foreach ($importRegexes as $importRegex) {
196 if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) {
197 $matches = array_merge($matches, $regexMatches);
198 }
199 }
200
201 $search = array();
202 $replace = array();
203
204 // loop the matches
205 foreach ($matches as $match) {
206 // get the path for the file that will be imported
207 $importPath = dirname($source) . '/' . $match['path'];
208
209 // only replace the import with the content if we can grab the
210 // content of the file
211 if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) {
212 continue;
213 }
214
215 // check if current file was not imported previously in the same
216 // import chain.
217 if (in_array($importPath, $parents)) {
218 throw new FileImportException('Failed to import file "' . $importPath . '": circular reference detected.');
219 }
220
221 // grab referenced file & minify it (which may include importing
222 // yet other @import statements recursively)
223 $minifier = new self($importPath);
224 $minifier->setMaxImportSize($this->maxImportSize);
225 $minifier->setImportExtensions($this->importExtensions);
226 $importContent = $minifier->execute($source, $parents);
227
228 // check if this is only valid for certain media
229 if (!empty($match['media'])) {
230 $importContent = '@media ' . $match['media'] . '{' . $importContent . '}';
231 }
232
233 // add to replacement array
234 $search[] = $match[0];
235 $replace[] = $importContent;
236 }
237
238 // replace the import statements
239 return str_replace($search, $replace, $content);
240 }
241
242 /**
243 * Import files into the CSS, base64-ized.
244 *
245 * @url(image.jpg) images will be loaded and their content merged into the
246 * original file, to save HTTP requests.
247 *
248 * @param string $source The file to import files for
249 * @param string $content The CSS content to import files for
250 *
251 * @return string
252 */
253 protected function importFiles($source, $content)
254 {
255 $regex = '/url\((["\']?)(.+?)\\1\)/i';
256 if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
257 $search = array();
258 $replace = array();
259
260 // loop the matches
261 foreach ($matches as $match) {
262 $extension = substr(strrchr($match[2], '.'), 1);
263 if ($extension && !array_key_exists($extension, $this->importExtensions)) {
264 continue;
265 }
266
267 // get the path for the file that will be imported
268 $path = $match[2];
269 $path = dirname($source) . '/' . $path;
270
271 // only replace the import with the content if we're able to get
272 // the content of the file, and it's relatively small
273 if ($this->canImportFile($path) && $this->canImportBySize($path)) {
274 // grab content && base64-ize
275 $importContent = $this->load($path);
276 $importContent = base64_encode($importContent);
277
278 // build replacement
279 $search[] = $match[0];
280 $replace[] = 'url(' . $this->importExtensions[$extension] . ';base64,' . $importContent . ')';
281 }
282 }
283
284 // replace the import statements
285 $content = str_replace($search, $replace, $content);
286 }
287
288 return $content;
289 }
290
291 /**
292 * Minify the data.
293 * Perform CSS optimizations.
294 *
295 * @param string[optional] $path Path to write the data to
296 * @param string[] $parents Parent paths, for circular reference checks
297 *
298 * @return string The minified data
299 */
300 public function execute($path = null, $parents = array())
301 {
302 $content = '';
303
304 // loop CSS data (raw data and files)
305 foreach ($this->data as $source => $css) {
306 /*
307 * Let's first take out strings & comments, since we can't just
308 * remove whitespace anywhere. If whitespace occurs inside a string,
309 * we should leave it alone. E.g.:
310 * p { content: "a test" }
311 */
312 $this->extractStrings();
313 $this->stripComments();
314 $this->extractMath();
315 $this->extractCustomProperties();
316 $css = $this->replace($css);
317
318 $css = $this->stripWhitespace($css);
319 $css = $this->convertLegacyColors($css);
320 $css = $this->cleanupModernColors($css);
321 $css = $this->shortenHEXColors($css);
322 $css = $this->shortenZeroes($css);
323 $css = $this->shortenFontWeights($css);
324 $css = $this->stripEmptyTags($css);
325
326 // restore the string we've extracted earlier
327 $css = $this->restoreExtractedData($css);
328
329 $source = is_int($source) ? '' : $source;
330 $parents = $source ? array_merge($parents, array($source)) : $parents;
331 $css = $this->combineImports($source, $css, $parents);
332 $css = $this->importFiles($source, $css);
333
334 /*
335 * If we'll save to a new path, we'll have to fix the relative paths
336 * to be relative no longer to the source file, but to the new path.
337 * If we don't write to a file, fall back to same path so no
338 * conversion happens (because we still want it to go through most
339 * of the move code, which also addresses url() & @import syntax...)
340 */
341 $converter = $this->getPathConverter($source, $path ?: $source);
342 $css = $this->move($converter, $css);
343
344 // combine css
345 $content .= $css;
346 }
347
348 $content = $this->moveImportsToTop($content);
349
350 return $content;
351 }
352
353 /**
354 * Moving a css file should update all relative urls.
355 * Relative references (e.g. ../images/image.gif) in a certain css file,
356 * will have to be updated when a file is being saved at another location
357 * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper).
358 *
359 * @param ConverterInterface $converter Relative path converter
360 * @param string $content The CSS content to update relative urls for
361 *
362 * @return string
363 */
364 protected function move(ConverterInterface $converter, $content)
365 {
366 /*
367 * Relative path references will usually be enclosed by url(). @import
368 * is an exception, where url() is not necessary around the path (but is
369 * allowed).
370 * This *could* be 1 regular expression, where both regular expressions
371 * in this array are on different sides of a |. But we're using named
372 * patterns in both regexes, the same name on both regexes. This is only
373 * possible with a (?J) modifier, but that only works after a fairly
374 * recent PCRE version. That's why I'm doing 2 separate regular
375 * expressions & combining the matches after executing of both.
376 */
377 $relativeRegexes = array(
378 // url(xxx)
379 '/
380 # open url()
381 url\(
382
383 \s*
384
385 # open path enclosure
386 (?P<quotes>["\'])?
387
388 # fetch path
389 (?P<path>.+?)
390
391 # close path enclosure
392 (?(quotes)(?P=quotes))
393
394 \s*
395
396 # close url()
397 \)
398
399 /ix',
400
401 // @import "xxx"
402 '/
403 # import statement
404 @import
405
406 # whitespace
407 \s+
408
409 # we don\'t have to check for @import url(), because the
410 # condition above will already catch these
411
412 # open path enclosure
413 (?P<quotes>["\'])
414
415 # fetch path
416 (?P<path>.+?)
417
418 # close path enclosure
419 (?P=quotes)
420
421 /ix',
422 );
423
424 // find all relative urls in css
425 $matches = array();
426 foreach ($relativeRegexes as $relativeRegex) {
427 if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) {
428 $matches = array_merge($matches, $regexMatches);
429 }
430 }
431
432 $search = array();
433 $replace = array();
434
435 // loop all urls
436 foreach ($matches as $match) {
437 // determine if it's a url() or an @import match
438 $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url');
439
440 $url = $match['path'];
441 if ($this->canImportByPath($url)) {
442 // attempting to interpret GET-params makes no sense, so let's discard them for awhile
443 $params = strrchr($url, '?');
444 $url = $params ? substr($url, 0, -strlen($params)) : $url;
445
446 // fix relative url
447 $url = $converter->convert($url);
448
449 // now that the path has been converted, re-apply GET-params
450 $url .= $params;
451 }
452
453 /*
454 * Urls with control characters above 0x7e should be quoted.
455 * According to Mozilla's parser, whitespace is only allowed at the
456 * end of unquoted urls.
457 * Urls with `)` (as could happen with data: uris) should also be
458 * quoted to avoid being confused for the url() closing parentheses.
459 * And urls with a # have also been reported to cause issues.
460 * Urls with quotes inside should also remain escaped.
461 *
462 * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation
463 * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378
464 * @see https://github.com/matthiasmullie/minify/issues/193
465 */
466 $url = trim($url);
467 if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) {
468 $url = $match['quotes'] . $url . $match['quotes'];
469 }
470
471 // build replacement
472 $search[] = $match[0];
473 if ($type === 'url') {
474 $replace[] = 'url(' . $url . ')';
475 } elseif ($type === 'import') {
476 $replace[] = '@import "' . $url . '"';
477 }
478 }
479
480 // replace urls
481 return str_replace($search, $replace, $content);
482 }
483
484 /**
485 * Shorthand HEX color codes.
486 * #FF0000FF -> #f00 -> red
487 * #FF00FF00 -> transparent.
488 *
489 * @param string $content The CSS content to shorten the HEX color codes for
490 *
491 * @return string
492 */
493 protected function shortenHexColors($content)
494 {
495 // shorten repeating patterns within HEX ..
496 $content = preg_replace('/(?<=[: ])#([0-9a-f])\\1([0-9a-f])\\2([0-9a-f])\\3(?:([0-9a-f])\\4)?(?=[; }])/i', '#$1$2$3$4', $content);
497
498 // remove alpha channel if it's pointless ..
499 $content = preg_replace('/(?<=[: ])#([0-9a-f]{6})ff(?=[; }])/i', '#$1', $content);
500 $content = preg_replace('/(?<=[: ])#([0-9a-f]{3})f(?=[; }])/i', '#$1', $content);
501
502 // replace `transparent` with shortcut ..
503 $content = preg_replace('/(?<=[: ])#[0-9a-f]{6}00(?=[; }])/i', '#fff0', $content);
504
505 $colors = array(
506 // make these more readable
507 '#00f' => 'blue',
508 '#dc143c' => 'crimson',
509 '#0ff' => 'cyan',
510 '#8b0000' => 'darkred',
511 '#696969' => 'dimgray',
512 '#ff69b4' => 'hotpink',
513 '#0f0' => 'lime',
514 '#fdf5e6' => 'oldlace',
515 '#87ceeb' => 'skyblue',
516 '#d8bfd8' => 'thistle',
517 // we can shorten some even more by replacing them with their color name
518 '#f0ffff' => 'azure',
519 '#f5f5dc' => 'beige',
520 '#ffe4c4' => 'bisque',
521 '#a52a2a' => 'brown',
522 '#ff7f50' => 'coral',
523 '#ffd700' => 'gold',
524 '#808080' => 'gray',
525 '#008000' => 'green',
526 '#4b0082' => 'indigo',
527 '#fffff0' => 'ivory',
528 '#f0e68c' => 'khaki',
529 '#faf0e6' => 'linen',
530 '#800000' => 'maroon',
531 '#000080' => 'navy',
532 '#808000' => 'olive',
533 '#ffa500' => 'orange',
534 '#da70d6' => 'orchid',
535 '#cd853f' => 'peru',
536 '#ffc0cb' => 'pink',
537 '#dda0dd' => 'plum',
538 '#800080' => 'purple',
539 '#f00' => 'red',
540 '#fa8072' => 'salmon',
541 '#a0522d' => 'sienna',
542 '#c0c0c0' => 'silver',
543 '#fffafa' => 'snow',
544 '#d2b48c' => 'tan',
545 '#008080' => 'teal',
546 '#ff6347' => 'tomato',
547 '#ee82ee' => 'violet',
548 '#f5deb3' => 'wheat',
549 // or the other way around
550 'black' => '#000',
551 'fuchsia' => '#f0f',
552 'magenta' => '#f0f',
553 'white' => '#fff',
554 'yellow' => '#ff0',
555 // and also `transparent`
556 'transparent' => '#fff0',
557 );
558
559 return preg_replace_callback(
560 '/(?<=[: ])(' . implode('|', array_keys($colors)) . ')(?=[; }])/i',
561 function ($match) use ($colors) {
562 return $colors[strtolower($match[0])];
563 },
564 $content
565 );
566 }
567
568 /**
569 * Convert RGB|HSL color codes.
570 * rgb(255,0,0,.5) -> rgb(255 0 0 / .5).
571 * rgb(255,0,0) -> #f00.
572 *
573 * @param string $content The CSS content to shorten the RGB color codes for
574 *
575 * @return string
576 */
577 protected function convertLegacyColors($content)
578 {
579 /*
580 https://drafts.csswg.org/css-color/#color-syntax-legacy
581 https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb
582 https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl
583 */
584
585 // convert legacy color syntax
586 $content = preg_replace('/(rgb)a?\(\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0,1]?(?:\.[0-9]*)?)\s*\)/i', '$1($2 $3 $4 / $5)', $content);
587 $content = preg_replace('/(rgb)a?\(\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*\)/i', '$1($2 $3 $4)', $content);
588 $content = preg_replace('/(hsl)a?\(\s*([0-9]+(?:deg|grad|rad|turn)?)\s*,\s*([0-9]{1,3}%)\s*,\s*([0-9]{1,3}%)\s*,\s*([0,1]?(?:\.[0-9]*)?)\s*\)/i', '$1($2 $3 $4 / $5)', $content);
589 $content = preg_replace('/(hsl)a?\(\s*([0-9]+(?:deg|grad|rad|turn)?)\s*,\s*([0-9]{1,3}%)\s*,\s*([0-9]{1,3}%)\s*\)/i', '$1($2 $3 $4)', $content);
590
591 // convert `rgb` to `hex`
592 $dec = '([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])';
593 return preg_replace_callback(
594 "/rgb\($dec $dec $dec\)/i",
595 function ($match) {
596 return sprintf('#%02x%02x%02x', $match[1], $match[2], $match[3]);
597 },
598 $content
599 );
600 }
601
602 /**
603 * Cleanup RGB|HSL|HWB|LCH|LAB
604 * rgb(255 0 0 / 1) -> rgb(255 0 0).
605 * rgb(255 0 0 / 0) -> transparent.
606 *
607 * @param string $content The CSS content to cleanup HSL|HWB|LCH|LAB
608 *
609 * @return string
610 */
611 protected function cleanupModernColors($content)
612 {
613 /*
614 https://drafts.csswg.org/css-color/#color-syntax-modern
615 https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hwb
616 https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lch
617 https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lab
618 https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch
619 https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklab
620 */
621 $tag = '(rgb|hsl|hwb|(?:(?:ok)?(?:lch|lab)))';
622
623 // remove alpha channel if it's pointless ..
624 $content = preg_replace('/' . $tag . '\(\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+\/\s+1(?:(?:\.\d?)*|00%)?\s*\)/i', '$1($2 $3 $4)', $content);
625
626 // replace `transparent` with shortcut ..
627 $content = preg_replace('/' . $tag . '\(\s*[^\s]+\s+[^\s]+\s+[^\s]+\s+\/\s+0(?:[\.0%]*)?\s*\)/i', '#fff0', $content);
628
629 return $content;
630 }
631
632 /**
633 * Shorten CSS font weights.
634 *
635 * @param string $content The CSS content to shorten the font weights for
636 *
637 * @return string
638 */
639 protected function shortenFontWeights($content)
640 {
641 $weights = array(
642 'normal' => 400,
643 'bold' => 700,
644 );
645
646 $callback = function ($match) use ($weights) {
647 return $match[1] . $weights[$match[2]];
648 };
649
650 return preg_replace_callback('/(font-weight\s*:\s*)(' . implode('|', array_keys($weights)) . ')(?=[;}])/', $callback, $content);
651 }
652
653 /**
654 * Shorthand 0 values to plain 0, instead of e.g. -0em.
655 *
656 * @param string $content The CSS content to shorten the zero values for
657 *
658 * @return string
659 */
660 protected function shortenZeroes($content)
661 {
662 // we don't want to strip units in `calc()` expressions:
663 // `5px - 0px` is valid, but `5px - 0` is not
664 // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but
665 // `10 * 0` is invalid
666 // we've extracted calcs earlier, so we don't need to worry about this
667
668 // reusable bits of code throughout these regexes:
669 // before & after are used to make sure we don't match lose unintended
670 // 0-like values (e.g. in #000, or in http://url/1.0)
671 // units can be stripped from 0 values, or used to recognize non 0
672 // values (where wa may be able to strip a .0 suffix)
673 $before = '(?<=[:(, ])';
674 $after = '(?=[ ,);}])';
675 $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)';
676
677 // strip units after zeroes (0px -> 0)
678 // NOTE: it should be safe to remove all units for a 0 value, but in
679 // practice, Webkit (especially Safari) seems to stumble over at least
680 // 0%, potentially other units as well. Only stripping 'px' for now.
681 // @see https://github.com/matthiasmullie/minify/issues/60
682 $content = preg_replace('/' . $before . '(-?0*(\.0+)?)(?<=0)px' . $after . '/', '\\1', $content);
683
684 // strip 0-digits (.0 -> 0)
685 $content = preg_replace('/' . $before . '\.0+' . $units . '?' . $after . '/', '0\\1', $content);
686 // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px
687 $content = preg_replace('/' . $before . '(-?[0-9]+\.[0-9]+)0+' . $units . '?' . $after . '/', '\\1\\2', $content);
688 // strip trailing 0: 50.00 -> 50, 50.00px -> 50px
689 $content = preg_replace('/' . $before . '(-?[0-9]+)\.0+' . $units . '?' . $after . '/', '\\1\\2', $content);
690 // strip leading 0: 0.1 -> .1, 01.1 -> 1.1
691 $content = preg_replace('/' . $before . '(-?)0+([0-9]*\.[0-9]+)' . $units . '?' . $after . '/', '\\1\\2\\3', $content);
692
693 // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
694 $content = preg_replace('/' . $before . '-?0+' . $units . '?' . $after . '/', '0\\1', $content);
695
696 // IE doesn't seem to understand a unitless flex-basis value (correct -
697 // it goes against the spec), so let's add it in again (make it `%`,
698 // which is only 1 char: 0%, 0px, 0 anything, it's all just the same)
699 // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex
700 $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content);
701 $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content);
702
703 return $content;
704 }
705
706 /**
707 * Strip empty tags from source code.
708 *
709 * @param string $content
710 *
711 * @return string
712 */
713 protected function stripEmptyTags($content)
714 {
715 $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content);
716 $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content);
717
718 return $content;
719 }
720
721 /**
722 * Strip comments from source code.
723 */
724 protected function stripComments()
725 {
726 $this->stripMultilineComments();
727 }
728
729 /**
730 * Strip whitespace.
731 *
732 * @param string $content The CSS content to strip the whitespace for
733 *
734 * @return string
735 */
736 protected function stripWhitespace($content)
737 {
738 // remove leading & trailing whitespace
739 $content = preg_replace('/^\s*/m', '', $content);
740 $content = preg_replace('/\s*$/m', '', $content);
741
742 // replace newlines with a single space
743 $content = preg_replace('/\s+/', ' ', $content);
744
745 // remove whitespace around meta characters
746 // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex
747 $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content);
748 $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content);
749 $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content);
750 $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content);
751
752 // whitespace around + and - can only be stripped inside some pseudo-
753 // classes, like `:nth-child(3+2n)`
754 // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or
755 // selectors like `div.weird- p`
756 $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type');
757 $content = preg_replace('/:(' . implode('|', $pseudos) . ')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content);
758
759 // remove semicolon/whitespace followed by closing bracket
760 $content = str_replace(';}', '}', $content);
761
762 return trim($content);
763 }
764
765 /**
766 * Replace all occurrences of functions that may contain math, where
767 * whitespace around operators needs to be preserved (e.g. calc, clamp).
768 */
769 protected function extractMath()
770 {
771 $functions = array('calc', 'clamp', 'min', 'max');
772 $pattern = '/\b(' . implode('|', $functions) . ')(\(.+?)(?=$|;|})/m';
773
774 // PHP only supports $this inside anonymous functions since 5.4
775 $minifier = $this;
776 $callback = function ($match) use ($minifier, $pattern, &$callback) {
777 $function = $match[1];
778 $length = strlen($match[2]);
779 $expr = '';
780 $opened = 0;
781
782 // the regular expression for extracting math has 1 significant problem:
783 // it can't determine the correct closing parenthesis...
784 // instead, it'll match a larger portion of code to where it's certain that
785 // the calc() musts have ended, and we'll figure out which is the correct
786 // closing parenthesis here, by counting how many have opened
787 for ($i = 0; $i < $length; ++$i) {
788 $char = $match[2][$i];
789 $expr .= $char;
790 if ($char === '(') {
791 ++$opened;
792 } elseif ($char === ')' && --$opened === 0) {
793 break;
794 }
795 }
796
797 // now that we've figured out where the calc() starts and ends, extract it
798 $count = count($minifier->extracted);
799 $placeholder = 'math(' . $count . ')';
800 $minifier->extracted[$placeholder] = $function . '(' . trim(substr($expr, 1, -1)) . ')';
801
802 // and since we've captured more code than required, we may have some leftover
803 // calc() in here too - go recursive on the remaining but of code to go figure
804 // that out and extract what is needed
805 $rest = $minifier->str_replace_first($function . $expr, '', $match[0]);
806 $rest = preg_replace_callback($pattern, $callback, $rest);
807
808 return $placeholder . $rest;
809 };
810
811 $this->registerPattern($pattern, $callback);
812 }
813
814 /**
815 * Replace custom properties, whose values may be used in scenarios where
816 * we wouldn't want them to be minified (e.g. inside calc).
817 */
818 protected function extractCustomProperties()
819 {
820 // PHP only supports $this inside anonymous functions since 5.4
821 $minifier = $this;
822 $this->registerPattern(
823 '/(?<=^|[;}{])\s*(--[^:;{}"\'\s]+)\s*:([^;{}]+)/m',
824 function ($match) use ($minifier) {
825 $placeholder = '--custom-' . count($minifier->extracted) . ':0';
826 $minifier->extracted[$placeholder] = $match[1] . ':' . trim($match[2]);
827
828 return $placeholder;
829 }
830 );
831 }
832
833 /**
834 * Check if file is small enough to be imported.
835 *
836 * @param string $path The path to the file
837 *
838 * @return bool
839 */
840 protected function canImportBySize($path)
841 {
842 return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024;
843 }
844
845 /**
846 * Check if file a file can be imported, going by the path.
847 *
848 * @param string $path
849 *
850 * @return bool
851 */
852 protected function canImportByPath($path)
853 {
854 return preg_match('/^(data:|https?:|\\/)/', $path) === 0;
855 }
856
857 /**
858 * Return a converter to update relative paths to be relative to the new
859 * destination.
860 *
861 * @param string $source
862 * @param string $target
863 *
864 * @return ConverterInterface
865 */
866 protected function getPathConverter($source, $target)
867 {
868 return new Converter($source, $target);
869 }
870 }
871