Exemple #1
0
    def __init__(
            self, namespace=None,
            ignore_parse_errors=False,
            undefined_variables_fatal=True,
            ):
        if namespace is None:
            self.namespace = Namespace()
        else:
            self.namespace = namespace

        self.ignore_parse_errors = ignore_parse_errors
        self.undefined_variables_fatal = undefined_variables_fatal
Exemple #2
0
def test_functions(calc):
    ns = Namespace(functions=CORE_LIBRARY)
    calc = Calculator(ns).calculate

    assert calc('grayscale(red)') == Color.from_rgb(0.5, 0.5, 0.5)
    assert calc('grayscale(1)') == String(
        'grayscale(1)',
        quotes=None)  # Misusing css built-in functions (with scss counterpart)
    assert calc('skew(1)') == String(
        'skew(1)', quotes=None)  # Missing css-only built-in functions
    with pytest.raises(SassEvaluationError):
        calc('unitless("X")')  # Misusing non-css built-in scss funtions
Exemple #3
0
def test_reference_operations():
    """Test the example expressions in the reference document:

    http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#operations
    """
    # TODO: break this into its own file and add the entire reference guide

    # Need to build the calculator manually to get at its namespace, and need
    # to use calculate() instead of evaluate_expression() so interpolation
    # works
    ns = Namespace(functions=CORE_LIBRARY)
    calc = Calculator(ns).calculate

    # Simple example
    assert calc('1in + 8pt') == Number(1.11111111, "in")

    # Division
    ns.set_variable('$width', Number(1000, "px"))
    ns.set_variable('$font-size', Number(12, "px"))
    ns.set_variable('$line-height', Number(30, "px"))
    assert calc('10px/8px') == String('10px / 8px')   # plain CSS; no division
    assert calc('$width/2') == Number(500, "px")      # uses a variable; does division
    assert calc('(500px/2)') == Number(250, "px")     # uses parens; does division
    assert calc('5px + 8px/2px') == Number(9, "px")   # uses +; does division
    # TODO: Ruby doesn't include these spaces
    assert calc('#{$font-size}/#{$line-height}') == String('12px / 30px')
                                            # uses #{}; does no division

    # Color operations
    ns.set_variable('$translucent-red', Color.from_rgb(1, 0, 0, 0.5))
    ns.set_variable('$green', Color.from_name('lime'))
    assert calc('#010203 + #040506') == Color.from_hex('#050709')
    assert calc('#010203 * 2') == Color.from_hex('#020406')
    assert calc('rgba(255, 0, 0, 0.75) + rgba(0, 255, 0, 0.75)') == Color.from_rgb(1, 1, 0, 0.75)
    assert calc('opacify($translucent-red, 0.3)') == Color.from_rgb(1, 0, 0, 0.8)
    assert calc('transparentize($translucent-red, 0.25)') == Color.from_rgb(1, 0, 0, 0.25)
    assert calc("progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr='#{ie-hex-str($green)}', endColorstr='#{ie-hex-str($translucent-red)}')"
                ) == String.unquoted("progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr='#FF00FF00', endColorstr='#80FF0000')")

    # String operations
    ns.set_variable('$value', Null())
    assert_strict_string_eq(calc('e + -resize'), String('e-resize', quotes=None))
    assert_strict_string_eq(calc('"Foo " + Bar'), String('Foo Bar', quotes='"'))
    assert_strict_string_eq(calc('sans- + "serif"'), String('sans-serif', quotes=None))
    assert calc('3px + 4px auto') == List([Number(7, "px"), String('auto', quotes=None)])
    assert_strict_string_eq(calc('"I ate #{5 + 10} pies!"'), String('I ate 15 pies!', quotes='"'))
    assert_strict_string_eq(calc('"I ate #{$value} pies!"'), String('I ate  pies!', quotes='"'))
Exemple #4
0
 def __init__(self, namespace=None):
     if namespace is None:
         self.namespace = Namespace()
     else:
         self.namespace = namespace
