[Python-modules-commits] [python-pyscss] 01/07: Import python-pyscss_1.3.5.orig.tar.gz

Wolfgang Borgert debacle at moszumanska.debian.org
Sat Jul 23 16:23:24 UTC 2016


This is an automated email from the git hooks/post-receive script.

debacle pushed a commit to branch master
in repository python-pyscss.

commit a9738572b12839d4c3f4c7561e5b92f81a04ca67
Author: W. Martin Borgert <debacle at debian.org>
Date:   Sat Jul 23 12:52:00 2016 +0200

    Import python-pyscss_1.3.5.orig.tar.gz
---
 docs/back-matter.rst                               |  32 ++++-
 scss/ast.py                                        |  41 +++++-
 scss/compiler.py                                   | 113 +++++------------
 scss/cssdefs.py                                    |  27 ++--
 scss/extension/compass/gradients.py                |  11 +-
 scss/extension/core.py                             | 141 ++++++++++++++-------
 scss/extension/fonts.py                            |  10 +-
 scss/grammar/expression.g                          |  11 +-
 scss/grammar/expression.py                         |  62 +++++----
 scss/legacy.py                                     |   8 +-
 scss/less2scss.py                                  | 106 ++++++++++++++++
 scss/namespace.py                                  |  14 +-
 scss/scss_meta.py                                  |   4 +-
 scss/selector.py                                   |  24 +++-
 scss/source.py                                     |   6 +-
 scss/tests/extension/test_core.py                  |   8 ++
 scss/tests/files/bugs/append.css                   |   5 +
 scss/tests/files/bugs/append.scss                  |  10 ++
 scss/tests/files/bugs/base64url.css                |   3 +
 scss/tests/files/bugs/base64url.scss               |   5 +
 scss/tests/files/bugs/trailing-combinators.css     |  11 ++
 scss/tests/files/bugs/trailing-combinators.scss    |  18 +++
 scss/tests/files/bugs/unit-float-precision.css     |   3 +
 scss/tests/files/bugs/unit-float-precision.scss    |   7 +
 scss/tests/files/compass/current-color.css         |   3 +
 scss/tests/files/compass/current-color.scss        |   4 +
 scss/tests/files/fonts/fonts.css                   |   4 +-
 scss/tests/files/general/css-calc.css              |   2 +
 scss/tests/files/general/css-calc.scss             |   2 +
 .../tests/files/general/global-variable-exists.css |   5 +
 .../files/general/global-variable-exists.scss      |  27 ++++
 scss/tests/files/general/if-lazy.css               |   4 +
 scss/tests/files/general/if-lazy.scss              |   6 +
 .../files/regressions/include/_issue-334.scss      |   3 +
 .../files/regressions/include/_issue.334.scss      |   3 +
 scss/tests/files/regressions/issue-258.css         |   1 +
 scss/tests/files/regressions/issue-334-a.css       |   3 +
 scss/tests/files/regressions/issue-334-a.scss      |   1 +
 scss/tests/files/regressions/issue-334-b.css       |   3 +
 scss/tests/files/regressions/issue-334-b.scss      |   1 +
 scss/tests/files/regressions/issue-334-c.css       |   3 +
 scss/tests/files/regressions/issue-334-c.scss      |   1 +
 scss/tests/files/regressions/issue-334-d.css       |   3 +
 scss/tests/files/regressions/issue-334-d.scss      |   1 +
 scss/tests/files/regressions/issue-334-e.css       |   3 +
 scss/tests/files/regressions/issue-334-e.scss      |   1 +
 scss/tests/files/regressions/issue-334-f.css       |   3 +
 scss/tests/files/regressions/issue-334-f.scss      |   1 +
 scss/tests/files/regressions/issue-334-g.css       |   3 +
 scss/tests/files/regressions/issue-334-g.scss      |   1 +
 scss/tests/files/regressions/issue-334-h.css       |   3 +
 scss/tests/files/regressions/issue-334-h.scss      |   1 +
 scss/tool.py                                       |   6 +-
 scss/types.py                                      |  19 ++-
 setup.py                                           |   1 +
 tox.ini                                            |   4 +-
 56 files changed, 610 insertions(+), 196 deletions(-)

diff --git a/docs/back-matter.rst b/docs/back-matter.rst
index fdae94d..9d81638 100644
--- a/docs/back-matter.rst
+++ b/docs/back-matter.rst
@@ -46,7 +46,7 @@ License and copyright
 ---------------------
 
 Copyright © 2012 German M. Bravo (Kronuz), with additional heavy contributions
-by Eevee (Alex Munroe).  Licensed under the `MIT license`_.
+by Eevee (Lexy Munroe).  Licensed under the `MIT license`_.
 
 .. _MIT license: http://www.opensource.org/licenses/mit-license.php
 
@@ -67,6 +67,36 @@ working hours.  Yelp does not claim copyright.
 Changelog
 ---------
 
+1.3.5 (June 8, 2016)
+^^^^^^^^^^^^^^^^^^^^
+
+* The new ``less2scss`` module attempts to convert Less syntax to SCSS.
+* The ``*-exists`` functions from Sass 3.3 are now supported.
+* The contents of a file ``@import``-ed in the middle of an input file now
+  appears in the expected place, not at the end of the output.
+* Double-slashes within URLs, as often happens with base64-encoded data URLs,
+  are no longer stripped as comments.
+* Nesting selectors that end with a combinator, e.g. ``div > { p { ... } }``,
+  now works correctly.
+* ``invert()`` is now left alone when the argument is a number, indicating the
+  CSS filter rather than the Sass function.
+* ``if()`` now evaluates its arguments lazily.
+* ``str-slice()`` now silently corrects out-of-bounds indices.
+* ``append()`` now defaults to returning a space-delimited list, when the given
+  list has fewer than two elements.
+* ``-moz-calc`` and ``-webkit-calc`` are recognized as variants of the
+  ``calc()`` CSS function.
+* Filenames containing dots can now be imported.
+* Properties with a computed value of ``null`` are now omitted from the output.
+* The ``opacity`` token in IE's strange ``alpha(opacity=N)`` construct is now
+  recognized case-insensitively.
+* The Compass gradient functions now recognize ``currentColor`` as a color.
+* The fonts extension should now work under Python 3.
+* Escaped astral plane characters no longer crash narrow Python 2 builds.
+* The alpha value in ``rgba(...)`` is no longer truncated to only two decimal places.
+* Some edge cases with float precision were fixed, so 742px - 40px is no longer
+  701.99999999px.
+
 1.3.4 (Dec 15, 2014)
 ^^^^^^^^^^^^^^^^^^^^
 
diff --git a/scss/ast.py b/scss/ast.py
index a8ab69b..16a4518 100644
--- a/scss/ast.py
+++ b/scss/ast.py
@@ -212,10 +212,6 @@ class CallOp(Expression):
         funct = None
         try:
             funct = calculator.namespace.function(func_name, argspec_len)
-            # @functions take a ns as first arg.  TODO: Python functions possibly
-            # should too
-            if getattr(funct, '__name__', None) == '__call':
-                funct = partial(funct, calculator.namespace)
         except KeyError:
             try:
                 # DEVIATION: Fall back to single parameter
@@ -226,7 +222,12 @@ class CallOp(Expression):
                     log.error("Function not found: %s:%s", func_name, argspec_len, extra={'stack': True})
 
         if funct:
-            ret = funct(*args, **kwargs)
+            if getattr(funct, '_pyscss_needs_namespace', False):
+                # @functions and some Python functions take the namespace as an
+                # extra first argument
+                ret = funct(calculator.namespace, *args, **kwargs)
+            else:
+                ret = funct(*args, **kwargs)
             if not isinstance(ret, Value):
                 raise TypeError("Expected Sass type as return value, got %r" % (ret,))
             return ret
@@ -532,3 +533,33 @@ class AlphaFunctionLiteral(Expression):
             # TODO compress
             contents = child.render()
         return Function('opacity=' + contents, 'alpha', quotes=None)
+
+
+class TernaryOp(Expression):
+    """Sass implements this with a function:
+
+        prop: if(condition, true-value, false-value);
+
+    However, the second and third arguments are guaranteed not to be evaluated
+    unless necessary.  Functions always receive evaluated arguments, so this is
+    a syntactic construct in disguise.
+    """
+    def __repr__(self):
+        return '<%s(%r, %r, %r)>' % (
+            self.__class__.__name__,
+            self.condition,
+            self.true_expression,
+            self.false_expression,
+        )
+
+    def __init__(self, list_literal):
+        args = list_literal.items
+        if len(args) != 3:
+            raise SyntaxError("if() must have exactly 3 arguments")
+        self.condition, self.true_expression, self.false_expression = args
+
+    def evaluate(self, calculator, divide=False):
+        if self.condition.evaluate(calculator, divide=True):
+            return self.true_expression.evaluate(calculator, divide=True)
+        else:
+            return self.false_expression.evaluate(calculator, divide=True)
diff --git a/scss/compiler.py b/scss/compiler.py
index 434f7ac..7b8dbd1 100644
--- a/scss/compiler.py
+++ b/scss/compiler.py
@@ -182,6 +182,13 @@ class Compiler(object):
                 raise
 
     def compile(self, *filenames):
