Completed
Push — master ( 17888a...f6b0e7 )
by Matthias
01:53
created

CSS::getPathConverter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
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
            $css = $this->replace($css);
312
313
            $css = $this->stripWhitespace($css);
314
            $css = $this->shortenColors($css);
315
            $css = $this->shortenZeroes($css);
316
            $css = $this->shortenFontWeights($css);
317
            $css = $this->stripEmptyTags($css);
318
319
            // restore the string we've extracted earlier
320
            $css = $this->restoreExtractedData($css);
321
322
            $source = is_int($source) ? '' : $source;
323
            $parents = $source ? array_merge($parents, array($source)) : $parents;
324
            $css = $this->combineImports($source, $css, $parents);
325
            $css = $this->importFiles($source, $css);
326
327
            /*
328
             * If we'll save to a new path, we'll have to fix the relative paths
329
             * to be relative no longer to the source file, but to the new path.
330
             * If we don't write to a file, fall back to same path so no
331
             * conversion happens (because we still want it to go through most
332
             * of the move code, which also addresses url() & @import syntax...)
333
             */
334
            $converter = $this->getPathConverter($source, $path ?: $source);
335
            $css = $this->move($converter, $css);
336
337
            // combine css
338
            $content .= $css;
339
        }
340
341
        $content = $this->moveImportsToTop($content);
342
343
        return $content;
344
    }
345
346
    /**
347
     * Moving a css file should update all relative urls.
348
     * Relative references (e.g. ../images/image.gif) in a certain css file,
349
     * will have to be updated when a file is being saved at another location
350
     * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper).
351
     *
352
     * @param ConverterInterface $converter Relative path converter
353
     * @param string             $content   The CSS content to update relative urls for
354
     *
355
     * @return string
356
     */
357
    protected function move(ConverterInterface $converter, $content)
