From 856b495995be279888eb7844d78e1931a85cfc09 Mon Sep 17 00:00:00 2001
From: Ruud <ruud@crashdummy.nl>
Date: Mon, 4 Feb 2013 21:48:02 +0100
Subject: [PATCH] Minifier

---
 couchpotato/core/_base/clientscript/main.py   | 128 +++++++++-
 couchpotato/core/plugins/base.py              |   2 +-
 couchpotato/core/plugins/file/main.py         |   2 +-
 .../static/scripts/library/prefix_free.js     |  22 +-
 .../static/style/{page => }/settings.css      |   0
 couchpotato/templates/_desktop.html           |  43 +---
 libs/minify/__init__.py                       |   0
 libs/minify/cssmin.py                         | 223 ++++++++++++++++++
 libs/minify/jsmin.py                          | 218 +++++++++++++++++
 9 files changed, 586 insertions(+), 52 deletions(-)
 rename couchpotato/static/style/{page => }/settings.css (100%)
 create mode 100644 libs/minify/__init__.py
 create mode 100644 libs/minify/cssmin.py
 create mode 100644 libs/minify/jsmin.py

diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript/main.py
index bb380be6..e8624b0a 100644
--- a/couchpotato/core/_base/clientscript/main.py
+++ b/couchpotato/core/_base/clientscript/main.py
@@ -1,15 +1,58 @@
 from couchpotato.core.event import addEvent
+from couchpotato.core.helpers.variable import tryInt
 from couchpotato.core.logger import CPLog
 from couchpotato.core.plugins.base import Plugin
+from couchpotato.environment import Env
+from minify.cssmin import cssmin
+from minify.jsmin import jsmin
+import os
 
 log = CPLog(__name__)
 
 
 class ClientScript(Plugin):
 
