Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
3 lars 1
<?
2
/* Taken from minify by Ryan Grove and Steve Clay and distributed
3
   under the following license:
4
 
5
   Copyright (c) 2008 Ryan Grove <ryan@wonko.com>
6
   Copyright (c) 2008 Steve Clay <steve@mrclay.org>
7
   All rights reserved.
8
 
9
   Redistribution and use in source and binary forms, with or without
10
   modification, are permitted provided that the following conditions are met:
11
 
12
     * Redistributions of source code must retain the above copyright notice,
13
       this list of conditions and the following disclaimer.
14
     * Redistributions in binary form must reproduce the above copyright notice,
15
       this list of conditions and the following disclaimer in the documentation
16
       and/or other materials provided with the distribution.
17
     * Neither the name of this project nor the names of its contributors may be
18
       used to endorse or promote products derived from this software without
19
       specific prior written permission.
20
 
21
   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22
   ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23
   WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
   DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
25
   ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26
   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27
   LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28
   ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
*/
31
 
32
class Minify_CSS {
33
 
34
    public static function minify($css, $options = array())
35
    {
36
        if (isset($options['preserveComments'])
37
            && !$options['preserveComments']) {
38
            $css = Minify_CSS_Compressor::process($css, $options);
39
        } else {
40
            $css = Minify_CommentPreserver::process(
41
                $css
42
                ,array('Minify_CSS_Compressor', 'process')
43
                ,array($options)
44
            );
45
        }
46
        if (! isset($options['currentDir']) && ! isset($options['prependRelativePath'])) {
47
            return $css;
48
        }
49
        if (isset($options['currentDir'])) {
50
            return Minify_CSS_UriRewriter::rewrite(
51
                $css
52
                ,$options['currentDir']
53
                ,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT']
54
                ,isset($options['symlinks']) ? $options['symlinks'] : array()
55
            );
56
        } else {
57
            return Minify_CSS_UriRewriter::prepend(
58
                $css
59
                ,$options['prependRelativePath']
60
            );
61
        }
62
    }
63
}
64
 
65
class Minify_CSS_UriRewriter {
66
 
67
    /**
68
     * Defines which class to call as part of callbacks, change this
69
     * if you extend Minify_CSS_UriRewriter
70
     * @var string
71
     */
72
    protected static $className = 'Minify_CSS_UriRewriter';
73
 
74
    /**
75
     * rewrite() and rewriteRelative() append debugging information here
76
     * @var string
77
     */
78
    public static $debugText = '';
79
 
80
    /**
81
     * Rewrite file relative URIs as root relative in CSS files
82
     *
83
     * @param string $css
84
     *
85
     * @param string $currentDir The directory of the current CSS file.
86
     *
87
     * @param string $docRoot The document root of the web site in which
88
     * the CSS file resides (default = $_SERVER['DOCUMENT_ROOT']).
89
     *
90
     * @param array $symlinks (default = array()) If the CSS file is stored in
91
     * a symlink-ed directory, provide an array of link paths to
92
     * target paths, where the link paths are within the document root. Because
93
     * paths need to be normalized for this to work, use "//" to substitute
94
     * the doc root in the link paths (the array keys). E.g.:
95
     * <code>
96
     * array('//symlink' => '/real/target/path') // unix
97
     * array('//static' => 'D:\\staticStorage')  // Windows
98
     * </code>
99
     *
100
     * @return string
101
     */
102
    public static function rewrite($css, $currentDir, $docRoot = null, $symlinks = array())
103
    {
104
        self::$_docRoot = self::_realpath(
105
            $docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT']
106
        );
107
        self::$_currentDir = self::_realpath($currentDir);
108
        self::$_symlinks = array();
109
 
110
        // normalize symlinks
111
        foreach ($symlinks as $link => $target) {
112
            $link = ($link === '//')
113
                ? self::$_docRoot
114
                : str_replace('//', self::$_docRoot . '/', $link);
115
            $link = strtr($link, '/', DIRECTORY_SEPARATOR);
116
            self::$_symlinks[$link] = self::_realpath($target);
117
        }
118
 
119
        self::$debugText .= "docRoot    : " . self::$_docRoot . "\n"
120
                          . "currentDir : " . self::$_currentDir . "\n";
121
        if (self::$_symlinks) {
122
            self::$debugText .= "symlinks : " . var_export(self::$_symlinks, 1) . "\n";
123
        }
124
        self::$debugText .= "\n";
125
 
126
        $css = self::_trimUrls($css);
127
 
128
        // rewrite
129
        $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/'
130
            ,array(self::$className, '_processUriCB'), $css);
131
        $css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
132
            ,array(self::$className, '_processUriCB'), $css);
