Completed
Push — master ( 9a01d9...c990d6 )
by frank
08:32
created

external/php/yui-php-cssmin-2.4.8-p10/cssmin.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/*!
4
 * cssmin.php
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 CLASSCOLON = '___YUICSSMIN_PSEUDOCLASSCOLON___';
27
    const QUERY_FRACTION = '___YUICSSMIN_QUERY_FRACTION___';
28
29
    const TOKEN = '___YUICSSMIN_PRESERVED_TOKEN_';
30
    const COMMENT = '___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_';
31
    const AT_RULE_BLOCK = '___YUICSSMIN_PRESERVE_AT_RULE_BLOCK_';
32
33
    private $comments;
34
    private $atRuleBlocks;
35
    private $preservedTokens;
36
    private $chunkLength = 5000;
37
    private $minChunkLength = 100;
38
    private $memoryLimit;
39
    private $maxExecutionTime = 60; // 1 min
40
    private $pcreBacktrackLimit;
41
    private $pcreRecursionLimit;
42
    private $raisePhpLimits;
43
44
    private $unitsGroupRegex = '(?:ch|cm|em|ex|gd|in|mm|px|pt|pc|q|rem|vh|vmax|vmin|vw|%)';
45
    private $numRegex;
46
47
    /**
48
     * @param bool|int $raisePhpLimits If true, PHP settings will be raised if needed
49
     */
50
    public function __construct($raisePhpLimits = true)
51
    {
52
        $this->memoryLimit = 128 * 1048576; // 128MB in bytes
53
        $this->pcreBacktrackLimit = 1000 * 1000;
54
        $this->pcreRecursionLimit = 500 * 1000;
55
56
        $this->raisePhpLimits = (bool) $raisePhpLimits;
57
58
        $this->numRegex = '(?:\+|-)?\d*\.?\d+' . $this->unitsGroupRegex .'?';
59
    }
60
61
    /**
62
     * Minifies a string of CSS
63
     * @param string $css
64
     * @param int|bool $linebreakPos
65
     * @return string
66
     */
67
    public function run($css = '', $linebreakPos = false)
68
    {
69
        if (empty($css)) {
70
            return '';
71
        }
72
73
        if ($this->raisePhpLimits) {
74
            $this->doRaisePhpLimits();
75
        }
76
77
        $this->comments = array();
78
        $this->atRuleBlocks = array();
79
        $this->preservedTokens = array();
80
81
        // process data urls
82
        $css = $this->processDataUrls($css);
83
84
        // process comments
85
        $css = preg_replace_callback('/(?<!\\\\)\/\*(.*?)\*(?<!\\\\)\//Ss', array($this, 'processComments'), $css);
86
87
        // process strings so their content doesn't get accidentally minified
88
        $css = preg_replace_callback(
89
            '/(?:"(?:[^\\\\"]|\\\\.|\\\\)*")|'."(?:'(?:[^\\\\']|\\\\.|\\\\)*')/S",
90
            array($this, 'processStrings'),
91
            $css
92
        );
93
94
        // Safe chunking: process at rule blocks so after chunking nothing gets stripped out
95
        $css = preg_replace_callback(
96
            '/@(?:document|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframes|media|supports).+?\}\s*\}/si',
97
            array($this, 'processAtRuleBlocks'),
98
            $css
99
        );
100
101
        // Let's divide css code in chunks of {$this->chunkLength} chars aprox.
102
        // Reason: PHP's PCRE functions like preg_replace have a "backtrack limit"
103
        // of 100.000 chars by default (php < 5.3.7) so if we're dealing with really
104
        // long strings and a (sub)pattern matches a number of chars greater than
105
        // the backtrack limit number (i.e. /(.*)/s) PCRE functions may fail silently
106
        // returning NULL and $css would be empty.
107
        $charset = '';
108
        $charsetRegexp = '/(@charset)( [^;]+;)/i';
109
        $cssChunks = array();
110
        $l = strlen($css);
111
112
        // if the number of characters is <= {$this->chunkLength}, do not chunk
113
        if ($l <= $this->chunkLength) {
114
            $cssChunks[] = $css;
115
        } else {
116
            // chunk css code securely
117
            for ($startIndex = 0, $i = $this->chunkLength; $i < $l; $i++) {
118
                if ($css[$i - 1] === '}' && $i - $startIndex >= $this->chunkLength) {
119
                    $cssChunks[] = $this->strSlice($css, $startIndex, $i);
120
                    $startIndex = $i;
121
                    // Move forward saving iterations when possible!
122
                    if ($startIndex + $this->chunkLength < $l) {
123
                        $i += $this->chunkLength;
124
                    }
125
                }
126
            }
127
128
            // Final chunk
129
            $cssChunks[] = $this->strSlice($css, $startIndex);
130
        }
131
132
        // Minify each chunk
133 View Code Duplication
        for ($i = 0, $n = count($cssChunks); $i < $n; $i++) {
134
            $cssChunks[$i] = $this->minify($cssChunks[$i], $linebreakPos);
135
            // Keep the first @charset at-rule found
136
            if (empty($charset) && preg_match($charsetRegexp, $cssChunks[$i], $matches)) {
137
                $charset = strtolower($matches[1]) . $matches[2];
138
            }
139
            // Delete all @charset at-rules
140
            $cssChunks[$i] = preg_replace($charsetRegexp, '', $cssChunks[$i]);
141
        }
142
143
        // Update the first chunk and push the charset to the top of the file.
144
        $cssChunks[0] = $charset . $cssChunks[0];
145
146
        return trim(implode('', $cssChunks));
147
    }