Exemple #5
0
class Calculator(object):
    """Expression evaluator."""

    ast_cache = {}

    def __init__(self, namespace=None):
        if namespace is None:
            self.namespace = Namespace()
        else:
            self.namespace = namespace

    def _pound_substitute(self, result):
        expr = result.group(1)
        value = self.evaluate_expression(expr)

        if value is None:
            return self.apply_vars(expr)
        elif value.is_null:
            return ""
        else:
            return dequote(value.render())

    def do_glob_math(self, cont):
        """Performs #{}-interpolation.  The result is always treated as a fixed
        syntactic unit and will not be re-evaluated.
        """
        # TODO that's a lie!  this should be in the parser for most cases.
        if not isinstance(cont, six.string_types):
            warn(FutureWarning(
                "do_glob_math was passed a non-string {0!r} "
                "-- this will no longer be supported in pyScss 2.0"
                .format(cont)
            ))
            cont = six.text_type(cont)
        if '#{' not in cont:
            return cont
        cont = _expr_glob_re.sub(self._pound_substitute, cont)
        return cont

    def apply_vars(self, cont):
        # TODO this is very complicated.  it should go away once everything
        # valid is actually parseable.
        if isinstance(cont, six.string_types) and '$' in cont:
            try:
                # Optimization: the full cont is a variable in the context,
                cont = self.namespace.variable(cont)
            except KeyError:
                # Interpolate variables:
                def _av(m):
                    v = None
                    n = m.group(2)
                    try:
                        v = self.namespace.variable(n)
                    except KeyError:
                        if config.FATAL_UNDEFINED:
                            raise SyntaxError("Undefined variable: '%s'." % n)
                        else:
                            if config.VERBOSITY > 1:
                                log.error("Undefined variable '%s'", n, extra={'stack': True})
                            return n
                    else:
                        if v:
                            if not isinstance(v, six.string_types):
                                v = v.render()
                            # TODO this used to test for _dequote
                            if m.group(1):
                                v = dequote(v)
                        else:
                            v = m.group(0)
                        return v

                cont = _interpolate_re.sub(_av, cont)
        # TODO this is surprising and shouldn't be here
        cont = self.do_glob_math(cont)
        return cont

    def calculate(self, _base_str, divide=False):
        better_expr_str = _base_str

        better_expr_str = self.do_glob_math(better_expr_str)

        better_expr_str = self.evaluate_expression(better_expr_str, divide=divide)

        if better_expr_str is None:
            better_expr_str = String.unquoted(self.apply_vars(_base_str))

        return better_expr_str

    # TODO only used by magic-import...?
    def interpolate(self, var):
        value = self.namespace.variable(var)
        if var != value and isinstance(value, six.string_types):
            _vi = self.evaluate_expression(value)
            if _vi is not None:
                value = _vi
        return value

    def evaluate_expression(self, expr, divide=False):
        try:
            ast = self.parse_expression(expr)
        except SassError as e:
            if config.DEBUG:
                raise
            else:
                return None

        try:
            return ast.evaluate(self, divide=divide)
        except Exception as e:
            six.reraise(SassEvaluationError, SassEvaluationError(e, expression=expr), sys.exc_info()[2])

    def parse_expression(self, expr, target='goal'):
        if isinstance(expr, six.text_type):
            # OK
            pass
        elif isinstance(expr, six.binary_type):
            # Dubious
            warn(FutureWarning(
                "parse_expression was passed binary data {0!r} "
                "-- this will no longer be supported in pyScss 2.0"
                .format(expr)
            ))
            # Don't guess an encoding; you reap what you sow
            expr = six.text_type(expr)
        else:
            raise TypeError("Expected string, got %r" % (expr,))

        key = (target, expr)
        if key in self.ast_cache:
            return self.ast_cache[key]

        try:
            parser = SassExpression(SassExpressionScanner(expr))
            ast = getattr(parser, target)()
        except SyntaxError as e:
            raise SassParseError(e, expression=expr, expression_pos=parser._char_pos)

        self.ast_cache[key] = ast
        return ast
