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 27import sys 28is_3 = sys.version_info >= (3, 0) 29if is_3: 30 import io 31else: 32 import StringIO 33 try: 34 import cStringIO 35 except ImportError: 36 cStringIO = None 37 38 39__all__ = ['jsmin', 'JavascriptMinify'] 40__version__ = '2.2.1' 41 42 43def jsmin(js, **kwargs): 44 """ 45 returns a minified version of the javascript string 46 """ 47 if not is_3: 48 if cStringIO and not isinstance(js, unicode): 49 # strings can use cStringIO for a 3x performance 50 # improvement, but unicode (in python2) cannot 51 klass = cStringIO.StringIO 52 else: 53 klass = StringIO.StringIO 54 else: 55 klass = io.StringIO 56 ins = klass(js) 57 outs = klass() 58 JavascriptMinify(ins, outs, **kwargs).minify() 59 return outs.getvalue() 60 61 62class JavascriptMinify(object): 63 """ 64 Minify an input stream of javascript, writing 65 to an output stream 66 """ 67 68 def __init__(self, instream=None, outstream=None, quote_chars="'\""): 69 self.ins = instream 70 self.outs = outstream 71 self.quote_chars = quote_chars 72 73 def minify(self, instream=None, outstream=None): 74 if instream and outstream: 75 self.ins, self.outs = instream, outstream 76 77 self.is_return = False 78 self.return_buf = '' 79 80 def write(char): 81 # all of this is to support literal regular expressions. 82 # sigh 83 if char in 'return': 84 self.return_buf += char 85 self.is_return = self.return_buf == 'return' 86 else: 87 self.return_buf = '' 88 self.is_return = self.is_return and char < '!' 89 self.outs.write(char) 90 if self.is_return: 91 self.return_buf = '' 92 93 read = self.ins.read 94 95 space_strings = "abcdefghijklmnopqrstuvwxyz"\ 96 "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\" 97 self.space_strings = space_strings 98 starters, enders = '{[(+-', '}])+-/' + self.quote_chars 99 newlinestart_strings = starters + space_strings + self.quote_chars 100 newlineend_strings = enders + space_strings + self.quote_chars 101 self.newlinestart_strings = newlinestart_strings 102 self.newlineend_strings = newlineend_strings 103 104 do_newline = False 105 do_space = False 106 escape_slash_count = 0 107 in_quote = '' 108 quote_buf = [] 109 110 previous = ';' 111 previous_non_space = ';' 112 next1 = read(1) 113 114 while next1: 115 next2 = read(1) 116 if in_quote: 117 quote_buf.append(next1) 118 119 if next1 == in_quote: 120 numslashes = 0 121 for c in reversed(quote_buf[:-1]): 122 if c != '\\': 123 break 124 else: 125 numslashes += 1 126 if numslashes % 2 == 0: 127 in_quote = '' 128 write(''.join(quote_buf)) 129 elif next1 in '\r\n': 130 next2, do_newline = self.newline( 131 previous_non_space, next2, do_newline) 132 elif next1 < '!': 133 if (previous_non_space in space_strings \ 134 or previous_non_space > '~') \ 135 and (next2 in space_strings or next2 > '~'): 136 do_space = True 137 elif previous_non_space in '-+' and next2 == previous_non_space: 138 # protect against + ++ or - -- sequences 139 do_space = True 140 elif self.is_return and next2 == '/': 141 # returning a regex... 142 write(' ') 143 elif next1 == '/': 144 if do_space: 145 write(' ') 146 if next2 == '/': 147 # Line comment: treat it as a newline, but skip it 148 next2 = self.line_comment(next1, next2) 149 next1 = '\n' 150 next2, do_newline = self.newline( 151 previous_non_space, next2, do_newline) 152 elif next2 == '*': 153 self.block_comment(next1, next2) 154 next2 = read(1) 155 if previous_non_space in space_strings: 156 do_space = True 157 next1 = previous 158 else: 159 if previous_non_space in '{(,=:[?!&|;' or self.is_return: 160 self.regex_literal(next1, next2) 161 # hackish: after regex literal next1 is still / 162 # (it was the initial /, now it's the last /) 163 next2 = read(1) 164 else: 165 write('/') 166 else: 167 if do_newline: 168 write('\n') 169 do_newline = False 170 do_space = False 171 if do_space: 172 do_space = False 173 write(' ') 174 175 write(next1) 176 if next1 in self.quote_chars: 177 in_quote = next1 178 quote_buf = [] 179 180 if next1 >= '!': 181 previous_non_space = next1 182 183 if next1 == '\\': 184 escape_slash_count += 1 185 else: 186 escape_slash_count = 0 187 188 previous = next1 189 next1 = next2 190 191 def regex_literal(self, next1, next2): 192 assert next1 == '/' # otherwise we should not be called! 193 194 self.return_buf = '' 195 196 read = self.ins.read 197 write = self.outs.write 198 199 in_char_class = False 200 201 write('/') 202 203 next = next2 204 while next and (next != '/' or in_char_class): 205 write(next) 206 if next == '\\': 207 write(read(1)) # whatever is next is escaped 208 elif next == '[': 209 write(read(1)) # character class cannot be empty 210 in_char_class = True 211 elif next == ']': 212 in_char_class = False 213 next = read(1) 214 215 write('/') 216 217 def line_comment(self, next1, next2): 218 assert next1 == next2 == '/' 219 220 read = self.ins.read 221 222 while next1 and next1 not in '\r\n': 223 next1 = read(1) 224 while next1 and next1 in '\r\n': 225 next1 = read(1) 226 227 return next1 228 229 def block_comment(self, next1, next2): 230 assert next1 == '/' 231 assert next2 == '*' 232 233 read = self.ins.read 234 235 # Skip past first /* and avoid catching on /*/...*/ 236 next1 = read(1) 237 next2 = read(1) 238 239 comment_buffer = '/*' 240 while next1 != '*' or next2 != '/': 241 comment_buffer += next1 242 next1 = next2 243 next2 = read(1) 244 245 if comment_buffer.startswith("/*!"): 246 # comment needs preserving 247 self.outs.write(comment_buffer) 248 self.outs.write("*/\n") 249 250 251 def newline(self, previous_non_space, next2, do_newline): 252 read = self.ins.read 253 254 if previous_non_space and ( 255 previous_non_space in self.newlineend_strings 256 or previous_non_space > '~'): 257 while 1: 258 if next2 < '!': 259 next2 = read(1) 260 if not next2: 261 break 262 else: 263 if next2 in self.newlinestart_strings \ 264 or next2 > '~' or next2 == '/': 265 do_newline = True 266 break 267 268 return next2, do_newline 269