148
149
    /**
150
     * Sets the approximate number of characters to use when splitting a string in chunks.
151
     * @param int $length
152
     */
153
    public function set_chunk_length($length)
154
    {
155
        $length = (int) $length;
156
        $this->chunkLength = $length < $this->minChunkLength ? $this->minChunkLength : $length;
157
    }
158
159
    /**
160
     * Sets the memory limit for this script
161
     * @param int|string $limit
162
     */
163
    public function set_memory_limit($limit)
164
    {
165
        $this->memoryLimit = $this->normalizeInt($limit);
166
    }
167
168
    /**
169
     * Sets the maximum execution time for this script
170
     * @param int|string $seconds
171
     */
172
    public function set_max_execution_time($seconds)
173
    {
174
        $this->maxExecutionTime = (int) $seconds;
175
    }
176
177
    /**
178
     * Sets the PCRE backtrack limit for this script
179
     * @param int $limit
180
     */
181
    public function set_pcre_backtrack_limit($limit)
182
    {
183
        $this->pcreBacktrackLimit = (int) $limit;
184
    }
185
186
    /**
187
     * Sets the PCRE recursion limit for this script
188
     * @param int $limit
189
     */
190
    public function set_pcre_recursion_limit($limit)
191
    {
192
        $this->pcreRecursionLimit = (int) $limit;
193
    }
194
195
    /**
196
     * Tries to configure PHP to use at least the suggested minimum settings
197
     * @return void
198
     */
199
    private function doRaisePhpLimits()
200
    {
201
        $phpLimits = array(
202
            'memory_limit' => $this->memoryLimit,
203
            'max_execution_time' => $this->maxExecutionTime,
204
            'pcre.backtrack_limit' => $this->pcreBacktrackLimit,
205
            'pcre.recursion_limit' =>  $this->pcreRecursionLimit
206
        );
207
208
        // If current settings are higher respect them.
209
        foreach ($phpLimits as $name => $suggested) {
210
            $current = $this->normalizeInt(ini_get($name));
211
212
            if ($current > $suggested) {
213
                continue;
214
            }
215
216
            // memoryLimit exception: allow -1 for "no memory limit".
217
            if ($name === 'memory_limit' && $current === -1) {
218
                continue;
219
            }
220
221
            // maxExecutionTime exception: allow 0 for "no memory limit".
222
            if ($name === 'max_execution_time' && $current === 0) {
223
                continue;
224
            }
225
226
            ini_set($name, $suggested);
227
        }
228
    }
229
230
    /**
231
     * Registers a preserved token
232
     * @param $token
233
     * @return string The token ID string
234
     */
235
    private function registerPreservedToken($token)
236
    {
237
        $this->preservedTokens[] = $token;
238
        return self::TOKEN . (count($this->preservedTokens) - 1) .'___';
239
    }
240
241
    /**
242
     * Gets the regular expression to match the specified token ID string
243
     * @param $id
244
     * @return string
245
     */
246
    private function getPreservedTokenPlaceholderRegexById($id)
247
    {
248
        return '/'. self::TOKEN . $id .'___/';
249
    }
250
251
    /**
252
     * Registers a candidate comment token
253
     * @param $comment
254
     * @return string The comment token ID string
255
     */
256
    private function registerComment($comment)
257
    {
258
        $this->comments[] = $comment;
259
        return '/*'. self::COMMENT . (count($this->comments) - 1) .'___*/';
260
    }
261
262
    /**
263
     * Gets the candidate comment token ID string for the specified comment token ID
264
     * @param $id
265
     * @return string
266
     */
267
    private function getCommentPlaceholderById($id)
268
    {
269
        return self::COMMENT . $id .'___';
270
    }
271
272
    /**
273
     * Gets the regular expression to match the specified comment token ID string
274
     * @param $id
275
     * @return string
276
     */
277
    private function getCommentPlaceholderRegexById($id)
278
    {
279
        return '/'. $this->getCommentPlaceholderById($id) .'/';
280
    }
281
282
    /**
283
     * Registers an at rule block token
284
     * @param $block
285
     * @return string The comment token ID string
286
     */
287
    private function registerAtRuleBlock($block)
288
    {
289
        $this->atRuleBlocks[] = $block;
290
        return self::AT_RULE_BLOCK . (count($this->atRuleBlocks) - 1) .'___';
291
    }
292
293
    /**
294
     * Gets the regular expression to match the specified at rule block token ID string
295
     * @param $id
296
     * @return string
297
     */
298
    private function getAtRuleBlockPlaceholderRegexById($id)
299
    {
300
        return '/'. self::AT_RULE_BLOCK . $id .'___/';
301
    }
302
303
    /**
304
     * Minifies the given input CSS string
305
     * @param string $css
306
     * @param int|bool $linebreakPos
307
     * @return string
308
     */
309
    private function minify($css, $linebreakPos)
