Completed
Push — release-2.1 ( c71d4f...42c11a )
by John
06:53
created

CSS::stripComments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace MatthiasMullie\Minify;
4
5
use MatthiasMullie\Minify\Exceptions\FileImportException;
6
use MatthiasMullie\PathConverter\Converter;
7
8
/**
9
 * CSS minifier.
10
 *
11
 * Please report bugs on https://github.com/matthiasmullie/minify/issues
12
 *
13
 * @author Matthias Mullie <[email protected]>
14
 * @author Tijs Verkoyen <[email protected]>
15
 * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
16
 * @license MIT License
17
 */
18
class CSS extends Minify
19
{
20
    /**
21
     * @var int
22
     */
23
    protected $maxImportSize = 5;
24
25
    /**
26
     * @var string[]
27
     */
28
    protected $importExtensions = array(
29
        'gif' => 'data:image/gif',
30
        'png' => 'data:image/png',
31
        'jpe' => 'data:image/jpeg',
32
        'jpg' => 'data:image/jpeg',
33
        'jpeg' => 'data:image/jpeg',
34
        'svg' => 'data:image/svg+xml',
35
        'woff' => 'data:application/x-font-woff',
36
        'tif' => 'image/tiff',
37
        'tiff' => 'image/tiff',
38
        'xbm' => 'image/x-xbitmap',
39
    );
40
41
    /**
42
     * Set the maximum size if files to be imported.
43
     *
44
     * Files larger than this size (in kB) will not be imported into the CSS.
45
     * Importing files into the CSS as data-uri will save you some connections,
46
     * but we should only import relatively small decorative images so that our
47
     * CSS file doesn't get too bulky.
48
     *
49
     * @param int $size Size in kB
50
     */
51
    public function setMaxImportSize($size)
52
    {
53
        $this->maxImportSize = $size;
54
    }
55
56
    /**
57
     * Set the type of extensions to be imported into the CSS (to save network
58
     * connections).
59
     * Keys of the array should be the file extensions & respective values
60
     * should be the data type.
61
     *
62
     * @param string[] $extensions Array of file extensions
63
     */
64
    public function setImportExtensions(array $extensions)
65
    {
66
        $this->importExtensions = $extensions;
67
    }
68
69
    /**
70
     * Move any import statements to the top.
71
     *
72
     * @param string $content Nearly finished CSS content
73
     *
74
     * @return string
75
     */
76
    protected function moveImportsToTop($content)
77
    {
78
        if (preg_match_all('/@import[^;]+;/', $content, $matches)) {
79
            // remove from content
80
            foreach ($matches[0] as $import) {
81
                $content = str_replace($import, '', $content);
82
            }
83
84
            // add to top
85
            $content = implode('', $matches[0]).$content;
86
        }
87
88
        return $content;
89
    }
90
91
    /**
92
     * Combine CSS from import statements.
93
     *
94
     * @import's will be loaded and their content merged into the original file,
95
     * to save HTTP requests.
96
     *
97
     * @param string   $source  The file to combine imports for
98
     * @param string   $content The CSS content to combine imports for
99
     * @param string[] $parents Parent paths, for circular reference checks
100
     *
101
     * @return string
102
     *
103
     * @throws FileImportException
104
     */
105
    protected function combineImports($source, $content, $parents)
106
    {
107
        $importRegexes = array(
108
            // @import url(xxx)
109
            '/
110
            # import statement
111
            @import
112
113
            # whitespace
114
            \s+
115
116
                # open url()
117
                url\(
118
119
                    # (optional) open path enclosure
120
                    (?P<quotes>["\']?)
121
122
                        # fetch path
123
                        (?P<path>
124
125
                            # do not fetch data uris or external sources
126
                            (?!(
127
                                ["\']?
128
                                (data|https?):
129
                            ))
130
131
                            .+?
132
                        )
133
134
                    # (optional) close path enclosure
135
                    (?P=quotes)
136
137
                # close url()
138
                \)
139
140
                # (optional) trailing whitespace
141
                \s*
142
143
                # (optional) media statement(s)
144
                (?P<media>[^;]*)
145
146
                # (optional) trailing whitespace
147
                \s*
148
149
            # (optional) closing semi-colon
150
            ;?
151
152
            /ix',
153
154
            // @import 'xxx'
155
            '/
156
157
            # import statement
158
            @import
159
160
            # whitespace
161
            \s+
162
163
                # open path enclosure
164
                (?P<quotes>["\'])
165
166
                    # fetch path
167
                    (?P<path>
168
169
                        # do not fetch data uris or external sources
170
                        (?!(
171
                            ["\']?
172
                            (data|https?):
173
                        ))
174
175
                        .+?
176
                    )
177
178
                # close path enclosure
179
                (?P=quotes)
180
181
                # (optional) trailing whitespace
182
                \s*
183
184
                # (optional) media statement(s)
185
                (?P<media>[^;]*)
186
187
                # (optional) trailing whitespace
188
                \s*
189
190
            # (optional) closing semi-colon
191
            ;?
192
193
            /ix',
194
        );
195
196
        // find all relative imports in css
197
        $matches = array();
198
        foreach ($importRegexes as $importRegex) {
199
            if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) {
200
                $matches = array_merge($matches, $regexMatches);
201
            }
202
        }
203
204
        $search = array();
205
        $replace = array();
206
207
        // loop the matches
208
        foreach ($matches as $match) {
209
            // get the path for the file that will be imported
210
            $importPath = dirname($source).'/'.$match['path'];
211
212
            // only replace the import with the content if we can grab the
213
            // content of the file
214
            if ($this->canImportFile($importPath)) {
215
                // check if current file was not imported previously in the same
216
                // import chain.
217
                if (in_array($importPath, $parents)) {
218
                    throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.');
219
                }
220
221
                // grab referenced file & minify it (which may include importing
222
                // yet other @import statements recursively)
223
                $minifier = new static($importPath);
224
                $importContent = $minifier->execute($source, $parents);
225
226
                // check if this is only valid for certain media
227
                if (!empty($match['media'])) {
228
                    $importContent = '@media '.$match['media'].'{'.$importContent.'}';
229
                }
230
231
                // add to replacement array
232
                $search[] = $match[0];
233
                $replace[] = $importContent;
234
            }
235
        }
236
237
        // replace the import statements
238
        $content = str_replace($search, $replace, $content);
239
240
        return $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
        $extensions = array_keys($this->importExtensions);
257
        $regex = '/url\((["\']?)((?!["\']?data:).*?\.('.implode('|', $extensions).'))\\1\)/i';
258
        if ($extensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions of type integer[] 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...
259
            $search = array();
260
            $replace = array();
261
262
            // loop the matches
263
            foreach ($matches as $match) {
264
                // get the path for the file that will be imported
265
                $path = $match[2];
266
                $path = dirname($source).'/'.$path;
267
                $extension = $match[3];
268
269
                // only replace the import with the content if we're able to get
270
                // the content of the file, and it's relatively small
271
                if ($this->canImportFile($path) && $this->canImportBySize($path)) {
272
                    // grab content && base64-ize
273
                    $importContent = $this->load($path);
274
                    $importContent = base64_encode($importContent);
275
276
                    // build replacement
277
                    $search[] = $match[0];
278
                    $replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')';
279
                }
280
            }
281
282
            // replace the import statements
283
            $content = str_replace($search, $replace, $content);
284
        }
285
286
        return $content;
287
    }
288
289
    /**
290
     * Minify the data.
291
     * Perform CSS optimizations.
292
     *
293
     * @param string[optional] $path    Path to write the data to
294
     * @param string[]         $parents Parent paths, for circular reference checks
295
     *
296
     * @return string The minified data
297
     */
298
    public function execute($path = null, $parents = array())
299
    {
300
        $content = '';
301
302
        // loop css data (raw data and files)
303
        foreach ($this->data as $source => $css) {
304
            /*
305
             * Let's first take out strings & comments, since we can't just remove
306
             * whitespace anywhere. If whitespace occurs inside a string, we should
307
             * leave it alone. E.g.:
308
             * p { content: "a   test" }
309
             */
310
            $this->extractStrings();
311
            $this->stripComments();
312
            $css = $this->replace($css);
313
314
            $css = $this->stripWhitespace($css);
315
            $css = $this->shortenHex($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...)
334
             */
335
            $converter = new Converter($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 Converter $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(Converter $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
                        # do not fetch data uris or external sources
386
                        (?!(
387
                            \s?
388
                            ["\']?
389
                            (data|https?):
390
                        ))
391
392
                        .+?
393
                    )
394
395
                # close path enclosure
396
                (?(quotes)(?P=quotes))
397
398
                \s*
399
400
            # close url()
401
            \)
402
403
            /ix',
404
405
            // @import "xxx"
406
            '/
407
            # import statement
408
            @import
409
410
            # whitespace
411
            \s+
412
413
                # we don\'t have to check for @import url(), because the
414
                # condition above will already catch these
415
416
                # open path enclosure
417
                (?P<quotes>["\'])
418
419
                    # fetch path
420
                    (?P<path>
421
422
                        # do not fetch data uris or external sources
423
                        (?!(
424
                            ["\']?
425
                            (data|https?):
426
                        ))
427
428
                        .+?
429
                    )
430
431
                # close path enclosure
432
                (?P=quotes)
433
434
            /ix',
435
        );
436
437
        // find all relative urls in css
438
        $matches = array();
439
        foreach ($relativeRegexes as $relativeRegex) {
440
            if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) {
441
                $matches = array_merge($matches, $regexMatches);
442
            }
443
        }
444
445
        $search = array();
446
        $replace = array();
447
448
        // loop all urls
449
        foreach ($matches as $match) {
450
            // determine if it's a url() or an @import match
451
            $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url');
452
453
            // attempting to interpret GET-params makes no sense, so let's discard them for awhile
454
            $params = strrchr($match['path'], '?');
455
            $url = $params ? substr($match['path'], 0, -strlen($params)) : $match['path'];
456
457
            // fix relative url
458
            $url = $converter->convert($url);
459
460
            // now that the path has been converted, re-apply GET-params
461
            $url .= $params;
462
463
            // build replacement
464
            $search[] = $match[0];
465
            if ($type == 'url') {
466
                $replace[] = 'url('.$url.')';
467
            } elseif ($type == 'import') {
468
                $replace[] = '@import "'.$url.'"';
469
            }
470
        }
471
472
        // replace urls
473
        $content = str_replace($search, $replace, $content);
474
475
        return $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 shortenHex($content)
487
    {
488
        $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?=[; }])/i', '#$1$2$3', $content);
489
490
        // we can shorten some even more by replacing them with their color name
491
        $colors = array(
492
            '#F0FFFF' => 'azure',
493
            '#F5F5DC' => 'beige',
494
            '#A52A2A' => 'brown',
495
            '#FF7F50' => 'coral',
496
            '#FFD700' => 'gold',
497
            '#808080' => 'gray',
498
            '#008000' => 'green',
499
            '#4B0082' => 'indigo',
500
            '#FFFFF0' => 'ivory',
501
            '#F0E68C' => 'khaki',
502
            '#FAF0E6' => 'linen',
503
            '#800000' => 'maroon',
504
            '#000080' => 'navy',
505
            '#808000' => 'olive',
506
            '#CD853F' => 'peru',
507
            '#FFC0CB' => 'pink',
508
            '#DDA0DD' => 'plum',
509
            '#800080' => 'purple',
510
            '#F00' => 'red',
511
            '#FA8072' => 'salmon',
512
            '#A0522D' => 'sienna',
513
            '#C0C0C0' => 'silver',
514
            '#FFFAFA' => 'snow',
515
            '#D2B48C' => 'tan',
516
            '#FF6347' => 'tomato',
517
            '#EE82EE' => 'violet',
518
            '#F5DEB3' => 'wheat',
519
        );
520
521
        return preg_replace_callback(
522
            '/(?<=[: ])('.implode(array_keys($colors), '|').')(?=[; }])/i',
523
            function ($match) use ($colors) {
524
                return $colors[strtoupper($match[0])];
525
            },
526
            $content
527
        );
528
    }
529
530
    /**
531
     * Shorten CSS font weights.
532
     *
533
     * @param string $content The CSS content to shorten the font weights for
534
     *
535
     * @return string
536
     */
537
    protected function shortenFontWeights($content)
538
    {
539
        $weights = array(
540
            'normal' => 400,
541
            'bold' => 700,
542
        );
543
544
        $callback = function ($match) use ($weights) {
545
            return $match[1].$weights[$match[2]];
546
        };
547
548
        return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content);
549
    }
550
551
    /**
552
     * Shorthand 0 values to plain 0, instead of e.g. -0em.
553
     *
554
     * @param string $content The CSS content to shorten the zero values for
555
     *
556
     * @return string
557
     */
558
    protected function shortenZeroes($content)
559
    {
560
        // reusable bits of code throughout these regexes:
561
        // before & after are used to make sure we don't match lose unintended
562
        // 0-like values (e.g. in #000, or in http://url/1.0)
563
        // units can be stripped from 0 values, or used to recognize non 0
564
        // values (where wa may be able to strip a .0 suffix)
565
        $before = '(?<=[:(, ])';
566
        $after = '(?=[ ,);}])';
567
        $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)';
568
569
        // strip units after zeroes (0px -> 0)
570
        // NOTE: it should be safe to remove all units for a 0 value, but in
571
        // practice, Webkit (especially Safari) seems to stumble over at least
572
        // 0%, potentially other units as well. Only stripping 'px' for now.
573
        // @see https://github.com/matthiasmullie/minify/issues/60
574
        $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content);
575
576
        // strip 0-digits (.0 -> 0)
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
577
        $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content);
578
        // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px
0 ignored issues
show
Unused Code Comprehensibility introduced by
41% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
579
        $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content);
580
        // strip trailing 0: 50.00 -> 50, 50.00px -> 50px
0 ignored issues
show
Unused Code Comprehensibility introduced by
41% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
581
        $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content);
582
        // strip leading 0: 0.1 -> .1, 01.1 -> 1.1
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
583
        $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content);
584
585
        // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
586
        $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content);
587
588
        // remove zeroes where they make no sense in calc: e.g. calc(100px - 0)
589
        // the 0 doesn't have any effect, and this isn't even valid without unit
590
        // strip all `+ 0` or `- 0` occurrences: calc(10% + 0) -> calc(10%)
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
591
        // looped because there may be multiple 0s inside 1 group of parentheses
592
        do {
593
            $previous = $content;
594
            $content = preg_replace('/\(([^\(\)]+)\s+[\+\-]\s+0(\s+[^\(\)]+)?\)/', '(\\1\\2)', $content);
595
        } while ($content !== $previous);
596
        // strip all `0 +` occurrences: calc(0 + 10%) -> calc(10%)
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
597
        $content = preg_replace('/\(\s*0\s+\+\s+([^\(\)]+)\)/', '(\\1)', $content);
598
        // strip all `0 -` occurrences: calc(0 - 10%) -> calc(-10%)
599
        $content = preg_replace('/\(\s*0\s+\-\s+([^\(\)]+)\)/', '(-\\1)', $content);
600
        // I'm not going to attempt to optimize away `x * 0` instances:
601
        // it's dumb enough code already that it likely won't occur, and it's
602
        // too complex to do right (order of operations would have to be
603
        // respected etc)
604
        // what I cared about most here was fixing incorrectly truncated units
605
606
        return $content;
607
    }
