summaryrefslogtreecommitdiff
path: root/scss/tests/test_expression.py
blob: 33adfe2def58398c25f32031d68d13df89f75499 (plain)
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