Completed
Pull Request — master (#320)
by David
01:20
created

CSS::extractCalcs()   A

Complexity

Conditions 5
Paths 1

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 9.1288
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
        $preservecommentpattern = '/(
299
            # optional newline
300
            \n?
301
            # start comment
302
            \/\*
303
            # comment content
304
            (?:
305
                # either starts with an !
306
                !
307
            |
308
                # or, after some number of characters which do not end the comment
309
                (?:(?!\*\/).)*?
310
                # there is either a @license or @preserve tag
311
                @(?:license|preserve)
312
            )
313
            # then match to the end of the comment
314
            .*?\*\/\n?
315
            )/ixs';
316
317
        // loop CSS data (raw data and files)
318
        foreach ($this->data as $source => $css) {
319
            // Split JS on special comments.
320
            $chunks = preg_split($preservecommentpattern, $css, -1, PREG_SPLIT_DELIM_CAPTURE );
321
            $processed = [];
322
            for ($i = 0; $i < count($chunks); $i += 2) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
323
                $code = $chunks[$i];
324
                $comment = '';
325
                if (isset($chunks[$i + 1])) {
326
                    $comment = $chunks[$i + 1];
327
                }
328
                /*
329
                * Let's first take out strings & other comments, since we can't just
330
                * remove whitespace anywhere. If whitespace occurs inside a string,
331
                * we should leave it alone. E.g.:
332
                * p { content: "a   test" }
333
                */
334
                $this->extractStrings();
335
                $this->stripComments();
336
                $this->extractCalcs();
337
                $code = $this->replace($code);
338
339
                $code = $this->stripWhitespace($code);
340
                $code = $this->shortenColors($code);
341
                $code = $this->shortenZeroes($code);
342
                $code = $this->shortenFontWeights($code);
343
                $code = $this->stripEmptyTags($code);
344
345
                // restore the string we've extracted earlier
346
                $code = $this->restoreExtractedData($code);
347
348
                $processed[] = $code;
349
                $processed[] = $comment;
350
            }
351
            $css = implode($processed);
352
            $source = is_int($source) ? '' : $source;
353
            $parents = $source ? array_merge($parents, array($source)) : $parents;
354
            $css = $this->combineImports($source, $css, $parents);
355
            $css = $this->importFiles($source, $css);
356
357
            /*
358
             * If we'll save to a new path, we'll have to fix the relative paths
359
             * to be relative no longer to the source file, but to the new path.
360
             * If we don't write to a file, fall back to same path so no
361
             * conversion happens (because we still want it to go through most
362
             * of the move code, which also addresses url() & @import syntax...)
363
             */
364
            $converter = $this->getPathConverter($source, $path ?: $source);
365
            $css = $this->move($converter, $css);
366
367
            // combine css
368
            $content[] = $css;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$content was never initialized. Although not strictly required by PHP, it is generally a good practice to add $content = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
369
        }
370
        $content = implode($content);
0 ignored issues
show
Bug introduced by
The variable $content does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

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