summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEevee (Alex Munroe) <eevee.git@veekun.com>2014-09-04 14:35:03 -0700
committerEevee (Alex Munroe) <eevee.git@veekun.com>2014-09-04 14:35:03 -0700
commite2a5d7c95d7f9dcbb3c79bb1d6eed43bded8d667 (patch)
treebe4dd9ebb468f2af7cbeb8c6523cd804517e2ae4
parent493a349952b18332da31cb7828e4d78124b7f236 (diff)
downloadpyscss-e2a5d7c95d7f9dcbb3c79bb1d6eed43bded8d667.tar.gz
Lots of fixes to gritty details of how Sass treats url().
Basically: if it looks like it's a single bare URL, only interpolations are allowed. Otherwise, it's expected to be a whole expression. Handling of interpolation in barewords is much improved. Also fixed the wrapping of SassSyntaxError.
-rw-r--r--scss/ast.py38
-rw-r--r--scss/errors.py4
-rw-r--r--scss/grammar/expression.g82
-rw-r--r--scss/grammar/expression.py76
-rw-r--r--scss/tests/test_expression.py86
-rwxr-xr-xyapps2.py8
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
diff --git a/yapps2.py b/yapps2.py
index e94a7b2..a732c35 100755
--- a/yapps2.py
+++ b/yapps2.py
@@ -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]