608
609
    /**
610
     * Strip comments from source code.
611
     *
612
     * @param string $content
613
     *
614
     * @return string
615
     */
616
    protected function stripEmptyTags($content)
617
    {
618
        return preg_replace('/(^|\}|;)[^\{\};]+\{\s*\}/', '\\1', $content);
619
    }
620
621
    /**
622
     * Strip comments from source code.
623
     */
624
    protected function stripComments()
625
    {
626
        $this->registerPattern('/\/\*.*?\*\//s', '');
627
    }
628
629
    /**
630
     * Strip whitespace.
631
     *
632
     * @param string $content The CSS content to strip the whitespace for
633
     *
634
     * @return string
635
     */
636
    protected function stripWhitespace($content)
637
    {
638
        // remove leading & trailing whitespace
639
        $content = preg_replace('/^\s*/m', '', $content);
640
        $content = preg_replace('/\s*$/m', '', $content);
641
642
        // replace newlines with a single space
643
        $content = preg_replace('/\s+/', ' ', $content);
644
645
        // remove whitespace around meta characters
646
        // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex
647
        $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content);
648
        $content = preg_replace('/([\[(:])\s+/', '$1', $content);
649
        $content = preg_replace('/\s+([\]\)])/', '$1', $content);
650
        $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content);
651
652
        // whitespace around + and - can only be stripped in selectors, like
653
        // :nth-child(3+2n), not in things like calc(3px + 2px) or shorthands
654
        // like 3px -2px
655
        $content = preg_replace('/\s*([+-])\s*(?=[^}]*{)/', '$1', $content);
656
657
        // remove semicolon/whitespace followed by closing bracket
658
        $content = str_replace(';}', '}', $content);
659
660
        return trim($content);
661
    }
662
663
    /**
664
     * Check if file is small enough to be imported.
665
     *
666
     * @param string $path The path to the file
667
     *
668
     * @return bool
669
     */
670
    protected function canImportBySize($path)
671
    {
672
        return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024;
673
    }
674
}
675