+        # TODO this doesn't spit out the compilation itself, so if you want to
+        # get something out besides just the output, you have to copy this
+        # method.  that sucks.
+        # TODO i think the right thing is to get all the constructors out of
+        # SourceFile, since it's really the compiler that knows the import
+        # paths and should be consulted about this.  reconsider all this (but
+        # preserve it for now, SIGH) once importers are a thing
         compilation = self.make_compilation()
         for filename in filenames:
             # TODO maybe SourceFile should not be exposed to the end user, and
@@ -274,41 +281,29 @@ class Compilation(object):
         return source
 
     def run(self):
-        # this will compile and manage rule: child objects inside of a node
-        self.parse_children()
+        # Any @import will add the source file to self.sources and infect this
+        # list, so make a quick copy to insulate against that
+        # TODO maybe @import should just not do that?
+        for source_file in list(self.sources):
+            rule = SassRule(
+                source_file=source_file,
+                lineno=1,
+
+                unparsed_contents=source_file.contents,
+                namespace=self.root_namespace,
+            )
+            self.rules.append(rule)
+            self.manage_children(rule, scope=None)
+            self._warn_unused_imports(rule)
 
-        # this will manage @extends
+        # Run through all the rules and apply @extends in a separate pass
         self.rules = self.apply_extends(self.rules)
 
-        rules_by_file, css_files = self.parse_properties()
-
-        all_rules = 0
-        all_selectors = 0
-        exceeded = ''
-        final_cont = ''
-        files = len(css_files)
-        for source_file in css_files:
-            rules = rules_by_file[source_file]
-            fcont, total_rules, total_selectors = self.create_css(rules)
-            all_rules += total_rules
-            all_selectors += total_selectors
-            # TODO i would love for the output of this function to be something
-            # useful for producing stats, so this stuff can live on the Scss
-            # class only
-            if not exceeded and all_selectors > 4095:
-                exceeded = " (IE exceeded!)"
-                log.error("Maximum number of supported selectors in Internet Explorer (4095) exceeded!")
-            if files > 1 and self.compiler.generate_source_map:
-                final_cont += "/* %s %s generated from '%s' add up to a total of %s %s accumulated%s */\n" % (
-                    total_selectors,
-                    'selector' if total_selectors == 1 else 'selectors',
-                    source_file.path,
-                    all_selectors,
-                    'selector' if all_selectors == 1 else 'selectors',
-                    exceeded)
-            final_cont += fcont
-
-        return final_cont
+        output, total_selectors = self.create_css(self.rules)
+        if total_selectors >= 4096:
+            log.error("Maximum number of supported selectors in Internet Explorer (4095) exceeded!")
+
+        return output
 
     def parse_selectors(self, raw_selectors):
         """
@@ -333,26 +328,6 @@ class Compilation(object):
 
         return selectors, parents
 
-    # @print_timing(3)
-    def parse_children(self, scope=None):
-        children = []
-        root_namespace = self.root_namespace
-        for source_file in self.sources:
-            rule = SassRule(
-                source_file=source_file,
-                lineno=1,
-
-                unparsed_contents=source_file.contents,
-                namespace=root_namespace,
-            )
-            self.rules.append(rule)
-            children.append(rule)
-
-        for rule in children:
-            self.manage_children(rule, scope)
-
-        self._warn_unused_imports(self.rules[0])
-
     def _warn_unused_imports(self, rule):
         if not rule.legacy_compiler_options.get(
                 'warn_unused', self.compiler.warn_unused_imports):
@@ -709,6 +684,7 @@ class Compilation(object):
                         return e.retval
                     else:
                         return Null()
+                __call._pyscss_needs_namespace = True
                 return __call
             _mixin = _call(mixin)
             _mixin.mixin = mixin
@@ -1043,10 +1019,8 @@ class Compilation(object):
         _rule = rule.copy()
         _rule.unparsed_contents = block.unparsed_contents
         _rule.namespace = rule.namespace
-        _rule.properties = {}
+        _rule.properties = []
         self.manage_children(_rule, scope)
-        for name, value in _rule.properties.items():
-            rule.namespace.set_variable(name, value)
     _at_vars = _at_variables
 
     # @print_timing(10)
@@ -1063,7 +1037,7 @@ class Compilation(object):
         except IndexError:
             is_var = False
         if is_var:
-            warn_deprecated(rule, "Assignment with = is deprecated; use :.")
+            warn_deprecated(rule, "Assignment with = is deprecated; use : instead.")
         calculator = self._make_calculator(rule.namespace)
         prop = prop.strip()
         prop = calculator.do_glob_math(prop)
@@ -1121,6 +1095,8 @@ class Compilation(object):
                 # TODO kill this branch
                 pass
             else:
+                if value.is_null:
+                    return
                 style = rule.legacy_compiler_options.get(
                     'style', self.compiler.output_style)
                 compress = style == 'compressed'
@@ -1268,7 +1244,7 @@ class Compilation(object):
                     # TODO implement !optional
                     warn_deprecated(
                         rule,
-                        "Can't find any matching rules to extend {0!r} -- this"
+                        "Can't find any matching rules to extend {0!r} -- this "
                         "will be fatal in 2.0, unless !optional is specified!"
                         .format(selector.render()))
                     continue
@@ -1316,25 +1292,6 @@ class Compilation(object):
         return [rule for rule in rules if not rule.is_pure_placeholder]
 
     # @print_timing(3)
-    def parse_properties(self):
-        css_files = []
-        seen_files = set()
-        rules_by_file = {}
-
-        for rule in self.rules:
-            source_file = rule.source_file
-            rules_by_file.setdefault(source_file, []).append(rule)
-
-            if rule.is_empty:
-                continue
-
-            if source_file not in seen_files:
-                seen_files.add(source_file)
-                css_files.append(source_file)
-
-        return rules_by_file, css_files
-
-    # @print_timing(3)
     def create_css(self, rules):
         """
         Generate the final CSS string
@@ -1407,7 +1364,6 @@ class Compilation(object):
 
         prev_ancestry_headers = []
 
-        total_rules = 0
         total_selectors = 0
 
         result = ''
@@ -1488,7 +1444,6 @@ class Compilation(object):
                     header_string = header.render()
                 result += tb * (i + nesting) + header_string + sp + '{' + nl
 
-                total_rules += 1
                 if header.is_selector:
                     total_selectors += 1
 
@@ -1507,7 +1462,7 @@ class Compilation(object):
         if not result.endswith('\n'):
             result += '\n'
 
-        return (result, total_rules, total_selectors)
+        return (result, total_selectors)
 
     def _print_properties(self, properties, sc=True, sp=' ', tb='', nl='\n', lnl=' '):
         result = ''
diff --git a/scss/cssdefs.py b/scss/cssdefs.py
index 46b6877..6b4f5b7 100644
--- a/scss/cssdefs.py
+++ b/scss/cssdefs.py
@@ -1,6 +1,7 @@
 """Constants and functions defined by the CSS specification, not specific to
 Sass.
 """
+from fractions import Fraction
 from math import pi
 import re
 