133
 
134
        return $css;
135
    }
136
 
137
    /**
138
     * Prepend a path to relative URIs in CSS files
139
     *
140
     * @param string $css
141
     *
142
     * @param string $path The path to prepend.
143
     *
144
     * @return string
145
     */
146
    public static function prepend($css, $path)
147
    {
148
        self::$_prependPath = $path;
149
 
150
        $css = self::_trimUrls($css);
151
 
152
        // append
153
        $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/'
154
            ,array(self::$className, '_processUriCB'), $css);
155
        $css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
156
            ,array(self::$className, '_processUriCB'), $css);
157
 
158
        self::$_prependPath = null;
159
        return $css;
160
    }
161
 
162
 
163
    /**
164
     * @var string directory of this stylesheet
165
     */
166
    private static $_currentDir = '';
167
 
168
    /**
169
     * @var string DOC_ROOT
170
     */
171
    private static $_docRoot = '';
172
 
173
    /**
174
     * @var array directory replacements to map symlink targets back to their
175
     * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath'
176
     */
177
    private static $_symlinks = array();
178
 
179
    /**
180
     * @var string path to prepend
181
     */
182
    private static $_prependPath = null;
183
 
184
    private static function _trimUrls($css)
185
    {
186
        return preg_replace('/
187
            url\\(      # url(
188
            \\s*
189
            ([^\\)]+?)  # 1 = URI (assuming does not contain ")")
190
            \\s*
191
            \\)         # )
192
        /x', 'url($1)', $css);
193
    }
194
 
195
    private static function _processUriCB($m)
196
    {
197
        // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
198
        $isImport = ($m[0][0] === '@');
199
        // determine URI and the quote character (if any)
200
        if ($isImport) {
201
            $quoteChar = $m[1];
202
            $uri = $m[2];
203
        } else {
204
            // $m[1] is either quoted or not
205
            $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"')
206
                ? $m[1][0]
207
                : '';
208
            $uri = ($quoteChar === '')
209
                ? $m[1]
210
                : substr($m[1], 1, strlen($m[1]) - 2);
211
        }
212
        // analyze URI
213
        if ('/' !== $uri[0]                  // root-relative
214
            && false === strpos($uri, '//')  // protocol (non-data)
215
            && 0 !== strpos($uri, 'data:')   // data protocol
216
        ) {
217
            // URI is file-relative: rewrite depending on options
218
            $uri = (self::$_prependPath !== null)
219
                ? (self::$_prependPath . $uri)
220
                : self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks);
221
        }
222
        return $isImport
223
            ? "@import {$quoteChar}{$uri}{$quoteChar}"
224
            : "url({$quoteChar}{$uri}{$quoteChar})";
225
    }
226
 