-    urls = {
-        'style': {},
-        'script': {},
+    core_static = {
+        'style': [
+            'style/main.css',
+            'style/uniform.generic.css',
+            'style/uniform.css',
+            'style/settings.css',
+        ],
+        'script': [
+            'scripts/library/mootools.js',
+            'scripts/library/mootools_more.js',
+            'scripts/library/prefix_free.js',
+            'scripts/library/uniform.js',
+            'scripts/library/form_replacement/form_check.js',
+            'scripts/library/form_replacement/form_radio.js',
+            'scripts/library/form_replacement/form_dropdown.js',
+            'scripts/library/form_replacement/form_selectoption.js',
+            'scripts/library/question.js',
+            'scripts/library/scrollspy.js',
+            'scripts/library/spin.js',
+            'scripts/couchpotato.js',
+            'scripts/api.js',
+            'scripts/library/history.js',
+            'scripts/page.js',
+            'scripts/block.js',
+            'scripts/block/navigation.js',
+            'scripts/block/footer.js',
+            'scripts/block/menu.js',
+            'scripts/page/wanted.js',
+            'scripts/page/settings.js',
+            'scripts/page/about.js',
+            'scripts/page/manage.js',
+        ],
+    }
+
+
+    urls = {'style': {}, 'script': {}, }
+    minified = {'style': {}, 'script': {}, }
+    paths = {'style': {}, 'script': {}, }
+    comment = {
+       'style': '/*** %s:%d ***/\n',
+       'script': '// %s:%d\n'
     }
 
     html = {
@@ -24,6 +67,66 @@ class ClientScript(Plugin):
         addEvent('clientscript.get_styles', self.getStyles)
         addEvent('clientscript.get_scripts', self.getScripts)
 
+        addEvent('app.load', self.minify)
+
+        self.addCore()
+
+    def addCore(self):
+
+        for static_type in self.core_static:
+            for rel_path in self.core_static.get(static_type):
+                file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path)
+                core_url = 'api/%s/static/%s?%s' % (Env.setting('api_key'), rel_path, tryInt(os.path.getmtime(file_path)))
+
+                if static_type == 'script':
+                    self.registerScript(core_url, file_path, position = 'front')
+                else:
+                    self.registerStyle(core_url, file_path, position = 'front')
+
+
+    def minify(self):
+
+        for file_type in ['style', 'script']:
+            ext = 'js' if file_type is 'script' else 'css'
+            positions = self.paths.get(file_type, {})
+            for position in positions:
+                files = positions.get(position)
+                self._minify(file_type, files, position, position + '.' + ext)
+
+    def _minify(self, file_type, files, position, out):
+
+        cache = Env.get('cache_dir')
+        out_name = 'minified_' + out
+        out = os.path.join(cache, out_name)
+
+        raw = []
+        for file_path in files:
+            f = open(file_path, 'r').read()
+
+            if file_type == 'script':
+                data = jsmin(f)
+            else:
+                data = cssmin(f)
+                data = data.replace('../images/', '../static/images/')
+
+            raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
+
+        # Combine all files together with some comments
+        data = ''
+        for r in raw:
+            data += self.comment.get(file_type) % (r.get('file'), r.get('date'))
+            data += r.get('data') + '\n\n'
+
+        self.createFile(out, data.strip())
+
+        if not self.minified.get(file_type):
+            self.minified[file_type] = {}
+        if not self.minified[file_type].get(position):
+            self.minified[file_type][position] = []
+
+        minified_url = 'api/%s/file.cache/%s?%s' % (Env.setting('api_key'), out_name, tryInt(os.path.getmtime(out)))
+        self.minified[file_type][position].append(minified_url)
+
     def getStyles(self, *args, **kwargs):
         return self.get('style', *args, **kwargs)
 
@@ -35,22 +138,27 @@ class ClientScript(Plugin):
         data = '' if as_html else []
 
         try:
+            if Env.get('dev'):
+                return self.minified[type][location]
+
             return self.urls[type][location]
         except Exception, e:
             log.error(e)
 
         return data
 
-    def registerStyle(self, path, position = 'head'):
-        self.register(path, 'style', position)
+    def registerStyle(self, api_path, file_path, position = 'head'):
+        self.register(api_path, file_path, 'style', position)
 
-    def registerScript(self, path, position = 'head'):
-        self.register(path, 'script', position)
+    def registerScript(self, api_path, file_path, position = 'head'):
+        self.register(api_path, file_path, 'script', position)
 
-    def register(self, filepath, type, location):
+    def register(self, api_path, file_path, type, location):
 
         if not self.urls[type].get(location):
             self.urls[type][location] = []
+        self.urls[type][location].append(api_path)
 
-        filePath = filepath
-        self.urls[type][location].append(filePath)
+        if not self.paths[type].get(location):
+            self.paths[type][location] = []
+        self.paths[type][location].append(file_path)
diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py
index 73a2c306..edecaa94 100644
--- a/couchpotato/core/plugins/base.py
+++ b/couchpotato/core/plugins/base.py
@@ -64,7 +64,7 @@ class Plugin(object):
             for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
                 ext = getExt(f)
                 if ext in ['js', 'css']:
-                    fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f))
+                    fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
 
     def showStatic(self, filename):
         d = os.path.join(self.plugin_path, 'static')
diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py
index a9eab33d..0dc01783 100644
--- a/couchpotato/core/plugins/file/main.py
+++ b/couchpotato/core/plugins/file/main.py
@@ -71,7 +71,7 @@ class FileManager(Plugin):
             db = get_session()
             for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
                 for filename in walk_files:
-                    if root == python_cache: continue
+                    if root == python_cache or 'minified' in filename: continue
                     file_path = os.path.join(root, filename)
                     f = db.query(File).filter(File.path == toUnicode(file_path)).first()
                     if not f:
diff --git a/couchpotato/static/scripts/library/prefix_free.js b/couchpotato/static/scripts/library/prefix_free.js
index 8dd99e2e..b6d9812a 100644
--- a/couchpotato/static/scripts/library/prefix_free.js
+++ b/couchpotato/static/scripts/library/prefix_free.js
@@ -24,6 +24,9 @@ var self = window.StyleFix = {
 
 		var url = link.href || link.getAttribute('data-href'),
 		    base = url.replace(/[^\/]+$/, ''),
+		    base_scheme = (/^[a-z]{3,10}:/.exec(base) || [''])[0],
+		    base_domain = (/^[a-z]{3,10}:\/\/[^\/]+/.exec(base) || [''])[0],
+		    base_query = /^([^?]*)\??/.exec(url)[1],
 		    parent = link.parentNode,
 		    xhr = new XMLHttpRequest(),
 		    process;
@@ -43,12 +46,23 @@ var self = window.StyleFix = {
 					// Convert relative URLs to absolute, if needed
 					if(base) {
 						css = css.replace(/url\(\s*?((?:"|')?)(.+?)\1\s*?\)/gi, function($0, quote, url) {
-							if(!/^([a-z]{3,10}:|\/|#)/i.test(url)) { // If url not absolute & not a hash
+							if(/^([a-z]{3,10}:|#)/i.test(url)) { // Absolute & or hash-relative
+								return $0;
+							}
+							else if(/^\/\//.test(url)) { // Scheme-relative
 								// May contain sequences like /../ and /./ but those DO work
+								return 'url("' + base_scheme + url + '")';
+							}
+							else if(/^\//.test(url)) { // Domain-relative
+								return 'url("' + base_domain + url + '")';
+							}
+							else if(/^\?/.test(url)) { // Query-relative
+								return 'url("' + base_query + url + '")';
+							}
+							else {
+								// Path-relative
 								return 'url("' + base + url + '")';
 							}
-							
-							return $0;						
 						});
 
 						// behavior URLs shoudn’t be converted (Issue #19)
@@ -470,4 +484,4 @@ root.className += ' ' + self.prefix;
 StyleFix.register(self.prefixCSS);
 
 
-})(document.documentElement);
+})(document.documentElement);
\ No newline at end of file
diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/settings.css
similarity index 100%
rename from couchpotato/static/style/page/settings.css
rename to couchpotato/static/style/settings.css
diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html
index 8689b666..1d618066 100644
--- a/couchpotato/templates/_desktop.html
+++ b/couchpotato/templates/_desktop.html
@@ -1,43 +1,14 @@
 <!doctype html>
 <html>
 	<head>
-		<link rel="stylesheet" href="{{ url_for('web.static', filename='style/main.css') }}" type="text/css">
-		<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.generic.css') }}" type="text/css">
-		<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.css') }}" type="text/css">
-
-		<link rel="stylesheet" href="{{ url_for('web.static', filename='style/page/settings.css') }}" type="text/css">
-
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/mootools.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/mootools_more.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/prefix_free.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/uniform.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/form_replacement/form_check.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/form_replacement/form_radio.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/form_replacement/form_dropdown.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/form_replacement/form_selectoption.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/question.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/scrollspy.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/spin.js') }}"></script>
-
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/couchpotato.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/api.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/history.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/page.js') }}"></script>
-
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/block.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/block/navigation.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/block/footer.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/block/menu.js') }}"></script>
-
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/page/wanted.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/page/settings.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/page/about.js') }}"></script>
-		<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/page/manage.js') }}"></script>
-		<!--<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/page/soon.js') }}"></script>-->
-
-		{% for url in fireEvent('clientscript.get_scripts', as_html = True, single = True) %}
+		{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %}
+		<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %}
+		{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %}
+		<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %}
+
+		{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %}
 		<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %}
-		{% for url in fireEvent('clientscript.get_styles', as_html = True, single = True) %}
+		{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %}
 		<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %}
 
 		<link href="{{ url_for('web.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