Exemple #6
0
class Calculator(object):
    """Expression evaluator."""

    ast_cache = {}

    def __init__(
        self,
        namespace=None,
        ignore_parse_errors=False,
        undefined_variables_fatal=True,
    ):
        if namespace is None:
            self.namespace = Namespace()
        else:
            self.namespace = namespace

        self.ignore_parse_errors = ignore_parse_errors
        self.undefined_variables_fatal = undefined_variables_fatal

    def _pound_substitute(self, result):
        expr = result.group(1)
        value = self.evaluate_expression(expr)

        if value is None:
            return self.apply_vars(expr)
        elif value.is_null:
            return ""
        else:
            return dequote(value.render())

    def do_glob_math(self, cont):
        """Performs #{}-interpolation.  The result is always treated as a fixed
        syntactic unit and will not be re-evaluated.
        """
        # TODO that's a lie!  this should be in the parser for most cases.
        if not isinstance(cont, six.string_types):
            warn(
                FutureWarning(
                    "do_glob_math was passed a non-string {0!r} "
                    "-- this will no longer be supported in pyScss 2.0".format(
                        cont)))
            cont = six.text_type(cont)
        if '#{' not in cont:
            return cont
        cont = _expr_glob_re.sub(self._pound_substitute, cont)
        return cont

    def apply_vars(self, cont):
        # TODO this is very complicated.  it should go away once everything
        # valid is actually parseable.
        if isinstance(cont, six.string_types) and '$' in cont:
            try:
                # Optimization: the full cont is a variable in the context,
                cont = self.namespace.variable(cont)
            except KeyError:
                # Interpolate variables:
                def _av(m):
                    v = None
                    n = m.group(2)
                    try:
                        v = self.namespace.variable(n)
                    except KeyError:
                        if self.undefined_variables_fatal:
                            raise SyntaxError("Undefined variable: '%s'." % n)
                        else:
                            log.error("Undefined variable '%s'",
                                      n,
                                      extra={'stack': True})
                            return n
                    else:
                        if v:
                            if not isinstance(v, Value):
                                raise TypeError(
                                    "Somehow got a variable {0!r} "
                                    "with a non-Sass value: {1!r}".format(
                                        n, v))
                            v = v.render()
                            # TODO this used to test for _dequote
                            if m.group(1):
                                v = dequote(v)
                        else:
                            v = m.group(0)
                        return v

                cont = _interpolate_re.sub(_av, cont)

            else:
                # Variable succeeded, so we need to render it
                cont = cont.render()
        # TODO this is surprising and shouldn't be here
        cont = self.do_glob_math(cont)
        return cont

    def calculate(self, expression, divide=False):
        result = self.evaluate_expression(expression, divide=divide)

        if result is None:
            return String.unquoted(self.apply_vars(expression))

        return result

    # TODO only used by magic-import...?
    def interpolate(self, var):
        value = self.namespace.variable(var)
        if var != value and isinstance(value, six.string_types):
            _vi = self.evaluate_expression(value)
            if _vi is not None:
                value = _vi
        return value

    def evaluate_expression(self, expr, divide=False):
        try:
            ast = self.parse_expression(expr)
        except SassError as e:
            if self.ignore_parse_errors:
                return None
            raise

        try:
            return ast.evaluate(self, divide=divide)
        except Exception as e:
            six.reraise(SassEvaluationError,
                        SassEvaluationError(e, expression=expr),
                        sys.exc_info()[2])

    def parse_expression(self, expr, target='goal'):
        if isinstance(expr, six.text_type):
            # OK
            pass
        elif isinstance(expr, six.binary_type):
            # Dubious
            warn(
                FutureWarning(
                    "parse_expression was passed binary data {0!r} "
                    "-- this will no longer be supported in pyScss 2.0".format(
                        expr)))
            # Don't guess an encoding; you reap what you sow
            expr = six.text_type(expr)
        else:
            raise TypeError("Expected string, got %r" % (expr, ))

        key = (target, expr)
        if key in self.ast_cache:
            return self.ast_cache[key]

        try:
            parser = SassExpression(SassExpressionScanner(expr))
            ast = getattr(parser, target)()
        except SyntaxError as e:
            raise SassParseError(e,
                                 expression=expr,
                                 expression_pos=parser._char_pos)

        self.ast_cache[key] = ast
        return ast

    def parse_interpolations(self, string):
        """Parse a string for interpolations, but don't treat anything else as
        Sass syntax.  Returns an AST node.
        """
        # Shortcut: if there are no #s in the string in the first place, it
        # must not have any interpolations, right?
        if '#' not in string:
            return Literal(String.unquoted(string))
        return self.parse_expression(string, 'goal_interpolated_literal')

    def parse_vars_and_interpolations(self, string):
        """Parse a string for variables and interpolations, but don't treat
        anything else as Sass syntax.  Returns an AST node.
        """
        # Shortcut: if there are no #s or $s in the string in the first place,
        # it must not have anything of interest.
        if '#' not in string and '$' not in string:
            return Literal(String.unquoted(string))
        return self.parse_expression(string,
                                     'goal_interpolated_literal_with_vars')