310
    {
311
        // Restore preserved at rule blocks
312
        for ($i = 0, $max = count($this->atRuleBlocks); $i < $max; $i++) {
313
            $css = preg_replace(
314
                $this->getAtRuleBlockPlaceholderRegexById($i),
315
                $this->escapeReplacementString($this->atRuleBlocks[$i]),
316
                $css,
317
                1
318
            );
319
        }
320
321
        // strings are safe, now wrestle the comments
322
        for ($i = 0, $max = count($this->comments); $i < $max; $i++) {
323
            $comment = $this->comments[$i];
324
            $commentPlaceholder = $this->getCommentPlaceholderById($i);
325
            $commentPlaceholderRegex = $this->getCommentPlaceholderRegexById($i);
326
327
            // ! in the first position of the comment means preserve
328
            // so push to the preserved tokens keeping the !
329
            if (preg_match('/^!/', $comment)) {
330
                $preservedTokenPlaceholder = $this->registerPreservedToken($comment);
331
                $css = preg_replace($commentPlaceholderRegex, $preservedTokenPlaceholder, $css, 1);
332
                // Preserve new lines for /*! important comments
333
                $css = preg_replace('/\R+\s*(\/\*'. $preservedTokenPlaceholder .')/', self::NL.'$1', $css);
334
                $css = preg_replace('/('. $preservedTokenPlaceholder .'\*\/)\s*\R+/', '$1'.self::NL, $css);
335
                continue;
336
            }
337
338
            // \ in the last position looks like hack for Mac/IE5
339
            // shorten that to /*\*/ and the next one to /**/
340
            if (preg_match('/\\\\$/', $comment)) {
341
                $preservedTokenPlaceholder = $this->registerPreservedToken('\\');
342
                $css = preg_replace($commentPlaceholderRegex, $preservedTokenPlaceholder, $css, 1);
343
                $i = $i + 1; // attn: advancing the loop
344
                $preservedTokenPlaceholder = $this->registerPreservedToken('');
345
                $css = preg_replace($this->getCommentPlaceholderRegexById($i), $preservedTokenPlaceholder, $css, 1);
346
                continue;
347
            }
348
349
            // keep empty comments after child selectors (IE7 hack)
350
            // e.g. html >/**/ body
351
            if (strlen($comment) === 0) {
352
                $startIndex = $this->indexOf($css, $commentPlaceholder);
353
                if ($startIndex > 2) {
354
                    if (substr($css, $startIndex - 3, 1) === '>') {
355
                        $preservedTokenPlaceholder = $this->registerPreservedToken('');
356
                        $css = preg_replace($commentPlaceholderRegex, $preservedTokenPlaceholder, $css, 1);
357
                        continue;
358
                    }
359
                }
360
            }
361
362
            // in all other cases kill the comment
363
            $css = preg_replace('/\/\*' . $commentPlaceholder . '\*\//', '', $css, 1);
364
        }
365
366
        // Normalize all whitespace strings to single spaces. Easier to work with that way.
367
        $css = preg_replace('/\s+/', ' ', $css);
368
369
        // Remove spaces before & after newlines
370
        $css = preg_replace('/\s*'. self::NL .'\s*/', self::NL, $css);
371
372
        // Fix IE7 issue on matrix filters which browser accept whitespaces between Matrix parameters
373
        $css = preg_replace_callback(
374
            '/\s*filter:\s*progid:DXImageTransform\.Microsoft\.Matrix\(([^)]+)\)/',
375
            array($this, 'processOldIeSpecificMatrixDefinition'),
376
            $css
377
        );
378
379
        // Shorten & preserve calculations calc(...) since spaces are important
380
        $css = preg_replace_callback('/calc(\(((?:[^()]+|(?1))*)\))/i', array($this, 'processCalc'), $css);
381
382
        // Replace positive sign from numbers preceded by : or a white-space before the leading space is removed
383
        // +1.2em to 1.2em, +.8px to .8px, +2% to 2%
384
        $css = preg_replace('/((?<!\\\\):|\s)\+(\.?\d+)/S', '$1$2', $css);
385
386
        // Remove leading zeros from integer and float numbers preceded by : or a white-space
387
        // 000.6 to .6, -0.8 to -.8, 0050 to 50, -01.05 to -1.05
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
388
        $css = preg_replace('/((?<!\\\\):|\s)(-?)0+(\.?\d+)/S', '$1$2$3', $css);
389
390
        // Remove trailing zeros from float numbers preceded by : or a white-space
391
        // -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px
392
        $css = preg_replace('/((?<!\\\\):|\s)(-?)(\d?\.\d+?)0+([^\d])/S', '$1$2$3$4', $css);
393
394
        // Remove trailing .0 -> -9.0 to -9
395
        $css = preg_replace('/((?<!\\\\):|\s)(-?\d+)\.0([^\d])/S', '$1$2$3', $css);
396
397
        // Replace 0 length numbers with 0
398
        $css = preg_replace('/((?<!\\\\):|\s)-?\.?0+([^\d])/S', '${1}0$2', $css);
399
400
        // Remove the spaces before the things that should not have spaces before them.
401
        // But, be careful not to turn "p :link {...}" into "p:link{...}"
402
        // Swap out any pseudo-class colons with the token, and then swap back.
403
        $css = preg_replace_callback('/(?:^|\})[^{]*\s+:/', array($this, 'processColon'), $css);
404
405
        // Remove spaces before the things that should not have spaces before them.
406
        $css = preg_replace('/\s+([!{};:>+()\]~=,])/', '$1', $css);
407
408
        // Restore spaces for !important
409
        $css = preg_replace('/!important/i', ' !important', $css);
410
411
        // bring back the colon
412
        $css = preg_replace('/'. self::CLASSCOLON .'/', ':', $css);
413
414
        // retain space for special IE6 cases
415
        $css = preg_replace_callback('/:first-(line|letter)(\{|,)/i', array($this, 'lowercasePseudoFirst'), $css);
416
417
        // no space after the end of a preserved comment
418
        $css = preg_replace('/\*\/ /', '*/', $css);
419
420
        // lowercase some popular @directives
421
        $css = preg_replace_callback(
422
            '/@(document|font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframes|media|namespace|page|' .
423
            'supports|viewport)/i',
424
            array($this, 'lowercaseDirectives'),
425
            $css
426
        );
427
428
        // lowercase some more common pseudo-elements
429
        $css = preg_replace_callback(
430
            '/:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|' .
431
            'last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/i',
432
            array($this, 'lowercasePseudoElements'),
433
            $css
434
        );
435
436
        // lowercase some more common functions
437
        $css = preg_replace_callback(
438
            '/:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\(/i',
439
            array($this, 'lowercaseCommonFunctions'),
440
            $css
441
        );
442
443
        // lower case some common function that can be values
444
        // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us
445
        $css = preg_replace_callback(
446
            '/([:,( ]\s*)(attr|color-stop|from|rgba|to|url|-webkit-gradient|' .
447
            '(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient))/iS',
448
            array($this, 'lowercaseCommonFunctionsValues'),
449
            $css
450
        );
451
452
        // Put the space back in some cases, to support stuff like
453
        // @media screen and (-webkit-min-device-pixel-ratio:0){
454
        $css = preg_replace_callback('/(\s|\)\s)(and|not|or)\(/i', array($this, 'processAtRulesOperators'), $css);
455
456
        // Remove the spaces after the things that should not have spaces after them.
457
        $css = preg_replace('/([!{}:;>+(\[~=,])\s+/S', '$1', $css);
458
459
        // remove unnecessary semicolons
460
        $css = preg_replace('/;+\}/', '}', $css);
461
462
        // Fix for issue: #2528146
463
        // Restore semicolon if the last property is prefixed with a `*` (lte IE7 hack)
464
        // to avoid issues on Symbian S60 3.x browsers.
465
        $css = preg_replace('/(\*[a-z0-9\-]+\s*:[^;}]+)(\})/', '$1;$2', $css);
466
467
        // Shorten zero values for safe properties only
468
        $css = $this->shortenZeroValues($css);
469
470
        // Shorten font-weight values
471
        $css = preg_replace('/(font-weight:)bold\b/i', '${1}700', $css);
472
        $css = preg_replace('/(font-weight:)normal\b/i', '${1}400', $css);
473
474
        // Shorten suitable shorthand properties with repeated non-zero values
475
        $css = preg_replace(
476
            '/(margin|padding):('.$this->numRegex.') ('.$this->numRegex.') (?:\2) (?:\3)(;|\}| !)/i',
477
            '$1:$2 $3$4',
478
            $css
479
        );
480
        $css = preg_replace(
481
            '/(margin|padding):('.$this->numRegex.') ('.$this->numRegex.') ('.$this->numRegex.') (?:\3)(;|\}| !)/i',
482
            '$1:$2 $3 $4$5',
483
            $css
484
        );
485
486
        // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space)
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
487
        // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space)
488
        // This makes it more likely that it'll get further compressed in the next step.
489
        $css = preg_replace_callback('/rgb\s*\(\s*([0-9,\s\-.%]+)\s*\)(.{1})/i', array($this, 'rgbToHex'), $css);
490
        $css = preg_replace_callback('/hsl\s*\(\s*([0-9,\s\-.%]+)\s*\)(.{1})/i', array($this, 'hslToHex'), $css);
491
492
        // Shorten colors from #AABBCC to #ABC or shorter color name.
493
        $css = $this->shortenHexColors($css);
494
495
        // Shorten long named colors: white -> #fff.
496
        $css = $this->shortenNamedColors($css);
497
498
        // shorter opacity IE filter
499
        $css = preg_replace('/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/i', 'alpha(opacity=', $css);
500
501
        // Find a fraction that is used for Opera's -o-device-pixel-ratio query
502
        // Add token to add the "\" back in later
503
        $css = preg_replace('/\(([a-z\-]+):([0-9]+)\/([0-9]+)\)/i', '($1:$2'. self::QUERY_FRACTION .'$3)', $css);
504
505
        // Patch new lines to avoid being removed when followed by empty rules cases
506
        $css = preg_replace('/'. self::NL .'/', self::NL .'}', $css);
507
508
        // Remove empty rules.
509
        $css = preg_replace('/[^{};\/]+\{\}/S', '', $css);
510
511
        // Restore new lines for /*! important comments
512
        $css = preg_replace('/'. self::NL .'}/', "\n", $css);
513
514
        // Add "/" back to fix Opera -o-device-pixel-ratio query
515
        $css = preg_replace('/'. self::QUERY_FRACTION .'/', '/', $css);
516
517
        // Replace multiple semi-colons in a row by a single one
518
        // See SF bug #1980989
519
        $css = preg_replace('/;;+/', ';', $css);
520
521
        // Lowercase all uppercase properties
522
        $css = preg_replace_callback('/(\{|;)([A-Z\-]+)(:)/', array($this, 'lowercaseProperties'), $css);
523
524
        // Some source control tools don't like it when files containing lines longer
525
        // than, say 8000 characters, are checked in. The linebreak option is used in
526
        // that case to split long lines after a specific column.
527
        if ($linebreakPos !== false && (int) $linebreakPos >= 0) {
528
            $linebreakPos = (int) $linebreakPos;
529
            for ($startIndex = $i = 1, $l = strlen($css); $i < $l; $i++) {
530
                if ($css[$i - 1] === '}' && $i - $startIndex > $linebreakPos) {
531
                    $css = $this->strSlice($css, 0, $i) . "\n" . $this->strSlice($css, $i);
532
                    $l = strlen($css);
533
                    $startIndex = $i;
534
                }
535
            }
536
        }
537
538
        // restore preserved comments and strings in reverse order
539
        for ($i = count($this->preservedTokens) - 1; $i >= 0; $i--) {
540
            $css = preg_replace(
541
                $this->getPreservedTokenPlaceholderRegexById($i),
542
                $this->escapeReplacementString($this->preservedTokens[$i]),
543
                $css,
544
                1
545
            );
546
        }
547
548
        // Trim the final string for any leading or trailing white space but respect newlines!
549
        $css = preg_replace('/(^ | $)/', '', $css);
550
551
        return $css;
552
    }
