Completed
Push — composer-installed ( 5832b4 )
by Ilia
08:49
created

CSSmin   F

Complexity

Total Complexity 105

Size/Duplication

Total Lines 755
Duplicated Lines 0.26 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 2
loc 755
rs 1.845
c 0
b 0
f 0
wmc 105
lcom 1
cbo 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
D run() 0 91 15
A set_memory_limit() 0 4 1
A set_max_execution_time() 0 4 1
A set_pcre_backtrack_limit() 0 4 1
A set_pcre_recursion_limit() 0 4 1
A do_raise_php_limits() 0 18 5
C minify() 0 201 13
B extract_data_urls() 0 62 9
B compress_hex_colors() 0 51 8
A replace_string() 0 21 3
A replace_colon() 0 4 1
A replace_calc() 0 5 1
A rgb_to_hex() 0 25 5
A hsl_to_hex() 0 24 3
A hue_to_rgb() 2 8 6
A round_number() 0 4 1
A clamp_number() 0 4 1
A index_of() 0 6 2
B str_slice() 0 25 11
B normalize_int() 0 12 8

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CSSmin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CSSmin, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*!
4
 * cssmin.php v2.4.8-4
5
 * Author: Tubal Martin - http://tubalmartin.me/
6
 * Repo: https://github.com/tubalmartin/YUI-CSS-compressor-PHP-port
7
 *
8
 * This is a PHP port of the CSS minification tool distributed with YUICompressor,
9
 * itself a port of the cssmin utility by Isaac Schlueter - http://foohack.com/
10
 * Permission is hereby granted to use the PHP version under the same
11
 * conditions as the YUICompressor.
12
 */
13
14
/*!
15
 * YUI Compressor
16
 * http://developer.yahoo.com/yui/compressor/
17
 * Author: Julien Lecomte - http://www.julienlecomte.net/
18
 * Copyright (c) 2013 Yahoo! Inc. All rights reserved.
19
 * The copyrights embodied in the content of this file are licensed
20
 * by Yahoo! Inc. under the BSD (revised) open source license.
21
 */