@@ -166,15 +167,15 @@ BASE_UNIT_CONVERSIONS = {
     # Lengths
     'mm': (1, 'mm'),
     'cm': (10, 'mm'),
-    'in': (25.4, 'mm'),
-    'px': (25.4 / 96, 'mm'),
-    'pt': (25.4 / 72, 'mm'),
-    'pc': (25.4 / 6, 'mm'),
+    'in': (Fraction(254, 10), 'mm'),
+    'px': (Fraction(254, 960), 'mm'),
+    'pt': (Fraction(254, 720), 'mm'),
+    'pc': (Fraction(254, 60), 'mm'),
 
     # Angles
-    'deg': (1 / 360, 'turn'),
-    'grad': (1 / 400, 'turn'),
-    'rad': (pi / 2, 'turn'),
+    'deg': (Fraction(1, 360), 'turn'),
+    'grad': (Fraction(1, 400), 'turn'),
+    'rad': (Fraction.from_float(pi / 2), 'turn'),
     'turn': (1, 'turn'),
 
     # Times
@@ -187,7 +188,7 @@ BASE_UNIT_CONVERSIONS = {
 
     # Resolutions
     'dpi': (1, 'dpi'),
-    'dpcm': (2.54, 'dpi'),
+    'dpcm': (Fraction(254 / 100), 'dpi'),
     'dppx': (96, 'dpi'),
 }
 
@@ -252,7 +253,7 @@ def cancel_base_units(units, to_remove):
     # Copy the dict since we're about to mutate it
     to_remove = to_remove.copy()
     remaining_units = []
-    total_factor = 1
+    total_factor = Fraction(1)
 
     for unit in units:
         factor, base_unit = get_conversion_factor(unit)
@@ -448,7 +449,11 @@ escape_rx = re.compile(r"(?s)\\([0-9a-fA-F]{1,6})[\n\t ]?|\\(.)|\\\n")
 
 def _unescape_one(match):
     if match.group(1) is not None:
-        return six.unichr(int(match.group(1), 16))
+        try:
+            return six.unichr(int(match.group(1), 16))
+        except ValueError:
+            return (r'\U%08x' % int(match.group(1), 16)).decode(
+                'unicode-escape')
     elif match.group(2) is not None:
         return match.group(2)
     else:
@@ -480,6 +485,8 @@ _collapse_properties_space_re = re.compile(r'([:#])\s*{')
 _variable_re = re.compile('^\\$[-a-zA-Z0-9_]+$')
 
 _strings_re = re.compile(r'([\'"]).*?\1')
+# TODO i know, this is clumsy and won't always work; it's better than nothing
+_urls_re = re.compile(r'url[(].*?[)]')
 
 _has_placeholder_re = re.compile(r'(?<!\w)([a-z]\w*)?%')
 _prop_split_re = re.compile(r'[:=]')
diff --git a/scss/extension/compass/gradients.py b/scss/extension/compass/gradients.py
index dbcaea8..031fba2 100644
--- a/scss/extension/compass/gradients.py
+++ b/scss/extension/compass/gradients.py
@@ -19,6 +19,11 @@ log = logging.getLogger(__name__)
 ns = CompassExtension.namespace
 
 
+def _is_color(value):
+    # currentColor is not a Sass color value, but /is/ a CSS color value
+    return isinstance(value, Color) or value == String('currentColor')
+
+
 def __color_stops(percentages, *args):
     if len(args) == 1:
         if isinstance(args[0], (list, tuple, List)):
@@ -42,7 +47,7 @@ def __color_stops(percentages, *args):
     prev_color = False
     for c in args:
         for c in List.from_maybe(c):
-            if isinstance(c, Color):
+            if _is_color(c):
                 if prev_color:
                     stops.append(None)
                 colors.append(c)
@@ -169,7 +174,7 @@ def _get_gradient_position_and_angle(args):
         ret = None
         skip = False
         for a in arg:
-            if isinstance(a, Color):
+            if _is_color(a):
                 skip = True
                 break
             elif isinstance(a, Number):
@@ -205,7 +210,7 @@ def _get_gradient_color_stops(args):
     color_stops = []
     for arg in args:
         for a in List.from_maybe(arg):
-            if isinstance(a, Color):
+            if _is_color(a):
                 color_stops.append(arg)
                 break
     return color_stops or None
diff --git a/scss/extension/core.py b/scss/extension/core.py
index 6642f1d..5c7e765 100644
--- a/scss/extension/core.py
+++ b/scss/extension/core.py
@@ -34,12 +34,12 @@ class CoreExtension(Extension):
         # import relative to the current file even if the current file isn't
         # anywhere in the search path.  is that right?
         path = PurePosixPath(name)
-        if path.suffix:
-            search_exts = [path.suffix]
-        else:
-            search_exts = compilation.compiler.dynamic_extensions
 
-        basename = path.stem
+        search_exts = list(compilation.compiler.dynamic_extensions)
+        if path.suffix and path.suffix in search_exts:
+            basename = path.stem
+        else:
+            basename = path.name
         relative_to = path.parent
         search_path = []  # tuple of (origin, start_from)
         if relative_to.is_absolute():
@@ -378,6 +378,12 @@ def invert(color):
     """Returns the inverse (negative) of a color.  The red, green, and blue
     values are inverted, while the opacity is left alone.
     """
+    if isinstance(color, Number):
+        # invert(n) and invert(n%) are CSS3 filters and should be left
+        # intact
+        return String.unquoted("invert(%s)" % (color.render(),))
+
+    expect_type(color, Color)
     r, g, b, a = color.rgba
     return Color.from_rgb(1 - r, 1 - g, 1 - b, alpha=a)
 
@@ -576,9 +582,13 @@ def str_index(string, substring):
 def str_slice(string, start_at, end_at=None):
     expect_type(string, String)
     expect_type(start_at, Number, unit=None)
-    py_start_at = start_at.to_python_index(len(string.value))
 
-    if end_at is None:
+    if int(start_at) == 0:
+        py_start_at = 0
+    else:
+        py_start_at = start_at.to_python_index(len(string.value))
+
+    if end_at is None or int(end_at) > len(string.value):
         py_end_at = None
     else:
         expect_type(end_at, Number, unit=None)
@@ -622,26 +632,6 @@ ns.set_function('floor', 1, Number.wrap_python_function(math.floor))
 # ------------------------------------------------------------------------------
 # List functions
 
-def __parse_separator(separator, default_from=None):
-    if separator is None:
-        separator = 'auto'
-    separator = String.unquoted(separator).value
-
-    if separator == 'comma':
-        return True
-    elif separator == 'space':
-        return False
-    elif separator == 'auto':
-        if not default_from:
-            return True
-        elif len(default_from) < 2:
-            return True
-        else:
-            return default_from.use_comma
-    else:
-        raise ValueError('Separator must be auto, comma, or space')
-
-
 # TODO get the compass bit outta here
 @ns.declare_alias('-compass-list-size')
 @ns.declare
@@ -725,12 +715,26 @@ def max_(*lst):
 
 
 @ns.declare
-def append(lst, val, separator=None):
+def append(lst, val, separator=String.unquoted('auto')):
+    expect_type(separator, String)
+
     ret = []
     ret.extend(List.from_maybe(lst))
     ret.append(val)
 
-    use_comma = __parse_separator(separator, default_from=lst)
+    separator = separator.value
+    if separator == 'comma':
+        use_comma = True
+    elif separator == 'space':
+        use_comma = False
+    elif separator == 'auto':
+        if len(lst) < 2:
+            use_comma = False
+        else:
+            use_comma = lst.use_comma
+    else:
+        raise ValueError('Separator must be auto, comma, or space')
+
     return List(ret, use_comma=use_comma)
 
 
@@ -836,8 +840,71 @@ def map_merge_deep(*maps):
     return Map(pairs)
 
 
+ at ns.declare
+def keywords(value):
+    """Extract named arguments, as a map, from an argument list."""
+    expect_type(value, Arglist)
+    return value.extract_keywords()
+
+
 # ------------------------------------------------------------------------------
-# Meta functions
+# Introspection functions
+
+# TODO feature-exists
+
+ at ns.declare_internal
+def variable_exists(namespace, name):
+    expect_type(name, String)
+    try:
+        namespace.variable('$' + name.value)
+    except KeyError:
+        return Boolean(False)
+    else:
+        return Boolean(True)
+
+
+ at ns.declare_internal
+def global_variable_exists(namespace, name):
+    expect_type(name, String)
+
+    # TODO this is...  imperfect and invasive, but should be a good
+    # approximation
+    scope = namespace._variables
+    while len(scope.maps) > 1:
+        scope = scope.maps[-1]
+
+    try:
+        scope['$' + name.value]
+    except KeyError:
+        return Boolean(False)
+    else:
+        return Boolean(True)
+
+
+ at ns.declare_internal
+def function_exists(namespace, name):
+    expect_type(name, String)
+    # TODO invasive, but there's no other way to ask for this at the moment
+    for fname, arity in namespace._functions.keys():
+        if name.value == fname:
+            return Boolean(True)
+    return Boolean(False)
+
+
+ at ns.declare_internal
+def mixin_exists(namespace, name):
+    expect_type(name, String)
+    # TODO invasive, but there's no other way to ask for this at the moment
+    for fname, arity in namespace._mixins.keys():
+        if name.value == fname:
+            return Boolean(True)
+    return Boolean(False)
+
+
+ at ns.declare
+def inspect(value):
+    return String.unquoted(value.render())
+
 
 @ns.declare
 def type_of(obj):  # -> bool, number, string, color, list
@@ -873,16 +940,4 @@ def comparable(number1, number2):
         and left.unit_denom == right.unit_denom)
 
 
