diff options
author | Eevee (Alex Munroe) <eevee.git@veekun.com> | 2013-07-29 18:12:22 -0700 |
---|---|---|
committer | Eevee (Alex Munroe) <eevee.git@veekun.com> | 2013-07-29 18:12:22 -0700 |
commit | 50d6599e070da763f616882aaa7d0048a36512f8 (patch) | |
tree | 1abe6ae58ee08674f86524980e6f19d64b4023ff | |
parent | f87ceea3d5398c29c0b29ee2ddf3afd42b89954a (diff) | |
download | pyscss-50d6599e070da763f616882aaa7d0048a36512f8.tar.gz |
Leave / alone when (probably) not part of an expression.
-rw-r--r-- | scss/expression.py | 71 | ||||
-rw-r--r-- | scss/src/grammar/grammar.g | 2 | ||||
-rw-r--r-- | scss/src/grammar/grammar.py | 2 | ||||
-rw-r--r-- | scss/tests/files/bugs/008-division-vs-literal-slash.css | 15 | ||||
-rw-r--r-- | scss/tests/files/bugs/008-division-vs-literal-slash.scss | 29 |
5 files changed, 98 insertions, 21 deletions
diff --git a/scss/expression.py b/scss/expression.py index f2b7847..9176356 100644 --- a/scss/expression.py +++ b/scss/expression.py @@ -162,16 +162,34 @@ class Expression(object): def __repr__(self): return repr(self.__dict__) - def evaluate(self, calculator): + 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(object): + """An expression of the form `(foo)`. + + Only exists to force a slash to be interpreted as division when contained + within parentheses. + """ + def __init__(self, contents): + self.contents = contents + + def evaluate(self, calculator, divide=False): + return self.contents.evaluate(calculator, divide=True) + class UnaryOp(Expression): def __init__(self, op, operand): self.op = op self.operand = operand - def evaluate(self, calculator): - return self.op(self.operand.evaluate(calculator)) + def evaluate(self, calculator, divide=False): + return self.op(self.operand.evaluate(calculator, divide=True)) class BinaryOp(Expression): def __init__(self, op, left, right): @@ -179,40 +197,55 @@ class BinaryOp(Expression): self.left = left self.right = right - def evaluate(self, calculator): - left = self.left.evaluate(calculator) - right = self.right.evaluate(calculator) + 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.div + 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 __init__(self, *operands): self.operands = operands - def evaluate(self, calculator): - operands = [operand.evaluate(calculator) for operand in self.operands] + def evaluate(self, calculator, divide=False): + operands = [operand.evaluate(calculator, divide=True) for operand in self.operands] return any(operands) class AllOp(Expression): def __init__(self, *operands): self.operands = operands - def evaluate(self, calculator): - operands = [operand.evaluate(calculator) for operand in self.operands] + def evaluate(self, calculator, divide=False): + operands = [operand.evaluate(calculator, divide=True) for operand in self.operands] return all(operands) class NotOp(Expression): def __init__(self, operand): self.operand = operand - def evaluate(self, calculator): - return not(self.operand.evaluate(calculator)) + def evaluate(self, calculator, divide=False): + return not(self.operand.evaluate(calculator, divide=True)) class CallOp(Expression): def __init__(self, func_name, argspec): self.func_name = func_name self.argspec = argspec - def evaluate(self, calculator): + def evaluate(self, calculator, divide=False): # TODO bake this into the context and options "dicts", plus library name = normalize_var(self.func_name) @@ -223,7 +256,7 @@ class CallOp(Expression): kwargs = {} evald_argpairs = [] for var, expr in self.argspec.argpairs: - value = expr.evaluate(calculator) + value = expr.evaluate(calculator, divide=True) evald_argpairs.append((var, value)) if var is None: @@ -263,14 +296,14 @@ class Literal(Expression): def __init__(self, value): self.value = value - def evaluate(self, calculator): + def evaluate(self, calculator, divide=False): return self.value class Variable(Expression): def __init__(self, name): self.name = name - def evaluate(self, calculator): + def evaluate(self, calculator, divide=False): try: value = calculator.namespace.variable(self.name) except KeyError: @@ -290,8 +323,8 @@ class ListLiteral(Expression): self.items = items self.comma = comma - def evaluate(self, calculator): - items = [item.evaluate(calculator) for item in self.items] + def evaluate(self, calculator, divide=False): + items = [item.evaluate(calculator, divide=divide) for item in self.items] return ListValue(items, separator="," if self.comma else "") class ArgspecLiteral(Expression): @@ -509,7 +542,7 @@ class SassExpression(Parser): LPAR = self._scan('LPAR') expr_lst = self.expr_lst() RPAR = self._scan('RPAR') - return expr_lst + return Parentheses(expr_lst) elif _token_ == 'ID': ID = self._scan('ID') return Literal(parse_bareword(ID)) diff --git a/scss/src/grammar/grammar.g b/scss/src/grammar/grammar.g index 1ab9973..79d3742 100644 --- a/scss/src/grammar/grammar.g +++ b/scss/src/grammar/grammar.g @@ -76,7 +76,7 @@ parser SassExpression: | ADD u_expr {{ return UnaryOp(operator.pos, u_expr) }} | atom {{ return atom }} - rule atom: LPAR expr_lst RPAR {{ return expr_lst }} + rule atom: LPAR expr_lst RPAR {{ return Parentheses(expr_lst) }} | ID {{ return Literal(parse_bareword(ID)) }} | FNCT {{ v = ArgspecLiteral([]) }} LPAR [ diff --git a/scss/src/grammar/grammar.py b/scss/src/grammar/grammar.py index 0e707c5..1600d23 100644 --- a/scss/src/grammar/grammar.py +++ b/scss/src/grammar/grammar.py @@ -169,7 +169,7 @@ class SassExpression(Parser): LPAR = self._scan('LPAR') expr_lst = self.expr_lst() RPAR = self._scan('RPAR') - return expr_lst + return Parentheses(expr_lst) elif _token_ == 'ID': ID = self._scan('ID') return Literal(parse_bareword(ID)) diff --git a/scss/tests/files/bugs/008-division-vs-literal-slash.css b/scss/tests/files/bugs/008-division-vs-literal-slash.css new file mode 100644 index 0000000..0ecdebc --- /dev/null +++ b/scss/tests/files/bugs/008-division-vs-literal-slash.css @@ -0,0 +1,15 @@ +h1 { + font: 3px / 0; +} +h2 { + border-radius: 10px 9px 8px 7px / 6px 5px 4px 3px; +} +h3 { + line-height: 2; +} +h4 { + line-height: 2; +} +h5 { + line-height: 2; +} diff --git a/scss/tests/files/bugs/008-division-vs-literal-slash.scss b/scss/tests/files/bugs/008-division-vs-literal-slash.scss new file mode 100644 index 0000000..5584a7e --- /dev/null +++ b/scss/tests/files/bugs/008-division-vs-literal-slash.scss @@ -0,0 +1,29 @@ +// http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#division-and-slash + +// "by default, if two numbers are separated by / in SassScript, then they will +// appear that way in the resulting CSS." +h1 { + font: 3px/0; +} + +h2 { + border-radius: 10px 9px 8px 7px / 6px 5px 4px 3px; +} + +// "However, there are three situations where the / will be interpreted as +// division." +// 1. If the value, or any part of it, is stored in a variable. +$denom: 5; +h3 { + line-height: 10 / $denom; +} + +// 2. If the value is surrounded by parentheses. +h4 { + line-height: (10 / 5); +} + +// 3. If the value is used as part of another arithmetic expression. +h5 { + line-height: 10 / 5 + 0; +} |