1. Wolfgang Scherer
  2. WebHelpersWs


WebHelpersWs / webhelpers / pylonslib / minify.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim: sw=4 ts=4 fenc=utf-8
"""Minification helpers.

This module provides enhanced versions of the ``javascript_link`` and
``stylesheet_link`` helpers in ``webhelpers.html.tags``.  These versions add
three additional arguments:

* **minified**: If true, reduce the file size by squeezing out
  whitespace and other characters insignificant to the Javascript or CSS syntax.
* **combined**: If true, concatenate the specified files into one file to
  reduce page load time.
* **beaker_kwargs** (dict): arguments to pass to ``beaker_cache``.

Dependencies: ``Pylons``, ``Beaker``, ``jsmin``, and ``cssutils`` (all
available in PyPI). If "jsmin" is not installed, the helper issues a warning
and passes Javascript through unchanged. (Changed in WebHelpers 1.1: removed
built-in "_jsmin" package due to licensing issues; details in 
webhelpers/pylonslib/_jsmin.py .)

PYRAMID USERS: this implementation is incompatible with Pyramid. No
Pyramid-compatible implementation is currently known.

Contributed by Pedro Algarvio and Domen Kozar <ufs@ufsoft.org>.
URL: http://docs.fubar.si/minwebhelpers/

import re
import os
import logging
import StringIO
import warnings

# hide from 2to3
def isstring(obj):
    return isinstance(obj, basestring)
except NameError:
    def isstring(obj):
        return isinstance(obj, str) or isinstance(obj, bytes)

def _ucs(string, charset=None):                            # ||:fnc:||
    return unicode(string, charset or 'utf-8')
except NameError:
    _ucs = lambda s, c=None: s.decode(c or 'utf-8')

uc_type = type(_ucs(b""))

def ucs(value, charset=None):                              # ||:fnc:||
    Convert `value` to unicode string using charset or UTF-8, if
    `value` is a string.

    If `value` is not a string, or if it is already a unicode string,
    return it unmodified.
    if isstring(value) and not isinstance(value, uc_type):
        return _ucs(value, charset)
    return value

def u8s(string):                                           # ||:fnc:||
    Convert `string` to UTF-8-encoded byte string, if `string` is a
    unicode string.

    If `string` is not a unicode string, return it unmodified.
    if isinstance(string, uc_type):
        return string.encode('utf-8')
    return string

def nts(string):                                           # ||:fnc:||
    Convert string to native string, if applicable.

    Python2 native strings are byte strings, while Python3 native
    strings are unicode.
    # for python3, unicode strings have type str
    if isinstance(string, str):
        return string
    # for python2, encode unicode strings to utf-8 strings
    if isinstance(string, uc_type):
        return string.encode('utf-8')
    if isstring(string):
            return str(string.decode('utf-8'))
        except UnicodeDecodeError:
            #return str(string.decode('latin1'))
    return string

from webhelpers.html.tags import javascript_link as __javascript_link
from webhelpers.html.tags import stylesheet_link as __stylesheet_link

    from jsmin import JavascriptMinify
except ImportError:
    class JavascriptMinify(object):
        def minify(self, instream, outstream):
            warnings.warn(JSMIN_MISSING_MESSAGE, UserWarning)
            data = instream.read()

_jsmin has been removed from WebHelpers due to licensing issues
Your Javascript code has been passed through unchanged.
You can install the "jsmin" package from PyPI, and this helper will use it.

__all__ = ['javascript_link', 'stylesheet_link']
log = logging.getLogger(__name__)
beaker_kwargs = dict(key='sources',

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]

        # build a master file with all contents
        full_source = os.path.join(fs_root, source.lstrip('/'))
        f = open(full_source, 'r')

    # 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')

    return [os.path.join(base, fname)]

def minify_sources(sources, ext, fs_root=''):
    import cssutils 

    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:
            serializer = get_serializer()
            sheet = cssutils.parseFile(full_source)

        minified_sources.append(no_ext_source + ext)

    return minified_sources

def base_link(ext, *sources, **options):
    from pylons import config
    from pylons.decorators.cache import beaker_cache

    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:

        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)

_serializer_class = None

def get_serializer():
    # This is in a function to prevent a global import of ``cssutils``,
    # which is not a WebHelpers dependency.
    # The class is cached in a global variable so that it will be 
    # compiled only once.

    import cssutils

    global _serializer_class
    if not _serializer_class:
        class CSSUtilsMinificationSerializer(cssutils.CSSSerializer):
            def __init__(self, prefs=None):
                cssutils.CSSSerializer.__init__(self, prefs)

            def do_css_CSSStyleDeclaration(self, style, separator=None):
                    color = style.getPropertyValue('color')
                    if color and color is not ucs(''):
                        color = self.change_colors(color)
                        style.setProperty('color', color)
                return re.sub(r'0\.([\d])+', r'.\1',
                              re.sub(r'(([^\d][0])+(px|em)+)+', r'\2',
                                  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
        # End of class CSSUtilsMinificationSerializer
        _serializer_class = CSSUtilsMinificationSerializer
    return _serializer_class()