- at ns.declare
-def keywords(value):
-    """Extract named arguments, as a map, from an argument list."""
-    expect_type(value, Arglist)
-    return value.extract_keywords()
-
-
-# ------------------------------------------------------------------------------
-# Miscellaneous
-
- at ns.declare
-def if_(condition, if_true, if_false=Null()):
-    return if_true if condition else if_false
+# TODO call
diff --git a/scss/extension/fonts.py b/scss/extension/fonts.py
index b772ceb..bc06e42 100644
--- a/scss/extension/fonts.py
+++ b/scss/extension/fonts.py
@@ -260,7 +260,7 @@ def font_sheet(g, **kwargs):
                         height = float(m.group(1))
                     else:
                         height = None
-                    _glyph = tempfile.NamedTemporaryFile(delete=False, suffix=".svg")
+                    _glyph = tempfile.NamedTemporaryFile(delete=False, suffix=".svg", mode='w')
                     _glyph.file.write(svgtext)
                     _glyph.file.close()
                     yield _glyph.name, width, height
@@ -294,7 +294,7 @@ def font_sheet(g, **kwargs):
                     try:
                         if type_ == 'eot':
                             ttf_path = asset_paths['ttf']
-                            with open(ttf_path) as ttf_fh:
+                            with open(ttf_path, 'rb') as ttf_fh:
                                 contents = ttf2eot(ttf_fh.read())
                                 if contents is not None:
                                     with open(asset_path, 'wb') as asset_fh:
@@ -304,7 +304,7 @@ def font_sheet(g, **kwargs):
                             if type_ == 'ttf':
                                 contents = None
                                 if autohint:
-                                    with open(asset_path) as asset_fh:
+                                    with open(asset_path, 'rb') as asset_fh:
                                         contents = ttfautohint(asset_fh.read())
                                 if contents is not None:
                                     with open(asset_path, 'wb') as asset_fh:
@@ -330,7 +330,7 @@ def font_sheet(g, **kwargs):
                     contents = None
                     if type_ == 'eot':
                         ttf_path = asset_paths['ttf']
-                        with open(ttf_path) as ttf_fh:
+                        with open(ttf_path, 'rb') as ttf_fh:
                             contents = ttf2eot(ttf_fh.read())
                             if contents is None:
                                 continue
@@ -338,7 +338,7 @@ def font_sheet(g, **kwargs):
                         _tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.' + type_)
                         _tmp.file.close()
                         font.generate(_tmp.name)
-                        with open(_tmp.name) as asset_fh:
+                        with open(_tmp.name, 'rb') as asset_fh:
                             if autohint:
                                 if type_ == 'ttf':
                                     _contents = asset_fh.read()
diff --git a/scss/grammar/expression.g b/scss/grammar/expression.g
index fef8959..2ee1e99 100644
--- a/scss/grammar/expression.g
+++ b/scss/grammar/expression.g
@@ -26,6 +26,7 @@ from scss.ast import MapLiteral
 from scss.ast import ArgspecLiteral
 from scss.ast import FunctionLiteral
 from scss.ast import AlphaFunctionLiteral
+from scss.ast import TernaryOp
 from scss.cssdefs import unescape
 from scss.types import Color
 from scss.types import Function
@@ -96,10 +97,11 @@ parser SassExpression:
     token VAR: "\$[-a-zA-Z0-9_]+"
 
     # Cheating, to make sure these only match function names.
-    # The last of these is the IE filter nonsense
-    token LITERAL_FUNCTION: "(calc|expression|progid:[\w.]+)(?=[(])"
+    token LITERAL_FUNCTION: "(-moz-calc|-webkit-calc|calc|expression|progid:[\w.]+)(?=[(])"
     token ALPHA_FUNCTION: "alpha(?=[(])"
+    token OPACITY: "((?i)opacity)"
     token URL_FUNCTION: "url(?=[(])"
+    token IF_FUNCTION: "if(?=[(])"
     # This must come AFTER the above
     token FNCT: "[-a-zA-Z_][-a-zA-Z0-9_]*(?=\()"
 
@@ -244,10 +246,13 @@ parser SassExpression:
         # filter syntax, where it appears as alpha(opacity=NN).  Since = isn't
         # normally valid Sass, we have to special-case it here
         | ALPHA_FUNCTION LPAR (
-            "opacity" "=" atom RPAR
+            OPACITY "=" atom RPAR
                 {{ return AlphaFunctionLiteral(atom) }}
             | argspec RPAR          {{ return CallOp("alpha", argspec) }}
             )
+        # This is a ternary operator, disguised as a function
+        | IF_FUNCTION LPAR expr_lst RPAR
+            {{ return TernaryOp(expr_lst) }}
         | LITERAL_FUNCTION LPAR interpolated_function RPAR
             {{ return Interpolation.maybe(interpolated_function, type=Function, function_name=LITERAL_FUNCTION) }}
         | FNCT LPAR argspec RPAR    {{ return CallOp(FNCT, argspec) }}
diff --git a/scss/grammar/expression.py b/scss/grammar/expression.py
index 1d2a6b5..8b0dd82 100644
--- a/scss/grammar/expression.py
+++ b/scss/grammar/expression.py
@@ -26,6 +26,7 @@ from scss.ast import MapLiteral
 from scss.ast import ArgspecLiteral
 from scss.ast import FunctionLiteral
 from scss.ast import AlphaFunctionLiteral
+from scss.ast import TernaryOp
 from scss.cssdefs import unescape
 from scss.types import Color
 from scss.types import Function
@@ -42,7 +43,6 @@ class SassExpressionScanner(Scanner):
     patterns = None
     _patterns = [
         ('"="', '='),
-        ('"opacity"', 'opacity'),
         ('":"', ':'),
         ('","', ','),
         ('SINGLE_STRING_GUTS', "([^'\\\\#]|[\\\\].|#(?![{]))*"),
@@ -84,9 +84,11 @@ class SassExpressionScanner(Scanner):
         ('KWVAR', '\\$[-a-zA-Z0-9_]+(?=\\s*:)'),
         ('SLURPYVAR', '\\$[-a-zA-Z0-9_]+(?=[.][.][.])'),
         ('VAR', '\\$[-a-zA-Z0-9_]+'),
-        ('LITERAL_FUNCTION', '(calc|expression|progid:[\\w.]+)(?=[(])'),
+        ('LITERAL_FUNCTION', '(-moz-calc|-webkit-calc|calc|expression|progid:[\\w.]+)(?=[(])'),
         ('ALPHA_FUNCTION', 'alpha(?=[(])'),
+        ('OPACITY', '((?i)opacity)'),
         ('URL_FUNCTION', 'url(?=[(])'),
+        ('IF_FUNCTION', 'if(?=[(])'),
         ('FNCT', '[-a-zA-Z_][-a-zA-Z0-9_]*(?=\\()'),
         ('BAREWORD', '(?!\\d)(\\\\[0-9a-fA-F]{1,6}|\\\\.|[-a-zA-Z0-9_])+'),
         ('BANG_IMPORTANT', '!\\s*important'),
@@ -339,8 +341,8 @@ class SassExpression(Parser):
             ALPHA_FUNCTION = self._scan('ALPHA_FUNCTION')
             LPAR = self._scan('LPAR')
             _token_ = self._peek(self.atom_rsts_)
-            if _token_ == '"opacity"':
-                self._scan('"opacity"')
+            if _token_ == 'OPACITY':
+                OPACITY = self._scan('OPACITY')
                 self._scan('"="')
                 atom = self.atom()
                 RPAR = self._scan('RPAR')
@@ -349,6 +351,12 @@ class SassExpression(Parser):
                 argspec = self.argspec()
                 RPAR = self._scan('RPAR')
                 return CallOp("alpha", argspec)
+        elif _token_ == 'IF_FUNCTION':
+            IF_FUNCTION = self._scan('IF_FUNCTION')
+            LPAR = self._scan('LPAR')
+            expr_lst = self.expr_lst()
+            RPAR = self._scan('RPAR')
+            return TernaryOp(expr_lst)
         elif _token_ == 'LITERAL_FUNCTION':
             LITERAL_FUNCTION = self._scan('LITERAL_FUNCTION')
             LPAR = self._scan('LPAR')
@@ -537,9 +545,9 @@ class SassExpression(Parser):
         return Interpolation.maybe(parts)
 
     atom_chks_ = frozenset(['BAREWORD', 'INTERP_START'])
-    expr_map_or_list_rsts__ = frozenset(['LPAR', 'DOUBLE_QUOTE', 'VAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'RPAR', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE', '","'])
-    u_expr_chks = frozenset(['LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'ALPHA_FUNCTION', 'VAR', 'NUM', 'FNCT', 'LITERAL_FUNCTION', 'BANG_IMPORTANT', 'SINGLE_QUOTE'])
-    m_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","'])
+    expr_map_or_list_rsts__ = frozenset(['LPAR', 'RPAR', 'BANG_IMPORTANT', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'DOUBLE_QUOTE', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'IF_FUNCTION', 'SINGLE_QUOTE', '","'])
+    u_expr_chks = frozenset(['LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'VAR', 'NUM', 'FNCT', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'SINGLE_QUOTE'])
+    m_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","'])
     interpolated_bare_url_rsts_ = frozenset(['RPAR', 'INTERP_START', 'BAREURL', 'SPACE'])
     argspec_items_rsts = frozenset(['RPAR', 'END', '","'])
     expr_slst_chks = frozenset(['INTERP_END', 'RPAR', 'END', '":"', '","'])
@@ -547,39 +555,39 @@ class SassExpression(Parser):
     goal_interpolated_literal_rsts = frozenset(['END', 'INTERP_START'])
     expr_map_or_list_rsts = frozenset(['RPAR', '":"', '","'])
     goal_interpolated_literal_with_vars_rsts = frozenset(['VAR', 'END', 'INTERP_START'])
-    argspec_item_chks = frozenset(['LPAR', 'DOUBLE_QUOTE', 'VAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE'])
+    argspec_item_chks = frozenset(['LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'IF_FUNCTION', 'SINGLE_QUOTE'])
     a_expr_chks = frozenset(['ADD', 'SUB'])
     interpolated_function_parens_rsts = frozenset(['LPAR', 'RPAR', 'INTERP_START'])
-    expr_slst_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'VAR', 'END', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'FNCT', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'RPAR', '":"', 'NOT', 'INTERP_END', 'BANG_IMPORTANT', 'SINGLE_QUOTE', '","'])
-    interpolated_bareword_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SPACE', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","'])
-    atom_rsts__ = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'VAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'UNITS', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","'])
-    or_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', '":"', 'BAREWORD', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'OR', 'NOT', 'SINGLE_QUOTE', '","'])
+    expr_slst_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', '":"', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'NOT', 'SINGLE_QUOTE', '","'])
+    interpolated_bareword_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SPACE', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","'])
+    atom_rsts__ = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'VAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'UNITS', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","'])
+    or_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', '":"', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'OR', 'NOT', 'SINGLE_QUOTE', '","'])
     argspec_chks_ = frozenset(['END', 'RPAR'])
     interpolated_string_single_rsts = frozenset(['SINGLE_QUOTE', 'INTERP_START'])