358
    {
359
        /*
360
         * Relative path references will usually be enclosed by url(). @import
361
         * is an exception, where url() is not necessary around the path (but is
362
         * allowed).
363
         * This *could* be 1 regular expression, where both regular expressions
364
         * in this array are on different sides of a |. But we're using named
365
         * patterns in both regexes, the same name on both regexes. This is only
366
         * possible with a (?J) modifier, but that only works after a fairly
367
         * recent PCRE version. That's why I'm doing 2 separate regular
368
         * expressions & combining the matches after executing of both.
369
         */
370
        $relativeRegexes = array(
371
            // url(xxx)
372
            '/
373
            # open url()
374
            url\(
375
376
                \s*
377
378
                # open path enclosure
379
                (?P<quotes>["\'])?
380
381
                    # fetch path
382
                    (?P<path>.+?)
383
384
                # close path enclosure
385
                (?(quotes)(?P=quotes))
386
387
                \s*
388
389
            # close url()
390
            \)
391
392
            /ix',
393
394
            // @import "xxx"
395
            '/
396
            # import statement
397
            @import
398
399
            # whitespace
400
            \s+
401
402
                # we don\'t have to check for @import url(), because the
403
                # condition above will already catch these
404
405
                # open path enclosure
406
                (?P<quotes>["\'])
407
408
                    # fetch path
409
                    (?P<path>.+?)
410
411
                # close path enclosure
412
                (?P=quotes)
413
414
            /ix',
415
        );
416
417
        // find all relative urls in css
418
        $matches = array();
419
        foreach ($relativeRegexes as $relativeRegex) {
420
            if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) {
421
                $matches = array_merge($matches, $regexMatches);
422
            }
423
        }
424
425
        $search = array();
426
        $replace = array();
427
428
        // loop all urls
429
        foreach ($matches as $match) {
430
            // determine if it's a url() or an @import match
431
            $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url');
432
433
            $url = $match['path'];
434
            if ($this->canImportByPath($url)) {
435
                // attempting to interpret GET-params makes no sense, so let's discard them for awhile
436
                $params = strrchr($url, '?');
437
                $url = $params ? substr($url, 0, -strlen($params)) : $url;
438
439
                // fix relative url
440
                $url = $converter->convert($url);
441
442
                // now that the path has been converted, re-apply GET-params
443
                $url .= $params;
444
            }
445
446
            /*
447
             * Urls with control characters above 0x7e should be quoted.
448
             * According to Mozilla's parser, whitespace is only allowed at the
449
             * end of unquoted urls.
450
             * Urls with `)` (as could happen with data: uris) should also be
451
             * quoted to avoid being confused for the url() closing parentheses.
452
             * And urls with a # have also been reported to cause issues.
453
             * Urls with quotes inside should also remain escaped.
454
             *
455
             * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation
456
             * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378
457
             * @see https://github.com/matthiasmullie/minify/issues/193
458
             */
459
            $url = trim($url);
460
            if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) {
461
                $url = $match['quotes'] . $url . $match['quotes'];
462
            }
463
464
            // build replacement
465
            $search[] = $match[0];
466
            if ($type === 'url') {
467
                $replace[] = 'url('.$url.')';
468
            } elseif ($type === 'import') {
469
                $replace[] = '@import "'.$url.'"';
470
            }
471
        }
472
473
        // replace urls
474
        return str_replace($search, $replace, $content);
475
    }
476
477
    /**
478
     * Shorthand hex color codes.
479
     * #FF0000 -> #F00.
480
     *
481
     * @param string $content The CSS content to shorten the hex color codes for
482
     *
483
     * @return string
484
     */
485
    protected function shortenColors($content)
486
    {
487
        $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);
488
489
        // remove alpha channel if it's pointless...
490
        $content = preg_replace('/(?<=[: ])#([0-9a-z]{6})ff?(?=[; }])/i', '#$1', $content);
491
        $content = preg_replace('/(?<=[: ])#([0-9a-z]{3})f?(?=[; }])/i', '#$1', $content);
492
493
        $colors = array(
494
            // we can shorten some even more by replacing them with their color name
495
            '#F0FFFF' => 'azure',
496
            '#F5F5DC' => 'beige',
497
            '#A52A2A' => 'brown',
498
            '#FF7F50' => 'coral',
499
            '#FFD700' => 'gold',
500
            '#808080' => 'gray',
501
            '#008000' => 'green',
502
            '#4B0082' => 'indigo',
503
            '#FFFFF0' => 'ivory',
504
            '#F0E68C' => 'khaki',
505
            '#FAF0E6' => 'linen',
506
            '#800000' => 'maroon',
507
            '#000080' => 'navy',
508
            '#808000' => 'olive',
509
            '#CD853F' => 'peru',
510
            '#FFC0CB' => 'pink',
511
            '#DDA0DD' => 'plum',
512
            '#800080' => 'purple',
513
            '#F00' => 'red',
514
            '#FA8072' => 'salmon',
515
            '#A0522D' => 'sienna',
516
            '#C0C0C0' => 'silver',
517
            '#FFFAFA' => 'snow',
518
            '#D2B48C' => 'tan',
519
            '#FF6347' => 'tomato',
520
            '#EE82EE' => 'violet',
521
            '#F5DEB3' => 'wheat',
522
            // or the other way around
523
            'WHITE' => '#fff',
524
            'BLACK' => '#000',
525
        );
526
527
        return preg_replace_callback(
528
            '/(?<=[: ])('.implode(array_keys($colors), '|').')(?=[; }])/i',
529
            function ($match) use ($colors) {
530
                return $colors[strtoupper($match[0])];
531
            },
532
            $content
533
        );
534
    }
535
536
    /**
537
     * Shorten CSS font weights.
538
     *
539
     * @param string $content The CSS content to shorten the font weights for
540
     *
541
     * @return string
542
     */
543
    protected function shortenFontWeights($content)
544
    {
545
        $weights = array(
546
            'normal' => 400,
547
            'bold' => 700,
548
        );
549
550
        $callback = function ($match) use ($weights) {
551
            return $match[1].$weights[$match[2]];
552
        };
553
554
        return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content);
555
    }
556
557
    /**
558
     * Shorthand 0 values to plain 0, instead of e.g. -0em.
559
     *
560
     * @param string $content The CSS content to shorten the zero values for
561
     *
562
     * @return string
563
     */
564
    protected function shortenZeroes($content)
565
    {
566
        // we don't want to strip units in `calc()` expressions:
567
        // `5px - 0px` is valid, but `5px - 0` is not
568
        // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but
569
        // `10 * 0` is invalid
570
        // we've extracted calcs earlier, so we don't need to worry about this
571
572
        // reusable bits of code throughout these regexes:
573
        // before & after are used to make sure we don't match lose unintended
574
        // 0-like values (e.g. in #000, or in http://url/1.0)
575
        // units can be stripped from 0 values, or used to recognize non 0
576
        // values (where wa may be able to strip a .0 suffix)
577
        $before = '(?<=[:(, ])';
578
        $after = '(?=[ ,);}])';
579
        $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)';
580
581
        // strip units after zeroes (0px -> 0)
582
        // NOTE: it should be safe to remove all units for a 0 value, but in
583
        // practice, Webkit (especially Safari) seems to stumble over at least
584
        // 0%, potentially other units as well. Only stripping 'px' for now.
585
        // @see https://github.com/matthiasmullie/minify/issues/60
586
        $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content);
