def sprite(map, sprite, offset_x=None, offset_y=None, cache_buster=True): """ Returns the image and background position for use in a single shorthand property """ map = map.render() sprite_maps = _get_cache('sprite_maps') sprite_map = sprite_maps.get(map) sprite_name = String.unquoted(sprite).value sprite = sprite_map and sprite_map.get(sprite_name) if not sprite_map: log.error("No sprite map found: %s", map, extra={'stack': True}) elif not sprite: log.error("No sprite found: %s in %s", sprite_name, sprite_map['*n*'], extra={'stack': True}) if sprite: url = '%s%s' % (config.ASSETS_URL, sprite_map['*f*']) if cache_buster: url += '?_=%s' % sprite_map['*t*'] x = Number(offset_x or 0, 'px') y = Number(offset_y or 0, 'px') if not x.value or (x.value <= -1 or x.value >= 1) and not x.is_simple_unit('%'): x -= Number(sprite[2], 'px') if not y.value or (y.value <= -1 or y.value >= 1) and not y.is_simple_unit('%'): y -= Number(sprite[3], 'px') url = "url(%s)" % escape(url) return List([String.unquoted(url), x, y]) return List([Number(0), Number(0)])
def sprite_position(map, sprite, offset_x=None, offset_y=None): """ Returns the position for the original image in the sprite. This is suitable for use as a value to background-position. """ map = map.render() sprite_map = sprite_maps.get(map) sprite_name = String.unquoted(sprite).value sprite = sprite_map and sprite_map.get(sprite_name) if not sprite_map: log.error("No sprite map found: %s", map, extra={'stack': True}) elif not sprite: log.error("No sprite found: %s in %s", sprite_name, sprite_map['*n*'], extra={'stack': True}) if sprite: x = None if offset_x is not None and not isinstance(offset_x, Number): x = offset_x if not x or x.value not in ('left', 'right', 'center'): if x: offset_x = None x = Number(offset_x or 0, 'px') if not x.value or (x.value <= -1 or x.value >= 1) and not x.is_simple_unit('%'): x -= Number(sprite[2], 'px') y = None if offset_y is not None and not isinstance(offset_y, Number): y = offset_y if not y or y.value not in ('top', 'bottom', 'center'): if y: offset_y = None y = Number(offset_y or 0, 'px') if not y.value or (y.value <= -1 or y.value >= 1) and not y.is_simple_unit('%'): y -= Number(sprite[3], 'px') return List([x, y]) return List([Number(0), Number(0)])
def _font_files(args, inline): if args == (): return String.unquoted("") fonts = [] args_len = len(args) skip_next = False for index, arg in enumerate(args): if not skip_next: font_type = args[index + 1] if args_len > (index + 1) else None if font_type and font_type.value in FONT_TYPES: skip_next = True else: if re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value): font_type = String.unquoted( re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value).groups()[1]) if font_type.value in FONT_TYPES: fonts.append( List([ _font_url(arg, inline=inline), Function(FONT_TYPES[font_type.value], 'format'), ], use_comma=False)) else: raise Exception('Could not determine font type for "%s"' % arg.value) else: skip_next = False return List(fonts, separator=',')
def _render_standard_color_stops(color_stops): pairs = [] for i, (stop, color) in enumerate(color_stops): if ((i == 0 and stop == Number(0, '%')) or (i == len(color_stops) - 1 and stop == Number(100, '%'))): pairs.append(color) else: pairs.append(List([color, stop], use_comma=False)) return List(pairs, use_comma=True)
def dash_compass_slice(lst, start_index, end_index=None): start_index = Number(start_index).value end_index = Number(end_index).value if end_index is not None else None ret = {} lst = List(lst) if end_index: # This function has an inclusive end, but Python slicing is exclusive end_index += 1 ret = lst.value[start_index:end_index] return List(ret, use_comma=lst.use_comma)
def sprites(map): map = map.render() sprite_map = sprite_maps.get(map, {}) return List( list( String.unquoted(s) for s in sorted(s for s in sprite_map if not s.startswith('*'))))
def join(lst1, lst2, separator=None): ret = [] ret.extend(List.from_maybe(lst1)) ret.extend(List.from_maybe(lst2)) use_comma = __parse_separator(separator, default_from=lst1) return List(ret, use_comma=use_comma)
def nest(*arguments): if isinstance(arguments[0], List): lst = arguments[0].value else: lst = StringValue(arguments[0]).value.split(',') ret = [unicode(s).strip() for s in lst if unicode(s).strip()] for arg in arguments[1:]: if isinstance(arg, List): lst = arg.value else: lst = StringValue(arg).value.split(',') new_ret = [] for s in lst: s = unicode(s).strip() if s: for r in ret: if '&' in s: new_ret.append(s.replace('&', r)) else: if r[-1] in ('.', ':', '#'): new_ret.append(r + s) else: new_ret.append(r + ' ' + s) ret = new_ret return List(sorted(set(ret)), use_comma=True)
def enumerate_(prefix, frm, through, separator='-'): separator = StringValue(separator).value try: frm = int(getattr(frm, 'value', frm)) except ValueError: frm = 1 try: through = int(getattr(through, 'value', through)) except ValueError: through = frm if frm > through: frm, through = through, frm rev = reversed else: rev = lambda x: x ret = [] for i in rev(range(frm, through + 1)): if prefix.value: ret.append( StringValue(prefix.value + separator + str(i), quotes=None)) else: ret.append(NumberValue(i)) return List(ret, use_comma=True)
def _font_files(args, inline): if args == (): return String.unquoted("") fonts = [] args_len = len(args) skip_next = False for index in range(len(args)): arg = args[index] if not skip_next: font_type = args[index + 1] if args_len > (index + 1) else None if font_type and font_type.value in FONT_TYPES: skip_next = True else: if re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value): font_type = String.unquoted( re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value).groups()[1]) if font_type.value in FONT_TYPES: fonts.append( String.unquoted( '%s format("%s")' % (_font_url(arg, inline=inline), String.unquoted(FONT_TYPES[font_type.value]).value))) else: raise Exception('Could not determine font type for "%s"' % arg.value) else: skip_next = False return List(fonts, separator=',')
def _font_files(args, inline): args = List.from_maybe_starargs(args) n = 0 params = [[], []] for arg in args: if isinstance(arg, List): if len(arg) == 2: if n % 2 == 1: params[1].append(None) n += 1 params[0].append(arg[0]) params[1].append(arg[1]) n += 2 else: for arg2 in arg: params[n % 2].append(arg2) n += 1 else: params[n % 2].append(arg) n += 1 len0 = len(params[0]) len1 = len(params[1]) if len1 < len0: params[1] += [None] * (len0 - len1) elif len0 < len1: params[0] += [None] * (len1 - len0) fonts = [] for font, format in zip(params[0], params[1]): if format: fonts.append( '%s format("%s")' % (_font_url(font, inline=inline), StringValue(format).value)) else: fonts.append(_font_url(font, inline=inline)) return List(fonts)
def set_nth(list, n, value): expect_type(n, Number, unit=None) py_n = n.to_python_index(len(list)) return List( tuple(list[:py_n]) + (value,) + tuple(list[py_n + 1:]), use_comma=list.use_comma)
def join(lst1, lst2, separator=String.unquoted('auto')): expect_type(separator, String) ret = [] ret.extend(List.from_maybe(lst1)) ret.extend(List.from_maybe(lst2)) if separator.value == 'comma': use_comma = True elif separator.value == 'space': use_comma = False elif separator.value == 'auto': # The Sass docs are slightly misleading here, but the algorithm is: use # the delimiter from the first list that has at least 2 items, or # default to spaces. if len(lst1) > 1: use_comma = lst1.use_comma elif len(lst2) > 1: use_comma = lst2.use_comma else: use_comma = False else: raise ValueError("separator for join() must be comma, space, or auto") return List(ret, use_comma=use_comma)
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 append(lst, val, separator=None): ret = [] ret.extend(List.from_maybe(lst)) ret.append(val) use_comma = __parse_separator(separator, default_from=lst) return List(ret, use_comma=use_comma)
def enumerate_(prefix, frm, through, separator='-'): separator = String.unquoted(separator).value try: frm = int(getattr(frm, 'value', frm)) except ValueError: frm = 1 try: through = int(getattr(through, 'value', through)) except ValueError: through = frm if frm > through: # DEVIATION: allow reversed enumerations (and ranges as range() uses enumerate, like '@for .. from .. through') frm, through = through, frm rev = reversed else: rev = lambda x: x ret = [] for i in rev(range(frm, through + 1)): if prefix and prefix.value: ret.append( String.unquoted(prefix.value + separator + six.text_type(i))) else: ret.append(Number(i)) return List(ret, use_comma=True)
def test_parse(calc): # Tests for some general parsing. assert calc('foo !important bar') == List([ String('foo'), String('!important'), String('bar'), ])
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 sprites(map, remove_suffix=False): map = map.render() sprite_map = sprite_maps.get(map, {}) return List([ String.unquoted(s) for s in sorted( set( s.rsplit('-', 1)[0] if remove_suffix else s for s in sprite_map if not s.startswith('*'))) ])
def glyphs(sheet, remove_suffix=False): sheet = sheet.render() font_sheet = font_sheets.get(sheet, {}) return List([ String.unquoted(f) for f in sorted( set( f.rsplit('-', 1)[0] if remove_suffix else f for f in font_sheet if not f.startswith('*'))) ])
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 nest(*arguments): if isinstance(arguments[0], List): lst = arguments[0] elif isinstance(arguments[0], String): lst = arguments[0].value.split(',') else: raise TypeError("Expected list or string, got %r" % (arguments[0], )) ret = [] for s in lst: if isinstance(s, String): s = s.value elif isinstance(s, six.string_types): s = s else: raise TypeError("Expected string, got %r" % (s, )) s = s.strip() if not s: continue ret.append(s) for arg in arguments[1:]: if isinstance(arg, List): lst = arg elif isinstance(arg, String): lst = arg.value.split(',') else: raise TypeError("Expected list or string, got %r" % (arg, )) new_ret = [] for s in lst: if isinstance(s, String): s = s.value elif isinstance(s, six.string_types): s = s else: raise TypeError("Expected string, got %r" % (s, )) s = s.strip() if not s: continue for r in ret: if '&' in s: new_ret.append(s.replace('&', r)) else: if not r or r[-1] in ('.', ':', '#'): new_ret.append(r + s) else: new_ret.append(r + ' ' + s) ret = new_ret ret = [String.unquoted(s) for s in sorted(set(ret))] return List(ret, use_comma=True)
def reject(lst, *values): """Removes the given values from the list""" lst = List.from_maybe(lst) values = frozenset(List.from_maybe_starargs(values)) ret = [] for item in lst: if item not in values: ret.append(item) return List(ret, use_comma=lst.use_comma)
def compact(*args): """Returns a new list after removing any non-true values""" use_comma = True if len(args) == 1 and isinstance(args[0], List): use_comma = args[0].use_comma args = args[0] return List( [arg for arg in args if arg], use_comma=use_comma, )
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 evaluate(self, calculator, divide=False): items = [item.evaluate(calculator, divide=divide) for item in self.items] # Whether this is a "plain" literal matters for null removal: nulls are # left alone if this is a completely vanilla CSS property literal = True if divide: # TODO sort of overloading "divide" here... rename i think literal = False elif not all(isinstance(item, Literal) for item in self.items): literal = False return List(items, use_comma=self.comma, literal=literal)
def grad_point(*p): pos = set() hrz = vrt = Number(0.5, '%') for _p in p: pos.update(String.unquoted(_p).value.split()) if 'left' in pos: hrz = Number(0, '%') elif 'right' in pos: hrz = Number(1, '%') if 'top' in pos: vrt = Number(0, '%') elif 'bottom' in pos: vrt = Number(1, '%') return List([v for v in (hrz, vrt) if v is not None])
def prefix(prefix, *args): to_fnct_str = 'to_' + to_str(prefix).replace('-', '_') args = list(args) for i, arg in enumerate(args): if isinstance(arg, List): _value = [] for iarg in arg: to_fnct = getattr(iarg, to_fnct_str, None) if to_fnct: _value.append(to_fnct()) else: _value.append(iarg) args[i] = List(_value) else: to_fnct = getattr(arg, to_fnct_str, None) if to_fnct: args[i] = to_fnct() return List.maybe_new(args, use_comma=True)
def _position(opposite, positions): if positions is None: positions = DEFAULT_POSITION else: positions = List.from_maybe(positions) ret = [] for pos in positions: if isinstance(pos, (String, six.string_types)): pos_value = getattr(pos, 'value', pos) if pos_value in OPPOSITE_POSITIONS: if opposite: ret.append(OPPOSITE_POSITIONS[pos_value]) else: ret.append(pos) continue elif pos_value == 'to': # Gradient syntax keyword; leave alone ret.append(pos) continue elif isinstance(pos, Number): if pos.is_simple_unit('%'): if opposite: ret.append(Number(100 - pos.value, '%')) else: ret.append(pos) continue elif pos.is_simple_unit('deg'): # TODO support other angle types? if opposite: ret.append(Number((pos.value + 180) % 360, 'deg')) else: ret.append(pos) continue if opposite: log.warn("Can't find opposite for position %r" % (pos, )) ret.append(pos) return List(ret, use_comma=False).maybe()
def append(lst, val, separator=String.unquoted('auto')): expect_type(separator, String) ret = [] ret.extend(List.from_maybe(lst)) ret.append(val) separator = separator.value if separator == 'comma': use_comma = True elif separator == 'space': use_comma = False elif separator == 'auto': if len(lst) < 2: use_comma = False else: use_comma = lst.use_comma else: raise ValueError('Separator must be auto, comma, or space') return List(ret, use_comma=use_comma)