Commits

Mike Orr committed e5c29e3

Add Minification WebHelpers 3.1 as webhelpers.pylonslib.minify. (Original files; not working yet.)

Comments (0)

Files changed (3)

tests/test_pylonslib_minify.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+from unittest import TestCase
+
+import minwebhelpers
+from minwebhelpers import javascript_link, stylesheet_link, beaker_kwargs
+
+from fixtures import config, beaker_cache, fixture_path
+minwebhelpers.config = config
+minwebhelpers.beaker_cache = beaker_cache
+
+
+class MinificationTestCase(TestCase):
+
+    def purge_files(self, *files):
+        for file_ in files:
+            path = os.path.join(fixture_path, file_)
+            os.remove(path)
+
+    def test_paths(self):
+        """Testing if paths are constructed correctly"""
+        # minify and combine
+        js_source = javascript_link('/deep/a.js', '/b.js', combined=True, minified=True)
+        css_source = stylesheet_link('/deep/a.css', '/b.css', combined=True, minified=True)
+        self.assert_('"/a.b.COMBINED.min.css"' in css_source)
+        self.assert_('"/a.b.COMBINED.min.js"' in js_source)
+        
+        # combine
+        js_source = javascript_link('/deep/a.js', '/b.js', combined=True)
+        css_source = stylesheet_link('/deep/a.css', '/b.css', combined=True)
+        self.assert_('"/a.b.COMBINED.css"' in css_source)
+        self.assert_('"/a.b.COMBINED.js"' in js_source)
+
+        # minify
+        js_source = javascript_link('/deep/a.js', '/b.js', minified=True)
+        css_source = stylesheet_link('/deep/a.css', '/b.css', minified=True)
+        self.assert_('"/deep/a.min.css"' in css_source)
+        self.assert_('"/b.min.css"' in css_source)
+        self.assert_('"/deep/a.min.js"' in js_source)
+        self.assert_('"/b.min.js"' in js_source)
+
+        # root minify and combined
+        js_source = javascript_link('/c.js', '/b.js', combined=True, minified=True)
+        css_source = stylesheet_link('/c.css', '/b.css', combined=True, minified=True)
+        self.assert_('"/c.b.COMBINED.min.css"' in css_source)
+        self.assert_('"/c.b.COMBINED.min.js"' in js_source)
+
+        # root minify
+        js_source = javascript_link('/c.js', '/b.js', minified=True)
+        css_source = stylesheet_link('/c.css', '/b.css', minified=True)
+        self.assert_('"/b.min.css"' in css_source)
+        self.assert_('"/b.min.js"' in js_source)
+        self.assert_('"/c.min.js"' in js_source)
+        self.assert_('"/c.min.js"' in js_source)
+
+        # both root minify and combined
+        js_source = javascript_link('/deep/a.js', '/deep/d.js', combined=True, minified=True)
+        css_source = stylesheet_link('/deep/a.css', '/deep/d.css', combined=True, minified=True)
+        self.assert_('"/deep/a.d.COMBINED.min.css"' in css_source)
+        self.assert_('"/deep/a.d.COMBINED.min.js"' in js_source)
+
+        # Cleanup
+        self.purge_files('a.b.COMBINED.min.js', 'a.b.COMBINED.min.css')
+        self.purge_files('a.b.COMBINED.js', 'a.b.COMBINED.css')
+        self.purge_files('deep/a.min.css', 'deep/a.min.js', 'b.min.js', 'b.min.css')
+        self.purge_files('c.b.COMBINED.min.js', 'c.b.COMBINED.min.css')
+        #self.purge_files('b.min.js', 'b.min.css', 'c.min.js', 'c.min.css')
+        self.purge_files('deep/a.d.COMBINED.min.js', 'deep/a.d.COMBINED.min.css')
+
+    def test_beaker_kwargs(self):
+        """Testing for proper beaker kwargs usage"""
+        css_source = stylesheet_link('/deep/a.css', '/b.css', combined=True, minified=True)
+        from fixtures import beaker_container
+        self.assertEqual(beaker_container, beaker_kwargs)
+
+        css_source = stylesheet_link('/deep/a.css', '/b.css', combined=True, minified=True, beaker_kwargs={'foo': 'bar'})
+        from fixtures import beaker_container
+        beaker_kwargs.update({'foo': 'bar'})
+        self.assertEqual(beaker_container, beaker_kwargs)

webhelpers/pylonslib/jsmin.py

+#!/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)

