/ Scripts / jsmin.py
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')