blob: ea8e8cd93f7ab912b8c35bd90202e6fc29a11d1f [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2/**
3 * CSS Minifier
4 *
5 * Please report bugs on https://github.com/matthiasmullie/minify/issues
6 *
7 * @author Matthias Mullie <minify@mullie.eu>
8 * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
9 * @license MIT License
10 */
11
12namespace MatthiasMullie\Minify;
13
14use MatthiasMullie\Minify\Exceptions\FileImportException;
15use MatthiasMullie\PathConverter\ConverterInterface;
16use MatthiasMullie\PathConverter\Converter;
17
18/**
19 * CSS minifier
20 *
21 * Please report bugs on https://github.com/matthiasmullie/minify/issues
22 *
23 * @package Minify
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 */
29class 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 'tif' => 'image/tiff',
48 'tiff' => 'image/tiff',
49 'xbm' => 'image/x-xbitmap',
50 );
51
52 /**
53 * Set the maximum size if files to be imported.
54 *
55 * Files larger than this size (in kB) will not be imported into the CSS.
56 * Importing files into the CSS as data-uri will save you some connections,
57 * but we should only import relatively small decorative images so that our
58 * CSS file doesn't get too bulky.
59 *
60 * @param int $size Size in kB
61 */
62 public function setMaxImportSize($size)
63 {
64 $this->maxImportSize = $size;
65 }
66
67 /**
68 * Set the type of extensions to be imported into the CSS (to save network
69 * connections).
70 * Keys of the array should be the file extensions & respective values
71 * should be the data type.
72 *
73 * @param string[] $extensions Array of file extensions
74 */
75 public function setImportExtensions(array $extensions)
76 {
77 $this->importExtensions = $extensions;
78 }
79
80 /**
81 * Move any import statements to the top.
82 *
83 * @param string $content Nearly finished CSS content
84 *
85 * @return string
86 */
87 protected function moveImportsToTop($content)
88 {
89 if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) {
90 // remove from content
91 foreach ($matches[0] as $import) {
92 $content = str_replace($import, '', $content);
93 }
94
95 // add to top
96 $content = implode(';', $matches[2]).';'.trim($content, ';');
97 }
98
99 return $content;
100 }
101
102 /**
103 * Combine CSS from import statements.
104 *
105 * @import's will be loaded and their content merged into the original file,
106 * to save HTTP requests.
107 *
108 * @param string $source The file to combine imports for
109 * @param string $content The CSS content to combine imports for
110 * @param string[] $parents Parent paths, for circular reference checks
111 *
112 * @return string
113 *
114 * @throws FileImportException
115 */
116 protected function combineImports($source, $content, $parents)
117 {
118 $importRegexes = array(
119 // @import url(xxx)
120 '/
121 # import statement
122 @import
123
124 # whitespace
125 \s+
126
127 # open url()
128 url\(
129
130 # (optional) open path enclosure
131 (?P<quotes>["\']?)
132
133 # fetch path
134 (?P<path>.+?)
135
136 # (optional) close path enclosure
137 (?P=quotes)
138
139 # close url()
140 \)
141
142 # (optional) trailing whitespace
143 \s*
144
145 # (optional) media statement(s)
146 (?P<media>[^;]*)
147
148 # (optional) trailing whitespace
149 \s*
150
151 # (optional) closing semi-colon
152 ;?
153
154 /ix',
155
156 // @import 'xxx'
157 '/
158
159 # import statement
160 @import
161
162 # whitespace
163 \s+
164
165 # open path enclosure
166 (?P<quotes>["\'])
167
168 # fetch path
169 (?P<path>.+?)
170
171 # close path enclosure
172 (?P=quotes)
173
174 # (optional) trailing whitespace
175 \s*
176
177 # (optional) media statement(s)
178 (?P<media>[^;]*)
179
180 # (optional) trailing whitespace
181 \s*
182
183 # (optional) closing semi-colon
184 ;?
185
186 /ix',
187 );
188
189 // find all relative imports in css
190 $matches = array();
191 foreach ($importRegexes as $importRegex) {
192 if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) {
193 $matches = array_merge($matches, $regexMatches);
194 }
195 }
196
197 $search = array();
198 $replace = array();
199
200 // loop the matches
201 foreach ($matches as $match) {
202 // get the path for the file that will be imported
203 $importPath = dirname($source).'/'.$match['path'];
204
205 // only replace the import with the content if we can grab the
206 // content of the file
207 if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) {
208 continue;
209 }
210
211 // check if current file was not imported previously in the same
212 // import chain.
213 if (in_array($importPath, $parents)) {
214 throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.');
215 }
216
217 // grab referenced file & minify it (which may include importing
218 // yet other @import statements recursively)
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100219 $minifier = new self($importPath);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100220 $minifier->setMaxImportSize($this->maxImportSize);
221 $minifier->setImportExtensions($this->importExtensions);
222 $importContent = $minifier->execute($source, $parents);
223
224 // check if this is only valid for certain media
225 if (!empty($match['media'])) {
226 $importContent = '@media '.$match['media'].'{'.$importContent.'}';
227 }
228
229 // add to replacement array
230 $search[] = $match[0];
231 $replace[] = $importContent;
232 }
233
234 // replace the import statements
235 return str_replace($search, $replace, $content);
236 }
237
238 /**
239 * Import files into the CSS, base64-ized.
240 *
241 * @url(image.jpg) images will be loaded and their content merged into the
242 * original file, to save HTTP requests.
243 *
244 * @param string $source The file to import files for
245 * @param string $content The CSS content to import files for
246 *
247 * @return string
248 */
249 protected function importFiles($source, $content)
250 {
251 $regex = '/url\((["\']?)(.+?)\\1\)/i';
252 if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
253 $search = array();
254 $replace = array();
255
256 // loop the matches
257 foreach ($matches as $match) {
258 $extension = substr(strrchr($match[2], '.'), 1);
259 if ($extension && !array_key_exists($extension, $this->importExtensions)) {
260 continue;
261 }
262
263 // get the path for the file that will be imported
264 $path = $match[2];
265 $path = dirname($source).'/'.$path;
266
267 // only replace the import with the content if we're able to get
268 // the content of the file, and it's relatively small
269 if ($this->canImportFile($path) && $this->canImportBySize($path)) {
270 // grab content && base64-ize
271 $importContent = $this->load($path);
272 $importContent = base64_encode($importContent);
273
274 // build replacement
275 $search[] = $match[0];
276 $replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')';
277 }
278 }
279
280 // replace the import statements
281 $content = str_replace($search, $replace, $content);
282 }
283
284 return $content;
285 }
286
287 /**
288 * Minify the data.
289 * Perform CSS optimizations.
290 *
291 * @param string[optional] $path Path to write the data to
292 * @param string[] $parents Parent paths, for circular reference checks
293 *
294 * @return string The minified data
295 */
296 public function execute($path = null, $parents = array())
297 {
298 $content = '';
299
300 // loop CSS data (raw data and files)
301 foreach ($this->data as $source => $css) {
302 /*
303 * Let's first take out strings & comments, since we can't just
304 * remove whitespace anywhere. If whitespace occurs inside a string,
305 * we should leave it alone. E.g.:
306 * p { content: "a test" }
307 */
308 $this->extractStrings();
309 $this->stripComments();
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100310 $this->extractMath();
311 $this->extractCustomProperties();
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100312 $css = $this->replace($css);
313
314 $css = $this->stripWhitespace($css);
315 $css = $this->shortenColors($css);
316 $css = $this->shortenZeroes($css);
317 $css = $this->shortenFontWeights($css);
318 $css = $this->stripEmptyTags($css);
319
320 // restore the string we've extracted earlier
321 $css = $this->restoreExtractedData($css);
322
323 $source = is_int($source) ? '' : $source;
324 $parents = $source ? array_merge($parents, array($source)) : $parents;
325 $css = $this->combineImports($source, $css, $parents);
326 $css = $this->importFiles($source, $css);
327
328 /*
329 * If we'll save to a new path, we'll have to fix the relative paths
330 * to be relative no longer to the source file, but to the new path.
331 * If we don't write to a file, fall back to same path so no
332 * conversion happens (because we still want it to go through most
333 * of the move code, which also addresses url() & @import syntax...)
334 */
335 $converter = $this->getPathConverter($source, $path ?: $source);
336 $css = $this->move($converter, $css);
337
338 // combine css
339 $content .= $css;
340 }
341
342 $content = $this->moveImportsToTop($content);
343
344 return $content;
345 }
346
347 /**
348 * Moving a css file should update all relative urls.
349 * Relative references (e.g. ../images/image.gif) in a certain css file,
350 * will have to be updated when a file is being saved at another location
351 * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper).
352 *
353 * @param ConverterInterface $converter Relative path converter
354 * @param string $content The CSS content to update relative urls for
355 *
356 * @return string
357 */
358 protected function move(ConverterInterface $converter, $content)
359 {
360 /*
361 * Relative path references will usually be enclosed by url(). @import
362 * is an exception, where url() is not necessary around the path (but is
363 * allowed).
364 * This *could* be 1 regular expression, where both regular expressions
365 * in this array are on different sides of a |. But we're using named
366 * patterns in both regexes, the same name on both regexes. This is only
367 * possible with a (?J) modifier, but that only works after a fairly
368 * recent PCRE version. That's why I'm doing 2 separate regular
369 * expressions & combining the matches after executing of both.
370 */
371 $relativeRegexes = array(
372 // url(xxx)
373 '/
374 # open url()
375 url\(
376
377 \s*
378
379 # open path enclosure
380 (?P<quotes>["\'])?
381
382 # fetch path
383 (?P<path>.+?)
384
385 # close path enclosure
386 (?(quotes)(?P=quotes))
387
388 \s*
389
390 # close url()
391 \)
392
393 /ix',
394
395 // @import "xxx"
396 '/
397 # import statement
398 @import
399
400 # whitespace
401 \s+
402
403 # we don\'t have to check for @import url(), because the
404 # condition above will already catch these
405
406 # open path enclosure
407 (?P<quotes>["\'])
408
409 # fetch path
410 (?P<path>.+?)
411
412 # close path enclosure
413 (?P=quotes)
414
415 /ix',
416 );
417
418 // find all relative urls in css
419 $matches = array();
420 foreach ($relativeRegexes as $relativeRegex) {
421 if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) {
422 $matches = array_merge($matches, $regexMatches);
423 }
424 }
425
426 $search = array();
427 $replace = array();
428
429 // loop all urls
430 foreach ($matches as $match) {
431 // determine if it's a url() or an @import match
432 $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url');
433
434 $url = $match['path'];
435 if ($this->canImportByPath($url)) {
436 // attempting to interpret GET-params makes no sense, so let's discard them for awhile
437 $params = strrchr($url, '?');
438 $url = $params ? substr($url, 0, -strlen($params)) : $url;
439
440 // fix relative url
441 $url = $converter->convert($url);
442
443 // now that the path has been converted, re-apply GET-params
444 $url .= $params;
445 }
446
447 /*
448 * Urls with control characters above 0x7e should be quoted.
449 * According to Mozilla's parser, whitespace is only allowed at the
450 * end of unquoted urls.
451 * Urls with `)` (as could happen with data: uris) should also be
452 * quoted to avoid being confused for the url() closing parentheses.
453 * And urls with a # have also been reported to cause issues.
454 * Urls with quotes inside should also remain escaped.
455 *
456 * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation
457 * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378
458 * @see https://github.com/matthiasmullie/minify/issues/193
459 */
460 $url = trim($url);
461 if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) {
462 $url = $match['quotes'] . $url . $match['quotes'];
463 }
464
465 // build replacement
466 $search[] = $match[0];
467 if ($type === 'url') {
468 $replace[] = 'url('.$url.')';
469 } elseif ($type === 'import') {
470 $replace[] = '@import "'.$url.'"';
471 }
472 }
473
474 // replace urls
475 return str_replace($search, $replace, $content);
476 }
477
478 /**
479 * Shorthand hex color codes.
480 * #FF0000 -> #F00.
481 *
482 * @param string $content The CSS content to shorten the hex color codes for
483 *
484 * @return string
485 */
486 protected function shortenColors($content)
487 {
488 $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?:([0-9a-z])\\4)?(?=[; }])/i', '#$1$2$3$4', $content);
489
490 // remove alpha channel if it's pointless...
491 $content = preg_replace('/(?<=[: ])#([0-9a-z]{6})ff?(?=[; }])/i', '#$1', $content);
492 $content = preg_replace('/(?<=[: ])#([0-9a-z]{3})f?(?=[; }])/i', '#$1', $content);
493
494 $colors = array(
495 // we can shorten some even more by replacing them with their color name
496 '#F0FFFF' => 'azure',
497 '#F5F5DC' => 'beige',
498 '#A52A2A' => 'brown',
499 '#FF7F50' => 'coral',
500 '#FFD700' => 'gold',
501 '#808080' => 'gray',
502 '#008000' => 'green',
503 '#4B0082' => 'indigo',
504 '#FFFFF0' => 'ivory',
505 '#F0E68C' => 'khaki',
506 '#FAF0E6' => 'linen',
507 '#800000' => 'maroon',
508 '#000080' => 'navy',
509 '#808000' => 'olive',
510 '#CD853F' => 'peru',
511 '#FFC0CB' => 'pink',
512 '#DDA0DD' => 'plum',
513 '#800080' => 'purple',
514 '#F00' => 'red',
515 '#FA8072' => 'salmon',
516 '#A0522D' => 'sienna',
517 '#C0C0C0' => 'silver',
518 '#FFFAFA' => 'snow',
519 '#D2B48C' => 'tan',
520 '#FF6347' => 'tomato',
521 '#EE82EE' => 'violet',
522 '#F5DEB3' => 'wheat',
523 // or the other way around
524 'WHITE' => '#fff',
525 'BLACK' => '#000',
526 );
527
528 return preg_replace_callback(
529 '/(?<=[: ])('.implode('|', array_keys($colors)).')(?=[; }])/i',
530 function ($match) use ($colors) {
531 return $colors[strtoupper($match[0])];
532 },
533 $content
534 );
535 }
536
537 /**
538 * Shorten CSS font weights.
539 *
540 * @param string $content The CSS content to shorten the font weights for
541 *
542 * @return string
543 */
544 protected function shortenFontWeights($content)
545 {
546 $weights = array(
547 'normal' => 400,
548 'bold' => 700,
549 );
550
551 $callback = function ($match) use ($weights) {
552 return $match[1].$weights[$match[2]];
553 };
554
555 return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content);
556 }
557
558 /**
559 * Shorthand 0 values to plain 0, instead of e.g. -0em.
560 *
561 * @param string $content The CSS content to shorten the zero values for
562 *
563 * @return string
564 */
565 protected function shortenZeroes($content)
566 {
567 // we don't want to strip units in `calc()` expressions:
568 // `5px - 0px` is valid, but `5px - 0` is not
569 // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but
570 // `10 * 0` is invalid
571 // we've extracted calcs earlier, so we don't need to worry about this
572
573 // reusable bits of code throughout these regexes:
574 // before & after are used to make sure we don't match lose unintended
575 // 0-like values (e.g. in #000, or in http://url/1.0)
576 // units can be stripped from 0 values, or used to recognize non 0
577 // values (where wa may be able to strip a .0 suffix)
578 $before = '(?<=[:(, ])';
579 $after = '(?=[ ,);}])';
580 $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)';
581
582 // strip units after zeroes (0px -> 0)
583 // NOTE: it should be safe to remove all units for a 0 value, but in
584 // practice, Webkit (especially Safari) seems to stumble over at least
585 // 0%, potentially other units as well. Only stripping 'px' for now.
586 // @see https://github.com/matthiasmullie/minify/issues/60
587 $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content);
588
589 // strip 0-digits (.0 -> 0)
590 $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content);
591 // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px
592 $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content);
593 // strip trailing 0: 50.00 -> 50, 50.00px -> 50px
594 $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content);
595 // strip leading 0: 0.1 -> .1, 01.1 -> 1.1
596 $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content);
597
598 // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
599 $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content);
600
601 // IE doesn't seem to understand a unitless flex-basis value (correct -
602 // it goes against the spec), so let's add it in again (make it `%`,
603 // which is only 1 char: 0%, 0px, 0 anything, it's all just the same)
604 // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex
605 $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content);
606 $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content);
607
608 return $content;
609 }
610
611 /**
612 * Strip empty tags from source code.
613 *
614 * @param string $content
615 *
616 * @return string
617 */
618 protected function stripEmptyTags($content)
619 {
620 $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content);
621 $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content);
622
623 return $content;
624 }
625
626 /**
627 * Strip comments from source code.
628 */
629 protected function stripComments()
630 {
631 // PHP only supports $this inside anonymous functions since 5.4
632 $minifier = $this;
633 $callback = function ($match) use ($minifier) {
634 $count = count($minifier->extracted);
635 $placeholder = '/*'.$count.'*/';
636 $minifier->extracted[$placeholder] = $match[0];
637
638 return $placeholder;
639 };
640 $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback);
641
642 $this->registerPattern('/\/\*.*?\*\//s', '');
643 }
644
645 /**
646 * Strip whitespace.
647 *
648 * @param string $content The CSS content to strip the whitespace for
649 *
650 * @return string
651 */
652 protected function stripWhitespace($content)
653 {
654 // remove leading & trailing whitespace
655 $content = preg_replace('/^\s*/m', '', $content);
656 $content = preg_replace('/\s*$/m', '', $content);
657
658 // replace newlines with a single space
659 $content = preg_replace('/\s+/', ' ', $content);
660
661 // remove whitespace around meta characters
662 // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex
663 $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content);
664 $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content);
665 $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content);
666 $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content);
667
668 // whitespace around + and - can only be stripped inside some pseudo-
669 // classes, like `:nth-child(3+2n)`
670 // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or
671 // selectors like `div.weird- p`
672 $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type');
673 $content = preg_replace('/:('.implode('|', $pseudos).')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content);
674
675 // remove semicolon/whitespace followed by closing bracket
676 $content = str_replace(';}', '}', $content);
677
678 return trim($content);
679 }
680
681 /**
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100682 * Replace all occurrences of functions that may contain math, where
683 * whitespace around operators needs to be preserved (e.g. calc, clamp)
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100684 */
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100685 protected function extractMath()
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100686 {
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100687 $functions = array('calc', 'clamp', 'min', 'max');
688 $pattern = '/('. implode('|', $functions) .')(\(.+?)(?=$|;|})/m';
689
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100690 // PHP only supports $this inside anonymous functions since 5.4
691 $minifier = $this;
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100692 $callback = function ($match) use ($minifier, $pattern, &$callback) {
693 $function = $match[1];
694 $length = strlen($match[2]);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100695 $expr = '';
696 $opened = 0;
697
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100698 // the regular expression for extracting math has 1 significant problem:
699 // it can't determine the correct closing parenthesis...
700 // instead, it'll match a larger portion of code to where it's certain that
701 // the calc() musts have ended, and we'll figure out which is the correct
702 // closing parenthesis here, by counting how many have opened
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100703 for ($i = 0; $i < $length; $i++) {
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100704 $char = $match[2][$i];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100705 $expr .= $char;
706 if ($char === '(') {
707 $opened++;
708 } elseif ($char === ')' && --$opened === 0) {
709 break;
710 }
711 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100712
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100713 // now that we've figured out where the calc() starts and ends, extract it
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100714 $count = count($minifier->extracted);
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100715 $placeholder = 'math('.$count.')';
716 $minifier->extracted[$placeholder] = $function.'('.trim(substr($expr, 1, -1)).')';
717
718 // and since we've captured more code than required, we may have some leftover
719 // calc() in here too - go recursive on the remaining but of code to go figure
720 // that out and extract what is needed
721 $rest = str_replace($function.$expr, '', $match[0]);
722 $rest = preg_replace_callback($pattern, $callback, $rest);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100723
724 return $placeholder.$rest;
725 };
726
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100727 $this->registerPattern($pattern, $callback);
728 }
729
730 /**
731 * Replace custom properties, whose values may be used in scenarios where
732 * we wouldn't want them to be minified (e.g. inside calc)
733 */
734 protected function extractCustomProperties()
735 {
736 // PHP only supports $this inside anonymous functions since 5.4
737 $minifier = $this;
738 $this->registerPattern(
739 '/(?<=^|[;}])(--[^:;{}"\'\s]+)\s*:([^;{}]+)/m',
740 function ($match) use ($minifier) {
741 $placeholder = '--custom-'. count($minifier->extracted) . ':0';
742 $minifier->extracted[$placeholder] = $match[1] .':'. trim($match[2]);
743 return $placeholder;
744
745 }
746 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100747 }
748
749 /**
750 * Check if file is small enough to be imported.
751 *
752 * @param string $path The path to the file
753 *
754 * @return bool
755 */
756 protected function canImportBySize($path)
757 {
758 return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024;
759 }
760
761 /**
762 * Check if file a file can be imported, going by the path.
763 *
764 * @param string $path
765 *
766 * @return bool
767 */
768 protected function canImportByPath($path)
769 {
770 return preg_match('/^(data:|https?:|\\/)/', $path) === 0;
771 }
772
773 /**
774 * Return a converter to update relative paths to be relative to the new
775 * destination.
776 *
777 * @param string $source
778 * @param string $target
779 *
780 * @return ConverterInterface
781 */
782 protected function getPathConverter($source, $target)
783 {
784 return new Converter($source, $target);
785 }
786}