• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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