22
23
class CSSmin
24
{
25
    const NL = '___YUICSSMIN_PRESERVED_NL___';
26
    const TOKEN = '___YUICSSMIN_PRESERVED_TOKEN_';
27
    const COMMENT = '___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_';
28
    const CLASSCOLON = '___YUICSSMIN_PSEUDOCLASSCOLON___';
29
    const QUERY_FRACTION = '___YUICSSMIN_QUERY_FRACTION___';
30
31
    private $comments;
32
    private $preserved_tokens;
33
    private $memory_limit;
34
    private $max_execution_time;
35
    private $pcre_backtrack_limit;
36
    private $pcre_recursion_limit;
37
    private $raise_php_limits;
38
39
    /**
40
     * @param bool|int $raise_php_limits
41
     * If true, PHP settings will be raised if needed
42
     */
43
    public function __construct($raise_php_limits = TRUE)
44
    {
45
        // Set suggested PHP limits
46
        $this->memory_limit = 128 * 1048576; // 128MB in bytes
47
        $this->max_execution_time = 60; // 1 min
48
        $this->pcre_backtrack_limit = 1000 * 1000;
49
        $this->pcre_recursion_limit =  500 * 1000;
50
51
        $this->raise_php_limits = (bool) $raise_php_limits;
52
    }
53
54
    /**
55
     * Minify a string of CSS
56
     * @param string $css
57
     * @param int|bool $linebreak_pos
58
     * @return string
59
     */
60
    public function run($css = '', $linebreak_pos = FALSE)
61
    {
62
        if (empty($css)) {
63
            return '';
64
        }
65
66
        if ($this->raise_php_limits) {
67
            $this->do_raise_php_limits();
68
        }
69
70
        $this->comments = array();
71
        $this->preserved_tokens = array();
72
73
        $start_index = 0;
74
        $length = strlen($css);
75
76
        $css = $this->extract_data_urls($css);
77
78
        // collect all comment blocks...
79
        while (($start_index = $this->index_of($css, '/*', $start_index)) >= 0) {
80
            $end_index = $this->index_of($css, '*/', $start_index + 2);
81
            if ($end_index < 0) {
82
                $end_index = $length;
83
            }
84
            $comment_found = $this->str_slice($css, $start_index + 2, $end_index);
85
            $this->comments[] = $comment_found;
86
            $comment_preserve_string = self::COMMENT . (count($this->comments) - 1) . '___';
87
            $css = $this->str_slice($css, 0, $start_index + 2) . $comment_preserve_string . $this->str_slice($css, $end_index);
88
            // Set correct start_index: Fixes issue #2528130
89
            $start_index = $end_index + 2 + strlen($comment_preserve_string) - strlen($comment_found);
90
        }
91
92
        // preserve strings so their content doesn't get accidentally minified
93
        $css = preg_replace_callback('/(?:"(?:[^\\\\"]|\\\\.|\\\\)*")|'."(?:'(?:[^\\\\']|\\\\.|\\\\)*')/S", array($this, 'replace_string'), $css);
94
95
        // Let's divide css code in chunks of 5.000 chars aprox.
96
        // Reason: PHP's PCRE functions like preg_replace have a "backtrack limit"
97
        // of 100.000 chars by default (php < 5.3.7) so if we're dealing with really
98
        // long strings and a (sub)pattern matches a number of chars greater than
99
        // the backtrack limit number (i.e. /(.*)/s) PCRE functions may fail silently
100
        // returning NULL and $css would be empty.
101
        $charset = '';
102
        $charset_regexp = '/(@charset)( [^;]+;)/i';
103
        $css_chunks = array();
104
        $css_chunk_length = 5000; // aprox size, not exact
105
        $start_index = 0;
106
        $i = $css_chunk_length; // save initial iterations
107
        $l = strlen($css);
108
109
110
        // if the number of characters is 5000 or less, do not chunk
111
        if ($l <= $css_chunk_length) {
112
            $css_chunks[] = $css;
113
        } else {
114
            // chunk css code securely
115
            while ($i < $l) {
116
                $i += 50; // save iterations
117
                if ($l - $start_index <= $css_chunk_length || $i >= $l) {
118
                    $css_chunks[] = $this->str_slice($css, $start_index);
119
                    break;
120
                }
121
                if ($css[$i - 1] === '}' && $i - $start_index > $css_chunk_length) {
122
                    // If there are two ending curly braces }} separated or not by spaces,
123
                    // join them in the same chunk (i.e. @media blocks)
124
                    $next_chunk = substr($css, $i);
125
                    if (preg_match('/^\s*\}/', $next_chunk)) {
126
                        $i = $i + $this->index_of($next_chunk, '}') + 1;
127
                    }
128
129
                    $css_chunks[] = $this->str_slice($css, $start_index, $i);
130
                    $start_index = $i;
131
                }
132
            }
133
        }
134
135
        // Minify each chunk
136
        for ($i = 0, $n = count($css_chunks); $i < $n; $i++) {
137
            $css_chunks[$i] = $this->minify($css_chunks[$i], $linebreak_pos);
138
            // Keep the first @charset at-rule found
139
            if (empty($charset) && preg_match($charset_regexp, $css_chunks[$i], $matches)) {
140
                $charset = strtolower($matches[1]) . $matches[2];
141
            }
142
            // Delete all @charset at-rules
143
            $css_chunks[$i] = preg_replace($charset_regexp, '', $css_chunks[$i]);
144
        }
145
146
        // Update the first chunk and push the charset to the top of the file.
147
        $css_chunks[0] = $charset . $css_chunks[0];
148
149
        return implode('', $css_chunks);
150
    }
151
152
    /**
153
     * Sets the memory limit for this script
154
     * @param int|string $limit
155
     */
156
    public function set_memory_limit($limit)
157
    {
158
        $this->memory_limit = $this->normalize_int($limit);
159
    }
160
161
    /**
162
     * Sets the maximum execution time for this script
163
     * @param int|string $seconds
164
     */
165
    public function set_max_execution_time($seconds)
166
    {
167
        $this->max_execution_time = (int) $seconds;
168
    }
169
170
    /**
171
     * Sets the PCRE backtrack limit for this script
172
     * @param int $limit
173
     */
174
    public function set_pcre_backtrack_limit($limit)
175
    {
176
        $this->pcre_backtrack_limit = (int) $limit;
177
    }
178
179
    /**
180
     * Sets the PCRE recursion limit for this script
181
     * @param int $limit
182
     */
183
    public function set_pcre_recursion_limit($limit)
184
    {
185
        $this->pcre_recursion_limit = (int) $limit;
186
    }
187
188
    /**
189
     * Try to configure PHP to use at least the suggested minimum settings
190
     */
191
    private function do_raise_php_limits()
192
    {
193
        $php_limits = array(
194
            'memory_limit' => $this->memory_limit,
195
            'max_execution_time' => $this->max_execution_time,
196
            'pcre.backtrack_limit' => $this->pcre_backtrack_limit,
197
            'pcre.recursion_limit' =>  $this->pcre_recursion_limit
198
        );
199
200
        // If current settings are higher respect them.
201
        foreach ($php_limits as $name => $suggested) {
202
            $current = $this->normalize_int(ini_get($name));
203
            // memory_limit exception: allow -1 for "no memory limit".
204
            if ($current > -1 && ($suggested == -1 || $current < $suggested)) {
205
                ini_set($name, $suggested);
206
            }
207
        }
208
    }
209
210
    /**
211
     * Does bulk of the minification
212
     * @param string $css
213
     * @param int|bool $linebreak_pos
214
     * @return string
215
     */
216
    private function minify($css, $linebreak_pos)
217
    {
218
        // strings are safe, now wrestle the comments
219
        for ($i = 0, $max = count($this->comments); $i < $max; $i++) {
220
221
            $token = $this->comments[$i];
222
            $placeholder = '/' . self::COMMENT . $i . '___/';
223
224
            // ! in the first position of the comment means preserve
225
            // so push to the preserved tokens keeping the !
226
            if (substr($token, 0, 1) === '!') {
227
                $this->preserved_tokens[] = $token;
228
                $token_tring = self::TOKEN . (count($this->preserved_tokens) - 1) . '___';
229
                $css = preg_replace($placeholder, $token_tring, $css, 1);
230
                // Preserve new lines for /*! important comments
231
                $css = preg_replace('/\s*[\n\r\f]+\s*(\/\*'. $token_tring .')/S', self::NL.'$1', $css);
232
                $css = preg_replace('/('. $token_tring .'\*\/)\s*[\n\r\f]+\s*/', '$1'.self::NL, $css);
233
                continue;
234
            }
235
236
            // \ in the last position looks like hack for Mac/IE5
237
            // shorten that to /*\*/ and the next one to /**/
238
            if (substr($token, (strlen($token) - 1), 1) === '\\') {
239
                $this->preserved_tokens[] = '\\';
240
                $css = preg_replace($placeholder,  self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
241
                $i = $i + 1; // attn: advancing the loop
242
                $this->preserved_tokens[] = '';
243
                $css = preg_replace('/' . self::COMMENT . $i . '___/',  self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
244
                continue;
245
            }
246
247
            // keep empty comments after child selectors (IE7 hack)
248
            // e.g. html >/**/ body
249
            if (strlen($token) === 0) {
250
                $start_index = $this->index_of($css, $this->str_slice($placeholder, 1, -1));
251
                if ($start_index > 2) {
252
                    if (substr($css, $start_index - 3, 1) === '>') {
253
                        $this->preserved_tokens[] = '';
254
                        $css = preg_replace($placeholder,  self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
255
                    }
256
                }
257
            }
258
259
            // in all other cases kill the comment
260
            $css = preg_replace('/\/\*' . $this->str_slice($placeholder, 1, -1) . '\*\//', '', $css, 1);
261
        }
262
263
264
        // Normalize all whitespace strings to single spaces. Easier to work with that way.
265
        $css = preg_replace('/\s+/', ' ', $css);
266
267
		// Fix IE7 issue on matrix filters which browser accept whitespaces between Matrix parameters
268
		$css = preg_replace_callback('/\s*filter\:\s*progid:DXImageTransform\.Microsoft\.Matrix\(([^\)]+)\)/', array($this, 'preserve_old_IE_specific_matrix_definition'), $css);
269
270
        // Shorten & preserve calculations calc(...) since spaces are important
271
        $css = preg_replace_callback('/calc(\(((?:[^\(\)]+|(?1))*)\))/i', array($this, 'replace_calc'), $css);
272
273
        // Replace positive sign from numbers preceded by : or a white-space before the leading space is removed
274
        // +1.2em to 1.2em, +.8px to .8px, +2% to 2%
275
        $css = preg_replace('/((?<!\\\\)\:|\s)\+(\.?\d+)/S', '$1$2', $css);
276
277
        // Remove leading zeros from integer and float numbers preceded by : or a white-space
278
        // 000.6 to .6, -0.8 to -.8, 0050 to 50, -01.05 to -1.05
279
        $css = preg_replace('/((?<!\\\\)\:|\s)(\-?)0+(\.?\d+)/S', '$1$2$3', $css);
280
281
        // Remove trailing zeros from float numbers preceded by : or a white-space
282
        // -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px
283
        $css = preg_replace('/((?<!\\\\)\:|\s)(\-?)(\d?\.\d+?)0+([^\d])/S', '$1$2$3$4', $css);
284
285
        // Remove trailing .0 -> -9.0 to -9
286
        $css = preg_replace('/((?<!\\\\)\:|\s)(\-?\d+)\.0([^\d])/S', '$1$2$3', $css);
287
288
        // Replace 0 length numbers with 0
289
        $css = preg_replace('/((?<!\\\\)\:|\s)\-?\.?0+([^\d])/S', '${1}0$2', $css);
290
291
        // Remove the spaces before the things that should not have spaces before them.
292
        // But, be careful not to turn "p :link {...}" into "p:link{...}"
293
        // Swap out any pseudo-class colons with the token, and then swap back.
294
        $css = preg_replace_callback('/(?:^|\})[^\{]*\s+\:/', array($this, 'replace_colon'), $css);
295
296
        // Remove spaces before the things that should not have spaces before them.
297
        $css = preg_replace('/\s+([\!\{\}\;\:\>\+\(\)\]\~\=,])/', '$1', $css);
298
299
        // Restore spaces for !important
300
        $css = preg_replace('/\!important/i', ' !important', $css);
301
302
        // bring back the colon
303
        $css = preg_replace('/' . self::CLASSCOLON . '/', ':', $css);
304
305
        // retain space for special IE6 cases
306
        $css = preg_replace_callback('/\:first\-(line|letter)(\{|,)/i', array($this, 'lowercase_pseudo_first'), $css);
307
308
        // no space after the end of a preserved comment
309
        $css = preg_replace('/\*\/ /', '*/', $css);
310
311
        // lowercase some popular @directives
312
        $css = preg_replace_callback('/@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/i', array($this, 'lowercase_directives'), $css);
313
314
        // lowercase some more common pseudo-elements
315
        $css = preg_replace_callback('/:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/i', array($this, 'lowercase_pseudo_elements'), $css);
316
317
        // lowercase some more common functions
318
        $css = preg_replace_callback('/:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\(/i', array($this, 'lowercase_common_functions'), $css);
319
320
        // lower case some common function that can be values
321
        // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us
322
        $css = preg_replace_callback('/([:,\( ]\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)/iS', array($this, 'lowercase_common_functions_values'), $css);
323
324
        // Put the space back in some cases, to support stuff like
325
        // @media screen and (-webkit-min-device-pixel-ratio:0){
326
        $css = preg_replace('/\band\(/i', 'and (', $css);
327
328
        // Remove the spaces after the things that should not have spaces after them.
329
        $css = preg_replace('/([\!\{\}\:;\>\+\(\[\~\=,])\s+/S', '$1', $css);
330
331
        // remove unnecessary semicolons
332
        $css = preg_replace('/;+\}/', '}', $css);
333
334
        // Fix for issue: #2528146
335
        // Restore semicolon if the last property is prefixed with a `*` (lte IE7 hack)
336
        // to avoid issues on Symbian S60 3.x browsers.
337
        $css = preg_replace('/(\*[a-z0-9\-]+\s*\:[^;\}]+)(\})/', '$1;$2', $css);
338
339
        // Replace 0 <length> and 0 <percentage> values with 0.
340
        // <length> data type: https://developer.mozilla.org/en-US/docs/Web/CSS/length
341
        // <percentage> data type: https://developer.mozilla.org/en-US/docs/Web/CSS/percentage
342
        $css = preg_replace('/([^\\\\]\:|\s)0(?:em|ex|ch|rem|vw|vh|vm|vmin|cm|mm|in|px|pt|pc|%)/iS', '${1}0', $css);
343
344
		// 0% step in a keyframe? restore the % unit
345
		$css = preg_replace_callback('/(@[a-z\-]*?keyframes[^\{]+\{)(.*?)(\}\})/iS', array($this, 'replace_keyframe_zero'), $css);
346
347
        // Replace 0 0; or 0 0 0; or 0 0 0 0; with 0.
348
        $css = preg_replace('/\:0(?: 0){1,3}(;|\}| \!)/', ':0$1', $css);
349
350
        // Fix for issue: #2528142
351
        // Replace text-shadow:0; with text-shadow:0 0 0;
352
        $css = preg_replace('/(text-shadow\:0)(;|\}| \!)/i', '$1 0 0$2', $css);
353
354
        // Replace background-position:0; with background-position:0 0;
355
        // same for transform-origin
356
        // Changing -webkit-mask-position: 0 0 to just a single 0 will result in the second parameter defaulting to 50% (center)
357
        $css = preg_replace('/(background\-position|webkit-mask-position|(?:webkit|moz|o|ms|)\-?transform\-origin)\:0(;|\}| \!)/iS', '$1:0 0$2', $css);
358
359
        // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space)
360
        // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space)
361
        // This makes it more likely that it'll get further compressed in the next step.
362
        $css = preg_replace_callback('/rgb\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'rgb_to_hex'), $css);
363
        $css = preg_replace_callback('/hsl\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'hsl_to_hex'), $css);
364
365
        // Shorten colors from #AABBCC to #ABC or short color name.
366
        $css = $this->compress_hex_colors($css);
367
368
        // border: none to border:0, outline: none to outline:0
369
        $css = preg_replace('/(border\-?(?:top|right|bottom|left|)|outline)\:none(;|\}| \!)/iS', '$1:0$2', $css);
370
371
        // shorter opacity IE filter
372
        $css = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $css);
373
374
        // Find a fraction that is used for Opera's -o-device-pixel-ratio query
375
        // Add token to add the "\" back in later
376
        $css = preg_replace('/\(([a-z\-]+):([0-9]+)\/([0-9]+)\)/i', '($1:$2'. self::QUERY_FRACTION .'$3)', $css);
377
378
        // Remove empty rules.
379
        $css = preg_replace('/[^\};\{\/]+\{\}/S', '', $css);
380
381
        // Add "/" back to fix Opera -o-device-pixel-ratio query
382
        $css = preg_replace('/'. self::QUERY_FRACTION .'/', '/', $css);
383
384
		// Replace multiple semi-colons in a row by a single one
385
        // See SF bug #1980989
386
        $css = preg_replace('/;;+/', ';', $css);
387
388
        // Restore new lines for /*! important comments
389
        $css = preg_replace('/'. self::NL .'/', "\n", $css);
390
391
        // Lowercase all uppercase properties
392
        $css = preg_replace_callback('/(\{|\;)([A-Z\-]+)(\:)/', array($this, 'lowercase_properties'), $css);
393
394
        // Some source control tools don't like it when files containing lines longer
395
        // than, say 8000 characters, are checked in. The linebreak option is used in
396
        // that case to split long lines after a specific column.
397
        if ($linebreak_pos !== FALSE && (int) $linebreak_pos >= 0) {
398
            $linebreak_pos = (int) $linebreak_pos;
399
            $start_index = $i = 0;
400
            while ($i < strlen($css)) {
401
                $i++;
402
                if ($css[$i - 1] === '}' && $i - $start_index > $linebreak_pos) {
403
                    $css = $this->str_slice($css, 0, $i) . "\n" . $this->str_slice($css, $i);
404
                    $start_index = $i;
405
                }
406
            }
407
        }
408
409
        // restore preserved comments and strings in reverse order
410
        for ($i = count($this->preserved_tokens) - 1; $i >= 0; $i--) {
411
            $css = preg_replace('/' . self::TOKEN . $i . '___/', $this->preserved_tokens[$i], $css, 1);
412
        }
413
414
        // Trim the final string (for any leading or trailing white spaces)
415
        return trim($css);
416
    }
417
418
    /**
419
     * Utility method to replace all data urls with tokens before we start
420
     * compressing, to avoid performance issues running some of the subsequent
421
     * regexes against large strings chunks.
422
     *
423
     * @param string $css
424
     * @return string
425
     */
426
    private function extract_data_urls($css)
427
    {
428
        // Leave data urls alone to increase parse performance.
429
        $max_index = strlen($css) - 1;
430
        $append_index = $index = $last_index = $offset = 0;
431
        $sb = array();
432
        $pattern = '/url\(\s*(["\']?)data\:/i';
433
434
        // Since we need to account for non-base64 data urls, we need to handle
435
        // ' and ) being part of the data string. Hence switching to indexOf,
436
        // to determine whether or not we have matching string terminators and
437
        // handling sb appends directly, instead of using matcher.append* methods.
438
439
        while (preg_match($pattern, $css, $m, 0, $offset)) {
440
            $index = $this->index_of($css, $m[0], $offset);
441
            $last_index = $index + strlen($m[0]);
442
            $start_index = $index + 4; // "url(".length()
443
            $end_index = $last_index - 1;
444
            $terminator = $m[1]; // ', " or empty (not quoted)
445
            $found_terminator = FALSE;
446
447
            if (strlen($terminator) === 0) {
448
                $terminator = ')';
449
            }
450
451
            while ($found_terminator === FALSE && $end_index+1 <= $max_index) {
452
                $end_index = $this->index_of($css, $terminator, $end_index + 1);
453
454
                // endIndex == 0 doesn't really apply here
455
                if ($end_index > 0 && substr($css, $end_index - 1, 1) !== '\\') {
456
                    $found_terminator = TRUE;
457
                    if (')' != $terminator) {
458
                        $end_index = $this->index_of($css, ')', $end_index);
459
                    }
460
                }
461
            }
462
463
            // Enough searching, start moving stuff over to the buffer
464
            $sb[] = $this->str_slice($css, $append_index, $index);
465
466
            if ($found_terminator) {
467
                $token = $this->str_slice($css, $start_index, $end_index);
468
                $token = preg_replace('/\s+/', '', $token);
469
                $this->preserved_tokens[] = $token;
470
471
                $preserver = 'url(' . self::TOKEN . (count($this->preserved_tokens) - 1) . '___)';
472
                $sb[] = $preserver;
473
474
                $append_index = $end_index + 1;
475
            } else {
476
                // No end terminator found, re-add the whole match. Should we throw/warn here?
477
                $sb[] = $this->str_slice($css, $index, $last_index);
478
                $append_index = $last_index;
479
            }
480
481
            $offset = $last_index;
482
        }
483
484
        $sb[] = $this->str_slice($css, $append_index);
485
486
        return implode('', $sb);
487
    }
488
489
    /**
490
     * Utility method to compress hex color values of the form #AABBCC to #ABC or short color name.
491
     *
492
     * DOES NOT compress CSS ID selectors which match the above pattern (which would break things).
493
     * e.g. #AddressForm { ... }
494
     *
495
     * DOES NOT compress IE filters, which have hex color values (which would break things).
496
     * e.g. filter: chroma(color="#FFFFFF");
497
     *
498
     * DOES NOT compress invalid hex values.
499
     * e.g. background-color: #aabbccdd
500
     *
501
     * @param string $css
502
     * @return string
503
     */
504
    private function compress_hex_colors($css)
505
    {
506
        // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters)
507
        $pattern = '/(\=\s*?["\']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/iS';
508
        $_index = $index = $last_index = $offset = 0;
509
        $sb = array();
510
        // See: http://ajaxmin.codeplex.com/wikipage?title=CSS%20Colors
511
        $short_safe = array(
512
            '#808080' => 'gray',
513
            '#008000' => 'green',
514
            '#800000' => 'maroon',
515
            '#000080' => 'navy',
516
            '#808000' => 'olive',
517
            '#ffa500' => 'orange',
518
            '#800080' => 'purple',
519
            '#c0c0c0' => 'silver',
520
            '#008080' => 'teal',
521
            '#f00' => 'red'
522
        );
523
524
        while (preg_match($pattern, $css, $m, 0, $offset)) {
525
            $index = $this->index_of($css, $m[0], $offset);
526
            $last_index = $index + strlen($m[0]);
527
            $is_filter = $m[1] !== null && $m[1] !== '';
528
529
            $sb[] = $this->str_slice($css, $_index, $index);
530
531
            if ($is_filter) {
532
                // Restore, maintain case, otherwise filter will break
533
                $sb[] = $m[1] . '#' . $m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7];
534
            } else {
535
                if (strtolower($m[2]) == strtolower($m[3]) &&
536
                    strtolower($m[4]) == strtolower($m[5]) &&
537
                    strtolower($m[6]) == strtolower($m[7])) {
538
                    // Compress.
539
                    $hex = '#' . strtolower($m[3] . $m[5] . $m[7]);
540
                } else {
541
                    // Non compressible color, restore but lower case.
542
                    $hex = '#' . strtolower($m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]);
543
                }
544
                // replace Hex colors to short safe color names
545
                $sb[] = array_key_exists($hex, $short_safe) ? $short_safe[$hex] : $hex;
546
            }
547
548
            $_index = $offset = $last_index - strlen($m[8]);
549
        }
550
551
        $sb[] = $this->str_slice($css, $_index);
552
553
        return implode('', $sb);
554
    }
555
556
    /* CALLBACKS
557
     * ---------------------------------------------------------------------------------------------
558
     */
559
560
    private function replace_string($matches)
561
    {
562
        $match = $matches[0];
563
        $quote = substr($match, 0, 1);
564
        // Must use addcslashes in PHP to avoid parsing of backslashes
565
        $match = addcslashes($this->str_slice($match, 1, -1), '\\');
566
567
        // maybe the string contains a comment-like substring?
568
        // one, maybe more? put'em back then
569
        if (($pos = $this->index_of($match, self::COMMENT)) >= 0) {
570
            for ($i = 0, $max = count($this->comments); $i < $max; $i++) {
571
                $match = preg_replace('/' . self::COMMENT . $i . '___/', $this->comments[$i], $match, 1);
572
            }
573
        }
574
575
        // minify alpha opacity in filter strings
576
        $match = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $match);
577
578
        $this->preserved_tokens[] = $match;
579
        return $quote . self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . $quote;
580
    }
581
582
    private function replace_colon($matches)
583
    {
584
        return preg_replace('/\:/', self::CLASSCOLON, $matches[0]);
585
    }
586
587
    private function replace_calc($matches)
588
    {
589
        $this->preserved_tokens[] = trim(preg_replace('/\s*([\*\/\(\),])\s*/', '$1', $matches[2]));
590
        return 'calc('. self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . ')';
591
    }
592
593
	private function preserve_old_IE_specific_matrix_definition($matches)
594
	{
595
		$this->preserved_tokens[] = $matches[1];
596
		return 'filter:progid:DXImageTransform.Microsoft.Matrix(' . self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . ')';
597
    }
598
599
	private function replace_keyframe_zero($matches)
600
    {
601
        return $matches[1] . preg_replace('/0(\{|,[^\)\{]+\{)/', '0%$1', $matches[2]) . $matches[3];
602
    }
603
604
    private function rgb_to_hex($matches)
605
    {
606
        // Support for percentage values rgb(100%, 0%, 45%);
607
        if ($this->index_of($matches[1], '%') >= 0){
608
            $rgbcolors = explode(',', str_replace('%', '', $matches[1]));
609
            for ($i = 0; $i < count($rgbcolors); $i++) {
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...
610
                $rgbcolors[$i] = $this->round_number(floatval($rgbcolors[$i]) * 2.55);
611
            }
612
        } else {
613
            $rgbcolors = explode(',', $matches[1]);
614
        }
615
616
        // Values outside the sRGB color space should be clipped (0-255)
617
        for ($i = 0; $i < count($rgbcolors); $i++) {
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...
618
            $rgbcolors[$i] = $this->clamp_number(intval($rgbcolors[$i], 10), 0, 255);
619
            $rgbcolors[$i] = sprintf("%02x", $rgbcolors[$i]);
620
        }
621
622
        // Fix for issue #2528093
623
        if (!preg_match('/[\s\,\);\}]/', $matches[2])){
624
            $matches[2] = ' ' . $matches[2];
625
        }
626
627
        return '#' . implode('', $rgbcolors) . $matches[2];
628
    }
629
630
    private function hsl_to_hex($matches)
631
    {
632
        $values = explode(',', str_replace('%', '', $matches[1]));
633
        $h = floatval($values[0]);
634
        $s = floatval($values[1]);
635
        $l = floatval($values[2]);
636
637
        // Wrap and clamp, then fraction!
638
        $h = ((($h % 360) + 360) % 360) / 360;
639
        $s = $this->clamp_number($s, 0, 100) / 100;
640
        $l = $this->clamp_number($l, 0, 100) / 100;
641
642
        if ($s == 0) {
643
            $r = $g = $b = $this->round_number(255 * $l);
644
        } else {
645
            $v2 = $l < 0.5 ? $l * (1 + $s) : ($l + $s) - ($s * $l);
646
            $v1 = (2 * $l) - $v2;
647
            $r = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h + (1/3)));
648
            $g = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h));
