jsmin.py
1 # This code is original from jsmin by Douglas Crockford, it was translated to 2 # Python by Baruch Even. It was rewritten by Dave St.Germain for speed. 3 # 4 # The MIT License (MIT) 5 # 6 # Copyright (c) 2013 Dave St.Germain 7 # 8 # Permission is hereby granted, free of charge, to any person obtaining a copy 9 # of this software and associated documentation files (the "Software"), to deal 10 # in the Software without restriction, including without limitation the rights 11 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 # copies of the Software, and to permit persons to whom the Software is 13 # furnished to do so, subject to the following conditions: 14 # 15 # The above copyright notice and this permission notice shall be included in 16 # all copies or substantial portions of the Software. 17 # 18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 # THE SOFTWARE. 25 26 27 import sys 28 is_3 = sys.version_info >= (3, 0) 29 if is_3: 30 import io 31 python_text_type = str 32 else: 33 import StringIO 34 try: 35 import cStringIO 36 except ImportError: 37 cStringIO = None 38 python_text_type = basestring 39 40 41 __all__ = ['jsmin', 'JavascriptMinify'] 42 __version__ = '2.0.9' 43 44 45 def jsmin(js): 46 """ 47 returns a minified version of the javascript string 48 """ 49 if not is_3: 50 if cStringIO and not isinstance(js, unicode): 51 # strings can use cStringIO for a 3x performance 52 # improvement, but unicode (in python2) cannot 53 klass = cStringIO.StringIO 54 else: 55 klass = StringIO.StringIO 56 else: 57 klass = io.StringIO 58 ins = klass(js) 59 outs = klass() 60 JavascriptMinify(ins, outs).minify() 61 return outs.getvalue() 62 63 64 class JavascriptMinify(object): 65 """ 66 Minify an input stream of javascript, writing 67 to an output stream 68 """ 69 70 def __init__(self, instream=None, outstream=None): 71 self.ins = instream 72 self.outs = outstream 73 74 def minify(self, instream=None, outstream=None): 75 if instream and outstream: 76 self.ins, self.outs = instream, outstream 77 78 self.is_return = False 79 self.return_buf = '' 80 81 def write(char): 82 # all of this is to support literal regular expressions. 83 # sigh 84 if str(char) in 'return': 85 self.return_buf += char 86 self.is_return = self.return_buf == 'return' 87 self.outs.write(char) 88 if self.is_return: 89 self.return_buf = '' 90 91 def read(n): 92 char = self.ins.read(n) 93 if not isinstance(char, python_text_type): 94 raise ValueError("ERROR: The script jsmin.py can only handle text input, but it received input of type %s" % type(char)) 95 return char 96 97 space_strings = "abcdefghijklmnopqrstuvwxyz"\ 98 "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\" 99 starters, enders = '{[(+-', '}])+-"\'' 100 newlinestart_strings = starters + space_strings 101 newlineend_strings = enders + space_strings 102 do_newline = False 103 do_space = False 104 escape_slash_count = 0 105 doing_single_comment = False 106 previous_before_comment = '' 107 doing_multi_comment = False 108 in_re = False 109 in_quote = '' 110 quote_buf = [] 111 112 previous = read(1) 113 if previous == '\\': 114 escape_slash_count += 1 115 next1 = read(1) 116 if previous == '/': 117 if next1 == '/': 118 doing_single_comment = True 119 elif next1 == '*': 120 doing_multi_comment = True 121 previous = next1 122 next1 = read(1) 123 else: 124 write(previous) 125 elif not previous: 126 return 127 elif str(previous) >= "!": 128 if str(previous) in "'\"": 129 in_quote = previous 130 write(previous) 131 previous_non_space = previous 132 else: 133 previous_non_space = ' ' 134 if not next1: 135 return 136 137 while 1: 138 next2 = read(1) 139 if not next2: 140 last = next1.strip() 141 if not (doing_single_comment or doing_multi_comment)\ 142 and last not in ('', '/'): 143 if in_quote: 144 write(''.join(quote_buf)) 145 write(last) 146 break 147 if doing_multi_comment: 148 if next1 == '*' and next2 == '/': 149 doing_multi_comment = False 150 next2 = read(1) 151 elif doing_single_comment: 152 if next1 in '\r\n': 153 doing_single_comment = False 154 while next2 in '\r\n': 155 next2 = read(1) 156 if not next2: 157 break 158 if previous_before_comment in ')}]': 159 do_newline = True 160 elif previous_before_comment in space_strings: 161 write('\n') 162 elif in_quote: 163 quote_buf.append(next1) 164 165 if next1 == in_quote: 166 numslashes = 0 167 for c in reversed(quote_buf[:-1]): 168 if c != '\\': 169 break 170 else: 171 numslashes += 1 172 if numslashes % 2 == 0: 173 in_quote = '' 174 write(''.join(quote_buf)) 175 elif str(next1) in '\r\n': 176 if previous_non_space in newlineend_strings \ 177 or previous_non_space > '~': 178 while 1: 179 if next2 < '!': 180 next2 = read(1) 181 if not next2: 182 break 183 else: 184 if next2 in newlinestart_strings \ 185 or next2 > '~' or next2 == '/': 186 do_newline = True 187 break 188 elif str(next1) < '!' and not in_re: 189 if (previous_non_space in space_strings \ 190 or previous_non_space > '~') \ 191 and (next2 in space_strings or next2 > '~'): 192 do_space = True 193 elif previous_non_space in '-+' and next2 == previous_non_space: 194 # protect against + ++ or - -- sequences 195 do_space = True 196 elif self.is_return and next2 == '/': 197 # returning a regex... 198 write(' ') 199 elif next1 == '/': 200 if do_space: 201 write(' ') 202 if in_re: 203 if previous != '\\' or (not escape_slash_count % 2) or next2 in 'gimy': 204 in_re = False 205 write('/') 206 elif next2 == '/': 207 doing_single_comment = True 208 previous_before_comment = previous_non_space 209 elif next2 == '*': 210 doing_multi_comment = True 211 previous = next1 212 next1 = next2 213 next2 = read(1) 214 else: 215 in_re = previous_non_space in '(,=:[?!&|' or self.is_return # literal regular expression 216 write('/') 217 else: 218 if do_space: 219 do_space = False 220 write(' ') 221 if do_newline: 222 write('\n') 223 do_newline = False 224 225 write(next1) 226 if not in_re and str(next1) in "'\"`": 227 in_quote = next1 228 quote_buf = [] 229 230 previous = next1 231 next1 = next2 232 233 if str(previous) >= '!': 234 previous_non_space = previous 235 236 if previous == '\\': 237 escape_slash_count += 1 238 else: 239 escape_slash_count = 0 240 241 if __name__ == '__main__': 242 minifier = JavascriptMinify(sys.stdin, sys.stdout) 243 minifier.minify() 244 sys.stdout.write('\n')