PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.3
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.3
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / vendor / matthiasmullie / minify / src / CSS.php

CSS.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.3, at vendor/matthiasmullie/minify/src/CSS.php

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