649
            $b = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h - (1/3)));
650
        }
651
652
        return $this->rgb_to_hex(array('', $r.','.$g.','.$b, $matches[2]));
653
    }
654
655
    private function lowercase_pseudo_first($matches)
656
    {
657
        return ':first-'. strtolower($matches[1]) .' '. $matches[2];
658
    }
659
660
    private function lowercase_directives($matches)
661
    {
662
        return '@'. strtolower($matches[1]);
663
    }
664
665
    private function lowercase_pseudo_elements($matches)
666
    {
667
        return ':'. strtolower($matches[1]);
668
    }
669
670
    private function lowercase_common_functions($matches)
671
    {
672
        return ':'. strtolower($matches[1]) .'(';
673
    }
674
675
    private function lowercase_common_functions_values($matches)
676
    {
677
        return $matches[1] . strtolower($matches[2]);
678
    }
679
680
    private function lowercase_properties($matches)
681
    {
682
        return $matches[1].strtolower($matches[2]).$matches[3];
683
    }
684
685
    /* HELPERS
686
     * ---------------------------------------------------------------------------------------------
687
     */
688
689
    private function hue_to_rgb($v1, $v2, $vh)
690
    {
691
        $vh = $vh < 0 ? $vh + 1 : ($vh > 1 ? $vh - 1 : $vh);
692 View Code Duplication
        if ($vh * 6 < 1) return $v1 + ($v2 - $v1) * 6 * $vh;
693
        if ($vh * 2 < 1) return $v2;
694 View Code Duplication
        if ($vh * 3 < 2) return $v1 + ($v2 - $v1) * ((2/3) - $vh) * 6;
695
        return $v1;
696
    }