587
588
        // strip 0-digits (.0 -> 0)
589
        $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content);
590
        // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px
591
        $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content);
592
        // strip trailing 0: 50.00 -> 50, 50.00px -> 50px
593
        $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content);
594
        // strip leading 0: 0.1 -> .1, 01.1 -> 1.1
595
        $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content);
596
597
        // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
598
        $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content);
599
600
        // IE doesn't seem to understand a unitless flex-basis value (correct -
601
        // it goes against the spec), so let's add it in again (make it `%`,
602
        // which is only 1 char: 0%, 0px, 0 anything, it's all just the same)
603
        // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex
604
        $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content);
605
        $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content);
606
607
        return $content;
608
    }
609
610
    /**
611
     * Strip empty tags from source code.
612
     *
613
     * @param string $content
614
     *
615
     * @return string
616
     */
617
    protected function stripEmptyTags($content)
618
    {
619
        $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content);
620
        $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content);
621
622
        return $content;
623
    }
624
625
    /**
626
     * Strip comments from source code.
627
     */
628 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...
629
    {
630
        // PHP only supports $this inside anonymous functions since 5.4
631
        $minifier = $this;
632
        $callback = function ($match) use ($minifier) {
633
            $count = count($minifier->extracted);
634
            $placeholder = '/*'.$count.'*/';
635
            $minifier->extracted[$placeholder] = $match[0];
636
637
            return $placeholder;
638
        };
639
        $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback);
640
641
        $this->registerPattern('/\/\*.*?\*\//s', '');
642
    }
643
644
    /**
645
     * Strip whitespace.
646
     *
647
     * @param string $content The CSS content to strip the whitespace for
648
     *
649
     * @return string
650
     */
651
    protected function stripWhitespace($content)
652
    {
653
        // remove leading & trailing whitespace
654
        $content = preg_replace('/^\s*/m', '', $content);
655
        $content = preg_replace('/\s*$/m', '', $content);
656
657
        // replace newlines with a single space
658
        $content = preg_replace('/\s+/', ' ', $content);
659
660
        // remove whitespace around meta characters
661
        // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex
662
        $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content);
663
        $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content);
664
        $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content);
665
        $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content);
666
667
        // whitespace around + and - can only be stripped inside some pseudo-
668
        // classes, like `:nth-child(3+2n)`
669
        // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or
670
        // selectors like `div.weird- p`
671
        $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type');
672
        $content = preg_replace('/:('.implode('|', $pseudos).')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content);
673
674
        // remove semicolon/whitespace followed by closing bracket
675
        $content = str_replace(';}', '}', $content);
676
677
        return trim($content);
678
    }
679
680
    /**
681
     * Replace all `calc()` occurrences.
682
     */
683
    protected function extractCalcs()
684
    {
685
        // PHP only supports $this inside anonymous functions since 5.4
686
        $minifier = $this;
687
        $callback = function ($match) use ($minifier) {
688
            $length = strlen($match[1]);
689
            $expr = '';
690
            $opened = 0;
691
692
            for ($i = 0; $i < $length; $i++) {
693
                $char = $match[1][$i];
694
                $expr .= $char;
695
                if ($char === '(') {
696
                    $opened++;
697
                } elseif ($char === ')' && --$opened === 0) {
698
                    break;
699
                }
700
            }
701
            $rest = str_replace($expr, '', $match[1]);
702
            $expr = trim(substr($expr, 1, -1));
703
704
            $count = count($minifier->extracted);
705
            $placeholder = 'calc('.$count.')';
706
            $minifier->extracted[$placeholder] = 'calc('.$expr.')';
707
708
            return $placeholder.$rest;
709
        };
710
711
        $this->registerPattern('/calc(\(.+?)(?=$|;|calc\()/', $callback);
712
    }
713
714
    /**
715
     * Check if file is small enough to be imported.
716
     *
717
     * @param string $path The path to the file
718
     *
719
     * @return bool
720
     */
721
    protected function canImportBySize($path)
722
    {
723
        return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024;
724
    }
725
726
    /**
727
     * Check if file a file can be imported, going by the path.
728
     *
729
     * @param string $path
730
     *
731
     * @return bool
732
     */
733
    protected function canImportByPath($path)
734
    {
735
        return preg_match('/^(data:|https?:|\\/)/', $path) === 0;
736
    }
737
738
    /**
739
     * Return a converter to update relative paths to be relative to the new
740
     * destination.
741
     *
742
     * @param string $source
743
     * @param string $target
744
     *
745
     * @return ConverterInterface
746
     */
747
    protected function getPathConverter($source, $target)
748
    {
749
        return new Converter($source, $target);
750
    }
751
}
752