diff --git a/libs/minify/__init__.py b/libs/minify/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libs/minify/cssmin.py b/libs/minify/cssmin.py
new file mode 100644
index 00000000..c29cb83b
--- /dev/null
+++ b/libs/minify/cssmin.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# `cssmin.py` - A Python port of the YUI CSS compressor.
+
+
+from StringIO import StringIO # The pure-Python StringIO supports unicode.
+import re
+
+
+__version__ = '0.1.1'
+
+
+def remove_comments(css):
+    """Remove all CSS comment blocks."""
+
+    iemac = False
+    preserve = False
+    comment_start = css.find("/*")
+    while comment_start >= 0:
+        # Preserve comments that look like `/*!...*/`.
+        # Slicing is used to make sure we don"t get an IndexError.
+        preserve = css[comment_start + 2:comment_start + 3] == "!"
+
+        comment_end = css.find("*/", comment_start + 2)
+        if comment_end < 0:
+            if not preserve:
+                css = css[:comment_start]
+                break
+        elif comment_end >= (comment_start + 2):
+            if css[comment_end - 1] == "\\":
+                # This is an IE Mac-specific comment; leave this one and the
+                # following one alone.
+                comment_start = comment_end + 2
+                iemac = True
+            elif iemac:
+                comment_start = comment_end + 2
+                iemac = False
+            elif not preserve:
+                css = css[:comment_start] + css[comment_end + 2:]
+            else:
+                comment_start = comment_end + 2
+        comment_start = css.find("/*", comment_start)
+
+    return css
+
+
+def remove_unnecessary_whitespace(css):
+    """Remove unnecessary whitespace characters."""
+
+    def pseudoclasscolon(css):
+
+        """
+        Prevents 'p :link' from becoming 'p:link'.
+        
+        Translates 'p :link' into 'p ___PSEUDOCLASSCOLON___link'; this is
+        translated back again later.
+        """
+
+        regex = re.compile(r"(^|\})(([^\{\:])+\:)+([^\{]*\{)")
+        match = regex.search(css)
+        while match:
+            css = ''.join([
+                css[:match.start()],
+                match.group().replace(":", "___PSEUDOCLASSCOLON___"),
+                css[match.end():]])
+            match = regex.search(css)
+        return css
+
+    css = pseudoclasscolon(css)
+    # Remove spaces from before things.
+    css = re.sub(r"\s+([!{};:>+\(\)\],])", r"\1", css)
+
+    # If there is a `@charset`, then only allow one, and move to the beginning.
+    css = re.sub(r"^(.*)(@charset \"[^\"]*\";)", r"\2\1", css)
+    css = re.sub(r"^(\s*@charset [^;]+;\s*)+", r"\1", css)
+
+    # Put the space back in for a few cases, such as `@media screen` and
+    # `(-webkit-min-device-pixel-ratio:0)`.
+    css = re.sub(r"\band\(", "and (", css)
+
+    # Put the colons back.
+    css = css.replace('___PSEUDOCLASSCOLON___', ':')
+
+    # Remove spaces from after things.
+    css = re.sub(r"([!{}:;>+\(\[,])\s+", r"\1", css)
+
+    return css
+
+
+def remove_unnecessary_semicolons(css):
+    """Remove unnecessary semicolons."""
+
+    return re.sub(r";+\}", "}", css)
+
+
+def remove_empty_rules(css):
+    """Remove empty rules."""
+
+    return re.sub(r"[^\}\{]+\{\}", "", css)
+
+
+def normalize_rgb_colors_to_hex(css):
+    """Convert `rgb(51,102,153)` to `#336699`."""
+
+    regex = re.compile(r"rgb\s*\(\s*([0-9,\s]+)\s*\)")
+    match = regex.search(css)
+    while match:
+        colors = match.group(1).split(",")
+        hexcolor = '#%.2x%.2x%.2x' % tuple(map(int, colors))
+        css = css.replace(match.group(), hexcolor)
+        match = regex.search(css)
+    return css
+
+
+def condense_zero_units(css):
+    """Replace `0(px, em, %, etc)` with `0`."""
+
+    return re.sub(r"([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", r"\1\2", css)
+
+
+def condense_multidimensional_zeros(css):
+    """Replace `:0 0 0 0;`, `:0 0 0;` etc. with `:0;`."""
+
+    css = css.replace(":0 0 0 0;", ":0;")
+    css = css.replace(":0 0 0;", ":0;")
+    css = css.replace(":0 0;", ":0;")
+
+    # Revert `background-position:0;` to the valid `background-position:0 0;`.
+    css = css.replace("background-position:0;", "background-position:0 0;")
+
+    return css
+
+
+def condense_floating_points(css):
+    """Replace `0.6` with `.6` where possible."""
+
+    return re.sub(r"(:|\s)0+\.(\d+)", r"\1.\2", css)
+
+
+def condense_hex_colors(css):
+    """Shorten colors from #AABBCC to #ABC where possible."""
+
+    regex = re.compile(r"([^\"'=\s])(\s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])")
+    match = regex.search(css)
+    while match:
+        first = match.group(3) + match.group(5) + match.group(7)
+        second = match.group(4) + match.group(6) + match.group(8)
+        if first.lower() == second.lower():
+            css = css.replace(match.group(), match.group(1) + match.group(2) + '#' + first)
+            match = regex.search(css, match.end() - 3)
+        else:
+            match = regex.search(css, match.end())
+    return css
+
+
+def condense_whitespace(css):
+    """Condense multiple adjacent whitespace characters into one."""
+
+    return re.sub(r"\s+", " ", css)
+
+
+def condense_semicolons(css):
+    """Condense multiple adjacent semicolon characters into one."""
+
+    return re.sub(r";;+", ";", css)
+
+
+def wrap_css_lines(css, line_length):
+    """Wrap the lines of the given CSS to an approximate length."""
+
+    lines = []
+    line_start = 0
+    for i, char in enumerate(css):
+        # It's safe to break after `}` characters.
+        if char == '}' and (i - line_start >= line_length):
+            lines.append(css[line_start:i + 1])
+            line_start = i + 1
+
+    if line_start < len(css):
+        lines.append(css[line_start:])
+    return '\n'.join(lines)
+
+
+def cssmin(css, wrap = None):
+    css = remove_comments(css)
+    css = condense_whitespace(css)
+    # A pseudo class for the Box Model Hack
+    # (see http://tantek.com/CSS/Examples/boxmodelhack.html)
+    css = css.replace('"\\"}\\""', "___PSEUDOCLASSBMH___")
+    #css = remove_unnecessary_whitespace(css)
+    css = remove_unnecessary_semicolons(css)
+    css = condense_zero_units(css)
+    css = condense_multidimensional_zeros(css)
+    css = condense_floating_points(css)
+    css = normalize_rgb_colors_to_hex(css)
+    css = condense_hex_colors(css)
+    if wrap is not None:
+        css = wrap_css_lines(css, wrap)
+    css = css.replace("___PSEUDOCLASSBMH___", '"\\"}\\""')
+    css = condense_semicolons(css)
+    return css.strip()
+
+
+def main():
+    import optparse
+    import sys
+
+    p = optparse.OptionParser(
+        prog = "cssmin", version = __version__,
+        usage = "%prog [--wrap N]",
+        description = """Reads raw CSS from stdin, and writes compressed CSS to stdout.""")
+
+    p.add_option(
+        '-w', '--wrap', type = 'int', default = None, metavar = 'N',
+        help = "Wrap output to approximately N chars per line.")
+
+    options, args = p.parse_args()
+    sys.stdout.write(cssmin(sys.stdin.read(), wrap = options.wrap))
+
+
+if __name__ == '__main__':
+    main()
diff --git a/libs/minify/jsmin.py b/libs/minify/jsmin.py
new file mode 100644
index 00000000..a1b81f9a
--- /dev/null
+++ b/libs/minify/jsmin.py
@@ -0,0 +1,218 @@
+#!/usr/bin/python
+
+# This code is original from jsmin by Douglas Crockford, it was translated to
+# Python by Baruch Even. The original code had the following copyright and
+# license.
+#
+# /* jsmin.c
+#    2007-05-22
+#
+# Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
+#
+# 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 shall be used for Good, not Evil.
+#
+# 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.
+# */
+
+from StringIO import StringIO
+
+def jsmin(js):
+    ins = StringIO(js)
+    outs = StringIO()
+    JavascriptMinify().minify(ins, outs)
+    str = outs.getvalue()
+    if len(str) > 0 and str[0] == '\n':
+        str = str[1:]
+    return str
+
+def isAlphanum(c):
+    """return true if the character is a letter, digit, underscore,
+           dollar sign, or non-ASCII character.
+    """
+    return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
+            (c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));
+
+class UnterminatedComment(Exception):
+    pass
+
+class UnterminatedStringLiteral(Exception):
+    pass
+
+class UnterminatedRegularExpression(Exception):
+    pass
+
+class JavascriptMinify(object):
+
+    def _outA(self):
+        self.outstream.write(self.theA)
+    def _outB(self):
+        self.outstream.write(self.theB)
+
+    def _get(self):
+        """return the next character from stdin. Watch out for lookahead. If
+           the character is a control character, translate it to a space or
+           linefeed.
+        """
+        c = self.theLookahead
+        self.theLookahead = None
+        if c == None:
+            c = self.instream.read(1)
+        if c >= ' ' or c == '\n':
+            return c
+        if c == '': # EOF
+            return '\000'
+        if c == '\r':
+            return '\n'
+        return ' '
+
+    def _peek(self):
+        self.theLookahead = self._get()
+        return self.theLookahead
+
+    def _next(self):
+        """get the next character, excluding comments. peek() is used to see
+           if a '/' is followed by a '/' or '*'.
+        """
+        c = self._get()
+        if c == '/':
+            p = self._peek()
+            if p == '/':
+                c = self._get()
+                while c > '\n':
+                    c = self._get()
+                return c
+            if p == '*':
+                c = self._get()
+                while 1:
+                    c = self._get()
+                    if c == '*':
+                        if self._peek() == '/':
+                            self._get()
+                            return ' '
+                    if c == '\000':
+                        raise UnterminatedComment()
+
+        return c
+
+    def _action(self, action):
+        """do something! What you do is determined by the argument:
+           1   Output A. Copy B to A. Get the next B.
+           2   Copy B to A. Get the next B. (Delete A).
+           3   Get the next B. (Delete B).
+           action treats a string as a single character. Wow!
+           action recognizes a regular expression if it is preceded by ( or , or =.
+        """
+        if action <= 1:
+            self._outA()
+
+        if action <= 2:
+            self.theA = self.theB
+            if self.theA == "'" or self.theA == '"':
+                while 1:
+                    self._outA()
+                    self.theA = self._get()
+                    if self.theA == self.theB:
+                        break
+                    if self.theA <= '\n':
+                        raise UnterminatedStringLiteral()
+                    if self.theA == '\\':
+                        self._outA()
+                        self.theA = self._get()
+
+
+        if action <= 3:
+            self.theB = self._next()
+            if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
+                                     self.theA == '=' or self.theA == ':' or
+                                     self.theA == '[' or self.theA == '?' or
+                                     self.theA == '!' or self.theA == '&' or
+                                     self.theA == '|' or self.theA == ';' or
+                                     self.theA == '{' or self.theA == '}' or
+                                     self.theA == '\n'):
+                self._outA()
+                self._outB()
+                while 1:
+                    self.theA = self._get()
+                    if self.theA == '/':
+                        break
+                    elif self.theA == '\\':
+                        self._outA()
+                        self.theA = self._get()
+                    elif self.theA <= '\n':
+                        raise UnterminatedRegularExpression()
+                    self._outA()
+                self.theB = self._next()
+
+
+    def _jsmin(self):
+        """Copy the input to the output, deleting the characters which are
+           insignificant to JavaScript. Comments will be removed. Tabs will be
+           replaced with spaces. Carriage returns will be replaced with linefeeds.
+           Most spaces and linefeeds will be removed.
+        """
+        self.theA = '\n'
+        self._action(3)
+
+        while self.theA != '\000':
+            if self.theA == ' ':
+                if isAlphanum(self.theB):
+                    self._action(1)
+                else:
+                    self._action(2)
+            elif self.theA == '\n':
+                if self.theB in ['{', '[', '(', '+', '-']:
+                    self._action(1)
+                elif self.theB == ' ':
+                    self._action(3)
+                else:
+                    if isAlphanum(self.theB):
+                        self._action(1)
+                    else:
+                        self._action(2)
+            else:
+                if self.theB == ' ':
+                    if isAlphanum(self.theA):
+                        self._action(1)
+                    else:
+                        self._action(3)
+                elif self.theB == '\n':
+                    if self.theA in ['}', ']', ')', '+', '-', '"', '\'']:
+                        self._action(1)
+                    else:
+                        if isAlphanum(self.theA):
+                            self._action(1)
+                        else:
+                            self._action(3)
+                else:
+                    self._action(1)
+
+    def minify(self, instream, outstream):
+        self.instream = instream
+        self.outstream = outstream
+        self.theA = '\n'
+        self.theB = None
+        self.theLookahead = None
+
+        self._jsmin()
+        self.instream.close()
+
+if __name__ == '__main__':
+    import sys
+    jsm = JavascriptMinify()
+    jsm.minify(sys.stdin, sys.stdout)
-- 
GitLab