Subversion Repositories oidplus

Rev

Rev 248 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

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