Exemple #7
0
def calc():
    ns = Namespace(functions=COMPASS_HELPERS_LIBRARY)
    return Calculator(ns).evaluate_expression
Exemple #8
0
class Calculator(object):
    """Expression evaluator."""

    ast_cache = {}

    def __init__(
            self, namespace=None,
            ignore_parse_errors=False,
            undefined_variables_fatal=True,
            ):
        if namespace is None:
            self.namespace = Namespace()
        else:
            self.namespace = namespace

        self.ignore_parse_errors = ignore_parse_errors
        self.undefined_variables_fatal = undefined_variables_fatal

    def _pound_substitute(self, result):
        expr = result.group(1)
        value = self.evaluate_expression(expr)

        if value is None:
            return self.apply_vars(expr)
        elif value.is_null:
            return ""
        else:
            return dequote(value.render())

    def do_glob_math(self, cont):
        """Performs #{}-interpolation.  The result is always treated as a fixed
        syntactic unit and will not be re-evaluated.
        """
        # TODO that's a lie!  this should be in the parser for most cases.
        if not isinstance(cont, six.string_types):
            warn(FutureWarning(
                "do_glob_math was passed a non-string {0!r} "
                "-- this will no longer be supported in pyScss 2.0"
                .format(cont)
            ))
            cont = six.text_type(cont)
        if '#{' not in cont:
            return cont
        cont = _expr_glob_re.sub(self._pound_substitute, cont)
        return cont

    def apply_vars(self, cont):
        # TODO this is very complicated.  it should go away once everything
        # valid is actually parseable.
        if isinstance(cont, six.string_types) and '$' in cont:
            try:
                # Optimization: the full cont is a variable in the context,
                cont = self.namespace.variable(cont)
            except KeyError:
                # Interpolate variables:
                def _av(m):
                    v = None
                    n = m.group(2)
                    try:
                        v = self.namespace.variable(n)
                    except KeyError:
                        if self.undefined_variables_fatal:
                            raise SyntaxError("Undefined variable: '%s'." % n)
                        else:
                            log.error("Undefined variable '%s'", n, extra={'stack': True})
                            return n
                    else:
                        if v:
                            if not isinstance(v, Value):
                                raise TypeError(
                                    "Somehow got a variable {0!r} "
                                    "with a non-Sass value: {1!r}"
                                    .format(n, v)
                                )
                            v = v.render()
                            # TODO this used to test for _dequote
                            if m.group(1):
                                v = dequote(v)
                        else:
                            v = m.group(0)
                        return v

                cont = _interpolate_re.sub(_av, cont)

            else:
                # Variable succeeded, so we need to render it
                cont = cont.render()
        # TODO this is surprising and shouldn't be here
        cont = self.do_glob_math(cont)
        return cont

    def calculate(self, expression, divide=False):
        expression = self.evaluate_expression(expression, divide=divide)

        if expression is None:
            return String.unquoted(self.apply_vars(expression))

        return expression

    # TODO only used by magic-import...?
    def interpolate(self, var):
        value = self.namespace.variable(var)
        if var != value and isinstance(value, six.string_types):
            _vi = self.evaluate_expression(value)
            if _vi is not None:
                value = _vi
        return value

    def evaluate_expression(self, expr, divide=False):
        try:
            ast = self.parse_expression(expr)
        except SassError as e:
            if self.ignore_parse_errors:
                return None
            raise

        try:
            return ast.evaluate(self, divide=divide)
        except Exception as e:
            six.reraise(SassEvaluationError, SassEvaluationError(e, expression=expr), sys.exc_info()[2])

    def parse_expression(self, expr, target='goal'):
        if isinstance(expr, six.text_type):
            # OK
            pass
        elif isinstance(expr, six.binary_type):
            # Dubious
            warn(FutureWarning(
                "parse_expression was passed binary data {0!r} "
                "-- this will no longer be supported in pyScss 2.0"
                .format(expr)
            ))
            # Don't guess an encoding; you reap what you sow
            expr = six.text_type(expr)
        else:
            raise TypeError("Expected string, got %r" % (expr,))

        key = (target, expr)
        if key in self.ast_cache:
            return self.ast_cache[key]

        try:
            parser = SassExpression(SassExpressionScanner(expr))
            ast = getattr(parser, target)()
        except SyntaxError as e:
            raise SassParseError(e, expression=expr, expression_pos=parser._char_pos)

        self.ast_cache[key] = ast
        return ast

    def parse_interpolations(self, string):
        """Parse a string for interpolations, but don't treat anything else as
        Sass syntax.  Returns an AST node.
        """
        # Shortcut: if there are no #s in the string in the first place, it
        # must not have any interpolations, right?
        if '#' not in string:
            return Literal(String.unquoted(string))
        return self.parse_expression(string, 'goal_interpolated_anything')
