Skip to content

Commit bb71d73

Browse files
authored
Update js_minifier.py
1 parent 8f72452 commit bb71d73

File tree

1 file changed

+181
-129
lines changed

1 file changed

+181
-129
lines changed

css_html_js_minify/js_minifier.py

Lines changed: 181 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -5,110 +5,68 @@
55
"""JavaScript Minifier functions for CSS-HTML-JS-Minify."""
66

77

8-
import re
9-
10-
from io import StringIO # pure-Python StringIO supports unicode.
11-
12-
from .css_minifier import condense_semicolons
8+
import io
139

1410

1511
__all__ = ('js_minify', )
1612

1713

18-
def remove_commented_lines(js):
19-
"""Force remove commented out lines from Javascript."""
20-
result = ""
21-
for line in js.splitlines():
22-
line = re.sub(r"/\*.*\*/" ,"" ,line) # (/*COMMENT */)
23-
line = re.sub(r"//.*","" ,line) # (//COMMENT)
24-
result += '\n'+line
25-
return result
26-
27-
28-
def simple_replacer_js(js):
29-
"""Force strip simple replacements from Javascript."""
30-
return condense_semicolons(js.replace("debugger;", ";").replace(
31-
";}", "}").replace("; ", ";").replace(" ;", ";").rstrip("\n;"))
32-
33-
34-
def js_minify_keep_comments(js):
35-
"""Return a minified version of the Javascript string."""
36-
ins, outs = StringIO(js), StringIO()
37-
JavascriptMinify(ins, outs).minify()
38-
return force_single_line_js(outs.getvalue())
39-
40-
41-
def force_single_line_js(js):
42-
"""Force Javascript to a single line, even if need to add semicolon."""
43-
return ";".join(js.splitlines()) if len(js.splitlines()) > 1 else js
44-
4514

4615
class JavascriptMinify(object):
16+
"""
17+
Minify an input stream of javascript, writing
18+
to an output stream
19+
"""
4720

48-
"""Minify an input stream of Javascript, writing to an output stream."""
49-
50-
def __init__(self, instream=None, outstream=None):
51-
"""Init class."""
52-
self.ins, self.outs = instream, outstream
21+
def __init__(self, instream=None, outstream=None, quote_chars="'\""):
22+
self.ins = instream
23+
self.outs = outstream
24+
self.quote_chars = quote_chars
5325

