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 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
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='"'))
def __init__(self, namespace=None): if namespace is None: self.namespace = Namespace() else: self.namespace = namespace
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
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')
def calc(): ns = Namespace(functions=COMPASS_HELPERS_LIBRARY) return Calculator(ns).evaluate_expression
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')
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='"'))
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
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
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
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!"'