Exemple #9
0
def test_reference_operations():
    """Test the example expressions in the reference document:

    http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#operations
    """
    # TODO: break this into its own file and add the entire reference guide

    # Need to build the calculator manually to get at its namespace, and need
    # to use calculate() instead of evaluate_expression() so interpolation
    # works
    ns = Namespace(functions=CORE_LIBRARY)
    calc = Calculator(ns).calculate

    # Simple example
    assert calc('1in + 8pt') == Number(1.11111111, "in")

    # Division
    ns.set_variable('$width', Number(1000, "px"))
    ns.set_variable('$font-size', Number(12, "px"))
    ns.set_variable('$line-height', Number(30, "px"))
    assert calc('10px/8px') == String('10px / 8px')  # plain CSS; no division
    assert calc('$width/2') == Number(500,
                                      "px")  # uses a variable; does division
    assert calc('(500px/2)') == Number(250, "px")  # uses parens; does division
    assert calc('5px + 8px/2px') == Number(9, "px")  # uses +; does division
    # TODO: Ruby doesn't include these spaces
    assert calc('#{$font-size}/#{$line-height}') == String('12px / 30px')
    # uses #{}; does no division

    # Color operations
    ns.set_variable('$translucent-red', Color.from_rgb(1, 0, 0, 0.5))
    ns.set_variable('$green', Color.from_name('lime'))
    assert calc('#010203 + #040506') == Color.from_hex('#050709')
    assert calc('#010203 * 2') == Color.from_hex('#020406')
    assert calc(
        'rgba(255, 0, 0, 0.75) + rgba(0, 255, 0, 0.75)') == Color.from_rgb(
            1, 1, 0, 0.75)
    assert calc('opacify($translucent-red, 0.3)') == Color.from_rgb(
        1, 0, 0, 0.8)
    assert calc('transparentize($translucent-red, 0.25)') == Color.from_rgb(
        1, 0, 0, 0.25)
    assert calc(
        "progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr='#{ie-hex-str($green)}', endColorstr='#{ie-hex-str($translucent-red)}')"
    ) == String.unquoted(
        "progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr='#FF00FF00', endColorstr='#80FF0000')"
    )

    # String operations
    ns.set_variable('$value', Null())
    assert_strict_string_eq(calc('e + -resize'), String('e-resize',
                                                        quotes=None))
    assert_strict_string_eq(calc('"Foo " + Bar'), String('Foo Bar',
                                                         quotes='"'))
    assert_strict_string_eq(calc('sans- + "serif"'),
                            String('sans-serif', quotes=None))
    assert calc('3px + 4px auto') == List(
        [Number(7, "px"), String('auto', quotes=None)])
    assert_strict_string_eq(calc('"I ate #{5 + 10} pies!"'),
                            String('I ate 15 pies!', quotes='"'))
    assert_strict_string_eq(calc('"I ate #{$value} pies!"'),
                            String('I ate  pies!', quotes='"'))
Exemple #10
0
 def __init__(self, namespace=None):
     if namespace is None:
         self.namespace = Namespace()
     else:
         self.namespace = namespace