5426
def minify(self, instream=None, outstream=None):
55-
"""Minify Javascript using StringIO."""
5627
if instream and outstream:
5728
self.ins, self.outs = instream, outstream
58-
write, read = self.outs.write, self.ins.read
59-
space_strings = ("abcdefghijklmnopqrstuvwxyz"
60-
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\")
61-
starters, enders = '{[(+-', '}])+-"\''
62-
newlinestart_strings = starters + space_strings
63-
newlineend_strings = enders + space_strings
64-
do_newline, do_space = False, False
65-
doing_single_comment, doing_multi_comment = False, False
66-
previous_before_comment, in_quote = '', ''
67-
in_re, quote_buf = False, []
68-
previous = read(1)
69-
next1 = read(1)
70-
if previous == '/':
71-
if next1 == '/':
72-
doing_single_comment = True
73-
elif next1 == '*':
74-
doing_multi_comment = True
29+
30+
self.is_return = False
31+
self.return_buf = ''
32+
33+
def write(char):
34+
# all of this is to support literal regular expressions.
35+
# sigh
36+
if char in 'return':
37+
self.return_buf += char
38+
self.is_return = self.return_buf == 'return'
7539
else:
76-
write(previous)
77-
elif not previous:
78-
return
79-
elif previous >= '!':
80-
if previous in "'\"":
81-
in_quote = previous
82-
write(previous)
83-
previous_non_space = previous
84-
else:
85-
previous_non_space = ' '
86-
if not next1:
87-
return
88-
while True:
40+
self.return_buf = ''
41+
self.is_return = self.is_return and char < '!'
42+
self.outs.write(char)
43+
if self.is_return:
44+
self.return_buf = ''
45+
46+
read = self.ins.read
47+
48+
space_strings = "abcdefghijklmnopqrstuvwxyz" \
49+
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\"
50+
self.space_strings = space_strings
51+
starters, enders = '{[(+-', '}])+-/' + self.quote_chars
52+
newlinestart_strings = starters + space_strings + self.quote_chars
53+
newlineend_strings = enders + space_strings + self.quote_chars
54+
self.newlinestart_strings = newlinestart_strings
55+
self.newlineend_strings = newlineend_strings
56+
57+
do_newline = False
58+
do_space = False
59+
escape_slash_count = 0
60+
in_quote = ''
61+
quote_buf = []
62+
63+
previous = ';'
64+
previous_non_space = ';'
65+
next1 = read(1)
66+
67+
while next1:
8968
next2 = read(1)
90-
if not next2:
91-
last = next1.strip()
92-
conditional_1 = (doing_single_comment or doing_multi_comment)
93-
if not conditional_1 and last not in ('', '/'):
94-
write(last)
95-
break
96-
if doing_multi_comment:
97-
if next1 == '*' and next2 == '/':
98-
doing_multi_comment = False
99-
next2 = read(1)
100-
elif doing_single_comment:
101-
if next1 in '\r\n':
102-
doing_single_comment = False
103-
while next2 in '\r\n':
104-
next2 = read(1)
105-
if not next2:
106-
break
107-
if previous_before_comment in ')}]':
108-
do_newline = True
109-
elif previous_before_comment in space_strings:
110-
write('\n')
111-
elif in_quote:
69+
if in_quote:
11270
quote_buf.append(next1)
11371

11472
if next1 == in_quote:
@@ -122,58 +80,152 @@ def minify(self, instream=None, outstream=None):
12280
in_quote = ''
12381
write(''.join(quote_buf))
12482
elif next1 in '\r\n':
125-
conditional_2 = previous_non_space in newlineend_strings
126-
if conditional_2 or previous_non_space > '~':
127-
while 1:
128-
if next2 < '!':
129-
next2 = read(1)
130-
if not next2:
131-
break
132-
else:
133-
conditional_3 = next2 in newlinestart_strings
134-
if conditional_3 or next2 > '~' or next2 == '/':
135-
do_newline = True
136-
break
137-
elif next1 < '!' and not in_re:
138-
conditional_4 = next2 in space_strings or next2 > '~'
139-
conditional_5 = previous_non_space in space_strings
140-
conditional_6 = previous_non_space > '~'
141-
if (conditional_5 or conditional_6) and (conditional_4):
83+
next2, do_newline = self.newline(
84+
previous_non_space, next2, do_newline)
85+
elif next1 < '!':
86+
if (previous_non_space in space_strings \
87+
or previous_non_space > '~') \
88+
and (next2 in space_strings or next2 > '~'):
89+
do_space = True
90+
elif previous_non_space in '-+' and next2 == previous_non_space:
91+
# protect against + ++ or - -- sequences
14292
do_space = True
93+
elif self.is_return and next2 == '/':
94+
# returning a regex...
95+
write(' ')
14396
elif next1 == '/':
144-
if in_re:
145-
if previous != '\\':
146-
in_re = False
147-
write('/')
148-
elif next2 == '/':
149-
doing_single_comment = True
150-
previous_before_comment = previous_non_space
97+
if do_space:
98+
write(' ')
99+
if next2 == '/':
100+
# Line comment: treat it as a newline, but skip it
101+
next2 = self.line_comment(next1, next2)
102+
next1 = '\n'
103+
next2, do_newline = self.newline(
104+
previous_non_space, next2, do_newline)
151105
elif next2 == '*':
152-
doing_multi_comment = True
106+
self.block_comment(next1, next2)
107+
next2 = read(1)
108+
if previous_non_space in space_strings:
109+
do_space = True
110+
next1 = previous
153111
else:
154-
in_re = previous_non_space in '(,=:[?!&|'
155-
write('/')
112+
if previous_non_space in '{(,=:[?!&|;' or self.is_return:
113+
self.regex_literal(next1, next2)
114+
# hackish: after regex literal next1 is still /
115+
# (it was the initial /, now it's the last /)
116+
next2 = read(1)
117+
else:
118+
write('/')
156119
else:
157-
if do_space:
158-
do_space = False
159-
write(' ')
160120
if do_newline:
161121
write('\n')
162122
do_newline = False
123+
do_space = False
124+
if do_space:
125+
do_space = False
126+
write(' ')
127+
163128
write(next1)
164-
if not in_re and next1 in "'\"":
129+
if next1 in self.quote_chars:
165130
in_quote = next1
166131
quote_buf = []
132+
133+
if next1 >= '!':
134+
previous_non_space = next1
135+
136+
if next1 == '\\':
137+
escape_slash_count += 1
138+
else:
139+
escape_slash_count = 0
140+
167141
previous = next1
168142
next1 = next2
169-
if previous >= '!':
170-
previous_non_space = previous
143+
144+
def regex_literal(self, next1, next2):
145+
assert next1 == '/' # otherwise we should not be called!
146+
147+
self.return_buf = ''
148+
149+
read = self.ins.read
150+
write = self.outs.write
151+
152+
in_char_class = False
153+
154+
write('/')
155+
156+
next = next2
157+
while next and (next != '/' or in_char_class):
158+
write(next)
159+
if next == '\\':
160+
write(read(1)) # whatever is next is escaped
161+
elif next == '[':
162+
write(read(1)) # character class cannot be empty
163+
in_char_class = True
164+
elif next == ']':
165+
in_char_class = False
166+
next = read(1)
167+
168+
write('/')
169+
170+
def line_comment(self, next1, next2):
171+
assert next1 == next2 == '/'
172+
173+
read = self.ins.read
174+
175+
while next1 and next1 not in '\r\n':
176+
next1 = read(1)
177+
while next1 and next1 in '\r\n':
178+
next1 = read(1)
179+
180+
return next1
181+
182+
def block_comment(self, next1, next2):
183+
assert next1 == '/'
184+
assert next2 == '*'
185+
186+
read = self.ins.read
187+
188+
# Skip past first /* and avoid catching on /*/...*/
189+
next1 = read(1)
190+
next2 = read(1)
191+
192+
comment_buffer = '/*'
193+
while next1 != '*' or next2 != '/':
194+
comment_buffer += next1
195+
next1 = next2
196+
next2 = read(1)
197+
198+
if comment_buffer.startswith("/*!"):
199+
# comment needs preserving
200+
self.outs.write(comment_buffer)
201+
self.outs.write("*/\n")
202+
203+
def newline(self, previous_non_space, next2, do_newline):
204+
read = self.ins.read
205+
206+
if previous_non_space and (
207+
previous_non_space in self.newlineend_strings
208+
or previous_non_space > '~'):
209+
while 1:
210+
if next2 < '!':
211+
next2 = read(1)
212+
if not next2:
213+
break
214+
else:
215+
if next2 in self.newlinestart_strings \
216+
or next2 > '~' or next2 == '/':
217+
do_newline = True
218+
break
219+
220+
return next2, do_newline
171221

172222

173223
def js_minify(js):
174-
"""Minify a JavaScript string."""
175-
print("""Future JavaScript support is orphan and not supported!.
176-
If you want to make ES6,ES7 work feel free to send pull requests.""")
177-
js = remove_commented_lines(js)
178-
js = js_minify_keep_comments(js)
179-
return js.strip()
224+
"""
225+
returns a minified version of the javascript string
226+
"""
227+
klass = io.StringIO
228+
ins = klass(js)
229+
outs = klass()
230+
JavascriptMinify(ins, outs).minify()
231+
return outs.getvalue()

0 commit comments

Comments
 (0)