jscompress 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
File without changes
@@ -0,0 +1,56 @@
1
+
2
+
3
+ = Riassence JSCompress
4
+
5
+ * http://rsence.org/
6
+
7
+
8
+ == Description:
9
+
10
+ Simple C extension that compresses Javascript source with variables beginning with a underscore.
11
+ This used to be a fixed part of the Riassence Framework, but it's distributed as a separate gem now.
12
+
13
+ == Usage:
14
+
15
+ require 'jscompress'
16
+
17
+ # Makes a JSCompress instance that compresses js source.
18
+ # The reserved_names are ignored in the input.
19
+ reserved_names = ['_dontCompressMe','_not_me_either']
20
+ jscompress = JSCompress.new( reserved_names )
21
+
22
+ # Builds indexes by reading source code.
23
+ # This ensures the best compression, because the shortest
24
+ # names are assigned to the most often used variables.
25
+ # It also ensures the different files have the same compressed
26
+ # variable names, so they can be called from one file to another.
27
+ js1 = File.read( 'big_js1.js' )
28
+ jscompress.build_indexes( js1 )
29
+ js2 = File.read( 'big_js2.js' )
30
+ jscompress.build_indexes( js2 )
31
+ js3 = File.read( 'big_js3.js' )
32
+ jscompress.build_indexes( js3 )
33
+
34
+ # Pack the source. Basically this means something like _longAndDescriptiveName -> _e
35
+ compressed_js1 = jscompress.compress( js1 )
36
+ compressed_js2 = jscompress.compress( js2 )
37
+ compressed_js3 = jscompress.compress( js3 )
38
+
39
+ # You can continue to build indexes after compression too:
40
+ js4 = File.read( 'big_js4.js' )
41
+ jscompress.build_indexes( js4 )
42
+ compressed_js4 = jscompress.compress( js4 )
43
+
44
+ # To free the indexes, use:
45
+ jscompress.free_indexes
46
+
47
+
48
+ == Install:
49
+
50
+ * sudo gem install jscompress
51
+
52
+ == License:
53
+
54
+ Author: Domen Puncer <domen@cba.si>
55
+ License: BSD
56
+
@@ -0,0 +1,6 @@
1
+ require 'mkmf'
2
+ create_makefile('jscompress')
3
+ system('make clean')
4
+ system('make all')
5
+ require 'test_jscompress'
6
+
@@ -0,0 +1,451 @@
1
+ /*
2
+ * jscompress - compression of _words into _0.._Z, _00...
3
+ *
4
+ * Author: Domen Puncer <domen@cba.si>
5
+ * License: BSD
6
+ *
7
+ * Ideas taken from js_builder.rb (Riassence Framework) by Juha-Jarmo Heinonen <jjh@riassence.com>
8
+ */
9
+
10
+
11
+ #include <stdlib.h>
12
+ #include "ruby.h"
13
+
14
+ static char **reserved;
15
+ static int nreserved;
16
+ static int *reserved_indexes;
17
+
18
+
19
+ static inline int isvarchr(int c)
20
+ {
21
+ if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
22
+ (c >= '0' && c <= '9') || c == '_')
23
+ return 1;
24
+ return 0;
25
+ }
26
+
27
+ struct tree_node {
28
+ int left, right;
29
+ const char *var;
30
+ int count; /* how many occurances of this 'var' were there */
31
+ int index; /* most used var gets 0... */
32
+ int arr_index; /* original index in array, so resort can fix the tree */
33
+ };
34
+
35
+ static struct tree_node *tree_node_arr;
36
+ static const int tree_node_arr_inc = 256; /* allocate this many tree nodes at once */
37
+ static int tree_node_arr_count; /* this many nodes are full */
38
+ static int tree_node_arr_size; /* place for this many nodes */
39
+
40
+ static int reserved_indexes_already_matched; /* how many of the reserved words are already in the index */
41
+
42
+
43
+ /* string functions, except they're end-of-var terminated, not just '\0' */
44
+ static int strlenv(const char *s)
45
+ {
46
+ int len = 0;
47
+
48
+ while (isvarchr(*s++))
49
+ len++;
50
+ return len;
51
+ }
52
+
53
+ static int strcmpv(const char *s1, const char *s2)
54
+ {
55
+ int c1, c2;
56
+
57
+ do {
58
+ c1 = *s1++;
59
+ c2 = *s2++;
60
+
61
+ if (!isvarchr(c1))
62
+ c1 = 0;
63
+ if (!isvarchr(c2))
64
+ c2 = 0;
65
+
66
+ if (c1 != c2)
67
+ return c1-c2;
68
+
69
+ } while (c1 && c2);
70
+
71
+ return 0;
72
+ }
73
+
74
+ static char *strdupv(const char *s)
75
+ {
76
+ int len = strlenv(s);
77
+ char *r = malloc(len+1);
78
+
79
+ if (!r)
80
+ rb_raise(rb_eNoMemError, "malloc failed in %s", __func__);
81
+
82
+ memcpy(r, s, len+1);
83
+
84
+ return r;
85
+ }
86
+
87
+ /* tree functions, used to build a dictionary */
88
+ /* tree_find returns the matching node, or if no match, the closest node */
89
+ static struct tree_node *tree_find(struct tree_node *root, const char *name)
90
+ {
91
+ struct tree_node *node = root;
92
+
93
+ do {
94
+ int next;
95
+ int cmp;
96
+ cmp = strcmpv(name, node->var);
97
+ if (cmp == 0) {
98
+ return node;
99
+
100
+ } else if (cmp < 0) {
101
+ if (node->left == -1)
102
+ return node;
103
+ else
104
+ next = node->left;
105
+ } else {
106
+ if (node->right == -1)
107
+ return node;
108
+ else
109
+ next = node->right;
110
+ }
111
+ node = &root[next];
112
+
113
+ } while (1);
114
+ }
115
+
116
+ static void tree_add(const char *name)
117
+ {
118
+ struct tree_node *parent, *me;
119
+ int cmp;
120
+
121
+ if (tree_node_arr_count >= tree_node_arr_size) {
122
+ void *tmp = tree_node_arr;
123
+ tree_node_arr_size += tree_node_arr_inc;
124
+ tree_node_arr = realloc(tmp,
125
+ tree_node_arr_size * sizeof(tree_node_arr[0]));
126
+ if (!tree_node_arr) {
127
+ free(tmp);
128
+ rb_raise(rb_eNoMemError, "malloc failed in %s", __func__);
129
+ }
130
+ }
131
+
132
+ me = NULL;
133
+ if (tree_node_arr_count) {
134
+ parent = tree_find(tree_node_arr, name);
135
+
136
+ cmp = strcmpv(name, parent->var);
137
+ if (cmp == 0) {
138
+ parent->count++;
139
+ } else {
140
+ me = &tree_node_arr[tree_node_arr_count];
141
+ if (cmp < 0)
142
+ parent->left = tree_node_arr_count;
143
+ else
144
+ parent->right = tree_node_arr_count;
145
+
146
+ tree_node_arr_count++;
147
+ }
148
+
149
+ } else {
150
+ me = &tree_node_arr[tree_node_arr_count++];
151
+ }
152
+
153
+ if (me) {
154
+ me->left = me->right = -1;
155
+ // me->var = name;
156
+ me->var = strdupv(name);
157
+ me->count = 1;
158
+ }
159
+ }
160
+
161
+ /* qsort and bsearch compare function */
162
+ static int cmp_str(const void *p1, const void *p2)
163
+ {
164
+ const char * const *s1 = p1;
165
+ const char * const *s2 = p2;
166
+
167
+ return strcmpv(*s1, *s2);
168
+ }
169
+
170
+ static int cmp_int(const void *p1, const void *p2)
171
+ {
172
+ const int *i1 = p1;
173
+ const int *i2 = p2;
174
+
175
+ return *i1 > *i2;
176
+ }
177
+
178
+ static int is_reserved(const char *str)
179
+ {
180
+ return bsearch(&str, reserved, nreserved, sizeof(*reserved), cmp_str) != 0;
181
+ }
182
+
183
+ /* scan the string, and build a dictionary tree of variables */
184
+ static void jscompress_scan(const char *s, int len)
185
+ {
186
+ const char *end = s + len;
187
+ int invar = 0; /* currently parsing a variable */
188
+
189
+ while (s < end) {
190
+ char c = *s++;
191
+
192
+ /* start of a variable, add it to variable tree */
193
+ if (c == '_' && invar == 0) {
194
+ invar = 1;
195
+
196
+ if (!is_reserved(s))
197
+ tree_add(s);
198
+ }
199
+ invar = isvarchr(c);
200
+ }
201
+ }
202
+
203
+
204
+ /* input: index; output: characters 0..Z, 00..ZZ, ... */
205
+ static int jscompress_generate_name(char *dest, int x)
206
+ {
207
+ const int count = ('9'-'0'+1)+('z'-'a'+1)+('Z'-'A'+1);
208
+ int len = 0;
209
+
210
+ x++;
211
+ do {
212
+ int rem;
213
+ char chr;
214
+
215
+ x--;
216
+ rem = x % count;
217
+
218
+ if (rem < ('9'-'0'+1))
219
+ chr = rem + '0';
220
+ else {
221
+ rem -= ('9'-'0'+1);
222
+ if (rem < ('z'-'a'+1))
223
+ chr = rem + 'a';
224
+ else {
225
+ rem -= ('z'-'a'+1);
226
+ chr = rem + 'A';
227
+ }
228
+ }
229
+ *dest++ = chr;
230
+ len++;
231
+ x /= count;
232
+
233
+ } while (x > 0);
234
+
235
+ *dest = '\0';
236
+
237
+ return len;
238
+ }
239
+
240
+ /* reverse of upper function */
241
+ static int jscompress_index_from_name(const char *name)
242
+ {
243
+ const int count = ('9'-'0'+1)+('z'-'a'+1)+('Z'-'A'+1);
244
+ int x = 0;
245
+ char c = *name++;
246
+
247
+ if (c >= '0' && c <= '9')
248
+ x += c - '0';
249
+ else if (c >= 'a'&& c <= 'z')
250
+ x += c - 'a' + ('9'-'0'+1);
251
+ else
252
+ x += c - 'A' + ('9'-'0'+1) + ('z'-'a'+1);
253
+
254
+ if (isvarchr(*name)) {
255
+ x += count * (1+jscompress_index_from_name(name));
256
+ }
257
+
258
+ return x;
259
+ }
260
+
261
+ /* replace variable names with shorter ones */
262
+ static int jscompress_replace(char *_dest, const char *s, int len)
263
+ {
264
+ const char *end = s + len;
265
+ int invar = 0; /* currently parsing a variable */
266
+ char *dest = _dest;
267
+ int maxlen = len * 1.9;
268
+
269
+ while (s < end) {
270
+ char c = *s++;
271
+
272
+ *dest++ = c;
273
+
274
+ /* start of a variable, replace it with shorter */
275
+ if (c == '_' && invar == 0) {
276
+ struct tree_node *var;
277
+ int varlen;
278
+
279
+ invar = 1;
280
+
281
+ if (!is_reserved(s)) {
282
+ var = tree_find(tree_node_arr, s);
283
+
284
+ /* error out if identifier is not found? */
285
+ if (strcmpv(var->var, s) == 0) {
286
+ varlen = jscompress_generate_name(dest, var->index);
287
+ dest += varlen;
288
+ s += strlenv(s);
289
+ }
290
+ }
291
+ }
292
+ invar = isvarchr(c);
293
+
294
+ if (dest - _dest > maxlen)
295
+ rb_raise(rb_eException, "%s: destination larger than source", __func__);
296
+ }
297
+
298
+ return dest - _dest;
299
+ }
300
+
301
+ /* sorting helpers */
302
+ static int jscompress_cmp_count(const void *p1, const void *p2)
303
+ {
304
+ const struct tree_node *n1 = p1;
305
+ const struct tree_node *n2 = p2;
306
+
307
+ return n2->count - n1->count;
308
+ }
309
+
310
+ static int jscompress_cmp_arr_index(const void *p1, const void *p2)
311
+ {
312
+ const struct tree_node *n1 = p1;
313
+ const struct tree_node *n2 = p2;
314
+
315
+ return n1->arr_index - n2->arr_index;
316
+ }
317
+
318
+ static VALUE jscompress_build_indexes(VALUE self, VALUE str)
319
+ {
320
+ int i;
321
+ const char *s = RSTRING_PTR(str);
322
+ int len = RSTRING_LEN(str);
323
+ int off;
324
+ int res_idx;
325
+ int old_arr_count = tree_node_arr_count;
326
+
327
+ if (isvarchr(s[len-1]))
328
+ rb_raise(rb_eException, "%s: last character of file is variable char?", __func__);
329
+
330
+ /* build a dictionary */
331
+ jscompress_scan(s, len);
332
+
333
+ /* remember original positions of tree node in array */
334
+ for (i=old_arr_count; i<tree_node_arr_count; i++)
335
+ tree_node_arr[i].arr_index = i;
336
+
337
+ /* sort words descending by occurence */
338
+ qsort(&tree_node_arr[old_arr_count], tree_node_arr_count-old_arr_count,
339
+ sizeof(tree_node_arr[0]), jscompress_cmp_count);
340
+
341
+ /* mark word indexes and offset them, so reserved names are not used */
342
+ off = reserved_indexes_already_matched;
343
+ res_idx = 0;
344
+ for (i=old_arr_count; i<tree_node_arr_count; i++) {
345
+ if (reserved_indexes[res_idx] == i + off) {
346
+ res_idx++;
347
+ off++;
348
+ }
349
+ tree_node_arr[i].index = i + off;
350
+ }
351
+ reserved_indexes_already_matched = off;
352
+
353
+ /* restore original tree node positions in array */
354
+ qsort(&tree_node_arr[old_arr_count], tree_node_arr_count-old_arr_count,
355
+ sizeof(tree_node_arr[0]), jscompress_cmp_arr_index);
356
+
357
+ return Qnil;
358
+ }
359
+
360
+ static VALUE jscompress(VALUE self, VALUE str)
361
+ {
362
+ VALUE ret_str;
363
+ char *dest;
364
+ int dest_len;
365
+
366
+ if (!tree_node_arr)
367
+ rb_raise(rb_eException, "%s: indexes not built", __func__);
368
+
369
+ /* since we use minimal variables, destination is always smaller than source */
370
+ dest = malloc(RSTRING_LEN(str) * /*FIXME*/ 2);
371
+ if (!dest) {
372
+ free(tree_node_arr);
373
+ rb_raise(rb_eNoMemError, "%s: malloc failed", __func__);
374
+ }
375
+
376
+ /* replace variables with shorter versions */
377
+ dest_len = jscompress_replace(dest, RSTRING_PTR(str), RSTRING_LEN(str));
378
+
379
+ ret_str = rb_str_new(dest, dest_len);
380
+
381
+ free(dest);
382
+
383
+ return ret_str;
384
+ }
385
+
386
+ static VALUE jscompress_free_indexes(VALUE self)
387
+ {
388
+ int i;
389
+
390
+ for (i=0; i<tree_node_arr_count; i++)
391
+ free((char*)tree_node_arr[i].var);
392
+
393
+ free(tree_node_arr);
394
+
395
+ tree_node_arr_count = 0;
396
+ tree_node_arr_size = 0;
397
+ tree_node_arr = NULL;
398
+ reserved_indexes_already_matched = 0;
399
+
400
+ return Qnil;
401
+ }
402
+
403
+ static VALUE jscompress_initialize(VALUE self, VALUE res)
404
+ {
405
+ int i;
406
+
407
+ nreserved = RARRAY_LEN(res);
408
+ reserved = ALLOC_N(char *, nreserved);
409
+ reserved_indexes = ALLOC_N(int, nreserved+1);
410
+
411
+ /* prepare reserved identifiers for lookups */
412
+ for (i=0; i<nreserved; i++) {
413
+ VALUE str = RARRAY_PTR(res)[i];
414
+ int len = RSTRING_LEN(str);
415
+ const char *var = RSTRING_PTR(str)+1;
416
+ int var_len = strlenv(var);
417
+
418
+ reserved[i] = ALLOC_N(char, len);
419
+ memcpy(reserved[i], var, len-1);
420
+ reserved[i][len-1] = '\0';
421
+
422
+ /* >62**4 variables, no way this happens
423
+ * variables with extra '_' can't clash with our generated ones */
424
+ if (var_len > 4 || memchr(var, '_', var_len) != NULL)
425
+ reserved_indexes[i] = INT_MAX;
426
+ else
427
+ reserved_indexes[i] = jscompress_index_from_name(var);
428
+
429
+ }
430
+ reserved_indexes[i] = INT_MAX;
431
+
432
+ /* sort reserved names, so we can bsearch them */
433
+ qsort(reserved, nreserved, sizeof(*reserved), cmp_str);
434
+
435
+ /* asc sort of name indexes */
436
+ qsort(reserved_indexes, nreserved, sizeof(*reserved_indexes), cmp_int);
437
+
438
+ return self;
439
+ }
440
+
441
+
442
+ static VALUE cl;
443
+
444
+ void Init_jscompress()
445
+ {
446
+ cl = rb_define_class("JSCompress", rb_cObject);
447
+ rb_define_method(cl, "initialize", jscompress_initialize, 1);
448
+ rb_define_method(cl, "build_indexes", jscompress_build_indexes, 1);
449
+ rb_define_method(cl, "compress", jscompress, 1);
450
+ rb_define_method(cl, "free_indexes", jscompress_free_indexes, 0);
451
+ }
@@ -0,0 +1,31 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'jscompress'
3
+ s.author = 'Domen Puncer'
4
+ s.email = 'domen@cba.si'
5
+ s.version = '1.0.1'
6
+ s.date = '2010-02-26'
7
+ s.homepage = 'http://www.riassence.org/'
8
+ s.summary = 'Riassence JSCompress'
9
+ s.has_rdoc = true
10
+ s.require_path = 'jscompress'
11
+ s.description = <<END
12
+ C extension that replaces javascript variable names with short ones.
13
+ This used to be a fixed part of the Riassence Framework, but it's distributed as a separate gem now.
14
+ END
15
+ s.files = %w(
16
+ License.txt
17
+ README.rdoc
18
+ extconf.rb
19
+ jscompress.c
20
+ jscompress.gemspec
21
+ test_jscompress.rb
22
+ )
23
+ s.files.reject! { |fn| fn.include? ".svn" }
24
+ s.files.reject! { |fn| fn.include? ".git" }
25
+ s.test_file = 'test_jscompress.rb'
26
+ s.required_ruby_version = '>= 1.8.6'
27
+ s.extensions = [
28
+ 'extconf.rb'
29
+ ]
30
+ end
31
+
@@ -0,0 +1,77 @@
1
+ require "test/unit"
2
+ require "rubygems"
3
+ require "jscompress"
4
+
5
+ class TestJSCompress < Test::Unit::TestCase
6
+
7
+ @@test_input = %{
8
+
9
+ var Something = Foo.extend({
10
+ _fooBarLongName: function(_fooBar){
11
+ if(!_fooBar){
12
+ var _this = this;
13
+ _this['_anotherLongName']();
14
+ }
15
+ else{
16
+ this._dontCompressMe();
17
+ }
18
+ },
19
+ _anotherLongName: function(){
20
+ var _this = this;
21
+ _this.fooBarLongName(_this);
22
+ },
23
+ _dontCompressMe: function(){},
24
+ dontCompressMeEither: '_thisShouldNotBeCompressed';
25
+ });
26
+
27
+ }
28
+
29
+
30
+ @@test_output = %{
31
+
32
+ var Something = Foo.extend({
33
+ _3: function(_1){
34
+ if(!_1){
35
+ var _0 = this;
36
+ _0['_2']();
37
+ }
38
+ else{
39
+ this._dontCompressMe();
40
+ }
41
+ },
42
+ _2: function(){
43
+ var _0 = this;
44
+ _0.fooBarLongName(_0);
45
+ },
46
+ _dontCompressMe: function(){},
47
+ dontCompressMeEither: '_thisShouldNotBeCompressed';
48
+ });
49
+
50
+ }
51
+
52
+ @@reserved_names = [
53
+ '_thisShouldNotBeCompressed',
54
+ '_dontCompressMe'
55
+ ]
56
+
57
+ def test_init
58
+ jscompress = JSCompress.new( @@reserved_names )
59
+ end
60
+
61
+ def test_types
62
+ jscompress = JSCompress.new( @@reserved_names )
63
+ assert_equal( JSCompress, jscompress.class )
64
+ assert_equal( NilClass, jscompress.build_indexes(@@test_input).class )
65
+ assert_equal( String, jscompress.compress(@@test_input).class )
66
+ assert_equal( NilClass, jscompress.free_indexes().class )
67
+ end
68
+
69
+ def test_value
70
+ jscompress = JSCompress.new( @@reserved_names )
71
+ jscompress.build_indexes(@@test_input)
72
+ assert_equal( @@test_output, jscompress.compress(@@test_input) )
73
+ jscompress.free_indexes
74
+ end
75
+
76
+ end
77
+
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jscompress
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 1
9
+ version: 1.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Domen Puncer
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-02-26 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: |
22
+ C extension that replaces javascript variable names with short ones.
23
+ This used to be a fixed part of the Riassence Framework, but it's distributed as a separate gem now.
24
+
25
+ email: domen@cba.si
26
+ executables: []
27
+
28
+ extensions:
29
+ - extconf.rb
30
+ extra_rdoc_files: []
31
+
32
+ files:
33
+ - License.txt
34
+ - README.rdoc
35
+ - extconf.rb
36
+ - jscompress.c
37
+ - jscompress.gemspec
38
+ - test_jscompress.rb
39
+ has_rdoc: true
40
+ homepage: http://www.riassence.org/
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - jscompress
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ segments:
53
+ - 1
54
+ - 8
55
+ - 6
56
+ version: 1.8.6
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ requirements: []
65
+
66
+ rubyforge_project:
67
+ rubygems_version: 1.3.6
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: Riassence JSCompress
71
+ test_files:
72
+ - test_jscompress.rb