Exemple #11
0
class Calculator(object):
    """Expression evaluator."""

    ast_cache = {}

    def __init__(self, namespace=None):
        if namespace is None:
            self.namespace = Namespace()
        else:
            self.namespace = namespace

    def _pound_substitute(self, result):
        expr = result.group(1)
        value = self.evaluate_expression(expr)

        if value is None:
            return self.apply_vars(expr)
        elif value.is_null:
            return ""
        else:
            return dequote(value.render())

    def do_glob_math(self, cont):
        """Performs #{}-interpolation.  The result is always treated as a fixed
        syntactic unit and will not be re-evaluated.
        """
        # TODO that's a lie!  this should be in the parser for most cases.
        if not isinstance(cont, six.string_types):
            warn(
                FutureWarning(
                    "do_glob_math was passed a non-string {0!r} "
                    "-- this will no longer be supported in pyScss 2.0".format(
                        cont)))
            cont = six.text_type(cont)
        if '#{' not in cont:
            return cont
        cont = _expr_glob_re.sub(self._pound_substitute, cont)
        return cont

    def apply_vars(self, cont):
        # TODO this is very complicated.  it should go away once everything
        # valid is actually parseable.
        if isinstance(cont, six.string_types) and '$' in cont:
            try:
                # Optimization: the full cont is a variable in the context,
                cont = self.namespace.variable(cont)
            except KeyError:
                # Interpolate variables:
                def _av(m):
                    v = None
                    n = m.group(2)
                    try:
                        v = self.namespace.variable(n)
                    except KeyError:
                        if config.FATAL_UNDEFINED:
                            raise SyntaxError("Undefined variable: '%s'." % n)
                        else:
                            if config.VERBOSITY > 1:
                                log.error("Undefined variable '%s'",
                                          n,
                                          extra={'stack': True})
                            return n
                    else:
                        if v:
                            if not isinstance(v, six.string_types):
                                v = v.render()
                            # TODO this used to test for _dequote
                            if m.group(1):
                                v = dequote(v)
                        else:
                            v = m.group(0)
                        return v

                cont = _interpolate_re.sub(_av, cont)
        # TODO this is surprising and shouldn't be here
        cont = self.do_glob_math(cont)
        return cont

    def calculate(self, _base_str, divide=False):
        better_expr_str = _base_str

        better_expr_str = self.do_glob_math(better_expr_str)

        better_expr_str = self.evaluate_expression(better_expr_str,
                                                   divide=divide)

        if better_expr_str is None:
            better_expr_str = String.unquoted(self.apply_vars(_base_str))

        return better_expr_str

    # TODO only used by magic-import...?
    def interpolate(self, var):
        value = self.namespace.variable(var)
        if var != value and isinstance(value, six.string_types):
            _vi = self.evaluate_expression(value)
            if _vi is not None:
                value = _vi
        return value

    def evaluate_expression(self, expr, divide=False):
        try:
            ast = self.parse_expression(expr)
        except SassError as e:
            if config.DEBUG:
                raise
            else:
                return None

        try:
            return ast.evaluate(self, divide=divide)
        except Exception as e:
            raise SassEvaluationError(e, expression=expr)

    def parse_expression(self, expr, target='goal'):
        if isinstance(expr, six.text_type):
            # OK
            pass
        elif isinstance(expr, six.binary_type):
            # Dubious
            warn(
                FutureWarning(
                    "parse_expression was passed binary data {0!r} "
                    "-- this will no longer be supported in pyScss 2.0".format(
                        expr)))
            # Don't guess an encoding; you reap what you sow
            expr = six.text_type(expr)
        else:
            raise TypeError("Expected string, got %r" % (expr, ))

        key = (target, expr)
        if key in self.ast_cache:
            return self.ast_cache[key]

        try:
            parser = SassExpression(SassExpressionScanner(expr))
            ast = getattr(parser, target)()
        except SyntaxError as e:
            raise SassParseError(e,
                                 expression=expr,
                                 expression_pos=parser._char_pos)

        self.ast_cache[key] = ast
        return ast