227
    /**
228
     * Rewrite a file relative URI as root relative
229
     *
230
     * <code>
231
     * Minify_CSS_UriRewriter::rewriteRelative(
232
     *       '../img/hello.gif'
233
     *     , '/home/user/www/css'  // path of CSS file
234
     *     , '/home/user/www'      // doc root
235
     * );
236
     * // returns '/img/hello.gif'
237
     *
238
     * // example where static files are stored in a symlinked directory
239
     * Minify_CSS_UriRewriter::rewriteRelative(
240
     *       'hello.gif'
241
     *     , '/var/staticFiles/theme'
242
     *     , '/home/user/www'
243
     *     , array('/home/user/www/static' => '/var/staticFiles')
244
     * );
245
     * // returns '/static/theme/hello.gif'
246
     * </code>
247
     *
248
     * @param string $uri file relative URI
249
     *
250
     * @param string $realCurrentDir realpath of the current file's directory.
251
     *
252
     * @param string $realDocRoot realpath of the site document root.
253
     *
254
     * @param array $symlinks (default = array()) If the file is stored in
255
     * a symlink-ed directory, provide an array of link paths to
256
     * real target paths, where the link paths "appear" to be within the document
257
     * root. E.g.:
258
     * <code>
259
     * array('/home/foo/www/not/real/path' => '/real/target/path') // unix
260
     * array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path')  // Windows
261
     * </code>
262
     *
263
     * @return string
264
     */
265
    public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = array())
266
    {
267
        // prepend path with current dir separator (OS-independent)
268
        $path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR)
269
            . DIRECTORY_SEPARATOR . strtr($uri, '/', DIRECTORY_SEPARATOR);
270
 
271
        self::$debugText .= "file-relative URI  : {$uri}\n"
272
                          . "path prepended     : {$path}\n";
273
 
274
        // "unresolve" a symlink back to doc root
275
        foreach ($symlinks as $link => $target) {
276
            if (0 === strpos($path, $target)) {
277
                // replace $target with $link
278
                $path = $link . substr($path, strlen($target));
279
 
280
                self::$debugText .= "symlink unresolved : {$path}\n";
281
 
282
                break;
283
            }
284
        }
285
        // strip doc root
286
        $path = substr($path, strlen($realDocRoot));
287
 
288
        self::$debugText .= "docroot stripped   : {$path}\n";
289
 
290
        // fix to root-relative URI
291
 
292
        $uri = strtr($path, '/\\', '//');
293
 
294
        // remove /./ and /../ where possible
295
        $uri = str_replace('/./', '/', $uri);
296
        // inspired by patch from Oleg Cherniy
297
        do {
298
            $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed);
299
        } while ($changed);
300
 
301
        self::$debugText .= "traversals removed : {$uri}\n\n";
302
 
303
        return $uri;
304
    }
305
 
306
    /**
307
     * Get realpath with any trailing slash removed. If realpath() fails,
308
     * just remove the trailing slash.
309
     *
310
     * @param string $path
311
     *
312
     * @return mixed path with no trailing slash
313
     */
314
    protected static function _realpath($path)
315
    {
316
        $realPath = realpath($path);
317
        if ($realPath !== false) {
318
            $path = $realPath;
319
        }
320
        return rtrim($path, '/\\');
321
    }
322
}
323
 
324
 
325
class Minify_CommentPreserver {
326
 
327
    /**
328
     * String to be prepended to each preserved comment
329
     *
330
     * @var string
331
     */
332
    public static $prepend = "\n";
333
 
334
    /**
335
     * String to be appended to each preserved comment
336
     *
337
     * @var string
338
     */
339
    public static $append = "\n";
340
 
341
    /**
342
     * Process a string outside of C-style comments that begin with "/*!"
343
     *
344
     * On each non-empty string outside these comments, the given processor
345
     * function will be called. The first "!" will be removed from the
346
     * preserved comments, and the comments will be surrounded by
347
     * Minify_CommentPreserver::$preprend and Minify_CommentPreserver::$append.
348
     *
349
     * @param string $content
350
     * @param callback $processor function
351
     * @param array $args array of extra arguments to pass to the processor
352
     * function (default = array())
353
     * @return string
354
     */
355
    public static function process($content, $processor, $args = array())
356
    {
357
        $ret = '';
358
        while (true) {
359
            list($beforeComment, $comment, $afterComment) = self::_nextComment($content);
360
            if ('' !== $beforeComment) {
361
                $callArgs = $args;
362
                array_unshift($callArgs, $beforeComment);
363
                $ret .= call_user_func_array($processor, $callArgs);
364
            }
365
            if (false === $comment) {
366
                break;
367
            }
368
            $ret .= $comment;
369
            $content = $afterComment;
370
        }
371
        return $ret;
372
    }
373
 
374
    /**
375
     * Extract comments that YUI Compressor preserves.
376
     *
377
     * @param string $in input
378
     *
379
     * @return array 3 elements are returned. If a YUI comment is found, the
380
     * 2nd element is the comment and the 1st and 2nd are the surrounding
381
     * strings. If no comment is found, the entire string is returned as the
382
     * 1st element and the other two are false.
383
     */
384
    private static function _nextComment($in)
385
    {
386
        if (
387
            false === ($start = strpos($in, '/*!'))
388
            || false === ($end = strpos($in, '*/', $start + 3))
389
        ) {
390
            return array($in, false, false);
391
        }
392
        $ret = array(
393
            substr($in, 0, $start)
394
            ,self::$prepend . '/*' . substr($in, $start + 3, $end - $start - 1) . self::$append
395
        );
396
        $endChars = (strlen($in) - $end - 2);
397
        $ret[] = (0 === $endChars)
398
            ? ''
399
            : substr($in, -$endChars);
400
        return $ret;
401
    }
402
}
403
 