553
554
    /**
555
     * Searches & replaces all data urls with tokens before we start compressing,
556
     * to avoid performance issues running some of the subsequent regexes against large string chunks.
557
     * @param string $css
558
     * @return string
559
     */
560
    private function processDataUrls($css)
561
    {
562
        // Leave data urls alone to increase parse performance.
563
        $maxIndex = strlen($css) - 1;
564
        $appenIndex = $index = $lastIndex = $offset = 0;
565
        $sb = array();
566
        $pattern = '/url\(\s*(["\']?)data:/i';
567
568
        // Since we need to account for non-base64 data urls, we need to handle
569
        // ' and ) being part of the data string. Hence switching to indexOf,
570
        // to determine whether or not we have matching string terminators and
571
        // handling sb appends directly, instead of using matcher.append* methods.
572
        while (preg_match($pattern, $css, $m, 0, $offset)) {
573
            $index = $this->indexOf($css, $m[0], $offset);
574
            $lastIndex = $index + strlen($m[0]);
575
            $startIndex = $index + 4; // "url(".length()
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
576
            $endIndex = $lastIndex - 1;
577
            $terminator = $m[1]; // ', " or empty (not quoted)
578
            $terminatorFound = false;
579
580
            if (strlen($terminator) === 0) {
581
                $terminator = ')';
582
            }
583
584
            while ($terminatorFound === false && $endIndex+1 <= $maxIndex) {
585
                $endIndex = $this->indexOf($css, $terminator, $endIndex + 1);
586
                // endIndex == 0 doesn't really apply here
587
                if ($endIndex > 0 && substr($css, $endIndex - 1, 1) !== '\\') {
588
                    $terminatorFound = true;
589
                    if (')' !== $terminator) {
590
                        $endIndex = $this->indexOf($css, ')', $endIndex);
591
                    }
592
                }
593
            }
594
595
            // Enough searching, start moving stuff over to the buffer
596
            $sb[] = $this->strSlice($css, $appenIndex, $index);
597
598
            if ($terminatorFound) {
599
                $token = $this->strSlice($css, $startIndex, $endIndex);
600
                // Remove all spaces only for base64 encoded URLs.
601
                $token = preg_replace_callback(
602
                    '/.+base64,.+/s',
603
                    array($this, 'removeSpacesFromDataUrls'),
604
                    trim($token)
605
                );
606
                $preservedTokenPlaceholder = $this->registerPreservedToken($token);
607
                $sb[] = 'url('. $preservedTokenPlaceholder .')';
608
                $appenIndex = $endIndex + 1;
609
            } else {
610
                // No end terminator found, re-add the whole match. Should we throw/warn here?
611
                $sb[] = $this->strSlice($css, $index, $lastIndex);
612
                $appenIndex = $lastIndex;
613
            }
614
615
            $offset = $lastIndex;
616
        }
617
618
        $sb[] = $this->strSlice($css, $appenIndex);
619
620
        return implode('', $sb);
621
    }
