1from __future__ import absolute_import 2from __future__ import print_function 3from __future__ import unicode_literals 4 5import sys 6import logging 7from warnings import warn 8 9import six 10 11from scss.ast import Literal 12from scss.cssdefs import _expr_glob_re, _interpolate_re 13from scss.errors import SassError, SassEvaluationError, SassParseError 14from scss.grammar.expression import SassExpression, SassExpressionScanner 15from scss.rule import Namespace 16from scss.types import String 17from scss.types import Value 18from scss.util import dequote 19 20 21log = logging.getLogger(__name__) 22 23 24class Calculator(object): 25 """Expression evaluator.""" 26 27 ast_cache = {} 28 29 def __init__( 30 self, namespace=None, 31 ignore_parse_errors=False, 32 undefined_variables_fatal=True, 33 ): 34 if namespace is None: 35 self.namespace = Namespace() 36 else: 37 self.namespace = namespace 38 39 self.ignore_parse_errors = ignore_parse_errors 40 self.undefined_variables_fatal = undefined_variables_fatal 41 42 def _pound_substitute(self, result): 43 expr = result.group(1) 44 value = self.evaluate_expression(expr) 45 46 if value is None: 47 return self.apply_vars(expr) 48 elif value.is_null: 49 return "" 50 else: 51 return dequote(value.render()) 52 53 def do_glob_math(self, cont): 54 """Performs #{}-interpolation. The result is always treated as a fixed 55 syntactic unit and will not be re-evaluated. 56 """ 57 # TODO that's a lie! this should be in the parser for most cases. 58 if not isinstance(cont, six.string_types): 59 warn(FutureWarning( 60 "do_glob_math was passed a non-string {0!r} " 61 "-- this will no longer be supported in pyScss 2.0" 62 .format(cont) 63 )) 64 cont = six.text_type(cont) 65 if '#{' not in cont: 66 return cont 67 cont = _expr_glob_re.sub(self._pound_substitute, cont) 68 return cont 69 70 def apply_vars(self, cont): 71 # TODO this is very complicated. it should go away once everything 72 # valid is actually parseable. 73 if isinstance(cont, six.string_types) and '$' in cont: 74 try: 75 # Optimization: the full cont is a variable in the context, 76 cont = self.namespace.variable(cont) 77 except KeyError: 78 # Interpolate variables: 79 def _av(m): 80 v = None 81 n = m.group(2) 82 try: 83 v = self.namespace.variable(n) 84 except KeyError: 85 if self.undefined_variables_fatal: 86 raise SyntaxError("Undefined variable: '%s'." % n) 87 else: 88 log.error("Undefined variable '%s'", n, extra={'stack': True}) 89 return n 90 else: 91 if v: 92 if not isinstance(v, Value): 93 raise TypeError( 94 "Somehow got a variable {0!r} " 95 "with a non-Sass value: {1!r}" 96 .format(n, v) 97 ) 98 v = v.render() 99 # TODO this used to test for _dequote 100 if m.group(1): 101 v = dequote(v) 102 else: 103 v = m.group(0) 104 return v 105 106 cont = _interpolate_re.sub(_av, cont) 107 108 else: 109 # Variable succeeded, so we need to render it 110 cont = cont.render() 111 # TODO this is surprising and shouldn't be here 112 cont = self.do_glob_math(cont) 113 return cont 114 115 def calculate(self, expression, divide=False): 116 result = self.evaluate_expression(expression, divide=divide) 117 118 if result is None: 119 return String.unquoted(self.apply_vars(expression)) 120 121 return result 122 123 # TODO only used by magic-import...? 124 def interpolate(self, var): 125 value = self.namespace.variable(var) 126 if var != value and isinstance(value, six.string_types): 127 _vi = self.evaluate_expression(value) 128 if _vi is not None: 129 value = _vi 130 return value 131 132 def evaluate_expression(self, expr, divide=False): 133 try: 134 ast = self.parse_expression(expr) 135 except SassError as e: 136 if self.ignore_parse_errors: 137 return None 138 raise 139 140 try: 141 return ast.evaluate(self, divide=divide) 142 except Exception as e: 143 six.reraise(SassEvaluationError, SassEvaluationError(e, expression=expr), sys.exc_info()[2]) 144 145 def parse_expression(self, expr, target='goal'): 146 if isinstance(expr, six.text_type): 147 # OK 148 pass 149 elif isinstance(expr, six.binary_type): 150 # Dubious 151 warn(FutureWarning( 152 "parse_expression was passed binary data {0!r} " 153 "-- this will no longer be supported in pyScss 2.0" 154 .format(expr) 155 )) 156 # Don't guess an encoding; you reap what you sow 157 expr = six.text_type(expr) 158 else: 159 raise TypeError("Expected string, got %r" % (expr,)) 160 161 key = (target, expr) 162 if key in self.ast_cache: 163 return self.ast_cache[key] 164 165 try: 166 parser = SassExpression(SassExpressionScanner(expr)) 167 ast = getattr(parser, target)() 168 except SyntaxError as e: 169 raise SassParseError(e, expression=expr, expression_pos=parser._char_pos) 170 171 self.ast_cache[key] = ast 172 return ast 173 174 def parse_interpolations(self, string): 175 """Parse a string for interpolations, but don't treat anything else as 176 Sass syntax. Returns an AST node. 177 """ 178 # Shortcut: if there are no #s in the string in the first place, it 179 # must not have any interpolations, right? 180 if '#' not in string: 181 return Literal(String.unquoted(string)) 182 return self.parse_expression(string, 'goal_interpolated_literal') 183 184 def parse_vars_and_interpolations(self, string): 185 """Parse a string for variables and interpolations, but don't treat 186 anything else as Sass syntax. Returns an AST node. 187 """ 188 # Shortcut: if there are no #s or $s in the string in the first place, 189 # it must not have anything of interest. 190 if '#' not in string and '$' not in string: 191 return Literal(String.unquoted(string)) 192 return self.parse_expression( 193 string, 'goal_interpolated_literal_with_vars') 194 195 196__all__ = ('Calculator',) 197