404
class Minify_CSS_Compressor {
405
 
406
    /**
407
     * Minify a CSS string
408
     *
409
     * @param string $css
410
     *
411
     * @param array $options (currently ignored)
412
     *
413
     * @return string
414
     */
415
    public static function process($css, $options = array())
416
    {
417
        $obj = new Minify_CSS_Compressor($options);
418
        return $obj->_process($css);
419
    }
420
 
421
    /**
422
     * @var array options
423
     */
424
    protected $_options = null;
425
 
426
    /**
427
     * @var bool Are we "in" a hack?
428
     *
429
     * I.e. are some browsers targetted until the next comment?
430
     */
431
    protected $_inHack = false;
432
 
433
 
434
    /**
435
     * Constructor
436
     *
437
     * @param array $options (currently ignored)
438
     *
439
     * @return null
440
     */
441
    private function __construct($options) {
442
        $this->_options = $options;
443
    }
444
 
445
    /**
446
     * Minify a CSS string
447
     *
448
     * @param string $css
449
     *
450
     * @return string
451
     */
452
    protected function _process($css)
453
    {
454
        $css = str_replace("\r\n", "\n", $css);
455
 
456
        // preserve empty comment after '>'
457
        // http://www.webdevout.net/css-hacks#in_css-selectors
458
        $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
459
 
460
        // preserve empty comment between property and value
461
        // http://css-discuss.incutio.com/?page=BoxModelHack
462
        $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
463
        $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);
464
 
465
        // apply callback to all valid comments (and strip out surrounding ws
466
        $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@'
467
            ,array($this, '_commentCB'), $css);
468
 
469
        // remove ws around { } and last semicolon in declaration block
470
        $css = preg_replace('/\\s*{\\s*/', '{', $css);
471
        $css = preg_replace('/;?\\s*}\\s*/', '}', $css);
472
 
473
        // remove ws surrounding semicolons
474
        $css = preg_replace('/\\s*;\\s*/', ';', $css);
475
 
476
        // remove ws around urls
477
        $css = preg_replace('/
478
                url\\(      # url(
479
                \\s*
480
                ([^\\)]+?)  # 1 = the URL (really just a bunch of non right parenthesis)
481
                \\s*
482
                \\)         # )
483
            /x', 'url($1)', $css);
484
 
485
        // remove ws between rules and colons