622
623
    /**
624
     * Shortens all zero values for a set of safe properties
625
     * e.g. padding: 0px 1px; -> padding:0 1px
626
     * e.g. padding: 0px 0rem 0em 0.0pc; -> padding:0
627
     * @param string $css
628
     * @return string
629
     */
630
    private function shortenZeroValues($css)
631
    {
632
        $unitsGroupReg = $this->unitsGroupRegex;
633
        $numOrPosReg = '('. $this->numRegex .'|top|left|bottom|right|center)';
634
        $oneZeroSafeProperties = array(
635
            '(?:line-)?height',
636
            '(?:(?:min|max)-)?width',
637
            'top',
638
            'left',
639
            'background-position',
640
            'bottom',
641
            'right',
642
            'border(?:-(?:top|left|bottom|right))?(?:-width)?',
643
            'border-(?:(?:top|bottom)-(?:left|right)-)?radius',
644
            'column-(?:gap|width)',
645
            'margin(?:-(?:top|left|bottom|right))?',
646
            'outline-width',
647
            'padding(?:-(?:top|left|bottom|right))?'
648
        );
649
        $nZeroSafeProperties = array(
650
            'margin',
651
            'padding',
652
            'background-position'
653
        );
654
655
        $regStart = '/(;|\{)';
656
        $regEnd = '/i';
657
658
        // First zero regex start
659
        $oneZeroRegStart = $regStart .'('. implode('|', $oneZeroSafeProperties) .'):';
660
661
        // Multiple zeros regex start
662
        $nZerosRegStart = $regStart .'('. implode('|', $nZeroSafeProperties) .'):';
663
664
        $css = preg_replace(
665
            array(
666
                $oneZeroRegStart .'0'. $unitsGroupReg . $regEnd,
667
                $nZerosRegStart . $numOrPosReg .' 0'. $unitsGroupReg . $regEnd,
668
                $nZerosRegStart . $numOrPosReg .' '. $numOrPosReg .' 0'. $unitsGroupReg . $regEnd,
669
                $nZerosRegStart . $numOrPosReg .' '. $numOrPosReg .' '. $numOrPosReg .' 0'. $unitsGroupReg . $regEnd
670
            ),
671
            array(
672
                '$1$2:0',
673
                '$1$2:$3 0',
674
                '$1$2:$3 $4 0',
675
                '$1$2:$3 $4 $5 0'
676
            ),
677
            $css
678
        );
679
680
        // Remove background-position
681
        array_pop($nZeroSafeProperties);
682
683
        // Replace 0 0; or 0 0 0; or 0 0 0 0; with 0 for safe properties only.
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
684
        $css = preg_replace(
685
            '/('. implode('|', $nZeroSafeProperties) .'):0(?: 0){1,3}(;|\}| !)'. $regEnd,
686
            '$1:0$2',
687
            $css
688
        );
689
690
        // Replace 0 0 0; or 0 0 0 0; with 0 0 for background-position property.
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
691
        $css = preg_replace('/(background-position):0(?: 0){2,3}(;|\}| !)'. $regEnd, '$1:0 0$2', $css);
692
693
        return $css;
694
    }
