diff --git a/python/jsmin/jsmin/__init__.py b/python/jsmin/jsmin/__init__.py new file mode 100644 index 0000000000000..e7cdce625b1d0 --- /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 diff --git a/python/jsmin/jsmin/test.py b/python/jsmin/jsmin/test.py new file mode 100644 index 0000000000000..19dd01a00ed32 --- /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() diff --git a/python/jsmin/setup.cfg b/python/jsmin/setup.cfg new file mode 100644 index 0000000000000..861a9f554263e --- /dev/null +++ b/python/jsmin/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/python/jsmin/setup.py b/python/jsmin/setup.py new file mode 100644 index 0000000000000..3042d9c14cddd --- /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 +)