1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace MatthiasMullie\Minify; |
4
|
|
|
|
5
|
|
|
use MatthiasMullie\Minify\Exceptions\FileImportException; |
6
|
|
|
use MatthiasMullie\PathConverter\Converter; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* CSS minifier. |
10
|
|
|
* |
11
|
|
|
* Please report bugs on https://github.com/matthiasmullie/minify/issues |
12
|
|
|
* |
13
|
|
|
* @author Matthias Mullie <[email protected]> |
14
|
|
|
* @author Tijs Verkoyen <[email protected]> |
15
|
|
|
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved |
16
|
|
|
* @license MIT License |
17
|
|
|
*/ |
18
|
|
|
class CSS extends Minify |
19
|
|
|
{ |
20
|
|
|
/** |
21
|
|
|
* @var int |
22
|
|
|
*/ |
23
|
|
|
protected $maxImportSize = 5; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var string[] |
27
|
|
|
*/ |
28
|
|
|
protected $importExtensions = array( |
29
|
|
|
'gif' => 'data:image/gif', |
30
|
|
|
'png' => 'data:image/png', |
31
|
|
|
'jpe' => 'data:image/jpeg', |
32
|
|
|
'jpg' => 'data:image/jpeg', |
33
|
|
|
'jpeg' => 'data:image/jpeg', |
34
|
|
|
'svg' => 'data:image/svg+xml', |
35
|
|
|
'woff' => 'data:application/x-font-woff', |
36
|
|
|
'tif' => 'image/tiff', |
37
|
|
|
'tiff' => 'image/tiff', |
38
|
|
|
'xbm' => 'image/x-xbitmap', |
39
|
|
|
); |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Set the maximum size if files to be imported. |
43
|
|
|
* |
44
|
|
|
* Files larger than this size (in kB) will not be imported into the CSS. |
45
|
|
|
* Importing files into the CSS as data-uri will save you some connections, |
46
|
|
|
* but we should only import relatively small decorative images so that our |
47
|
|
|
* CSS file doesn't get too bulky. |
48
|
|
|
* |
49
|
|
|
* @param int $size Size in kB |
50
|
|
|
*/ |
51
|
|
|
public function setMaxImportSize($size) |
52
|
|
|
{ |
53
|
|
|
$this->maxImportSize = $size; |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Set the type of extensions to be imported into the CSS (to save network |
58
|
|
|
* connections). |
59
|
|
|
* Keys of the array should be the file extensions & respective values |
60
|
|
|
* should be the data type. |
61
|
|
|
* |
62
|
|
|
* @param string[] $extensions Array of file extensions |
63
|
|
|
*/ |
64
|
|
|
public function setImportExtensions(array $extensions) |
65
|
|
|
{ |
66
|
|
|
$this->importExtensions = $extensions; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Move any import statements to the top. |
71
|
|
|
* |
72
|
|
|
* @param string $content Nearly finished CSS content |
73
|
|
|
* |
74
|
|
|
* @return string |
75
|
|
|
*/ |
76
|
|
|
protected function moveImportsToTop($content) |
77
|
|
|
{ |
78
|
|
|
if (preg_match_all('/@import[^;]+;/', $content, $matches)) { |
79
|
|
|
// remove from content |
80
|
|
|
foreach ($matches[0] as $import) { |
81
|
|
|
$content = str_replace($import, '', $content); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
// add to top |
85
|
|
|
$content = implode('', $matches[0]).$content; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
return $content; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Combine CSS from import statements. |
93
|
|
|
* |
94
|
|
|
* @import's will be loaded and their content merged into the original file, |
95
|
|
|
* to save HTTP requests. |
96
|
|
|
* |
97
|
|
|
* @param string $source The file to combine imports for |
98
|
|
|
* @param string $content The CSS content to combine imports for |
99
|
|
|
* @param string[] $parents Parent paths, for circular reference checks |
100
|
|
|
* |
101
|
|
|
* @return string |
102
|
|
|
* |
103
|
|
|
* @throws FileImportException |
104
|
|
|
*/ |
105
|
|
|
protected function combineImports($source, $content, $parents) |
106
|
|
|
{ |
107
|
|
|
$importRegexes = array( |
108
|
|
|
// @import url(xxx) |
109
|
|
|
'/ |
110
|
|
|
# import statement |
111
|
|
|
@import |
112
|
|
|
|
113
|
|
|
# whitespace |
114
|
|
|
\s+ |
115
|
|
|
|
116
|
|
|
# open url() |
117
|
|
|
url\( |
118
|
|
|
|
119
|
|
|
# (optional) open path enclosure |
120
|
|
|
(?P<quotes>["\']?) |
121
|
|
|
|
122
|
|
|
# fetch path |
123
|
|
|
(?P<path> |
124
|
|
|
|
125
|
|
|
# do not fetch data uris or external sources |
126
|
|
|
(?!( |
127
|
|
|
["\']? |
128
|
|
|
(data|https?): |
129
|
|
|
)) |
130
|
|
|
|
131
|
|
|
.+? |
132
|
|
|
) |
133
|
|
|
|
134
|
|
|
# (optional) close path enclosure |
135
|
|
|
(?P=quotes) |
136
|
|
|
|
137
|
|
|
# close url() |
138
|
|
|
\) |
139
|
|
|
|
140
|
|
|
# (optional) trailing whitespace |
141
|
|
|
\s* |
142
|
|
|
|
143
|
|
|
# (optional) media statement(s) |
144
|
|
|
(?P<media>[^;]*) |
145
|
|
|
|
146
|
|
|
# (optional) trailing whitespace |
147
|
|
|
\s* |
148
|
|
|
|
149
|
|
|
# (optional) closing semi-colon |
150
|
|
|
;? |
151
|
|
|
|
152
|
|
|
/ix', |
153
|
|
|
|
154
|
|
|
// @import 'xxx' |
155
|
|
|
'/ |
156
|
|
|
|
157
|
|
|
# import statement |
158
|
|
|
@import |
159
|
|
|
|
160
|
|
|
# whitespace |
161
|
|
|
\s+ |
162
|
|
|
|
163
|
|
|
# open path enclosure |
164
|
|
|
(?P<quotes>["\']) |
165
|
|
|
|
166
|
|
|
# fetch path |
167
|
|
|
(?P<path> |
168
|
|
|
|
169
|
|
|
# do not fetch data uris or external sources |
170
|
|
|
(?!( |
171
|
|
|
["\']? |
172
|
|
|
(data|https?): |
173
|
|
|
)) |
174
|
|
|
|
175
|
|
|
.+? |
176
|
|
|
) |
177
|
|
|
|
178
|
|
|
# close path enclosure |
179
|
|
|
(?P=quotes) |
180
|
|
|
|
181
|
|
|
# (optional) trailing whitespace |
182
|
|
|
\s* |
183
|
|
|
|
184
|
|
|
# (optional) media statement(s) |
185
|
|
|
(?P<media>[^;]*) |
186
|
|
|
|
187
|
|
|
# (optional) trailing whitespace |
188
|
|
|
\s* |
189
|
|
|
|
190
|
|
|
# (optional) closing semi-colon |
191
|
|
|
;? |
192
|
|
|
|
193
|
|
|
/ix', |
194
|
|
|
); |
195
|
|
|
|
196
|
|
|
// find all relative imports in css |
197
|
|
|
$matches = array(); |
198
|
|
|
foreach ($importRegexes as $importRegex) { |
199
|
|
|
if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) { |
200
|
|
|
$matches = array_merge($matches, $regexMatches); |
201
|
|
|
} |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
$search = array(); |
205
|
|
|
$replace = array(); |
206
|
|
|
|
207
|
|
|
// loop the matches |
208
|
|
|
foreach ($matches as $match) { |
209
|
|
|
// get the path for the file that will be imported |
210
|
|
|
$importPath = dirname($source).'/'.$match['path']; |
211
|
|
|
|
212
|
|
|
// only replace the import with the content if we can grab the |
213
|
|
|
// content of the file |
214
|
|
|
if ($this->canImportFile($importPath)) { |
215
|
|
|
// check if current file was not imported previously in the same |
216
|
|
|
// import chain. |
217
|
|
|
if (in_array($importPath, $parents)) { |
218
|
|
|
throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.'); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
// grab referenced file & minify it (which may include importing |
222
|
|
|
// yet other @import statements recursively) |
223
|
|
|
$minifier = new static($importPath); |
224
|
|
|
$importContent = $minifier->execute($source, $parents); |
225
|
|
|
|
226
|
|
|
// check if this is only valid for certain media |
227
|
|
|
if (!empty($match['media'])) { |
228
|
|
|
$importContent = '@media '.$match['media'].'{'.$importContent.'}'; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
// add to replacement array |
232
|
|
|
$search[] = $match[0]; |
233
|
|
|
$replace[] = $importContent; |
234
|
|
|
} |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
// replace the import statements |
238
|
|
|
$content = str_replace($search, $replace, $content); |
239
|
|
|
|
240
|
|
|
return $content; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* Import files into the CSS, base64-ized. |
245
|
|
|
* |
246
|
|
|
* @url(image.jpg) images will be loaded and their content merged into the |
247
|
|
|
* original file, to save HTTP requests. |
248
|
|
|
* |
249
|
|
|
* @param string $source The file to import files for |
250
|
|
|
* @param string $content The CSS content to import files for |
251
|
|
|
* |
252
|
|
|
* @return string |
253
|
|
|
*/ |
254
|
|
|
protected function importFiles($source, $content) |
255
|
|
|
{ |
256
|
|
|
$extensions = array_keys($this->importExtensions); |
257
|
|
|
$regex = '/url\((["\']?)((?!["\']?data:).*?\.('.implode('|', $extensions).'))\\1\)/i'; |
258
|
|
|
if ($extensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { |
259
|
|
|
$search = array(); |
260
|
|
|
$replace = array(); |
261
|
|
|
|
262
|
|
|
// loop the matches |
263
|
|
|
foreach ($matches as $match) { |
264
|
|
|
// get the path for the file that will be imported |
265
|
|
|
$path = $match[2]; |
266
|
|
|
$path = dirname($source).'/'.$path; |
267
|
|
|
$extension = $match[3]; |
268
|
|
|
|
269
|
|
|
// only replace the import with the content if we're able to get |
270
|
|
|
// the content of the file, and it's relatively small |
271
|
|
|
if ($this->canImportFile($path) && $this->canImportBySize($path)) { |
272
|
|
|
// grab content && base64-ize |
273
|
|
|
$importContent = $this->load($path); |
274
|
|
|
$importContent = base64_encode($importContent); |
275
|
|
|
|
276
|
|
|
// build replacement |
277
|
|
|
$search[] = $match[0]; |
278
|
|
|
$replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')'; |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
// replace the import statements |
283
|
|
|
$content = str_replace($search, $replace, $content); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
return $content; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* Minify the data. |
291
|
|
|
* Perform CSS optimizations. |
292
|
|
|
* |
293
|
|
|
* @param string[optional] $path Path to write the data to |
294
|
|
|
* @param string[] $parents Parent paths, for circular reference checks |
295
|
|
|
* |
296
|
|
|
* @return string The minified data |
297
|
|
|
*/ |
298
|
|
|
public function execute($path = null, $parents = array()) |
299
|
|
|
{ |
300
|
|
|
$content = ''; |
301
|
|
|
|
302
|
|
|
// loop css data (raw data and files) |
303
|
|
|
foreach ($this->data as $source => $css) { |
304
|
|
|
/* |
305
|
|
|
* Let's first take out strings & comments, since we can't just remove |
306
|
|
|
* whitespace anywhere. If whitespace occurs inside a string, we should |
307
|
|
|
* leave it alone. E.g.: |
308
|
|
|
* p { content: "a test" } |
309
|
|
|
*/ |
310
|
|
|
$this->extractStrings(); |
311
|
|
|
$this->stripComments(); |
312
|
|
|
$css = $this->replace($css); |
313
|
|
|
|
314
|
|
|
$css = $this->stripWhitespace($css); |
315
|
|
|
$css = $this->shortenHex($css); |
316
|
|
|
$css = $this->shortenZeroes($css); |
317
|
|
|
$css = $this->shortenFontWeights($css); |
318
|
|
|
$css = $this->stripEmptyTags($css); |
319
|
|
|
|
320
|
|
|
// restore the string we've extracted earlier |
321
|
|
|
$css = $this->restoreExtractedData($css); |
322
|
|
|
|
323
|
|
|
$source = is_int($source) ? '' : $source; |
324
|
|
|
$parents = $source ? array_merge($parents, array($source)) : $parents; |
325
|
|
|
$css = $this->combineImports($source, $css, $parents); |
326
|
|
|
$css = $this->importFiles($source, $css); |
327
|
|
|
|
328
|
|
|
/* |
329
|
|
|
* If we'll save to a new path, we'll have to fix the relative paths |
330
|
|
|
* to be relative no longer to the source file, but to the new path. |
331
|
|
|
* If we don't write to a file, fall back to same path so no |
332
|
|
|
* conversion happens (because we still want it to go through most |
333
|
|
|
* of the move code...) |
334
|
|
|
*/ |
335
|
|
|
$converter = new Converter($source, $path ?: $source); |
336
|
|
|
$css = $this->move($converter, $css); |
337
|
|
|
|
338
|
|
|
// combine css |
339
|
|
|
$content .= $css; |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
$content = $this->moveImportsToTop($content); |
343
|
|
|
|
344
|
|
|
return $content; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* Moving a css file should update all relative urls. |
349
|
|
|
* Relative references (e.g. ../images/image.gif) in a certain css file, |
350
|
|
|
* will have to be updated when a file is being saved at another location |
351
|
|
|
* (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper). |
352
|
|
|
* |
353
|
|
|
* @param Converter $converter Relative path converter |
354
|
|
|
* @param string $content The CSS content to update relative urls for |
355
|
|
|
* |
356
|
|
|
* @return string |
357
|
|
|
*/ |
358
|
|
|
protected function move(Converter $converter, $content) |
359
|
|
|
{ |
360
|
|
|
/* |
361
|
|
|
* Relative path references will usually be enclosed by url(). @import |
362
|
|
|
* is an exception, where url() is not necessary around the path (but is |
363
|
|
|
* allowed). |
364
|
|
|
* This *could* be 1 regular expression, where both regular expressions |
365
|
|
|
* in this array are on different sides of a |. But we're using named |
366
|
|
|
* patterns in both regexes, the same name on both regexes. This is only |
367
|
|
|
* possible with a (?J) modifier, but that only works after a fairly |
368
|
|
|
* recent PCRE version. That's why I'm doing 2 separate regular |
369
|
|
|
* expressions & combining the matches after executing of both. |
370
|
|
|
*/ |
371
|
|
|
$relativeRegexes = array( |
372
|
|
|
// url(xxx) |
373
|
|
|
'/ |
374
|
|
|
# open url() |
375
|
|
|
url\( |
376
|
|
|
|
377
|
|
|
\s* |
378
|
|
|
|
379
|
|
|
# open path enclosure |
380
|
|
|
(?P<quotes>["\'])? |
381
|
|
|
|
382
|
|
|
# fetch path |
383
|
|
|
(?P<path> |
384
|
|
|
|
385
|
|
|
# do not fetch data uris or external sources |
386
|
|
|
(?!( |
387
|
|
|
\s? |
388
|
|
|
["\']? |
389
|
|
|
(data|https?): |
390
|
|
|
)) |
391
|
|
|
|
392
|
|
|
.+? |
393
|
|
|
) |
394
|
|
|
|
395
|
|
|
# close path enclosure |
396
|
|
|
(?(quotes)(?P=quotes)) |
397
|
|
|
|
398
|
|
|
\s* |
399
|
|
|
|
400
|
|
|
# close url() |
401
|
|
|
\) |
402
|
|
|
|
403
|
|
|
/ix', |
404
|
|
|
|
405
|
|
|
// @import "xxx" |
406
|
|
|
'/ |
407
|
|
|
# import statement |
408
|
|
|
@import |
409
|
|
|
|
410
|
|
|
# whitespace |
411
|
|
|
\s+ |
412
|
|
|
|
413
|
|
|
# we don\'t have to check for @import url(), because the |
414
|
|
|
# condition above will already catch these |
415
|
|
|
|
416
|
|
|
# open path enclosure |
417
|
|
|
(?P<quotes>["\']) |
418
|
|
|
|
419
|
|
|
# fetch path |
420
|
|
|
(?P<path> |
421
|
|
|
|
422
|
|
|
# do not fetch data uris or external sources |
423
|
|
|
(?!( |
424
|
|
|
["\']? |
425
|
|
|
(data|https?): |
426
|
|
|
)) |
427
|
|
|
|
428
|
|
|
.+? |
429
|
|
|
) |
430
|
|
|
|
431
|
|
|
# close path enclosure |
432
|
|
|
(?P=quotes) |
433
|
|
|
|
434
|
|
|
/ix', |
435
|
|
|
); |
436
|
|
|
|
437
|
|
|
// find all relative urls in css |
438
|
|
|
$matches = array(); |
439
|
|
|
foreach ($relativeRegexes as $relativeRegex) { |
440
|
|
|
if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) { |
441
|
|
|
$matches = array_merge($matches, $regexMatches); |
442
|
|
|
} |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
$search = array(); |
446
|
|
|
$replace = array(); |
447
|
|
|
|
448
|
|
|
// loop all urls |
449
|
|
|
foreach ($matches as $match) { |
450
|
|
|
// determine if it's a url() or an @import match |
451
|
|
|
$type = (strpos($match[0], '@import') === 0 ? 'import' : 'url'); |
452
|
|
|
|
453
|
|
|
// attempting to interpret GET-params makes no sense, so let's discard them for awhile |
454
|
|
|
$params = strrchr($match['path'], '?'); |
455
|
|
|
$url = $params ? substr($match['path'], 0, -strlen($params)) : $match['path']; |
456
|
|
|
|
457
|
|
|
// fix relative url |
458
|
|
|
$url = $converter->convert($url); |
459
|
|
|
|
460
|
|
|
// now that the path has been converted, re-apply GET-params |
461
|
|
|
$url .= $params; |
462
|
|
|
|
463
|
|
|
// build replacement |
464
|
|
|
$search[] = $match[0]; |
465
|
|
|
if ($type == 'url') { |
466
|
|
|
$replace[] = 'url('.$url.')'; |
467
|
|
|
} elseif ($type == 'import') { |
468
|
|
|
$replace[] = '@import "'.$url.'"'; |
469
|
|
|
} |
470
|
|
|
} |
471
|
|
|
|
472
|
|
|
// replace urls |
473
|
|
|
$content = str_replace($search, $replace, $content); |
474
|
|
|
|
475
|
|
|
return $content; |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
/** |
479
|
|
|
* Shorthand hex color codes. |
480
|
|
|
* #FF0000 -> #F00. |
481
|
|
|
* |
482
|
|
|
* @param string $content The CSS content to shorten the hex color codes for |
483
|
|
|
* |
484
|
|
|
* @return string |
485
|
|
|
*/ |
486
|
|
|
protected function shortenHex($content) |
487
|
|
|
{ |
488
|
|
|
$content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?=[; }])/i', '#$1$2$3', $content); |
489
|
|
|
|
490
|
|
|
// we can shorten some even more by replacing them with their color name |
491
|
|
|
$colors = array( |
492
|
|
|
'#F0FFFF' => 'azure', |
493
|
|
|
'#F5F5DC' => 'beige', |
494
|
|
|
'#A52A2A' => 'brown', |
495
|
|
|
'#FF7F50' => 'coral', |
496
|
|
|
'#FFD700' => 'gold', |
497
|
|
|
'#808080' => 'gray', |
498
|
|
|
'#008000' => 'green', |
499
|
|
|
'#4B0082' => 'indigo', |
500
|
|
|
'#FFFFF0' => 'ivory', |
501
|
|
|
'#F0E68C' => 'khaki', |
502
|
|
|
'#FAF0E6' => 'linen', |
503
|
|
|
'#800000' => 'maroon', |
504
|
|
|
'#000080' => 'navy', |
505
|
|
|
'#808000' => 'olive', |
506
|
|
|
'#CD853F' => 'peru', |
507
|
|
|
'#FFC0CB' => 'pink', |
508
|
|
|
'#DDA0DD' => 'plum', |
509
|
|
|
'#800080' => 'purple', |
510
|
|
|
'#F00' => 'red', |
511
|
|
|
'#FA8072' => 'salmon', |
512
|
|
|
'#A0522D' => 'sienna', |
513
|
|
|
'#C0C0C0' => 'silver', |
514
|
|
|
'#FFFAFA' => 'snow', |
515
|
|
|
'#D2B48C' => 'tan', |
516
|
|
|
'#FF6347' => 'tomato', |
517
|
|
|
'#EE82EE' => 'violet', |
518
|
|
|
'#F5DEB3' => 'wheat', |
519
|
|
|
); |
520
|
|
|
|
521
|
|
|
return preg_replace_callback( |
522
|
|
|
'/(?<=[: ])('.implode(array_keys($colors), '|').')(?=[; }])/i', |
523
|
|
|
function ($match) use ($colors) { |
524
|
|
|
return $colors[strtoupper($match[0])]; |
525
|
|
|
}, |
526
|
|
|
$content |
527
|
|
|
); |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
/** |
531
|
|
|
* Shorten CSS font weights. |
532
|
|
|
* |
533
|
|
|
* @param string $content The CSS content to shorten the font weights for |
534
|
|
|
* |
535
|
|
|
* @return string |
536
|
|
|
*/ |
537
|
|
|
protected function shortenFontWeights($content) |
538
|
|
|
{ |
539
|
|
|
$weights = array( |
540
|
|
|
'normal' => 400, |
541
|
|
|
'bold' => 700, |
542
|
|
|
); |
543
|
|
|
|
544
|
|
|
$callback = function ($match) use ($weights) { |
545
|
|
|
return $match[1].$weights[$match[2]]; |
546
|
|
|
}; |
547
|
|
|
|
548
|
|
|
return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content); |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
/** |
552
|
|
|
* Shorthand 0 values to plain 0, instead of e.g. -0em. |
553
|
|
|
* |
554
|
|
|
* @param string $content The CSS content to shorten the zero values for |
555
|
|
|
* |
556
|
|
|
* @return string |
557
|
|
|
*/ |
558
|
|
|
protected function shortenZeroes($content) |
559
|
|
|
{ |
560
|
|
|
// reusable bits of code throughout these regexes: |
561
|
|
|
// before & after are used to make sure we don't match lose unintended |
562
|
|
|
// 0-like values (e.g. in #000, or in http://url/1.0) |
563
|
|
|
// units can be stripped from 0 values, or used to recognize non 0 |
564
|
|
|
// values (where wa may be able to strip a .0 suffix) |
565
|
|
|
$before = '(?<=[:(, ])'; |
566
|
|
|
$after = '(?=[ ,);}])'; |
567
|
|
|
$units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)'; |
568
|
|
|
|
569
|
|
|
// strip units after zeroes (0px -> 0) |
570
|
|
|
// NOTE: it should be safe to remove all units for a 0 value, but in |
571
|
|
|
// practice, Webkit (especially Safari) seems to stumble over at least |
572
|
|
|
// 0%, potentially other units as well. Only stripping 'px' for now. |
573
|
|
|
// @see https://github.com/matthiasmullie/minify/issues/60 |
574
|
|
|
$content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content); |
575
|
|
|
|
576
|
|
|
// strip 0-digits (.0 -> 0) |
577
|
|
|
$content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content); |
578
|
|
|
// strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px |
579
|
|
|
$content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content); |
580
|
|
|
// strip trailing 0: 50.00 -> 50, 50.00px -> 50px |
581
|
|
|
$content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content); |
582
|
|
|
// strip leading 0: 0.1 -> .1, 01.1 -> 1.1 |
583
|
|
|
$content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content); |
584
|
|
|
|
585
|
|
|
// strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0) |
586
|
|
|
$content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content); |
587
|
|
|
|
588
|
|
|
// remove zeroes where they make no sense in calc: e.g. calc(100px - 0) |
589
|
|
|
// the 0 doesn't have any effect, and this isn't even valid without unit |
590
|
|
|
// strip all `+ 0` or `- 0` occurrences: calc(10% + 0) -> calc(10%) |
591
|
|
|
// looped because there may be multiple 0s inside 1 group of parentheses |
592
|
|
|
do { |
593
|
|
|
$previous = $content; |
594
|
|
|
$content = preg_replace('/\(([^\(\)]+)\s+[\+\-]\s+0(\s+[^\(\)]+)?\)/', '(\\1\\2)', $content); |
595
|
|
|
} while ($content !== $previous); |
596
|
|
|
// strip all `0 +` occurrences: calc(0 + 10%) -> calc(10%) |
597
|
|
|
$content = preg_replace('/\(\s*0\s+\+\s+([^\(\)]+)\)/', '(\\1)', $content); |
598
|
|
|
// strip all `0 -` occurrences: calc(0 - 10%) -> calc(-10%) |
599
|
|
|
$content = preg_replace('/\(\s*0\s+\-\s+([^\(\)]+)\)/', '(-\\1)', $content); |
600
|
|
|
// I'm not going to attempt to optimize away `x * 0` instances: |
601
|
|
|
// it's dumb enough code already that it likely won't occur, and it's |
602
|
|
|
// too complex to do right (order of operations would have to be |
603
|
|
|
// respected etc) |
604
|
|
|
// what I cared about most here was fixing incorrectly truncated units |
605
|
|
|
|
606
|
|
|
return $content; |
607
|
|
|
} |
608
|
|
|
|
609
|
|
|
/** |
610
|
|
|
* Strip comments from source code. |
611
|
|
|
* |
612
|
|
|
* @param string $content |
613
|
|
|
* |
614
|
|
|
* @return string |
615
|
|
|
*/ |
616
|
|
|
protected function stripEmptyTags($content) |
617
|
|
|
{ |
618
|
|
|
return preg_replace('/(^|\}|;)[^\{\};]+\{\s*\}/', '\\1', $content); |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
/** |
622
|
|
|
* Strip comments from source code. |
623
|
|
|
*/ |
624
|
|
|
protected function stripComments() |
625
|
|
|
{ |
626
|
|
|
$this->registerPattern('/\/\*.*?\*\//s', ''); |
627
|
|
|
} |
628
|
|
|
|
629
|
|
|
/** |
630
|
|
|
* Strip whitespace. |
631
|
|
|
* |
632
|
|
|
* @param string $content The CSS content to strip the whitespace for |
633
|
|
|
* |
634
|
|
|
* @return string |
635
|
|
|
*/ |
636
|
|
|
protected function stripWhitespace($content) |
637
|
|
|
{ |
638
|
|
|
// remove leading & trailing whitespace |
639
|
|
|
$content = preg_replace('/^\s*/m', '', $content); |
640
|
|
|
$content = preg_replace('/\s*$/m', '', $content); |
641
|
|
|
|
642
|
|
|
// replace newlines with a single space |
643
|
|
|
$content = preg_replace('/\s+/', ' ', $content); |
644
|
|
|
|
645
|
|
|
// remove whitespace around meta characters |
646
|
|
|
// inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex |
647
|
|
|
$content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content); |
648
|
|
|
$content = preg_replace('/([\[(:])\s+/', '$1', $content); |
649
|
|
|
$content = preg_replace('/\s+([\]\)])/', '$1', $content); |
650
|
|
|
$content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content); |
651
|
|
|
|
652
|
|
|
// whitespace around + and - can only be stripped in selectors, like |
653
|
|
|
// :nth-child(3+2n), not in things like calc(3px + 2px) or shorthands |
654
|
|
|
// like 3px -2px |
655
|
|
|
$content = preg_replace('/\s*([+-])\s*(?=[^}]*{)/', '$1', $content); |
656
|
|
|
|
657
|
|
|
// remove semicolon/whitespace followed by closing bracket |
658
|
|
|
$content = str_replace(';}', '}', $content); |
659
|
|
|
|
660
|
|
|
return trim($content); |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
/** |
664
|
|
|
* Check if file is small enough to be imported. |
665
|
|
|
* |
666
|
|
|
* @param string $path The path to the file |
667
|
|
|
* |
668
|
|
|
* @return bool |
669
|
|
|
*/ |
670
|
|
|
protected function canImportBySize($path) |
671
|
|
|
{ |
672
|
|
|
return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024; |
673
|
|
|
} |
674
|
|
|
} |
675
|
|
|
|