Commits

Alessandro Molina committed 25a12cc

Remove dependency from python-scss and minimal @import support

Comments (0)

Files changed (4)

 specific production environment configurations for some systems.
 
 tgext.scss has born to make life easier for `TurboGears2  <http://www.turbogears.org>`_ developers,
-it will rely on `python-scss <http://packages.python.org/scss/>`_ to serve all the files in your public directory
+it will rely on an internal minimal SCSS compiler
+(based on `Zeta-Library <https://github.com/klen/zeta-library>`_ SCSS parser)
+to serve all the files in your public directory
 that end with *.scss* as **text/css** converting them and minifying them.
 
 Installing
 Enabling tgext.scss
 ----------------------------------
 
-Using tgext.scss is really simple, you edit your `config/middeware.py` and just after 
+If tgext.pluggable is available enabling tgext.scss is just a matter of appending to your `config/app_cfg.py`::
+
+    from tgext.pluggable import plug
+    plug(base_config, 'tgext.scss')
+
+Manually using tgext.scss is really simple, you edit your `config/middeware.py` and just after
 the `#Wrap your base TurboGears 2 application with custom middleware here` comment wrap
 `app` with `SCSSMiddleware`::
 
 
 Now you just have to put your .scss file inside *public/css* and they will be served as CSS.
 