-    interpolated_bareword_rsts_ = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'DIV', 'BANG_IMPORTANT', 'INTERP_END', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'BAREWORD', 'GT', 'END', 'SPACE', 'SIGN', 'LITERAL_FUNCTION', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","'])
-    and_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', '":"', 'BAREWORD', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'AND', 'OR', 'NOT', 'SINGLE_QUOTE', '","'])
-    comparison_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'ADD', 'FNCT', 'VAR', 'EQ', 'AND', 'GE', 'SINGLE_QUOTE', 'NOT', 'OR', '","'])
+    interpolated_bareword_rsts_ = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'DIV', 'BANG_IMPORTANT', 'INTERP_END', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'BAREWORD', 'IF_FUNCTION', 'GT', 'END', 'SPACE', 'SIGN', 'LITERAL_FUNCTION', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","'])
+    and_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', '":"', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'AND', 'SINGLE_QUOTE', 'NOT', 'OR', '","'])
+    comparison_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'ADD', 'FNCT', 'VAR', 'EQ', 'AND', 'GE', 'SINGLE_QUOTE', 'NOT', 'OR', '","'])
     argspec_chks = frozenset(['DOTDOTDOT', 'SLURPYVAR'])
-    atom_rsts_ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'SLURPYVAR', 'ALPHA_FUNCTION', 'RPAR', 'BANG_IMPORTANT', '"opacity"', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', 'BAREWORD', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'DOTDOTDOT', 'NOT', 'SINGLE_QUOTE'])
+    atom_rsts_ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'SLURPYVAR', 'ALPHA_FUNCTION', 'RPAR', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'OPACITY', 'DOTDOTDOT', 'NOT', 'SINGLE_QUOTE'])
     interpolated_string_double_rsts = frozenset(['DOUBLE_QUOTE', 'INTERP_START'])
     atom_chks__ = frozenset(['COLOR', 'VAR'])
     expr_map_or_list_rsts_ = frozenset(['RPAR', '","'])
-    u_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'ALPHA_FUNCTION', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'LITERAL_FUNCTION', 'BANG_IMPORTANT', 'SINGLE_QUOTE'])
+    u_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'SINGLE_QUOTE'])
     interpolated_url_chks = frozenset(['INTERP_START_URL_HACK', 'BAREURL_HEAD_HACK'])
-    atom_chks = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'END', 'SLURPYVAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'DOTDOTDOT', 'INTERP_START', 'RPAR', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'SIGN', 'SINGLE_QUOTE'])
-    interpolated_url_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'VAR', 'SINGLE_QUOTE', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'FNCT', 'NOT', 'INTERP_START_URL_HACK', 'BANG_IMPORTANT', 'BAREURL_HEAD_HACK'])
+    atom_chks = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'SLURPYVAR', 'ALPHA_FUNCTION', 'RPAR', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'DOTDOTDOT', 'NOT', 'SINGLE_QUOTE'])
+    interpolated_url_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'SINGLE_QUOTE', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'INTERP_START_URL_HACK', 'IF_FUNCTION', 'BAREURL_HEAD_HACK'])
     comparison_chks = frozenset(['GT', 'GE', 'NE', 'LT', 'LE', 'EQ'])
-    argspec_items_rsts_ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'VAR', 'END', 'SLURPYVAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'DOTDOTDOT', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'RPAR', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE'])
-    a_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","'])
+    argspec_items_rsts_ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'SLURPYVAR', 'ALPHA_FUNCTION', 'RPAR', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'DOTDOTDOT', 'NOT', 'SINGLE_QUOTE'])
+    a_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","'])
     interpolated_string_rsts = frozenset(['DOUBLE_QUOTE', 'SINGLE_QUOTE'])
-    interpolated_bareword_rsts__ = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","'])
+    interpolated_bareword_rsts__ = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","'])
     m_expr_chks = frozenset(['MUL', 'DIV', 'MOD'])
     goal_interpolated_literal_with_vars_rsts_ = frozenset(['VAR', 'INTERP_START'])
     interpolated_bare_url_rsts = frozenset(['RPAR', 'INTERP_START'])
-    argspec_items_chks = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'VAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE'])
-    argspec_rsts = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'END', 'SLURPYVAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'DOTDOTDOT', 'INTERP_START', 'RPAR', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'SIGN', 'SINGLE_QUOTE'])
-    atom_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'RPAR', 'SINGLE_QUOTE'])
-    argspec_items_rsts__ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'VAR', 'SLURPYVAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'DOTDOTDOT', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE'])
-    argspec_rsts_ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'END', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'RPAR', 'SINGLE_QUOTE'])
+    argspec_items_chks = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'IF_FUNCTION', 'SINGLE_QUOTE'])
+    argspec_rsts = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'SLURPYVAR', 'ALPHA_FUNCTION', 'RPAR', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'DOTDOTDOT', 'NOT', 'SINGLE_QUOTE'])
+    atom_rsts = frozenset(['BANG_IMPORTANT', 'LPAR', 'DOUBLE_QUOTE', 'IF_FUNCTION', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'RPAR', 'SINGLE_QUOTE'])
+    argspec_items_rsts__ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'SLURPYVAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'DOTDOTDOT', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'IF_FUNCTION', 'SINGLE_QUOTE'])
+    argspec_rsts_ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'IF_FUNCTION', 'END', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'BANG_IMPORTANT', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'RPAR', 'SINGLE_QUOTE'])
 
 
diff --git a/scss/legacy.py b/scss/legacy.py
index b155330..ec4fd72 100644
--- a/scss/legacy.py
+++ b/scss/legacy.py
@@ -3,7 +3,9 @@ from __future__ import print_function
 from __future__ import unicode_literals
 from __future__ import division
 
+import os
 from pathlib import Path
+from collections import namedtuple
 
 import six
 
@@ -43,6 +45,8 @@ _default_scss_vars = {
     '$--curlybracketclosed': String.unquoted('}'),
 }
 
+SourceFileTuple = namedtuple('SourceFileTuple', ('parent_dir', 'filename'))
+
 
 # TODO using this should spew an actual deprecation warning
 class Scss(object):
@@ -182,7 +186,9 @@ class Scss(object):
                 source = SourceFile.from_string(contents, relpath=name)
                 compilation.add_source(source)
 
-        return compiler.call_and_catch_errors(compilation.run)
+        compiled = compiler.call_and_catch_errors(compilation.run)
+        self.source_files = list(SourceFileTuple(*os.path.split(s.path)) for s in compilation.source_index.values())
+        return compiled
 
     # Old, old alias
     Compilation = compile
