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 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 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_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(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 __grad_position(index, default, radial, color_stops): try: stops = Number(color_stops[index][0]) if radial and not stops.is_simple_unit('px') and (index == 0 or index == -1 or index == len(color_stops) - 1): log.warn("Webkit only supports pixels for the start and end stops for radial gradients. Got %s", stops) except IndexError: stops = Number(default) return stops
def __radial_svg(color_stops, cx, cy, r): gradient = '<radialGradient id="grad" gradientUnits="userSpaceOnUse" cx="%s" cy="%s" r="%s">%s</radialGradient>' % ( to_str(Number(cx)), to_str(Number(cy)), to_str(Number(r)), __color_stops_svg(color_stops) ) return __svg_template(gradient)
def _linear_svg(color_stops, x1, y1, x2, y2): gradient = '<linearGradient id="grad" x1="%s" y1="%s" x2="%s" y2="%s">%s</linearGradient>' % ( to_str(Number(x1)), to_str(Number(y1)), to_str(Number(x2)), to_str(Number(y2)), __color_stops_svg(color_stops) ) return __svg_template(gradient)
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 _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 background_noise(density=None, opacity=None, size=None, monochrome=False, intensity=(), color=None, background=None, inline=False): if not Image: raise Exception("Images manipulation require PIL") density = [Number(v).value for v in List.from_maybe(density)] intensity = [Number(v).value for v in List.from_maybe(intensity)] color = [Color(v).value for v in List.from_maybe(color) if v] opacity = [Number(v).value for v in List.from_maybe(opacity)] size = int(Number(size).value) if size else 0 if size < 1 or size > 512: size = 200 monochrome = bool(monochrome) background = Color(background).value if background else None new_image = Image.new(mode='RGBA', size=(size, size)) pixdata = new_image.load() _image_noise(pixdata, size, density, intensity, color, opacity, monochrome) if not inline: key = (size, density, intensity, color, opacity, monochrome) asset_file = 'noise-%s%sx%s' % ('mono-' if monochrome else '', size, size) # asset_file += '-[%s][%s]' % ('-'.join(to_str(s).replace('.', '_') for s in density or []), '-'.join(to_str(s).replace('.', '_') for s in opacity or [])) asset_file += '-' + base64.urlsafe_b64encode( hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_') asset_file += '.png' asset_path = os.path.join( config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets'), asset_file) try: new_image.save(asset_path) except IOError: log.exception("Error while saving image") inline = True # Retry inline version url = '%s%s' % (config.ASSETS_URL, asset_file) if inline: output = six.BytesIO() new_image.save(output, format='PNG') contents = output.getvalue() output.close() url = 'data:image/png;base64,' + base64.b64encode(contents) inline = 'url("%s")' % escape(url) return String.unquoted(inline)
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 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 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 adjust_color( color, red=None, green=None, blue=None, hue=None, saturation=None, lightness=None, alpha=None): do_rgb = red or green or blue do_hsl = hue or saturation or lightness if do_rgb and do_hsl: raise ValueError( "Can't adjust both RGB and HSL channels at the same time") zero = Number(0) a = color.alpha + (alpha or zero).value if do_rgb: r, g, b = color.rgba[:3] channels = [ current + (adjustment or zero).value / 255 for (current, adjustment) in zip(color.rgba, (red, green, blue))] return Color.from_rgb(*channels, alpha=a) else: h, s, l = color.hsl h = (h + (hue or zero).value / 360) % 1 s += _interpret_percentage(saturation or zero, relto=100, clamp=False) l += _interpret_percentage(lightness or zero, relto=100, clamp=False) return Color.from_hsl(h, s, l, a)
def image_height(image): """ Returns the height of the image found at the path supplied by `image` relative to your project's images directory. """ if not Image: raise Exception("Images manipulation require PIL") filepath = String.unquoted(image).value path = None try: height = _image_size_cache[filepath][1] except KeyError: height = 0 IMAGES_ROOT = _images_root() if callable(IMAGES_ROOT): try: _file, _storage = list(IMAGES_ROOT(filepath))[0] except IndexError: pass else: path = _storage.open(_file) else: _path = os.path.join(IMAGES_ROOT, filepath.strip('/')) if os.path.exists(_path): path = open(_path, 'rb') if path: image = Image.open(path) size = image.size height = size[1] _image_size_cache[filepath] = size return Number(height, 'px')
def image_width(image): """ Returns the width of the image found at the path supplied by `image` relative to your project's images directory. """ if not Image: raise SassMissingDependency('PIL', 'image manipulation') image_size_cache = _get_cache('image_size_cache') filepath = String.unquoted(image).value path = None try: width = image_size_cache[filepath][0] except KeyError: width = 0 IMAGES_ROOT = _images_root() if callable(IMAGES_ROOT): try: _file, _storage = list(IMAGES_ROOT(filepath))[0] except IndexError: pass else: path = _storage.open(_file) else: _path = os.path.join(IMAGES_ROOT, filepath.strip(os.sep)) if os.path.exists(_path): path = open(_path, 'rb') if path: image = Image.open(path) size = image.size width = size[0] image_size_cache[filepath] = size return Number(width, 'px')
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 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 test_comparison_null(): null = Null() assert null == null assert null != Number(0) with pytest.raises(TypeError): null < null
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 test_addition(calc): assert calc('123 + 456') == Number(579) assert calc('1px + 2px') == Number(3, "px") assert calc('123 + abc') == String('123abc') assert calc('abc + 123') == String('abc123') assert calc('abc + def') == String('abcdef') assert calc('abc + "def"') == String('abcdef') ret = calc('"abc" + def') assert ret == String('abcdef') assert ret.quotes == '"' ret = calc('"abc" + "def"') assert ret == String('abcdef') assert ret.quotes == '"' assert calc('#010305 + #050301') == Color.from_hex('#060606') assert calc('#ffffff + #ffffff') == Color.from_name('white')
def image_color(color, width=1, height=1): if not Image: raise Exception("Images manipulation require PIL") w = int(Number(width).value) h = int(Number(height).value) if w <= 0 or h <= 0: raise ValueError new_image = Image.new( mode='RGB' if color.alpha == 1 else 'RGBA', size=(w, h), color=color.rgba255, ) output = six.BytesIO() new_image.save(output, format='PNG') contents = output.getvalue() output.close() url = make_data_url('image/png', contents) inline = 'url("%s")' % escape(url) return String.unquoted(inline)
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 test_subtraction(): assert Number(123) - Number(456) == Number(-333) assert Number(456) - Number(123) == Number(333) # TODO test that subtracting e.g. strings doesn't work assert Color.from_hex('#0f0f0f') - Color.from_hex( '#050505') == Color.from_hex('#0a0a0a')
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_alpha_opacity(): assert calc('alpha(black)') == Number(1.) assert calc('alpha(rgba(black, 0.5))') == Number(0.5) assert calc('alpha(rgba(black, 0))') == Number(0.) # opacity is a synonym assert calc('opacity(black)') == Number(1.) assert calc('opacity(rgba(black, 0.5))') == Number(0.5) assert calc('opacity(rgba(black, 0))') == Number(0.)
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)')
def log_(number, base=None): if not isinstance(number, Number): raise TypeError("Expected number, got %r" % (number, )) elif not number.is_unitless: raise ValueError("Expected unitless number, got %r" % (number, )) if base is None: pass elif not isinstance(base, Number): raise TypeError("Expected number, got %r" % (base, )) elif not base.is_unitless: raise ValueError("Expected unitless number, got %r" % (base, )) if base is None: ret = math.log(number.value) else: ret = math.log(number.value, base.value) return Number(ret)
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 background_brushed(density=None, intensity=None, color=None, opacity=None, size=None, monochrome=False, direction=(), spread=(), background=None, inline=False): if not Image: raise Exception("Images manipulation require PIL") density = [Number(v).value for v in List.from_maybe(density)] intensity = [Number(v).value for v in List.from_maybe(intensity)] color = [Color(v).value for v in List.from_maybe(color) if v] opacity = [Number(v).value for v in List.from_maybe(opacity)] size = int(Number(size).value) if size else -1 if size < 0 or size > 512: size = 200 monochrome = bool(monochrome) direction = [Number(v).value for v in List.from_maybe(direction)] spread = [Number(v).value for v in List.from_maybe(spread)] background = Color(background).value if background else None new_image = Image.new( mode='RGBA', size=(size, size) ) pixdata = new_image.load() _image_brushed(pixdata, size, density, intensity, color, opacity, monochrome, direction, spread, background) if not inline: key = (size, density, intensity, color, opacity, monochrome, direction, spread, background) asset_file = 'brushed-%s%sx%s' % ('mono-' if monochrome else '', size, size) # asset_file += '-[%s][%s][%s]' % ('-'.join(to_str(s).replace('.', '_') for s in density or []), '-'.join(to_str(s).replace('.', '_') for s in opacity or []), '-'.join(to_str(s).replace('.', '_') for s in direction or [])) asset_file += '-' + make_filename_hash(key) asset_file += '.png' asset_path = os.path.join(config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets'), asset_file) try: new_image.save(asset_path) except IOError: log.exception("Error while saving image") inline = True # Retry inline version url = '%s%s' % (config.ASSETS_URL, asset_file) if inline: output = six.BytesIO() new_image.save(output, format='PNG') contents = output.getvalue() output.close() url = make_data_url('image/png', contents) inline = 'url("%s")' % escape(url) return String.unquoted(inline)
def test_comparison_numeric(): lo = Number(123) hi = Number(456) assert lo < hi assert lo <= hi assert lo <= lo assert hi > lo assert hi >= lo assert hi >= hi assert lo == lo assert lo != hi # Same tests, negated assert not lo > hi assert not lo >= hi assert not hi < lo assert not hi <= lo assert not lo != lo assert not lo == hi # Numbers with units should also auto-cast numbers with units units = Number(123, "px") plain = Number(123) assert units == plain assert units <= plain assert units >= plain assert not units != plain assert not units < plain assert not units > plain # Incompatible units have... rules. ems = Number(100, "em") pxs = Number(100, "px") with pytest.raises(ValueError): ems < pxs with pytest.raises(ValueError): ems > pxs with pytest.raises(ValueError): ems <= pxs with pytest.raises(ValueError): ems >= pxs assert not ems == pxs assert ems != pxs
def to_lower_case(string): expect_type(string, String) return String(string.value.lower(), quotes=string.quotes) # ------------------------------------------------------------------------------ # Number functions @ns.declare def percentage(value): expect_type(value, Number, unit=None) return value * Number(100, unit='%') ns.set_function('abs', 1, Number.wrap_python_function(abs)) ns.set_function('round', 1, Number.wrap_python_function(round)) ns.set_function('ceil', 1, Number.wrap_python_function(math.ceil)) ns.set_function('floor', 1, Number.wrap_python_function(math.floor)) # ------------------------------------------------------------------------------ # List functions def __parse_separator(separator, default_from=None): if separator is None: separator = 'auto' separator = String.unquoted(separator).value if separator == 'comma': return True
expect_type(string, String) return String(string.value.lower(), quotes=string.quotes) # ------------------------------------------------------------------------------ # Number functions @ns.declare def percentage(value): expect_type(value, Number, unit=None) return value * Number(100, unit="%") ns.set_function("abs", 1, Number.wrap_python_function(abs)) ns.set_function("round", 1, Number.wrap_python_function(round)) ns.set_function("ceil", 1, Number.wrap_python_function(math.ceil)) ns.set_function("floor", 1, Number.wrap_python_function(math.floor)) # ------------------------------------------------------------------------------ # List functions def __parse_separator(separator, default_from=None): if separator is None: separator = "auto" separator = String.unquoted(separator).value if separator == "comma":
raise ValueError("Expected unitless number, got %r" % (base,)) if base is None: ret = math.log(number.value) else: ret = math.log(number.value, base.value) return Number(ret) @register('pow', 2) def pow(number, exponent): return number ** exponent COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.sqrt), 'sqrt', 1) COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.sin), 'sin', 1) COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.cos), 'cos', 1) COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.tan), 'tan', 1) # ------------------------------------------------------------------------------ # Fonts def _fonts_root(): return config.STATIC_ROOT if config.FONTS_ROOT is None else config.FONTS_ROOT def _font_url(path, only_path=False, cache_buster=True, inline=False): filepath = String.unquoted(path).value file = None
return String(arg.value, quotes='"') else: return String(arg.render(), quotes='"') # ------------------------------------------------------------------------------ # Number functions @register("percentage", 1) def percentage(value): expect_type(value, Number, unit=None) return value * Number(100, unit="%") CORE_LIBRARY.add(Number.wrap_python_function(abs), "abs", 1) CORE_LIBRARY.add(Number.wrap_python_function(round), "round", 1) CORE_LIBRARY.add(Number.wrap_python_function(math.ceil), "ceil", 1) CORE_LIBRARY.add(Number.wrap_python_function(math.floor), "floor", 1) # ------------------------------------------------------------------------------ # List functions def __parse_separator(separator, default_from=None): if separator is None: return None separator = String.unquoted(separator).value if separator == "comma": return True
@register('to-lower-case', 1) def to_lower_case(string): expect_type(string, String) return String(string.value.lower(), quotes=string.quotes) # ------------------------------------------------------------------------------ # Number functions @register('percentage', 1) def percentage(value): expect_type(value, Number, unit=None) return value * Number(100, unit='%') CORE_LIBRARY.add(Number.wrap_python_function(abs), 'abs', 1) CORE_LIBRARY.add(Number.wrap_python_function(round), 'round', 1) CORE_LIBRARY.add(Number.wrap_python_function(math.ceil), 'ceil', 1) CORE_LIBRARY.add(Number.wrap_python_function(math.floor), 'floor', 1) # ------------------------------------------------------------------------------ # List functions def __parse_separator(separator, default_from=None): if separator is None: separator = 'auto' separator = String.unquoted(separator).value if separator == 'comma': return True
def sprite_map(g, **kwargs): """ Generates a sprite map from the files matching the glob pattern. Uses the keyword-style arguments passed in to control the placement. $direction - Sprite map layout. Can be `vertical` (default), `horizontal`, `diagonal` or `smart`. $position - For `horizontal` and `vertical` directions, the position of the sprite. (defaults to `0`) $<sprite>-position - Position of a given sprite. $padding, $spacing - Adds paddings to sprites (top, right, bottom, left). (defaults to `0, 0, 0, 0`) $<sprite>-padding, $<sprite>-spacing - Padding for a given sprite. $dst-color - Together with `$src-color`, forms a map of source colors to be converted to destiny colors (same index of `$src-color` changed to `$dst-color`). $<sprite>-dst-color - Destiny colors for a given sprite. (defaults to `$dst-color`) $src-color - Selects source colors to be converted to the corresponding destiny colors. (defaults to `black`) $<sprite>-dst-color - Source colors for a given sprite. (defaults to `$src-color`) $collapse - Collapses every image in the sprite map to a fixed size (`x` and `y`). $collapse-x - Collapses a size for `x`. $collapse-y - Collapses a size for `y`. """ if not Image: raise Exception("Images manipulation require PIL") now_time = time.time() g = String(g, quotes=None).value if g in sprite_maps: sprite_maps[glob]['*'] = now_time elif '..' not in g: # Protect against going to prohibited places... if callable(config.STATIC_ROOT): glob_path = g rfiles = files = sorted(config.STATIC_ROOT(g)) else: glob_path = os.path.join(config.STATIC_ROOT, g) files = glob.glob(glob_path) files = sorted((f, None) for f in files) rfiles = [(rf[len(config.STATIC_ROOT):], s) for rf, s in files] if not files: log.error("Nothing found at '%s'", glob_path) return String.unquoted('') map_name = os.path.normpath(os.path.dirname(g)).replace('\\', '_').replace('/', '_') key = [f for (f, s) in files] + [repr(kwargs), config.ASSETS_URL] key = map_name + '-' + make_filename_hash(key) asset_file = key + '.png' ASSETS_ROOT = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets') asset_path = os.path.join(ASSETS_ROOT, asset_file) cache_path = os.path.join(config.CACHE_ROOT or ASSETS_ROOT, asset_file + '.cache') inline = Boolean(kwargs.get('inline', False)) sprite_map = None asset = None file_asset = None inline_asset = None if os.path.exists(asset_path) or inline: try: save_time, file_asset, inline_asset, sprite_map, sizes = pickle.load(open(cache_path)) if file_asset: sprite_maps[file_asset.render()] = sprite_map if inline_asset: sprite_maps[inline_asset.render()] = sprite_map if inline: asset = inline_asset else: asset = file_asset except: pass if sprite_map: for file_, storage in files: _time = getmtime(file_, storage) if save_time < _time: if _time > now_time: log.warning("File '%s' has a date in the future (cache ignored)" % file_) sprite_map = None # Invalidate cached sprite map break if sprite_map is None or asset is None: cache_buster = Boolean(kwargs.get('cache_buster', True)) direction = String.unquoted(kwargs.get('direction', config.SPRTE_MAP_DIRECTION)).value repeat = String.unquoted(kwargs.get('repeat', 'no-repeat')).value collapse = kwargs.get('collapse', Number(0)) if isinstance(collapse, List): collapse_x = int(Number(collapse[0]).value) collapse_y = int(Number(collapse[-1]).value) else: collapse_x = collapse_y = int(Number(collapse).value) if 'collapse_x' in kwargs: collapse_x = int(Number(kwargs['collapse_x']).value) if 'collapse_y' in kwargs: collapse_y = int(Number(kwargs['collapse_y']).value) position = Number(kwargs.get('position', 0)) if not position.is_simple_unit('%') and position.value > 1: position = position.value / 100.0 else: position = position.value if position < 0: position = 0.0 elif position > 1: position = 1.0 padding = kwargs.get('padding', kwargs.get('spacing', Number(0))) padding = [int(Number(v).value) for v in List.from_maybe(padding)] padding = (padding * 4)[:4] dst_colors = kwargs.get('dst_color') dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_colors) if v] src_colors = kwargs.get('src_color', Color.from_name('black')) src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_colors)] len_colors = max(len(dst_colors), len(src_colors)) dst_colors = (dst_colors * len_colors)[:len_colors] src_colors = (src_colors * len_colors)[:len_colors] def images(f=lambda x: x): for file_, storage in f(files): if storage is not None: _file = storage.open(file_) else: _file = file_ _image = Image.open(_file) yield _image names = tuple(os.path.splitext(os.path.basename(file_))[0] for file_, storage in files) has_dst_colors = False all_dst_colors = [] all_src_colors = [] all_positions = [] all_paddings = [] for name in names: name = name.replace('-', '_') _position = kwargs.get(name + '_position') if _position is None: _position = position else: _position = Number(_position) if not _position.is_simple_unit('%') and _position.value > 1: _position = _position.value / 100.0 else: _position = _position.value if _position < 0: _position = 0.0 elif _position > 1: _position = 1.0 all_positions.append(_position) _padding = kwargs.get(name + '_padding', kwargs.get(name + '_spacing')) if _padding is None: _padding = padding else: _padding = [int(Number(v).value) for v in List.from_maybe(_padding)] _padding = (_padding * 4)[:4] all_paddings.append(_padding) _dst_colors = kwargs.get(name + '_dst_color') if _dst_colors is None: _dst_colors = dst_colors if dst_colors: has_dst_colors = True else: has_dst_colors = True _dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(_dst_colors) if v] _src_colors = kwargs.get(name + '_src_color', Color.from_name('black')) if _src_colors is None: _src_colors = src_colors else: _src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(_src_colors)] _len_colors = max(len(_dst_colors), len(_src_colors)) _dst_colors = (_dst_colors * _len_colors)[:_len_colors] _src_colors = (_src_colors * _len_colors)[:_len_colors] all_dst_colors.append(_dst_colors) all_src_colors.append(_src_colors) sizes = tuple((collapse_x or i.size[0], collapse_y or i.size[1]) for i in images()) if direction == 'horizontal': layout = HorizontalSpritesLayout(sizes, all_paddings, position=all_positions) elif direction == 'vertical': layout = VerticalSpritesLayout(sizes, all_paddings, position=all_positions) elif direction == 'diagonal': layout = DiagonalSpritesLayout(sizes, all_paddings) elif direction == 'smart': layout = PackedSpritesLayout(sizes, all_paddings) else: raise Exception("Invalid direction %r" % (direction,)) layout_positions = list(layout) new_image = Image.new( mode='RGBA', size=(layout.width, layout.height), color=(0, 0, 0, 0) ) useless_dst_color = has_dst_colors offsets_x = [] offsets_y = [] for i, image in enumerate(images()): x, y, width, height, cssx, cssy, cssw, cssh = layout_positions[i] iwidth, iheight = image.size if has_dst_colors: pixdata = image.load() for _y in xrange(iheight): for _x in xrange(iwidth): pixel = pixdata[_x, _y] a = pixel[3] if len(pixel) == 4 else 255 if a: rgb = pixel[:3] for j, dst_color in enumerate(all_dst_colors[i]): if rgb == all_src_colors[i][j]: new_color = tuple([int(c) for c in dst_color] + [a]) if pixel != new_color: pixdata[_x, _y] = new_color useless_dst_color = False break if iwidth != width or iheight != height: cy = 0 while cy < iheight: cx = 0 while cx < iwidth: new_image = alpha_composite(new_image, image, (x, y), (cx, cy, cx + width, cy + height)) cx += width cy += height else: new_image.paste(image, (x, y)) offsets_x.append(cssx) offsets_y.append(cssy) if useless_dst_color: log.warning("Useless use of $dst-color in sprite map for files at '%s' (never used for)" % glob_path) filetime = int(now_time) if not inline: try: new_image.save(asset_path) url = '%s%s' % (config.ASSETS_URL, asset_file) if cache_buster: url += '?_=%s' % filetime except IOError: log.exception("Error while saving image") inline = True if inline: output = six.BytesIO() new_image.save(output, format='PNG') contents = output.getvalue() output.close() mime_type = 'image/png' url = make_data_url(mime_type, contents) url = 'url(%s)' % escape(url) if inline: asset = inline_asset = List([String.unquoted(url), String.unquoted(repeat)]) else: asset = file_asset = List([String.unquoted(url), String.unquoted(repeat)]) # Add the new object: sprite_map = dict(zip(names, zip(sizes, rfiles, offsets_x, offsets_y))) sprite_map['*'] = now_time sprite_map['*f*'] = asset_file sprite_map['*k*'] = key sprite_map['*n*'] = map_name sprite_map['*t*'] = filetime sizes = zip(files, sizes) cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT) pickle.dump((now_time, file_asset, inline_asset, sprite_map, sizes), cache_tmp) cache_tmp.close() os.rename(cache_tmp.name, cache_path) # Use the sorted list to remove older elements (keep only 500 objects): if len(sprite_maps) > MAX_SPRITE_MAPS: for a in sorted(sprite_maps, key=lambda a: sprite_maps[a]['*'], reverse=True)[KEEP_SPRITE_MAPS:]: del sprite_maps[a] log.warning("Exceeded maximum number of sprite maps (%s)" % MAX_SPRITE_MAPS) sprite_maps[asset.render()] = sprite_map for file_, size in sizes: _image_size_cache[file_] = size # TODO this sometimes returns an empty list, or is never assigned to return asset
raise ValueError("Expected unitless number, got %r" % (base,)) if base is None: ret = math.log(number.value) else: ret = math.log(number.value, base.value) return Number(ret) @ns.declare def pow(number, exponent): return number ** exponent ns.set_function('sqrt', 1, Number.wrap_python_function(math.sqrt)) ns.set_function('sin', 1, Number.wrap_python_function(math.sin)) ns.set_function('cos', 1, Number.wrap_python_function(math.cos)) ns.set_function('tan', 1, Number.wrap_python_function(math.tan)) # ------------------------------------------------------------------------------ # Fonts def _fonts_root(): return config.STATIC_ROOT if config.FONTS_ROOT is None else config.FONTS_ROOT def _font_url(path, only_path=False, cache_buster=True, inline=False): filepath = String.unquoted(path).value file = None