def test_comparison_stringerific(): abc = String('abc') xyz = String('xyz') assert abc == abc assert abc != xyz assert not abc == xyz assert not abc != abc # Interaction with other types assert Number(123) != String('123') assert String('123') != Number(123) # Sass strings don't support ordering with pytest.raises(TypeError): abc < xyz with pytest.raises(TypeError): abc <= xyz with pytest.raises(TypeError): abc > xyz with pytest.raises(TypeError): abc >= xyz with pytest.raises(TypeError): Number(123) < String('123')
def kwatom(self): _token_ = self._peek(self.kwatom_rsts) if _token_ == '":"': pass elif _token_ == 'KWID': KWID = self._scan('KWID') return Literal(parse_bareword(KWID)) elif _token_ == 'KWNUM': KWNUM = self._scan('KWNUM') UNITS = None if self._peek(self.kwatom_rsts_) == 'UNITS': UNITS = self._scan('UNITS') return Literal(Number(float(KWNUM), unit=UNITS)) elif _token_ == 'KWSTR': KWSTR = self._scan('KWSTR') return Literal(String(KWSTR[1:-1], quotes="'")) elif _token_ == 'KWQSTR': KWQSTR = self._scan('KWQSTR') return Literal(String(KWQSTR[1:-1], quotes='"')) elif _token_ == 'KWCOLOR': KWCOLOR = self._scan('KWCOLOR') return Literal(Color.from_hex(COLOR, literal=True)) else: # == 'KWVAR' KWVAR = self._scan('KWVAR') return Variable(KWVAR)
def quote(*args): arg = List.from_maybe_starargs(args).maybe() if isinstance(arg, String): return String(arg.value, quotes='"') else: return String(arg.render(), quotes='"')
def test_quote(): # Examples from the Ruby docs ret = calc('quote("foo")') assert ret == String('foo') assert ret.quotes == '"' ret = calc('quote(foo)') assert ret == String('foo') assert ret.quotes == '"'
def test_parse(calc): # Tests for some general parsing. assert calc('foo !important bar') == List([ String('foo'), String('!important'), String('bar'), ])
def test_unquote(): # Examples from the Ruby docs ret = calc('unquote("foo")') assert ret == String('foo') assert ret.quotes is None ret = calc('unquote(foo)') assert ret == String('foo') assert ret.quotes is None assert calc('unquote((one, two, three))') == String('one, two, three')
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 join_file_segments(*segments): """Join path parts into a single path, using the appropriate OS-specific delimiter. """ parts = [] for segment in segments: expect_type(segment, String) parts.append(segment.value) if parts: return String(os.path.join(*parts)) else: return String('')
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 # 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_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 style_scss(self, *path): css_namespace = Namespace() for key, value in self.settings['keys'].items(): if isinstance(value, LCText): css_value = String(value) elif isinstance(value, LCColour): css_value = Color.from_hex(value) elif isinstance(value, LCBool): css_value = Boolean(value.simple()) elif isinstance(value, LCSpin): css_value = Number(value.simple()) else: raise ValueError("Unable to find comparable values") css_namespace.set_variable('${}'.format(key), css_value) cherrypy.response.headers['Content-Type'] = 'text/css' with open(os.path.join(self.settings['location'], *path), 'r') as css: css_content = css.read() compiler = Compiler(namespace=css_namespace, output_style='nested') # Something wrong with PyScss, # Syntax error: Found u'100%' but expected one of ADD. # Doesn't happen on next attempt, so we are doing bad thing attempts = 0 while attempts < 100: try: attempts += 1 ret_string = compiler.compile_string(css_content) return ret_string except Exception as exc: if attempts == 100: log.debug(exc)
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.namespace = Namespace() # Add variables to the SCSS Global Namespace Here self.namespace.set_variable('$static_url', String(settings.STATIC_URL))
def __init__(self, *args, **kwargs): super(HorizonScssFilter, self).__init__(*args, **kwargs) self.namespace = Namespace() # Add variables to the SCSS Global Namespace Here self.namespace.set_variable('$static_url', String(six.text_type(getattr(settings, 'STATIC_URL', '/static/'))))
def absolute_path(relative_path): """Return an absolute path for the given relative path, relative to the calling file. """ expect_type(relative_path, String) # TODO i can't get the calling file until "rule" or something else helpful # is actually passed in! return String(os.path.abspath(relative_path.value))
def ie_hex_str(color): c = Color(color).value return String("#{3:02X}{0:02X}{1:02X}{2:02X}".format( int(round(c[0])), int(round(c[1])), int(round(c[2])), int(round(c[3] * 255)), ))
def styles(request, name): src = '' namespace = Namespace() for tv in ThemeValue.objects.all(): namespace.set_variable('${}-{}'.format(tv.group, tv.name), String(tv.value)) compiler = Compiler(namespace=namespace) return HttpResponse(compiler.compile_string(src), content_type='text/css')
def evaluate(self, calculator, divide=False): # TODO bake this into the context and options "dicts", plus library func_name = normalize_var(self.func_name) argspec_node = self.argspec # Turn the pairs of arg tuples into *args and **kwargs # TODO unclear whether this is correct -- how does arg, kwarg, arg # work? args, kwargs = argspec_node.evaluate_call_args(calculator) argspec_len = len(args) + len(kwargs) # Translate variable names to Python identifiers # TODO what about duplicate kw names? should this happen in argspec? # how does that affect mixins? kwargs = dict((key.lstrip('$').replace('-', '_'), value) for key, value in kwargs.items()) # TODO merge this with the library funct = None try: funct = calculator.namespace.function(func_name, argspec_len) # @functions take a ns as first arg. TODO: Python functions possibly # should too if getattr(funct, '__name__', None) == '__call': funct = partial(funct, calculator.namespace) except KeyError: try: # DEVIATION: Fall back to single parameter funct = calculator.namespace.function(func_name, 1) args = [List(args, use_comma=True)] except KeyError: if not is_builtin_css_function(func_name): log.error("Function not found: %s:%s", func_name, argspec_len, extra={'stack': True}) if funct: ret = funct(*args, **kwargs) if not isinstance(ret, Value): raise TypeError("Expected Sass type as return value, got %r" % (ret, )) return ret # No matching function found, so render the computed values as a CSS # function call. Slurpy arguments are expanded and named arguments are # unsupported. if kwargs: raise TypeError( "The CSS function %s doesn't support keyword arguments." % (func_name, )) # TODO another candidate for a "function call" sass type rendered_args = [arg.render() for arg in args] return String(u"%s(%s)" % (func_name, u", ".join(rendered_args)), quotes=None)
def test_linear_gradient(): # Set up some values to = String.unquoted('to') bottom = String.unquoted('bottom') left = String.unquoted('left') red = Color.from_name('red') blue = Color.from_name('blue') start = Number(0, "%") middle = Number(50, "%") end = Number(100, "%") assert (linear_gradient(left, List((red, start)), List( (blue, middle))) == String('linear-gradient(left, red, blue 50%)')) assert (linear_gradient(List((to, bottom)), blue, List( (red, end))) == String('linear-gradient(to bottom, blue, red)'))
def str_insert(string, insert, index): expect_type(string, String) expect_type(insert, String) expect_type(index, Number, unit=None) py_index = index.to_python_index(len(string.value), check_bounds=False) return String( string.value[:py_index] + insert.value + string.value[py_index:], quotes=string.quotes)
def atom(self): _token_ = self._peek(self.u_expr_chks) if _token_ == 'LPAR': LPAR = self._scan('LPAR') _token_ = self._peek(self.atom_rsts) if _token_ not in self.argspec_item_chks: expr_map = self.expr_map() v = expr_map else: # in self.argspec_item_chks expr_lst = self.expr_lst() v = expr_lst RPAR = self._scan('RPAR') return Parentheses(v) elif _token_ == 'FNCT': FNCT = self._scan('FNCT') LPAR = self._scan('LPAR') argspec = self.argspec() RPAR = self._scan('RPAR') return CallOp(FNCT, argspec) elif _token_ == 'BANG_IMPORTANT': BANG_IMPORTANT = self._scan('BANG_IMPORTANT') return Literal(String(BANG_IMPORTANT, quotes=None)) elif _token_ == 'ID': ID = self._scan('ID') return Literal(parse_bareword(ID)) elif _token_ == 'NUM': NUM = self._scan('NUM') UNITS = None if self._peek(self.atom_rsts_) == 'UNITS': UNITS = self._scan('UNITS') return Literal(Number(float(NUM), unit=UNITS)) elif _token_ == 'STR': STR = self._scan('STR') return Literal(String(STR[1:-1], quotes="'")) elif _token_ == 'QSTR': QSTR = self._scan('QSTR') return Literal(String(QSTR[1:-1], quotes='"')) elif _token_ == 'COLOR': COLOR = self._scan('COLOR') return Literal(Color.from_hex(COLOR, literal=True)) else: # == 'VAR' VAR = self._scan('VAR') return Variable(VAR)
def _get_gradient_shape_and_size(args): for arg in args: for seek in ( 'circle', 'ellipse', 'closest-side', 'closest-corner', 'farthest-side', 'farthest-corner', 'contain', 'cover', ): if String(seek) in arg: return arg return None
def atom(self): _token_ = self._peek(self.u_expr_chks) if _token_ == 'LPAR': LPAR = self._scan('LPAR') expr_lst = self.expr_lst() RPAR = self._scan('RPAR') return Parentheses(expr_lst) elif _token_ == 'ID': ID = self._scan('ID') return Literal(parse_bareword(ID)) elif _token_ == 'BANG_IMPORTANT': BANG_IMPORTANT = self._scan('BANG_IMPORTANT') return Literal(String(BANG_IMPORTANT, quotes=None)) elif _token_ == 'FNCT': FNCT = self._scan('FNCT') v = ArgspecLiteral([]) LPAR = self._scan('LPAR') if self._peek(self.atom_rsts) != 'RPAR': argspec = self.argspec() v = argspec RPAR = self._scan('RPAR') return CallOp(FNCT, v) elif _token_ == 'NUM': NUM = self._scan('NUM') if self._peek(self.atom_rsts_) == 'UNITS': UNITS = self._scan('UNITS') return Literal(NumberValue(float(NUM), unit=UNITS.lower())) return Literal(NumberValue(float(NUM))) elif _token_ == 'STR': STR = self._scan('STR') return Literal(String(STR[1:-1], quotes="'")) elif _token_ == 'QSTR': QSTR = self._scan('QSTR') return Literal(String(QSTR[1:-1], quotes='"')) elif _token_ == 'COLOR': COLOR = self._scan('COLOR') return Literal(ColorValue(ParserValue(COLOR))) else: # == 'VAR' VAR = self._scan('VAR') return Variable(VAR)
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 parse_bareword(word): if word in COLOR_NAMES: return Color.from_name(word) elif word == 'null': return Null() elif word == 'undefined': return Undefined() elif word == 'true': return Boolean(True) elif word == 'false': return Boolean(False) else: return String(word, quotes=None)
def str_slice(string, start_at, end_at=None): expect_type(string, String) expect_type(start_at, Number, unit=None) py_start_at = start_at.to_python_index(len(string.value)) if end_at is None: py_end_at = None else: expect_type(end_at, Number, unit=None) # Endpoint is inclusive, unlike Python py_end_at = end_at.to_python_index(len(string.value)) + 1 return String(string.value[py_start_at:py_end_at], quotes=string.quotes)
def glyph_code(sheet, glyph): sheet = sheet.render() font_sheet = font_sheets.get(sheet) glyph_name = String.unquoted(glyph).value glyph = font_sheet and font_sheet.get(glyph_name) if not font_sheet: log.error("No font sheet found: %s", sheet, extra={'stack': True}) elif not glyph: log.error("No glyph found: %s in %s", glyph_name, font_sheet['*n*'], extra={'stack': True}) return String('\\%x' % glyph[1])
def from_bareword(cls, word): if word in COLOR_NAMES: value = Color.from_name(word) elif word == 'null': value = Null() elif word == 'undefined': value = Undefined() elif word == 'true': value = Boolean(True) elif word == 'false': value = Boolean(False) else: value = String(word, quotes=None) return cls(value)
def test_linear_gradient_idempotent(calc): # linear-gradient should leave valid syntax alone. # Examples graciously stolen from MDN: # https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient trials = [ 'linear-gradient(45deg, blue, red)', 'linear-gradient(to left top, blue, red)', 'linear-gradient(0deg, blue, green 40%, red)', 'linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet)', 'linear-gradient(to bottom right, red, rgba(255,0,0,0))', 'linear-gradient(to bottom, hsl(0, 80%, 70%), #bada55)', ] for trial in trials: assert calc(trial) == String(trial)
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.truediv 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)
def evaluate(self, calculator, divide=False): # TODO bake this into the context and options "dicts", plus library func_name = normalize_var(self.func_name) # Turn the pairs of arg tuples into *args and **kwargs # TODO unclear whether this is correct -- how does arg, kwarg, arg # work? args = [] kwargs = {} evald_argpairs = [] for var, expr in self.argspec.argpairs: value = expr.evaluate(calculator, divide=True) evald_argpairs.append((var, value)) if var is None: args.append(value) else: kwargs[var.lstrip('$').replace('-', '_')] = value num_args = len(self.argspec.argpairs) # TODO merge this with the library try: func = calculator.namespace.function(func_name, num_args) # @functions take a ns as first arg. TODO: Python functions possibly # should too if getattr(func, '__name__', None) == '__call': func = partial(func, calculator.namespace) except KeyError: if not is_builtin_css_function(func_name): log.error("Function not found: %s:%s", func_name, num_args, extra={'stack': True}) rendered_args = [] for var, value in evald_argpairs: rendered_value = value.render() if var is None: rendered_args.append(rendered_value) else: rendered_args.append("%s: %s" % (var, rendered_value)) return String(u"%s(%s)" % (func_name, u", ".join(rendered_args)), quotes=None) else: return func(*args, **kwargs)