Fix for #1231
[jquery.git] / build / js / pack.js
1 /*
2     packer, version 2.0.2 (2005-08-19)
3     Copyright 2004-2005, Dean Edwards
4     License: http://creativecommons.org/licenses/LGPL/2.1/
5 */
6
7 function pack(_script, _encoding, _fastDecode, _specialChars) {
8     // constants
9     var $IGNORE = "$1";
10
11     // validate parameters
12     _script += "\n";
13     _encoding = Math.min(parseInt(_encoding), 95);
14
15     // apply all parsing routines
16     function _pack($script) {
17         var i, $parse;
18         for (i = 0; ($parse = _parsers[i]); i++) {
19             $script = $parse($script);
20         }
21         return $script;
22     };
23
24     // unpacking function - this is the boot strap function
25     //  data extracted from this packing routine is passed to
26     //  this function when decoded in the target
27     var _unpack = function($packed, $ascii, $count, $keywords, $encode, $decode) {
28         while ($count--)
29             if ($keywords[$count])
30                 $packed = $packed.replace(new RegExp('\\b' + $encode($count) + '\\b', 'g'), $keywords[$count]);
31         return $packed;
32     };
33
34     // code-snippet inserted into the unpacker to speed up decoding
35     var _decode = function() {
36         // does the browser support String.replace where the
37         //  replacement value is a function?
38         if (!''.replace(/^/, String)) {
39             // decode all the values we need
40             while ($count--) $decode[$encode($count)] = $keywords[$count] || $encode($count);
41             // global replacement function
42             $keywords = [function($encoded){return $decode[$encoded]}];
43             // generic match
44             $encode = function(){return'\\w+'};
45             // reset the loop counter -  we are now doing a global replace
46             $count = 1;
47         }
48     };
49
50     // keep a list of parsing functions, they'll be executed all at once
51     var _parsers = [];
52     function _addParser($parser) {
53         _parsers[_parsers.length] = $parser;
54     };
55
56     // zero encoding - just removal of white space and comments
57     function _basicCompression($script) {
58         var $parser = new ParseMaster;
59         // make safe
60         $parser.escapeChar = "\\";
61         // protect strings
62         $parser.add(/'[^'\n\r]*'/, $IGNORE);
63         $parser.add(/"[^"\n\r]*"/, $IGNORE);
64         // remove comments
65         $parser.add(/\/\/[^\n\r]*[\n\r]/, " ");
66         $parser.add(/\/\*[^*]*\*+([^\/][^*]*\*+)*\//, " ");
67         // protect regular expressions
68         $parser.add(/\s+(\/[^\/\n\r\*][^\/\n\r]*\/g?i?)/, "$2"); // IGNORE
69         $parser.add(/[^\w\x24\/'"*)\?:]\/[^\/\n\r\*][^\/\n\r]*\/g?i?/, $IGNORE);
70         // remove: ;;; doSomething();
71         if (_specialChars) $parser.add(/;;;[^\n\r]+[\n\r]/);
72         // remove redundant semi-colons
73         $parser.add(/\(;;\)/, $IGNORE); // protect for (;;) loops
74         $parser.add(/;+\s*([};])/, "$2");
75         // apply the above
76         $script = $parser.exec($script);
77
78         // remove white-space
79         $parser.add(/(\b|\x24)\s+(\b|\x24)/, "$2 $3");
80         $parser.add(/([+\-])\s+([+\-])/, "$2 $3");
81         $parser.add(/\s+/, "");
82         // done
83         return $parser.exec($script);
84     };
85
86     function _encodeSpecialChars($script) {
87         var $parser = new ParseMaster;
88         // replace: $name -> n, $$name -> na
89         $parser.add(/((\x24+)([a-zA-Z$_]+))(\d*)/, function($match, $offset) {
90             var $length = $match[$offset + 2].length;
91             var $start = $length - Math.max($length - $match[$offset + 3].length, 0);
92             return $match[$offset + 1].substr($start, $length) + $match[$offset + 4];
93         });
94         // replace: _name -> _0, double-underscore (__name) is ignored
95         var $regexp = /\b_[A-Za-z\d]\w*/;
96         // build the word list
97         var $keywords = _analyze($script, _globalize($regexp), _encodePrivate);
98         // quick ref
99         var $encoded = $keywords.$encoded;
100         $parser.add($regexp, function($match, $offset) {
101             return $encoded[$match[$offset]];
102         });
103         return $parser.exec($script);
104     };
105
106     function _encodeKeywords($script) {
107         // escape high-ascii values already in the script (i.e. in strings)
108         if (_encoding > 62) $script = _escape95($script);
109         // create the parser
110         var $parser = new ParseMaster;
111         var $encode = _getEncoder(_encoding);
112         // for high-ascii, don't encode single character low-ascii
113         var $regexp = (_encoding > 62) ? /\w\w+/ : /\w+/;
114         // build the word list
115         $keywords = _analyze($script, _globalize($regexp), $encode);
116         var $encoded = $keywords.$encoded;
117         // encode
118         $parser.add($regexp, function($match, $offset) {
119         return $encoded[$match[$offset]];
120         });
121         // if encoded, wrap the script in a decoding function
122         return $script && _bootStrap($parser.exec($script), $keywords);
123     };
124
125     function _analyze($script, $regexp, $encode) {
126         // analyse
127         // retreive all words in the script
128         var $all = $script.match($regexp);
129         var $$sorted = []; // list of words sorted by frequency
130         var $$encoded = {}; // dictionary of word->encoding
131         var $$protected = {}; // instances of "protected" words
132         if ($all) {
133             var $unsorted = []; // same list, not sorted
134             var $protected = {}; // "protected" words (dictionary of word->"word")
135             var $values = {}; // dictionary of charCode->encoding (eg. 256->ff)
136             var $count = {}; // word->count
137             var i = $all.length, j = 0, $word;
138             // count the occurrences - used for sorting later
139             do {
140                 $word = "$" + $all[--i];
141                 if (!$count[$word]) {
142                     $count[$word] = 0;
143                     $unsorted[j] = $word;
144                     // make a dictionary of all of the protected words in this script
145                     //  these are words that might be mistaken for encoding
146                     $protected["$" + ($values[j] = $encode(j))] = j++;
147                 }
148                 // increment the word counter
149                 $count[$word]++;
150             } while (i);
151             // prepare to sort the word list, first we must protect
152             //  words that are also used as codes. we assign them a code
153             //  equivalent to the word itself.
154             // e.g. if "do" falls within our encoding range
155             //      then we store keywords["do"] = "do";
156             // this avoids problems when decoding
157             i = $unsorted.length;
158             do {
159                 $word = $unsorted[--i];
160                 if ($protected[$word] != null) {
161                     $$sorted[$protected[$word]] = $word.slice(1);
162                     $$protected[$protected[$word]] = true;
163                     $count[$word] = 0;
164                 }
165             } while (i);
166             // sort the words by frequency
167             $unsorted.sort(function($match1, $match2) {
168                 return $count[$match2] - $count[$match1];
169             });
170             j = 0;
171             // because there are "protected" words in the list
172             //  we must add the sorted words around them
173             do {
174                 if ($$sorted[i] == null) $$sorted[i] = $unsorted[j++].slice(1);
175                 $$encoded[$$sorted[i]] = $values[i];
176             } while (++i < $unsorted.length);
177         }
178         return {$sorted: $$sorted, $encoded: $$encoded, $protected: $$protected};
179     };
180
181     // build the boot function used for loading and decoding
182     function _bootStrap($packed, $keywords) {
183         var $ENCODE = _safeRegExp("$encode\\($count\\)", "g");
184
185         // $packed: the packed script
186         $packed = "'" + _escape($packed) + "'";
187
188         // $ascii: base for encoding
189         var $ascii = Math.min($keywords.$sorted.length, _encoding) || 1;
190
191         // $count: number of words contained in the script
192         var $count = $keywords.$sorted.length;
193
194         // $keywords: list of words contained in the script
195         for (var i in $keywords.$protected) $keywords.$sorted[i] = "";
196         // convert from a string to an array
197         $keywords = "'" + $keywords.$sorted.join("|") + "'.split('|')";
198
199         // $encode: encoding function (used for decoding the script)
200         var $encode = _encoding > 62 ? _encode95 : _getEncoder($ascii);
201         $encode = String($encode).replace(/_encoding/g, "$ascii").replace(/arguments\.callee/g, "$encode");
202         var $inline = "$count" + ($ascii > 10 ? ".toString($ascii)" : "");
203
204         // $decode: code snippet to speed up decoding
205         if (_fastDecode) {
206             // create the decoder
207             var $decode = _getFunctionBody(_decode);
208             if (_encoding > 62) $decode = $decode.replace(/\\\\w/g, "[\\xa1-\\xff]");
209             // perform the encoding inline for lower ascii values
210             else if ($ascii < 36) $decode = $decode.replace($ENCODE, $inline);
211             // special case: when $count==0 there are no keywords. I want to keep
212             //  the basic shape of the unpacking funcion so i'll frig the code...
213             if (!$count) $decode = $decode.replace(_safeRegExp("($count)\\s*=\\s*1"), "$1=0");
214         }
215
216         // boot function
217         var $unpack = String(_unpack);
218         if (_fastDecode) {
219             // insert the decoder
220             $unpack = $unpack.replace(/\{/, "{" + $decode + ";");
221         }
222         $unpack = $unpack.replace(/"/g, "'");
223         if (_encoding > 62) { // high-ascii
224             // get rid of the word-boundaries for regexp matches
225             $unpack = $unpack.replace(/'\\\\b'\s*\+|\+\s*'\\\\b'/g, "");
226         }
227         if ($ascii > 36 || _encoding > 62 || _fastDecode) {
228             // insert the encode function
229             $unpack = $unpack.replace(/\{/, "{$encode=" + $encode + ";");
230         } else {
231             // perform the encoding inline
232             $unpack = $unpack.replace($ENCODE, $inline);
233         }
234         // pack the boot function too
235         $unpack = pack($unpack, 0, false, true);
236
237         // arguments
238         var $params = [$packed, $ascii, $count, $keywords];
239         if (_fastDecode) {
240             // insert placeholders for the decoder
241             $params = $params.concat(0, "{}");
242         }
243
244         // the whole thing
245         return "eval(" + $unpack + "(" + $params + "))\n";
246     };
247
248     // mmm.. ..which one do i need ??
249     function _getEncoder($ascii) {
250         return $ascii > 10 ? $ascii > 36 ? $ascii > 62 ? _encode95 : _encode62 : _encode36 : _encode10;
251     };
252
253     // zero encoding
254     // characters: 0123456789
255     var _encode10 = function($charCode) {
256         return $charCode;
257     };
258
259     // inherent base36 support
260     // characters: 0123456789abcdefghijklmnopqrstuvwxyz
261     var _encode36 = function($charCode) {
262         return $charCode.toString(36);
263     };
264
265     // hitch a ride on base36 and add the upper case alpha characters
266     // characters: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
267     var _encode62 = function($charCode) {
268         return ($charCode < _encoding ? '' : arguments.callee(parseInt($charCode / _encoding))) +
269             (($charCode = $charCode % _encoding) > 35 ? String.fromCharCode($charCode + 29) : $charCode.toString(36));
270     };
271
272     // use high-ascii values
273     var _encode95 = function($charCode) {
274         return ($charCode < _encoding ? '' : arguments.callee($charCode / _encoding)) +
275             String.fromCharCode($charCode % _encoding + 161);
276     };
277
278     // special _chars
279     var _encodePrivate = function($charCode) {
280         return "_" + $charCode;
281     };
282
283     // protect characters used by the parser
284     function _escape($script) {
285         return $script.replace(/([\\'])/g, "\\$1");
286     };
287
288     // protect high-ascii characters already in the script
289     function _escape95($script) {
290         return $script.replace(/[\xa1-\xff]/g, function($match) {
291             return "\\x" + $match.charCodeAt(0).toString(16);
292         });
293     };
294
295     function _safeRegExp($string, $flags) {
296         return new RegExp($string.replace(/\$/g, "\\$"), $flags);
297     };
298
299     // extract the body of a function
300     function _getFunctionBody($function) {
301         with (String($function)) return slice(indexOf("{") + 1, lastIndexOf("}"));
302     };
303
304     // set the global flag on a RegExp (you have to create a new one)
305     function _globalize($regexp) {
306         return new RegExp(String($regexp).slice(1, -1), "g");
307     };
308
309     // build the parsing routine
310     _addParser(_basicCompression);
311     if (_specialChars) _addParser(_encodeSpecialChars);
312     if (_encoding) _addParser(_encodeKeywords);
313
314     // go!
315     return _pack(_script);
316 };