jscompress 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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