summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEevee (Alex Munroe) <eevee.git@veekun.com>2013-07-29 18:12:22 -0700
committerEevee (Alex Munroe) <eevee.git@veekun.com>2013-07-29 18:12:22 -0700
commit50d6599e070da763f616882aaa7d0048a36512f8 (patch)
tree1abe6ae58ee08674f86524980e6f19d64b4023ff
parentf87ceea3d5398c29c0b29ee2ddf3afd42b89954a (diff)
downloadpyscss-50d6599e070da763f616882aaa7d0048a36512f8.tar.gz
Leave / alone when (probably) not part of an expression.
-rw-r--r--scss/expression.py71
-rw-r--r--scss/src/grammar/grammar.g2
-rw-r--r--scss/src/grammar/grammar.py2
-rw-r--r--scss/tests/files/bugs/008-division-vs-literal-slash.css15
-rw-r--r--scss/tests/files/bugs/008-division-vs-literal-slash.scss29
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;
+}