695
696
    /**
697
     * Shortens all named colors with a shorter HEX counterpart for a set of safe properties
698
     * e.g. white -> #fff
699
     * @param string $css
700
     * @return string
701
     */
702
    private function shortenNamedColors($css)
703
    {
704
        $patterns = array();
705
        $replacements = array();
706
        $longNamedColors = include 'data/named-to-hex-color-map.php';
707
        $propertiesWithColors = array(
708
            'color',
709
            'background(?:-color)?',
710
            'border(?:-(?:top|right|bottom|left|color)(?:-color)?)?',
711
            'outline(?:-color)?',
712
            '(?:text|box)-shadow'
713
        );
714
715
        $regStart = '/(;|\{)('. implode('|', $propertiesWithColors) .'):([^;}]*)\b';
716
        $regEnd = '\b/iS';
717
718
        foreach ($longNamedColors as $colorName => $colorCode) {
719
            $patterns[] = $regStart . $colorName . $regEnd;
720
            $replacements[] = '$1$2:$3'. $colorCode;
721
        }
722
723
        // Run at least 4 times to cover most cases (same color used several times for the same property)
724
        for ($i = 0; $i < 4; $i++) {
725
            $css = preg_replace($patterns, $replacements, $css);
726
        }
727
728
        return $css;
729
    }
730
731
    /**
732
     * Compresses HEX color values of the form #AABBCC to #ABC or short color name.
733
     *
734
     * DOES NOT compress CSS ID selectors which match the above pattern (which would break things).
735
     * e.g. #AddressForm { ... }
736
     *
737
     * DOES NOT compress IE filters, which have hex color values (which would break things).
738
     * e.g. filter: chroma(color="#FFFFFF");
739
     *
740
     * DOES NOT compress invalid hex values.
741
     * e.g. background-color: #aabbccdd
742
     *
743
     * @param string $css
744
     * @return string
745
     */
746
    private function shortenHexColors($css)
747
    {
748
        // Look for hex colors inside { ... } (to avoid IDs) and
749
        // which don't have a =, or a " in front of them (to avoid filters)
750
        $pattern =
751
            '/(=\s*?["\']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/iS';
752
        $_index = $index = $lastIndex = $offset = 0;
753
        $longHexColors = include 'data/hex-to-named-color-map.php';
754
        $sb = array();
755
756
        while (preg_match($pattern, $css, $m, 0, $offset)) {
757
            $index = $this->indexOf($css, $m[0], $offset);
758
            $lastIndex = $index + strlen($m[0]);
759
            $isFilter = $m[1] !== null && $m[1] !== '';
760
761
            $sb[] = $this->strSlice($css, $_index, $index);
762
763
            if ($isFilter) {
764
                // Restore, maintain case, otherwise filter will break
765
                $sb[] = $m[1] .'#'. $m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7];
766
            } else {
767
                if (strtolower($m[2]) == strtolower($m[3]) &&
768
                    strtolower($m[4]) == strtolower($m[5]) &&
769
                    strtolower($m[6]) == strtolower($m[7])) {
770
                    // Compress.
771
                    $hex = '#'. strtolower($m[3] . $m[5] . $m[7]);
772
                } else {
773
                    // Non compressible color, restore but lower case.
774
                    $hex = '#'. strtolower($m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]);
775
                }
776
                // replace Hex colors with shorter color names
777
                $sb[] = array_key_exists($hex, $longHexColors) ? $longHexColors[$hex] : $hex;
778
            }
779
780
            $_index = $offset = $lastIndex - strlen($m[8]);
781
        }
782
783
        $sb[] = $this->strSlice($css, $_index);
784
785
        return implode('', $sb);
786
    }
787
788
    // ---------------------------------------------------------------------------------------------
789
    // CALLBACKS
790
    // ---------------------------------------------------------------------------------------------
