"Fossies" - the Fresh Open Source Software Archive

Member "grav/vendor/matthiasmullie/minify/src/CSS.php" (1 Sep 2020, 25451 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "CSS.php" see the Fossies "Dox" file reference documentation.

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