5
5
"""JavaScript Minifier functions for CSS-HTML-JS-Minify."""
6
6
7
7
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
13
9
14
10
15
11
__all__ = ('js_minify' , )
16
12
17
13
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
-
45
14
46
15
class JavascriptMinify (object ):
16
+ """
17
+ Minify an input stream of javascript, writing
18
+ to an output stream
19
+ """
47
20
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
53
25
54
26
def minify (self , instream = None , outstream = None ):
55
- """Minify Javascript using StringIO."""
56
27
if instream and outstream :
57
28
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'
75
39
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 :
89
68
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 :
112
70
quote_buf .append (next1 )
113
71
114
72
if next1 == in_quote :
@@ -122,58 +80,152 @@ def minify(self, instream=None, outstream=None):
122
80
in_quote = ''
123
81
write ('' .join (quote_buf ))
124
82
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
142
92
do_space = True
93
+ elif self .is_return and next2 == '/' :
94
+ # returning a regex...
95
+ write (' ' )
143
96
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 )
151
105
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
153
111
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 ('/' )
156
119
else :
157
- if do_space :
158
- do_space = False
159
- write (' ' )
160
120
if do_newline :
161
121
write ('\n ' )
162
122
do_newline = False
123
+ do_space = False
124
+ if do_space :
125
+ do_space = False
126
+ write (' ' )
127
+
163
128
write (next1 )
164
- if not in_re and next1 in "' \" " :
129
+ if next1 in self . quote_chars :
165
130
in_quote = next1
166
131
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
+
167
141
previous = next1
168
142
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
171
221
172
222
173
223
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