+@Import Support
+-----------------------------------
+
+tgext.scss provides minimal support for @import command. The required syntax is in the form::
+
+    @import url('/css/file.scss');
+
+The specified path is relative to your project plubic files directory.
+*Nested imports are not implemented right now, this means that imported files cannot import another scss*
+
 How much it will slow me down?
 -----------------------------------
 
       zip_safe=False,
       install_requires=[
         "TurboGears2 >= 2.0b7",
-        "scss"
       ],
       entry_points="""
       # -*- Entry points: -*-

tgext/scss/compiler.py

+#!/usr/bin/env python
+#-*- coding: utf-8 -*-
+"""
+pyScss, a Scss compiler for Python
+
+@author     German M. Bravo (Kronuz) <german.mb@gmail.com>
+@version    1.0
+@see        https://github.com/Kronuz/pyScss
+@copyright  (c) 2011 German M. Bravo (Kronuz)
+@license    MIT License
+            http://www.opensource.org/licenses/mit-license.php
+
+pyScss compiles Scss, a superset of CSS that is more powerful, elegant and
+easier to maintain than plain-vanilla CSS. The library acts as a CSS source code
+preprocesor which allows you to use variables, nested rules, mixins, andhave
+inheritance of rules, all with a CSS-compatible syntax which the preprocessor
+then compiles to standard CSS.
+
+Scss, as an extension of CSS, helps keep large stylesheets well-organized. It
+borrows concepts and functionality from projects such as OOCSS and other similar
+frameworks like as Sass. It's build on top of the original PHP xCSS codebase
+structure but it's been completely rewritten, many bugs have been fixed and it
+has been extensively extended to support almost the full range of Sass' Scss
+syntax and functionality.
+
+Bits of code in pyScss come from various projects:
+Compass:
+    (c) 2009 Christopher M. Eppstein
+    http://compass-style.org/
+Sass:
+    (c) 2006-2009 Hampton Catlin and Nathan Weizenbaum
+    http://sass-lang.com/
+xCSS:
+    (c) 2010 Anton Pawlik
+    http://xcss.antpaw.org/docs/
+
+"""
+
+################################################################################
+# Configuration:
+import os
+VERBOSITY = 1
+DEBUG = 0
+################################################################################
+
+import logging
+logging.basicConfig()
+log = logging.getLogger(__name__)
+log.setLevel(logging.INFO)
+
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+import re
+import sys
+import time
+import textwrap
+from collections import deque
+
+profiling = {}
+
+# units and conversions
+_units = ['em', 'ex', 'px', 'cm', 'mm', 'in', 'pt', 'pc', 'deg', 'rad'
+                                                                 'grad', 'ms', 's', 'hz', 'khz', '%']
+_zero_units = ['em', 'ex', 'px', 'cm', 'mm', 'in', 'pt', 'pc']  # units that can be zeroed
+_units_weights = {
+    'em': 10,
+    'mm': 10,
+    'ms': 10,
+    'hz': 10,
+    '%': 100,
+    }
+_conv = {
+    'size': {
+        'em': 13.0,
+        'px': 1.0
+    },
+    'length': {
+        'mm':  1.0,
+        'cm':  10.0,
+        'in':  25.4,
+        'pt':  25.4 / 72,
+        'pc':  25.4 / 6
+    },
+    'time': {
+        'ms':  1.0,
+        's':   1000.0
+    },
+    'freq': {
+        'hz':  1.0,
+        'khz': 1000.0
+    },
+    'any': {
+        '%': 1.0 / 100
+    }
+}
+_conv_type = {}
+_conv_factor = {}
+for t, m in _conv.items():
+    for k, f in m.items():
+        _conv_type[k] = t
+        _conv_factor[k] = f
+del t, m, k, f
+
+# color literals
+_colors = {
+    'aliceblue': '#f0f8ff',
+    'antiquewhite': '#faebd7',
+    'aqua': '#00ffff',
+    'aquamarine': '#7fffd4',
+    'azure': '#f0ffff',
+    'beige': '#f5f5dc',
+    'bisque': '#ffe4c4',
+    'black': '#000000',
+    'blanchedalmond': '#ffebcd',
+    'blue': '#0000ff',
+    'blueviolet': '#8a2be2',
+    'brown': '#a52a2a',
+    'burlywood': '#deb887',
+    'cadetblue': '#5f9ea0',
+    'chartreuse': '#7fff00',
+    'chocolate': '#d2691e',
+    'coral': '#ff7f50',
+    'cornflowerblue': '#6495ed',
+    'cornsilk': '#fff8dc',
+    'crimson': '#dc143c',
+    'cyan': '#00ffff',
+    'darkblue': '#00008b',
+    'darkcyan': '#008b8b',
+    'darkgoldenrod': '#b8860b',
+    'darkgray': '#a9a9a9',
+    'darkgreen': '#006400',
+    'darkkhaki': '#bdb76b',
+    'darkmagenta': '#8b008b',
+    'darkolivegreen': '#556b2f',
+    'darkorange': '#ff8c00',
+    'darkorchid': '#9932cc',
+    'darkred': '#8b0000',
+    'darksalmon': '#e9967a',
+    'darkseagreen': '#8fbc8f',
+    'darkslateblue': '#483d8b',
+    'darkslategray': '#2f4f4f',
+    'darkturquoise': '#00ced1',
+    'darkviolet': '#9400d3',
+    'deeppink': '#ff1493',
+    'deepskyblue': '#00bfff',
+    'dimgray': '#696969',
+    'dodgerblue': '#1e90ff',
+    'firebrick': '#b22222',
+    'floralwhite': '#fffaf0',
+    'forestgreen': '#228b22',
+    'fuchsia': '#ff00ff',
+    'gainsboro': '#dcdcdc',
+    'ghostwhite': '#f8f8ff',
+    'gold': '#ffd700',
+    'goldenrod': '#daa520',
+    'gray': '#808080',
+    'green': '#008000',
+    'greenyellow': '#adff2f',
+    'honeydew': '#f0fff0',
+    'hotpink': '#ff69b4',
+    'indianred': '#cd5c5c',
+    'indigo': '#4b0082',
+    'ivory': '#fffff0',
+    'khaki': '#f0e68c',
+    'lavender': '#e6e6fa',
+    'lavenderblush': '#fff0f5',
+    'lawngreen': '#7cfc00',
+    'lemonchiffon': '#fffacd',
+    'lightblue': '#add8e6',
+    'lightcoral': '#f08080',
+    'lightcyan': '#e0ffff',
+    'lightgoldenrodyellow': '#fafad2',
+    'lightgreen': '#90ee90',
+    'lightgrey': '#d3d3d3',
+    'lightpink': '#ffb6c1',
+    'lightsalmon': '#ffa07a',
+    'lightseagreen': '#20b2aa',
+    'lightskyblue': '#87cefa',
+    'lightslategray': '#778899',
+    'lightsteelblue': '#b0c4de',
+    'lightyellow': '#ffffe0',
+    'lime': '#00ff00',
+    'limegreen': '#32cd32',
+    'linen': '#faf0e6',
+    'magenta': '#ff00ff',
+    'maroon': '#800000',
+    'mediumaquamarine': '#66cdaa',
+    'mediumblue': '#0000cd',
+    'mediumorchid': '#ba55d3',
+    'mediumpurple': '#9370db',
+    'mediumseagreen': '#3cb371',
+    'mediumslateblue': '#7b68ee',
+    'mediumspringgreen': '#00fa9a',
+    'mediumturquoise': '#48d1cc',
+    'mediumvioletred': '#c71585',
+    'midnightblue': '#191970',
+    'mintcream': '#f5fffa',
+    'mistyrose': '#ffe4e1',
+    'moccasin': '#ffe4b5',
+    'navajowhite': '#ffdead',
+    'navy': '#000080',
+    'oldlace': '#fdf5e6',
+    'olive': '#808000',
+    'olivedrab': '#6b8e23',
+    'orange': '#ffa500',
+    'orangered': '#ff4500',
+    'orchid': '#da70d6',
+    'palegoldenrod': '#eee8aa',
+    'palegreen': '#98fb98',
+    'paleturquoise': '#afeeee',
+    'palevioletred': '#db7093',
+    'papayawhip': '#ffefd5',
+    'peachpuff': '#ffdab9',
+    'peru': '#cd853f',
+    'pink': '#ffc0cb',
+    'plum': '#dda0dd',
+    'powderblue': '#b0e0e6',
+    'purple': '#800080',
+    'red': '#ff0000',
+    'rosybrown': '#bc8f8f',
+    'royalblue': '#4169e1',
+    'saddlebrown': '#8b4513',
+    'salmon': '#fa8072',
+    'sandybrown': '#f4a460',
+    'seagreen': '#2e8b57',
+    'seashell': '#fff5ee',
+    'sienna': '#a0522d',
+    'silver': '#c0c0c0',
+    'skyblue': '#87ceeb',
+    'slateblue': '#6a5acd',
+    'slategray': '#708090',
+    'snow': '#fffafa',
+    'springgreen': '#00ff7f',
+    'steelblue': '#4682b4',
+    'tan': '#d2b48c',
+    'teal': '#008080',
+    'thistle': '#d8bfd8',
+    'tomato': '#ff6347',
+    'turquoise': '#40e0d0',
+    'violet': '#ee82ee',
+    'wheat': '#f5deb3',
+    'white': '#ffffff',
+    'whitesmoke': '#f5f5f5',
+    'yellow': '#ffff00',
+    'yellowgreen': '#9acd32'
+}
+
+_safe_strings = {
+    '^doubleslash^': '//',
+    '^bigcopen^': '/*',
+    '^bigcclose^': '*/',
+    '^doubledot^': ':',
+    '^semicolon^': ';',
+    '^curlybracketopen^': '{',
+    '^curlybracketclosed^': '}',
+    }
+_reverse_safe_strings = dict((v, k) for k, v in _safe_strings.items())
+_safe_strings_re = re.compile('|'.join(map(re.escape, _safe_strings)))
+_reverse_safe_strings_re = re.compile('|'.join(map(re.escape, _reverse_safe_strings)))
+
+_default_scss_files = {}  # Files to be compiled ({file: content, ...})
+
+_default_scss_index = {0: '<unknown>:0'}
+
+_default_scss_vars = {
+    # unsafe chars will be hidden as vars
+    '$__doubleslash': '//',
+    '$__bigcopen': '/*',
+    '$__bigcclose': '*/',
+    '$__doubledot': ':',
+    '$__semicolon': ';',
+    '$__curlybracketopen': '{',
+    '$__curlybracketclosed': '}',
+
+    # shortcuts (it's "a hidden feature" for now)
+    'bg:': 'background:',
+    'bgc:': 'background-color:',
+    }
+
+_default_scss_opts = {
+    'verbosity': VERBOSITY,
+    'compress': 1,
+    'compress_short_colors': 1,  # Converts things like #RRGGBB to #RGB
+    'compress_reverse_colors': 1,  # Gets the shortest name of all for colors
+    'short_colors': 0,  # Converts things like #RRGGBB to #RGB
+    'reverse_colors': 0,  # Gets the shortest name of all for colors
+}
+
+SEPARATOR = '\x00'
+_nl_re = re.compile(r'\n', re.MULTILINE)
+_nl_num_re = re.compile(r'\n.+' + SEPARATOR, re.MULTILINE)
+_nl_num_nl_re = re.compile(r'\n.+' + SEPARATOR + r'\s*\n', re.MULTILINE)
+
+_short_color_re = re.compile(r'(?<!\w)#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3\b', re.IGNORECASE)
+_long_color_re = re.compile(r'(?<!\w)#([a-f0-9]){2}([a-f0-9]){2}([a-f0-9]){2}\b', re.IGNORECASE)
+_reverse_colors = dict((v, k) for k, v in _colors.items())
+for long_k, v in _colors.items():
+    # Calculate the different possible representations of a color:
+    short_k = _short_color_re.sub(r'#\1\2\3', v).lower()
+    rgb_k = _long_color_re.sub(lambda m: 'rgb(%d, %d, %d)' % (int(m.group(1), 16), int(m.group(2), 16), int(m.group(3), 16)), v)
+    rgba_k = _long_color_re.sub(lambda m: 'rgba(%d, %d, %d, 1)' % (int(m.group(1), 16), int(m.group(2), 16), int(m.group(3), 16)), v)
+    # get the shortest of all to use it:
+    k = min([short_k, long_k, rgb_k, rgba_k], key=len)
+    _reverse_colors[long_k] = k
+    _reverse_colors[short_k] = k
+    _reverse_colors[rgb_k] = k
+    _reverse_colors[rgba_k] = k
+_reverse_colors_re = re.compile(r'(?<![-\w.#$])(' + '|'.join(map(re.escape, _reverse_colors)) + r')(?![-\w])', re.IGNORECASE)
+_colors_re = re.compile(r'(?<![-\w.#$])(' + '|'.join(map(re.escape, _colors)) + r')(?![-\w])', re.IGNORECASE)
+
+_expr_glob_re = re.compile(r'''
+    \#\{(.*?)\}                   # Global Interpolation only
+''', re.VERBOSE)
+
+_ml_comment_re = re.compile(r'\/\*(.*?)\*\/', re.DOTALL)
+_sl_comment_re = re.compile(r'(?<!\w{2}:)\/\/.*')
+_zero_units_re = re.compile(r'\b0(' + '|'.join(map(re.escape, _zero_units)) + r')(?!\w)', re.IGNORECASE)
+_zero_re = re.compile(r'\b0\.(?=\d)')
+
+_escape_chars_re = re.compile(r'([^-a-zA-Z0-9_])')
+_variable_re = re.compile('^\\$[-a-zA-Z0-9_]+$')
+_interpolate_re = re.compile(r'(#\{\s*)?(\$[-\w]+)(?(1)\s*\})')
+_spaces_re = re.compile(r'\s+')
+_expand_rules_space_re = re.compile(r'\s*{')
+_collapse_properties_space_re = re.compile(r'([:#])\s*{')
+
+_strings_re = re.compile(r'([\'"]).*?\1')
+_blocks_re = re.compile(r'[{},;()\'"\n]')
+
+_prop_split_re = re.compile(r'[:=]')
+_skip_word_re = re.compile(r'-?[\w\s#.,:%]*$|[\w\-#.,:%]*$', re.MULTILINE)
+_skip_re = re.compile(r'''
+    (?:url|alpha)\([^)]*\)$
+|
+    [\w\-#.,:%]+(?:\s+[\w\-#.,:%]+)*$
+''', re.MULTILINE | re.IGNORECASE | re.VERBOSE)
+_has_code_re = re.compile('''
+    (?:^|(?<=[{;}]))            # the character just before it should be a '{', a ';' or a '}'
+    \s*                         # ...followed by any number of spaces
+    (?:
+        (?:
+            \+
+        |
+            @include
+        |
+            @warn
+        |
+            @mixin
+        |
+            @function
+        |
+            @if
+        |
+            @else
+        |
+            @for
+        |
+            @each
+        )
+        (?![^(:;}]*['"])
+    |
+        @import
+    )
+''', re.VERBOSE)
+
+_css_function_re = re.compile(r'^(from|to|mask|rotate|format|local|url|attr|counter|counters|color-stop|rect|-webkit-.*|-webkit-.*|-moz-.*|-pie-.*|-ms-.*|-o-.*)$')
+
+FILEID = 0
+POSITION = 1
+CODESTR = 2
+DEPS = 3
+CONTEXT = 4
+OPTIONS = 5
+SELECTORS = 6
+PROPERTIES = 7
+PATH = 8
+INDEX = 9
+LINENO = 10
+FINAL = 11
+MEDIA = 12
+RULE_VARS = {
+    'FILEID': FILEID,
+    'POSITION': POSITION,
+    'CODESTR': CODESTR,
+    'DEPS': DEPS,
+    'CONTEXT': CONTEXT,
+    'OPTIONS': OPTIONS,
+    'SELECTORS': SELECTORS,
+    'PROPERTIES': PROPERTIES,
+    'PATH': PATH,
+    'INDEX': INDEX,
+    'LINENO': LINENO,
+    'FINAL': FINAL,
+    'MEDIA': MEDIA,
+    }
+
+
+def spawn_rule(rule=None, **kwargs):
+    if rule is None:
+        rule = [None] * len(RULE_VARS)
+        rule[DEPS] = set()
+        rule[SELECTORS] = ''
+        rule[PROPERTIES] = []
+        rule[PATH] = './'
+        rule[INDEX] = {}
+        rule[LINENO] = 0
+        rule[FINAL] = False
+    else:
+        rule = list(rule)
+    for k, v in kwargs.items():
+        rule[RULE_VARS[k.upper()]] = v
+    return rule
+
+
+def print_timing(level=0):
+    def _print_timing(func):
+        if VERBOSITY:
+            def wrapper(*arg):
+                if VERBOSITY >= level:
+                    t1 = time.time()
+                    res = func(*arg)
+                    t2 = time.time()
+                    profiling.setdefault(func.func_name, 0)
+                    profiling[func.func_name] += (t2 - t1)
+                    return res
+                else:
+                    return func(*arg)
+            return wrapper
+        else:
+            return func
+    return _print_timing
+
+
+def split_params(params):
+    params = params.split(',') or []
+    if params:
+        final_params = []
+        param = params.pop(0)
+        try:
+            while True:
+                while param.count('(') != param.count(')'):
+                    try:
+                        param = param + ',' + params.pop(0)
+                    except IndexError:
+                        break
+                final_params.append(param)
+                param = params.pop(0)
+        except IndexError:
+            pass
+        params = final_params
+    return params
+
+
+def dequote(s):
+    if s and s[0] in ('"', "'") and s[-1] == s[0]:
+        s = s[1:-1]
+        s = unescape(s)
+    return s
+
+
+def depar(s):
+    while s and s[0] == '(' and s[-1] == ')':
+        s = s[1:-1]
+    return s
+
+
+class Scss(object):
+    # configuration:
+    construct = 'self'
+
+    def __init__(self, scss_vars=None, scss_opts=None):
+        if scss_vars is None:
+            self.scss_vars = None
+        else:
+            self.scss_vars = _default_scss_vars.copy()
+            self.scss_vars.update(scss_vars)
+        if scss_opts is None:
+            self.scss_opts = None
+        else:
+            self.scss_opts = _default_scss_opts.copy()
+            self.scss_opts.update(scss_opts)
+        self.scss_files = None
+        self.scss_index = None
+        self.reset()
+
+    def clean(self):
+        self.children = deque()
+        self.rules = []
+        self._rules = {}
+        self.parts = {}
+
+    def reset(self, input_scss=None):
+        # Initialize
+        self.css_files = []
+        self._scss_vars = self.scss_vars.copy() if self.scss_vars is not None else _default_scss_vars.copy()
+        self._scss_opts = self.scss_opts.copy() if self.scss_opts is not None else _default_scss_opts.copy()
+        self._scss_files = self.scss_files.copy() if self.scss_files is not None else _default_scss_files.copy()
+        self._scss_index = self.scss_index.copy() if self.scss_index is not None else _default_scss_index.copy()
+
+        self._contexts = {}
+        self._replaces = {}
+
+        self.clean()
+
+    def longest_common_prefix(self, seq1, seq2):
+        start = 0
+        common = 0
+        length = min(len(seq1), len(seq2))
+        while start < length:
+            if seq1[start] != seq2[start]:
+                break
+            if seq1[start] == ' ':
+                common = start + 1
+            elif seq1[start] in ('#', ':', '.'):
+                common = start
+            start += 1
+        return common
+
+    def longest_common_suffix(self, seq1, seq2):
+        seq1, seq2 = seq1[::-1], seq2[::-1]
+        start = 0
+        common = 0
+        length = min(len(seq1), len(seq2))
+        while start < length:
+            if seq1[start] != seq2[start]:
+                break
+            if seq1[start] == ' ':
+                common = start + 1
+            elif seq1[start] in ('#', ':', '.'):
+                common = start + 1
+            start += 1
+        return common
+
+    def locate_blocks(self, codestr):
+        """
+        Returns all code blocks between `{` and `}` and a proper key
+        that can be multilined as long as it's joined by `,` or enclosed in
+        `(` and `)`.
+        Returns the "lose" code that's not part of the block as a third item.
+        """
+        def _strip_selprop(selprop, lineno):
+            _lineno, _sep, selprop = selprop.partition(SEPARATOR)
+            if _sep == SEPARATOR:
+                lineno = _lineno.strip()
+                lineno = int(lineno) if lineno else 0
+            else:
+                selprop = _lineno
+            selprop = _nl_num_re.sub('\n', selprop)
+            selprop = selprop.strip()
+            return selprop, lineno
+
+        def _strip(selprop):
+            selprop, _ = _strip_selprop(selprop, 0)
+            return selprop
+
+        lineno = 0
+
+        par = 0
+        instr = None
+        depth = 0
+        skip = False
+        thin = None
+        i = init = safe = lose = 0
+        start = end = None
+
+        for m in _blocks_re.finditer(codestr):
+            _s = m.start(0)
+            _e = m.end(0)
+            i = _e - 1
+            if _s == _e:
+                break
+            if instr is not None:
+                if codestr[i] == instr:
+                    instr = None
+            elif codestr[i] in ('"', "'"):
+                instr = codestr[i]
+            elif codestr[i] == '(':
+                par += 1
+                thin = None
+                safe = i + 1
+            elif codestr[i] == ')':
+                par -= 1
+            elif not par and not instr:
+                if codestr[i] == '{':
+                    if depth == 0:
+                        if i > 0 and codestr[i - 1] == '#':
+                            skip = True
+                        else:
+                            start = i
+                            if thin is not None and _strip(codestr[thin:i - 1]):
+                                init = thin
+                            if lose < init:
+                                losestr = codestr[lose:init]
+                                for _property in losestr.split(';'):
+                                    _property, lineno = _strip_selprop(_property, lineno)
+                                    if _property:
+                                        yield lineno, _property, None
+                                lose = init
+                            thin = None
+                    depth += 1
+                elif codestr[i] == '}':
+                    if depth > 0:
+                        depth -= 1
+                        if depth == 0:
+                            if not skip:
+                                end = i
+                                _selectors, lineno = _strip_selprop(codestr[init:start], lineno)
+                                _codestr = codestr[start + 1:end].strip()
+                                if _selectors:
+                                    yield lineno, _selectors, _codestr
+                                init = safe = lose = end + 1
+                                thin = None
+                            skip = False
+                elif depth == 0:
+                    if codestr[i] == ';':
+                        init = safe = i + 1
+                        thin = None
+                    elif codestr[i] == ',':
+                        if thin is not None and _strip(codestr[thin:i - 1]):
+                            init = thin
+                        thin = None
+                        safe = i + 1
+                    elif codestr[i] == '\n':
+                        if thin is None and _strip(codestr[safe:i - 1]):
+                            thin = i + 1
+                        elif thin is not None and _strip(codestr[thin:i - 1]):
+                            init = i + 1
+                            thin = None
+        if depth > 0:
+            if not skip:
+                _selectors, lineno = _strip_selprop(codestr[init:start], lineno)
+                _codestr = codestr[start + 1:].strip()
+                if _selectors:
+                    yield lineno, _selectors, _codestr
+                if par:
+                    raise Exception("Missing closing parenthesis somewhere in block: '%s'" % _selectors)
+                elif instr:
+                    raise Exception("Missing closing string somewhere in block: '%s'" % _selectors)
+                else:
+                    raise Exception("Block never closed: '%s'" % _selectors)
+        losestr = codestr[lose:]
+        for _property in losestr.split(';'):
+            _property, lineno = _strip_selprop(_property, lineno)
+            if _property:
+                yield lineno, _property, None
+
+    def normalize_selectors(self, _selectors, extra_selectors=None, extra_parents=None):
+        """
+        Normalizes or extends selectors in a string.
+        An optional extra parameter that can be a list of extra selectors to be
+        added to the final normalized selectors string.
+        """
+        # Fixe tabs and spaces in selectors
+        _selectors = _spaces_re.sub(' ', _selectors)
+
+        if isinstance(extra_selectors, basestring):
+            extra_selectors = extra_selectors.split(',')
+
+        if isinstance(extra_parents, basestring):
+            extra_parents = extra_parents.split('&')
+
+        parents = set()
+        if ' extends ' in _selectors:
+            selectors = set()
+            for key in _selectors.split(','):
+                child, _, parent = key.partition(' extends ')
+                child = child.strip()
+                parent = parent.strip()
+                selectors.add(child)
+                parents.update(s.strip() for s in parent.split('&') if s.strip())
+        else:
+            selectors = set(s.strip() for s in _selectors.split(',') if s.strip())
+        if extra_selectors:
+            selectors.update(s.strip() for s in extra_selectors if s.strip())
+        selectors.discard('')
+        if not selectors:
+            return ''
+        if extra_parents:
+            parents.update(s.strip() for s in extra_parents if s.strip())
+        parents.discard('')
+        if parents:
+            return ','.join(sorted(selectors)) + ' extends ' + '&'.join(sorted(parents))
+        return ','.join(sorted(selectors))
+
+    def apply_vars(self, cont, context, options=None, rule=None, _dequote=False):
+        if isinstance(cont, basestring) and '$' in cont:
+            if cont in context:
+                # Optimization: the full cont is a variable in the context,
+                # flatten the interpolation and use it:
+                while isinstance(cont, basestring) and cont in context:
+                    _cont = context[cont]
+                    if _cont == cont:
+                        break
+                    cont = _cont
+            else:
+                # Flatten the context (no variables mapping to variables)
+                flat_context = {}
+                for k, v in context.items():
+                    while isinstance(v, basestring) and v in context:
+                        _v = context[v]
+                        if _v == v:
+                            break
+                        v = _v
+                    flat_context[k] = v
+
+                # Interpolate variables:
+                def _av(m):
+                    v = flat_context.get(m.group(2))
+                    if v:
+                        v = to_str(v)
+                        if _dequote and m.group(1):
+                            v = dequote(v)
+                    elif v is not None:
+                        v = to_str(v)
+                    else:
+                        v = m.group(0)
+                    return v
+
+                cont = _interpolate_re.sub(_av, cont)
+        if options is not None:
+            # ...apply math:
+            cont = self.do_glob_math(cont, context, options, rule, _dequote)
+        return cont
+
+    @print_timing(2)
+    def Compilation(self, input_scss=None):
+        self.reset()
+
+        if input_scss is not None:
+            self._scss_files = {'<string>': input_scss}
+
+        # Compile
+        for fileid, codestr in self._scss_files.iteritems():
+            codestr = self.load_string(codestr, fileid)
+            self._scss_files[fileid] = codestr
+            rule = spawn_rule(fileid=fileid, codestr=codestr, context=self._scss_vars, options=self._scss_opts, index=self._scss_index)
+            self.children.append(rule)
+
+        # this will manage rule: child objects inside of a node
+        self.parse_children()
+
+        # this will manage rule: ' extends '
+        self.parse_extends()
+
+        # this will manage the order of the rules
+        self.manage_order()
+
+        self.parse_properties()
+
+        final_cont = ''
+        for fileid in self.css_files:
+            if fileid != '<string>':
+                final_cont += '/* Generated from: ' + fileid + ' */\n'
+            fcont = self.create_css(fileid)
+            final_cont += fcont
+
+        final_cont = self.post_process(final_cont)
+
+        return final_cont
+    compile = Compilation
+
+    def load_string(self, codestr, filename=None):
+        if filename is not None:
+            codestr += '\n'
+
+            idx = {
+                'next_id': len(self._scss_index),
+                'line': 1,
+                }
+
+            def _cnt(m):
+                idx['line'] += 1
+                lineno = '%s:%d' % (filename, idx['line'])
+                next_id = idx['next_id']
+                self._scss_index[next_id] = lineno
+                idx['next_id'] += 1
+                return '\n' + str(next_id) + SEPARATOR
+            lineno = '%s:%d' % (filename, idx['line'])
+            next_id = idx['next_id']
+            self._scss_index[next_id] = lineno
+            codestr = str(next_id) + SEPARATOR + _nl_re.sub(_cnt, codestr)
+
+        # remove empty lines
+        codestr = _nl_num_nl_re.sub('\n', codestr)
+
+        # protects codestr: "..." strings
+        codestr = _strings_re.sub(lambda m: _reverse_safe_strings_re.sub(lambda n: _reverse_safe_strings[n.group(0)], m.group(0)), codestr)
+
+        # removes multiple line comments
+        codestr = _ml_comment_re.sub('', codestr)
+
+        # removes inline comments, but not :// (protocol)
+        codestr = _sl_comment_re.sub('', codestr)
+
+        codestr = _safe_strings_re.sub(lambda m: _safe_strings[m.group(0)], codestr)
+
+        # expand the space in rules
+        codestr = _expand_rules_space_re.sub(' {', codestr)
+
+        # collapse the space in properties blocks
+        codestr = _collapse_properties_space_re.sub(r'\1{', codestr)
+
+        # to do math operations, we need to get the color's hex values (for color names):
+        def _pp(m):
+            v = m.group(0)
+            return _colors.get(v, v)
+        codestr = _colors_re.sub(_pp, codestr)
+
+        return codestr
+
+    @print_timing(3)
+    def parse_children(self):
+        pos = 0
+        while True:
+            try:
+                rule = self.children.popleft()
+            except:
+                break
+                # Check if the block has nested blocks and work it out:
+            _selectors, _, _parents = rule[SELECTORS].partition(' extends ')
+            _selectors = _selectors.split(',')
+            _parents = set(_parents.split('&'))
+            _parents.discard('')
+
+            # manage children or expand children:
+            _children = deque()
+            self.manage_children(rule, _selectors, _parents, _children, None, rule[MEDIA])
+            self.children.extendleft(_children)
+
+            # prepare maps:
+            if _parents:
+                rule[SELECTORS] = ','.join(_selectors) + ' extends ' + '&'.join(_parents)
+            rule[POSITION] = pos
+            selectors = rule[SELECTORS]
+            self.parts.setdefault(selectors, [])
+            self.parts[selectors].append(rule)
+            self.rules.append(rule)
+            pos += 1
+
+            #print >>sys.stderr, '='*80
+            #for r in [rule]+list(self.children)[:5]: print >>sys.stderr, repr(r[POSITION]), repr(r[SELECTORS]), repr(r[CODESTR][:80]+('...' if len(r[CODESTR])>80 else ''))
+            #for r in [rule]+list(self.children)[:5]: print >>sys.stderr, repr(r[POSITION]), repr(r[SELECTORS]), repr(r[CODESTR][:80]+('...' if len(r[CODESTR])>80 else '')), dict((k, v) for k, v in r[CONTEXT].items() if k.startswith('$') and not k.startswith('$__')), dict(r[PROPERTIES]).keys()
+
+    @print_timing(4)
+    def manage_children(self, rule, p_selectors, p_parents, p_children, scope, media):
+        for c_lineno, c_property, c_codestr in self.locate_blocks(rule[CODESTR]):
+            if '@return' in rule[OPTIONS]:
+                return
+                # Rules preprocessing...
+            if c_property.startswith('+'):  # expands a '+' at the beginning of a rule as @include
+                c_property = '@include ' + c_property[1:]
+                try:
+                    if '(' not in c_property or c_property.index(':') < c_property.index('('):
+                        c_property = c_property.replace(':', '(', 1)
+                        if '(' in c_property:
+                            c_property += ')'
+                except ValueError:
+                    pass
+            elif c_property.startswith('='):  # expands a '=' at the beginning of a rule as @mixin
+                c_property = '@mixin' + c_property[1:]
+            elif c_property == '@prototype ':  # Remove '@prototype '
+                c_property = c_property[11:]
+                ####################################################################
+            if c_property.startswith('@'):
+                code, name = (c_property.split(None, 1) + [''])[:2]
+                code = code.lower()
+                if code == '@warn':
+                    name = self.calculate(name, rule[CONTEXT], rule[OPTIONS], rule)
+                    log.warn(dequote(to_str(name)))
+                elif code == '@print':
+                    name = self.calculate(name, rule[CONTEXT], rule[OPTIONS], rule)
+                    log.info(dequote(to_str(name)))
+                elif code == '@raw':
+                    name = self.calculate(name, rule[CONTEXT], rule[OPTIONS], rule)
+                    log.info(repr(name))
+                elif code == '@debug':
+                    global DEBUG
+                    name = name.strip()
+                    if name.lower() in ('1', 'true', 't', 'yes', 'y', 'on'):
+                        name = 1
+                    elif name.lower() in ('0', 'false', 'f', 'no', 'n', 'off'):
+                        name = 0
+                    DEBUG = name
+                    log.info("Debug mode is %s", 'On' if DEBUG else 'Off')
+                elif code == '@option':
+                    self._settle_options(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name)
+                elif code == '@import':
+                    self._do_import(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name)
+                elif code == '@extend':
+                    name = self.apply_vars(name, rule[CONTEXT], rule[OPTIONS], rule)
+                    p_parents.update(p.strip() for p in name.replace(',', '&').split('&'))
+                    p_parents.discard('')
+                elif c_codestr is not None and code in ('@mixin', '@function'):
+                    self._do_functions(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name)
+                elif code == '@return':
+                    ret = self.calculate(name, rule[CONTEXT], rule[OPTIONS], rule)
+                    rule[OPTIONS]['@return'] = ret
+                elif code == '@include':
+                    self._do_include(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name)
+                elif c_codestr is not None and (code == '@if' or c_property.startswith('@else if ')):
+                    self._do_if(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name)
+                elif c_codestr is not None and code == '@else':
+                    self._do_else(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name)
+                elif c_codestr is not None and code == '@for':
+                    self._do_for(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name)
+                elif c_codestr is not None and code == '@each':
+                    self._do_each(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name)
+                # elif c_codestr is not None and code == '@while':
+                #     self._do_while(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name)
+                elif c_codestr is not None and code in ('@variables', '@vars'):
+                    self._get_variables(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr)
+                elif c_codestr is not None and code == '@media':
+                    _media = (media or []) + [name]
+                    rule[CODESTR] = self.construct + ' {' + c_codestr + '}'
+                    self.manage_children(rule, p_selectors, p_parents, p_children, scope, _media)
+                elif c_codestr is None:
+                    rule[PROPERTIES].append((c_lineno, c_property, None))
+                elif scope is None:  # needs to have no scope to crawl down the nested rules
+                    self._nest_rules(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr)
+            ####################################################################
+            # Properties
+            elif c_codestr is None:
+                self._get_properties(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr)
+            # Nested properties
+            elif c_property.endswith(':'):
+                rule[CODESTR] = c_codestr
+                self.manage_children(rule, p_selectors, p_parents, p_children, (scope or '') + c_property[:-1] + '-', media)
+            ####################################################################
+            # Nested rules
+            elif scope is None:  # needs to have no scope to crawl down the nested rules
+                self._nest_rules(rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr)
+
+    @print_timing(10)
+    def _settle_options(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name):
+        for option in name.split(','):
+            option, value = (option.split(':', 1) + [''])[:2]
+            option = option.strip().lower()
+            value = value.strip()
+            if option:
+                if value.lower() in ('1', 'true', 't', 'yes', 'y', 'on'):
+                    value = 1
+                elif value.lower() in ('0', 'false', 'f', 'no', 'n', 'off'):
+                    value = 0
+                rule[OPTIONS][option] = value
+
+    @print_timing(10)
+    def _do_functions(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name):
+        """
+        Implements @mixin and @function
+        """
+        if name:
+            funct, params, _ = name.partition('(')
+            funct = funct.strip()
+            params = split_params(depar(params + _))
+            defaults = {}
+            new_params = []
+            for param in params:
+                param, _, default = param.partition(':')
+                param = param.strip()
+                default = default.strip()
+                if param:
+                    new_params.append(param)
+                    if default:
+                        default = self.apply_vars(default, rule[CONTEXT], None, rule)
+                        defaults[param] = default
+            context = rule[CONTEXT].copy()
+            for p in new_params:
+                context.pop(p, None)
+            mixin = [list(new_params), defaults, self.apply_vars(c_codestr, context, None, rule)]
+            if code == '@function':
+                def _call(mixin):
+                    def __call(R, *args, **kwargs):
+                        m_params = mixin[0]
+                        m_vars = rule[CONTEXT].copy()
+                        m_vars.update(mixin[1])
+                        m_codestr = mixin[2]
+                        for i, a in enumerate(args):
+                            m_vars[m_params[i]] = a
+                        m_vars.update(kwargs)
+                        _options = rule[OPTIONS].copy()
+                        _rule = spawn_rule(R, codestr=m_codestr, context=m_vars, options=_options, deps=set(), properties=[], final=False, lineno=c_lineno)
+                        self.manage_children(_rule, p_selectors, p_parents, p_children, (scope or '') + '', R[MEDIA])
+                        ret = _rule[OPTIONS].pop('@return', '')
+                        return ret
+                    return __call
+                _mixin = _call(mixin)
+                _mixin.mixin = mixin
+                mixin = _mixin
+                # Insert as many @mixin options as the default parameters:
+            while len(new_params):
+                rule[OPTIONS]['%s %s:%d' % (code, funct, len(new_params))] = mixin
+                param = new_params.pop()
+                if param not in defaults:
+                    break
+            if not new_params:
+                rule[OPTIONS][code + ' ' + funct + ':0'] = mixin
+
+    @print_timing(10)
+    def _do_include(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name):
+        """
+        Implements @include, for @mixins
+        """
+        funct, params, _ = name.partition('(')
+        funct = funct.strip()
+        funct = self.do_glob_math(funct, rule[CONTEXT], rule[OPTIONS], rule, True)
+        params = split_params(depar(params + _))
+        new_params = {}
+        num_args = 0
+        for param in params:
+            varname, _, param = param.partition(':')
+            if param:
+                param = param.strip()
+                varname = varname.strip()
+            else:
+                param = varname.strip()
+                varname = num_args
+                if param:
+                    num_args += 1
+            if param:
+                new_params[varname] = param
+        mixin = rule[OPTIONS].get('@mixin %s:%s' % (funct, num_args))
+        if not mixin:
+            # Fallback to single parmeter:
+            mixin = rule[OPTIONS].get('@mixin %s:1' % (funct,))
+            if mixin and all(map(lambda o: isinstance(o, int), new_params.keys())):
+                new_params = {0: ', '.join(new_params.values())}
+        if mixin:
+            m_params = mixin[0]
+            m_vars = mixin[1].copy()
+            m_codestr = mixin[2]
+            for varname, value in new_params.items():
+                try:
+                    m_param = m_params[varname]
+                except:
+                    m_param = varname
+                value = self.calculate(value, rule[CONTEXT], rule[OPTIONS], rule)
+                m_vars[m_param] = value
+            for p in m_vars:
+                if p not in new_params:
+                    if isinstance(m_vars[p], basestring):
+                        value = self.calculate(m_vars[p], m_vars, rule[OPTIONS], rule)
+                        m_vars[p] = value
+            _context = rule[CONTEXT].copy()
+            _context.update(m_vars)
+            _rule = spawn_rule(rule, codestr=m_codestr, context=_context, lineno=c_lineno)
+            self.manage_children(_rule, p_selectors, p_parents, p_children, scope, media)
+        else:
+            log.error("Required mixin not found: %s:%d (%s)", funct, num_args, rule[INDEX][rule[LINENO]])
+
+    @print_timing(10)
+    def _do_if(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name):
+        """
+        Implements @if and @else if
+        """
+        if code != '@if':
+            if '@if' not in rule[OPTIONS]:
+                log.error("@else with no @if (%s)", rule[INDEX][rule[LINENO]])
+            val = not rule[OPTIONS].get('@if', True)
+            name = c_property[9:].strip()
+        else:
+            val = True
+        if val:
+            val = self.calculate(name, rule[CONTEXT], rule[OPTIONS], rule)
+            val = bool(False if not val or val in('0', 'false',) or (isinstance(val, basestring) and _variable_re.match(val)) else val)
+            if val:
+                rule[CODESTR] = c_codestr
+                self.manage_children(rule, p_selectors, p_parents, p_children, scope, media)
+            rule[OPTIONS]['@if'] = val
+
+    @print_timing(10)
+    def _do_else(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name):
+        """
+        Implements @else
+        """
+        if '@if' not in rule[OPTIONS]:
+            log.error("@else with no @if (%s", rule[INDEX][rule[LINENO]])
+        val = rule[OPTIONS].pop('@if', True)
+        if not val:
+            rule[CODESTR] = c_codestr
+            self.manage_children(rule, p_selectors, p_parents, p_children, scope, media)
+
+    @print_timing(10)
+    def _do_for(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name):
+        """
+        Implements @for
+        """
+        var, _, name = name.partition('from')
+        frm, _, through = name.partition('through')
+        if not through:
+            frm, _, through = frm.partition('to')
+        frm = self.calculate(frm, rule[CONTEXT], rule[OPTIONS], rule)
+        through = self.calculate(through, rule[CONTEXT], rule[OPTIONS], rule)
+        try:
+            frm = int(float(frm))
+            through = int(float(through))
+        except ValueError:
+            pass
+        else:
+            if frm > through:
+                frm, through = through, frm
+                rev = reversed
+            else:
+                rev = lambda x: x
+            var = var.strip()
+            var = self.do_glob_math(var, rule[CONTEXT], rule[OPTIONS], rule, True)
+
+            for i in rev(range(frm, through + 1)):
+                rule[CODESTR] = c_codestr
+                rule[CONTEXT][var] = str(i)
+                self.manage_children(rule, p_selectors, p_parents, p_children, scope, media)
+
+    @print_timing(10)
+    def _do_each(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name):
+        """
+        Implements @each
+        """
+        var, _, name = name.partition('in')
+        name = self.calculate(name, rule[CONTEXT], rule[OPTIONS], rule)
+        if name:
+            name = ListValue(name)
+            var = var.strip()
+            var = self.do_glob_math(var, rule[CONTEXT], rule[OPTIONS], rule, True)
+
+            for n, v in name.items():
+                v = to_str(v)
+                rule[CODESTR] = c_codestr
+                rule[CONTEXT][var] = v
+                if not isinstance(n, int):
+                    rule[CONTEXT][n] = v
+                self.manage_children(rule, p_selectors, p_parents, p_children, scope, media)
+
+    # @print_timing(10)
+    # def _do_while(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr, code, name):
+    #     THIS DOES NOT WORK AS MODIFICATION OF INNER VARIABLES ARE NOT KNOWN AT THIS POINT!!
+    #     """
+    #     Implements @while
+    #     """
+    #     first_val = None
+    #     while True:
+    #         val = self.calculate(name, rule[CONTEXT], rule[OPTIONS], rule)
+    #         val = bool(False if not val or val in('0', 'false',) or (isinstance(val, basestring) and _variable_re.match(val)) else val)
+    #         if first_val is None:
+    #             first_val = val
+    #         if not val:
+    #             break
+    #         rule[CODESTR] = c_codestr
+    #         self.manage_children(rule, p_selectors, p_parents, p_children, scope, media)
+    #     rule[OPTIONS]['@if'] = first_val
+
+    @print_timing(10)
+    def _get_variables(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr):
+        """
+        Implements @variables and @vars
+        """
+        _rule = list(rule)
+        _rule[CODESTR] = c_codestr
+        _rule[PROPERTIES] = rule[CONTEXT]
+        self.manage_children(_rule, p_selectors, p_parents, p_children, scope, media)
+
+    @print_timing(10)
+    def _get_properties(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr):
+        """
+        Implements properties and variables extraction
+        """
+        prop, value = (_prop_split_re.split(c_property, 1) + [None])[:2]
+        try:
+            is_var = (c_property[len(prop)] == '=')
+        except IndexError:
+            is_var = False
+        prop = prop.strip()
+        prop = self.do_glob_math(prop, rule[CONTEXT], rule[OPTIONS], rule, True)
+        if prop:
+            if value:
+                value = value.strip()
+                value = self.calculate(value, rule[CONTEXT], rule[OPTIONS], rule)
+            _prop = (scope or '') + prop
+            if is_var or prop.startswith('$') and value is not None:
+                if isinstance(value, basestring):
+                    if '!default' in value:
+                        if _prop in rule[CONTEXT]:
+                            value = None
+                        else:
+                            value = value.replace('!default', '').replace('  ', ' ').strip()
+                elif isinstance(value, ListValue):
+                    value = ListValue(value)
+                    for k, v in value.value.items():
+                        if v == '!default':
+                            if _prop in rule[CONTEXT]:
+                                value = None
+                            else:
+                                del value.value[k]
+                                value = value.first() if len(value) == 1 else value
+                            break
+                if value is not None:
+                    rule[CONTEXT][_prop] = value
+            else:
+                _prop = self.apply_vars(_prop, rule[CONTEXT], rule[OPTIONS], rule, True)
+                rule[PROPERTIES].append((c_lineno, _prop, to_str(value) if value is not None else None))
+
+    @print_timing(10)
+    def _nest_rules(self, rule, p_selectors, p_parents, p_children, scope, media, c_lineno, c_property, c_codestr):
+        """
+        Implements Nested CSS rules
+        """
+        if c_property == self.construct and rule[MEDIA] == media:
+            rule[CODESTR] = c_codestr
+            self.manage_children(rule, p_selectors, p_parents, p_children, scope, media)
+        else:
+            c_property = self.apply_vars(c_property, rule[CONTEXT], rule[OPTIONS], rule, True)
+
+            c_selectors = self.normalize_selectors(c_property)
+            c_selectors, _, c_parents = c_selectors.partition(' extends ')
+
+            better_selectors = set()
+            c_selectors = c_selectors.split(',')
+            for c_selector in c_selectors:
+                for p_selector in p_selectors:
+                    if c_selector == self.construct:
+                        better_selectors.add(p_selector)
+                    elif '&' in c_selector:  # Parent References
+                        better_selectors.add(c_selector.replace('&', p_selector))
+                    elif p_selector:
+                        better_selectors.add(p_selector + ' ' + c_selector)
+                    else:
+                        better_selectors.add(c_selector)
+            better_selectors = ','.join(sorted(better_selectors))
+
+            if c_parents:
+                parents = set(p.strip() for p in c_parents.split('&'))
+                parents.discard('')
+                if parents:
+                    better_selectors += ' extends ' + '&'.join(sorted(parents))
+
+            _rule = spawn_rule(rule, codestr=c_codestr, deps=set(), context=rule[CONTEXT].copy(), options=rule[OPTIONS].copy(), selectors=better_selectors, properties=[], final=False, media=media, lineno=c_lineno)
+
+            p_children.appendleft(_rule)
+
+    @print_timing(4)
+    def link_with_parents(self, parent, c_selectors, c_rules):
+        """
+        Link with a parent for the current child rule.
+        If parents found, returns a list of parent rules to the child
+        """
+        parent_found = None
+        for p_selectors, p_rules in self.parts.items():
+            _p_selectors, _, _ = p_selectors.partition(' extends ')
+            _p_selectors = _p_selectors.split(',')
+
+            new_selectors = set()
+            found = False
+
+            # Finds all the parent selectors and parent selectors with another
+            # bind selectors behind. For example, if `.specialClass extends .baseClass`,
+            # and there is a `.baseClass` selector, the extension should create
+            # `.specialClass` for that rule, but if there's also a `.baseClass a`
+            # it also should create `.specialClass a`
+            for p_selector in _p_selectors:
+                if parent in p_selector:
+                    # get the new child selector to add (same as the parent selector but with the child name)
+                    # since selectors can be together, separated with # or . (i.e. something.parent) check that too:
+                    for c_selector in c_selectors.split(','):
+                        # Get whatever is different between the two selectors:
+                        _c_selector, _parent = c_selector, parent
+                        lcp = self.longest_common_prefix(_c_selector, _parent)
+                        if lcp:
+                            _c_selector = _c_selector[lcp:]
+                            _parent = _parent[lcp:]
+                        lcs = self.longest_common_suffix(_c_selector, _parent)
+                        if lcs:
+                            _c_selector = _c_selector[:-lcs]
+                            _parent = _parent[:-lcs]
+                        if _c_selector and _parent:
+                            # Get the new selectors:
+                            prev_symbol = '(?<![#.:])' if _parent[0] in ('#', '.', ':') else r'(?<![-\w#.:])'
+                            post_symbol = r'(?![-\w])'
+                            new_parent = re.sub(prev_symbol + _parent + post_symbol, _c_selector, p_selector)
+                            if p_selector != new_parent:
+                                new_selectors.add(new_parent)
+                                found = True
+
+            if found:
+                # add parent:
+                parent_found = parent_found or []
+                parent_found.extend(p_rules)
+
+            if new_selectors:
+                new_selectors = self.normalize_selectors(p_selectors, new_selectors)
+                # rename node:
+                if new_selectors != p_selectors:
+                    del self.parts[p_selectors]
+                    self.parts.setdefault(new_selectors, [])
+                    self.parts[new_selectors].extend(p_rules)
+
+                deps = set()
+                # save child dependencies:
+                for c_rule in c_rules or []:
+                    c_rule[SELECTORS] = c_selectors  # re-set the SELECTORS for the rules
+                    deps.add(c_rule[POSITION])
+
+                for p_rule in p_rules:
+                    p_rule[SELECTORS] = new_selectors  # re-set the SELECTORS for the rules
+                    p_rule[DEPS].update(deps)  # position is the "index" of the object
+
+        return parent_found
+
+    @print_timing(3)
+    def parse_extends(self):
+        """
+        For each part, create the inheritance parts from the ' extends '
+        """
+        # To be able to manage multiple extends, you need to
+        # destroy the actual node and create many nodes that have
+        # mono extend. The first one gets all the css rules
+        for _selectors, rules in self.parts.items():
+            if ' extends ' in _selectors:
+                selectors, _, parent = _selectors.partition(' extends ')
+                parents = parent.split('&')
+                del self.parts[_selectors]
+                for parent in parents:
+                    new_selectors = selectors + ' extends ' + parent
+                    self.parts.setdefault(new_selectors, [])
+                    self.parts[new_selectors].extend(rules)
+                    rules = []  # further rules extending other parents will be empty
+
+        cnt = 0
+        parents_left = True
+        while parents_left and cnt < 10:
+            cnt += 1
+            parents_left = False
+            for _selectors in self.parts.keys():
+                selectors, _, parent = _selectors.partition(' extends ')
+                if parent:
+                    parents_left = True
+                    if _selectors not in self.parts:
+                        continue  # Nodes might have been renamed while linking parents...
+
+                    rules = self.parts[_selectors]
+
+                    del self.parts[_selectors]
+                    self.parts.setdefault(selectors, [])
+                    self.parts[selectors].extend(rules)
+
+                    parents = self.link_with_parents(parent, selectors, rules)
+
+                    if parents is None:
+                        log.warn("Parent rule not found: %s", parent)
+                    else:
+                        # from the parent, inherit the context and the options:
+                        new_context = {}
+                        new_options = {}
+                        for parent in parents:
+                            new_context.update(parent[CONTEXT])
+                            new_options.update(parent[OPTIONS])
+                        for rule in rules:
+                            _new_context = new_context.copy()
+                            _new_context.update(rule[CONTEXT])
+                            rule[CONTEXT] = _new_context
+                            _new_options = new_options.copy()
+                            _new_options.update(rule[OPTIONS])
+                            rule[OPTIONS] = _new_options
+
+    @print_timing(3)
+    def manage_order(self):
+        # order rules according with their dependencies
+        for rule in self.rules:
+            if rule[POSITION] is not None:
+                rule[DEPS].add(rule[POSITION] + 1)
+                # This moves the rules just above the topmost dependency during the sorted() below:
+                rule[POSITION] = min(rule[DEPS])
+        self.rules = sorted(self.rules, key=lambda o: o[POSITION])
+
+    @print_timing(3)
+    def parse_properties(self):
+        self.css_files = []
+        self._rules = {}
+        css_files = set()
+        old_fileid = None
+        for rule in self.rules:
+            #print >>sys.stderr, rule[FILEID], rule[POSITION], [ c for c in rule[CONTEXT] if c[1] != '_' ], rule[OPTIONS].keys(), rule[SELECTORS], rule[DEPS]
+            if rule[POSITION] is not None and rule[PROPERTIES]:
+                fileid = rule[FILEID]
+                self._rules.setdefault(fileid, [])
+                self._rules[fileid].append(rule)
+                if old_fileid != fileid:
+                    old_fileid = fileid
+                    if fileid not in css_files:
+                        css_files.add(fileid)
+                        self.css_files.append(fileid)
+
+    @print_timing(3)
+    def create_css(self, fileid=None):
+        """
+        Generate the final CSS string
+        """
+        if fileid:
+            rules = self._rules.get(fileid) or []
+        else:
+            rules = self.rules
+
+        compress = self._scss_opts.get('compress', True)
+        if compress:
+            sc, sp, tb, nl = False, '', '', ''
+        else:
+            sc, sp, tb, nl = True, ' ', '  ', '\n'
+
+        scope = set()
+        return self._create_css(rules, scope, sc, sp, tb, nl, not compress and self._scss_opts.get('debug_info', False))
+
+    def _create_css(self, rules, scope=None, sc=True, sp=' ', tb='  ', nl='\n', debug_info=False):
+        scope = set() if scope is None else scope
+
+        open_selectors = False
+        old_selectors = None
+        open_media = False
+        old_media = None
+        old_property = None
+
+        wrap = textwrap.TextWrapper(break_long_words=False)
+        wrap.wordsep_re = re.compile(r'(?<=,)(\s*)')
+        wrap = wrap.wrap
+
+        result = ''
+        for rule in rules:
+            #print >>sys.stderr, rule[FILEID], rule[MEDIA], rule[POSITION], [ c for c in rule[CONTEXT] if not c.startswith('$__') ], rule[OPTIONS].keys(), rule[SELECTORS], rule[DEPS]
+            if rule[POSITION] is not None and rule[PROPERTIES]:
+                selectors = rule[SELECTORS]
+                media = rule[MEDIA]
+                _tb = tb if old_media else ''
+                if old_media != media or media is not None:
+                    if open_selectors:
+                        if not sc:
+                            if result[-1] == ';':
+                                result = result[:-1]
+                        result += _tb + '}' + nl
+                        open_selectors = False
+                    if open_media:
+                        if not sc:
+                            if result[-1] == ';':
+                                result = result[:-1]
+                        result += '}' + nl
+                        open_media = False
+                    if media:
+                        result += '@media ' + (' and ').join(set(media)) + sp + '{' + nl
+                        open_media = True
+                    old_media = media
+                    old_selectors = None  # force entrance to add a new selector
+                _tb = tb if media else ''
+                if old_selectors != selectors or selectors is not None:
+                    if open_selectors:
+                        if not sc:
+                            if result[-1] == ';':
+                                result = result[:-1]
+                        result += _tb + '}' + nl
+                        open_selectors = False
+                    if selectors:
+                        if debug_info:
+                            filename, lineno = rule[INDEX][rule[LINENO]].rsplit(':', 1)
+                            filename = _escape_chars_re.sub(r'\\\1', filename)
+                            sass_debug_info = '@media -sass-debug-info{filename{font-family:file\:\/\/%s}line{font-family:\\00003%s}}' % (filename, lineno)
+                            result += sass_debug_info + nl
+                        selector = (',' + sp).join(selectors.split(',')) + sp + '{'
+                        if nl:
+                            selector = nl.join(wrap(selector))
+                        result += _tb + selector + nl
+                        open_selectors = True
+                    old_selectors = selectors
+                    scope = set()
+                if selectors:
+                    _tb += tb
+                if rule[OPTIONS].get('verbosity', 0) > 1:
+                    result += _tb + '/* file: ' + rule[FILEID] + ' */' + nl
+                    if rule[CONTEXT]:
+                        result += _tb + '/* vars:' + nl
+                        for k, v in rule[CONTEXT].items():
+                            result += _tb + _tb + k + ' = ' + v + ';' + nl
+                        result += _tb + '*/' + nl
+                result += self._print_properties(rule[PROPERTIES], scope, [old_property], sc, sp, _tb, nl, wrap)
+
+        if open_media:
+            _tb = tb
+        else:
+            _tb = ''
+        if open_selectors:
+            if not sc:
+                if result[-1] == ';':
+                    result = result[:-1]
+            result += _tb + '}' + nl
+
+        if open_media:
+            if not sc:
+                if result[-1] == ';':
+                    result = result[:-1]
+            result += '}' + nl
+
+        return result + '\n'
+
+    def _print_properties(self, properties, scope=None, old_property=None, sc=True, sp=' ', _tb='', nl='\n', wrap=None):
+        if wrap is None:
+            wrap = textwrap.TextWrapper(break_long_words=False)
+            wrap.wordsep_re = re.compile(r'(?<=,)(\s*)')
+            wrap = wrap.wrap
+        result = ''
+        old_property = [None] if old_property is None else old_property
+        scope = set() if scope is None else scope
+        for lineno, prop, value in properties:
+            if value is not None:
+                if nl:
+                    value = (nl + _tb + _tb).join(wrap(value))
+                property = prop + ':' + sp + value
+            else:
+                property = prop
+            if '!default' in property:
+                property = property.replace('!default', '').replace('  ', ' ').strip()
+                if prop in scope:
+                    continue
+            if old_property[0] != property:
+                old_property[0] = property
+                scope.add(prop)
+                old_property[0] = property
+                result += _tb + property + ';' + nl
+        return result
+
+    def calculate(self, _base_str, context, options, rule):
+        try:
+            better_expr_str = self._replaces[_base_str]
+        except KeyError:
+            better_expr_str = _base_str
+
+            if _skip_word_re.match(better_expr_str) and '- ' not in better_expr_str and ' and ' not in better_expr_str and ' or ' not in better_expr_str and 'not ' not in better_expr_str:
+                self._replaces[_base_str] = better_expr_str
+                return better_expr_str
+
+            better_expr_str = self.do_glob_math(better_expr_str, context, options, rule)
+
+            better_expr_str = eval_expr(better_expr_str, rule, True)
+            if better_expr_str is None:
+                better_expr_str = self.apply_vars(_base_str, context, options, rule)
+
+            if '$' not in _base_str:
+                self._replaces[_base_str] = better_expr_str
+        return better_expr_str
+
+    def _calculate_expr(self, context, options, rule, _dequote):
+        def __calculate_expr(result):
+            _group0 = result.group(1)
+            _base_str = _group0
+            try:
+                better_expr_str = self._replaces[_group0]
+            except KeyError:
+                better_expr_str = _base_str
+
+                if _skip_re.match(better_expr_str) and '- ' not in better_expr_str:
+                    self._replaces[_group0] = better_expr_str
+                    return better_expr_str
+
+                better_expr_str = eval_expr(better_expr_str, rule)
+
+                if better_expr_str is None:
+                    better_expr_str = _base_str
+                elif _dequote:
+                    better_expr_str = dequote(better_expr_str)
+
+                if '$' not in _group0:
+                    self._replaces[_group0] = better_expr_str
+
+            return better_expr_str
+        return __calculate_expr
+
+    def do_glob_math(self, cont, context, options, rule, _dequote=False):
+        cont = str(cont)
+        if '#{' not in cont:
+            return cont
+        cont = _expr_glob_re.sub(self._calculate_expr(context, options, rule, _dequote), cont)
+        return cont
+
+    @print_timing(3)
+    def post_process(self, cont):
+        compress = self._scss_opts.get('compress', 1) and 'compress_' or ''
+        # short colors:
+        if self._scss_opts.get(compress + 'short_colors', 1):
+            cont = _short_color_re.sub(r'#\1\2\3', cont)
+            # color names:
+        if self._scss_opts.get(compress + 'reverse_colors', 1):
+            cont = _reverse_colors_re.sub(lambda m: _reverse_colors[m.group(0).lower()], cont)
+        if compress:
+            # zero units out (i.e. 0px or 0em -> 0):
+            cont = _zero_units_re.sub('0', cont)
+            # remove zeros before decimal point (i.e. 0.3 -> .3)
+            cont = _zero_re.sub('.', cont)
+        return cont
+
+
+import hashlib
+import base64
+import datetime
+import mimetypes
+import glob
+import math
+import operator
+import colorsys
+
+try:
+    import cStringIO as StringIO
+except:
+    import StringIO
+
+try:
+    from PIL import Image, ImageDraw
+except ImportError:
+    try:
+        import Image, ImageDraw
+    except:
+        Image = None
+
+
+################################################################################
+
+
+def to_str(num):
+    if isinstance(num, dict):
+        s = sorted(num.items())
+        sp = num.get('_', '')
+        return (sp + ' ').join(to_str(v) for n, v in s if n != '_')
+    elif isinstance(num, float):
+        num = ('%0.03f' % round(num, 3)).rstrip('0').rstrip('.')
+        return num
+    elif isinstance(num, bool):
+        return 'true' if num else 'false'
+    elif num is None:
+        return ''
+    return str(num)
+
+
+def to_float(num):
+    if isinstance(num, (float, int)):
+        return float(num)
+    num = to_str(num)
+    if num and num[-1] == '%':
+        return float(num[:-1]) / 100.0
+    else:
+        return float(num)
+
+
+hex2rgba = {
+    9: lambda c: (int(c[1:3], 16), int(c[3:5], 16), int(c[5:7], 16), int(c[7:9], 16)),
+    7: lambda c: (int(c[1:3], 16), int(c[3:5], 16), int(c[5:7], 16), 1.0),
+    5: lambda c: (int(c[1] * 2, 16), int(c[2] * 2, 16), int(c[3] * 2, 16), int(c[4] * 2, 16)),
+    4: lambda c: (int(c[1] * 2, 16), int(c[2] * 2, 16), int(c[3] * 2, 16), 1.0),
+    }
+
+
+def escape(s):
+    return re.sub(r'''(["'])''', r'\\\1', s)
+
+
+def unescape(s):
+    return re.sub(r'''\\(['"])''', r'\1', s)
+
+
+################################################################################
+# Sass/Compass Library Functions:
+
+
+def _rgb(r, g, b, type='rgb'):
+    return _rgba(r, g, b, 1.0, type)
+
+
+def _rgba(r, g, b, a, type='rgba'):
+    c = NumberValue(r), NumberValue(g), NumberValue(b), NumberValue(a)
+
+    col = [c[i].value * 255.0 if (c[i].unit == '%' or c[i].value > 0 and c[i].value <= 1) else
+           0.0 if c[i].value < 0 else
+           255.0 if c[i].value > 255 else
+           c[i].value
+           for i in range(3)
+    ]
+    col += [0.0 if c[3].value < 0 else 1.0 if c[3].value > 1 else c[3].value]
+    col += [type]
+    return ColorValue(col)
+
+
+def _color_type(color, a, type):
+    color = ColorValue(color).value
+    a = NumberValue(a).value if a is not None else color[3]
+    col = list(color[:3])
+    col += [0.0 if a < 0 else 1.0 if a > 1 else a]
+    col += [type]
+    return ColorValue(col)
+
+
+def _rgb2(color):
+    return _color_type(color, 1.0, 'rgb')
+
+
+def _rgba2(color, a=None):
+    return _color_type(color, a, 'rgba')
+
+
+def _hsl2(color):
+    return _color_type(color, 1.0, 'hsl')
+
+
+def _hsla2(color, a=None):
+    return _color_type(color, a, 'hsla')
+
+
+def _ie_hex_str(color):
+    c = ColorValue(color).value
+    return StringValue('#%02X%02X%02X%02X' % (round(c[3] * 255), round(c[0]), round(c[1]), round(c[2])))
+
+
+def _hsl(h, s, l, type='hsl'):
+    return _hsla(h, s, l, 1.0, type)
+
+
+def _hsla(h, s, l, a, type='hsla'):
+    c = NumberValue(h), NumberValue(s), NumberValue(l), NumberValue(a)
+    col = [c[0] if (c[0].unit == '%' and c[0].value > 0 and c[0].value <= 1) else (c[0].value % 360.0) / 360.0]
+    col += [0.0 if cl <= 0 else 1.0 if cl >= 1.0 else cl
+            for cl in [
+    c[i].value if (c[i].unit == '%' or c[i].value > 0 and c[i].value <= 1) else
+    c[i].value / 100.0
+    for i in range(1, 4)
+    ]
+    ]
+    col += [type]
+    c = [c * 255.0 for c in colorsys.hls_to_rgb(col[0], 0.999999 if col[2] == 1 else col[2], 0.999999 if col[1] == 1 else col[1])] + [col[3], type]
+    col = ColorValue(c)
+    return col
+
+
+def __rgba_op(op, color, r, g, b, a):
+    color = ColorValue(color)
+    c = color.value
+    a = [
+        None if r is None else NumberValue(r).value,
+        None if g is None else NumberValue(g).value,
+        None if b is None else NumberValue(b).value,
+        None if a is None else NumberValue(a).value,
+        ]
+    # Do the additions:
+    c = [op(c[i], a[i]) if op is not None and a[i] is not None else a[i] if a[i] is not None else c[i] for i in range(4)]
+    # Validations:
+    r = 255.0, 255.0, 255.0, 1.0
+    c = [0.0 if c[i] < 0 else r[i] if c[i] > r[i] else c[i] for i in range(4)]
+    color.value = tuple(c)
+    return color
+
+
+def _opacify(color, amount):
+    return __rgba_op(operator.__add__, color, 0, 0, 0, amount)
+
+
+def _transparentize(color, amount):
+    return __rgba_op(operator.__sub__, color, 0, 0, 0, amount)
+
+
+def __hsl_op(op, color, h, s, l):
+    color = ColorValue(color)
+    c = color.value
+    h = None if h is None else NumberValue(h)
+    s = None if s is None else NumberValue(s)
+    l = None if l is None else NumberValue(l)
+    a = [
+        None if h is None else h.value / 360.0,
+        None if s is None else s.value / 100.0 if s.unit != '%' and s.value >= 1 else s.value,
+        None if l is None else l.value / 100.0 if l.unit != '%' and l.value >= 1 else l.value,
+        ]
+    # Convert to HSL:
+    h, l, s = list(colorsys.rgb_to_hls(c[0] / 255.0, c[1] / 255.0, c[2] / 255.0))
+    c = h, s, l
+    # Do the additions:
+    c = [0.0 if c[i] < 0 else 1.0 if c[i] > 1 else op(c[i], a[i]) if op is not None and a[i] is not None else a[i] if a[i] is not None else c[i] for i in range(3)]
+    # Validations:
+    c[0] = (c[0] * 360.0) % 360
+    r = 360.0, 1.0, 1.0
+    c = [0.0 if c[i] < 0 else r[i] if c[i] > r[i] else c[i] for i in range(3)]
+    # Convert back to RGB:
+    c = colorsys.hls_to_rgb(c[0] / 360.0, 0.999999 if c[2] == 1 else c[2], 0.999999 if c[1] == 1 else c[1])
+    color.value = (c[0] * 255.0, c[1] * 255.0, c[2] * 255.0, color.value[3])
+    return color
+
+
+def _lighten(color, amount):
+    return __hsl_op(operator.__add__, color, 0, 0, amount)
+
+
+def _darken(color, amount):
+    return __hsl_op(operator.__sub__, color, 0, 0, amount)
+
+
+def _saturate(color, amount):
+    return __hsl_op(operator.__add__, color, 0, amount, 0)
+
+
+def _desaturate(color, amount):
+    return __hsl_op(operator.__sub__, color, 0, amount, 0)
+
+
+def _grayscale(color):
+    return __hsl_op(operator.__sub__, color, 0, 100.0, 0)
+
+
+def _adjust_hue(color, degrees):
+    return __hsl_op(operator.__add__, color, degrees, 0, 0)
+
+
+def _complement(color):
+    return __hsl_op(operator.__add__, color, 180.0, 0, 0)
+
+
+def _invert(color):
+    """
+    Returns the inverse (negative) of a color.
+    The red, green, and blue values are inverted, while the opacity is left alone.
+    """
+    col = ColorValue(color)
+    c = col.value
+    c[0] = 255.0 - c[0]
+    c[1] = 255.0 - c[1]
+    c[2] = 255.0 - c[2]
+    return col
+
+
+def _adjust_lightness(color, amount):
+    return __hsl_op(operator.__add__, color, 0, 0, amount)
+
+
+def _adjust_saturation(color, amount):
+    return __hsl_op(operator.__add__, color, 0, amount, 0)
+
+
+def _scale_lightness(color, amount):
+    return __hsl_op(operator.__mul__, color, 0, 0, amount)
+
+
+def _scale_saturation(color, amount):
+    return __hsl_op(operator.__mul__, color, 0, amount, 0)
+
+
+def _asc_color(op, color, saturation=None, lightness=None, red=None, green=None, blue=None, alpha=None):
+    if lightness or saturation:
+        color = __hsl_op(op, color, 0, saturation, lightness)
+    if red or green or blue or alpha:
+        color = __rgba_op(op, color, red, green, blue, alpha)
+    return color
+
+
+def _adjust_color(color, saturation=None, lightness=None, red=None, green=None, blue=None, alpha=None):
+    return _asc_color(operator.__add__, color, saturation, lightness, red, green, blue, alpha)
+
+
+def _scale_color(color, saturation=None, lightness=None, red=None, green=None, blue=None, alpha=None):
+    return _asc_color(operator.__mul__, color, saturation, lightness, red, green, blue, alpha)
+
+
+def _change_color(color, saturation=None, lightness=None, red=None, green=None, blue=None, alpha=None):
+    return _asc_color(None, color, saturation, lightness, red, green, blue, alpha)
+
+
+def _mix(color1, color2, weight=None):
+    """
+    Mixes together two colors. Specifically, takes the average of each of the
+    RGB components, optionally weighted by the given percentage.
+    The opacity of the colors is also considered when weighting the components.
+
+    Specifically, takes the average of each of the RGB components,
+    optionally weighted by the given percentage.
+    The opacity of the colors is also considered when weighting the components.
+
+    The weight specifies the amount of the first color that should be included
+    in the returned color.
+    50%, means that half the first color
+        and half the second color should be used.
+    25% means that a quarter of the first color
+        and three quarters of the second color should be used.
+
+    For example:
+
+        mix(#f00, #00f) => #7f007f
+        mix(#f00, #00f, 25%) => #3f00bf
+        mix(rgba(255, 0, 0, 0.5), #00f) => rgba(63, 0, 191, 0.75)
+
+    """
+    # This algorithm factors in both the user-provided weight
+    # and the difference between the alpha values of the two colors
+    # to decide how to perform the weighted average of the two RGB values.
+    #
+    # It works by first normalizing both parameters to be within [-1, 1],
+    # where 1 indicates "only use color1", -1 indicates "only use color 0",
+    # and all values in between indicated a proportionately weighted average.
+    #
+    # Once we have the normalized variables w and a,
+    # we apply the formula (w + a)/(1 + w*a)
+    # to get the combined weight (in [-1, 1]) of color1.
+    # This formula has two especially nice properties:
+    #
+    #   * When either w or a are -1 or 1, the combined weight is also that number
+    #     (cases where w * a == -1 are undefined, and handled as a special case).
+    #
+    #   * When a is 0, the combined weight is w, and vice versa
+    #
+    # Finally, the weight of color1 is renormalized to be within [0, 1]
+    # and the weight of color2 is given by 1 minus the weight of color1.
+    #
+    # Algorithm from the Sass project: http://sass-lang.com/
+
+    c1 = ColorValue(color1).value
+    c2 = ColorValue(color2).value
+    p = NumberValue(weight).value if weight is not None else 0.5
+    p = 0.0 if p < 0 else 1.0 if p > 1 else p
+
+    w = p * 2 - 1
+    a = c1[3] - c2[3]
+
+    w1 = ((w if (w * a == -1) else (w + a) / (1 + w * a)) + 1) / 2.0
+
+    w2 = 1 - w1
+    q = [w1, w1, w1, p]
+    r = [w2, w2, w2, 1 - p]
+
+    color = ColorValue(None).merge(c1).merge(c2)
+    color.value = [c1[i] * q[i] + c2[i] * r[i] for i in range(4)]
+
+    return color
+
+
+def _red(color):
+    c = ColorValue(color).value
+    return NumberValue(c[0])
+
+
+def _green(color):
+    c = ColorValue(color).value
+    return NumberValue(c[1])
+
+
+def _blue(color):
+    c = ColorValue(color).value
+    return NumberValue(c[2])
+
+
+def _alpha(color):
+    c = ColorValue(color).value
+    return NumberValue(c[3])
+
+
+def _hue(color):
+    c = ColorValue(color).value
+    h, l, s = colorsys.rgb_to_hls(c[0] / 255.0, c[1] / 255.0, c[2] / 255.0)
+    ret = NumberValue(h * 360.0)
+    ret.units = {'deg': _units_weights.get('deg', 1), '_': 'deg'}
+    return ret
+
+
+def _saturation(color):
+    c = ColorValue(color).value
+    h, l, s = colorsys.rgb_to_hls(c[0] / 255.0, c[1] / 255.0, c[2] / 255.0)
+    ret = NumberValue(s)
+    ret.units = {'%': _units_weights.get('%', 1), '_': '%'}
+    return ret
+
+
+def _lightness(color):
+    c = ColorValue(color).value
+    h, l, s = colorsys.rgb_to_hls(c[0] / 255.0, c[1] / 255.0, c[2] / 255.0)
+    ret = NumberValue(l)
+    ret.units = {'%': _units_weights.get('%', 1), '_': '%'}
+    return ret
+
+
+def __color_stops(percentages, *args):
+    if len(args) == 1:
+        if isinstance(args[0], list):
+            return args[0]
+        elif isinstance(args[0], StringValue):
+            color_stops = []
+            colors = split_params(args[0].value)
+            for color in colors:
+                color = color.strip()
+                if color.startswith('color-stop('):
+                    s, c = split_params(color[11:].rstrip(')'))
+                    s = s.strip()
+                    c = c.strip()
+                else:
+                    c, s = color.split()
+                color_stops.append((to_float(s), c))
+            return color_stops
+
+    colors = []
+    stops = []
+    prev_color = False
+    for c in args:
+        if isinstance(c, ListValue):
+            for i, c in c.items():
+                if isinstance(c, ColorValue):
+                    if prev_color:
+                        stops.append(None)
+                    colors.append(c)
+                    prev_color = True
+                elif isinstance(c, NumberValue):
+                    stops.append(c)
+                    prev_color = False
+        else:
+            if isinstance(c, ColorValue):
+                if prev_color:
+                    stops.append(None)
+                colors.append(c)
+                prev_color = True
+            elif isinstance(c, NumberValue):
+                stops.append(NumberValue(c))
+                prev_color = False
+    if prev_color:
+        stops.append(None)
+    stops = stops[:len(colors)]
+    if percentages:
+        max_stops = max(s and (s.value if s.unit != '%' else None) or None for s in stops)
+    else:
+        max_stops = max(s and (s if s.unit != '%' else None) or None for s in stops)
+    stops = [s and (s.value / max_stops if s.unit != '%' else s.value) for s in stops]
+    stops[0] = 0
+
+    init = 0
+    start = None
+    for i, s in enumerate(stops + [1.0]):
+        if s is None:
+            if start is None:
+                start = i
+            end = i
+        else:
+            final = s
+            if start is not None:
+                stride = (final - init) / (end - start + 1 + (1 if i < len(stops) else 0))
+                for j in range(start, end + 1):
+                    stops[j] = init + stride * (j - start + 1)
+            init = final
+            start = None
+
+    if not max_stops or percentages:
+        stops = [NumberValue(s, '%') for s in stops]
+    else:
+        stops = [s * max_stops for s in stops]
+    return zip(stops, colors)
+
+
+def _grad_color_stops(*args):
+    color_stops = __color_stops(True, *args)
+    ret = ', '.join(['color-stop(%s, %s)' % (to_str(s), c) for s, c in color_stops])
+    return StringValue(ret)
+
+
+def __grad_end_position(radial, color_stops):
+    return __grad_position(-1, 100, radial, color_stops)
+
+
+def __grad_position(index, default, radial, color_stops):
+    try:
+        stops = NumberValue(color_stops[index][0])
+        if radial and stops.unit != 'px' and (index == 0 or index == -1 or index == len(color_stops) - 1):