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