486
        $css = preg_replace('/
487
                \\s*
488
                ([{;])              # 1 = beginning of block or rule separator
489
                \\s*
490
                ([\\*_]?[\\w\\-]+)  # 2 = property (and maybe IE filter)
491
                \\s*
492
                :
493
                \\s*
494
                (\\b|[#\'"])        # 3 = first character of a value
495
            /x', '$1$2:$3', $css);
496
 
497
        // remove ws in selectors
498
        $css = preg_replace_callback('/
499
                (?:              # non-capture
500
                    \\s*
501
                    [^~>+,\\s]+  # selector part
502
                    \\s*
503
                    [,>+~]       # combinators
504
                )+
505
                \\s*
506
                [^~>+,\\s]+      # selector part
507
                {                # open declaration block
508
            /x'
509
            ,array($this, '_selectorsCB'), $css);
510
 
511
        // minimize hex colors
512
        $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i'
513
            , '$1#$2$3$4$5', $css);
514
 
515
        // remove spaces between font families
516
        $css = preg_replace_callback('/font-family:([^;}]+)([;}])/'
517
            ,array($this, '_fontFamilyCB'), $css);
518
 
519
        $css = preg_replace('/@import\\s+url/', '@import url', $css);
520
 
521
        // replace any ws involving newlines with a single newline
522
        $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);
523
 
524
        // separate common descendent selectors w/ newlines (to limit line lengths)
525
        $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);
526
 
527
        // Use newline after 1st numeric value (to limit line lengths).
528
        $css = preg_replace('/
529
            ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value
530
            \\s+
531
            /x'
532
            ,"$1\n", $css);
533
 
534
        // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/
535
        $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);
536
 
537
        return trim($css);
538
    }
539
 
540
    /**
541
     * Replace what looks like a set of selectors
542
     *
543
     * @param array $m regex matches
544
     *
545
     * @return string
546
     */
547
    protected function _selectorsCB($m)
548
    {
549
        // remove ws around the combinators
550
        return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
551
    }
552
 
553
    /**
554
     * Process a comment and return a replacement
555
     *
556
     * @param array $m regex matches
557
     *
558
     * @return string
559
     */
560
    protected function _commentCB($m)
561
    {
562
        $hasSurroundingWs = (trim($m[0]) !== $m[1]);
563
        $m = $m[1];
564
        // $m is the comment content w/o the surrounding tokens,
565
        // but the return value will replace the entire comment.
566
        if ($m === 'keep') {
567
            return '/**/';
568
        }
569
        if ($m === '" "') {
570
            // component of http://tantek.com/CSS/Examples/midpass.html
571
            return '/*" "*/';
572
        }
573
        if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {
574
            // component of http://tantek.com/CSS/Examples/midpass.html
575
            return '/*";}}/* */';
576
        }
577
        if ($this->_inHack) {
578
            // inversion: feeding only to one browser
579
            if (preg_match('@
580
                    ^/               # comment started like /*/
581
                    \\s*
582
                    (\\S[\\s\\S]+?)  # has at least some non-ws content
583
                    \\s*
584
                    /\\*             # ends like /*/ or /**/
585
                @x', $m, $n)) {
586
                // end hack mode after this comment, but preserve the hack and comment content
587
                $this->_inHack = false;
588
                return "/*/{$n[1]}/**/";
589
            }
590
        }
591
        if (substr($m, -1) === '\\') { // comment ends like \*/
592
            // begin hack mode and preserve hack
593
            $this->_inHack = true;
594
            return '/*\\*/';
595
        }
596
        if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */
597
            // begin hack mode and preserve hack
598
            $this->_inHack = true;
599
            return '/*/*/';
600
        }
601
        if ($this->_inHack) {
602
            // a regular comment ends hack mode but should be preserved
603
            $this->_inHack = false;
604
            return '/**/';
605
        }
606
        // Issue 107: if there's any surrounding whitespace, it may be important, so
607
        // replace the comment with a single space
608
        return $hasSurroundingWs // remove all other comments
609
            ? ' '
610
            : '';
611
    }
612
 
613
    /**
614
     * Process a font-family listing and return a replacement
615
     *
616
     * @param array $m regex matches
617
     *
618
     * @return string
619
     */
620
    protected function _fontFamilyCB($m)
621
    {
622
        $m[1] = preg_replace('/
623
                \\s*
624
                (
625
                    "[^"]+"      # 1 = family in double qutoes
626
                    |\'[^\']+\'  # or 1 = family in single quotes
627
                    |[\\w\\-]+   # or 1 = unquoted family
628
                )
629
                \\s*
630
            /x', '$1', $m[1]);
631
        return 'font-family:' . $m[1] . $m[2];
632
    }
633
}