791
792
    private function processComments($matches)
793
    {
794
        $match = !empty($matches[1]) ? $matches[1] : '';
795
        return $this->registerComment($match);
796
    }
797
798
    private function processStrings($matches)
799
    {
800
        $match = $matches[0];
801
        $quote = substr($match, 0, 1);
802
        $match = $this->strSlice($match, 1, -1);
803
804
        // maybe the string contains a comment-like substring?
805
        // one, maybe more? put'em back then
806
        if (($pos = strpos($match, self::COMMENT)) !== false) {
807
            for ($i = 0, $max = count($this->comments); $i < $max; $i++) {
808
                $match = preg_replace(
809
                    $this->getCommentPlaceholderRegexById($i),
810
                    $this->escapeReplacementString($this->comments[$i]),
811
                    $match,
812
                    1
813
                );
814
            }
815
        }
816
817
        // minify alpha opacity in filter strings
818
        $match = preg_replace('/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/i', 'alpha(opacity=', $match);
819
820
        $preservedTokenPlaceholder = $this->registerPreservedToken($match);
821
        return $quote . $preservedTokenPlaceholder . $quote;
822
    }
823
824
    private function processAtRuleBlocks($matches)
825
    {
826
        return $this->registerAtRuleBlock($matches[0]);
827
    }
828
829
    private function processCalc($matches)
830
    {
831
        $token = preg_replace(
832
            '/\)([+\-]{1})/',
833
            ') $1',
834
            preg_replace(
835
                '/([+\-]{1})\(/',
836
                '$1 (',
837
                trim(preg_replace('/\s*([*\/(),])\s*/', '$1', $matches[2]))
838
            )
839
        );
840
        $preservedTokenPlaceholder = $this->registerPreservedToken($token);
841
        return 'calc('. $preservedTokenPlaceholder .')';
842
    }
843
844
    private function processOldIeSpecificMatrixDefinition($matches)
845
    {
846
        $preservedTokenPlaceholder = $this->registerPreservedToken($matches[1]);
847
        return 'filter:progid:DXImageTransform.Microsoft.Matrix('. $preservedTokenPlaceholder .')';
848
    }
849
850
    private function processColon($matches)
851
    {
852
        return preg_replace('/\:/', self::CLASSCOLON, $matches[0]);
853
    }
854
855
    private function removeSpacesFromDataUrls($matches)
856
    {
857
        return preg_replace('/\s+/', '', $matches[0]);
858
    }
859
860
    private function rgbToHex($matches)
861
    {
862
        $hexColors = array();
863
        $rgbColors = explode(',', $matches[1]);
864
865
        // Values outside the sRGB color space should be clipped (0-255)
866
        for ($i = 0, $l = count($rgbColors); $i < $l; $i++) {
867
            $hexColors[$i] = sprintf("%02x", $this->clampNumberSrgb($this->rgbPercentageToRgbInteger($rgbColors[$i])));
868
        }
869
870
        // Fix for issue #2528093
871
        if (!preg_match('/[\s,);}]/', $matches[2])) {
872
            $matches[2] = ' '. $matches[2];
873
        }
874
875
        return '#'. implode('', $hexColors) . $matches[2];
876
    }
877
878
    private function hslToHex($matches)
879
    {
880
        $hslValues = explode(',', $matches[1]);
881
882
        $rgbColors = $this->hslToRgb($hslValues);
883
884
        return $this->rgbToHex(array('', implode(',', $rgbColors), $matches[2]));
885
    }
886
887
    private function processAtRulesOperators($matches)
888
    {
889
        return $matches[1] . strtolower($matches[2]) .' (';
890
    }
891
892
    private function lowercasePseudoFirst($matches)
893
    {
894
        return ':first-'. strtolower($matches[1]) .' '. $matches[2];
895
    }
896
897
    private function lowercaseDirectives($matches)
898
    {
899
        return '@'. strtolower($matches[1]);
900
    }
901
902
    private function lowercasePseudoElements($matches)
903
    {
904
        return ':'. strtolower($matches[1]);
905
    }
906
907
    private function lowercaseCommonFunctions($matches)
908
    {
909
        return ':'. strtolower($matches[1]) .'(';
910
    }
911
912
    private function lowercaseCommonFunctionsValues($matches)
913
    {
914
        return $matches[1] . strtolower($matches[2]);
915
    }
916
917
    private function lowercaseProperties($matches)
918
    {
919
        return $matches[1] . strtolower($matches[2]) . $matches[3];
920
    }
921
922
    // ---------------------------------------------------------------------------------------------
923
    // HELPERS
924
    // ---------------------------------------------------------------------------------------------
925
926
    /**
927
     * Clamps a number between a minimum and a maximum value.
928
     * @param int|float $n the number to clamp
929
     * @param int|float $min the lower end number allowed
930
     * @param int|float $max the higher end number allowed
931
     * @return int|float
932
     */
933
    private function clampNumber($n, $min, $max)
934
    {
935
        return min(max($n, $min), $max);
936
    }
937
938
    /**
939
     * Clamps a RGB color number outside the sRGB color space
940
     * @param int|float $n the number to clamp
941
     * @return int|float
942
     */
943
    private function clampNumberSrgb($n)
944
    {
945
        return $this->clampNumber($n, 0, 255);
946
    }
947
948
    /**
949
     * Escapes backreferences such as \1 and $1 in a regular expression replacement string
950
     * @param $string
951
     * @return string
952
     */