diff --git a/scss/less2scss.py b/scss/less2scss.py
new file mode 100644
index 0000000..b352348
--- /dev/null
+++ b/scss/less2scss.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+"""
+Tool for converting Less files to Scss
+
+Usage: python -m scss.less2scss [file]
+
+"""
+# http://stackoverflow.com/questions/14970224/anyone-know-of-a-good-way-to-convert-from-less-to-sass
+from __future__ import unicode_literals, absolute_import, print_function
+
+import re
+import os
+import sys
+
+
+class Less2Scss(object):
+    at_re = re.compile(r'@(?!(media|import|mixin|font-face)(\s|\())')
+    mixin_re = re.compile(r'\.([\w\-]*)\s*\((.*)\)\s*\{')
+    include_re = re.compile(r'(\s|^)\.([\w\-]*\(?.*\)?;)')
+    functions_map = {
+        'spin': 'adjust-hue',
+    }
+    functions_re = re.compile(r'(%s)\(' % '|'.join(functions_map))
+
+    def convert(self, content):
+        content = self.convertVariables(content)
+        content = self.convertMixins(content)
+        content = self.includeMixins(content)
+        content = self.convertFunctions(content)
+        return content
+
+    def convertVariables(self, content):
+        # Matches any @ that doesn't have 'media ' or 'import ' after it.
+        content = self.at_re.sub('$', content)
+        return content
+
+    def convertMixins(self, content):
+        content = self.mixin_re.sub('@mixin \1(\2) {', content)
+        return content
+
+    def includeMixins(self, content):
+        content = self.mixin_re.sub('\1 at include \2', content)
+        return content
+
+    def convertFunctions(self, content):
+        content = self.functions_re.sub(lambda m: '%s(' % self.functions_map[m.group(0)], content)
+        return content
+
+
+def less2scss(options, args):
+    if not args:
+        args = ['-']
+
+    less2scss = Less2Scss()
+
+    for source_path in args:
+        if source_path == '-':
+            source = sys.stdin
+            destiny = sys.stdout
+        else:
+            try:
+                source = open(source_path)
+                destiny_path, ext = os.path.splitext(source_path)
+                destiny_path += '.scss'
+                if not options.force and os.path.exists(destiny_path):
+                    raise IOError("File already exists: %s" % destiny_path)
+                destiny = open(destiny_path, 'w')
+            except Exception as e:
+                error = "%s" % e
+                if destiny_path in error:
+                    ignoring = "Ignoring"
+                else:
+                    ignoring = "Ignoring %s" % destiny_path
+                print("WARNING -- %s. %s" % (ignoring, error), file=sys.stderr)
+                continue
+        content = source.read()
+        content = less2scss.convert(content)
+        destiny.write(content)
+
+
+def main():
+    from optparse import OptionParser, SUPPRESS_HELP
+
+    parser = OptionParser(usage="Usage: %prog [file]",
+                          description="Converts Less files to Scss.",
+                          add_help_option=False)
+    parser.add_option("-f", "--force", action="store_true",
+                      dest="force", default=False,
+                      help="Forces overwriting output file if it already exists")
+    parser.add_option("-?", action="help", help=SUPPRESS_HELP)
+    parser.add_option("-h", "--help", action="help",
+                      help="Show this message and exit")
+    parser.add_option("-v", "--version", action="store_true",
+                      help="Print version and exit")
+
+    options, args = parser.parse_args()
+
+    if options.version:
+        from scss.tool import print_version
+        print_version()
+    else:
+        less2scss(options, args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scss/namespace.py b/scss/namespace.py
index 8076bca..939cde5 100644
--- a/scss/namespace.py
+++ b/scss/namespace.py
@@ -157,7 +157,17 @@ class Namespace(object):
 
         return decorator
 
-    def _auto_register_function(self, function, name):
+    def declare_internal(self, function):
+        """Like declare(), but the registered function will also receive the
+        current namespace as its first argument.  Useful for functions that
+        inspect the state of the compilation, like ``variable-exists()``.
+        Probably not so useful for anything else.
+        """
+        function._pyscss_needs_namespace = True
+        self._auto_register_function(function, function.__name__, 1)
+        return function
+
+    def _auto_register_function(self, function, name, ignore_args=0):
         name = name.replace('_', '-').rstrip('-')
         argspec = inspect.getargspec(function)
 
@@ -170,7 +180,7 @@ class Namespace(object):
                 num_optional = len(argspec.defaults)
             else:
                 num_optional = 0
-            num_args = len(argspec.args)
+            num_args = len(argspec.args) - ignore_args
             arities = range(num_args - num_optional, num_args + 1)
 
         for arity in arities:
diff --git a/scss/scss_meta.py b/scss/scss_meta.py
index 7f17b9a..b1a9759 100644
--- a/scss/scss_meta.py
+++ b/scss/scss_meta.py
@@ -46,8 +46,8 @@ from __future__ import unicode_literals
 
 import sys
 
-VERSION_INFO = (1, 3, 4)
-DATE_INFO = (2014, 12, 15)  # YEAR, MONTH, DAY
+VERSION_INFO = (1, 3, 5)
+DATE_INFO = (2016, 6, 8)  # YEAR, MONTH, DAY
 VERSION = '.'.join(str(i) for i in VERSION_INFO)
 REVISION = '%04d%02d%02d' % DATE_INFO
 BUILD_INFO = "pyScss v" + VERSION + " (" + REVISION + ")"
diff --git a/scss/selector.py b/scss/selector.py
index e1f8994..8a6ecdf 100644
--- a/scss/selector.py
+++ b/scss/selector.py
@@ -91,6 +91,15 @@ class SimpleSelector(object):
 
     For lack of a better name, each of the individual parts is merely called a
     "token".
+
+    Note that it's possible to have zero tokens.  This isn't legal CSS, but
+    it's perfectly legal Sass, since you might nest blocks like so:
+
+        body > {
+            div {
+                ...
+            }
+        }
     """
     def __init__(self, combinator, tokens):
         self.combinator = combinator
@@ -250,10 +259,12 @@ class SimpleSelector(object):
     def render(self):
         # TODO fail if there are no tokens, or if one is a placeholder?
         rendered = ''.join(self.tokens)
-        if self.combinator != ' ':
-            rendered = ' '.join((self.combinator, rendered))
-
-        return rendered
+        if self.combinator == ' ':
+            return rendered
+        elif rendered:
+            return self.combinator + ' ' + rendered
+        else:
+            return self.combinator
 
 
 class Selector(object):
@@ -287,6 +298,10 @@ class Selector(object):
 
         def promote_selector():
             promote_simple()
+            if pending['combinator'] != ' ':
+                pending['simples'].append(
+                    SimpleSelector(pending['combinator'], []))
+                pending['combinator'] = ' '
             if pending['simples']:
                 ret.append(cls(pending['simples']))
             pending['simples'] = []
@@ -310,7 +325,6 @@ class Selector(object):
 
             if token == ',':
                 # End current selector
-                # TODO what about "+ ,"?  what do i even do with that
                 promote_selector()
             elif token in ' +>~':
                 # End current simple selector
diff --git a/scss/source.py b/scss/source.py
index 462278d..bb1f1ec 100644
--- a/scss/source.py
+++ b/scss/source.py
@@ -13,7 +13,7 @@ import six
 from scss.cssdefs import (
     _ml_comment_re, _sl_comment_re,
     _collapse_properties_space_re,
-    _strings_re,
+    _strings_re, _urls_re,
 )
 from scss.cssdefs import determine_encoding
 
@@ -356,6 +356,10 @@ class SourceFile(object):
             lambda m: _reverse_safe_strings_re.sub(
                 lambda n: _reverse_safe_strings[n.group(0)], m.group(0)),
             codestr)
+        codestr = _urls_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)
diff --git a/scss/tests/extension/test_core.py b/scss/tests/extension/test_core.py
index 8c26aef..4e83e99 100644
--- a/scss/tests/extension/test_core.py
+++ b/scss/tests/extension/test_core.py
@@ -152,6 +152,11 @@ def test_invert():
     assert calc('invert(yellow)') == Color.from_rgb(0., 0., 1.)
 
 
+def test_invert_css_filter():
+    # invert(number) is a CSS filter and should be left alone
+    assert calc('invert(50%)') == String("invert(50%)")
+
+
 # ------------------------------------------------------------------------------
 # Opacity functions
 
@@ -277,6 +282,9 @@ def test_str_slice():
     assert calc('str-slice("abcd", 2)') == calc('"bcd"')
     assert calc('str-slice("abcd", -3, -2)') == calc('"bc"')
     assert calc('str-slice("abcd", 2, -2)') == calc('"bc"')
+    assert calc('str-slice("abcd", 0, 3)') == calc('"abc"')
+    assert calc('str-slice("abcd", 1, 3)') == calc('"abc"')
+    assert calc('str-slice("abcd", 1, 30)') == calc('"abcd"')
 
 
 def test_to_upper_case():
diff --git a/scss/tests/files/bugs/append.css b/scss/tests/files/bugs/append.css
new file mode 100644
index 0000000..c5e87fa
--- /dev/null
+++ b/scss/tests/files/bugs/append.css
@@ -0,0 +1,5 @@
+p {
+  a: 1;
+  b: 1 2;
+  c: 1, 2, 3;
+}
diff --git a/scss/tests/files/bugs/append.scss b/scss/tests/files/bugs/append.scss
new file mode 100644
index 0000000..2b6aff9
--- /dev/null
+++ b/scss/tests/files/bugs/append.scss
@@ -0,0 +1,10 @@
+// Default when the first list has < 2 elements is space
+$a: append((), 1);
+$b: append(append((), 1, comma), 2);
+$c: append((1, 2), 3);
+
+p {
+  a: $a;
+  b: $b;
+  c: $c;
+}
diff --git a/scss/tests/files/bugs/base64url.css b/scss/tests/files/bugs/base64url.css
new file mode 100644
index 0000000..846635e
--- /dev/null
+++ b/scss/tests/files/bugs/base64url.css
@@ -0,0 +1,3 @@
+.logo {
+  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAP0lEQVQIHWWMAQoAIAgDR/QJ/Ub//04+w7ZICBwcOg5FZi5iBB82AGzixEglJrd4TVK5XUJpskSTEvpdFzX9AB2pGziSQcvAAAAAAElFTkSuQmCC);
+}
diff --git a/scss/tests/files/bugs/base64url.scss b/scss/tests/files/bugs/base64url.scss
new file mode 100644
index 0000000..38c73a6
--- /dev/null
+++ b/scss/tests/files/bugs/base64url.scss
@@ -0,0 +1,5 @@
+// Two slashes inside a URL were being stripped as a comment, leaving bogus SCSS
+// https://github.com/Kronuz/pyScss/issues/350
+.logo {
+  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAP0lEQVQIHWWMAQoAIAgDR/QJ/Ub//04+w7ZICBwcOg5FZi5iBB82AGzixEglJrd4TVK5XUJpskSTEvpdFzX9AB2pGziSQcvAAAAAAElFTkSuQmCC);
+}
diff --git a/scss/tests/files/bugs/trailing-combinators.css b/scss/tests/files/bugs/trailing-combinators.css
new file mode 100644
index 0000000..ee1ddda
--- /dev/null
+++ b/scss/tests/files/bugs/trailing-combinators.css
@@ -0,0 +1,11 @@
+div ~ p, div ~ blockquote {
+  background-color: blue;
+}
+
+div > p, div > blockquote {
+  background-color: blue;
+}
+
+div + p, div + blockquote {
+  background-color: blue;
+}
diff --git a/scss/tests/files/bugs/trailing-combinators.scss b/scss/tests/files/bugs/trailing-combinators.scss
new file mode 100644
index 0000000..1e0b0d9
--- /dev/null
+++ b/scss/tests/files/bugs/trailing-combinators.scss
@@ -0,0 +1,18 @@
+div ~ {
+  p,
+  blockquote {
+    background-color: blue;
+  }
+}
+div > {
+  p,
+  blockquote {
+    background-color: blue;
+  }
+}
+div + {
+  p,
+  blockquote {
+    background-color: blue;
+  }
+}
diff --git a/scss/tests/files/bugs/unit-float-precision.css b/scss/tests/files/bugs/unit-float-precision.css
new file mode 100644
index 0000000..16e9d86
--- /dev/null
+++ b/scss/tests/files/bugs/unit-float-precision.css
@@ -0,0 +1,3 @@
+a {
+  width: 702px;
+}
diff --git a/scss/tests/files/bugs/unit-float-precision.scss b/scss/tests/files/bugs/unit-float-precision.scss
new file mode 100644
index 0000000..a57b294
--- /dev/null
+++ b/scss/tests/files/bugs/unit-float-precision.scss
@@ -0,0 +1,7 @@
+// A number of pixels is actually stored internally as millimeters, which
+// causes some slight precision issues in corner cases.  In this case, the
+// answer came out as 701px rather than 702px, because the subtraction yielded
+// 701.99999999px.
+a {
+    width: floor(742px - 40px);
+}
diff --git a/scss/tests/files/compass/current-color.css b/scss/tests/files/compass/current-color.css
new file mode 100644
index 0000000..302ddc9
--- /dev/null
+++ b/scss/tests/files/compass/current-color.css
@@ -0,0 +1,3 @@
+p {
+  background-image: linear-gradient(to top, transparent, currentColor);
+}
diff --git a/scss/tests/files/compass/current-color.scss b/scss/tests/files/compass/current-color.scss
new file mode 100644
index 0000000..9922299
--- /dev/null
+++ b/scss/tests/files/compass/current-color.scss
@@ -0,0 +1,4 @@
+p {
+  // currentColor is not actually a SCSS color value, but should be left alone
+  background-image: linear-gradient(to top, transparent, currentColor);
+}
diff --git a/scss/tests/files/fonts/fonts.css b/scss/tests/files/fonts/fonts.css
index b6d3215..77ce3a9 100644
--- a/scss/tests/files/fonts/fonts.css
+++ b/scss/tests/files/fonts/fonts.css
@@ -1,10 +1,10 @@
 @font-face {
   font-family: 'FontSheet';
   src: url(static/assets/fontsheet-g7MrCA_HKKgVf2zD68L54g.eot);
-  src: url(static/assets/fontsheet-g7MrCA_HKKgVf2zD68L54g.svg?#iefix&#fontsheet) format('svg'),
+  src: url(static/assets/fontsheet-g7MrCA_HKKgVf2zD68L54g.eot) format('embedded-opentype'),
     url(static/assets/fontsheet-g7MrCA_HKKgVf2zD68L54g.woff) format('woff'),
     url(static/assets/fontsheet-g7MrCA_HKKgVf2zD68L54g.ttf) format('truetype'),
-    url(static/assets/fontsheet-g7MrCA_HKKgVf2zD68L54g.eot) format('embedded-opentype');
+    url(static/assets/fontsheet-g7MrCA_HKKgVf2zD68L54g.svg?#iefix&#fontsheet) format('svg');
   font-weight: normal;
   font-style: normal;
 }
diff --git a/scss/tests/files/general/css-calc.css b/scss/tests/files/general/css-calc.css
index 6d13266..5f8cfac 100644
--- a/scss/tests/files/general/css-calc.css
+++ b/scss/tests/files/general/css-calc.css
@@ -1,3 +1,5 @@
 p {
+  width: -moz-calc(100% - 10px);
+  width: -webkit-calc(100% - 10px);
   width: calc(100% - 10px);
 }
diff --git a/scss/tests/files/general/css-calc.scss b/scss/tests/files/general/css-calc.scss
index aba10f7..7f2d55d 100644
--- a/scss/tests/files/general/css-calc.scss
+++ b/scss/tests/files/general/css-calc.scss
@@ -1,3 +1,5 @@
 p {
+    width: -moz-calc(100% - 10px);
+    width: -webkit-calc(100% - 10px);
     width: calc(100% - 10px);
 }
diff --git a/scss/tests/files/general/global-variable-exists.css b/scss/tests/files/general/global-variable-exists.css
new file mode 100644
index 0000000..a614018
--- /dev/null
+++ b/scss/tests/files/general/global-variable-exists.css
@@ -0,0 +1,5 @@
+blockquote {
+  background: lime;
+  color: red;
+  float: right;
+}
diff --git a/scss/tests/files/general/global-variable-exists.scss b/scss/tests/files/general/global-variable-exists.scss
new file mode 100644
index 0000000..4404ed0
--- /dev/null
+++ b/scss/tests/files/general/global-variable-exists.scss
@@ -0,0 +1,27 @@
+ at mixin myblockquote {
+  blockquote {
+    @if global-variable-exists(blockquote-color) {
+      background: $blockquote-color;
+    }
+    @else {
+      background: lime;
+    }
+
+    @if mixin-exists(myblockquote) {
+      color: red;
+    }
+    @else {
+      color: blue;
+    }
+
+    @if variable-exists(foo) {
+      float: left;
+    }
+    $foo: 1;
+    @if variable-exists(foo) {
+      float: right;
+    }
+  }
+}
+
+ at include myblockquote;
diff --git a/scss/tests/files/general/if-lazy.css b/scss/tests/files/general/if-lazy.css
new file mode 100644
index 0000000..fb97f11
--- /dev/null
+++ b/scss/tests/files/general/if-lazy.css
@@ -0,0 +1,4 @@
+p {
+  color: red;
+  color: blue;
+}
diff --git a/scss/tests/files/general/if-lazy.scss b/scss/tests/files/general/if-lazy.scss
new file mode 100644
index 0000000..87ee449
--- /dev/null
+++ b/scss/tests/files/general/if-lazy.scss
@@ -0,0 +1,6 @@
+p {
+    // if()'s arguments are evaluated lazily.  $bogus doesn't exist, so if
+    // those expressions are ever evaluated, pyscss will die with an error
+    color: if(false, $bogus, red);
+    color: if(true, blue, $bogus);
+}
diff --git a/scss/tests/files/regressions/include/_issue-334.scss b/scss/tests/files/regressions/include/_issue-334.scss
new file mode 100644
index 0000000..fb1123c
--- /dev/null
+++ b/scss/tests/files/regressions/include/_issue-334.scss
@@ -0,0 +1,3 @@
+a {
+    color: white;
+}
\ No newline at end of file
diff --git a/scss/tests/files/regressions/include/_issue.334.scss b/scss/tests/files/regressions/include/_issue.334.scss
new file mode 100644
index 0000000..fb1123c
--- /dev/null
+++ b/scss/tests/files/regressions/include/_issue.334.scss
@@ -0,0 +1,3 @@
+a {
+    color: white;
+}
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-258.css b/scss/tests/files/regressions/issue-258.css
index 559cc05..27feb03 100644
--- a/scss/tests/files/regressions/issue-258.css
+++ b/scss/tests/files/regressions/issue-258.css
@@ -3,6 +3,7 @@
     any-property: any-value;
   }
 }
+
 #B {
   my-property: this-should-be-before-media-query;
 }
diff --git a/scss/tests/files/regressions/issue-334-a.css b/scss/tests/files/regressions/issue-334-a.css
new file mode 100644
index 0000000..763d165
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-a.css
@@ -0,0 +1,3 @@
+a {
+  color: white;
+}
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-a.scss b/scss/tests/files/regressions/issue-334-a.scss
new file mode 100644
index 0000000..f05dc54
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-a.scss
@@ -0,0 +1 @@
+ at import "issue-334";
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-b.css b/scss/tests/files/regressions/issue-334-b.css
new file mode 100644
index 0000000..763d165
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-b.css
@@ -0,0 +1,3 @@
+a {
+  color: white;
+}
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-b.scss b/scss/tests/files/regressions/issue-334-b.scss
new file mode 100644
index 0000000..12e9298
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-b.scss
@@ -0,0 +1 @@
+ at import "issue.334";
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-c.css b/scss/tests/files/regressions/issue-334-c.css
new file mode 100644
index 0000000..763d165
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-c.css
@@ -0,0 +1,3 @@
+a {
+  color: white;
+}
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-c.scss b/scss/tests/files/regressions/issue-334-c.scss
new file mode 100644
index 0000000..28e51b3
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-c.scss
@@ -0,0 +1 @@
+ at import "_issue-334";
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-d.css b/scss/tests/files/regressions/issue-334-d.css
new file mode 100644
index 0000000..763d165
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-d.css
@@ -0,0 +1,3 @@
+a {
+  color: white;
+}
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-d.scss b/scss/tests/files/regressions/issue-334-d.scss
new file mode 100644
index 0000000..f44c269
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-d.scss
@@ -0,0 +1 @@
+ at import "_issue.334";
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-e.css b/scss/tests/files/regressions/issue-334-e.css
new file mode 100644
index 0000000..763d165
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-e.css
@@ -0,0 +1,3 @@
+a {
+  color: white;
+}
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-e.scss b/scss/tests/files/regressions/issue-334-e.scss
new file mode 100644
index 0000000..e7bb33e
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-e.scss
@@ -0,0 +1 @@
+ at import "issue.334.scss";
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-f.css b/scss/tests/files/regressions/issue-334-f.css
new file mode 100644
index 0000000..763d165
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-f.css
@@ -0,0 +1,3 @@
+a {
+  color: white;
+}
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-f.scss b/scss/tests/files/regressions/issue-334-f.scss
new file mode 100644
index 0000000..3a3a476
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-f.scss
@@ -0,0 +1 @@
+ at import "issue-334.scss";
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-g.css b/scss/tests/files/regressions/issue-334-g.css
new file mode 100644
index 0000000..763d165
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-g.css
@@ -0,0 +1,3 @@
+a {
+  color: white;
+}
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-g.scss b/scss/tests/files/regressions/issue-334-g.scss
new file mode 100644
index 0000000..1f677b7
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-g.scss
@@ -0,0 +1 @@
+ at import "include/issue-334";
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-h.css b/scss/tests/files/regressions/issue-334-h.css
new file mode 100644
index 0000000..763d165
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-h.css
@@ -0,0 +1,3 @@
+a {
+  color: white;
+}
\ No newline at end of file
diff --git a/scss/tests/files/regressions/issue-334-h.scss b/scss/tests/files/regressions/issue-334-h.scss
new file mode 100644
index 0000000..1f677b7
--- /dev/null
+++ b/scss/tests/files/regressions/issue-334-h.scss
@@ -0,0 +1 @@
+ at import "include/issue-334";
\ No newline at end of file
diff --git a/scss/tool.py b/scss/tool.py
index 08abb44..f9b83b4 100644
--- a/scss/tool.py
+++ b/scss/tool.py
@@ -250,8 +250,8 @@ def watch_sources(options):
                 dest_path = os.path.join(os.path.dirname(src_path), fname)
 
             print("Compiling %s => %s" % (src_path, dest_path))
-            dest_file = open(dest_path, 'w')
-            dest_file.write(self.css.compile(scss_file=src_path))
+            dest_file = open(dest_path, 'wb')
+            dest_file.write(self.css.compile(scss_file=src_path).encode('utf-8'))
 
         def on_moved(self, event):
             super(ScssEventHandler, self).on_moved(event)
@@ -348,6 +348,7 @@ class SassRepl(object):
                     self.compilation._at_options(self.calculator, rule, scope, block)
                     continue
                 elif code == '@import':
+                    # TODO this doesn't really work either since there's no path
                     self.compilation._at_import(self.calculator, rule, scope, block)
                     continue
                 elif code == '@include':
@@ -357,6 +358,7 @@ class SassRepl(object):
                     if code:
                         final_cont += code
                     if children:
+                        # TODO this almost certainly doesn't work, and is kind of goofy anyway since @mixin isn't supported
                         self.compilation.children.extendleft(children)
                         self.compilation.parse_children()
                         code = self.compilation._create_css(self.compilation.rules).rstrip('\n')
diff --git a/scss/types.py b/scss/types.py
index b844c92..c35905a 100644
--- a/scss/types.py
+++ b/scss/types.py
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
 
 from collections import Iterable
 import colorsys
+from fractions import Fraction
 import operator
 import re
 import string
@@ -255,7 +256,21 @@ class Number(Value):
             self.unit_denom = amount.unit_denom
             return
 
-        if not isinstance(amount, (int, float)):
+        # Numbers with units are stored internally as a "base" unit, which can
+        # involve float division, which can lead to precision errors in obscure
+        # cases.  Storing the original units would only partially solve this
+        # problem, because there'd still be a possible loss of precision when
+        # converting in Sass-land.  Almost all of the conversion factors are
+        # simple ratios of small whole numbers, so using Fraction across the
+        # board preserves as much precision as possible.
+        # TODO in fact, i wouldn't mind parsing Sass values as fractions of a
+        # power of ten!
+        # TODO this slowed the test suite down by about 10%, ha
+        if isinstance(amount, (int, float)):
+            amount = Fraction.from_float(amount)
+        elif isinstance(amount, Fraction):
+            pass
+        else:
             raise TypeError("Expected number, got %r" % (amount,))
 
         if unit is not None:
@@ -1023,7 +1038,7 @@ class Color(Value):
                 sp = ''
             else:
                 sp = ' '
-            candidates.append("rgba(%d,%s%d,%s%d,%s%.2g)" % (r, sp, g, sp, b, sp, a))
+            candidates.append("rgba(%d,%s%d,%s%d,%s%.6g)" % (r, sp, g, sp, b, sp, a))
 
         if compress:
             return min(candidates, key=len)
diff --git a/setup.py b/setup.py
index 3615565..9daf6a0 100644
--- a/setup.py
+++ b/setup.py
@@ -120,6 +120,7 @@ def run_setup(with_binary):
         entry_points="""
         [console_scripts]
         pyscss = scss.tool:main
+        less2scss = scss.less2scss:main
         """,
     )
 
diff --git a/tox.ini b/tox.ini
index ca9ec35..a19d5b6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,9 +9,7 @@ deps =
     pillow
     six
     pytest
-setenv =
-    PYTHONPATH = {toxinidir}
-commands = {toxworkdir}/{envname}/Scripts/py.test []
+commands = py.test {posargs:scss/tests}
 
 
 [testenv:py26]

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-pyscss.git



More information about the Python-modules-commits mailing list