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
|