953
    private function escapeReplacementString($string)
954
    {
955
        return addcslashes($string, '\\$');
956
    }
957
958
    /**
959
     * Converts a HSL color into a RGB color
960
     * @param array $hslValues
961
     * @return array
962
     */
963
    private function hslToRgb($hslValues)
964
    {
965
        $h = floatval($hslValues[0]);
966
        $s = floatval(str_replace('%', '', $hslValues[1]));
967
        $l = floatval(str_replace('%', '', $hslValues[2]));
968
969
        // Wrap and clamp, then fraction!
970
        $h = ((($h % 360) + 360) % 360) / 360;
971
        $s = $this->clampNumber($s, 0, 100) / 100;
972
        $l = $this->clampNumber($l, 0, 100) / 100;
973
974 View Code Duplication
        if ($s == 0) {
975
            $r = $g = $b = $this->roundNumber(255 * $l);
976
        } else {
977
            $v2 = $l < 0.5 ? $l * (1 + $s) : ($l + $s) - ($s * $l);
978
            $v1 = (2 * $l) - $v2;
979
            $r = $this->roundNumber(255 * $this->hueToRgb($v1, $v2, $h + (1/3)));
980
            $g = $this->roundNumber(255 * $this->hueToRgb($v1, $v2, $h));
981
            $b = $this->roundNumber(255 * $this->hueToRgb($v1, $v2, $h - (1/3)));
982
        }
983
984
        return array($r, $g, $b);
985
    }
986
987
    /**
988
     * Tests and selects the correct formula for each RGB color channel
989
     * @param $v1
990
     * @param $v2
991
     * @param $vh
992
     * @return mixed
993
     */
994 View Code Duplication
    private function hueToRgb($v1, $v2, $vh)
995
    {
996
        $vh = $vh < 0 ? $vh + 1 : ($vh > 1 ? $vh - 1 : $vh);
997
998
        if ($vh * 6 < 1) {
999
            return $v1 + ($v2 - $v1) * 6 * $vh;
1000
        }
1001
1002
        if ($vh * 2 < 1) {
1003
            return $v2;
1004
        }
1005
1006
        if ($vh * 3 < 2) {
1007
            return $v1 + ($v2 - $v1) * ((2 / 3) - $vh) * 6;
1008
        }
1009
1010
        return $v1;
1011
    }
1012
1013
    /**
1014
     * PHP port of Javascript's "indexOf" function for strings only
1015
     * Author: Tubal Martin
1016
     *
1017
     * @param string $haystack
1018
     * @param string $needle
1019
     * @param int    $offset index (optional)
1020
     * @return int
1021
     */
1022
    private function indexOf($haystack, $needle, $offset = 0)
1023
    {
1024
        $index = strpos($haystack, $needle, $offset);
1025
1026
        return ($index !== false) ? $index : -1;
1027
    }
1028
1029
    /**
1030
     * Convert strings like "64M" or "30" to int values
1031
     * @param mixed $size
1032
     * @return int
1033
     */
1034
    private function normalizeInt($size)
1035
    {
1036
        if (is_string($size)) {
1037
            $letter = substr($size, -1);
1038
            $size = intval($size);
1039
            switch ($letter) {
1040
                case 'M':
1041
                case 'm':
1042
                    return (int) $size * 1048576;
1043
                case 'K':
1044
                case 'k':
1045
                    return (int) $size * 1024;
1046
                case 'G':
1047
                case 'g':
1048
                    return (int) $size * 1073741824;
1049
            }
1050
        }
1051
        return (int) $size;
1052
    }
1053
1054
    /**
1055
     * Converts a string containing and RGB percentage value into a RGB integer value i.e. '90%' -> 229.5
1056
     * @param $rgbPercentage
1057
     * @return int
1058
     */
1059
    private function rgbPercentageToRgbInteger($rgbPercentage)
1060
    {
1061
        if (strpos($rgbPercentage, '%') !== false) {
1062
            $rgbPercentage = $this->roundNumber(floatval(str_replace('%', '', $rgbPercentage)) * 2.55);
1063
        }
1064
1065
        return intval($rgbPercentage, 10);
1066
    }
1067
1068
    /**
1069
     * Rounds a number to its closest integer
1070
     * @param $n
1071
     * @return int
1072
     */
1073
    private function roundNumber($n)
1074
    {
1075
        return intval(round(floatval($n)), 10);
1076
    }
1077
1078
    /**
1079
     * PHP port of Javascript's "slice" function for strings only
1080
     * Author: Tubal Martin
1081
     *
1082
     * @param string   $str
1083
     * @param int      $start index
1084
     * @param int|bool $end index (optional)
1085
     * @return string
1086
     */
1087 View Code Duplication
    private function strSlice($str, $start = 0, $end = false)
1088
    {
1089
        if ($end !== false && ($start < 0 || $end <= 0)) {
1090
            $max = strlen($str);
1091
1092
            if ($start < 0) {
1093
                if (($start = $max + $start) < 0) {
1094
                    return '';
1095
                }
1096
            }
1097
1098
            if ($end < 0) {
1099
                if (($end = $max + $end) < 0) {
1100
                    return '';
1101
                }
1102
            }
1103
1104
            if ($end <= $start) {
1105
                return '';
1106
            }
1107
        }
1108
1109
        $slice = ($end === false) ? substr($str, $start) : substr($str, $start, $end - $start);
1110
        return ($slice === false) ? '' : $slice;
1111
    }
1112
}
1113