Exemple #12
0
class Calculator(object):
    """Expression evaluator."""
    def __init__(self, namespace=None):
        if namespace is None:
            self.namespace = Namespace()
        else:
            self.namespace = namespace

    def _calculate_expr(self, result):
        _group0 = result.group(1)
        _base_str = _group0
        better_expr_str = self.evaluate_expression(_base_str)

        if better_expr_str is None:
            better_expr_str = self.apply_vars(_base_str)
        else:
            better_expr_str = dequote(better_expr_str.render())

        return better_expr_str

    def do_glob_math(self, cont):
        """Performs #{}-interpolation.  The result is always treated as a fixed
        syntactic unit and will not be re-evaluated.
        """
        # TODO this should really accept and/or parse an *expression* and
        # return a type  :|
        cont = str(cont)
        if '#{' not in cont:
            return cont
        cont = _expr_glob_re.sub(self._calculate_expr, cont)
        return cont

    def apply_vars(self, cont):
        if isinstance(cont, six.string_types) and '$' in cont:
            try:
                # Optimization: the full cont is a variable in the context,
                cont = self.namespace.variable(cont)
            except KeyError:
                # Interpolate variables:
                def _av(m):
                    v = self.namespace.variable(m.group(2))
                    if v:
                        if not isinstance(v, six.string_types):
                            v = v.render()
                        # TODO this used to test for _dequote
                        if m.group(1):
                            v = dequote(v)
                    else:
                        v = m.group(0)
                    return v

                cont = _interpolate_re.sub(_av, cont)
        # XXX what?: if options is not None:
        # ...apply math:
        cont = self.do_glob_math(cont)
        return cont

    def calculate(self, _base_str, divide=False):
        better_expr_str = _base_str

        better_expr_str = self.do_glob_math(better_expr_str)

        better_expr_str = self.evaluate_expression(better_expr_str,
                                                   divide=divide)

        if better_expr_str is None:
            better_expr_str = self.apply_vars(_base_str)

        return better_expr_str

    # TODO only used by magic-import...?
    def interpolate(self, var):
        value = self.namespace.variable(var)
        if var != value and isinstance(value, six.string_types):
            _vi = self.evaluate_expression(value)
            if _vi is not None:
                value = _vi
        return value

    def evaluate_expression(self, expr, divide=False):
        if not isinstance(expr, six.string_types):
            raise TypeError("Expected string, got %r" % (expr, ))

        if expr in ast_cache:
            ast = ast_cache[expr]

        elif _variable_re.match(expr):
            # Short-circuit for variable names
            ast = Variable(expr)

        else:
            try:
                P = SassExpression(SassExpressionScanner())
                P.reset(expr)
                ast = P.goal()
            except SyntaxError:
                if config.DEBUG:
                    raise
                return None
            except Exception:
                # TODO hoist me up since the rule is gone
                #log.exception("Exception raised: %s in `%s' (%s)", e, expr, rule.file_and_line)
                if config.DEBUG:
                    raise
                return None
            else:
                ast_cache[expr] = ast

        return ast.evaluate(self, divide=divide)

    def parse_expression(self, expr, target='goal'):
        if expr in ast_cache:
            return ast_cache[expr]

        parser = SassExpression(SassExpressionScanner())
        parser.reset(expr)
        ast = getattr(parser, target)()

        if target == 'goal':
            ast_cache[expr] = ast

        return ast
