-
Notifications
You must be signed in to change notification settings - Fork 0
/
pyjscompressor.py
359 lines (288 loc) · 11.3 KB
/
pyjscompressor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
#!/usr/bin/env python
# Copyright (C) 2010 Sujan Shakya, suzan.shakya@gmail.com
# Copyright (C) 2012 PJSHAB <pjbraby@gmail.com>
# Copyright (C) 2012 Alok Parlikar <aup@cs.cmu.edu>
#
# This script works with the google closure compiler
# http://closure-compiler.googlecode.com/files/compiler-latest.zip
#
# The closure compiler requires java to be installed and an entry for
# your java directory in your system PATH
#
# The script needs the path to your google closure compiler.jar file:
# Pass the path to your compiler as the second argument or
# create an environment variable COMPILER=/path/to/google/compiler
# Then run this script. This will reduce the output size to ~50%.
# Usage:
# python pyjscompressor.py [-c COMPILER] [-j NUM] <pyjs_output_directory>
#
# optional arguments:
# -h, --help show this help message and exit
# -c COMPILER, --compiler COMPILER
# Path to Google Closure compiler.jar
#
# -j NUM Run NUM processes in parallel
#
# Running with -j 0 will run as many processes as cpu count.
import os
import re
import shutil
import subprocess
import sys
import tempfile
try:
import multiprocessing
enable_multiprocessing = True
except ImportError:
enable_multiprocessing = False
MERGE_SCRIPTS = re.compile(
'</script>\s*(?:<!--.*?-->\s*)*<script(?:(?!\ssrc).)*?>', re.DOTALL)
SCRIPT = re.compile('<script(?:(?!\ssrc).)*?>(.*?)</script>', re.DOTALL)
def compile(js_file, js_output_file, html_file=''):
# SIMPLE_OPTIMIZATIONS has some problem with Opera, so we'll use
# WHITESPACE_ONLY for opera
if 'opera' in html_file:
level = 'WHITESPACE_ONLY'
else:
level = 'SIMPLE_OPTIMIZATIONS'
global compiler_path
args = ['java',
'-jar', compiler_path,
'--compilation_level', level,
'--js', js_file,
'--js_output_file', js_output_file]
error = subprocess.call(args=args,
stdout=open(os.devnull, 'w'),
stderr=subprocess.STDOUT)
if error:
raise Exception(' '.join([
'Error(s) occurred while compiling %s' % js_file,
'possible cause: file may be invalid javascript.']))
def compress_css(css_file):
css_output_file = tempfile.NamedTemporaryFile()
f = open(css_file)
css = f.read()
css = re.sub(r"\s+([!{};:>+\(\)\],])", r"\1", css)
css = re.sub(r"([!{}:;>+\(\[,])\s+", r"\1", css)
css = re.sub(r"\s+", " ", css)
css_output_file.write(css)
css_output_file.flush()
return finish_compressors(css_output_file.name, css_file)
def compress_js(js_file):
js_output_file = tempfile.NamedTemporaryFile()
compile(js_file, js_output_file.name)
return finish_compressors(js_output_file.name, js_file)
def compress_html(html_file):
html_output_file = tempfile.NamedTemporaryFile()
f = open(html_file)
html = f.read()
f.close()
# remove comments betn <script> and merge all <script>
html = MERGE_SCRIPTS.sub('', html)
# now extract the merged scripts
template = '<!--compiled-js-%d-->'
scripts = []
def script_repl(matchobj):
scripts.append(matchobj.group(1))
return '<script type="text/javascript">%s</script>' % template % \
(len(scripts) - 1)
html = SCRIPT.sub(script_repl, html)
# save js files as temporary files and compile them with simple
# optimizations
js_output_files = []
for script in scripts:
js_file = tempfile.NamedTemporaryFile()
js_file.write(script)
js_file.flush()
js_output_file = tempfile.NamedTemporaryFile()
js_output_files.append(js_output_file)
compile(js_file.name, js_output_file.name, html_file)
# now write all compiled js back to html file
for idx, js_output_file in enumerate(js_output_files):
script = js_output_file.read()
html = html.replace(template % idx, script)
html_output_file.write(html)
html_output_file.flush()
return finish_compressors(html_output_file.name, html_file)
def finish_compressors(new_path, old_path):
p_size, n_size = getsize(old_path), getsize(new_path)
shutil.copyfile(new_path, old_path)
return p_size, n_size, old_path
def compress(path):
try:
ext = os.path.splitext(path)[1]
if ext == '.css':
return compress_css(path)
elif ext == '.js':
return compress_js(path)
elif ext == '.html':
return compress_html(path)
uncomp_type_size = getsize(path)
return (uncomp_type_size, uncomp_type_size, path)
except KeyboardInterrupt:
# This function runs in child processes under multiprocessing,
# and any ^C on the terminal is not handled properly. The
# parent (see main) handles KeyboardInterrupt, so we ignore
# it.
if enable_multiprocessing:
pass
else:
# Raise the error if single processing is on.
raise
def getsize(path):
return os.path.getsize(path)
def getcompression(p_size, n_size):
try:
return n_size / float(p_size) * 100
except ZeroDivisionError:
return 100.0
def getsmallpath(path, max_chars):
"""Returns a shortened path to fit into max_chars for pretty
output on the screen
"""
if len(path) <= max_chars:
return path
# First remove the path and keep last name
smallpath = os.path.basename(path)
if len(smallpath) <= max_chars:
return smallpath
# Split the filename and insert ellipsis in the middle
max_chars = max_chars - 3 # 3 characters of ellipsis
first_half = smallpath[:(max_chars / 2)]
second_half = smallpath[-(max_chars / 2):]
smallpath = ''.join([first_half, '...', second_half])
return smallpath
def compress_all(path):
# Print headers for progress output
print('%45s %s' % ('Files', 'Compression'))
global num_procs
p_size = 0
n_size = 0
if os.path.isfile(path):
# Don't need to run multiprocessing for what is a single file
p_size, n_size, oldpath = compress(path)
else:
files_to_compress = []
for root, dirs, files in os.walk(path):
for file in files:
files_to_compress.append(os.path.join(root, file))
if num_procs > 1:
proc_pool = multiprocessing.Pool(num_procs)
# Run the compress function using imap. Chunk size is 1 so
# that we can update progress report as soon as entries
# are processed.
result = proc_pool.imap(compress, files_to_compress, 1)
count_done = 0
count_total = len(files_to_compress)
try:
# If a keyboad interrupt occurs while we are waiting
# for output, we need to handle it and quit.
compression_result = None
while count_done < count_total:
try:
# Poll for new results every half a second
compression_result = result.next(0.5)
count_done += 1
except multiprocessing.TimeoutError:
continue
dp, dn, path = compression_result
p_size += dp
n_size += dn
ratio = getcompression(dp, dn)
smallpath = getsmallpath(path, 40)
print('%45s %4.1f%%' % (smallpath, ratio))
except KeyboardInterrupt:
# Stop child processes, and pass on the error to caller
proc_pool.terminate()
raise
else:
# Running on single process
for file in files_to_compress:
try:
(dp, dn, path) = compress(file)
except TypeError:
# Could occur upon KeyboardInterrupt in compress function
break
p_size += dp
n_size += dn
ratio = getcompression(dp, dn)
smallpath = getsmallpath(path, 40)
print('%45s %4.1f%%' % (smallpath, ratio))
compression = getcompression(p_size, n_size)
sizes = "Initial size: %.1fKiB Final size: %.1fKiB" % \
(p_size / 1024., n_size / 1024.)
print('%s %s' % (sizes.ljust(51), "%4.1f%%" % compression))
if __name__ == '__main__':
try:
import argparse
# Available only on Python 2.7+
mode = 'argparse'
except ImportError:
import optparse
mode = 'optparse'
# Take one position argument (directory)
# and optional arguments for compiler path and multiprocessing
global compiler_path
global num_procs
num_procs = 1 # By default, disable multiprocessing
if mode == 'argparse':
parser = argparse.ArgumentParser(
description='Compress HTML, CSS and JS in PYJS output')
parser.add_argument('directory', type=str,
help='Pyjamas Output Directory')
parser.add_argument('-c', '--compiler', type=str, default='',
help='Path to Google Closure compiler.jar')
parser.add_argument('-j', metavar='NUM', default=0, type=int,
dest='num_procs',
help='Run NUM processes in parallel')
args = parser.parse_args()
directory = args.directory
compiler_path = args.compiler
num_procs = args.num_procs
else:
# Use optparse
usage = 'usage: %prog [options] <pyjamas-output-directory>'
parser = optparse.OptionParser(usage=usage)
parser.add_option('-c', '--compiler', type=str, default='',
help='Path to Google Closure compiler.jar')
parser.add_option('-j', metavar='NUM', default=0, type=int,
dest='num_procs',
help='Run NUM processes in parallel')
options, args = parser.parse_args()
if len(args) != 1:
parser.error('Please specify the directory to compress')
directory = args[0]
compiler_path = options.compiler
num_procs = options.num_procs
if not compiler_path:
# Not specified on command line
# Try environment
try:
compiler_path = os.environ['COMPILER']
except KeyError:
sys.exit('Closure compiler not found\n'
'Either specify it using the -c option,\n'
'or set the COMPILER environment variable to \n'
'the location of compiler.jar')
if not os.path.isfile(compiler_path):
sys.exit('\n'.join([
'Compiler path "%s" not valid.' % compiler_path,
'Check the path to your compiler is correct.']))
if not enable_multiprocessing:
# Even if user has specified -j we can't use it if
# multiprocessing module doesn't exist
num_procs = 1
print("multiprocessing not available.")
if num_procs == 0:
print("Detecting cpu_count")
try:
num_procs = multiprocessing.cpu_count()
except NotImplementedError:
print("Could not determine CPU Count. Using One process")
num_procs = 1
print("Running %d processes" % num_procs)
try:
compress_all(directory)
except KeyboardInterrupt:
print('')
print('Compression Aborted')