From 376e51167ce1a6b7f86857c12d56f59047b34e92 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Fri, 23 Aug 2013 19:14:03 -0700 Subject: Parser support for slurpy arguments. --- scss/__init__.py | 20 +++++------ scss/expression.py | 88 +++++++++++++++++++++++++++++++++------------- scss/src/grammar/grammar.g | 33 +++++++++++------ 3 files changed, 96 insertions(+), 45 deletions(-) diff --git a/scss/__init__.py b/scss/__init__.py index 9a696ff..1bf0db7 100644 --- a/scss/__init__.py +++ b/scss/__init__.py @@ -628,16 +628,17 @@ class Scss(object): funct = normalize_var(funct.strip()) argstr = argstr.strip() + # Parse arguments with the argspec rule if lpar: - # Has arguments; parse them with the argspec rule if not argstr.endswith(')'): raise SyntaxError("Expected ')', found end of line for %s (%s)" % (funct, rule.file_and_line)) argstr = argstr[:-1].strip() - argspec_node = calculator.parse_expression(argstr, target='goal_argspec') if argstr else None - # print(argstr, repr(argspec_node)) - return funct, argspec_node + else: + # Whoops, no parens at all. That's like calling with no arguments. + argstr = '' - return funct, None + argspec_node = calculator.parse_expression(argstr, target='goal_argspec') + return funct, argspec_node @print_timing(10) def _do_functions(self, rule, p_children, scope, block): @@ -653,11 +654,10 @@ class Scss(object): defaults = {} new_params = [] - if argspec_node: - for var_name, default in argspec_node.iter_def_argspec(): - new_params.append(var_name) - if default is not None: - defaults[var_name] = default + for var_name, default in argspec_node.iter_def_argspec(): + new_params.append(var_name) + if default is not None: + defaults[var_name] = default mixin = [list(new_params), defaults, block.unparsed_contents, rule.namespace] if block.directive == '@function': diff --git a/scss/expression.py b/scss/expression.py index 91a164e..d4fa107 100644 --- a/scss/expression.py +++ b/scss/expression.py @@ -411,13 +411,13 @@ class ArgspecLiteral(Expression): def __repr__(self): return '<%s(%s)>' % (self.__class__.__name__, repr(self.argpairs)) - def __init__(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). - while argpairs and argpairs[-1] == (None, None): - argpairs = argpairs[:-1] - self.argpairs = tuple((var, Literal(Undefined()) if value is None else value) for var, value in argpairs) + # slurp is the name of a variable to receive slurpy arguments. + self.argpairs = tuple(argpairs) + self.slurp = slurp def iter_list_argspec(self): yield None, ListLiteral(zip(*self.argpairs)[1]) @@ -426,15 +426,30 @@ class ArgspecLiteral(Expression): """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 = Literal(Undefined()) + 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 iter_call_argspec(self): @@ -526,6 +541,7 @@ class SassExpressionScanner(Scanner): ('GE', '>='), ('LT', '<'), ('GT', '>'), + ('DOTDOTDOT', '[.]{3}'), ('KWSTR', "'[^']*'(?=\\s*:)"), ('STR', "'[^']*'"), ('KWQSTR', '"[^"]*"(?=\\s*:)'), @@ -536,6 +552,7 @@ class SassExpressionScanner(Scanner): ('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*:)'), @@ -565,18 +582,38 @@ class SassExpression(Parser): return argspec def argspec(self): + _token_ = self._peek(self.argspec_rsts) + if _token_ != 'SLURPYVAR': + 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([]) + else: # == 'SLURPYVAR' + SLURPYVAR = self._scan('SLURPYVAR') + DOTDOTDOT = self._scan('DOTDOTDOT') + return ArgspecLiteral([], slurp=SLURPYVAR) + + def argspec_items(self): + slurpy = None argspec_item = self.argspec_item() - argpairs = [argspec_item] - while self._peek(self.argspec_rsts) == '","': + args = [argspec_item] + if self._peek(self.argspec_items_rsts) == '","': self._scan('","') - argspec_item = (None, None) - if self._peek(self.argspec_rsts_) not in self.argspec_rsts: - argspec_item = self.argspec_item() - argpairs.append(argspec_item) - return ArgspecLiteral(argpairs) + 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 + 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_item_rsts) + _token_ = self._peek(self.argspec_items_chks) if _token_ == 'KWVAR': KWVAR = self._scan('KWVAR') self._scan('":"') @@ -606,7 +643,7 @@ class SassExpression(Parser): def expr_lst(self): expr_slst = self.expr_slst() v = [expr_slst] - while self._peek(self.argspec_rsts) == '","': + while self._peek(self.argspec_items_rsts) == '","': self._scan('","') expr_slst = self.expr_slst() v.append(expr_slst) @@ -615,7 +652,7 @@ class SassExpression(Parser): def expr_slst(self): or_expr = self.or_expr() v = [or_expr] - while self._peek(self.expr_slst_rsts) not in self.argspec_rsts: + 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] @@ -738,10 +775,8 @@ class SassExpression(Parser): return Parentheses(v) elif _token_ == 'FNCT': FNCT = self._scan('FNCT') - argspec = ArgspecLiteral([]) LPAR = self._scan('LPAR') - if self._peek(self.atom_rsts_) not in self.atom_chks: - argspec = self.argspec() + argspec = self.argspec() RPAR = self._scan('RPAR') return CallOp(FNCT, argspec) elif _token_ == 'BANG_IMPORTANT': @@ -753,7 +788,7 @@ class SassExpression(Parser): elif _token_ == 'NUM': NUM = self._scan('NUM') UNITS = None - if self._peek(self.atom_rsts__) == 'UNITS': + if self._peek(self.atom_rsts_) == 'UNITS': UNITS = self._scan('UNITS') return Literal(Number(float(NUM), unit=UNITS)) elif _token_ == 'STR': @@ -797,27 +832,30 @@ class SassExpression(Parser): 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', 'SLURPYVAR', 'COLOR', 'QSTR', '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', '","']) - 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', '","']) or_expr_rsts = set(['LPAR', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'STR', 'NOT', 'ID', 'BANG_IMPORTANT', 'OR', '","']) - argspec_item_rsts = set(['KWVAR', 'LPAR', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'BANG_IMPORTANT', 'ID']) atom_rsts = set(['KWVAR', 'KWID', 'KWSTR', 'BANG_IMPORTANT', 'LPAR', 'COLOR', 'KWQSTR', 'SIGN', 'KWCOLOR', 'VAR', 'ADD', 'NUM', '":"', 'STR', 'NOT', 'QSTR', 'KWNUM', 'ID', 'FNCT']) 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', '","']) - atom_rsts_ = set(['KWVAR', 'LPAR', 'BANG_IMPORTANT', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'RPAR', 'ID']) + argspec_chks = set(['END', 'RPAR']) + 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']) - atom_chks = set(['END', 'RPAR']) comparison_chks = set(['GT', 'GE', 'NE', 'LT', 'LE', 'EQ']) - 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', '","']) + argspec_items_rsts_ = set(['KWVAR', 'LPAR', 'END', 'SLURPYVAR', 'COLOR', 'QSTR', '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_rsts = set(['RPAR', 'END', '","']) + 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', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'RPAR', 'ID']) and_expr_rsts = set(['AND', 'LPAR', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'STR', 'NOT', 'ID', 'BANG_IMPORTANT', 'OR', '","']) - argspec_rsts_ = set(['KWVAR', 'LPAR', 'BANG_IMPORTANT', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'RPAR', 'ID', '","']) + argspec_rsts_ = set(['KWVAR', 'LPAR', 'BANG_IMPORTANT', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'RPAR', 'ID']) + ### Grammar ends. ################################################################################ diff --git a/scss/src/grammar/grammar.g b/scss/src/grammar/grammar.g index 1da3f46..d6d663e 100644 --- a/scss/src/grammar/grammar.g +++ b/scss/src/grammar/grammar.g @@ -23,6 +23,7 @@ parser SassExpression: token GE: ">=" token LT: "<" token GT: ">" + token DOTDOTDOT: '[.]{3}' token KWSTR: "'[^']*'(?=\s*:)" token STR: "'[^']*'" token KWQSTR: '"[^"]*"(?=\s*:)' @@ -33,6 +34,7 @@ parser SassExpression: token KWCOLOR: "#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])(?=\s*:)" token COLOR: "#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])" token KWVAR: "\$[-a-zA-Z0-9_]+(?=\s*:)" + token SLURPYVAR: "\$[-a-zA-Z0-9_]+(?=[.][.][.])" token VAR: "\$[-a-zA-Z0-9_]+" token FNCT: "[-a-zA-Z_][-a-zA-Z0-9_]*(?=\()" token KWID: "[-a-zA-Z_][-a-zA-Z0-9_]*(?=\s*:)" @@ -45,16 +47,26 @@ parser SassExpression: rule goal_argspec: argspec END {{ return argspec }} # Arguments: - rule argspec: argspec_item {{ argpairs = [argspec_item] }} - ( - "," {{ argspec_item = (None, None) }} - [ argspec_item ] {{ argpairs.append(argspec_item) }} - )* {{ return ArgspecLiteral(argpairs) }} - - rule argspec_item: - KWVAR ":" expr_slst {{ return (Variable(KWVAR), expr_slst) }} + # Note that at the moment, named arguments and slurpy arguments appear to + # be incompatible. + rule argspec: [ + argspec_items {{ args, slurpy = argspec_items }} + {{ return ArgspecLiteral(args, slurp=slurpy) }} + ] {{ return ArgspecLiteral([]) }} + | SLURPYVAR DOTDOTDOT {{ return ArgspecLiteral([], slurp=SLURPYVAR) }} + + rule argspec_items: {{ slurpy = None }} + argspec_item {{ args = [argspec_item] }} + [ "," [ + SLURPYVAR DOTDOTDOT {{ slurpy = SLURPYVAR }} + | argspec_items {{ more_args, slurpy = argspec_items }} + {{ args.extend(more_args) }} + ] ] {{ return args, slurpy }} + + rule argspec_item: KWVAR ":" expr_slst {{ return (Variable(KWVAR), expr_slst) }} | expr_slst {{ return (None, expr_slst) }} + # Maps: rule expr_map: map_item {{ pairs = [map_item] }} ( @@ -64,6 +76,7 @@ parser SassExpression: rule map_item: kwatom ":" expr_slst {{ return (kwatom, expr_slst) }} + # Lists: rule expr_lst: expr_slst {{ v = [expr_slst] }} ( @@ -71,6 +84,7 @@ parser SassExpression: expr_slst {{ v.append(expr_slst) }} )* {{ return ListLiteral(v) if len(v) > 1 else v[0] }} + # Expressions: rule expr_slst: or_expr {{ v = [or_expr] }} ( @@ -120,8 +134,7 @@ parser SassExpression: expr_map {{ v = expr_map }} | expr_lst {{ v = expr_lst }} ) RPAR {{ return Parentheses(v) }} - | FNCT {{ argspec = ArgspecLiteral([]) }} - LPAR [ argspec ] RPAR {{ return CallOp(FNCT, argspec) }} + | FNCT LPAR argspec RPAR {{ return CallOp(FNCT, argspec) }} | BANG_IMPORTANT {{ return Literal(String(BANG_IMPORTANT, quotes=None)) }} | ID {{ return Literal(parse_bareword(ID)) }} | NUM {{ UNITS = None }} -- cgit v1.2.1