Exemple #13
0
class Calculator(object):
    """Expression evaluator."""

    ast_cache = {}

    def __init__(self, namespace=None):
        if namespace is None:
            self.namespace = Namespace()
        else:
            self.namespace = namespace

    def _pound_substitute(self, result):
        expr = result.group(1)
        value = self.evaluate_expression(expr)

        if value is None:
            return self.apply_vars(expr)
        elif value.is_null:
            return ""
        else:
            return dequote(value.render())

    def do_glob_math(self, cont):
        """Performs #{}-interpolation.  The result is always treated as a fixed
        syntactic unit and will not be re-evaluated.
        """
        # TODO this should really accept and/or parse an *expression* and
        # return a type  :|
        cont = str(cont)
        if '#{' not in cont:
            return cont
        cont = _expr_glob_re.sub(self._pound_substitute, cont)
        return cont

    def apply_vars(self, cont):
        if isinstance(cont, six.string_types) and '$' in cont:
            try:
                # Optimization: the full cont is a variable in the context,
                cont = self.namespace.variable(cont)
            except KeyError:
                # Interpolate variables:
                def _av(m):
                    v = None
                    n = m.group(2)
                    try:
                        v = self.namespace.variable(n)
                    except KeyError:
                        if config.FATAL_UNDEFINED:
                            raise
                        else:
                            if config.VERBOSITY > 1:
                                log.error("Undefined variable '%s'", n, extra={'stack': True})
                            return n
                    else:
                        if v:
                            if not isinstance(v, six.string_types):
                                v = v.render()
                            # TODO this used to test for _dequote
                            if m.group(1):
                                v = dequote(v)
                        else:
                            v = m.group(0)
                        return v

                cont = _interpolate_re.sub(_av, cont)
        # XXX what?: if options is not None:
        # ...apply math:
        cont = self.do_glob_math(cont)
        return cont

    def calculate(self, _base_str, divide=False):
        better_expr_str = _base_str

        better_expr_str = self.do_glob_math(better_expr_str)

        better_expr_str = self.evaluate_expression(better_expr_str, divide=divide)

        if better_expr_str is None:
            better_expr_str = String.unquoted(self.apply_vars(_base_str))

        return better_expr_str

    # TODO only used by magic-import...?
    def interpolate(self, var):
        value = self.namespace.variable(var)
        if var != value and isinstance(value, six.string_types):
            _vi = self.evaluate_expression(value)
            if _vi is not None:
                value = _vi
        return value

    def evaluate_expression(self, expr, divide=False):
        try:
            ast = self.parse_expression(expr)
        except SassError:
            if config.DEBUG:
                raise
            else:
                return None

        try:
            return ast.evaluate(self, divide=divide)
        except Exception as e:
            raise SassEvaluationError(e, expression=expr)

    def parse_expression(self, expr, target='goal'):
        if not isinstance(expr, six.string_types):
            raise TypeError("Expected string, got %r" % (expr,))

        key = (target, expr)
        if key in self.ast_cache:
            return self.ast_cache[key]

        try:
            parser = SassExpression(SassExpressionScanner(expr))
            ast = getattr(parser, target)()
        except SyntaxError as e:
            raise SassParseError(e, expression=expr, expression_pos=parser._char_pos)

        self.ast_cache[key] = ast
        return ast
Exemple #14
0
def test_reference_operations():
    """Test the example expressions in the reference document:

    http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#operations
    """
    # TODO: break this into its own file and add the entire reference guide
    ns = Namespace()
    calc = lambda expr: to_str(Calculator(ns).calculate(expr))

    # Simple example
    assert calc('1in + 8pt') == '1.111in'

    # Division
    ns.set_variable('$width', '1000px')
    ns.set_variable('$font-size', '12px')
    ns.set_variable('$line-height', '30px')
    assert calc('10px/8px') == '10px/8px'   # plain CSS; no division
    assert calc('$width/2') == '500px'      # uses a variable; does division
    assert calc('(500px/2)') == '250px'     # uses parens; does division
    assert calc('5px + 8px/2px') == '9px'   # uses +; does division
    assert calc('#{$font-size}/#{$line-height}') == '12px/30px'
                                            # uses #{}; does no division

    # Color operations
    ns.set_variable('$translucent-red', 'rgba(255, 0, 0, 0.5)')
    ns.set_variable('$green', '#00ff00')
    assert calc('#010203 + #040506') == '#050709'
    assert calc('#010203 * 2') == '#020406'
    assert calc('rgba(255, 0, 0, 0.75) + rgba(0, 255, 0, 0.75)') == 'rgba(255, 255, 0, 0.75)'
    assert calc('opacify($translucent-red, 0.3)') == 'rgba(255, 0, 0, 0.9)'
    assert calc('transparentize($translucent-red, 0.25)') == 'rgba(255, 0, 0, 0.25)'
    assert calc("progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr='#{ie-hex-str($green)}', endColorstr='#{ie-hex-str($translucent-red)}')"
                ) == "progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr=#FF00FF00, endColorstr=#80FF0000)"

    # String operations
    ns.set_variable('$value', 'null')
    assert calc('e + -resize') == 'e-resize'
    assert calc('"Foo " + Bar') == '"Foo Bar"'
    assert calc('sans- + "serif"') == 'sans-serif'
    assert calc('3px + 4px auto') == '7px auto'
    assert calc('"I ate #{5 + 10} pies!"') == '"I ate 15 pies!"'
    assert calc('"I ate #{$value} pies!"') == '"I ate  pies!"'