Bug 903149 - Part 1: Add jsmin Python package; r=glandium
authorGregory Szorc <gps@mozilla.com>
Wed, 04 Sep 2013 18:47:42 -0700
changeset 183470 19f34c6aeb281b66e84c0a6fcd2156ade37c2358
parent 183469 cb0eed75619ed345a3747db1b02256f53d595aae
child 183471 92f909129ecb01ca0e8cff2c520be1ffe2344b42
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-esr52@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersglandium
bugs903149
milestone31.0a1
Bug 903149 - Part 1: Add jsmin Python package; r=glandium Exported source code from changeset fdfa1b187681 from https://bitbucket.org/dcs/jsmin without modifications.
python/jsmin/jsmin/__init__.py
python/jsmin/jsmin/test.py
python/jsmin/setup.cfg
python/jsmin/setup.py
new file mode 100644
--- /dev/null
+++ b/python/jsmin/jsmin/__init__.py
@@ -0,0 +1,195 @@
+# 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.0.3'
+
+
+def jsmin(js):
+    """
+    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).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):
+        self.ins = instream
+        self.outs = outstream
+
+    def minify(self, instream=None, outstream=None):
+        if instream and outstream:
+            self.ins, self.outs = instream, outstream
+        write = self.outs.write
+        read = self.ins.read
+
+        space_strings = "abcdefghijklmnopqrstuvwxyz"\
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\"
+        starters, enders = '{[(+-', '}])+-"\''
+        newlinestart_strings = starters + space_strings
+        newlineend_strings = enders + space_strings
+        do_newline = False
+        do_space = False
+        doing_single_comment = False
+        previous_before_comment = ''
+        doing_multi_comment = False
+        in_re = False
+        in_quote = ''
+        quote_buf = []
+
+        previous = read(1)
+        next1 = read(1)
+        if previous == '/':
+            if next1 == '/':
+                doing_single_comment = True
+            elif next1 == '*':
+                doing_multi_comment = True
+            else:
+                write(previous)
+        elif not previous:
+            return
+        elif previous >= '!':
+            if previous in "'\"":
+                in_quote = previous
+            write(previous)
+            previous_non_space = previous
+        else:
+            previous_non_space = ' '
+        if not next1:
+            return
+
+        while 1:
+            next2 = read(1)  
+            if not next2:
+                last = next1.strip()
+                if not (doing_single_comment or doing_multi_comment)\
+                    and last not in ('', '/'):
+                    write(last)
+                break
+            if doing_multi_comment:
+                if next1 == '*' and next2 == '/':
+                    doing_multi_comment = False
+                    next2 = read(1)
+            elif doing_single_comment:
+                if next1 in '\r\n':
+                    doing_single_comment = False
+                    while next2 in '\r\n':
+                        next2 = read(1)
+                        if not next2:
+                            break
+                    if previous_before_comment in ')}]':
+                        do_newline = True
+                    elif previous_before_comment in space_strings:
+                        write('\n')
+            elif 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':
+                if previous_non_space in newlineend_strings \
+                    or previous_non_space > '~':
+                    while 1:
+                        if next2 < '!':
+                            next2 = read(1)
+                            if not next2:
+                                break
+                        else:
+                            if next2 in newlinestart_strings \
+                                or next2 > '~' or next2 == '/':
+                                do_newline = True
+                            break
+            elif next1 < '!' and not in_re:
+                if (previous_non_space in space_strings \
+                    or previous_non_space > '~') \
+                    and (next2 in space_strings or next2 > '~'):
+                    do_space = True
+            elif next1 == '/':
+                if in_re:
+                    if previous != '\\':
+                        in_re = False
+                    write('/')
+                elif next2 == '/':
+                    doing_single_comment = True
+                    previous_before_comment = previous_non_space
+                elif next2 == '*':
+                    doing_multi_comment = True
+                else:
+                    in_re = previous_non_space in '(,=:[?!&|'
+                    write('/')
+            else:
+                if do_space:
+                    do_space = False
+                    write(' ')
+                if do_newline:
+                    write('\n')
+                    do_newline = False
+                write(next1)
+                if not in_re and next1 in "'\"":
+                    in_quote = next1
+                    quote_buf = []
+            previous = next1
+            next1 = next2
+
+            if previous >= '!':
+                previous_non_space = previous
new file mode 100644
--- /dev/null
+++ b/python/jsmin/jsmin/test.py
@@ -0,0 +1,252 @@
+import unittest
+import jsmin
+import sys
+
+class JsTests(unittest.TestCase):
+    def _minify(self, js):
+        return jsmin.jsmin(js)
+
+    def assertEqual(self, thing1, thing2):
+        if thing1 != thing2:
+            print(repr(thing1), repr(thing2))
+            raise AssertionError
+        return True
+    
+    def assertMinified(self, js_input, expected):
+        minified = jsmin.jsmin(js_input)
+        assert minified == expected, "%r != %r" % (minified, expected)
+        
+    def testQuoted(self):
+        js = r'''
+        Object.extend(String, {
+          interpret: function(value) {
+            return value == null ? '' : String(value);
+          },
+          specialChar: {
+            '\b': '\\b',
+            '\t': '\\t',
+            '\n': '\\n',
+            '\f': '\\f',
+            '\r': '\\r',
+            '\\': '\\\\'
+          }
+        });
+
+        '''
+        expected = r"""Object.extend(String,{interpret:function(value){return value==null?'':String(value);},specialChar:{'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','\\':'\\\\'}});"""
+        self.assertMinified(js, expected)
+
+    def testSingleComment(self):
+        js = r'''// use native browser JS 1.6 implementation if available
+        if (Object.isFunction(Array.prototype.forEach))
+          Array.prototype._each = Array.prototype.forEach;
+
+        if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
+
+        // hey there
+        function() {// testing comment
+        foo;
+        //something something
+
+        location = 'http://foo.com;';   // goodbye
+        }
+        //bye
+        '''
+        expected = r""" 
+if(Object.isFunction(Array.prototype.forEach))
+Array.prototype._each=Array.prototype.forEach;if(!Array.prototype.indexOf)Array.prototype.indexOf=function(item,i){ function(){ foo; location='http://foo.com;';}"""
+        # print expected
+        self.assertMinified(js, expected)
+    
+    def testEmpty(self):
+        self.assertMinified('', '')
+        self.assertMinified(' ', '')
+        self.assertMinified('\n', '')
+        self.assertMinified('\r\n', '')
+        self.assertMinified('\t', '')
+        
+        
+    def testMultiComment(self):
+        js = r"""
+        function foo() {
+            print('hey');
+        }
+        /*
+        if(this.options.zindex) {
+          this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+          this.element.style.zIndex = this.options.zindex;
+        }
+        */
+        another thing;
+        """
+        expected = r"""function foo(){print('hey');}
+another thing;"""
+        self.assertMinified(js, expected)
+    
+    def testLeadingComment(self):
+        js = r"""/* here is a comment at the top
+        
+        it ends here */
+        function foo() {
+            alert('crud');
+        }
+        
+        """
+        expected = r"""function foo(){alert('crud');}"""
+        self.assertMinified(js, expected)
+        
+    def testJustAComment(self):
+        self.assertMinified('     // a comment', '')
+        
+    def testRe(self):
+        js = r'''  
+        var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+        return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+        });'''
+        expected = r"""var str=this.replace(/\\./g,'@').replace(/"[^"\\\n\r]*"/g,'');return(/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);});"""
+        self.assertMinified(js, expected)
+
+    def testIgnoreComment(self):
+        js = r"""
+        var options_for_droppable = {
+          overlap:     options.overlap,
+          containment: options.containment,
+          tree:        options.tree,
+          hoverclass:  options.hoverclass,
+          onHover:     Sortable.onHover
+        }
+
+        var options_for_tree = {
+          onHover:      Sortable.onEmptyHover,
+          overlap:      options.overlap,
+          containment:  options.containment,
+          hoverclass:   options.hoverclass
+        }
+
+        // fix for gecko engine
+        Element.cleanWhitespace(element); 
+        """
+        expected = r"""var options_for_droppable={overlap:options.overlap,containment:options.containment,tree:options.tree,hoverclass:options.hoverclass,onHover:Sortable.onHover}
+var options_for_tree={onHover:Sortable.onEmptyHover,overlap:options.overlap,containment:options.containment,hoverclass:options.hoverclass} 
+Element.cleanWhitespace(element);"""
+        self.assertMinified(js, expected)
+
+    def testHairyRe(self):
+        js = r"""
+        inspect: function(useDoubleQuotes) {
+          var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+            var character = String.specialChar[match[0]];
+            return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+          });
+          if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+          return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+        },
+
+        toJSON: function() {
+          return this.inspect(true);
+        },
+
+        unfilterJSON: function(filter) {
+          return this.sub(filter || Prototype.JSONFilter, '#{1}');
+        },
+        """
+        expected = r"""inspect:function(useDoubleQuotes){var escapedString=this.gsub(/[\x00-\x1f\\]/,function(match){var character=String.specialChar[match[0]];return character?character:'\\u00'+match[0].charCodeAt().toPaddedString(2,16);});if(useDoubleQuotes)return'"'+escapedString.replace(/"/g,'\\"')+'"';return"'"+escapedString.replace(/'/g,'\\\'')+"'";},toJSON:function(){return this.inspect(true);},unfilterJSON:function(filter){return this.sub(filter||Prototype.JSONFilter,'#{1}');},"""
+        self.assertMinified(js, expected)
+
+    def testNoBracesWithComment(self):
+        js = r"""
+        onSuccess: function(transport) {
+            var js = transport.responseText.strip();
+            if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
+              throw 'Server returned an invalid collection representation.';
+            this._collection = eval(js);
+            this.checkForExternalText();
+          }.bind(this),
+          onFailure: this.onFailure
+        });
+        """
+        expected = r"""onSuccess:function(transport){var js=transport.responseText.strip();if(!/^\[.*\]$/.test(js)) 
+throw'Server returned an invalid collection representation.';this._collection=eval(js);this.checkForExternalText();}.bind(this),onFailure:this.onFailure});"""
+        self.assertMinified(js, expected)
+    
+    def testSpaceInRe(self):
+        js = r"""
+        num = num.replace(/ /g,'');
+        """
+        self.assertMinified(js, "num=num.replace(/ /g,'');")
+    
+    def testEmptyString(self):
+        js = r'''
+        function foo('') {
+        
+        }
+        '''
+        self.assertMinified(js, "function foo(''){}")
+    
+    def testDoubleSpace(self):
+        js = r'''
+var  foo    =  "hey";
+        '''
+        self.assertMinified(js, 'var foo="hey";')
+    
+    def testLeadingRegex(self):
+        js = r'/[d]+/g    '
+        self.assertMinified(js, js.strip())
+    
+    def testLeadingString(self):
+        js = r"'a string in the middle of nowhere'; // and a comment"
+        self.assertMinified(js, "'a string in the middle of nowhere';")
+    
+    def testSingleCommentEnd(self):
+        js = r'// a comment\n'
+        self.assertMinified(js, '')
+    
+    def testInputStream(self):
+        try:
+            from StringIO import StringIO
+        except ImportError:
+            from io import StringIO
+            
+        ins = StringIO(r'''
+            function foo('') {
+
+            }
+            ''')
+        outs = StringIO()
+        m = jsmin.JavascriptMinify()
+        m.minify(ins, outs)
+        output = outs.getvalue()
+        assert output == "function foo(''){}"
+    
+    def testUnicode(self):
+        instr = u'\u4000 //foo'
+        expected = u'\u4000'
+        output = jsmin.jsmin(instr)
+        self.assertEqual(output, expected)
+
+    def testCommentBeforeEOF(self):
+        self.assertMinified("//test\r\n", "")
+    
+    def testCommentInObj(self):
+        self.assertMinified("""{ 
+            a: 1,//comment
+            }""", "{a:1,}")
+
+    def testCommentInObj2(self):
+        self.assertMinified("{a: 1//comment\r\n}", "{a:1\n}")
+
+    def testImplicitSemicolon(self):
+        # return \n 1  is equivalent with   return; 1
+        # so best make sure jsmin retains the newline
+        self.assertMinified("return;//comment\r\na", "return;a")
+
+    def testImplicitSemicolon2(self):
+        self.assertMinified("return//comment...\r\na", "return\na")
+    
+    def testSingleComment2(self):
+        self.assertMinified('x.replace(/\//, "_")// slash to underscore',
+                'x.replace(/\//,"_")')
+
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/python/jsmin/setup.cfg
@@ -0,0 +1,5 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
new file mode 100644
--- /dev/null
+++ b/python/jsmin/setup.py
@@ -0,0 +1,35 @@
+from setuptools import setup
+
+import os, sys, re
+
+os.environ['COPYFILE_DISABLE'] = 'true'  # this disables including resource forks in tar files on os x
+
+
+extra = {}
+if sys.version_info >= (3,0):
+    extra['use_2to3'] = True
+
+setup(
+    name="jsmin",
+    version=re.search(r'__version__ = ["\']([^"\']+)', open('jsmin/__init__.py').read()).group(1),
+    packages=['jsmin'],
+    description='JavaScript minifier.',
+    author='Dave St.Germain',
+    author_email='dave@st.germa.in',
+    test_suite='jsmin.test.JsTests',
+    license='MIT License',
+    url='https://bitbucket.org/dcs/jsmin/',
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Web Environment',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: MIT License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 3',
+        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+        'Topic :: Software Development :: Pre-processors',
+        'Topic :: Text Processing :: Filters',
+    ],
+    **extra
+)