]>
Commit | Line | Data |
---|---|---|
04150375 AD |
1 | <?php |
2 | /** | |
3 | * jsmin.php - PHP implementation of Douglas Crockford's JSMin. | |
4 | * | |
5 | * This is pretty much a direct port of jsmin.c to PHP with just a few | |
6 | * PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and | |
7 | * outputs to stdout, this library accepts a string as input and returns another | |
8 | * string as output. | |
9 | * | |
10 | * PHP 5 or higher is required. | |
11 | * | |
12 | * Permission is hereby granted to use this version of the library under the | |
13 | * same terms as jsmin.c, which has the following license: | |
14 | * | |
15 | * -- | |
16 | * Copyright (c) 2002 Douglas Crockford (www.crockford.com) | |
17 | * | |
18 | * Permission is hereby granted, free of charge, to any person obtaining a copy of | |
19 | * this software and associated documentation files (the "Software"), to deal in | |
20 | * the Software without restriction, including without limitation the rights to | |
21 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |
22 | * of the Software, and to permit persons to whom the Software is furnished to do | |
23 | * so, subject to the following conditions: | |
24 | * | |
25 | * The above copyright notice and this permission notice shall be included in all | |
26 | * copies or substantial portions of the Software. | |
27 | * | |
28 | * The Software shall be used for Good, not Evil. | |
29 | * | |
30 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
31 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
32 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
33 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
34 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
35 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
36 | * SOFTWARE. | |
37 | * -- | |
38 | * | |
39 | * @package JSMin | |
40 | * @author Ryan Grove <ryan@wonko.com> | |
41 | * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c) | |
42 | * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port) | |
43 | * @license http://opensource.org/licenses/mit-license.php MIT License | |
44 | * @version 1.1.1 (2008-03-02) | |
45 | * @link https://github.com/rgrove/jsmin-php/ | |
46 | */ | |
47 | ||
48 | class JSMin { | |
49 | const ORD_LF = 10; | |
50 | const ORD_SPACE = 32; | |
51 | const ACTION_KEEP_A = 1; | |
52 | const ACTION_DELETE_A = 2; | |
53 | const ACTION_DELETE_A_B = 3; | |
54 | ||
55 | protected $a = ''; | |
56 | protected $b = ''; | |
57 | protected $input = ''; | |
58 | protected $inputIndex = 0; | |
59 | protected $inputLength = 0; | |
60 | protected $lookAhead = null; | |
61 | protected $output = ''; | |
62 | ||
63 | // -- Public Static Methods -------------------------------------------------- | |
64 | ||
65 | /** | |
66 | * Minify Javascript | |
67 | * | |
68 | * @uses __construct() | |
69 | * @uses min() | |
70 | * @param string $js Javascript to be minified | |
71 | * @return string | |
72 | */ | |
73 | public static function minify($js) { | |
74 | $jsmin = new JSMin($js); | |
75 | return $jsmin->min(); | |
76 | } | |
77 | ||
78 | // -- Public Instance Methods ------------------------------------------------ | |
79 | ||
80 | /** | |
81 | * Constructor | |
82 | * | |
83 | * @param string $input Javascript to be minified | |
84 | */ | |
85 | public function __construct($input) { | |
86 | $this->input = str_replace("\r\n", "\n", $input); | |
87 | $this->inputLength = strlen($this->input); | |
88 | } | |
89 | ||
90 | // -- Protected Instance Methods --------------------------------------------- | |
91 | ||
92 | /** | |
93 | * Action -- do something! What to do is determined by the $command argument. | |
94 | * | |
95 | * action treats a string as a single character. Wow! | |
96 | * action recognizes a regular expression if it is preceded by ( or , or =. | |
97 | * | |
98 | * @uses next() | |
99 | * @uses get() | |
100 | * @throws JSMinException If parser errors are found: | |
101 | * - Unterminated string literal | |
102 | * - Unterminated regular expression set in regex literal | |
103 | * - Unterminated regular expression literal | |
104 | * @param int $command One of class constants: | |
105 | * ACTION_KEEP_A Output A. Copy B to A. Get the next B. | |
106 | * ACTION_DELETE_A Copy B to A. Get the next B. (Delete A). | |
107 | * ACTION_DELETE_A_B Get the next B. (Delete B). | |
108 | */ | |
109 | protected function action($command) { | |
110 | switch($command) { | |
111 | case self::ACTION_KEEP_A: | |
112 | $this->output .= $this->a; | |
113 | ||
114 | case self::ACTION_DELETE_A: | |
115 | $this->a = $this->b; | |
116 | ||
117 | if ($this->a === "'" || $this->a === '"') { | |
118 | for (;;) { | |
119 | $this->output .= $this->a; | |
120 | $this->a = $this->get(); | |
121 | ||
122 | if ($this->a === $this->b) { | |
123 | break; | |
124 | } | |
125 | ||
126 | if (ord($this->a) <= self::ORD_LF) { | |
127 | throw new JSMinException('Unterminated string literal.'); | |
128 | } | |
129 | ||
130 | if ($this->a === '\\') { | |
131 | $this->output .= $this->a; | |
132 | $this->a = $this->get(); | |
133 | } | |
134 | } | |
135 | } | |
136 | ||
137 | case self::ACTION_DELETE_A_B: | |
138 | $this->b = $this->next(); | |
139 | ||
140 | if ($this->b === '/' && ( | |
141 | $this->a === '(' || $this->a === ',' || $this->a === '=' || | |
142 | $this->a === ':' || $this->a === '[' || $this->a === '!' || | |
143 | $this->a === '&' || $this->a === '|' || $this->a === '?' || | |
144 | $this->a === '{' || $this->a === '}' || $this->a === ';' || | |
145 | $this->a === "\n" )) { | |
146 | ||
147 | $this->output .= $this->a . $this->b; | |
148 | ||
149 | for (;;) { | |
150 | $this->a = $this->get(); | |
151 | ||
152 | if ($this->a === '[') { | |
153 | /* | |
154 | inside a regex [...] set, which MAY contain a '/' itself. Example: mootools Form.Validator near line 460: | |
155 | return Form.Validator.getValidator('IsEmpty').test(element) || (/^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]\.?){0,63}[a-z0-9!#$%&'*+/=?^_`{|}~-]@(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\])$/i).test(element.get('value')); | |
156 | */ | |
157 | for (;;) { | |
158 | $this->output .= $this->a; | |
159 | $this->a = $this->get(); | |
160 | ||
161 | if ($this->a === ']') { | |
162 | break; | |
163 | } elseif ($this->a === '\\') { | |
164 | $this->output .= $this->a; | |
165 | $this->a = $this->get(); | |
166 | } elseif (ord($this->a) <= self::ORD_LF) { | |
167 | throw new JSMinException('Unterminated regular expression set in regex literal.'); | |
168 | } | |
169 | } | |
170 | } elseif ($this->a === '/') { | |
171 | break; | |
172 | } elseif ($this->a === '\\') { | |
173 | $this->output .= $this->a; | |
174 | $this->a = $this->get(); | |
175 | } elseif (ord($this->a) <= self::ORD_LF) { | |
176 | throw new JSMinException('Unterminated regular expression literal.'); | |
177 | } | |
178 | ||
179 | $this->output .= $this->a; | |
180 | } | |
181 | ||
182 | $this->b = $this->next(); | |
183 | } | |
184 | } | |
185 | } | |
186 | ||
187 | /** | |
188 | * Get next char. Convert ctrl char to space. | |
189 | * | |
190 | * @return string|null | |
191 | */ | |
192 | protected function get() { | |
193 | $c = $this->lookAhead; | |
194 | $this->lookAhead = null; | |
195 | ||
196 | if ($c === null) { | |
197 | if ($this->inputIndex < $this->inputLength) { | |
198 | $c = substr($this->input, $this->inputIndex, 1); | |
199 | $this->inputIndex += 1; | |
200 | } else { | |
201 | $c = null; | |
202 | } | |
203 | } | |
204 | ||
205 | if ($c === "\r") { | |
206 | return "\n"; | |
207 | } | |
208 | ||
209 | if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) { | |
210 | return $c; | |
211 | } | |
212 | ||
213 | return ' '; | |
214 | } | |
215 | ||
216 | /** | |
217 | * Is $c a letter, digit, underscore, dollar sign, or non-ASCII character. | |
218 | * | |
219 | * @return bool | |
220 | */ | |
221 | protected function isAlphaNum($c) { | |
222 | return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1; | |
223 | } | |
224 | ||
225 | /** | |
226 | * Perform minification, return result | |
227 | * | |
228 | * @uses action() | |
229 | * @uses isAlphaNum() | |
230 | * @return string | |
231 | */ | |
232 | protected function min() { | |
233 | $this->a = "\n"; | |
234 | $this->action(self::ACTION_DELETE_A_B); | |
235 | ||
236 | while ($this->a !== null) { | |
237 | switch ($this->a) { | |
238 | case ' ': | |
239 | if ($this->isAlphaNum($this->b)) { | |
240 | $this->action(self::ACTION_KEEP_A); | |
241 | } else { | |
242 | $this->action(self::ACTION_DELETE_A); | |
243 | } | |
244 | break; | |
245 | ||
246 | case "\n": | |
247 | switch ($this->b) { | |
248 | case '{': | |
249 | case '[': | |
250 | case '(': | |
251 | case '+': | |
252 | case '-': | |
253 | $this->action(self::ACTION_KEEP_A); | |
254 | break; | |
255 | ||
256 | case ' ': | |
257 | $this->action(self::ACTION_DELETE_A_B); | |
258 | break; | |
259 | ||
260 | default: | |
261 | if ($this->isAlphaNum($this->b)) { | |
262 | $this->action(self::ACTION_KEEP_A); | |
263 | } | |
264 | else { | |
265 | $this->action(self::ACTION_DELETE_A); | |
266 | } | |
267 | } | |
268 | break; | |
269 | ||
270 | default: | |
271 | switch ($this->b) { | |
272 | case ' ': | |
273 | if ($this->isAlphaNum($this->a)) { | |
274 | $this->action(self::ACTION_KEEP_A); | |
275 | break; | |
276 | } | |
277 | ||
278 | $this->action(self::ACTION_DELETE_A_B); | |
279 | break; | |
280 | ||
281 | case "\n": | |
282 | switch ($this->a) { | |
283 | case '}': | |
284 | case ']': | |
285 | case ')': | |
286 | case '+': | |
287 | case '-': | |
288 | case '"': | |
289 | case "'": | |
290 | $this->action(self::ACTION_KEEP_A); | |
291 | break; | |
292 | ||
293 | default: | |
294 | if ($this->isAlphaNum($this->a)) { | |
295 | $this->action(self::ACTION_KEEP_A); | |
296 | } | |
297 | else { | |
298 | $this->action(self::ACTION_DELETE_A_B); | |
299 | } | |
300 | } | |
301 | break; | |
302 | ||
303 | default: | |
304 | $this->action(self::ACTION_KEEP_A); | |
305 | break; | |
306 | } | |
307 | } | |
308 | } | |
309 | ||
310 | return $this->output; | |
311 | } | |
312 | ||
313 | /** | |
314 | * Get the next character, skipping over comments. peek() is used to see | |
315 | * if a '/' is followed by a '/' or '*'. | |
316 | * | |
317 | * @uses get() | |
318 | * @uses peek() | |
319 | * @throws JSMinException On unterminated comment. | |
320 | * @return string | |
321 | */ | |
322 | protected function next() { | |
323 | $c = $this->get(); | |
324 | ||
325 | if ($c === '/') { | |
326 | switch($this->peek()) { | |
327 | case '/': | |
328 | for (;;) { | |
329 | $c = $this->get(); | |
330 | ||
331 | if (ord($c) <= self::ORD_LF) { | |
332 | return $c; | |
333 | } | |
334 | } | |
335 | ||
336 | case '*': | |
337 | $this->get(); | |
338 | ||
339 | for (;;) { | |
340 | switch($this->get()) { | |
341 | case '*': | |
342 | if ($this->peek() === '/') { | |
343 | $this->get(); | |
344 | return ' '; | |
345 | } | |
346 | break; | |
347 | ||
348 | case null: | |
349 | throw new JSMinException('Unterminated comment.'); | |
350 | } | |
351 | } | |
352 | ||
353 | default: | |
354 | return $c; | |
355 | } | |
356 | } | |
357 | ||
358 | return $c; | |
359 | } | |
360 | ||
361 | /** | |
362 | * Get next char. If is ctrl character, translate to a space or newline. | |
363 | * | |
364 | * @uses get() | |
365 | * @return string|null | |
366 | */ | |
367 | protected function peek() { | |
368 | $this->lookAhead = $this->get(); | |
369 | return $this->lookAhead; | |
370 | } | |
371 | } | |
372 | ||
373 | // -- Exceptions --------------------------------------------------------------- | |
374 | class JSMinException extends Exception {} | |
375 | ?> |