├── .gitignore
├── composer.json
├── LICENSE
├── README.md
├── function.combine.php
└── minify
├── JSmin.php
└── CSSmin.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "name": "dead23angel/smarty-combine",
4 | "description": "Combine and minify many JS or CSS to one file.",
5 | "license": "MIT",
6 | "keywords": [
7 | "smarty",
8 | "minify"
9 | ],
10 | "authors": [
11 | {
12 | "name": "Vital Smereka",
13 | "homepage": "https://github.com/2nlin3",
14 | "role": "Developer"
15 | },
16 | {
17 | "name": "Gorochov Ivan",
18 | "homepage": "https://github.com/dead23angel",
19 | "role": "Developer"
20 | },
21 | {
22 | "name": "Leif Neland",
23 | "homepage": "https://github.com/leifnel",
24 | "role": "Developer"
25 | }
26 | ],
27 | "require": {
28 | "php": ">=5.6.40",
29 | "smarty/smarty": "~3.1|~4.1"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Philipp Tkachev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Smarty Combine
2 | ==============
3 |
4 | Combine plugin allows concatenating several js or css files into one.
5 | It can be useful for big projects with a lot of several small CSS and JS files.
6 |
7 | ### Usage examples
8 |
9 | **Template inline example Smarty 3 or 4.1.0**
10 |
11 | ```{combine input=array('/bm.js','/bm2.js') output='/cache/big.js' use_true_path=false age='30' debug=false}```
12 |
13 | **Smarty 2 example**
14 |
15 | **PHP code**
16 |
17 | ```$js_filelist = array('/js/core.js','/js/slideviewer.js');```
18 |
19 | ```$smarty_object->assign('js_files', $js_filelist);```
20 |
21 | **Template code**
22 |
23 | ```{combine input=$js_files output='/cache/big.js' use_true_path=false age='30' debug=false}```
24 |
25 | **The plugin has 4 parameters:**
26 | * **input** - must be an array, containing list with absolute pathes to files. In Smarty 3 it can be inline array, for Smarty 2 you will need to pass from yours controller a variable, which will contains this array.
27 | * **output** - absolute path to output file. Directory must be writable to www daemon (Usually chmod 777 resolve this problem :)
28 | * **use_true_path** - value is a boolean. If it is set to true the plugin will use the paths of the asset files as they are but if set to false it will assume the files have relative paths. By default it is set to false. You can omit this parameter.
29 | * **age** - value of seconds between checks when original files were changed. By default it is 3600 - one hour. You can omit this parameter.
30 | * **debug** - parameter in the value of TRUE, disable compilation useful for debugging when developing a site.. By default it is FALSE. You can omit this parameter.
31 |
32 | Basedir of css-file is added to relative paths in url()
33 |
34 | A semicolon is added at the end of .js-files, as it is not needed at the end
35 | of a file, but is needed between concatenated files.
36 |
--------------------------------------------------------------------------------
/function.combine.php:
--------------------------------------------------------------------------------
1 |
13 | * Name: combine
14 | * Date: September 5, 2015
15 | * Purpose: Combine content from several JS or CSS files into one
16 | * Input: string to count
17 | * Example: {combine input=$array_of_files_to_combine output=$path_to_output_file use_true_path=true age=$seconds_to_try_recombine_file}
18 | *
19 | * @author Gorochov Ivan
20 | * @author Vital Smereka
21 | * @version 1.4
22 | * @param array
23 | * @param string
24 | * @param int
25 | * @return string
26 | */
27 |
28 | function smarty_function_combine($params, &$smarty)
29 | {
30 | require_once dirname(__FILE__) . '/minify/JSmin.php';
31 | require_once dirname(__FILE__) . '/minify/CSSmin.php';
32 |
33 | /**
34 | * Build combined file
35 | *
36 | * @param array $params
37 | */
38 | if ( ! function_exists('smarty_build_combine')) {
39 | function smarty_build_combine($params, $skip_out_for_shutdown = false)
40 | {
41 | $filelist = array();
42 | $lastest_mtime = 0;
43 |
44 | foreach ($params['input'] as $item) {
45 | $mtime = filemtime($params['file_path'] . $item);
46 | $lastest_mtime = max($lastest_mtime, $mtime);
47 | $filelist[] = array('name' => $item, 'time' => $mtime);
48 | }
49 |
50 | if ($params['debug'] === true) {
51 | $output_filename = '';
52 | foreach ($filelist as $file) {
53 | if ($params['type'] == 'js') {
54 | $output_filename .= '' . "\n";
55 | } elseif ($params['type'] == 'css') {
56 | $output_filename .= '' . "\n";
57 | }
58 | }
59 |
60 | echo $output_filename;
61 | return;
62 | }
63 |
64 | $last_cmtime = 0;
65 |
66 | if (file_exists($params['file_path'] . $params['cache_file_name'])) {
67 | $last_cmtime = filemtime($params['file_path'] . $params['cache_file_name']);
68 | }
69 |
70 | if ($lastest_mtime > $last_cmtime) {
71 | $glob_mask = preg_replace('/\.(js|css)$/i', '_*.$1', $params['output']);
72 | $files_to_cleanup = glob($params['file_path'] . $glob_mask);
73 |
74 | foreach ($files_to_cleanup as $cfile) {
75 | if (is_file($cfile) && file_exists($cfile)) {
76 | unlink($cfile);
77 | }
78 | }
79 |
80 | $output_filename = preg_replace('/\.(js|css)$/i', date('_YmdHis.', $lastest_mtime) . '$1', $params['output']);
81 |
82 | $dirname = dirname($params['file_path'] . $output_filename);
83 |
84 | if ( ! is_dir($dirname)) {
85 | mkdir($dirname, 0755, true);
86 | }
87 |
88 | $sleep = checkWritabeFile($params['file_path'] . $output_filename);
89 |
90 | $fh = fopen($params['file_path'] . $output_filename, 'w');
91 |
92 | if (flock($fh, LOCK_EX)) {
93 | foreach ($filelist as $file) {
94 | $min = '';
95 |
96 | $dirname = dirname(str_replace($_SERVER['DOCUMENT_ROOT'],'',$params['file_path'] . $file['name']));
97 |
98 | if ($params['type'] == 'js') {
99 | $min = JSMin::minify(file_get_contents($params['file_path'] . $file['name'])) . ";" ;
100 | } elseif ($params['type'] == 'css') {
101 | $min = CSSMin::minify(preg_replace('/url\\(((?>["\']?))(?!(\\/|http(s)?:|data:|#))(.*?)\\1\\)/', 'url("' . $dirname . '/$4")', file_get_contents($params['file_path'] . $file['name'])));
102 | } else {
103 | fputs($fh, PHP_EOL . PHP_EOL . '/* ' . $file['name'] . ' @ ' . date('c', $file['time']) . ' */' . PHP_EOL . PHP_EOL);
104 | $min = file_get_contents($params['file_path'] . $file['name']);
105 | }
106 |
107 | fputs($fh, $min);
108 | }
109 |
110 | flock($fh, LOCK_UN);
111 | file_put_contents($params['file_path'] . $params['cache_file_name'], $lastest_mtime, LOCK_EX);
112 | }
113 |
114 | fclose($fh);
115 | clearstatcache();
116 | }
117 |
118 | touch($params['file_path'] . $params['cache_file_name'], $lastest_mtime);
119 |
120 | if(!$skip_out_for_shutdown){
121 | smarty_print_out($params);
122 | }
123 | }
124 | }
125 |
126 | /**
127 | * Print filename
128 | *
129 | * @param string $params
130 | */
131 | if ( ! function_exists('smarty_print_out')) {
132 | function smarty_print_out($params)
133 | {
134 | $mtime = 0;
135 |
136 | if (file_exists($params['file_path'] . $params['cache_file_name'])) {
137 | $mtime = filemtime($params['file_path'] . $params['cache_file_name']);
138 | }
139 |
140 | $output_filename = preg_replace('/\.(js|css)$/i', date('_YmdHis.', $mtime) . '$1', $params['output']);
141 |
142 | if ($params['type'] == 'js') {
143 | echo '';
144 | } elseif ($params['type'] == 'css') {
145 | echo '';
146 | } else {
147 | echo $output_filename;
148 | }
149 | }
150 | }
151 |
152 | /**
153 | * This function gets the base url for the project where this plugin is used
154 | * If this plugin is used within Code Igniter, the base_url() would have already been defined
155 | */
156 | if ( ! function_exists('base_url')) {
157 | function base_url(){
158 |
159 | return sprintf(
160 | "%s://%s%s",
161 | isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off' ? 'https' : 'http',
162 | $_SERVER['HTTP_HOST'],
163 | rtrim(dirname($_SERVER['PHP_SELF']), '/\\')
164 | );
165 | }
166 | }
167 |
168 | // The new 'use_true_path' option that tells this plugin to use the path to the files as it is
169 | if ( isset($params['use_true_path']) && !is_bool($params['use_true_path'])) {
170 | trigger_error('use_true_path must be boolean', E_USER_NOTICE);
171 | return;
172 | }
173 |
174 | if ( ! isset($params['use_true_path'])) {
175 | $params['use_true_path'] = false;
176 | }
177 |
178 | // use the relative path or the true path of the file based on the 'use_true_path' option passed in
179 | $params['file_path'] = ($params['use_true_path']) ? '' : getenv('DOCUMENT_ROOT');
180 |
181 |
182 | if ( ! isset($params['input'])) {
183 | trigger_error('input cannot be empty', E_USER_NOTICE);
184 | return;
185 | }
186 |
187 | if ( ! is_array($params['input']) || count($params['input']) < 1) {
188 | trigger_error('input must be array and have one item at least', E_USER_NOTICE);
189 | return;
190 | }
191 |
192 | foreach ($params['input'] as $file) {
193 | if ( ! file_exists($params['file_path'] . $file)) {
194 | trigger_error('File ' . $params['file_path'] . $file . ' does not exist!', E_USER_WARNING);
195 | return;
196 | }
197 |
198 | $ext = pathinfo($file, PATHINFO_EXTENSION);
199 |
200 | if ( ! in_array($ext, array('js', 'css'))) {
201 | trigger_error('all input files must have js or css extension', E_USER_NOTICE);
202 | return;
203 | }
204 |
205 | $files_extensions[] = $ext;
206 | }
207 |
208 | if (count(array_unique($files_extensions)) > 1) {
209 | trigger_error('all input files must have the same extension', E_USER_NOTICE);
210 | return;
211 | }
212 |
213 | $params['type'] = $ext;
214 |
215 | if ( ! isset($params['output'])) {
216 | $params['output'] = dirname($params['input'][0]) . '/combined.' . $ext;
217 | }
218 |
219 | if ( ! isset($params['age'])) {
220 | $params['age'] = 3600;
221 | }
222 |
223 | if ( ! isset($params['cache_file_name'])) {
224 | $params['cache_file_name'] = $params['output'] . '.cache';
225 | }
226 |
227 | if ( ! isset($params['debug'])) {
228 | $params['debug'] = false;
229 | }
230 |
231 | /** Build combine in background fastcgi_finish_request() */
232 | if ( ! function_exists('build_cache_combine')) {
233 | function build_cache_combine($params){
234 | register_shutdown_function(function($params){
235 | ignore_user_abort(true);
236 |
237 | if (function_exists('fastcgi_finish_request')) {
238 | fastcgi_finish_request();
239 | }
240 |
241 | smarty_build_combine($params, true);
242 | }, $params);
243 | }
244 | }
245 |
246 | $file_cache_exists = file_exists($params['file_path'] . $params['cache_file_name']);
247 |
248 | $cache_mtime = $file_cache_exists ? filemtime($params['file_path'] . $params['cache_file_name']) : 0;
249 |
250 | if ($params['debug'] === true || !$file_cache_exists) {
251 | $time = time();
252 |
253 | if($cache_mtime + $params['age'] < $time) {
254 | $filelist = array();
255 |
256 | foreach ($params['input'] as $item) {
257 | $filelist[] = ['name' => $item];
258 | }
259 |
260 | $out = '';
261 | foreach ($filelist as $file) {
262 | if ($params['type'] == 'js') {
263 | $out .= '' . "\n";
264 | } elseif ($params['type'] == 'css') {
265 | $out .= '' . "\n";
266 | }
267 | }
268 |
269 | echo $out;
270 |
271 | //smarty_build_combine($params);
272 | register_shutdown_function('build_cache_combine', $params);
273 |
274 | return;
275 | }
276 | }
277 |
278 | smarty_print_out($params);
279 | }
280 |
281 | function checkWritabeFile($file)
282 | {
283 | $i = 0;
284 |
285 | while(!is_writable($file))
286 | {
287 | $i++;
288 |
289 | if($i > 5)
290 | {
291 | break;
292 | }
293 |
294 | sleep(rand(0,2));
295 | }
296 |
297 | return $i;
298 | }
299 |
--------------------------------------------------------------------------------
/minify/JSmin.php:
--------------------------------------------------------------------------------
1 |
6 | * $minifiedJs = JSMin::minify($js);
7 | *
8 | *
9 | * This is a modified port of jsmin.c. Improvements:
10 | *
11 | * Does not choke on some regexp literals containing quote characters. E.g. /'/
12 | *
13 | * Spaces are preserved after some add/sub operators, so they are not mistakenly
14 | * converted to post-inc/dec. E.g. a + ++b -> a+ ++b
15 | *
16 | * Preserves multi-line comments that begin with /*!
17 | *
18 | * PHP 5 or higher is required.
19 | *
20 | * Permission is hereby granted to use this version of the library under the
21 | * same terms as jsmin.c, which has the following license:
22 | *
23 | * --
24 | * Copyright (c) 2002 Douglas Crockford (www.crockford.com)
25 | *
26 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
27 | * this software and associated documentation files (the "Software"), to deal in
28 | * the Software without restriction, including without limitation the rights to
29 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
30 | * of the Software, and to permit persons to whom the Software is furnished to do
31 | * so, subject to the following conditions:
32 | *
33 | * The above copyright notice and this permission notice shall be included in all
34 | * copies or substantial portions of the Software.
35 | *
36 | * The Software shall be used for Good, not Evil.
37 | *
38 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
44 | * SOFTWARE.
45 | * --
46 | *
47 | * @package JSMin
48 | * @author Ryan Grove (PHP port)
49 | * @author Steve Clay (modifications + cleanup)
50 | * @author Andrea Giammarchi (spaceBeforeRegExp)
51 | * @copyright 2002 Douglas Crockford (jsmin.c)
52 | * @copyright 2008 Ryan Grove (PHP port)
53 | * @license http://opensource.org/licenses/mit-license.php MIT License
54 | * @link http://code.google.com/p/jsmin-php/
55 | */
56 |
57 | class JSMin
58 | {
59 | const ORD_LF = 10;
60 | const ORD_SPACE = 32;
61 | const ACTION_KEEP_A = 1;
62 | const ACTION_DELETE_A = 2;
63 | const ACTION_DELETE_A_B = 3;
64 |
65 | protected $a = "\n";
66 | protected $b = '';
67 | protected $input = '';
68 | protected $inputIndex = 0;
69 | protected $inputLength = 0;
70 | protected $lookAhead = null;
71 | protected $output = '';
72 | protected $lastByteOut = '';
73 | protected $keptComment = '';
74 |
75 | /**
76 | * Minify Javascript.
77 | *
78 | * @param string $js Javascript to be minified
79 | *
80 | * @return string
81 | */
82 | public static function minify($js)
83 | {
84 | $jsmin = new JSMin($js);
85 | return $jsmin->min();
86 | }
87 |
88 | /**
89 | * @param string $input
90 | */
91 | public function __construct($input)
92 | {
93 | $this->input = $input;
94 | }
95 |
96 | /**
97 | * Perform minification, return result
98 | *
99 | * @return string
100 | */
101 | public function min()
102 | {
103 | if ($this->output !== '') { // min already run
104 | return $this->output;
105 | }
106 |
107 | $mbIntEnc = null;
108 | if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) {
109 | $mbIntEnc = mb_internal_encoding();
110 | mb_internal_encoding('8bit');
111 | }
112 | $this->input = str_replace("\r\n", "\n", $this->input);
113 | $this->inputLength = strlen($this->input);
114 |
115 | $this->action(self::ACTION_DELETE_A_B);
116 |
117 | while ($this->a !== null) {
118 | // determine next command
119 | $command = self::ACTION_KEEP_A; // default
120 | if ($this->a === ' ') {
121 | if (($this->lastByteOut === '+' || $this->lastByteOut === '-')
122 | && ($this->b === $this->lastByteOut)) {
123 | // Don't delete this space. If we do, the addition/subtraction
124 | // could be parsed as a post-increment
125 | } elseif (! $this->isAlphaNum($this->b)) {
126 | $command = self::ACTION_DELETE_A;
127 | }
128 | } elseif ($this->a === "\n") {
129 | if ($this->b === ' ') {
130 | $command = self::ACTION_DELETE_A_B;
131 |
132 | // in case of mbstring.func_overload & 2, must check for null b,
133 | // otherwise mb_strpos will give WARNING
134 | } elseif ($this->b === null
135 | || (false === strpos('{[(+-!~', $this->b)
136 | && ! $this->isAlphaNum($this->b))) {
137 | $command = self::ACTION_DELETE_A;
138 | }
139 | } elseif (! $this->isAlphaNum($this->a)) {
140 | if ($this->b === ' '
141 | || ($this->b === "\n"
142 | && (false === strpos('}])+-"\'', $this->a)))) {
143 | $command = self::ACTION_DELETE_A_B;
144 | }
145 | }
146 | $this->action($command);
147 | }
148 | $this->output = trim($this->output);
149 |
150 | if ($mbIntEnc !== null) {
151 | mb_internal_encoding($mbIntEnc);
152 | }
153 | return $this->output;
154 | }
155 |
156 | /**
157 | * ACTION_KEEP_A = Output A. Copy B to A. Get the next B.
158 | * ACTION_DELETE_A = Copy B to A. Get the next B.
159 | * ACTION_DELETE_A_B = Get the next B.
160 | *
161 | * @param int $command
162 | * @throws JSMin_UnterminatedRegExpException|JSMin_UnterminatedStringException
163 | */
164 | protected function action($command)
165 | {
166 | // make sure we don't compress "a + ++b" to "a+++b", etc.
167 | if ($command === self::ACTION_DELETE_A_B
168 | && $this->b === ' '
169 | && ($this->a === '+' || $this->a === '-')) {
170 | // Note: we're at an addition/substraction operator; the inputIndex
171 | // will certainly be a valid index
172 | if ($this->input[$this->inputIndex] === $this->a) {
173 | // This is "+ +" or "- -". Don't delete the space.
174 | $command = self::ACTION_KEEP_A;
175 | }
176 | }
177 |
178 | switch ($command) {
179 | case self::ACTION_KEEP_A: // 1
180 | $this->output .= $this->a;
181 |
182 | if ($this->keptComment) {
183 | $this->output = rtrim($this->output, "\n");
184 | $this->output .= $this->keptComment;
185 | $this->keptComment = '';
186 | }
187 |
188 | $this->lastByteOut = $this->a;
189 |
190 | // fallthrough intentional
191 | case self::ACTION_DELETE_A: // 2
192 | $this->a = $this->b;
193 | if ($this->a === "'" || $this->a === '"') { // string literal
194 | $str = $this->a; // in case needed for exception
195 | for (;;) {
196 | $this->output .= $this->a;
197 | $this->lastByteOut = $this->a;
198 |
199 | $this->a = $this->get();
200 | if ($this->a === $this->b) { // end quote
201 | break;
202 | }
203 | if ($this->isEOF($this->a)) {
204 | throw new JSMin_UnterminatedStringException(
205 | "JSMin: Unterminated String at byte {$this->inputIndex}: {$str}");
206 | }
207 | $str .= $this->a;
208 | if ($this->a === '\\') {
209 | $this->output .= $this->a;
210 | $this->lastByteOut = $this->a;
211 |
212 | $this->a = $this->get();
213 | $str .= $this->a;
214 | }
215 | }
216 | }
217 |
218 | // fallthrough intentional
219 | case self::ACTION_DELETE_A_B: // 3
220 | $this->b = $this->next();
221 | if ($this->b === '/' && $this->isRegexpLiteral()) {
222 | $this->output .= $this->a . $this->b;
223 | $pattern = '/'; // keep entire pattern in case we need to report it in the exception
224 | for (;;) {
225 | $this->a = $this->get();
226 | $pattern .= $this->a;
227 | if ($this->a === '[') {
228 | for (;;) {
229 | $this->output .= $this->a;
230 | $this->a = $this->get();
231 | $pattern .= $this->a;
232 | if ($this->a === ']') {
233 | break;
234 | }
235 | if ($this->a === '\\') {
236 | $this->output .= $this->a;
237 | $this->a = $this->get();
238 | $pattern .= $this->a;
239 | }
240 | if ($this->isEOF($this->a)) {
241 | throw new JSMin_UnterminatedRegExpException(
242 | "JSMin: Unterminated set in RegExp at byte "
243 | . $this->inputIndex .": {$pattern}");
244 | }
245 | }
246 | }
247 |
248 | if ($this->a === '/') { // end pattern
249 | break; // while (true)
250 | } elseif ($this->a === '\\') {
251 | $this->output .= $this->a;
252 | $this->a = $this->get();
253 | $pattern .= $this->a;
254 | } elseif ($this->isEOF($this->a)) {
255 | throw new JSMin_UnterminatedRegExpException(
256 | "JSMin: Unterminated RegExp at byte {$this->inputIndex}: {$pattern}");
257 | }
258 | $this->output .= $this->a;
259 | $this->lastByteOut = $this->a;
260 | }
261 | $this->b = $this->next();
262 | }
263 | // end case ACTION_DELETE_A_B
264 | }
265 | }
266 |
267 | /**
268 | * @return bool
269 | */
270 | protected function isRegexpLiteral()
271 | {
272 | if (false !== strpos("(,=:[!&|?+-~*{;", $this->a)) {
273 | // we obviously aren't dividing
274 | return true;
275 | }
276 | if ($this->a === ' ' || $this->a === "\n") {
277 | $length = strlen($this->output);
278 | if ($length < 2) { // weird edge case
279 | return true;
280 | }
281 | // you can't divide a keyword
282 | if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m)) {
283 | if ($this->output === $m[0]) { // odd but could happen
284 | return true;
285 | }
286 | // make sure it's a keyword, not end of an identifier
287 | $charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1);
288 | if (! $this->isAlphaNum($charBeforeKeyword)) {
289 | return true;
290 | }
291 | }
292 | }
293 | return false;
294 | }
295 |
296 | /**
297 | * Return the next character from stdin. Watch out for lookahead. If the character is a control character,
298 | * translate it to a space or linefeed.
299 | *
300 | * @return string
301 | */
302 | protected function get()
303 | {
304 | $c = $this->lookAhead;
305 | $this->lookAhead = null;
306 | if ($c === null) {
307 | // getc(stdin)
308 | if ($this->inputIndex < $this->inputLength) {
309 | $c = $this->input[$this->inputIndex];
310 | $this->inputIndex += 1;
311 | } else {
312 | $c = null;
313 | }
314 | }
315 | if (ord($c) >= self::ORD_SPACE || $c === "\n" || $c === null) {
316 | return $c;
317 | }
318 | if ($c === "\r") {
319 | return "\n";
320 | }
321 | return ' ';
322 | }
323 |
324 | /**
325 | * Does $a indicate end of input?
326 | *
327 | * @param string $a
328 | * @return bool
329 | */
330 | protected function isEOF($a)
331 | {
332 | return ord($a) <= self::ORD_LF;
333 | }
334 |
335 | /**
336 | * Get next char (without getting it). If is ctrl character, translate to a space or newline.
337 | *
338 | * @return string
339 | */
340 | protected function peek()
341 | {
342 | $this->lookAhead = $this->get();
343 | return $this->lookAhead;
344 | }
345 |
346 | /**
347 | * Return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character.
348 | *
349 | * @param string $c
350 | *
351 | * @return bool
352 | */
353 | protected function isAlphaNum($c)
354 | {
355 | return (preg_match('/^[a-z0-9A-Z_\\$\\\\]$/', $c) || ord($c) > 126);
356 | }
357 |
358 | /**
359 | * Consume a single line comment from input (possibly retaining it)
360 | */
361 | protected function consumeSingleLineComment()
362 | {
363 | $comment = '';
364 | while (true) {
365 | $get = $this->get();
366 | $comment .= $get;
367 | if (ord($get) <= self::ORD_LF) { // end of line reached
368 | // if IE conditional comment
369 | if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
370 | $this->keptComment .= "/{$comment}";
371 | }
372 | return;
373 | }
374 | }
375 | }
376 |
377 | /**
378 | * Consume a multiple line comment from input (possibly retaining it)
379 | *
380 | * @throws JSMin_UnterminatedCommentException
381 | */
382 | protected function consumeMultipleLineComment()
383 | {
384 | $this->get();
385 | $comment = '';
386 | for (;;) {
387 | $get = $this->get();
388 | if ($get === '*') {
389 | if ($this->peek() === '/') { // end of comment reached
390 | $this->get();
391 | if (0 === strpos($comment, '!')) {
392 | // preserved by YUI Compressor
393 | if (!$this->keptComment) {
394 | // don't prepend a newline if two comments right after one another
395 | $this->keptComment = "\n";
396 | }
397 | $this->keptComment .= "/*!" . substr($comment, 1) . "*/\n";
398 | } elseif (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
399 | // IE conditional
400 | $this->keptComment .= "/*{$comment}*/";
401 | }
402 | return;
403 | }
404 | } elseif ($get === null) {
405 | throw new JSMin_UnterminatedCommentException(
406 | "JSMin: Unterminated comment at byte {$this->inputIndex}: /*{$comment}");
407 | }
408 | $comment .= $get;
409 | }
410 | }
411 |
412 | /**
413 | * Get the next character, skipping over comments. Some comments may be preserved.
414 | *
415 | * @return string
416 | */
417 | protected function next()
418 | {
419 | $get = $this->get();
420 | if ($get === '/') {
421 | switch ($this->peek()) {
422 | case '/':
423 | $this->consumeSingleLineComment();
424 | $get = "\n";
425 | break;
426 | case '*':
427 | $this->consumeMultipleLineComment();
428 | $get = ' ';
429 | break;
430 | }
431 | }
432 | return $get;
433 | }
434 | }
435 |
436 | class JSMin_UnterminatedStringException extends Exception
437 | {
438 | }
439 | class JSMin_UnterminatedCommentException extends Exception
440 | {
441 | }
442 | class JSMin_UnterminatedRegExpException extends Exception
443 | {
444 | }
445 |
--------------------------------------------------------------------------------
/minify/CSSmin.php:
--------------------------------------------------------------------------------
1 | run($css, $linebreak_pos);
51 | }
52 |
53 | /**
54 | * @param bool|int $raise_php_limits
55 | * If true, PHP settings will be raised if needed
56 | */
57 | public function __construct($raise_php_limits = true)
58 | {
59 | // Set suggested PHP limits
60 | $this->memory_limit = 128 * 1048576; // 128MB in bytes
61 | $this->max_execution_time = 60; // 1 min
62 | $this->pcre_backtrack_limit = 1000 * 1000;
63 | $this->pcre_recursion_limit = 500 * 1000;
64 |
65 | $this->raise_php_limits = (bool) $raise_php_limits;
66 | }
67 |
68 | /**
69 | * Minify a string of CSS
70 | * @param string $css
71 | * @param int|bool $linebreak_pos
72 | * @return string
73 | */
74 | public function run($css = '', $linebreak_pos = false)
75 | {
76 | if (empty($css)) {
77 | return '';
78 | }
79 |
80 | if ($this->raise_php_limits) {
81 | $this->do_raise_php_limits();
82 | }
83 |
84 | $this->comments = array();
85 | $this->preserved_tokens = array();
86 |
87 | $start_index = 0;
88 | $length = strlen($css);
89 |
90 | $css = $this->extract_data_urls($css);
91 |
92 | // collect all comment blocks...
93 | while (($start_index = $this->index_of($css, '/*', $start_index)) >= 0) {
94 | $end_index = $this->index_of($css, '*/', $start_index + 2);
95 | if ($end_index < 0) {
96 | $end_index = $length;
97 | }
98 | $comment_found = $this->str_slice($css, $start_index + 2, $end_index);
99 | $this->comments[] = $comment_found;
100 | $comment_preserve_string = self::COMMENT . (count($this->comments) - 1) . '___';
101 | $css = $this->str_slice($css, 0, $start_index + 2) . $comment_preserve_string . $this->str_slice($css, $end_index);
102 | // Set correct start_index: Fixes issue #2528130
103 | $start_index = $end_index + 2 + strlen($comment_preserve_string) - strlen($comment_found);
104 | }
105 |
106 | // preserve strings so their content doesn't get accidentally minified
107 | $css = preg_replace_callback('/(?:"(?:[^\\\\"]|\\\\.|\\\\)*")|'."(?:'(?:[^\\\\']|\\\\.|\\\\)*')/S", array($this, 'replace_string'), $css);
108 |
109 | // Let's divide css code in chunks of 25.000 chars aprox.
110 | // Reason: PHP's PCRE functions like preg_replace have a "backtrack limit"
111 | // of 100.000 chars by default (php < 5.3.7) so if we're dealing with really
112 | // long strings and a (sub)pattern matches a number of chars greater than
113 | // the backtrack limit number (i.e. /(.*)/s) PCRE functions may fail silently
114 | // returning NULL and $css would be empty.
115 | $charset = '';
116 | $charset_regexp = '/(@charset)( [^;]+;)/i';
117 | $css_chunks = array();
118 | $css_chunk_length = 25000; // aprox size, not exact
119 | $start_index = 0;
120 | $i = $css_chunk_length; // save initial iterations
121 | $l = strlen($css);
122 |
123 |
124 | // if the number of characters is 25000 or less, do not chunk
125 | if ($l <= $css_chunk_length) {
126 | $css_chunks[] = $css;
127 | } else {
128 | // chunk css code securely
129 | while ($i < $l) {
130 | $i += 50; // save iterations. 500 checks for a closing curly brace }
131 | if ($l - $start_index <= $css_chunk_length || $i >= $l) {
132 | $css_chunks[] = $this->str_slice($css, $start_index);
133 | break;
134 | }
135 | if ($css[$i - 1] === '}' && $i - $start_index > $css_chunk_length) {
136 | // If there are two ending curly braces }} separated or not by spaces,
137 | // join them in the same chunk (i.e. @media blocks)
138 | $next_chunk = substr($css, $i);
139 | if (preg_match('/^\s*\}/', $next_chunk)) {
140 | $i = $i + $this->index_of($next_chunk, '}') + 1;
141 | }
142 |
143 | $css_chunks[] = $this->str_slice($css, $start_index, $i);
144 | $start_index = $i;
145 | }
146 | }
147 | }
148 |
149 | // Minify each chunk
150 | for ($i = 0, $n = count($css_chunks); $i < $n; $i++) {
151 | $css_chunks[$i] = $this->min($css_chunks[$i], $linebreak_pos);
152 | // Keep the first @charset at-rule found
153 | if (empty($charset) && preg_match($charset_regexp, $css_chunks[$i], $matches)) {
154 | $charset = strtolower($matches[1]) . $matches[2];
155 | }
156 | // Delete all @charset at-rules
157 | $css_chunks[$i] = preg_replace($charset_regexp, '', $css_chunks[$i]);
158 | }
159 |
160 | // Update the first chunk and push the charset to the top of the file.
161 | $css_chunks[0] = $charset . $css_chunks[0];
162 |
163 | return implode('', $css_chunks);
164 | }
165 |
166 | /**
167 | * Sets the memory limit for this script
168 | * @param int|string $limit
169 | */
170 | public function set_memory_limit($limit)
171 | {
172 | $this->memory_limit = $this->normalize_int($limit);
173 | }
174 |
175 | /**
176 | * Sets the maximum execution time for this script
177 | * @param int|string $seconds
178 | */
179 | public function set_max_execution_time($seconds)
180 | {
181 | $this->max_execution_time = (int) $seconds;
182 | }
183 |
184 | /**
185 | * Sets the PCRE backtrack limit for this script
186 | * @param int $limit
187 | */
188 | public function set_pcre_backtrack_limit($limit)
189 | {
190 | $this->pcre_backtrack_limit = (int) $limit;
191 | }
192 |
193 | /**
194 | * Sets the PCRE recursion limit for this script
195 | * @param int $limit
196 | */
197 | public function set_pcre_recursion_limit($limit)
198 | {
199 | $this->pcre_recursion_limit = (int) $limit;
200 | }
201 |
202 | /**
203 | * Try to configure PHP to use at least the suggested minimum settings
204 | */
205 | private function do_raise_php_limits()
206 | {
207 | $php_limits = array(
208 | 'memory_limit' => $this->memory_limit,
209 | 'max_execution_time' => $this->max_execution_time,
210 | 'pcre.backtrack_limit' => $this->pcre_backtrack_limit,
211 | 'pcre.recursion_limit' => $this->pcre_recursion_limit
212 | );
213 |
214 | // If current settings are higher respect them.
215 | foreach ($php_limits as $name => $suggested) {
216 | $current = $this->normalize_int(ini_get($name));
217 | // memory_limit exception: allow -1 for "no memory limit".
218 | if ($current > -1 && ($suggested == -1 || $current < $suggested)) {
219 | ini_set($name, $suggested);
220 | }
221 | }
222 | }
223 |
224 | /**
225 | * Does bulk of the minification
226 | * @param string $css
227 | * @param int|bool $linebreak_pos
228 | * @return string
229 | */
230 | private function min($css, $linebreak_pos)
231 | {
232 | // strings are safe, now wrestle the comments
233 | for ($i = 0, $max = count($this->comments); $i < $max; $i++) {
234 | $token = $this->comments[$i];
235 | $placeholder = '/' . self::COMMENT . $i . '___/';
236 |
237 | // ! in the first position of the comment means preserve
238 | // so push to the preserved tokens keeping the !
239 | if (substr($token, 0, 1) === '!') {
240 | $this->preserved_tokens[] = $token;
241 | $token_tring = self::TOKEN . (count($this->preserved_tokens) - 1) . '___';
242 | $css = preg_replace($placeholder, $token_tring, $css, 1);
243 | // Preserve new lines for /*! important comments
244 | $css = preg_replace('/\s*[\n\r\f]+\s*(\/\*'. $token_tring .')/S', self::NL.'$1', $css);
245 | $css = preg_replace('/('. $token_tring .'\*\/)\s*[\n\r\f]+\s*/', '$1'.self::NL, $css);
246 | continue;
247 | }
248 |
249 | // \ in the last position looks like hack for Mac/IE5
250 | // shorten that to /*\*/ and the next one to /**/
251 | if (substr($token, (strlen($token) - 1), 1) === '\\') {
252 | $this->preserved_tokens[] = '\\';
253 | $css = preg_replace($placeholder, self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
254 | $i = $i + 1; // attn: advancing the loop
255 | $this->preserved_tokens[] = '';
256 | $css = preg_replace('/' . self::COMMENT . $i . '___/', self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
257 | continue;
258 | }
259 |
260 | // keep empty comments after child selectors (IE7 hack)
261 | // e.g. html >/**/ body
262 | if (strlen($token) === 0) {
263 | $start_index = $this->index_of($css, $this->str_slice($placeholder, 1, -1));
264 | if ($start_index > 2) {
265 | if (substr($css, $start_index - 3, 1) === '>') {
266 | $this->preserved_tokens[] = '';
267 | $css = preg_replace($placeholder, self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
268 | }
269 | }
270 | }
271 |
272 | // in all other cases kill the comment
273 | $css = preg_replace('/\/\*' . $this->str_slice($placeholder, 1, -1) . '\*\//', '', $css, 1);
274 | }
275 |
276 |
277 | // Normalize all whitespace strings to single spaces. Easier to work with that way.
278 | $css = preg_replace('/\s+/', ' ', $css);
279 |
280 | // Shorten & preserve calculations calc(...) since spaces are important
281 | $css = preg_replace_callback('/calc(\(((?:[^\(\)]+|(?1))*)\))/i', array($this, 'replace_calc'), $css);
282 |
283 | // Replace positive sign from numbers preceded by : or a white-space before the leading space is removed
284 | // +1.2em to 1.2em, +.8px to .8px, +2% to 2%
285 | $css = preg_replace('/((? -9.0 to -9
296 | $css = preg_replace('/((?\+\(\)\]\~\=,])/', '$1', $css);
308 |
309 | // Restore spaces for !important
310 | $css = preg_replace('/\!important/i', ' !important', $css);
311 |
312 | // bring back the colon
313 | $css = preg_replace('/' . self::CLASSCOLON . '/', ':', $css);
314 |
315 | // retain space for special IE6 cases
316 | $css = preg_replace_callback('/\:first\-(line|letter)(\{|,)/i', array($this, 'lowercase_pseudo_first'), $css);
317 |
318 | // no space after the end of a preserved comment
319 | $css = preg_replace('/\*\/ /', '*/', $css);
320 |
321 | // lowercase some popular @directives
322 | $css = preg_replace_callback('/@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/i', array($this, 'lowercase_directives'), $css);
323 |
324 | // lowercase some more common pseudo-elements
325 | $css = preg_replace_callback('/:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/i', array($this, 'lowercase_pseudo_elements'), $css);
326 |
327 | // lowercase some more common functions
328 | $css = preg_replace_callback('/:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\(/i', array($this, 'lowercase_common_functions'), $css);
329 |
330 | // lower case some common function that can be values
331 | // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us
332 | $css = preg_replace_callback('/([:,\( ]\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)/iS', array($this, 'lowercase_common_functions_values'), $css);
333 |
334 | // Put the space back in some cases, to support stuff like
335 | // @media screen and (-webkit-min-device-pixel-ratio:0){
336 | $css = preg_replace('/\band\(/i', 'and (', $css);
337 |
338 | // Remove the spaces after the things that should not have spaces after them.
339 | $css = preg_replace('/([\!\{\}\:;\>\+\(\[\~\=,])\s+/S', '$1', $css);
340 |
341 | // remove unnecessary semicolons
342 | $css = preg_replace('/;+\}/', '}', $css);
343 |
344 | // Fix for issue: #2528146
345 | // Restore semicolon if the last property is prefixed with a `*` (lte IE7 hack)
346 | // to avoid issues on Symbian S60 3.x browsers.
347 | $css = preg_replace('/(\*[a-z0-9\-]+\s*\:[^;\}]+)(\})/', '$1;$2', $css);
348 |
349 | // Replace 0 length units 0(px,em,%) with 0.
350 | $css = preg_replace('/(^|[^0-9])(?:0?\.)?0(?:em|ex|ch|rem|vw|vh|vm|vmin|cm|mm|in|px|pt|pc|%|deg|g?rad|m?s|k?hz)/iS', '${1}0', $css);
351 |
352 | // Replace 0 0; or 0 0 0; or 0 0 0 0; with 0.
353 | $css = preg_replace('/\:0(?: 0){1,3}(;|\}| \!)/', ':0$1', $css);
354 |
355 | // Fix for issue: #2528142
356 | // Replace text-shadow:0; with text-shadow:0 0 0;
357 | $css = preg_replace('/(text-shadow\:0)(;|\}| \!)/i', '$1 0 0$2', $css);
358 |
359 | // Replace background-position:0; with background-position:0 0;
360 | // same for transform-origin
361 | // Changing -webkit-mask-position: 0 0 to just a single 0 will result in the second parameter defaulting to 50% (center)
362 | $css = preg_replace('/(background\-position|webkit-mask-position|(?:webkit|moz|o|ms|)\-?transform\-origin)\:0(;|\}| \!)/iS', '$1:0 0$2', $css);
363 |
364 | // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space)
365 | // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space)
366 | // This makes it more likely that it'll get further compressed in the next step.
367 | $css = preg_replace_callback('/rgb\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'rgb_to_hex'), $css);
368 | $css = preg_replace_callback('/hsl\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'hsl_to_hex'), $css);
369 |
370 | // Shorten colors from #AABBCC to #ABC or short color name.
371 | $css = $this->compress_hex_colors($css);
372 |
373 | // border: none to border:0, outline: none to outline:0
374 | $css = preg_replace('/(border\-?(?:top|right|bottom|left|)|outline)\:none(;|\}| \!)/iS', '$1:0$2', $css);
375 |
376 | // shorter opacity IE filter
377 | $css = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $css);
378 |
379 | // Find a fraction that is used for Opera's -o-device-pixel-ratio query
380 | // Add token to add the "\" back in later
381 | $css = preg_replace('/\(([a-z\-]+):([0-9]+)\/([0-9]+)\)/i', '($1:$2'. self::QUERY_FRACTION .'$3)', $css);
382 |
383 | // Remove empty rules.
384 | $css = preg_replace('/[^\};\{\/]+\{\}/S', '', $css);
385 |
386 | // Add "/" back to fix Opera -o-device-pixel-ratio query
387 | $css = preg_replace('/'. self::QUERY_FRACTION .'/', '/', $css);
388 |
389 | // Some source control tools don't like it when files containing lines longer
390 | // than, say 8000 characters, are checked in. The linebreak option is used in
391 | // that case to split long lines after a specific column.
392 | if ($linebreak_pos !== false && (int) $linebreak_pos >= 0) {
393 | $linebreak_pos = (int) $linebreak_pos;
394 | $start_index = $i = 0;
395 | while ($i < strlen($css)) {
396 | $i++;
397 | if ($css[$i - 1] === '}' && $i - $start_index > $linebreak_pos) {
398 | $css = $this->str_slice($css, 0, $i) . "\n" . $this->str_slice($css, $i);
399 | $start_index = $i;
400 | }
401 | }
402 | }
403 |
404 | // Replace multiple semi-colons in a row by a single one
405 | // See SF bug #1980989
406 | $css = preg_replace('/;;+/', ';', $css);
407 |
408 | // Restore new lines for /*! important comments
409 | $css = preg_replace('/'. self::NL .'/', "\n", $css);
410 |
411 | // Lowercase all uppercase properties
412 | $css = preg_replace_callback('/(\{|\;)([A-Z\-]+)(\:)/', array($this, 'lowercase_properties'), $css);
413 |
414 | // restore preserved comments and strings
415 | for ($i = 0, $max = count($this->preserved_tokens); $i < $max; $i++) {
416 | $css = preg_replace('/' . self::TOKEN . $i . '___/', $this->preserved_tokens[$i], $css, 1);
417 | }
418 |
419 | // Trim the final string (for any leading or trailing white spaces)
420 | return trim($css);
421 | }
422 |
423 | /**
424 | * Utility method to replace all data urls with tokens before we start
425 | * compressing, to avoid performance issues running some of the subsequent
426 | * regexes against large strings chunks.
427 | *
428 | * @param string $css
429 | * @return string
430 | */
431 | private function extract_data_urls($css)
432 | {
433 | // Leave data urls alone to increase parse performance.
434 | $max_index = strlen($css) - 1;
435 | $append_index = $index = $last_index = $offset = 0;
436 | $sb = array();
437 | $pattern = '/url\(\s*(["\']?)data\:/i';
438 |
439 | // Since we need to account for non-base64 data urls, we need to handle
440 | // ' and ) being part of the data string. Hence switching to indexOf,
441 | // to determine whether or not we have matching string terminators and
442 | // handling sb appends directly, instead of using matcher.append* methods.
443 |
444 | while (preg_match($pattern, $css, $m, 0, $offset)) {
445 | $index = $this->index_of($css, $m[0], $offset);
446 | $last_index = $index + strlen($m[0]);
447 | $start_index = $index + 4; // "url(".length()
448 | $end_index = $last_index - 1;
449 | $terminator = $m[1]; // ', " or empty (not quoted)
450 | $found_terminator = false;
451 |
452 | if (strlen($terminator) === 0) {
453 | $terminator = ')';
454 | }
455 |
456 | while ($found_terminator === false && $end_index+1 <= $max_index) {
457 | $end_index = $this->index_of($css, $terminator, $end_index + 1);
458 |
459 | // endIndex == 0 doesn't really apply here
460 | if ($end_index > 0 && substr($css, $end_index - 1, 1) !== '\\') {
461 | $found_terminator = true;
462 | if (')' != $terminator) {
463 | $end_index = $this->index_of($css, ')', $end_index);
464 | }
465 | }
466 | }
467 |
468 | // Enough searching, start moving stuff over to the buffer
469 | $sb[] = $this->str_slice($css, $append_index, $index);
470 |
471 | if ($found_terminator) {
472 | $token = $this->str_slice($css, $start_index, $end_index);
473 | $token = preg_replace('/\s+/', '', $token);
474 | $this->preserved_tokens[] = $token;
475 |
476 | $preserver = 'url(' . self::TOKEN . (count($this->preserved_tokens) - 1) . '___)';
477 | $sb[] = $preserver;
478 |
479 | $append_index = $end_index + 1;
480 | } else {
481 | // No end terminator found, re-add the whole match. Should we throw/warn here?
482 | $sb[] = $this->str_slice($css, $index, $last_index);
483 | $append_index = $last_index;
484 | }
485 |
486 | $offset = $last_index;
487 | }
488 |
489 | $sb[] = $this->str_slice($css, $append_index);
490 |
491 | return implode('', $sb);
492 | }
493 |
494 | /**
495 | * Utility method to compress hex color values of the form #AABBCC to #ABC or short color name.
496 | *
497 | * DOES NOT compress CSS ID selectors which match the above pattern (which would break things).
498 | * e.g. #AddressForm { ... }
499 | *
500 | * DOES NOT compress IE filters, which have hex color values (which would break things).
501 | * e.g. filter: chroma(color="#FFFFFF");
502 | *
503 | * DOES NOT compress invalid hex values.
504 | * e.g. background-color: #aabbccdd
505 | *
506 | * @param string $css
507 | * @return string
508 | */
509 | private function compress_hex_colors($css)
510 | {
511 | // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters)
512 | $pattern = '/(\=\s*?["\']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/iS';
513 | $_index = $index = $last_index = $offset = 0;
514 | $sb = array();
515 | // See: http://ajaxmin.codeplex.com/wikipage?title=CSS%20Colors
516 | $short_safe = array(
517 | '#808080' => 'gray',
518 | '#008000' => 'green',
519 | '#800000' => 'maroon',
520 | '#000080' => 'navy',
521 | '#808000' => 'olive',
522 | '#ffa500' => 'orange',
523 | '#800080' => 'purple',
524 | '#c0c0c0' => 'silver',
525 | '#008080' => 'teal',
526 | '#f00' => 'red'
527 | );
528 |
529 | while (preg_match($pattern, $css, $m, 0, $offset)) {
530 | $index = $this->index_of($css, $m[0], $offset);
531 | $last_index = $index + strlen($m[0]);
532 | $is_filter = $m[1] !== null && $m[1] !== '';
533 |
534 | $sb[] = $this->str_slice($css, $_index, $index);
535 |
536 | if ($is_filter) {
537 | // Restore, maintain case, otherwise filter will break
538 | $sb[] = $m[1] . '#' . $m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7];
539 | } else {
540 | if (strtolower($m[2]) == strtolower($m[3]) &&
541 | strtolower($m[4]) == strtolower($m[5]) &&
542 | strtolower($m[6]) == strtolower($m[7])) {
543 | // Compress.
544 | $hex = '#' . strtolower($m[3] . $m[5] . $m[7]);
545 | } else {
546 | // Non compressible color, restore but lower case.
547 | $hex = '#' . strtolower($m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]);
548 | }
549 | // replace Hex colors to short safe color names
550 | $sb[] = array_key_exists($hex, $short_safe) ? $short_safe[$hex] : $hex;
551 | }
552 |
553 | $_index = $offset = $last_index - strlen($m[8]);
554 | }
555 |
556 | $sb[] = $this->str_slice($css, $_index);
557 |
558 | return implode('', $sb);
559 | }
560 |
561 | /* CALLBACKS
562 | * ---------------------------------------------------------------------------------------------
563 | */
564 |
565 | private function replace_string($matches)
566 | {
567 | $match = $matches[0];
568 | $quote = substr($match, 0, 1);
569 | // Must use addcslashes in PHP to avoid parsing of backslashes
570 | $match = addcslashes($this->str_slice($match, 1, -1), '\\');
571 |
572 | // maybe the string contains a comment-like substring?
573 | // one, maybe more? put'em back then
574 | if (($pos = $this->index_of($match, self::COMMENT)) >= 0) {
575 | for ($i = 0, $max = count($this->comments); $i < $max; $i++) {
576 | $match = preg_replace('/' . self::COMMENT . $i . '___/', $this->comments[$i], $match, 1);
577 | }
578 | }
579 |
580 | // minify alpha opacity in filter strings
581 | $match = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $match);
582 |
583 | $this->preserved_tokens[] = $match;
584 | return $quote . self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . $quote;
585 | }
586 |
587 | private function replace_colon($matches)
588 | {
589 | return preg_replace('/\:/', self::CLASSCOLON, $matches[0]);
590 | }
591 |
592 | private function replace_calc($matches)
593 | {
594 | $this->preserved_tokens[] = trim(preg_replace('/\s*([\*\/\(\),])\s*/', '$1', $matches[2]));
595 | return 'calc('. self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . ')';
596 | }
597 |
598 | private function rgb_to_hex($matches)
599 | {
600 | // Support for percentage values rgb(100%, 0%, 45%);
601 | if ($this->index_of($matches[1], '%') >= 0) {
602 | $rgbcolors = explode(',', str_replace('%', '', $matches[1]));
603 | for ($i = 0; $i < count($rgbcolors); $i++) {
604 | $rgbcolors[$i] = $this->round_number(floatval($rgbcolors[$i]) * 2.55);
605 | }
606 | } else {
607 | $rgbcolors = explode(',', $matches[1]);
608 | }
609 |
610 | // Values outside the sRGB color space should be clipped (0-255)
611 | for ($i = 0; $i < count($rgbcolors); $i++) {
612 | $rgbcolors[$i] = $this->clamp_number(intval($rgbcolors[$i], 10), 0, 255);
613 | $rgbcolors[$i] = sprintf("%02x", $rgbcolors[$i]);
614 | }
615 |
616 | // Fix for issue #2528093
617 | if (!preg_match('/[\s\,\);\}]/', $matches[2])) {
618 | $matches[2] = ' ' . $matches[2];
619 | }
620 |
621 | return '#' . implode('', $rgbcolors) . $matches[2];
622 | }
623 |
624 | private function hsl_to_hex($matches)
625 | {
626 | $values = explode(',', str_replace('%', '', $matches[1]));
627 | $h = floatval($values[0]);
628 | $s = floatval($values[1]);
629 | $l = floatval($values[2]);
630 |
631 | // Wrap and clamp, then fraction!
632 | $h = ((($h % 360) + 360) % 360) / 360;
633 | $s = $this->clamp_number($s, 0, 100) / 100;
634 | $l = $this->clamp_number($l, 0, 100) / 100;
635 |
636 | if ($s == 0) {
637 | $r = $g = $b = $this->round_number(255 * $l);
638 | } else {
639 | $v2 = $l < 0.5 ? $l * (1 + $s) : ($l + $s) - ($s * $l);
640 | $v1 = (2 * $l) - $v2;
641 | $r = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h + (1/3)));
642 | $g = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h));
643 | $b = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h - (1/3)));
644 | }
645 |
646 | return $this->rgb_to_hex(array('', $r.','.$g.','.$b, $matches[2]));
647 | }
648 |
649 | private function lowercase_pseudo_first($matches)
650 | {
651 | return ':first-'. strtolower($matches[1]) .' '. $matches[2];
652 | }
653 |
654 | private function lowercase_directives($matches)
655 | {
656 | return '@'. strtolower($matches[1]);
657 | }
658 |
659 | private function lowercase_pseudo_elements($matches)
660 | {
661 | return ':'. strtolower($matches[1]);
662 | }
663 |
664 | private function lowercase_common_functions($matches)
665 | {
666 | return ':'. strtolower($matches[1]) .'(';
667 | }
668 |
669 | private function lowercase_common_functions_values($matches)
670 | {
671 | return $matches[1] . strtolower($matches[2]);
672 | }
673 |
674 | private function lowercase_properties($matches)
675 | {
676 | return $matches[1].strtolower($matches[2]).$matches[3];
677 | }
678 |
679 | /* HELPERS
680 | * ---------------------------------------------------------------------------------------------
681 | */
682 |
683 | private function hue_to_rgb($v1, $v2, $vh)
684 | {
685 | $vh = $vh < 0 ? $vh + 1 : ($vh > 1 ? $vh - 1 : $vh);
686 | if ($vh * 6 < 1) {
687 | return $v1 + ($v2 - $v1) * 6 * $vh;
688 | }
689 | if ($vh * 2 < 1) {
690 | return $v2;
691 | }
692 | if ($vh * 3 < 2) {
693 | return $v1 + ($v2 - $v1) * ((2/3) - $vh) * 6;
694 | }
695 | return $v1;
696 | }
697 |
698 | private function round_number($n)
699 | {
700 | return intval(floor(floatval($n) + 0.5), 10);
701 | }
702 |
703 | private function clamp_number($n, $min, $max)
704 | {
705 | return min(max($n, $min), $max);
706 | }
707 |
708 | /**
709 | * PHP port of Javascript's "indexOf" function for strings only
710 | * Author: Tubal Martin http://blog.margenn.com
711 | *
712 | * @param string $haystack
713 | * @param string $needle
714 | * @param int $offset index (optional)
715 | * @return int
716 | */
717 | private function index_of($haystack, $needle, $offset = 0)
718 | {
719 | $index = strpos($haystack, $needle, $offset);
720 |
721 | return ($index !== false) ? $index : -1;
722 | }
723 |
724 | /**
725 | * PHP port of Javascript's "slice" function for strings only
726 | * Author: Tubal Martin http://blog.margenn.com
727 | * Tests: http://margenn.com/tubal/str_slice/
728 | *
729 | * @param string $str
730 | * @param int $start index
731 | * @param int|bool $end index (optional)
732 | * @return string
733 | */
734 | private function str_slice($str, $start = 0, $end = false)
735 | {
736 | if ($end !== false && ($start < 0 || $end <= 0)) {
737 | $max = strlen($str);
738 |
739 | if ($start < 0) {
740 | if (($start = $max + $start) < 0) {
741 | return '';
742 | }
743 | }
744 |
745 | if ($end < 0) {
746 | if (($end = $max + $end) < 0) {
747 | return '';
748 | }
749 | }
750 |
751 | if ($end <= $start) {
752 | return '';
753 | }
754 | }
755 |
756 | $slice = ($end === false) ? substr($str, $start) : substr($str, $start, $end - $start);
757 | return ($slice === false) ? '' : $slice;
758 | }
759 |
760 | /**
761 | * Convert strings like "64M" or "30" to int values
762 | * @param mixed $size
763 | * @return int
764 | */
765 | private function normalize_int($size)
766 | {
767 | if (is_string($size)) {
768 | switch (substr($size, -1)) {
769 | case 'M': case 'm': return (int) substr($size, 0, -1) * 1048576;
770 | case 'K': case 'k': return (int) substr($size, 0, -1) * 1024;
771 | case 'G': case 'g': return (int) substr($size, 0, -1) * 1073741824;
772 | }
773 | }
774 |
775 | return (int) $size;
776 | }
777 | }
778 |
--------------------------------------------------------------------------------