Completed
Pull Request — master (#322)
by
unknown
02:15
created

CSS::extractCalcs()   A

Complexity

Conditions 5
Paths 1

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 9.1128
c 0
b 0
f 0
cc 5
nc 1
nop 0
1
<?php
2
/**
3
 * CSS Minifier
4
 *
5
 * Please report bugs on https://github.com/matthiasmullie/minify/issues
6
 *
7
 * @author Matthias Mullie <[email protected]>
8
 * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
9
 * @license MIT License
10
 */
11
12
namespace MatthiasMullie\Minify;
13
14
use MatthiasMullie\Minify\Exceptions\FileImportException;
15
use MatthiasMullie\PathConverter\ConverterInterface;
16
use 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 <[email protected]>
25
 * @author Tijs Verkoyen <[email protected]>
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
        '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)
219
            $minifier = new static($importPath);
220
            $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)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->importExtensions of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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();
310
            $this->extractCalcs();
311
            $this->extractVarsZero();
312
            $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 stip units in vars, for example `--some-var: 0px`
568
        //we've extracted the zeroed vars earlier in extractVarsZero, so it's OK.
569
570
        // we don't want to strip units in `calc()` expressions:
571
        // `5px - 0px` is valid, but `5px - 0` is not
572
        // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but
573
        // `10 * 0` is invalid
574
        // we've extracted calcs earlier, so we don't need to worry about this
575
576
        // reusable bits of code throughout these regexes:
577
        // before & after are used to make sure we don't match lose unintended
578
        // 0-like values (e.g. in #000, or in http://url/1.0)
579
        // units can be stripped from 0 values, or used to recognize non 0
580
        // values (where wa may be able to strip a .0 suffix)
581
        $before = '(?<=[:(, ])';
582
        $after = '(?=[ ,);}])';
583
        $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)';
584
585
        // strip units after zeroes (0px -> 0)
586
        // NOTE: it should be safe to remove all units for a 0 value, but in
587
        // practice, Webkit (especially Safari) seems to stumble over at least
588
        // 0%, potentially other units as well. Only stripping 'px' for now.
589
        // @see https://github.com/matthiasmullie/minify/issues/60
590
        $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content);
591
592
        // strip 0-digits (.0 -> 0)
593
        $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content);
594
        // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px
595
        $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content);
596
        // strip trailing 0: 50.00 -> 50, 50.00px -> 50px
597
        $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content);
598
        // strip leading 0: 0.1 -> .1, 01.1 -> 1.1
599
        $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content);
600
601
        // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
602
        $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content);
603
604
        // IE doesn't seem to understand a unitless flex-basis value (correct -
605
        // it goes against the spec), so let's add it in again (make it `%`,
606
        // which is only 1 char: 0%, 0px, 0 anything, it's all just the same)
607
        // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex
608
        $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content);
609
        $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content);
610
611
        return $content;
612
    }
613
614
    /**
615
     * Strip empty tags from source code.
616
     *
617
     * @param string $content
618
     *
619
     * @return string
620
     */
621
    protected function stripEmptyTags($content)
622
    {
623
        $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content);
624
        $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content);
625
626
        return $content;
627
    }
628
629
    /**
630
     * Strip comments from source code.
631
     */
632 View Code Duplication
    protected function stripComments()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
633
    {
634
        // PHP only supports $this inside anonymous functions since 5.4
635
        $minifier = $this;
636
        $callback = function ($match) use ($minifier) {
637
            $count = count($minifier->extracted);
638
            $placeholder = '/*'.$count.'*/';
639
            $minifier->extracted[$placeholder] = $match[0];
640
641
            return $placeholder;
642
        };
643
        $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback);
644
645
        $this->registerPattern('/\/\*.*?\*\//s', '');
646
    }
647
648
    /**
649
     * Strip whitespace.
650
     *
651
     * @param string $content The CSS content to strip the whitespace for
652
     *
653
     * @return string
654
     */