697
698
    private function round_number($n)
699
    {
700
        return intval(floor(floatval($n) + 0.5), 10);
701
    }
702
703
    private function clamp_number($n, $min, $max)
704
    {
705
        return min(max($n, $min), $max);
706
    }
707
708
    /**
709
     * PHP port of Javascript's "indexOf" function for strings only
710
     * Author: Tubal Martin http://blog.margenn.com
711
     *
712
     * @param string $haystack
713
     * @param string $needle
714
     * @param int    $offset index (optional)
715
     * @return int
716
     */
717
    private function index_of($haystack, $needle, $offset = 0)
718
    {
719
        $index = strpos($haystack, $needle, $offset);
720
721
        return ($index !== FALSE) ? $index : -1;
722
    }
723
724
    /**
725
     * PHP port of Javascript's "slice" function for strings only
726
     * Author: Tubal Martin http://blog.margenn.com
727
     * Tests: http://margenn.com/tubal/str_slice/
728
     *
729
     * @param string   $str
730
     * @param int      $start index
731
     * @param int|bool $end index (optional)
732
     * @return string
733
     */
734
    private function str_slice($str, $start = 0, $end = FALSE)
735
    {
736
        if ($end !== FALSE && ($start < 0 || $end <= 0)) {
737
            $max = strlen($str);
738
739
            if ($start < 0) {
740
                if (($start = $max + $start) < 0) {
741
                    return '';
742
                }
743
            }
744
745
            if ($end < 0) {
746
                if (($end = $max + $end) < 0) {
747
                    return '';
748
                }
749
            }
750
751
            if ($end <= $start) {
752
                return '';
753
            }
754
        }
755
756
        $slice = ($end === FALSE) ? substr($str, $start) : substr($str, $start, $end - $start);
757
        return ($slice === FALSE) ? '' : $slice;
758
    }
759
760
    /**
761
     * Convert strings like "64M" or "30" to int values
762
     * @param mixed $size
763
     * @return int
764
     */
765
    private function normalize_int($size)
766
    {
767
        if (is_string($size)) {
768
            switch (substr($size, -1)) {
769
                case 'M': case 'm': return (int) $size * 1048576;
770
                case 'K': case 'k': return (int) $size * 1024;
771
                case 'G': case 'g': return (int) $size * 1073741824;
772
            }
773
        }
774
775
        return (int) $size;
776
    }
777
}
778