# This code is original from jsmin by Douglas Crockford, it was translated to # Python by Baruch Even. It was rewritten by Dave St.Germain for speed. # # The MIT License (MIT) # # Copyright (c) 2013 Dave St.Germain # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import sys is_3 = sys.version_info >= (3, 0) if is_3: import io else: import StringIO try: import cStringIO except ImportError: cStringIO = None __all__ = ['jsmin', 'JavascriptMinify'] __version__ = '2.2.0' def jsmin(js, **kwargs): """ returns a minified version of the javascript string """ if not is_3: if cStringIO and not isinstance(js, unicode): # strings can use cStringIO for a 3x performance # improvement, but unicode (in python2) cannot klass = cStringIO.StringIO else: klass = StringIO.StringIO else: klass = io.StringIO ins = klass(js) outs = klass() JavascriptMinify(ins, outs, **kwargs).minify() return outs.getvalue() class JavascriptMinify(object): """ Minify an input stream of javascript, writing to an output stream """ def __init__(self, instream=None, outstream=None, quote_chars="'\""): self.ins = instream self.outs = outstream self.quote_chars = quote_chars def minify(self, instream=None, outstream=None): if instream and outstream: self.ins, self.outs = instream, outstream self.is_return = False self.return_buf = '' def write(char): # all of this is to support literal regular expressions. # sigh if char in 'return': self.return_buf += char self.is_return = self.return_buf == 'return' else: self.return_buf = '' self.outs.write(char) if self.is_return: self.return_buf = '' read = self.ins.read space_strings = "abcdefghijklmnopqrstuvwxyz"\ "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\" self.space_strings = space_strings starters, enders = '{[(+-', '}])+-/' + self.quote_chars newlinestart_strings = starters + space_strings + self.quote_chars newlineend_strings = enders + space_strings + self.quote_chars self.newlinestart_strings = newlinestart_strings self.newlineend_strings = newlineend_strings do_newline = False do_space = False escape_slash_count = 0 in_quote = '' quote_buf = [] previous = ';' previous_non_space = ';' next1 = read(1) while next1: next2 = read(1) if in_quote: quote_buf.append(next1) if next1 == in_quote: numslashes = 0 for c in reversed(quote_buf[:-1]): if c != '\\': break else: numslashes += 1 if numslashes % 2 == 0: in_quote = '' write(''.join(quote_buf)) elif next1 in '\r\n': next2, do_newline = self.newline( previous_non_space, next2, do_newline) elif next1 < '!': if (previous_non_space in space_strings \ or previous_non_space > '~') \ and (next2 in space_strings or next2 > '~'): do_space = True elif previous_non_space in '-+' and next2 == previous_non_space: # protect against + ++ or - -- sequences do_space = True elif self.is_return and next2 == '/': # returning a regex... write(' ') elif next1 == '/': if do_space: write(' ') if next2 == '/': # Line comment: treat it as a newline, but skip it next2 = self.line_comment(next1, next2) next1 = '\n' next2, do_newline = self.newline( previous_non_space, next2, do_newline) elif next2 == '*': self.block_comment(next1, next2) next2 = read(1) if previous_non_space in space_strings: do_space = True next1 = previous else: if previous_non_space in '{(,=:[?!&|;' or self.is_return: self.regex_literal(next1, next2) # hackish: after regex literal next1 is still / # (it was the initial /, now it's the last /) next2 = read(1) else: write('/') else: if do_newline: write('\n') do_newline = False do_space = False if do_space: do_space = False write(' ') write(next1) if next1 in self.quote_chars: in_quote = next1 quote_buf = [] if next1 >= '!': previous_non_space = next1 if next1 == '\\': escape_slash_count += 1 else: escape_slash_count = 0 previous = next1 next1 = next2 def regex_literal(self, next1, next2): assert next1 == '/' # otherwise we should not be called! self.return_buf = '' read = self.ins.read write = self.outs.write in_char_class = False write('/') next = next2 while next != '/' or in_char_class: write(next) if next == '\\': write(read(1)) # whatever is next is escaped elif next == '[': write(read(1)) # character class cannot be empty in_char_class = True elif next == ']': in_char_class = False next = read(1) write('/') def line_comment(self, next1, next2): assert next1 == next2 == '/' read = self.ins.read while next1 and next1 not in '\r\n': next1 = read(1) while next1 and next1 in '\r\n': next1 = read(1) return next1 def block_comment(self, next1, next2): assert next1 == '/' assert next2 == '*' read = self.ins.read # Skip past first /* and avoid catching on /*/...*/ next1 = read(1) next2 = read(1) comment_buffer = '/*' while next1 != '*' or next2 != '/': comment_buffer += next1 next1 = next2 next2 = read(1) if comment_buffer.startswith("/*!"): # comment needs preserving self.outs.write(comment_buffer) self.outs.write("*/\n") def newline(self, previous_non_space, next2, do_newline): read = self.ins.read if previous_non_space and ( previous_non_space in self.newlineend_strings or previous_non_space > '~'): while 1: if next2 < '!': next2 = read(1) if not next2: break else: if next2 in self.newlinestart_strings \ or next2 > '~' or next2 == '/': do_newline = True break return next2, do_newline