1#!/usr/local/bin/python3.8 2 3# This code is original from jsmin by Douglas Crockford, it was translated to 4# Python by Baruch Even. It was rewritten by Dave St.Germain for speed. 5# 6# The MIT License (MIT) 7# 8# Copyright (c) 2013 Dave St.Germain 9# 10# Permission is hereby granted, free of charge, to any person obtaining a copy 11# of this software and associated documentation files (the "Software"), to deal 12# in the Software without restriction, including without limitation the rights 13# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14# copies of the Software, and to permit persons to whom the Software is 15# furnished to do so, subject to the following conditions: 16# 17# The above copyright notice and this permission notice shall be included in 18# all copies or substantial portions of the Software. 19# 20# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26# THE SOFTWARE. 27 28 29import sys 30is_3 = sys.version_info >= (3, 0) 31if is_3: 32 import io 33else: 34 import StringIO 35 try: 36 import cStringIO 37 except ImportError: 38 cStringIO = None 39 40 41__all__ = ['jsmin', 'JavascriptMinify'] 42__version__ = '2.1.6' 43 44 45def jsmin(js, **kwargs): 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, **kwargs).minify() 61 return outs.getvalue() 62 63 64class 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, quote_chars="'\""): 71 self.ins = instream 72 self.outs = outstream 73 self.quote_chars = quote_chars 74 75 def minify(self, instream=None, outstream=None): 76 if instream and outstream: 77 self.ins, self.outs = instream, outstream 78 79 self.is_return = False 80 self.return_buf = '' 81 82 def write(char): 83 # all of this is to support literal regular expressions. 84 # sigh 85 if char in 'return': 86 self.return_buf += char 87 self.is_return = self.return_buf == 'return' 88 else: 89 self.return_buf = '' 90 self.outs.write(char) 91 if self.is_return: 92 self.return_buf = '' 93 94 read = self.ins.read 95 96 space_strings = "abcdefghijklmnopqrstuvwxyz"\ 97 "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\" 98 self.space_strings = space_strings 99 starters, enders = '{[(+-', '}])+-/' + self.quote_chars 100 newlinestart_strings = starters + space_strings + self.quote_chars 101 newlineend_strings = enders + space_strings + self.quote_chars 102 self.newlinestart_strings = newlinestart_strings 103 self.newlineend_strings = newlineend_strings 104 105 do_newline = False 106 do_space = False 107 escape_slash_count = 0 108 in_quote = '' 109 quote_buf = [] 110 111 previous = ';' 112 previous_non_space = ';' 113 next1 = read(1) 114 115 while next1: 116 next2 = read(1) 117 if in_quote: 118 quote_buf.append(next1) 119 120 if next1 == in_quote: 121 numslashes = 0 122 for c in reversed(quote_buf[:-1]): 123 if c != '\\': 124 break 125 else: 126 numslashes += 1 127 if numslashes % 2 == 0: 128 in_quote = '' 129 write(''.join(quote_buf)) 130 elif next1 in '\r\n': 131 next2, do_newline = self.newline( 132 previous_non_space, next2, do_newline) 133 elif next1 < '!': 134 if (previous_non_space in space_strings \ 135 or previous_non_space > '~') \ 136 and (next2 in space_strings or next2 > '~'): 137 do_space = True 138 elif previous_non_space in '-+' and next2 == previous_non_space: 139 # protect against + ++ or - -- sequences 140 do_space = True 141 elif self.is_return and next2 == '/': 142 # returning a regex... 143 write(' ') 144 elif next1 == '/': 145 if do_space: 146 write(' ') 147 if next2 == '/': 148 # Line comment: treat it as a newline, but skip it 149 next2 = self.line_comment(next1, next2) 150 next1 = '\n' 151 next2, do_newline = self.newline( 152 previous_non_space, next2, do_newline) 153 elif next2 == '*': 154 self.block_comment(next1, next2) 155 next2 = read(1) 156 if previous_non_space in space_strings: 157 do_space = True 158 next1 = previous 159 else: 160 if previous_non_space in '{(,=:[?!&|;' or self.is_return: 161 self.regex_literal(next1, next2) 162 # hackish: after regex literal next1 is still / 163 # (it was the initial /, now it's the last /) 164 next2 = read(1) 165 else: 166 write('/') 167 else: 168 if do_newline: 169 write('\n') 170 do_newline = False 171 do_space = False 172 if do_space: 173 do_space = False 174 write(' ') 175 176 write(next1) 177 if next1 in self.quote_chars: 178 in_quote = next1 179 quote_buf = [] 180 181 if next1 >= '!': 182 previous_non_space = next1 183 184 if next1 == '\\': 185 escape_slash_count += 1 186 else: 187 escape_slash_count = 0 188 189 previous = next1 190 next1 = next2 191 192 def regex_literal(self, next1, next2): 193 assert next1 == '/' # otherwise we should not be called! 194 195 self.return_buf = '' 196 197 read = self.ins.read 198 write = self.outs.write 199 200 in_char_class = False 201 202 write('/') 203 204 next = next2 205 while next != '/' or in_char_class: 206 write(next) 207 if next == '\\': 208 write(read(1)) # whatever is next is escaped 209 elif next == '[': 210 write(read(1)) # character class cannot be empty 211 in_char_class = True 212 elif next == ']': 213 in_char_class = False 214 next = read(1) 215 216 write('/') 217 218 def line_comment(self, next1, next2): 219 assert next1 == next2 == '/' 220 221 read = self.ins.read 222 223 while next1 and next1 not in '\r\n': 224 next1 = read(1) 225 while next1 and next1 in '\r\n': 226 next1 = read(1) 227 228 return next1 229 230 def block_comment(self, next1, next2): 231 assert next1 == '/' 232 assert next2 == '*' 233 234 read = self.ins.read 235 236 # Skip past first /* and avoid catching on /*/...*/ 237 next1 = read(1) 238 next2 = read(1) 239 while next1 != '*' or next2 != '/': 240 next1 = next2 241 next2 = read(1) 242 243 def newline(self, previous_non_space, next2, do_newline): 244 read = self.ins.read 245 246 if previous_non_space and ( 247 previous_non_space in self.newlineend_strings 248 or previous_non_space > '~'): 249 while 1: 250 if next2 < '!': 251 next2 = read(1) 252 if not next2: 253 break 254 else: 255 if next2 in self.newlinestart_strings \ 256 or next2 > '~' or next2 == '/': 257 do_newline = True 258 break 259 260 return next2, do_newline 261 262if __name__ == '__main__': 263 import sys, os, glob 264 265#for f in sys.argv[1:]: 266# with open(f, 'r') as js: 267# minifier = JavascriptMinify(js, sys.stdout) 268# minifier.minify() 269# sys.stdout.write('\n') 270 271 minifier = JavascriptMinify(sys.stdin, sys.stdout) 272 minifier.minify() 273 sys.stdout.write('\n') 274 275