1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
|
# -*- coding: utf-8 -*-
"""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.calculator import Calculator
from scss.errors import SassEvaluationError
from scss.errors import SassSyntaxError
from scss.extension.core import CoreExtension
from scss.types import Color, List, Null, Number, String
from scss.types import Function
import pytest
@pytest.fixture
def calc():
return Calculator().evaluate_expression
def assert_strict_string_eq(expected, actual):
assert expected.value == actual.value
assert expected.quotes == actual.quotes
def test_reference_operations():
"""Test the example expressions in the reference document:
http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#operations
"""
# TODO: break this into its own file and add the entire reference guide
# Need to build the calculator manually to get at its namespace, and need
# to use calculate() instead of evaluate_expression() so interpolation
# works
ns = CoreExtension.namespace.derive()
calc = Calculator(ns).calculate
# Simple example
assert calc('1in + 8pt') == Number(1.1111111111111112, "in")
# Division
ns.set_variable('$width', Number(1000, "px"))
ns.set_variable('$font-size', Number(12, "px"))
ns.set_variable('$line-height', Number(30, "px"))
assert calc('10px/8px') == String('10px / 8px') # plain CSS; no division
assert calc('$width/2') == Number(500, "px") # uses a variable; does division
assert calc('(500px/2)') == Number(250, "px") # uses parens; does division
assert calc('5px + 8px/2px') == Number(9, "px") # uses +; does division
# TODO, again: Ruby Sass correctly renders this without spaces
assert calc('#{$font-size}/#{$line-height}') == String('12px / 30px')
# uses #{}; does no division
# Modulo
assert calc('29 % 12') == Number(5)
assert calc('29px % 12') == Number(5, 'px')
assert calc('29px % 12px') == Number(5, 'px')
# Color operations
ns.set_variable('$translucent-red', Color.from_rgb(1, 0, 0, 0.5))
ns.set_variable('$green', Color.from_name('lime'))
assert calc('#010203 + #040506') == Color.from_hex('#050709')
assert calc('#010203 * 2') == Color.from_hex('#020406')
assert calc('rgba(255, 0, 0, 0.75) + rgba(0, 255, 0, 0.75)') == Color.from_rgb(1, 1, 0, 0.75)
assert calc('opacify($translucent-red, 0.3)') == Color.from_rgb(1, 0, 0, 0.8)
assert calc('transparentize($translucent-red, 0.25)') == Color.from_rgb(1, 0, 0, 0.25)
assert calc("progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr='#{ie-hex-str($green)}', endColorstr='#{ie-hex-str($translucent-red)}')"
).render() == "progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr='#FF00FF00', endColorstr='#80FF0000')"
# String operations
ns.set_variable('$value', Null())
assert_strict_string_eq(calc('e + -resize'), String('e-resize', quotes=None))
assert_strict_string_eq(calc('"Foo " + Bar'), String('Foo Bar', quotes='"'))
assert_strict_string_eq(calc('sans- + "serif"'), String('sans-serif', quotes=None))
assert calc('3px + 4px auto') == List([Number(7, "px"), String('auto', quotes=None)])
assert_strict_string_eq(calc('"I ate #{5 + 10} pies!"'), String('I ate 15 pies!', quotes='"'))
assert_strict_string_eq(calc('"I ate #{$value} pies!"'), String('I ate pies!', quotes='"'))
def test_functions(calc):
calc = Calculator(CoreExtension.namespace).calculate
assert calc('grayscale(red)') == Color.from_rgb(0.5, 0.5, 0.5)
assert calc('grayscale(1)') == String('grayscale(1)', quotes=None) # Misusing css built-in functions (with scss counterpart)
assert calc('skew(1)') == String('skew(1)', quotes=None) # Missing css-only built-in functions
with pytest.raises(SassEvaluationError):
calc('unitless("X")') # Misusing non-css built-in scss funtions
def test_parse_strings(calc):
# Escapes in barewords are preserved.
assert calc('auto\\9') == String.unquoted('auto\\9')
# Escapes in quoted strings are expanded.
assert calc('"\\2022"') == String("•", quotes='"')
assert calc('"\\2022"').render() == '"•"'
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')
assert calc('alpha(opacity = 5)').render() == 'alpha(opacity=5)'
# 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
|