diff options
-rw-r--r-- | scss/ast.py | 38 | ||||
-rw-r--r-- | scss/errors.py | 4 | ||||
-rw-r--r-- | scss/grammar/expression.g | 82 | ||||
-rw-r--r-- | scss/grammar/expression.py | 76 | ||||
-rw-r--r-- | scss/tests/test_expression.py | 86 | ||||
-rwxr-xr-x | yapps2.py | 8 |
6 files changed, 235 insertions, 59 deletions
diff --git a/scss/ast.py b/scss/ast.py index 06de23a..3ccd10c 100644 --- a/scss/ast.py +++ b/scss/ast.py @@ -78,6 +78,20 @@ class UnaryOp(Expression): class BinaryOp(Expression): + OPERATORS = { + operator.lt: '<', + operator.gt: '>', + operator.le: '<=', + operator.ge: '>=', + operator.eq: '==', + operator.eq: '!=', + operator.add: '+', + operator.sub: '-', + operator.mul: '*', + operator.truediv: '/', + operator.mod: '%', + } + def __repr__(self): return '<%s(%s, %s, %s)>' % (self.__class__.__name__, repr(self.op), repr(self.left), repr(self.right)) @@ -90,19 +104,35 @@ class BinaryOp(Expression): left = self.left.evaluate(calculator, divide=True) right = self.right.evaluate(calculator, divide=True) + # Determine whether to actually evaluate, or just print the operator + # literally. + literal = False + + # If either operand starts with an interpolation, treat the whole + # shebang as literal. + if any(isinstance(operand, Interpolation) and operand.parts[0] == '' + for operand in (self.left, self.right)): + literal = 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. + # operands are literals, there are no parentheses, and this isn't 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 ( + elif ( 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) + literal = True + + if literal: + # TODO we don't currently preserve the spacing, whereas Sass + # remembers whether there was space on either side + op = " {0} ".format(self.OPERATORS[self.op]) + return String.unquoted(left.render() + op + right.render()) return self.op(left, right) diff --git a/scss/errors.py b/scss/errors.py index 8ab5652..65f5350 100644 --- a/scss/errors.py +++ b/scss/errors.py @@ -112,11 +112,15 @@ class SassSyntaxError(SassBaseError): usually caught and wrapped later on. """ def __init__(self, input_string, position, desired_tokens): + super(SassSyntaxError, self).__init__() + self.input_string = input_string self.position = position self.desired_tokens = desired_tokens def __str__(self): + # TODO this doesn't show the rule stack; should inherit from SassError + # instead? if self.position == 0: after = "Syntax error" else: diff --git a/scss/grammar/expression.g b/scss/grammar/expression.g index fdab97c..d2fc806 100644 --- a/scss/grammar/expression.g +++ b/scss/grammar/expression.g @@ -45,6 +45,14 @@ parser SassExpression: token INTERP_ANYTHING: "([^#]|#(?![{]))*" token INTERP_NO_PARENS: "([^#()]|#(?![{]))*" + # This is a stupid lookahead used for diverting url(#{...}) to its own + # branch; otherwise it would collide with the atom rule. + token INTERP_START_URL_HACK: "(?=[#][{])" + token INTERP_START: "#[{]" + + token SPACE: "[ \r\t\n]+" + + # Now we can list the ignore token and everything else. ignore: "[ \r\t\n]+" token LPAR: "\\(|\\[" token RPAR: "\\)|\\]" @@ -68,12 +76,21 @@ parser SassExpression: token SINGLE_QUOTE: "'" token DOUBLE_QUOTE: '"' + # Must appear before BAREWORD, so url(foo) parses as a URL + # http://dev.w3.org/csswg/css-syntax-3/#consume-a-url-token0 + # Bare URLs may not contain quotes, parentheses, unprintables, or space. + # TODO reify escapes, for this and for strings + # FIXME: Also, URLs may not contain $ as it breaks urls with variables? + token BAREURL_HEAD_HACK: "((?:[\\\\].|[^#$'\"()\\x00-\\x08\\x0b\\x0e-\\x20\\x7f]|#(?![{]))+)(?=#[{]|\s*[)])" + token BAREURL: "(?:[\\\\].|[^#$'\"()\\x00-\\x08\\x0b\\x0e-\\x20\\x7f]|#(?![{]))+" + token UNITS: "(?<!\s)(?:[a-zA-Z]+|%)(?![-\w])" token NUM: "(?:\d+(?:\.\d*)?|\.\d+)" 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_]+" + # Cheating, to make sure these only match function names. # The last of these is the IE filter nonsense token LITERAL_FUNCTION: "(calc|expression|progid:[\w.]+)(?=[(])" @@ -81,18 +98,12 @@ parser SassExpression: token URL_FUNCTION: "url(?=[(])" # This must come AFTER the above token FNCT: "[-a-zA-Z_][-a-zA-Z0-9_]*(?=\()" + # TODO Ruby is a bit more flexible here, for example allowing 1#{2}px token BAREWORD: "[-a-zA-Z_][-a-zA-Z0-9_]*" token BANG_IMPORTANT: "!important" - token INTERP_START: "#[{]" token INTERP_END: "[}]" - # http://dev.w3.org/csswg/css-syntax-3/#consume-a-url-token0 - # Bare URLs may not contain quotes, parentheses, or unprintables. Quoted - # URLs may, of course, contain whatever they like. - # TODO reify escapes, for this and for strings - # FIXME: Also, URLs may not contain $ as it breaks urls with variables? - token BAREURL: "(?:[\\\\].|[^#$'\"()\\x00-\\x08\\x0b\\x0e-\\x1f\\x7f]|#(?![{]))*" # ------------------------------------------------------------------------- # Goals: @@ -252,19 +263,42 @@ parser SassExpression: INTERP_END {{ return expr_lst }} rule interpolated_url: - # Note: This rule DOES NOT include the url(...) delimiters + # Note: This rule DOES NOT include the url(...) delimiters. + # Parsing a URL is finnicky: it can wrap an expression like any other + # function call, OR it can wrap a literal URL (like regular ol' CSS + # syntax) but possibly with Sass interpolations. + # The string forms of url(), of course, are just special cases of + # expressions. + # The exact rules aren't documented, but after some experimentation, I + # think Sass assumes a literal if it sees either #{} or the end of the + # call before it sees a space, and an expression otherwise. + # Translating that into LL is tricky. We can't look for + # interpolations, because interpolations are also expressions, and + # left-factoring this would be a nightmare. So instead the first rule + # has some wacky lookahead tokens; see below. interpolated_bare_url {{ return Interpolation.maybe(interpolated_bare_url, type=Url, quotes=None) }} - | interpolated_string_single - {{ return Interpolation.maybe(interpolated_string_single, type=Url, quotes="'") }} - | interpolated_string_double - {{ return Interpolation.maybe(interpolated_string_double, type=Url, quotes='"') }} + | expr_lst + {{ return Interpolation(['', expr_lst], type=Url, quotes=None) }} rule interpolated_bare_url: - BAREURL {{ parts = [BAREURL] }} + ( + # This token is identical to BASEURL, except that it ends with a + # lookahead asserting that the next thing is either an + # interpolation, OR optional whitespace and a closing paren. + BAREURL_HEAD_HACK {{ parts = [BAREURL_HEAD_HACK] }} + # And this token merely checks that an interpolation comes next -- + # because if it does, we want the grammar to come down THIS path + # rather than going down expr_lst and into atom (which also looks + # for INTERP_START). + | INTERP_START_URL_HACK {{ parts = [''] }} + ) ( interpolation {{ parts.append(interpolation) }} - BAREURL {{ parts.append(BAREURL) }} + ( BAREURL {{ parts.append(BAREURL) }} + | SPACE {{ return parts }} + | {{ parts.append('') }} + ) )* {{ return parts }} rule interpolated_string: @@ -292,18 +326,28 @@ parser SassExpression: DOUBLE_QUOTE {{ return parts }} rule interpolated_bareword: - # This one is slightly fiddly because it can't be /completely/ empty. + # This one is slightly fiddly because it can't be /completely/ empty, + # and any space between tokens ends the bareword (via early return). + # TODO yapps2 is spitting out warnings for the BAREWORD shenanigans, + # because it's technically ambiguous with a spaced list of barewords -- + # but SPACE will match first in practice and yapps2 doesn't know that ( BAREWORD {{ parts = [BAREWORD] }} + [ SPACE {{ return parts }} + ] | interpolation {{ parts = ['', interpolation] }} - {{ BAREWORD = '' }} - [ BAREWORD ] {{ parts.append(BAREWORD) }} + ( BAREWORD {{ parts.append(BAREWORD) }} + | SPACE {{ return parts }} + | {{ parts.append('') }} + ) ) ( interpolation {{ parts.append(interpolation) }} - {{ BAREWORD = '' }} - [ BAREWORD ] {{ parts.append(BAREWORD) }} + ( BAREWORD {{ parts.append(BAREWORD) }} + | SPACE {{ return parts }} + | {{ parts.append('') }} + ) )* {{ return parts }} rule interpolated_function: diff --git a/scss/grammar/expression.py b/scss/grammar/expression.py index 2b58738..d79173f 100644 --- a/scss/grammar/expression.py +++ b/scss/grammar/expression.py @@ -47,6 +47,9 @@ class SassExpressionScanner(Scanner): ('DOUBLE_STRING_GUTS', '([^"\\\\#]|[\\\\].|#(?![{]))*'), ('INTERP_ANYTHING', '([^#]|#(?![{]))*'), ('INTERP_NO_PARENS', '([^#()]|#(?![{]))*'), + ('INTERP_START_URL_HACK', '(?=[#][{])'), + ('INTERP_START', '#[{]'), + ('SPACE', '[ \r\t\n]+'), ('[ \r\t\n]+', '[ \r\t\n]+'), ('LPAR', '\\(|\\['), ('RPAR', '\\)|\\]'), @@ -69,6 +72,8 @@ class SassExpressionScanner(Scanner): ('DOTDOTDOT', '[.]{3}'), ('SINGLE_QUOTE', "'"), ('DOUBLE_QUOTE', '"'), + ('BAREURL_HEAD_HACK', '((?:[\\\\].|[^#$\'"()\\x00-\\x08\\x0b\\x0e-\\x20\\x7f]|#(?![{]))+)(?=#[{]|\\s*[)])'), + ('BAREURL', '(?:[\\\\].|[^#$\'"()\\x00-\\x08\\x0b\\x0e-\\x20\\x7f]|#(?![{]))+'), ('UNITS', '(?<!\\s)(?:[a-zA-Z]+|%)(?![-\\w])'), ('NUM', '(?:\\d+(?:\\.\\d*)?|\\.\\d+)'), ('COLOR', '#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])'), @@ -81,9 +86,7 @@ class SassExpressionScanner(Scanner): ('FNCT', '[-a-zA-Z_][-a-zA-Z0-9_]*(?=\\()'), ('BAREWORD', '[-a-zA-Z_][-a-zA-Z0-9_]*'), ('BANG_IMPORTANT', '!important'), - ('INTERP_START', '#[{]'), ('INTERP_END', '[}]'), - ('BAREURL', '(?:[\\\\].|[^#$\'"()\\x00-\\x08\\x0b\\x0e-\\x1f\\x7f]|#(?![{]))*'), ] def __init__(self, input=None): @@ -379,24 +382,33 @@ class SassExpression(Parser): def interpolated_url(self): _token_ = self._peek(self.interpolated_url_rsts) - if _token_ == 'BAREURL': + if _token_ in self.interpolated_url_chks: interpolated_bare_url = self.interpolated_bare_url() return Interpolation.maybe(interpolated_bare_url, type=Url, quotes=None) - elif _token_ == 'SINGLE_QUOTE': - interpolated_string_single = self.interpolated_string_single() - return Interpolation.maybe(interpolated_string_single, type=Url, quotes="'") - else: # == 'DOUBLE_QUOTE' - interpolated_string_double = self.interpolated_string_double() - return Interpolation.maybe(interpolated_string_double, type=Url, quotes='"') + else: # in self.argspec_item_chks + expr_lst = self.expr_lst() + return Interpolation(['', expr_lst], type=Url, quotes=None) def interpolated_bare_url(self): - BAREURL = self._scan('BAREURL') - parts = [BAREURL] + _token_ = self._peek(self.interpolated_url_chks) + if _token_ == 'BAREURL_HEAD_HACK': + BAREURL_HEAD_HACK = self._scan('BAREURL_HEAD_HACK') + parts = [BAREURL_HEAD_HACK] + else: # == 'INTERP_START_URL_HACK' + INTERP_START_URL_HACK = self._scan('INTERP_START_URL_HACK') + parts = [''] while self._peek(self.interpolated_bare_url_rsts) == 'INTERP_START': interpolation = self.interpolation() parts.append(interpolation) - BAREURL = self._scan('BAREURL') - parts.append(BAREURL) + _token_ = self._peek(self.interpolated_bare_url_rsts_) + if _token_ == 'BAREURL': + BAREURL = self._scan('BAREURL') + parts.append(BAREURL) + elif _token_ == 'SPACE': + SPACE = self._scan('SPACE') + return parts + else: # in self.interpolated_bare_url_rsts + parts.append('') return parts def interpolated_string(self): @@ -437,20 +449,33 @@ class SassExpression(Parser): if _token_ == 'BAREWORD': BAREWORD = self._scan('BAREWORD') parts = [BAREWORD] + if self._peek(self.interpolated_bareword_rsts) == 'SPACE': + SPACE = self._scan('SPACE') + return parts else: # == 'INTERP_START' interpolation = self.interpolation() parts = ['', interpolation] - BAREWORD = '' - if self._peek(self.interpolated_bareword_rsts) == 'BAREWORD': + _token_ = self._peek(self.interpolated_bareword_rsts_) + if _token_ == 'BAREWORD': BAREWORD = self._scan('BAREWORD') - parts.append(BAREWORD) - while self._peek(self.interpolated_bareword_rsts_) == 'INTERP_START': + parts.append(BAREWORD) + elif _token_ == 'SPACE': + SPACE = self._scan('SPACE') + return parts + elif 1: + parts.append('') + while self._peek(self.interpolated_bareword_rsts__) == 'INTERP_START': interpolation = self.interpolation() parts.append(interpolation) - BAREWORD = '' - if self._peek(self.interpolated_bareword_rsts) == 'BAREWORD': + _token_ = self._peek(self.interpolated_bareword_rsts_) + if _token_ == 'BAREWORD': BAREWORD = self._scan('BAREWORD') - parts.append(BAREWORD) + parts.append(BAREWORD) + elif _token_ == 'SPACE': + SPACE = self._scan('SPACE') + return parts + elif 1: + parts.append('') return parts def interpolated_function(self): @@ -490,20 +515,21 @@ class SassExpression(Parser): expr_map_or_list_rsts__ = set(['LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'ALPHA_FUNCTION', 'RPAR', 'VAR', 'NUM', 'FNCT', 'LITERAL_FUNCTION', 'BANG_IMPORTANT', 'SINGLE_QUOTE', '","']) u_expr_chks = set(['LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'ALPHA_FUNCTION', 'VAR', 'NUM', 'FNCT', 'LITERAL_FUNCTION', 'BANG_IMPORTANT', 'SINGLE_QUOTE']) m_expr_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) + interpolated_bare_url_rsts_ = set(['RPAR', 'INTERP_START', 'BAREURL', 'SPACE']) argspec_items_rsts = set(['RPAR', 'END', '","']) expr_slst_chks = set(['INTERP_END', 'RPAR', 'END', '":"', '","']) - expr_lst_rsts = set(['INTERP_END', 'END', '","']) + expr_lst_rsts = set(['INTERP_END', 'RPAR', 'END', '","']) expr_map_or_list_rsts = set(['RPAR', '":"', '","']) argspec_item_chks = set(['LPAR', 'DOUBLE_QUOTE', 'VAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE']) a_expr_chks = set(['ADD', 'SUB']) interpolated_function_parens_rsts = set(['LPAR', 'RPAR', 'INTERP_START']) expr_slst_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'VAR', 'END', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'FNCT', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'RPAR', '":"', 'NOT', 'INTERP_END', 'BANG_IMPORTANT', 'SINGLE_QUOTE', '","']) - interpolated_bareword_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'BAREWORD', 'GT', 'END', 'SIGN', 'LITERAL_FUNCTION', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) + interpolated_bareword_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SPACE', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) atom_rsts__ = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'VAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'UNITS', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) or_expr_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', '":"', 'BAREWORD', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'OR', 'NOT', 'SINGLE_QUOTE', '","']) argspec_chks_ = set(['END', 'RPAR']) interpolated_string_single_rsts = set(['SINGLE_QUOTE', 'INTERP_START']) - interpolated_bareword_rsts_ = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) + interpolated_bareword_rsts_ = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'BAREWORD', 'GT', 'END', 'SPACE', 'SIGN', 'LITERAL_FUNCTION', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) and_expr_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', '":"', 'BAREWORD', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'AND', 'OR', 'NOT', 'SINGLE_QUOTE', '","']) comparison_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'ADD', 'FNCT', 'VAR', 'EQ', 'AND', 'GE', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) argspec_chks = set(['DOTDOTDOT', 'SLURPYVAR']) @@ -512,12 +538,14 @@ class SassExpression(Parser): atom_chks__ = set(['COLOR', 'VAR']) expr_map_or_list_rsts_ = set(['RPAR', '","']) u_expr_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'ALPHA_FUNCTION', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'LITERAL_FUNCTION', 'BANG_IMPORTANT', 'SINGLE_QUOTE']) + interpolated_url_chks = set(['INTERP_START_URL_HACK', 'BAREURL_HEAD_HACK']) atom_chks = set(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'END', 'SLURPYVAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'DOTDOTDOT', 'INTERP_START', 'RPAR', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'SIGN', 'SINGLE_QUOTE']) - interpolated_url_rsts = set(['DOUBLE_QUOTE', 'BAREURL', 'SINGLE_QUOTE']) + interpolated_url_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'VAR', 'SINGLE_QUOTE', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'FNCT', 'NOT', 'INTERP_START_URL_HACK', 'BANG_IMPORTANT', 'BAREURL_HEAD_HACK']) comparison_chks = set(['GT', 'GE', 'NE', 'LT', 'LE', 'EQ']) argspec_items_rsts_ = set(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'VAR', 'END', 'SLURPYVAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'DOTDOTDOT', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'RPAR', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE']) a_expr_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) interpolated_string_rsts = set(['DOUBLE_QUOTE', 'SINGLE_QUOTE']) + interpolated_bareword_rsts__ = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) m_expr_chks = set(['MUL', 'DIV']) goal_interpolated_anything_rsts = set(['END', 'INTERP_START']) interpolated_bare_url_rsts = set(['RPAR', 'INTERP_START']) diff --git a/scss/tests/test_expression.py b/scss/tests/test_expression.py index de39b01..a6775cc 100644 --- a/scss/tests/test_expression.py +++ b/scss/tests/test_expression.py @@ -1,14 +1,18 @@ -"""Tests for expressions. This test module is currently a bit ill-defined and -contains a variety of expression-related tests. +"""Tests for expressions -- both their evaluation and their general +parsability. """ from __future__ import absolute_import +from __future__ import division +from __future__ import print_function from __future__ import unicode_literals from scss.errors import SassEvaluationError +from scss.errors import SassSyntaxError from scss.expression import Calculator from scss.extension.core import CoreExtension from scss.rule import Namespace from scss.types import Color, List, Null, Number, String +from scss.types import Function import pytest @@ -71,14 +75,6 @@ def test_reference_operations(): assert_strict_string_eq(calc('"I ate #{$value} pies!"'), String('I ate pies!', quotes='"')) -def test_parse(calc): - # Tests for some general parsing. - - assert calc('foo !important bar') == List([ - String('foo'), String('!important'), String('bar'), - ]) - - def test_functions(calc): calc = Calculator(CoreExtension.namespace).calculate @@ -89,4 +85,74 @@ def test_functions(calc): calc('unitless("X")') # Misusing non-css built-in scss funtions +def test_parse_strings(calc): + # Test edge cases with string parsing. + assert calc('auto\\9') == String.unquoted('auto\\9') + + +def test_parse_bang_important(calc): + # The !important flag is treated as part of a spaced list. + assert calc('40px !important') == List([ + Number(40, 'px'), String.unquoted('!important'), + ], use_comma=False) + + # And is allowed anywhere in the string. + assert calc('foo !important bar') == List([ + String('foo'), String('!important'), String('bar'), + ], use_comma=False) + + # And may have space before the !. + assert calc('40px ! important') == List([ + Number(40, 'px'), String.unquoted('!important'), + ], use_comma=False) + + +def test_parse_special_functions(): + ns = CoreExtension.namespace.derive() + calc = Calculator(ns).calculate + + # expression() allows absolutely any old garbage inside + # TODO we can't deal with an unmatched { due to the block locator, but ruby + # can + for gnarly_expression in ( + "not ~* remotely *~ valid {syntax}", + "expression( ( -0 - floater.offsetHeight + ( document" + ".documentElement.clientHeight ? document.documentElement" + ".clientHeight : document.body.clientHeight ) + ( ignoreMe" + " = document.documentElement.scrollTop ? document" + ".documentElement.scrollTop : document.body.scrollTop ) ) +" + " 'px' )"): + expr = 'expression(' + gnarly_expression + ')' + assert calc(expr).render() == expr + + # alpha() doubles as a special function if it contains opacity=n, the IE + # filter syntax + assert calc('alpha(black)') == Number(1) + assert calc('alpha(opacity = 5)') == Function('opacity=5', 'alpha') + + # url() allows both an opaque URL and a Sass expression, based on some + # heuristics + ns.set_variable('$foo', String.unquoted('foo')) + assert calc('url($foo)').render() == "url(foo)" + assert calc('url(#{$foo}foo)').render() == "url(foofoo)" + assert calc('url($foo + $foo)').render() == "url(foofoo)" + # TODO this one doesn't work if $foo has quotes; Url.render() tries to + # escape them. which i'm not sure is wrong, but we're getting into + # territory where it's obvious bad output... + assert calc('url($foo + #{$foo})').render() == "url(foo + foo)" + assert calc('url(foo #{$foo} foo)').render() == "url(foo foo foo)" + with pytest.raises(SassSyntaxError): + # Starting with #{} means it's a url, which can't contain spaces + calc('url(#{$foo} foo)') + with pytest.raises(SassSyntaxError): + # Or variables + calc('url(#{$foo}$foo)') + with pytest.raises(SassSyntaxError): + # This looks like a URL too + calc('url(foo#{$foo} foo)') + + # TODO write more! i'm lazy. +# TODO assert things about particular kinds of parse /errors/, too +# TODO errors really need to be more understandable :( i think this requires +# some additions to yapps @@ -747,7 +747,9 @@ class Scanner(object): msg = "Bad Token" if restrict: msg = "Trying to find one of " + ", ".join(restrict) - raise SyntaxError("SyntaxError[@ char %s: %s]" % (repr(self.pos), msg)) + err = SyntaxError("SyntaxError[@ char %s: %s]" % (repr(self.pos), msg)) + err.pos = self.pos + raise err # If we found something that isn't to be ignored, return it if best_pat in self.ignore: @@ -875,7 +877,9 @@ class Parser(object): """ tok = self._scanner.token(self._pos, set([type])) if tok[2] != type: - raise SyntaxError("SyntaxError[@ char %s: %s]" % (repr(tok[0]), "Trying to find " + type)) + err = SyntaxError("SyntaxError[@ char %s: %s]" % (repr(tok[0]), "Trying to find " + type)) + err.pos = tok[0] + raise err self._pos += 1 return tok[3] |