webhelpers/pylonslib/minify.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vim: sw=4 ts=4 fenc=utf-8
+
+import re
+import os
+import logging
+import StringIO
+
+import cssutils
+from jsmin import JavascriptMinify
+from cssutils.serialize import CSSSerializer
+from pylons import config
+from pylons.decorators.cache import beaker_cache
+
+from webhelpers.html.tags import javascript_link as __javascript_link
+from webhelpers.html.tags import stylesheet_link as __stylesheet_link
+
+
+__all__ = ['javascript_link', 'stylesheet_link']
+log = logging.getLogger(__name__)
+beaker_kwargs = dict(key='sources',
+                     expire='never',
+                     type='memory')
+
+def combine_sources(sources, ext, fs_root):
+    if len(sources) < 2:
+        return sources
+
+    names = list()
+    js_buffer = StringIO.StringIO()
+    base = os.path.commonprefix([os.path.dirname(s) for s in sources])
+
+    for source in sources:
+        # get a list of all filenames without extensions
+        js_file = os.path.basename(source)
+        js_file_name = os.path.splitext(js_file)[0]
+        names.append(js_file_name)
+
+        # build a master file with all contents
+        full_source = os.path.join(fs_root, source.lstrip('/'))
+        f = open(full_source, 'r')
+        js_buffer.write(f.read())
+        js_buffer.write('\n')
+        f.close()
+
+    # glue a new name and generate path to it
+    fname = '.'.join(names + ['COMBINED', ext])
+    fpath = os.path.join(fs_root, base.strip('/'), fname)
+
+    # write the combined file
+    f = open(fpath, 'w')
+    f.write(js_buffer.getvalue())
+    f.close()
+
+    return [os.path.join(base, fname)]
+
+def minify_sources(sources, ext, fs_root=''):
+    if 'js' in ext:
+        js_minify = JavascriptMinify()
+    minified_sources = []
+
+    for source in sources:
+        # generate full path to source
+        no_ext_source = os.path.splitext(source)[0]
+        full_source = os.path.join(fs_root, (no_ext_source + ext).lstrip('/'))
+
+        # generate minified source path
+        full_source = os.path.join(fs_root, (source).lstrip('/'))
+        no_ext_full_source = os.path.splitext(full_source)[0]
+        minified = no_ext_full_source + ext
+
+        f_minified_source = open(minified, 'w')
+
+        # minify js source (read stream is auto-closed inside)
+        if 'js' in ext:
+            js_minify.minify(open(full_source, 'r'), f_minified_source)
+        # minify css source
+        if 'css' in ext:
+            sheet = cssutils.parseFile(full_source)
+            sheet.setSerializer(CSSUtilsMinificationSerializer())
+            cssutils.ser.prefs.useMinified()
+            f_minified_source.write(sheet.cssText)
+
+        f_minified_source.close()
+        minified_sources.append(no_ext_source + ext)
+
+    return minified_sources
+
+def base_link(ext, *sources, **options):
+    combined = options.pop('combined', False)
+    minified = options.pop('minified', False)
+    beaker_options = options.pop('beaker_kwargs', False)
+    fs_root = config.get('pylons.paths').get('static_files')
+
+    if not (config.get('debug', False) or options.get('builtins', False)):
+        if beaker_options:
+            beaker_kwargs.update(beaker_options)
+
+        if combined:
+            sources = beaker_cache(**beaker_kwargs)(combine_sources)(list(sources), ext, fs_root)
+
+        if minified:
+            sources = beaker_cache(**beaker_kwargs)(minify_sources)(list(sources), '.min.' + ext, fs_root)
+
+    if 'js' in ext:
+        return __javascript_link(*sources, **options)
+    if 'css' in ext:
+        return __stylesheet_link(*sources, **options)
+
+def javascript_link(*sources, **options):
+    return base_link('js', *sources, **options)
+
+def stylesheet_link(*sources, **options):
+    return base_link('css', *sources, **options)
+
+
+class CSSUtilsMinificationSerializer(CSSSerializer):
+    def __init__(self, prefs=None):
+        CSSSerializer.__init__(self, prefs)
+
+    def do_css_CSSStyleDeclaration(self, style, separator=None):
+        try:
+            color = style.getPropertyValue('color')
+            if color and color is not u'':
+                color = self.change_colors(color)
+                style.setProperty('color', color)
+        except:
+            pass
+        return re.sub(r'0\.([\d])+', r'.\1',
+                      re.sub(r'(([^\d][0])+(px|em)+)+', r'\2',
+                      CSSSerializer.do_css_CSSStyleDeclaration(self, style,
+                                                               separator)))
+
+    def change_colors(self, color):
+        colours = {
+            'black': '#000000',
+            'fuchia': '#ff00ff',
+            'yellow': '#ffff00',
+            '#808080': 'gray',
+            '#008000': 'green',
+            '#800000': 'maroon',
+            '#000800': 'navy',
+            '#808000': 'olive',
+            '#800080': 'purple',
+            '#ff0000': 'red',
+            '#c0c0c0': 'silver',
+            '#008080': 'teal'
+        }
+        if color.lower() in colours:
+            color = colours[color.lower()]
+
+        if color.startswith('#') and len(color) == 7:
+            if color[1]==color[2] and color[3]==color[4] and color[5]==color[6]:
+                color = '#%s%s%s' % (color[1], color[3], color[5])
+        return color