655
    protected function stripWhitespace($content)
656
    {
657
        // remove leading & trailing whitespace
658
        $content = preg_replace('/^\s*/m', '', $content);
659
        $content = preg_replace('/\s*$/m', '', $content);
660
661
        // replace newlines with a single space
662
        $content = preg_replace('/\s+/', ' ', $content);
663
664
        // remove whitespace around meta characters
665
        // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex
666
        $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content);
667
        $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content);
668
        $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content);
669
        $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content);
670
671
        // whitespace around + and - can only be stripped inside some pseudo-
672
        // classes, like `:nth-child(3+2n)`
673
        // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or
674
        // selectors like `div.weird- p`
675
        $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type');
676
        $content = preg_replace('/:('.implode('|', $pseudos).')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content);
677
678
        // remove semicolon/whitespace followed by closing bracket
679
        $content = str_replace(';}', '}', $content);
680
681
        return trim($content);
682
    }
683
684
    /**
685
     * Replace all `calc()` occurrences.
686
     */
687
    protected function extractCalcs()
688
    {
689
        // PHP only supports $this inside anonymous functions since 5.4
690
        $minifier = $this;
691
        $callback = function ($match) use ($minifier) {
692
            $length = strlen($match[1]);
693
            $expr = '';
694
            $opened = 0;
695
696
            for ($i = 0; $i < $length; $i++) {
697
                $char = $match[1][$i];
698
                $expr .= $char;
699
                if ($char === '(') {
700
                    $opened++;
701
                } elseif ($char === ')' && --$opened === 0) {
702
                    break;
703
                }
704
            }
705
            $rest = str_replace($expr, '', $match[1]);
706
            $expr = trim(substr($expr, 1, -1));
707
708
            $count = count($minifier->extracted);
709
            $placeholder = 'calc('.$count.')';
710
            $minifier->extracted[$placeholder] = 'calc('.$expr.')';
711
712
            return $placeholder.$rest;
713
        };
714
715
        $this->registerPattern('/calc(\(.+?)(?=$|;|}|calc\()/', $callback);
716
        $this->registerPattern('/calc(\(.+?)(?=$|;|}|calc\()/m', $callback);
717
    }
718
719
    /**
720
     * Replace all `--my-variable: 0px` so they won't have the px removed
721
     */
722
    protected function extractVarsZero()
723
    {
724
        // PHP only supports $this inside anonymous functions since 5.4
725
        $minifier = $this;
726
        $callback = function ($match) use ($minifier) {
727
            $expr = '';
728
729
            $rest = str_replace($expr, '', $match[1]);
0 ignored issues
show
Unused Code introduced by
$rest is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
730
731
            $count = count($minifier->extracted);
732
            $placeholder = '--var-' . $count . ':1px';
733
            $minifier->extracted[$placeholder] = $match[1].':0'.$match[2];
734
735
            return $placeholder;
736
        };
737
738
        $this->registerPattern('/(--[_a-zA-Z0-9-]+)\s*:\s*0(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)/', $callback);
739
    }
740
741
    /**
742
     * Check if file is small enough to be imported.
743
     *
744
     * @param string $path The path to the file
745
     *
746
     * @return bool
747
     */
748
    protected function canImportBySize($path)
749
    {
750
        return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024;
751
    }
752
753
    /**
754
     * Check if file a file can be imported, going by the path.
755
     *
756
     * @param string $path
757
     *
758
     * @return bool
759
     */
760
    protected function canImportByPath($path)
761
    {
762
        return preg_match('/^(data:|https?:|\\/)/', $path) === 0;
763
    }
764
765
    /**
766
     * Return a converter to update relative paths to be relative to the new
767
     * destination.
768
     *
769
     * @param string $source
770
     * @param string $target
771
     *
772
     * @return ConverterInterface
773
     */
774
    protected function getPathConverter($source, $target)
775
    {
776
        return new Converter($source, $target);
777
    }
778
}
779