906 lines
33 KiB
Python
906 lines
33 KiB
Python
from __future__ import absolute_import
|
|
from __future__ import print_function
|
|
|
|
from functools import partial
|
|
import logging
|
|
import operator
|
|
import re
|
|
|
|
import six
|
|
|
|
import scss.config as config
|
|
from scss.cssdefs import COLOR_NAMES, is_builtin_css_function, _expr_glob_re, _interpolate_re
|
|
from scss.errors import SassError, SassEvaluationError, SassParseError
|
|
from scss.rule import Namespace
|
|
from scss.types import Boolean, Color, List, Map, Null, Number, String, Undefined, Value
|
|
from scss.util import dequote, normalize_var
|
|
|
|
################################################################################
|
|
# Load C acceleration modules
|
|
Scanner = None
|
|
try:
|
|
from scss._speedups import Scanner
|
|
except ImportError:
|
|
from scss._native import Scanner
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
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.
|
|
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):
|
|
# 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:
|
|
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
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Expression classes -- the AST resulting from a parse
|
|
|
|
class Expression(object):
|
|
def __repr__(self):
|
|
return '<%s()>' % (self.__class__.__name__)
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
"""Evaluate this AST node, and return a Sass value.
|
|
|
|
`divide` indicates whether a descendant node representing a division
|
|
should be forcibly treated as a division. See the commentary in
|
|
`BinaryOp`.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
|
|
class Parentheses(Expression):
|
|
"""An expression of the form `(foo)`.
|
|
|
|
Only exists to force a slash to be interpreted as division when contained
|
|
within parentheses.
|
|
"""
|
|
def __repr__(self):
|
|
return '<%s(%s)>' % (self.__class__.__name__, repr(self.contents))
|
|
|
|
def __init__(self, contents):
|
|
self.contents = contents
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
return self.contents.evaluate(calculator, divide=True)
|
|
|
|
|
|
class UnaryOp(Expression):
|
|
def __repr__(self):
|
|
return '<%s(%s, %s)>' % (self.__class__.__name__, repr(self.op), repr(self.operand))
|
|
|
|
def __init__(self, op, operand):
|
|
self.op = op
|
|
self.operand = operand
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
return self.op(self.operand.evaluate(calculator, divide=True))
|
|
|
|
|
|
class BinaryOp(Expression):
|
|
def __repr__(self):
|
|
return '<%s(%s, %s, %s)>' % (self.__class__.__name__, repr(self.op), repr(self.left), repr(self.right))
|
|
|
|
def __init__(self, op, left, right):
|
|
self.op = op
|
|
self.left = left
|
|
self.right = right
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
left = self.left.evaluate(calculator, divide=True)
|
|
right = self.right.evaluate(calculator, divide=True)
|
|
|
|
# Special handling of division: treat it as a literal slash if both
|
|
# operands are literals, there are parentheses, or this is part of a
|
|
# bigger expression.
|
|
# The first condition is covered by the type check. The other two are
|
|
# covered by the `divide` argument: other nodes that perform arithmetic
|
|
# will pass in True, indicating that this should always be a division.
|
|
if (
|
|
self.op is operator.truediv
|
|
and not divide
|
|
and isinstance(self.left, Literal)
|
|
and isinstance(self.right, Literal)
|
|
):
|
|
return String(left.render() + ' / ' + right.render(), quotes=None)
|
|
|
|
return self.op(left, right)
|
|
|
|
|
|
class AnyOp(Expression):
|
|
def __repr__(self):
|
|
return '<%s(*%s)>' % (self.__class__.__name__, repr(self.op), repr(self.operands))
|
|
|
|
def __init__(self, *operands):
|
|
self.operands = operands
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
for operand in self.operands:
|
|
value = operand.evaluate(calculator, divide=True)
|
|
if value:
|
|
return value
|
|
return value
|
|
|
|
|
|
class AllOp(Expression):
|
|
def __repr__(self):
|
|
return '<%s(*%s)>' % (self.__class__.__name__, repr(self.operands))
|
|
|
|
def __init__(self, *operands):
|
|
self.operands = operands
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
for operand in self.operands:
|
|
value = operand.evaluate(calculator, divide=True)
|
|
if not value:
|
|
return value
|
|
return value
|
|
|
|
|
|
class NotOp(Expression):
|
|
def __repr__(self):
|
|
return '<%s(%s)>' % (self.__class__.__name__, repr(self.operand))
|
|
|
|
def __init__(self, operand):
|
|
self.operand = operand
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
operand = self.operand.evaluate(calculator, divide=True)
|
|
return Boolean(not(operand))
|
|
|
|
|
|
class CallOp(Expression):
|
|
def __repr__(self):
|
|
return '<%s(%s, %s)>' % (self.__class__.__name__, repr(self.func_name), repr(self.argspec))
|
|
|
|
def __init__(self, func_name, argspec):
|
|
self.func_name = func_name
|
|
self.argspec = argspec
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
# TODO bake this into the context and options "dicts", plus library
|
|
func_name = normalize_var(self.func_name)
|
|
|
|
argspec_node = self.argspec
|
|
|
|
# Turn the pairs of arg tuples into *args and **kwargs
|
|
# TODO unclear whether this is correct -- how does arg, kwarg, arg
|
|
# work?
|
|
args, kwargs = argspec_node.evaluate_call_args(calculator)
|
|
argspec_len = len(args) + len(kwargs)
|
|
|
|
# Translate variable names to Python identifiers
|
|
# TODO what about duplicate kw names? should this happen in argspec?
|
|
# how does that affect mixins?
|
|
kwargs = dict(
|
|
(key.lstrip('$').replace('-', '_'), value)
|
|
for key, value in kwargs.items())
|
|
|
|
# TODO merge this with the library
|
|
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
|
|
funct = calculator.namespace.function(func_name, 1)
|
|
args = [List(args, use_comma=True)]
|
|
except KeyError:
|
|
if not is_builtin_css_function(func_name):
|
|
log.error("Function not found: %s:%s", func_name, argspec_len, extra={'stack': True})
|
|
|
|
if funct:
|
|
ret = funct(*args, **kwargs)
|
|
if not isinstance(ret, Value):
|
|
raise TypeError("Expected Sass type as return value, got %r" % (ret,))
|
|
return ret
|
|
|
|
# No matching function found, so render the computed values as a CSS
|
|
# function call. Slurpy arguments are expanded and named arguments are
|
|
# unsupported.
|
|
if kwargs:
|
|
raise TypeError("The CSS function %s doesn't support keyword arguments." % (func_name,))
|
|
|
|
# TODO another candidate for a "function call" sass type
|
|
rendered_args = [arg.render() for arg in args]
|
|
|
|
return String(
|
|
u"%s(%s)" % (func_name, u", ".join(rendered_args)),
|
|
quotes=None)
|
|
|
|
|
|
class Literal(Expression):
|
|
def __repr__(self):
|
|
return '<%s(%s)>' % (self.__class__.__name__, repr(self.value))
|
|
|
|
def __init__(self, value):
|
|
if isinstance(value, Undefined) and config.FATAL_UNDEFINED:
|
|
raise SyntaxError("Undefined literal.")
|
|
else:
|
|
self.value = value
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
return self.value
|
|
|
|
|
|
class Variable(Expression):
|
|
def __repr__(self):
|
|
return '<%s(%s)>' % (self.__class__.__name__, repr(self.name))
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
try:
|
|
value = calculator.namespace.variable(self.name)
|
|
except KeyError:
|
|
if config.FATAL_UNDEFINED:
|
|
raise SyntaxError("Undefined variable: '%s'." % self.name)
|
|
else:
|
|
if config.VERBOSITY > 1:
|
|
log.error("Undefined variable '%s'", self.name, extra={'stack': True})
|
|
return Undefined()
|
|
else:
|
|
if isinstance(value, six.string_types):
|
|
evald = calculator.evaluate_expression(value)
|
|
if evald is not None:
|
|
return evald
|
|
return value
|
|
|
|
|
|
class ListLiteral(Expression):
|
|
def __repr__(self):
|
|
return '<%s(%s, comma=%s)>' % (self.__class__.__name__, repr(self.items), repr(self.comma))
|
|
|
|
def __init__(self, items, comma=True):
|
|
self.items = items
|
|
self.comma = comma
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
items = [item.evaluate(calculator, divide=divide) for item in self.items]
|
|
|
|
# Whether this is a "plain" literal matters for null removal: nulls are
|
|
# left alone if this is a completely vanilla CSS property
|
|
is_literal = True
|
|
if divide:
|
|
# TODO sort of overloading "divide" here... rename i think
|
|
is_literal = False
|
|
elif not all(isinstance(item, Literal) for item in self.items):
|
|
is_literal = False
|
|
|
|
return List(items, use_comma=self.comma, is_literal=is_literal)
|
|
|
|
|
|
class MapLiteral(Expression):
|
|
def __repr__(self):
|
|
return '<%s(%s)>' % (self.__class__.__name__, repr(self.pairs))
|
|
|
|
def __init__(self, pairs):
|
|
self.pairs = tuple((var, value) for var, value in pairs if value is not None)
|
|
|
|
def evaluate(self, calculator, divide=False):
|
|
scss_pairs = []
|
|
for key, value in self.pairs:
|
|
scss_pairs.append((
|
|
key.evaluate(calculator),
|
|
value.evaluate(calculator),
|
|
))
|
|
|
|
return Map(scss_pairs)
|
|
|
|
|
|
class ArgspecLiteral(Expression):
|
|
"""Contains pairs of argument names and values, as parsed from a function
|
|
definition or function call.
|
|
|
|
Note that the semantics are somewhat ambiguous. Consider parsing:
|
|
|
|
$foo, $bar: 3
|
|
|
|
If this appeared in a function call, $foo would refer to a value; if it
|
|
appeared in a function definition, $foo would refer to an existing
|
|
variable. This it's up to the caller to use the right iteration function.
|
|
"""
|
|
def __repr__(self):
|
|
return '<%s(%s)>' % (self.__class__.__name__, repr(self.argpairs))
|
|
|
|
def __init__(self, argpairs, slurp=None):
|
|
# argpairs is a list of 2-tuples, parsed as though this were a function
|
|
# call, so (variable name as string or None, default value as AST
|
|
# node).
|
|
# slurp is the name of a variable to receive slurpy arguments.
|
|
self.argpairs = tuple(argpairs)
|
|
if slurp is all:
|
|
# DEVIATION: special syntax to allow injecting arbitrary arguments
|
|
# from the caller to the callee
|
|
self.inject = True
|
|
self.slurp = None
|
|
elif slurp:
|
|
self.inject = False
|
|
self.slurp = Variable(slurp)
|
|
else:
|
|
self.inject = False
|
|
self.slurp = None
|
|
|
|
def iter_list_argspec(self):
|
|
yield None, ListLiteral(zip(*self.argpairs)[1])
|
|
|
|
def iter_def_argspec(self):
|
|
"""Interpreting this literal as a function definition, yields pairs of
|
|
(variable name as a string, default value as an AST node or None).
|
|
"""
|
|
started_kwargs = False
|
|
seen_vars = set()
|
|
|
|
for var, value in self.argpairs:
|
|
if var is None:
|
|
# value is actually the name
|
|
var = value
|
|
value = None
|
|
|
|
if started_kwargs:
|
|
raise SyntaxError(
|
|
"Required argument %r must precede optional arguments"
|
|
% (var.name,))
|
|
|
|
else:
|
|
started_kwargs = True
|
|
|
|
if not isinstance(var, Variable):
|
|
raise SyntaxError("Expected variable name, got %r" % (var,))
|
|
|
|
if var.name in seen_vars:
|
|
raise SyntaxError("Duplicate argument %r" % (var.name,))
|
|
seen_vars.add(var.name)
|
|
|
|
yield var.name, value
|
|
|
|
def evaluate_call_args(self, calculator):
|
|
"""Interpreting this literal as a function call, return a 2-tuple of
|
|
``(args, kwargs)``.
|
|
"""
|
|
args = []
|
|
kwargs = {}
|
|
for var_node, value_node in self.argpairs:
|
|
value = value_node.evaluate(calculator, divide=True)
|
|
if var_node is None:
|
|
# Positional
|
|
args.append(value)
|
|
else:
|
|
# Named
|
|
if not isinstance(var_node, Variable):
|
|
raise SyntaxError("Expected variable name, got %r" % (var_node,))
|
|
kwargs[var_node.name] = value
|
|
|
|
# Slurpy arguments go on the end of the args
|
|
if self.slurp:
|
|
args.extend(self.slurp.evaluate(calculator, divide=True))
|
|
|
|
return args, kwargs
|
|
|
|
|
|
def parse_bareword(word):
|
|
if word in COLOR_NAMES:
|
|
return Color.from_name(word)
|
|
elif word == 'null':
|
|
return Null()
|
|
elif word == 'undefined':
|
|
return Undefined()
|
|
elif word == 'true':
|
|
return Boolean(True)
|
|
elif word == 'false':
|
|
return Boolean(False)
|
|
else:
|
|
return String(word, quotes=None)
|
|
|
|
|
|
class Parser(object):
|
|
def __init__(self, scanner):
|
|
self._scanner = scanner
|
|
self._pos = 0
|
|
self._char_pos = 0
|
|
|
|
def reset(self, input):
|
|
self._scanner.reset(input)
|
|
self._pos = 0
|
|
self._char_pos = 0
|
|
|
|
def _peek(self, types):
|
|
"""
|
|
Returns the token type for lookahead; if there are any args
|
|
then the list of args is the set of token types to allow
|
|
"""
|
|
try:
|
|
tok = self._scanner.token(self._pos, types)
|
|
return tok[2]
|
|
except SyntaxError:
|
|
return None
|
|
|
|
def _scan(self, type):
|
|
"""
|
|
Returns the matched text, and moves to the next token
|
|
"""
|
|
tok = self._scanner.token(self._pos, set([type]))
|
|
self._char_pos = tok[0]
|
|
if tok[2] != type:
|
|
raise SyntaxError("SyntaxError[@ char %s: %s]" % (repr(tok[0]), "Trying to find " + type))
|
|
self._pos += 1
|
|
return tok[3]
|
|
|
|
|
|
################################################################################
|
|
## Grammar compiled using Yapps:
|
|
|
|
class SassExpressionScanner(Scanner):
|
|
patterns = None
|
|
_patterns = [
|
|
('":"', ':'),
|
|
('","', ','),
|
|
('[ \r\t\n]+', '[ \r\t\n]+'),
|
|
('LPAR', '\\(|\\['),
|
|
('RPAR', '\\)|\\]'),
|
|
('END', '$'),
|
|
('MUL', '[*]'),
|
|
('DIV', '/'),
|
|
('ADD', '[+]'),
|
|
('SUB', '-\\s'),
|
|
('SIGN', '-(?![a-zA-Z_])'),
|
|
('AND', '(?<![-\\w])and(?![-\\w])'),
|
|
('OR', '(?<![-\\w])or(?![-\\w])'),
|
|
('NOT', '(?<![-\\w])not(?![-\\w])'),
|
|
('NE', '!='),
|
|
('INV', '!'),
|
|
('EQ', '=='),
|
|
('LE', '<='),
|
|
('GE', '>='),
|
|
('LT', '<'),
|
|
('GT', '>'),
|
|
('DOTDOTDOT', '[.]{3}'),
|
|
('KWSTR', "'[^']*'(?=\\s*:)"),
|
|
('STR', "'[^']*'"),
|
|
('KWQSTR', '"[^"]*"(?=\\s*:)'),
|
|
('QSTR', '"[^"]*"'),
|
|
('UNITS', '(?<!\\s)(?:[a-zA-Z]+|%)(?![-\\w])'),
|
|
('KWNUM', '(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?=\\s*:)'),
|
|
('NUM', '(?:\\d+(?:\\.\\d*)?|\\.\\d+)'),
|
|
('KWCOLOR', '#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])(?=\\s*:)'),
|
|
('COLOR', '#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])'),
|
|
('KWVAR', '\\$[-a-zA-Z0-9_]+(?=\\s*:)'),
|
|
('SLURPYVAR', '\\$[-a-zA-Z0-9_]+(?=[.][.][.])'),
|
|
('VAR', '\\$[-a-zA-Z0-9_]+'),
|
|
('FNCT', '[-a-zA-Z_][-a-zA-Z0-9_]*(?=\\()'),
|
|
('KWID', '[-a-zA-Z_][-a-zA-Z0-9_]*(?=\\s*:)'),
|
|
('ID', '[-a-zA-Z_][-a-zA-Z0-9_]*'),
|
|
('BANG_IMPORTANT', '!important'),
|
|
]
|
|
|
|
def __init__(self, input=None):
|
|
if hasattr(self, 'setup_patterns'):
|
|
self.setup_patterns(self._patterns)
|
|
elif self.patterns is None:
|
|
self.__class__.patterns = []
|
|
for t, p in self._patterns:
|
|
self.patterns.append((t, re.compile(p)))
|
|
super(SassExpressionScanner, self).__init__(None, ['[ \r\t\n]+'], input)
|
|
|
|
|
|
class SassExpression(Parser):
|
|
def goal(self):
|
|
expr_lst = self.expr_lst()
|
|
END = self._scan('END')
|
|
return expr_lst
|
|
|
|
def goal_argspec(self):
|
|
argspec = self.argspec()
|
|
END = self._scan('END')
|
|
return argspec
|
|
|
|
def argspec(self):
|
|
_token_ = self._peek(self.argspec_rsts)
|
|
if _token_ not in self.argspec_chks:
|
|
if self._peek(self.argspec_rsts_) not in self.argspec_chks_:
|
|
argspec_items = self.argspec_items()
|
|
args, slurpy = argspec_items
|
|
return ArgspecLiteral(args, slurp=slurpy)
|
|
return ArgspecLiteral([])
|
|
elif _token_ == 'SLURPYVAR':
|
|
SLURPYVAR = self._scan('SLURPYVAR')
|
|
DOTDOTDOT = self._scan('DOTDOTDOT')
|
|
return ArgspecLiteral([], slurp=SLURPYVAR)
|
|
else: # == 'DOTDOTDOT'
|
|
DOTDOTDOT = self._scan('DOTDOTDOT')
|
|
return ArgspecLiteral([], slurp=all)
|
|
|
|
def argspec_items(self):
|
|
slurpy = None
|
|
argspec_item = self.argspec_item()
|
|
args = [argspec_item]
|
|
if self._peek(self.argspec_items_rsts) == '","':
|
|
self._scan('","')
|
|
if self._peek(self.argspec_items_rsts_) not in self.argspec_chks_:
|
|
_token_ = self._peek(self.argspec_items_rsts__)
|
|
if _token_ == 'SLURPYVAR':
|
|
SLURPYVAR = self._scan('SLURPYVAR')
|
|
DOTDOTDOT = self._scan('DOTDOTDOT')
|
|
slurpy = SLURPYVAR
|
|
elif _token_ == 'DOTDOTDOT':
|
|
DOTDOTDOT = self._scan('DOTDOTDOT')
|
|
slurpy = all
|
|
else: # in self.argspec_items_chks
|
|
argspec_items = self.argspec_items()
|
|
more_args, slurpy = argspec_items
|
|
args.extend(more_args)
|
|
return args, slurpy
|
|
|
|
def argspec_item(self):
|
|
_token_ = self._peek(self.argspec_items_chks)
|
|
if _token_ == 'KWVAR':
|
|
KWVAR = self._scan('KWVAR')
|
|
self._scan('":"')
|
|
expr_slst = self.expr_slst()
|
|
return (Variable(KWVAR), expr_slst)
|
|
else: # in self.argspec_item_chks
|
|
expr_slst = self.expr_slst()
|
|
return (None, expr_slst)
|
|
|
|
def expr_map(self):
|
|
map_item = self.map_item()
|
|
pairs = [map_item]
|
|
while self._peek(self.expr_map_rsts) == '","':
|
|
self._scan('","')
|
|
map_item = (None, None)
|
|
if self._peek(self.expr_map_rsts_) not in self.expr_map_rsts:
|
|
map_item = self.map_item()
|
|
pairs.append(map_item)
|
|
return MapLiteral(pairs)
|
|
|
|
def map_item(self):
|
|
kwatom = self.kwatom()
|
|
self._scan('":"')
|
|
expr_slst = self.expr_slst()
|
|
return (kwatom, expr_slst)
|
|
|
|
def expr_lst(self):
|
|
expr_slst = self.expr_slst()
|
|
v = [expr_slst]
|
|
while self._peek(self.argspec_items_rsts) == '","':
|
|
self._scan('","')
|
|
expr_slst = self.expr_slst()
|
|
v.append(expr_slst)
|
|
return ListLiteral(v) if len(v) > 1 else v[0]
|
|
|
|
def expr_slst(self):
|
|
or_expr = self.or_expr()
|
|
v = [or_expr]
|
|
while self._peek(self.expr_slst_rsts) not in self.argspec_items_rsts:
|
|
or_expr = self.or_expr()
|
|
v.append(or_expr)
|
|
return ListLiteral(v, comma=False) if len(v) > 1 else v[0]
|
|
|
|
def or_expr(self):
|
|
and_expr = self.and_expr()
|
|
v = and_expr
|
|
while self._peek(self.or_expr_rsts) == 'OR':
|
|
OR = self._scan('OR')
|
|
and_expr = self.and_expr()
|
|
v = AnyOp(v, and_expr)
|
|
return v
|
|
|
|
def and_expr(self):
|
|
not_expr = self.not_expr()
|
|
v = not_expr
|
|
while self._peek(self.and_expr_rsts) == 'AND':
|
|
AND = self._scan('AND')
|
|
not_expr = self.not_expr()
|
|
v = AllOp(v, not_expr)
|
|
return v
|
|
|
|
def not_expr(self):
|
|
_token_ = self._peek(self.argspec_item_chks)
|
|
if _token_ != 'NOT':
|
|
comparison = self.comparison()
|
|
return comparison
|
|
else: # == 'NOT'
|
|
NOT = self._scan('NOT')
|
|
not_expr = self.not_expr()
|
|
return NotOp(not_expr)
|
|
|
|
def comparison(self):
|
|
a_expr = self.a_expr()
|
|
v = a_expr
|
|
while self._peek(self.comparison_rsts) in self.comparison_chks:
|
|
_token_ = self._peek(self.comparison_chks)
|
|
if _token_ == 'LT':
|
|
LT = self._scan('LT')
|
|
a_expr = self.a_expr()
|
|
v = BinaryOp(operator.lt, v, a_expr)
|
|
elif _token_ == 'GT':
|
|
GT = self._scan('GT')
|
|
a_expr = self.a_expr()
|
|
v = BinaryOp(operator.gt, v, a_expr)
|
|
elif _token_ == 'LE':
|
|
LE = self._scan('LE')
|
|
a_expr = self.a_expr()
|
|
v = BinaryOp(operator.le, v, a_expr)
|
|
elif _token_ == 'GE':
|
|
GE = self._scan('GE')
|
|
a_expr = self.a_expr()
|
|
v = BinaryOp(operator.ge, v, a_expr)
|
|
elif _token_ == 'EQ':
|
|
EQ = self._scan('EQ')
|
|
a_expr = self.a_expr()
|
|
v = BinaryOp(operator.eq, v, a_expr)
|
|
else: # == 'NE'
|
|
NE = self._scan('NE')
|
|
a_expr = self.a_expr()
|
|
v = BinaryOp(operator.ne, v, a_expr)
|
|
return v
|
|
|
|
def a_expr(self):
|
|
m_expr = self.m_expr()
|
|
v = m_expr
|
|
while self._peek(self.a_expr_rsts) in self.a_expr_chks:
|
|
_token_ = self._peek(self.a_expr_chks)
|
|
if _token_ == 'ADD':
|
|
ADD = self._scan('ADD')
|
|
m_expr = self.m_expr()
|
|
v = BinaryOp(operator.add, v, m_expr)
|
|
else: # == 'SUB'
|
|
SUB = self._scan('SUB')
|
|
m_expr = self.m_expr()
|
|
v = BinaryOp(operator.sub, v, m_expr)
|
|
return v
|
|
|
|
def m_expr(self):
|
|
u_expr = self.u_expr()
|
|
v = u_expr
|
|
while self._peek(self.m_expr_rsts) in self.m_expr_chks:
|
|
_token_ = self._peek(self.m_expr_chks)
|
|
if _token_ == 'MUL':
|
|
MUL = self._scan('MUL')
|
|
u_expr = self.u_expr()
|
|
v = BinaryOp(operator.mul, v, u_expr)
|
|
else: # == 'DIV'
|
|
DIV = self._scan('DIV')
|
|
u_expr = self.u_expr()
|
|
v = BinaryOp(operator.truediv, v, u_expr)
|
|
return v
|
|
|
|
def u_expr(self):
|
|
_token_ = self._peek(self.u_expr_rsts)
|
|
if _token_ == 'SIGN':
|
|
SIGN = self._scan('SIGN')
|
|
u_expr = self.u_expr()
|
|
return UnaryOp(operator.neg, u_expr)
|
|
elif _token_ == 'ADD':
|
|
ADD = self._scan('ADD')
|
|
u_expr = self.u_expr()
|
|
return UnaryOp(operator.pos, u_expr)
|
|
else: # in self.u_expr_chks
|
|
atom = self.atom()
|
|
return atom
|
|
|
|
def atom(self):
|
|
_token_ = self._peek(self.u_expr_chks)
|
|
if _token_ == 'LPAR':
|
|
LPAR = self._scan('LPAR')
|
|
_token_ = self._peek(self.atom_rsts)
|
|
if _token_ not in self.argspec_item_chks:
|
|
expr_map = self.expr_map()
|
|
v = expr_map
|
|
else: # in self.argspec_item_chks
|
|
expr_lst = self.expr_lst()
|
|
v = expr_lst
|
|
RPAR = self._scan('RPAR')
|
|
return Parentheses(v)
|
|
elif _token_ == 'FNCT':
|
|
FNCT = self._scan('FNCT')
|
|
LPAR = self._scan('LPAR')
|
|
argspec = self.argspec()
|
|
RPAR = self._scan('RPAR')
|
|
return CallOp(FNCT, argspec)
|
|
elif _token_ == 'BANG_IMPORTANT':
|
|
BANG_IMPORTANT = self._scan('BANG_IMPORTANT')
|
|
return Literal(String(BANG_IMPORTANT, quotes=None))
|
|
elif _token_ == 'ID':
|
|
ID = self._scan('ID')
|
|
return Literal(parse_bareword(ID))
|
|
elif _token_ == 'NUM':
|
|
NUM = self._scan('NUM')
|
|
UNITS = None
|
|
if self._peek(self.atom_rsts_) == 'UNITS':
|
|
UNITS = self._scan('UNITS')
|
|
return Literal(Number(float(NUM), unit=UNITS))
|
|
elif _token_ == 'STR':
|
|
STR = self._scan('STR')
|
|
return Literal(String(STR[1:-1], quotes="'"))
|
|
elif _token_ == 'QSTR':
|
|
QSTR = self._scan('QSTR')
|
|
return Literal(String(QSTR[1:-1], quotes='"'))
|
|
elif _token_ == 'COLOR':
|
|
COLOR = self._scan('COLOR')
|
|
return Literal(Color.from_hex(COLOR, literal=True))
|
|
else: # == 'VAR'
|
|
VAR = self._scan('VAR')
|
|
return Variable(VAR)
|
|
|
|
def kwatom(self):
|
|
_token_ = self._peek(self.kwatom_rsts)
|
|
if _token_ == '":"':
|
|
pass
|
|
elif _token_ == 'KWID':
|
|
KWID = self._scan('KWID')
|
|
return Literal(parse_bareword(KWID))
|
|
elif _token_ == 'KWNUM':
|
|
KWNUM = self._scan('KWNUM')
|
|
UNITS = None
|
|
if self._peek(self.kwatom_rsts_) == 'UNITS':
|
|
UNITS = self._scan('UNITS')
|
|
return Literal(Number(float(KWNUM), unit=UNITS))
|
|
elif _token_ == 'KWSTR':
|
|
KWSTR = self._scan('KWSTR')
|
|
return Literal(String(KWSTR[1:-1], quotes="'"))
|
|
elif _token_ == 'KWQSTR':
|
|
KWQSTR = self._scan('KWQSTR')
|
|
return Literal(String(KWQSTR[1:-1], quotes='"'))
|
|
elif _token_ == 'KWCOLOR':
|
|
KWCOLOR = self._scan('KWCOLOR')
|
|
return Literal(Color.from_hex(COLOR, literal=True))
|
|
else: # == 'KWVAR'
|
|
KWVAR = self._scan('KWVAR')
|
|
return Variable(KWVAR)
|
|
|
|
u_expr_chks = set(['LPAR', 'COLOR', 'QSTR', 'NUM', 'FNCT', 'STR', 'VAR', 'BANG_IMPORTANT', 'ID'])
|
|
m_expr_rsts = set(['LPAR', 'SUB', 'QSTR', 'RPAR', 'MUL', 'DIV', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'STR', 'VAR', 'EQ', 'ID', 'AND', 'ADD', 'NOT', 'OR', '","'])
|
|
argspec_items_rsts = set(['RPAR', 'END', '","'])
|
|
expr_map_rsts = set(['RPAR', '","'])
|
|
argspec_items_rsts__ = set(['KWVAR', 'LPAR', 'QSTR', 'SLURPYVAR', 'COLOR', 'DOTDOTDOT', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'BANG_IMPORTANT', 'ID'])
|
|
kwatom_rsts = set(['KWVAR', 'KWID', 'KWSTR', 'KWQSTR', 'KWCOLOR', '":"', 'KWNUM'])
|
|
argspec_item_chks = set(['LPAR', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'BANG_IMPORTANT', 'ID'])
|
|
a_expr_chks = set(['ADD', 'SUB'])
|
|
expr_slst_rsts = set(['LPAR', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'STR', 'NOT', 'BANG_IMPORTANT', 'ID', '","'])
|
|
or_expr_rsts = set(['LPAR', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'STR', 'NOT', 'ID', 'BANG_IMPORTANT', 'OR', '","'])
|
|
and_expr_rsts = set(['AND', 'LPAR', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'STR', 'NOT', 'ID', 'BANG_IMPORTANT', 'OR', '","'])
|
|
comparison_rsts = set(['LPAR', 'QSTR', 'RPAR', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'GT', 'END', 'SIGN', 'ADD', 'FNCT', 'STR', 'VAR', 'EQ', 'ID', 'AND', 'GE', 'NOT', 'OR', '","'])
|
|
argspec_chks = set(['DOTDOTDOT', 'SLURPYVAR'])
|
|
atom_rsts_ = set(['LPAR', 'SUB', 'QSTR', 'RPAR', 'VAR', 'MUL', 'DIV', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'STR', 'UNITS', 'EQ', 'ID', 'AND', 'ADD', 'NOT', 'OR', '","'])
|
|
expr_map_rsts_ = set(['KWVAR', 'KWID', 'KWSTR', 'KWQSTR', 'RPAR', 'KWCOLOR', '":"', 'KWNUM', '","'])
|
|
u_expr_rsts = set(['LPAR', 'COLOR', 'QSTR', 'SIGN', 'ADD', 'NUM', 'FNCT', 'STR', 'VAR', 'BANG_IMPORTANT', 'ID'])
|
|
comparison_chks = set(['GT', 'GE', 'NE', 'LT', 'LE', 'EQ'])
|
|
argspec_items_rsts_ = set(['KWVAR', 'LPAR', 'QSTR', 'END', 'SLURPYVAR', 'COLOR', 'DOTDOTDOT', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'STR', 'NOT', 'BANG_IMPORTANT', 'ID'])
|
|
a_expr_rsts = set(['LPAR', 'SUB', 'QSTR', 'RPAR', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'STR', 'VAR', 'EQ', 'ID', 'AND', 'ADD', 'NOT', 'OR', '","'])
|
|
m_expr_chks = set(['MUL', 'DIV'])
|
|
kwatom_rsts_ = set(['UNITS', '":"'])
|
|
argspec_items_chks = set(['KWVAR', 'LPAR', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'BANG_IMPORTANT', 'ID'])
|
|
argspec_rsts = set(['KWVAR', 'LPAR', 'BANG_IMPORTANT', 'END', 'SLURPYVAR', 'COLOR', 'DOTDOTDOT', 'RPAR', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'QSTR', 'SIGN', 'ID'])
|
|
atom_rsts = set(['KWVAR', 'KWID', 'KWSTR', 'BANG_IMPORTANT', 'LPAR', 'COLOR', 'KWQSTR', 'SIGN', 'KWCOLOR', 'VAR', 'ADD', 'NUM', '":"', 'STR', 'NOT', 'QSTR', 'KWNUM', 'ID', 'FNCT'])
|
|
argspec_chks_ = set(['END', 'RPAR'])
|
|
argspec_rsts_ = set(['KWVAR', 'LPAR', 'BANG_IMPORTANT', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'RPAR', 'ID'])
|
|
|
|
|
|
### Grammar ends.
|
|
################################################################################
|
|
|
|
__all__ = ('Calculator',)
|