1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/*! |
4
|
|
|
* CssMin |
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
|
|
|
namespace Autoptimize\tubalmartin\CssMin; |
24
|
|
|
|
25
|
|
|
class Minifier |
26
|
|
|
{ |
27
|
|
|
const QUERY_FRACTION = '_CSSMIN_QF_'; |
28
|
|
|
const COMMENT_TOKEN = '_CSSMIN_CMT_%d_'; |
29
|
|
|
const COMMENT_TOKEN_START = '_CSSMIN_CMT_'; |
30
|
|
|
const RULE_BODY_TOKEN = '_CSSMIN_RBT_%d_'; |
31
|
|
|
const PRESERVED_TOKEN = '_CSSMIN_PTK_%d_'; |
32
|
|
|
const UNQUOTED_FONT_TOKEN = '_CSSMIN_UFT_%d_'; |
33
|
|
|
|
34
|
|
|
// Token lists |
35
|
|
|
private $comments = array(); |
36
|
|
|
private $ruleBodies = array(); |
37
|
|
|
private $preservedTokens = array(); |
38
|
|
|
private $unquotedFontTokens = array(); |
39
|
|
|
|
40
|
|
|
// Output options |
41
|
|
|
private $keepImportantComments = true; |
42
|
|
|
private $keepSourceMapComment = false; |
43
|
|
|
private $linebreakPosition = 0; |
44
|
|
|
|
45
|
|
|
// PHP ini limits |
46
|
|
|
private $raisePhpLimits; |
47
|
|
|
private $memoryLimit; |
48
|
|
|
private $maxExecutionTime = 60; // 1 min |
49
|
|
|
private $pcreBacktrackLimit; |
50
|
|
|
private $pcreRecursionLimit; |
51
|
|
|
|
52
|
|
|
// Color maps |
53
|
|
|
private $hexToNamedColorsMap; |
54
|
|
|
private $namedToHexColorsMap; |
55
|
|
|
|
56
|
|
|
// Regexes |
57
|
|
|
private $numRegex; |
58
|
|
|
private $charsetRegex = '/@charset [^;]+;/Si'; |
59
|
|
|
private $importRegex = '/@import [^;]+;/Si'; |
60
|
|
|
private $namespaceRegex = '/@namespace [^;]+;/Si'; |
61
|
|
|
private $namedToHexColorsRegex; |
62
|
|
|
private $shortenOneZeroesRegex; |
63
|
|
|
private $shortenTwoZeroesRegex; |
64
|
|
|
private $shortenThreeZeroesRegex; |
65
|
|
|
private $shortenFourZeroesRegex; |
66
|
|
|
private $unitsGroupRegex = '(?:ch|cm|em|ex|gd|in|mm|px|pt|pc|q|rem|vh|vmax|vmin|vw|%)'; |
67
|
|
|
private $unquotedFontsRegex = '/(font-family:|font:)([^\'"]+?)[^};]*/Si'; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @param bool|int $raisePhpLimits If true, PHP settings will be raised if needed |
71
|
|
|
*/ |
72
|
|
|
public function __construct($raisePhpLimits = true) |
73
|
|
|
{ |
74
|
|
|
$this->raisePhpLimits = (bool) $raisePhpLimits; |
75
|
|
|
$this->memoryLimit = 128 * 1048576; // 128MB in bytes |
76
|
|
|
$this->pcreBacktrackLimit = 1000 * 1000; |
77
|
|
|
$this->pcreRecursionLimit = 500 * 1000; |
78
|
|
|
$this->hexToNamedColorsMap = Colors::getHexToNamedMap(); |
79
|
|
|
$this->namedToHexColorsMap = Colors::getNamedToHexMap(); |
80
|
|
|
$this->namedToHexColorsRegex = sprintf( |
81
|
|
|
'/([:,( ])(%s)( |,|\)|;|$)/Si', |
82
|
|
|
implode('|', array_keys($this->namedToHexColorsMap)) |
83
|
|
|
); |
84
|
|
|
$this->numRegex = sprintf('-?\d*\.?\d+%s?', $this->unitsGroupRegex); |
85
|
|
|
$this->setShortenZeroValuesRegexes(); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* Parses & minifies the given input CSS string |
90
|
|
|
* @param string $css |
91
|
|
|
* @return string |
92
|
|
|
*/ |
93
|
|
|
public function run($css = '') |
94
|
|
|
{ |
95
|
|
|
if (empty($css) || !is_string($css)) { |
96
|
|
|
return ''; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
$this->resetRunProperties(); |
100
|
|
|
|
101
|
|
|
if ($this->raisePhpLimits) { |
102
|
|
|
$this->doRaisePhpLimits(); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
return $this->minify($css); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Sets whether to keep or remove sourcemap special comment. |
110
|
|
|
* Sourcemap comments are removed by default. |
111
|
|
|
* @param bool $keepSourceMapComment |
112
|
|
|
*/ |
113
|
|
|
public function keepSourceMapComment($keepSourceMapComment = true) |
114
|
|
|
{ |
115
|
|
|
$this->keepSourceMapComment = (bool) $keepSourceMapComment; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Sets whether to keep or remove important comments. |
120
|
|
|
* Important comments outside of a declaration block are kept by default. |
121
|
|
|
* @param bool $removeImportantComments |
122
|
|
|
*/ |
123
|
|
|
public function removeImportantComments($removeImportantComments = true) |
124
|
|
|
{ |
125
|
|
|
$this->keepImportantComments = !(bool) $removeImportantComments; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Sets the approximate column after which long lines will be splitted in the output |
130
|
|
|
* with a linebreak. |
131
|
|
|
* @param int $position |
132
|
|
|
*/ |
133
|
|
|
public function setLineBreakPosition($position) |
134
|
|
|
{ |
135
|
|
|
$this->linebreakPosition = (int) $position; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Sets the memory limit for this script |
140
|
|
|
* @param int|string $limit |
141
|
|
|
*/ |
142
|
|
|
public function setMemoryLimit($limit) |
143
|
|
|
{ |
144
|
|
|
$this->memoryLimit = Utils::normalizeInt($limit); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Sets the maximum execution time for this script |
149
|
|
|
* @param int|string $seconds |
150
|
|
|
*/ |
151
|
|
|
public function setMaxExecutionTime($seconds) |
152
|
|
|
{ |
153
|
|
|
$this->maxExecutionTime = (int) $seconds; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Sets the PCRE backtrack limit for this script |
158
|
|
|
* @param int $limit |
159
|
|
|
*/ |
160
|
|
|
public function setPcreBacktrackLimit($limit) |
161
|
|
|
{ |
162
|
|
|
$this->pcreBacktrackLimit = (int) $limit; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Sets the PCRE recursion limit for this script |
167
|
|
|
* @param int $limit |
168
|
|
|
*/ |
169
|
|
|
public function setPcreRecursionLimit($limit) |
170
|
|
|
{ |
171
|
|
|
$this->pcreRecursionLimit = (int) $limit; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* Builds regular expressions needed for shortening zero values |
176
|
|
|
*/ |
177
|
|
|
private function setShortenZeroValuesRegexes() |
178
|
|
|
{ |
179
|
|
|
$zeroRegex = '0'. $this->unitsGroupRegex; |
180
|
|
|
$numOrPosRegex = '('. $this->numRegex .'|top|left|bottom|right|center) '; |
181
|
|
|
$oneZeroSafeProperties = array( |
182
|
|
|
'(?:line-)?height', |
183
|
|
|
'(?:(?:min|max)-)?width', |
184
|
|
|
'top', |
185
|
|
|
'left', |
186
|
|
|
'background-position', |
187
|
|
|
'bottom', |
188
|
|
|
'right', |
189
|
|
|
'border(?:-(?:top|left|bottom|right))?(?:-width)?', |
190
|
|
|
'border-(?:(?:top|bottom)-(?:left|right)-)?radius', |
191
|
|
|
'column-(?:gap|width)', |
192
|
|
|
'margin(?:-(?:top|left|bottom|right))?', |
193
|
|
|
'outline-width', |
194
|
|
|
'padding(?:-(?:top|left|bottom|right))?' |
195
|
|
|
); |
196
|
|
|
|
197
|
|
|
// First zero regex |
198
|
|
|
$regex = '/(^|;)('. implode('|', $oneZeroSafeProperties) .'):%s/Si'; |
199
|
|
|
$this->shortenOneZeroesRegex = sprintf($regex, $zeroRegex); |
200
|
|
|
|
201
|
|
|
// Multiple zeroes regexes |
202
|
|
|
$regex = '/(^|;)(margin|padding|border-(?:width|radius)|background-position):%s/Si'; |
203
|
|
|
$this->shortenTwoZeroesRegex = sprintf($regex, $numOrPosRegex . $zeroRegex); |
204
|
|
|
$this->shortenThreeZeroesRegex = sprintf($regex, $numOrPosRegex . $numOrPosRegex . $zeroRegex); |
205
|
|
|
$this->shortenFourZeroesRegex = sprintf($regex, $numOrPosRegex . $numOrPosRegex . $numOrPosRegex . $zeroRegex); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* Resets properties whose value may change between runs |
210
|
|
|
*/ |
211
|
|
|
private function resetRunProperties() |
212
|
|
|
{ |
213
|
|
|
$this->comments = array(); |
214
|
|
|
$this->ruleBodies = array(); |
215
|
|
|
$this->preservedTokens = array(); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Tries to configure PHP to use at least the suggested minimum settings |
220
|
|
|
* @return void |
221
|
|
|
*/ |
222
|
|
|
private function doRaisePhpLimits() |
223
|
|
|
{ |
224
|
|
|
$phpLimits = array( |
225
|
|
|
'memory_limit' => $this->memoryLimit, |
226
|
|
|
'max_execution_time' => $this->maxExecutionTime, |
227
|
|
|
'pcre.backtrack_limit' => $this->pcreBacktrackLimit, |
228
|
|
|
'pcre.recursion_limit' => $this->pcreRecursionLimit |
229
|
|
|
); |
230
|
|
|
|
231
|
|
|
// If current settings are higher respect them. |
232
|
|
|
foreach ($phpLimits as $name => $suggested) { |
233
|
|
|
$current = Utils::normalizeInt(ini_get($name)); |
234
|
|
|
|
235
|
|
|
if ($current >= $suggested) { |
236
|
|
|
continue; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
// memoryLimit exception: allow -1 for "no memory limit". |
240
|
|
|
if ($name === 'memory_limit' && $current === -1) { |
241
|
|
|
continue; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
// maxExecutionTime exception: allow 0 for "no memory limit". |
245
|
|
|
if ($name === 'max_execution_time' && $current === 0) { |
246
|
|
|
continue; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
ini_set($name, $suggested); |
250
|
|
|
} |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Registers a preserved token |
255
|
|
|
* @param string $token |
256
|
|
|
* @return string The token ID string |
257
|
|
|
*/ |
258
|
|
|
private function registerPreservedToken($token) |
259
|
|
|
{ |
260
|
|
|
$tokenId = sprintf(self::PRESERVED_TOKEN, count($this->preservedTokens)); |
261
|
|
|
$this->preservedTokens[$tokenId] = $token; |
262
|
|
|
return $tokenId; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Registers a candidate comment token |
267
|
|
|
* @param string $comment |
268
|
|
|
* @return string The comment token ID string |
269
|
|
|
*/ |
270
|
|
|
private function registerCommentToken($comment) |
271
|
|
|
{ |
272
|
|
|
$tokenId = sprintf(self::COMMENT_TOKEN, count($this->comments)); |
273
|
|
|
$this->comments[$tokenId] = $comment; |
274
|
|
|
return $tokenId; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Registers a rule body token |
279
|
|
|
* @param string $body the minified rule body |
280
|
|
|
* @return string The rule body token ID string |
281
|
|
|
*/ |
282
|
|
View Code Duplication |
private function registerRuleBodyToken($body) |
|
|
|
|
283
|
|
|
{ |
284
|
|
|
if (empty($body)) { |
285
|
|
|
return ''; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
$tokenId = sprintf(self::RULE_BODY_TOKEN, count($this->ruleBodies)); |
289
|
|
|
$this->ruleBodies[$tokenId] = $body; |
290
|
|
|
return $tokenId; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
View Code Duplication |
private function registerUnquotedFontToken($body) |
|
|
|
|
294
|
|
|
{ |
295
|
|
|
if (empty($body)) { |
296
|
|
|
return ''; |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
$tokenId = sprintf(self::UNQUOTED_FONT_TOKEN, count($this->unquotedFontTokens)); |
300
|
|
|
$this->unquotedFontTokens[$tokenId] = $body; |
301
|
|
|
return $tokenId; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* Parses & minifies the given input CSS string |
306
|
|
|
* @param string $css |
307
|
|
|
* @return string |
308
|
|
|
*/ |
309
|
|
|
private function minify($css) |
310
|
|
|
{ |
311
|
|
|
// Process data urls |
312
|
|
|
$css = $this->processDataUrls($css); |
313
|
|
|
|
314
|
|
|
// Process comments |
315
|
|
|
$css = preg_replace_callback( |
316
|
|
|
'/(?<!\\\\)\/\*(.*?)\*(?<!\\\\)\//Ss', |
317
|
|
|
array($this, 'processCommentsCallback'), |
318
|
|
|
$css |
319
|
|
|
); |
320
|
|
|
|
321
|
|
|
// IE7: Process Microsoft matrix filters (whitespaces between Matrix parameters). Can contain strings inside. |
322
|
|
|
$css = preg_replace_callback( |
323
|
|
|
'/filter:\s*progid:DXImageTransform\.Microsoft\.Matrix\(([^)]+)\)/Ss', |
324
|
|
|
array($this, 'processOldIeSpecificMatrixDefinitionCallback'), |
325
|
|
|
$css |
326
|
|
|
); |
327
|
|
|
|
328
|
|
|
// Process quoted unquotable attribute selectors to unquote them. Covers most common cases. |
329
|
|
|
// Likelyhood of a quoted attribute selector being a substring in a string: Very very low. |
330
|
|
|
$css = preg_replace( |
331
|
|
|
'/\[\s*([a-z][a-z-]+)\s*([\*\|\^\$~]?=)\s*[\'"](-?[a-z_][a-z0-9-_]+)[\'"]\s*\]/Ssi', |
332
|
|
|
'[$1$2$3]', |
333
|
|
|
$css |
334
|
|
|
); |
335
|
|
|
|
336
|
|
|
// Process strings so their content doesn't get accidentally minified |
337
|
|
|
$css = preg_replace_callback( |
338
|
|
|
'/(?:"(?:[^\\\\"]|\\\\.|\\\\)*")|'."(?:'(?:[^\\\\']|\\\\.|\\\\)*')/S", |
339
|
|
|
array($this, 'processStringsCallback'), |
340
|
|
|
$css |
341
|
|
|
); |
342
|
|
|
|
343
|
|
|
// Normalize all whitespace strings to single spaces. Easier to work with that way. |
344
|
|
|
$css = preg_replace('/\s+/S', ' ', $css); |
345
|
|
|
|
346
|
|
|
// Process import At-rules with unquoted URLs so URI reserved characters such as a semicolon may be used safely. |
347
|
|
|
$css = preg_replace_callback( |
348
|
|
|
'/@import url\(([^\'"]+?)\)( |;)/Si', |
349
|
|
|
array($this, 'processImportUnquotedUrlAtRulesCallback'), |
350
|
|
|
$css |
351
|
|
|
); |
352
|
|
|
|
353
|
|
|
// Process comments |
354
|
|
|
$css = $this->processComments($css); |
355
|
|
|
|
356
|
|
|
// Process rule bodies |
357
|
|
|
$css = $this->processRuleBodies($css); |
358
|
|
|
|
359
|
|
|
// Process at-rules and selectors |
360
|
|
|
$css = $this->processAtRulesAndSelectors($css); |
361
|
|
|
|
362
|
|
|
// Restore preserved rule bodies before splitting |
363
|
|
|
$css = strtr($css, $this->ruleBodies); |
364
|
|
|
|
365
|
|
|
// Split long lines in output if required |
366
|
|
|
$css = $this->processLongLineSplitting($css); |
367
|
|
|
|
368
|
|
|
// Restore preserved comments and strings |
369
|
|
|
$css = strtr($css, $this->preservedTokens); |
370
|
|
|
|
371
|
|
|
return trim($css); |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
/** |
375
|
|
|
* Searches & replaces all data urls with tokens before we start compressing, |
376
|
|
|
* to avoid performance issues running some of the subsequent regexes against large string chunks. |
377
|
|
|
* @param string $css |
378
|
|
|
* @return string |
379
|
|
|
*/ |
380
|
|
|
private function processDataUrls($css) |
381
|
|
|
{ |
382
|
|
|
$ret = ''; |
383
|
|
|
$searchOffset = $substrOffset = 0; |
384
|
|
|
|
385
|
|
|
// Since we need to account for non-base64 data urls, we need to handle |
386
|
|
|
// ' and ) being part of the data string. |
387
|
|
|
while (preg_match('/url\(\s*(["\']?)data:/Si', $css, $m, PREG_OFFSET_CAPTURE, $searchOffset)) { |
388
|
|
|
$matchStartIndex = $m[0][1]; |
389
|
|
|
$dataStartIndex = $matchStartIndex + 4; // url( length |
390
|
|
|
$searchOffset = $matchStartIndex + strlen($m[0][0]); |
391
|
|
|
$terminator = $m[1][0]; // ', " or empty (not quoted) |
392
|
|
|
$terminatorRegex = '/(?<!\\\\)'. (strlen($terminator) === 0 ? '' : $terminator.'\s*') .'(\))/S'; |
393
|
|
|
|
394
|
|
|
$ret .= substr($css, $substrOffset, $matchStartIndex - $substrOffset); |
395
|
|
|
|
396
|
|
|
// Terminator found |
397
|
|
|
if (preg_match($terminatorRegex, $css, $matches, PREG_OFFSET_CAPTURE, $searchOffset)) { |
398
|
|
|
$matchEndIndex = $matches[1][1]; |
399
|
|
|
$searchOffset = $matchEndIndex + 1; |
400
|
|
|
$token = substr($css, $dataStartIndex, $matchEndIndex - $dataStartIndex); |
401
|
|
|
|
402
|
|
|
// Remove all spaces only for base64 encoded URLs. |
403
|
|
|
if (stripos($token, 'base64,') !== false) { |
404
|
|
|
$token = preg_replace('/\s+/S', '', $token); |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
$ret .= 'url('. $this->registerPreservedToken(trim($token)) .')'; |
408
|
|
|
// No end terminator found, re-add the whole match. Should we throw/warn here? |
409
|
|
|
} else { |
410
|
|
|
$ret .= substr($css, $matchStartIndex, $searchOffset - $matchStartIndex); |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
$substrOffset = $searchOffset; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
$ret .= substr($css, $substrOffset); |
417
|
|
|
|
418
|
|
|
return $ret; |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* Registers all comments found as candidates to be preserved. |
423
|
|
|
* @param array $matches |
424
|
|
|
* @return string |
425
|
|
|
*/ |
426
|
|
|
private function processCommentsCallback($matches) |
427
|
|
|
{ |
428
|
|
|
return '/*'. $this->registerCommentToken($matches[1]) .'*/'; |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
/** |
432
|
|
|
* Preserves old IE Matrix string definition |
433
|
|
|
* @param array $matches |
434
|
|
|
* @return string |
435
|
|
|
*/ |
436
|
|
|
private function processOldIeSpecificMatrixDefinitionCallback($matches) |
437
|
|
|
{ |
438
|
|
|
return 'filter:progid:DXImageTransform.Microsoft.Matrix('. $this->registerPreservedToken($matches[1]) .')'; |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* Preserves strings found |
443
|
|
|
* @param array $matches |
444
|
|
|
* @return string |
445
|
|
|
*/ |
446
|
|
|
private function processStringsCallback($matches) |
447
|
|
|
{ |
448
|
|
|
$match = $matches[0]; |
449
|
|
|
$quote = substr($match, 0, 1); |
450
|
|
|
$match = substr($match, 1, -1); |
451
|
|
|
|
452
|
|
|
// maybe the string contains a comment-like substring? |
453
|
|
|
// one, maybe more? put'em back then |
454
|
|
|
if (strpos($match, self::COMMENT_TOKEN_START) !== false) { |
455
|
|
|
$match = strtr($match, $this->comments); |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
// minify alpha opacity in filter strings |
459
|
|
|
$match = str_ireplace('progid:DXImageTransform.Microsoft.Alpha(Opacity=', 'alpha(opacity=', $match); |
460
|
|
|
|
461
|
|
|
return $quote . $this->registerPreservedToken($match) . $quote; |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
/** |
465
|
|
|
* Searches & replaces all import at-rule unquoted urls with tokens so URI reserved characters such as a semicolon |
466
|
|
|
* may be used safely in a URL. |
467
|
|
|
* @param array $matches |
468
|
|
|
* @return string |
469
|
|
|
*/ |
470
|
|
|
private function processImportUnquotedUrlAtRulesCallback($matches) |
471
|
|
|
{ |
472
|
|
|
return '@import url('. $this->registerPreservedToken($matches[1]) .')'. $matches[2]; |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
/** |
476
|
|
|
* Preserves or removes comments found. |
477
|
|
|
* @param string $css |
478
|
|
|
* @return string |
479
|
|
|
*/ |
480
|
|
|
private function processComments($css) |
481
|
|
|
{ |
482
|
|
|
foreach ($this->comments as $commentId => $comment) { |
483
|
|
|
$commentIdString = '/*'. $commentId .'*/'; |
484
|
|
|
|
485
|
|
|
// ! in the first position of the comment means preserve |
486
|
|
|
// so push to the preserved tokens keeping the ! |
487
|
|
View Code Duplication |
if ($this->keepImportantComments && strpos($comment, '!') === 0) { |
|
|
|
|
488
|
|
|
$preservedTokenId = $this->registerPreservedToken($comment); |
489
|
|
|
// Put new lines before and after /*! important comments |
490
|
|
|
$css = str_replace($commentIdString, "\n/*$preservedTokenId*/\n", $css); |
491
|
|
|
continue; |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
// # sourceMappingURL= in the first position of the comment means sourcemap |
495
|
|
|
// so push to the preserved tokens if {$this->keepSourceMapComment} is truthy. |
496
|
|
View Code Duplication |
if ($this->keepSourceMapComment && strpos($comment, '# sourceMappingURL=') === 0) { |
|
|
|
|
497
|
|
|
$preservedTokenId = $this->registerPreservedToken($comment); |
498
|
|
|
// Add new line before the sourcemap comment |
499
|
|
|
$css = str_replace($commentIdString, "\n/*$preservedTokenId*/", $css); |
500
|
|
|
continue; |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
// Keep empty comments after child selectors (IE7 hack) |
504
|
|
|
// e.g. html >/**/ body |
505
|
|
|
if (strlen($comment) === 0 && strpos($css, '>/*'.$commentId) !== false) { |
506
|
|
|
$css = str_replace($commentId, $this->registerPreservedToken(''), $css); |
507
|
|
|
continue; |
508
|
|
|
} |
509
|
|
|
|
510
|
|
|
// in all other cases kill the comment |
511
|
|
|
$css = str_replace($commentIdString, '', $css); |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
// Normalize whitespace again |
515
|
|
|
$css = preg_replace('/ +/S', ' ', $css); |
516
|
|
|
|
517
|
|
|
return $css; |
518
|
|
|
} |
519
|
|
|
|
520
|
|
|
/** |
521
|
|
|
* Finds, minifies & preserves all rule bodies. |
522
|
|
|
* @param string $css the whole stylesheet. |
523
|
|
|
* @return string |
524
|
|
|
*/ |
525
|
|
|
private function processRuleBodies($css) |
526
|
|
|
{ |
527
|
|
|
$ret = ''; |
528
|
|
|
$searchOffset = $substrOffset = 0; |
529
|
|
|
|
530
|
|
|
while (($blockStartPos = strpos($css, '{', $searchOffset)) !== false) { |
531
|
|
|
$blockEndPos = strpos($css, '}', $blockStartPos); |
532
|
|
|
// When ending curly brace is missing, let's |
533
|
|
|
// behave like there was one at the end of the block... |
534
|
|
|
if ( false === $blockEndPos ) { |
535
|
|
|
$blockEndPos = strlen($css) - 1; |
536
|
|
|
} |
537
|
|
|
$nextBlockStartPos = strpos($css, '{', $blockStartPos + 1); |
538
|
|
|
$ret .= substr($css, $substrOffset, $blockStartPos - $substrOffset); |
539
|
|
|
|
540
|
|
|
if ($nextBlockStartPos !== false && $nextBlockStartPos < $blockEndPos) { |
541
|
|
|
$ret .= substr($css, $blockStartPos, $nextBlockStartPos - $blockStartPos); |
542
|
|
|
$searchOffset = $nextBlockStartPos; |
543
|
|
|
} else { |
544
|
|
|
$ruleBody = substr($css, $blockStartPos + 1, $blockEndPos - $blockStartPos - 1); |
545
|
|
|
$ruleBodyToken = $this->registerRuleBodyToken($this->processRuleBody($ruleBody)); |
546
|
|
|
$ret .= '{'. $ruleBodyToken .'}'; |
547
|
|
|
$searchOffset = $blockEndPos + 1; |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
$substrOffset = $searchOffset; |
551
|
|
|
} |
552
|
|
|
|
553
|
|
|
$ret .= substr($css, $substrOffset); |
554
|
|
|
|
555
|
|
|
return $ret; |
556
|
|
|
} |
557
|
|
|
|
558
|
|
|
/** |
559
|
|
|
* Compresses non-group rule bodies. |
560
|
|
|
* @param string $body The rule body without curly braces |
561
|
|
|
* @return string |
562
|
|
|
*/ |
563
|
|
|
private function processRuleBody($body) |
564
|
|
|
{ |
565
|
|
|
$body = trim($body); |
566
|
|
|
|
567
|
|
|
// Remove spaces before the things that should not have spaces before them. |
568
|
|
|
$body = preg_replace('/ ([:=,)*\/;\n])/S', '$1', $body); |
569
|
|
|
|
570
|
|
|
// Remove the spaces after the things that should not have spaces after them. |
571
|
|
|
$body = preg_replace('/([:=,(*\/!;\n]) /S', '$1', $body); |
572
|
|
|
|
573
|
|
|
// Replace multiple semi-colons in a row by a single one |
574
|
|
|
$body = preg_replace('/;;+/S', ';', $body); |
575
|
|
|
|
576
|
|
|
// Remove semicolon before closing brace except when: |
577
|
|
|
// - The last property is prefixed with a `*` (lte IE7 hack) to avoid issues on Symbian S60 3.x browsers. |
578
|
|
|
if (!preg_match('/\*[a-z0-9-]+:[^;]+;$/Si', $body)) { |
579
|
|
|
$body = rtrim($body, ';'); |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
// Remove important comments inside a rule body (because they make no sense here). |
583
|
|
|
if (strpos($body, '/*') !== false) { |
584
|
|
|
$body = preg_replace('/\n?\/\*[A-Z0-9_]+\*\/\n?/S', '', $body); |
585
|
|
|
} |
586
|
|
|
|
587
|
|
|
// Empty rule body? Exit :) |
588
|
|
|
if (empty($body)) { |
589
|
|
|
return ''; |
590
|
|
|
} |
591
|
|
|
|
592
|
|
|
// Shorten font-weight values |
593
|
|
|
$body = preg_replace( |
594
|
|
|
array('/(font-weight:)bold\b/Si', '/(font-weight:)normal\b/Si'), |
595
|
|
|
array('${1}700', '${1}400'), |
596
|
|
|
$body |
597
|
|
|
); |
598
|
|
|
|
599
|
|
|
// Shorten background property |
600
|
|
|
$body = preg_replace('/(background:)(?:none|transparent)( !|;|$)/Si', '${1}0 0$2', $body); |
601
|
|
|
|
602
|
|
|
// Shorten opacity IE filter |
603
|
|
|
$body = str_ireplace('progid:DXImageTransform.Microsoft.Alpha(Opacity=', 'alpha(opacity=', $body); |
604
|
|
|
|
605
|
|
|
// Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space) |
606
|
|
|
// Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space) |
607
|
|
|
// This makes it more likely that it'll get further compressed in the next step. |
608
|
|
|
$body = preg_replace_callback( |
609
|
|
|
'/(rgb|hsl)\(([0-9,.% -]+)\)(.|$)/Si', |
610
|
|
|
array($this, 'shortenHslAndRgbToHexCallback'), |
611
|
|
|
$body |
612
|
|
|
); |
613
|
|
|
|
614
|
|
|
// Shorten colors from #AABBCC to #ABC or shorter color name: |
615
|
|
|
// - Look for hex colors which don't have a "=" in front of them (to avoid MSIE filters) |
616
|
|
|
$body = preg_replace_callback( |
617
|
|
|
'/(?<!=)#([0-9a-f]{3,6})( |,|\)|;|$)/Si', |
618
|
|
|
array($this, 'shortenHexColorsCallback'), |
619
|
|
|
$body |
620
|
|
|
); |
621
|
|
|
|
622
|
|
|
// Tokenize unquoted font names in order to hide them from |
623
|
|
|
// color name replacements. |
624
|
|
|
$body = preg_replace_callback( |
625
|
|
|
$this->unquotedFontsRegex, |
626
|
|
|
array($this, 'preserveUnquotedFontTokens'), |
627
|
|
|
$body |
628
|
|
|
); |
629
|
|
|
|
630
|
|
|
// Shorten long named colors with a shorter HEX counterpart: white -> #fff. |
631
|
|
|
// Run at least 2 times to cover most cases |
632
|
|
|
$body = preg_replace_callback( |
633
|
|
|
array($this->namedToHexColorsRegex, $this->namedToHexColorsRegex), |
634
|
|
|
array($this, 'shortenNamedColorsCallback'), |
635
|
|
|
$body |
636
|
|
|
); |
637
|
|
|
|
638
|
|
|
// Restore unquoted font tokens now after colors have been changed. |
639
|
|
|
$body = $this->restoreUnquotedFontTokens($body); |
640
|
|
|
|
641
|
|
|
// Replace positive sign from numbers before the leading space is removed. |
642
|
|
|
// +1.2em to 1.2em, +.8px to .8px, +2% to 2% |
643
|
|
|
$body = preg_replace('/([ :,(])\+(\.?\d+)/S', '$1$2', $body); |
644
|
|
|
|
645
|
|
|
// shorten ms to s |
646
|
|
|
$body = preg_replace_callback('/([ :,(])(-?)(\d{3,})ms/Si', function ($matches) { |
647
|
|
|
return $matches[1] . $matches[2] . ((int) $matches[3] / 1000) .'s'; |
648
|
|
|
}, $body); |
649
|
|
|
|
650
|
|
|
// Remove leading zeros from integer and float numbers. |
651
|
|
|
// 000.6 to .6, -0.8 to -.8, 0050 to 50, -01.05 to -1.05 |
652
|
|
|
$body = preg_replace('/([ :,(])(-?)0+([1-9]?\.?\d+)/S', '$1$2$3', $body); |
653
|
|
|
|
654
|
|
|
// Remove trailing zeros from float numbers. |
655
|
|
|
// -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px |
656
|
|
|
$body = preg_replace('/([ :,(])(-?\d?\.\d+?)0+([^\d])/S', '$1$2$3', $body); |
657
|
|
|
|
658
|
|
|
// Remove trailing .0 -> -9.0 to -9 |
659
|
|
|
$body = preg_replace('/([ :,(])(-?\d+)\.0([^\d])/S', '$1$2$3', $body); |
660
|
|
|
|
661
|
|
|
// Replace 0 length numbers with 0 |
662
|
|
|
$body = preg_replace('/([ :,(])-?\.?0+([^\d])/S', '${1}0$2', $body); |
663
|
|
|
|
664
|
|
|
// Shorten zero values for safe properties only |
665
|
|
|
$body = preg_replace( |
666
|
|
|
array( |
667
|
|
|
$this->shortenOneZeroesRegex, |
668
|
|
|
$this->shortenTwoZeroesRegex, |
669
|
|
|
$this->shortenThreeZeroesRegex, |
670
|
|
|
$this->shortenFourZeroesRegex |
671
|
|
|
), |
672
|
|
|
array( |
673
|
|
|
'$1$2:0', |
674
|
|
|
'$1$2:$3 0', |
675
|
|
|
'$1$2:$3 $4 0', |
676
|
|
|
'$1$2:$3 $4 $5 0' |
677
|
|
|
), |
678
|
|
|
$body |
679
|
|
|
); |
680
|
|
|
|
681
|
|
|
// Replace 0 0 0; or 0 0 0 0; with 0 0 for background-position property. |
682
|
|
|
$body = preg_replace('/(background-position):0(?: 0){2,3}( !|;|$)/Si', '$1:0 0$2', $body); |
683
|
|
|
|
684
|
|
|
// Shorten suitable shorthand properties with repeated values |
685
|
|
|
$body = preg_replace( |
686
|
|
|
array( |
687
|
|
|
'/(margin|padding|border-(?:width|radius)):('.$this->numRegex.')(?: \2)+( !|;|$)/Si', |
688
|
|
|
'/(border-(?:style|color)):([#a-z0-9]+)(?: \2)+( !|;|$)/Si' |
689
|
|
|
), |
690
|
|
|
'$1:$2$3', |
691
|
|
|
$body |
692
|
|
|
); |
693
|
|
|
$body = preg_replace( |
694
|
|
|
array( |
695
|
|
|
'/(margin|padding|border-(?:width|radius)):'. |
696
|
|
|
'('.$this->numRegex.') ('.$this->numRegex.') \2 \3( !|;|$)/Si', |
697
|
|
|
'/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) \2 \3( !|;|$)/Si' |
698
|
|
|
), |
699
|
|
|
'$1:$2 $3$4', |
700
|
|
|
$body |
701
|
|
|
); |
702
|
|
|
$body = preg_replace( |
703
|
|
|
array( |
704
|
|
|
'/(margin|padding|border-(?:width|radius)):'. |
705
|
|
|
'('.$this->numRegex.') ('.$this->numRegex.') ('.$this->numRegex.') \3( !|;|$)/Si', |
706
|
|
|
'/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) ([#a-z0-9]+) \3( !|;|$)/Si' |
707
|
|
|
), |
708
|
|
|
'$1:$2 $3 $4$5', |
709
|
|
|
$body |
710
|
|
|
); |
711
|
|
|
|
712
|
|
|
// Lowercase some common functions that can be values |
713
|
|
|
$body = preg_replace_callback( |
714
|
|
|
'/(?:attr|blur|brightness|circle|contrast|cubic-bezier|drop-shadow|ellipse|from|grayscale|'. |
715
|
|
|
'hsla?|hue-rotate|inset|invert|local|minmax|opacity|perspective|polygon|rgba?|rect|repeat|saturate|sepia|'. |
716
|
|
|
'steps|to|url|var|-webkit-gradient|'. |
717
|
|
|
'(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|(?:repeating-)?(?:linear|radial)-gradient))\(/Si', |
718
|
|
|
array($this, 'strtolowerCallback'), |
719
|
|
|
$body |
720
|
|
|
); |
721
|
|
|
|
722
|
|
|
// Lowercase all uppercase properties |
723
|
|
|
$body = preg_replace_callback('/(?:^|;)[A-Z-]+:/S', array($this, 'strtolowerCallback'), $body); |
724
|
|
|
|
725
|
|
|
return $body; |
726
|
|
|
} |
727
|
|
|
|
728
|
|
|
private function preserveUnquotedFontTokens($matches) |
729
|
|
|
{ |
730
|
|
|
return $this->registerUnquotedFontToken($matches[0]); |
731
|
|
|
} |
732
|
|
|
|
733
|
|
|
private function restoreUnquotedFontTokens($body) |
734
|
|
|
{ |
735
|
|
|
return strtr($body, $this->unquotedFontTokens); |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
/** |
739
|
|
|
* Compresses At-rules and selectors. |
740
|
|
|
* @param string $css the whole stylesheet with rule bodies tokenized. |
741
|
|
|
* @return string |
742
|
|
|
*/ |
743
|
|
|
private function processAtRulesAndSelectors($css) |
744
|
|
|
{ |
745
|
|
|
$charset = ''; |
746
|
|
|
$imports = ''; |
747
|
|
|
$namespaces = ''; |
748
|
|
|
|
749
|
|
|
// Remove spaces before the things that should not have spaces before them. |
750
|
|
|
$css = preg_replace('/ ([@{};>+)\]~=,\/\n])/S', '$1', $css); |
751
|
|
|
|
752
|
|
|
// Remove the spaces after the things that should not have spaces after them. |
753
|
|
|
$css = preg_replace('/([{}:;>+(\[~=,\/\n]) /S', '$1', $css); |
754
|
|
|
|
755
|
|
|
// Shorten shortable double colon (CSS3) pseudo-elements to single colon (CSS2) |
756
|
|
|
$css = preg_replace('/::(before|after|first-(?:line|letter))(\{|,)/Si', ':$1$2', $css); |
757
|
|
|
|
758
|
|
|
// Retain space for special IE6 cases |
759
|
|
|
$css = preg_replace_callback('/:first-(line|letter)(\{|,)/Si', function ($matches) { |
760
|
|
|
return ':first-'. strtolower($matches[1]) .' '. $matches[2]; |
761
|
|
|
}, $css); |
762
|
|
|
|
763
|
|
|
// Find a fraction that may used in some @media queries such as: (min-aspect-ratio: 1/1) |
764
|
|
|
// Add token to add the "/" back in later |
765
|
|
|
$css = preg_replace('/\(([a-z-]+):([0-9]+)\/([0-9]+)\)/Si', '($1:$2'. self::QUERY_FRACTION .'$3)', $css); |
766
|
|
|
|
767
|
|
|
// Remove empty rule blocks up to 2 levels deep. |
768
|
|
|
$css = preg_replace(array_fill(0, 2, '/(\{)[^{};\/\n]+\{\}/S'), '$1', $css); |
769
|
|
|
$css = preg_replace('/[^{};\/\n]+\{\}/S', '', $css); |
770
|
|
|
|
771
|
|
|
// Two important comments next to each other? Remove extra newline. |
772
|
|
|
if ($this->keepImportantComments) { |
773
|
|
|
$css = str_replace("\n\n", "\n", $css); |
774
|
|
|
} |
775
|
|
|
|
776
|
|
|
// Restore fraction |
777
|
|
|
$css = str_replace(self::QUERY_FRACTION, '/', $css); |
778
|
|
|
|
779
|
|
|
// Lowercase some popular @directives |
780
|
|
|
$css = preg_replace_callback( |
781
|
|
|
'/(?<!\\\\)@(?:charset|document|font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframes|media|'. |
782
|
|
|
'namespace|page|supports|viewport)/Si', |
783
|
|
|
array($this, 'strtolowerCallback'), |
784
|
|
|
$css |
785
|
|
|
); |
786
|
|
|
|
787
|
|
|
// Lowercase some popular media types |
788
|
|
|
$css = preg_replace_callback( |
789
|
|
|
'/[ ,](?:all|aural|braille|handheld|print|projection|screen|tty|tv|embossed|speech)[ ,;{]/Si', |
790
|
|
|
array($this, 'strtolowerCallback'), |
791
|
|
|
$css |
792
|
|
|
); |
793
|
|
|
|
794
|
|
|
// Lowercase some common pseudo-classes & pseudo-elements |
795
|
|
|
$css = preg_replace_callback( |
796
|
|
|
'/(?<!\\\\):(?:active|after|before|checked|default|disabled|empty|enabled|first-(?:child|of-type)|'. |
797
|
|
|
'focus(?:-within)?|hover|indeterminate|in-range|invalid|lang\(|last-(?:child|of-type)|left|link|not\(|'. |
798
|
|
|
'nth-(?:child|of-type)\(|nth-last-(?:child|of-type)\(|only-(?:child|of-type)|optional|out-of-range|'. |
799
|
|
|
'read-(?:only|write)|required|right|root|:selection|target|valid|visited)/Si', |
800
|
|
|
array($this, 'strtolowerCallback'), |
801
|
|
|
$css |
802
|
|
|
); |
803
|
|
|
|
804
|
|
|
// @charset handling |
805
|
|
|
if (preg_match($this->charsetRegex, $css, $matches)) { |
806
|
|
|
// Keep the first @charset at-rule found |
807
|
|
|
$charset = $matches[0]; |
808
|
|
|
// Delete all @charset at-rules |
809
|
|
|
$css = preg_replace($this->charsetRegex, '', $css); |
810
|
|
|
} |
811
|
|
|
|
812
|
|
|
// @import handling |
813
|
|
|
$css = preg_replace_callback($this->importRegex, function ($matches) use (&$imports) { |
814
|
|
|
// Keep all @import at-rules found for later |
815
|
|
|
$imports .= $matches[0]; |
816
|
|
|
// Delete all @import at-rules |
817
|
|
|
return ''; |
818
|
|
|
}, $css); |
819
|
|
|
|
820
|
|
|
// @namespace handling |
821
|
|
|
$css = preg_replace_callback($this->namespaceRegex, function ($matches) use (&$namespaces) { |
822
|
|
|
// Keep all @namespace at-rules found for later |
823
|
|
|
$namespaces .= $matches[0]; |
824
|
|
|
// Delete all @namespace at-rules |
825
|
|
|
return ''; |
826
|
|
|
}, $css); |
827
|
|
|
|
828
|
|
|
// Order critical at-rules: |
829
|
|
|
// 1. @charset first |
830
|
|
|
// 2. @imports below @charset |
831
|
|
|
// 3. @namespaces below @imports |
832
|
|
|
$css = $charset . $imports . $namespaces . $css; |
833
|
|
|
|
834
|
|
|
return $css; |
835
|
|
|
} |
836
|
|
|
|
837
|
|
|
/** |
838
|
|
|
* Splits long lines after a specific column. |
839
|
|
|
* |
840
|
|
|
* Some source control tools don't like it when files containing lines longer |
841
|
|
|
* than, say 8000 characters, are checked in. The linebreak option is used in |
842
|
|
|
* that case to split long lines after a specific column. |
843
|
|
|
* |
844
|
|
|
* @param string $css the whole stylesheet. |
845
|
|
|
* @return string |
846
|
|
|
*/ |
847
|
|
|
private function processLongLineSplitting($css) |
848
|
|
|
{ |
849
|
|
|
if ($this->linebreakPosition > 0) { |
850
|
|
|
$l = strlen($css); |
851
|
|
|
$offset = $this->linebreakPosition; |
852
|
|
|
while (preg_match('/(?<!\\\\)\}(?!\n)/S', $css, $matches, PREG_OFFSET_CAPTURE, $offset)) { |
853
|
|
|
$matchIndex = $matches[0][1]; |
854
|
|
|
$css = substr_replace($css, "\n", $matchIndex + 1, 0); |
855
|
|
|
$offset = $matchIndex + 2 + $this->linebreakPosition; |
856
|
|
|
$l += 1; |
857
|
|
|
if ($offset > $l) { |
858
|
|
|
break; |
859
|
|
|
} |
860
|
|
|
} |
861
|
|
|
} |
862
|
|
|
|
863
|
|
|
return $css; |
864
|
|
|
} |
865
|
|
|
|
866
|
|
|
/** |
867
|
|
|
* Converts hsl() & rgb() colors to HEX format. |
868
|
|
|
* @param $matches |
869
|
|
|
* @return string |
870
|
|
|
*/ |
871
|
|
|
private function shortenHslAndRgbToHexCallback($matches) |
872
|
|
|
{ |
873
|
|
|
$type = $matches[1]; |
874
|
|
|
$values = explode(',', $matches[2]); |
875
|
|
|
$terminator = $matches[3]; |
876
|
|
|
|
877
|
|
|
if ($type === 'hsl') { |
878
|
|
|
$values = Utils::hslToRgb($values); |
879
|
|
|
} |
880
|
|
|
|
881
|
|
|
$hexColors = Utils::rgbToHex($values); |
882
|
|
|
|
883
|
|
|
// Restore space after rgb() or hsl() function in some cases such as: |
884
|
|
|
// background-image: linear-gradient(to bottom, rgb(210,180,140) 10%, rgb(255,0,0) 90%); |
885
|
|
|
if (!empty($terminator) && !preg_match('/[ ,);]/S', $terminator)) { |
886
|
|
|
$terminator = ' '. $terminator; |
887
|
|
|
} |
888
|
|
|
|
889
|
|
|
return '#'. implode('', $hexColors) . $terminator; |
890
|
|
|
} |
891
|
|
|
|
892
|
|
|
/** |
893
|
|
|
* Compresses HEX color values of the form #AABBCC to #ABC or short color name. |
894
|
|
|
* @param $matches |
895
|
|
|
* @return string |
896
|
|
|
*/ |
897
|
|
|
private function shortenHexColorsCallback($matches) |
898
|
|
|
{ |
899
|
|
|
$hex = $matches[1]; |
900
|
|
|
|
901
|
|
|
// Shorten suitable 6 chars HEX colors |
902
|
|
|
if (strlen($hex) === 6 && preg_match('/^([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3$/Si', $hex, $m)) { |
903
|
|
|
$hex = $m[1] . $m[2] . $m[3]; |
904
|
|
|
} |
905
|
|
|
|
906
|
|
|
// Lowercase |
907
|
|
|
$hex = '#'. strtolower($hex); |
908
|
|
|
|
909
|
|
|
// Replace Hex colors with shorter color names |
910
|
|
|
$color = array_key_exists($hex, $this->hexToNamedColorsMap) ? $this->hexToNamedColorsMap[$hex] : $hex; |
911
|
|
|
|
912
|
|
|
return $color . $matches[2]; |
913
|
|
|
} |
914
|
|
|
|
915
|
|
|
/** |
916
|
|
|
* Shortens all named colors with a shorter HEX counterpart for a set of safe properties |
917
|
|
|
* e.g. white -> #fff |
918
|
|
|
* @param array $matches |
919
|
|
|
* @return string |
920
|
|
|
*/ |
921
|
|
|
private function shortenNamedColorsCallback($matches) |
922
|
|
|
{ |
923
|
|
|
return $matches[1] . $this->namedToHexColorsMap[strtolower($matches[2])] . $matches[3]; |
924
|
|
|
} |
925
|
|
|
|
926
|
|
|
/** |
927
|
|
|
* Makes a string lowercase |
928
|
|
|
* @param array $matches |
929
|
|
|
* @return string |
930
|
|
|
*/ |
931
|
|
|
private function strtolowerCallback($matches) |
932
|
|
|
{ |
933
|
|
|
return strtolower($matches[0]); |
934
|
|
|
} |
935
|
|
|
} |
936
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.