From a78bb715cab3a9686440a9963b498eeda3aa869b Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Wed, 27 Aug 2014 16:10:10 -0700 Subject: Move Compass to an extension. --- scss/compiler.py | 3 +- scss/extension/bootstrap.py | 4 +- scss/extension/compass/__init__.py | 32 ++ scss/extension/compass/gradients.py | 431 ++++++++++++++++ scss/extension/compass/helpers.py | 645 ++++++++++++++++++++++++ scss/extension/compass/images.py | 275 +++++++++++ scss/extension/compass/layouts.py | 347 +++++++++++++ scss/extension/compass/sprites.py | 559 +++++++++++++++++++++ scss/functions/compass/__init__.py | 2 - scss/functions/compass/gradients.py | 434 ---------------- scss/functions/compass/helpers.py | 656 ------------------------- scss/functions/compass/images.py | 287 ----------- scss/functions/compass/layouts.py | 347 ------------- scss/functions/compass/sprites.py | 566 --------------------- scss/legacy.py | 4 +- scss/tests/functions/compass/test_gradients.py | 6 +- scss/tests/functions/compass/test_helpers.py | 17 +- scss/tests/functions/compass/test_images.py | 26 +- 18 files changed, 2315 insertions(+), 2326 deletions(-) create mode 100644 scss/extension/compass/__init__.py create mode 100644 scss/extension/compass/gradients.py create mode 100644 scss/extension/compass/helpers.py create mode 100644 scss/extension/compass/images.py create mode 100644 scss/extension/compass/layouts.py create mode 100644 scss/extension/compass/sprites.py delete mode 100644 scss/functions/compass/__init__.py delete mode 100644 scss/functions/compass/gradients.py delete mode 100644 scss/functions/compass/helpers.py delete mode 100644 scss/functions/compass/images.py delete mode 100644 scss/functions/compass/layouts.py delete mode 100644 scss/functions/compass/sprites.py diff --git a/scss/compiler.py b/scss/compiler.py index 6afd149..0530efe 100644 --- a/scss/compiler.py +++ b/scss/compiler.py @@ -24,8 +24,7 @@ from scss.expression import Calculator from scss.extension import Extension from scss.extension.core import CoreExtension from scss.extension import NamespaceAdapterExtension -from scss.functions import COMPASS_LIBRARY -from scss.functions.compass.sprites import sprite_map +from scss.extension.compass.sprites import sprite_map from scss.rule import BlockAtRuleHeader from scss.rule import Namespace from scss.rule import RuleAncestry diff --git a/scss/extension/bootstrap.py b/scss/extension/bootstrap.py index 76cdb02..bd1b75e 100644 --- a/scss/extension/bootstrap.py +++ b/scss/extension/bootstrap.py @@ -4,9 +4,9 @@ from __future__ import unicode_literals from __future__ import division from scss.extension import Extension +from scss.extension.compass.helpers import _font_url +from scss.extension.compass.images import _image_url from scss.namespace import Namespace -from scss.functions.compass.helpers import _font_url -from scss.functions.compass.images import _image_url class BootstrapExtension(Extension): diff --git a/scss/extension/compass/__init__.py b/scss/extension/compass/__init__.py new file mode 100644 index 0000000..daed9ef --- /dev/null +++ b/scss/extension/compass/__init__.py @@ -0,0 +1,32 @@ +"""Extension providing Compass support.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from scss.extension import Extension +from scss.namespace import Namespace + + +# Global cache of image sizes, shared between sprites and images libraries. +# TODO put on the extension, somehow. +_image_size_cache = {} + + +# Import all our children to register their functions +from .gradients import gradients_namespace +from .helpers import helpers_namespace +from .images import images_namespace +from .sprites import sprites_namespace + + +class CompassExtension(Extension): + name = 'compass' + namespace = Namespace.derive_from( + gradients_namespace, + helpers_namespace, + images_namespace, + sprites_namespace, + ) + + +__all__ = ['CompassExtension'] diff --git a/scss/extension/compass/gradients.py b/scss/extension/compass/gradients.py new file mode 100644 index 0000000..81edbfd --- /dev/null +++ b/scss/extension/compass/gradients.py @@ -0,0 +1,431 @@ +"""Utilities for working with gradients. Inspired by Compass, but not quite +the same. +""" +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import base64 +import logging + +import six + +from .helpers import opposite_position, position +from scss.namespace import Namespace +from scss.types import Color, List, Number, String +from scss.util import escape, split_params, to_float, to_str + +log = logging.getLogger(__name__) +ns = gradients_namespace = Namespace() +__all__ = ['gradients_namespace'] + + +def __color_stops(percentages, *args): + if len(args) == 1: + if isinstance(args[0], (list, tuple, List)): + list(args[0]) + elif isinstance(args[0], (String, six.string_types)): + color_stops = [] + colors = split_params(getattr(args[0], 'value', args[0])) + for color in colors: + color = color.strip() + if color.startswith('color-stop('): + s, c = split_params(color[11:].rstrip(')')) + s = s.strip() + c = c.strip() + else: + c, s = color.split() + color_stops.append((to_float(s), c)) + return color_stops + + colors = [] + stops = [] + prev_color = False + for c in args: + for c in List.from_maybe(c): + if isinstance(c, Color): + if prev_color: + stops.append(None) + colors.append(c) + prev_color = True + elif isinstance(c, Number): + stops.append(c) + prev_color = False + + if prev_color: + stops.append(None) + stops = stops[:len(colors)] + if stops[0] is None: + stops[0] = Number(0, '%') + if stops[-1] is None: + stops[-1] = Number(100, '%') + + maxable_stops = [s for s in stops if s and not s.is_simple_unit('%')] + if maxable_stops: + max_stops = max(maxable_stops) + else: + max_stops = None + + stops = [_s / max_stops if _s and not _s.is_simple_unit('%') else _s for _s in stops] + + init = 0 + start = None + for i, s in enumerate(stops + [1.0]): + if s is None: + if start is None: + start = i + end = i + else: + final = s + if start is not None: + stride = (final - init) / Number(end - start + 1 + (1 if i < len(stops) else 0)) + for j in range(start, end + 1): + stops[j] = init + stride * Number(j - start + 1) + init = final + start = None + + if not max_stops or percentages: + pass + else: + stops = [s if s.is_simple_unit('%') else s * max_stops for s in stops] + + return List(List(pair) for pair in zip(stops, colors)) + + +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) + + +@ns.declare +def grad_color_stops(*args): + args = List.from_maybe_starargs(args) + color_stops = __color_stops(True, *args) + ret = ', '.join(['color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops]) + return String.unquoted(ret) + + +def __grad_end_position(radial, color_stops): + return __grad_position(-1, 100, radial, color_stops) + + +@ns.declare +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 __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 + + +@ns.declare +def grad_end_position(*color_stops): + color_stops = __color_stops(False, *color_stops) + return Number(__grad_end_position(False, color_stops)) + + +@ns.declare +def color_stops(*args): + args = List.from_maybe_starargs(args) + color_stops = __color_stops(False, *args) + ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops]) + return String.unquoted(ret) + + +@ns.declare +def color_stops_in_percentages(*args): + args = List.from_maybe_starargs(args) + color_stops = __color_stops(True, *args) + ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops]) + return String.unquoted(ret) + + +def _get_gradient_position_and_angle(args): + for arg in args: + ret = None + skip = False + for a in arg: + if isinstance(a, Color): + skip = True + break + elif isinstance(a, Number): + ret = arg + if skip: + continue + if ret is not None: + return ret + for seek in ( + 'center', + 'top', 'bottom', + 'left', 'right', + ): + if String(seek) in arg: + return arg + return None + + +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 _get_gradient_color_stops(args): + color_stops = [] + for arg in args: + for a in List.from_maybe(arg): + if isinstance(a, Color): + color_stops.append(arg) + break + return color_stops or None + + +# TODO these functions need to be +# 1. well-defined +# 2. guaranteed to never wreck css3 syntax +# 3. updated to whatever current compass does +# 4. fixed to use a custom type instead of monkeypatching + + +@ns.declare +def radial_gradient(*args): + args = List.from_maybe_starargs(args) + + try: + # Do a rough check for standard syntax first -- `shape at position` + at_position = list(args[0]).index(String('at')) + except (IndexError, ValueError): + shape_and_size = _get_gradient_shape_and_size(args) + position_and_angle = _get_gradient_position_and_angle(args) + else: + shape_and_size = List.maybe_new(args[0][:at_position]) + position_and_angle = List.maybe_new(args[0][at_position + 1:]) + + color_stops = _get_gradient_color_stops(args) + if color_stops is None: + raise Exception('No color stops provided to radial-gradient function') + color_stops = __color_stops(False, *color_stops) + + if position_and_angle: + rendered_position = position(position_and_angle) + else: + rendered_position = None + rendered_color_stops = _render_standard_color_stops(color_stops) + + args = [] + if shape_and_size and rendered_position: + args.append(List([shape_and_size, String.unquoted('at'), rendered_position], use_comma=False)) + elif rendered_position: + args.append(rendered_position) + elif shape_and_size: + args.append(shape_and_size) + args.extend(rendered_color_stops) + + legacy_args = [] + if rendered_position: + legacy_args.append(rendered_position) + if shape_and_size: + legacy_args.append(shape_and_size) + legacy_args.extend(rendered_color_stops) + + ret = String.unquoted( + 'radial-gradient(' + ', '.join(a.render() for a in args) + ')') + + legacy_ret = 'radial-gradient(' + ', '.join(a.render() for a in legacy_args) + ')' + + def to__css2(): + return String.unquoted('') + ret.to__css2 = to__css2 + + def to__moz(): + return String.unquoted('-moz-' + legacy_ret) + ret.to__moz = to__moz + + def to__pie(): + log.warn("PIE does not support radial-gradient.") + return String.unquoted('-pie-radial-gradient(unsupported)') + ret.to__pie = to__pie + + def to__webkit(): + return String.unquoted('-webkit-' + legacy_ret) + ret.to__webkit = to__webkit + + def to__owg(): + args = [ + 'radial', + grad_point(*position_and_angle) if position_and_angle is not None else 'center', + '0', + grad_point(*position_and_angle) if position_and_angle is not None else 'center', + __grad_end_position(True, color_stops), + ] + args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops) + ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')' + return String.unquoted(ret) + ret.to__owg = to__owg + + def to__svg(): + return radial_svg_gradient(*(list(color_stops) + list(position_and_angle or [String('center')]))) + ret.to__svg = to__svg + + return ret + + +@ns.declare +def linear_gradient(*args): + args = List.from_maybe_starargs(args) + + position_and_angle = _get_gradient_position_and_angle(args) + color_stops = _get_gradient_color_stops(args) + if color_stops is None: + raise Exception('No color stops provided to linear-gradient function') + color_stops = __color_stops(False, *color_stops) + + args = [ + position(position_and_angle) if position_and_angle is not None else None, + ] + args.extend(_render_standard_color_stops(color_stops)) + + to__s = 'linear-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')' + ret = String.unquoted(to__s) + + def to__css2(): + return String.unquoted('') + ret.to__css2 = to__css2 + + def to__moz(): + return String.unquoted('-moz-' + to__s) + ret.to__moz = to__moz + + def to__pie(): + return String.unquoted('-pie-' + to__s) + ret.to__pie = to__pie + + def to__ms(): + return String.unquoted('-ms-' + to__s) + ret.to__ms = to__ms + + def to__o(): + return String.unquoted('-o-' + to__s) + ret.to__o = to__o + + def to__webkit(): + return String.unquoted('-webkit-' + to__s) + ret.to__webkit = to__webkit + + def to__owg(): + args = [ + 'linear', + position(position_and_angle or None), + opposite_position(position_and_angle or None), + ] + args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops) + ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args if a is not None) + ')' + return String.unquoted(ret) + ret.to__owg = to__owg + + def to__svg(): + return linear_svg_gradient(color_stops, position_and_angle or 'top') + ret.to__svg = to__svg + + return ret + + +@ns.declare +def radial_svg_gradient(*args): + args = List.from_maybe_starargs(args) + color_stops = args + center = None + if isinstance(args[-1], (String, Number)): + center = args[-1] + color_stops = args[:-1] + color_stops = __color_stops(False, *color_stops) + cx, cy = grad_point(center) + r = __grad_end_position(True, color_stops) + svg = __radial_svg(color_stops, cx, cy, r) + url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg) + inline = 'url("%s")' % escape(url) + return String.unquoted(inline) + + +@ns.declare +def linear_svg_gradient(*args): + args = List.from_maybe_starargs(args) + color_stops = args + start = None + if isinstance(args[-1], (String, Number)): + start = args[-1] + color_stops = args[:-1] + color_stops = __color_stops(False, *color_stops) + x1, y1 = grad_point(start) + x2, y2 = grad_point(opposite_position(start)) + svg = _linear_svg(color_stops, x1, y1, x2, y2) + url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg) + inline = 'url("%s")' % escape(url) + return String.unquoted(inline) + + +def __color_stops_svg(color_stops): + ret = ''.join('' % (to_str(s), c) for s, c in color_stops) + return ret + + +def __svg_template(gradient): + ret = '\ +\ +%s\ +\ +' % gradient + return ret + + +def _linear_svg(color_stops, x1, y1, x2, y2): + gradient = '%s' % ( + 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 __radial_svg(color_stops, cx, cy, r): + gradient = '%s' % ( + to_str(Number(cx)), + to_str(Number(cy)), + to_str(Number(r)), + __color_stops_svg(color_stops) + ) + return __svg_template(gradient) diff --git a/scss/extension/compass/helpers.py b/scss/extension/compass/helpers.py new file mode 100644 index 0000000..4abb4fe --- /dev/null +++ b/scss/extension/compass/helpers.py @@ -0,0 +1,645 @@ +"""Miscellaneous helper functions ported from Compass. + +See: http://compass-style.org/reference/compass/helpers/ + +This collection is not necessarily complete or up-to-date. +""" +from __future__ import absolute_import +from __future__ import unicode_literals + +import logging +import math +import os.path + +import six + +from scss import config +from scss.namespace import Namespace +from scss.types import Boolean, List, Null, Number, String +from scss.util import escape, to_str, getmtime, make_data_url +import re + +log = logging.getLogger(__name__) +ns = helpers_namespace = Namespace() +__all__ = ['gradients_namespace'] + +FONT_TYPES = { + 'woff': 'woff', + 'otf': 'opentype', + 'opentype': 'opentype', + 'ttf': 'truetype', + 'truetype': 'truetype', + 'svg': 'svg', + 'eot': 'embedded-opentype' +} + + +def add_cache_buster(url, mtime): + fragment = url.split('#') + query = fragment[0].split('?') + if len(query) > 1 and query[1] != '': + cb = '&_=%s' % (mtime) + url = '?'.join(query) + cb + else: + cb = '?_=%s' % (mtime) + url = query[0] + cb + if len(fragment) > 1: + url += '#' + fragment[1] + return url + + +# ------------------------------------------------------------------------------ +# Data manipulation + +@ns.declare +def blank(*objs): + """Returns true when the object is false, an empty string, or an empty list""" + for o in objs: + if isinstance(o, Boolean): + is_blank = not o + elif isinstance(o, String): + is_blank = not len(o.value.strip()) + elif isinstance(o, List): + is_blank = all(blank(el) for el in o) + else: + is_blank = False + + if not is_blank: + return Boolean(False) + + return Boolean(True) + + +@ns.declare +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, + ) + + +@ns.declare +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) + + +@ns.declare +def first_value_of(*args): + if len(args) == 1 and isinstance(args[0], String): + first = args[0].value.split()[0] + return type(args[0])(first) + + args = List.from_maybe_starargs(args) + if len(args): + return args[0] + else: + return Null() + + +@ns.declare_alias('-compass-list') +def dash_compass_list(*args): + return List.from_maybe_starargs(args) + + +@ns.declare_alias('-compass-space-list') +def dash_compass_space_list(*lst): + """ + If the argument is a list, it will return a new list that is space delimited + Otherwise it returns a new, single element, space-delimited list. + """ + ret = dash_compass_list(*lst) + ret.value.pop('_', None) + return ret + + +@ns.declare_alias('-compass-slice') +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) + + +# ------------------------------------------------------------------------------ +# Property prefixing + +@ns.declare +def prefixed(prefix, *args): + to_fnct_str = 'to_' + to_str(prefix).replace('-', '_') + for arg in List.from_maybe_starargs(args): + if hasattr(arg, to_fnct_str): + return Boolean(True) + return Boolean(False) + + +@ns.declare +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) + + +@ns.declare_alias('-moz') +def dash_moz(*args): + return prefix('_moz', *args) + + +@ns.declare_alias('-svg') +def dash_svg(*args): + return prefix('_svg', *args) + + +@ns.declare_alias('-css2') +def dash_css2(*args): + return prefix('_css2', *args) + + +@ns.declare_alias('-pie') +def dash_pie(*args): + return prefix('_pie', *args) + + +@ns.declare_alias('-webkit') +def dash_webkit(*args): + return prefix('_webkit', *args) + + +@ns.declare_alias('-owg') +def dash_owg(*args): + return prefix('_owg', *args) + + +@ns.declare_alias('-khtml') +def dash_khtml(*args): + return prefix('_khtml', *args) + + +@ns.declare_alias('-ms') +def dash_ms(*args): + return prefix('_ms', *args) + + +@ns.declare_alias('-o') +def dash_o(*args): + return prefix('_o', *args) + + +# ------------------------------------------------------------------------------ +# Selector generation + +@ns.declare +def append_selector(selector, to_append): + if isinstance(selector, List): + lst = selector.value + else: + lst = String.unquoted(selector).value.split(',') + to_append = String.unquoted(to_append).value.strip() + ret = sorted(set(s.strip() + to_append for s in lst if s.strip())) + ret = dict(enumerate(ret)) + ret['_'] = ',' + return ret + + +_elements_of_type_block = 'address, article, aside, blockquote, center, dd, details, dir, div, dl, dt, fieldset, figcaption, figure, footer, form, frameset, h1, h2, h3, h4, h5, h6, header, hgroup, hr, isindex, menu, nav, noframes, noscript, ol, p, pre, section, summary, ul' +_elements_of_type_inline = 'a, abbr, acronym, audio, b, basefont, bdo, big, br, canvas, cite, code, command, datalist, dfn, em, embed, font, i, img, input, kbd, keygen, label, mark, meter, output, progress, q, rp, rt, ruby, s, samp, select, small, span, strike, strong, sub, sup, textarea, time, tt, u, var, video, wbr' +_elements_of_type_table = 'table' +_elements_of_type_list_item = 'li' +_elements_of_type_table_row_group = 'tbody' +_elements_of_type_table_header_group = 'thead' +_elements_of_type_table_footer_group = 'tfoot' +_elements_of_type_table_row = 'tr' +_elements_of_type_table_cel = 'td, th' +_elements_of_type_html5_block = 'article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary' +_elements_of_type_html5_inline = 'audio, canvas, command, datalist, embed, keygen, mark, meter, output, progress, rp, rt, ruby, time, video, wbr' +_elements_of_type_html5 = 'article, aside, audio, canvas, command, datalist, details, embed, figcaption, figure, footer, header, hgroup, keygen, mark, menu, meter, nav, output, progress, rp, rt, ruby, section, summary, time, video, wbr' +_elements_of_type = { + 'block': sorted(_elements_of_type_block.replace(' ', '').split(',')), + 'inline': sorted(_elements_of_type_inline.replace(' ', '').split(',')), + 'table': sorted(_elements_of_type_table.replace(' ', '').split(',')), + 'list-item': sorted(_elements_of_type_list_item.replace(' ', '').split(',')), + 'table-row-group': sorted(_elements_of_type_table_row_group.replace(' ', '').split(',')), + 'table-header-group': sorted(_elements_of_type_table_header_group.replace(' ', '').split(',')), + 'table-footer-group': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')), + 'table-row': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')), + 'table-cell': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')), + 'html5-block': sorted(_elements_of_type_html5_block.replace(' ', '').split(',')), + 'html5-inline': sorted(_elements_of_type_html5_inline.replace(' ', '').split(',')), + 'html5': sorted(_elements_of_type_html5.replace(' ', '').split(',')), +} + + +@ns.declare +def elements_of_type(display): + d = String.unquoted(display) + ret = _elements_of_type.get(d.value, None) + if ret is None: + raise Exception("Elements of type '%s' not found!" % d.value) + return List(map(String, ret), use_comma=True) + + +@ns.declare +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) + + +@ns.declare_alias('headings') +@ns.declare +def headers(frm=None, to=None): + if frm and to is None: + if isinstance(frm, String) and frm.value.lower() == 'all': + frm = 1 + to = 6 + else: + try: + to = int(getattr(frm, 'value', frm)) + except ValueError: + to = 6 + frm = 1 + else: + try: + frm = 1 if frm is None else int(getattr(frm, 'value', frm)) + except ValueError: + frm = 1 + try: + to = 6 if to is None else int(getattr(to, 'value', to)) + except ValueError: + to = 6 + ret = [String.unquoted('h' + six.text_type(i)) for i in range(frm, to + 1)] + return List(ret, use_comma=True) + + +@ns.declare +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) + + +# This isn't actually from Compass, but it's just a shortcut for enumerate(). +# DEVIATION: allow reversed ranges (range() uses enumerate() which allows reversed values, like '@for .. from .. through') +@ns.declare +def range_(frm, through=None): + if through is None: + through = frm + frm = 1 + return enumerate_(None, frm, through) + +# ------------------------------------------------------------------------------ +# Working with CSS constants + +OPPOSITE_POSITIONS = { + 'top': String.unquoted('bottom'), + 'bottom': String.unquoted('top'), + 'left': String.unquoted('right'), + 'right': String.unquoted('left'), + 'center': String.unquoted('center'), +} +DEFAULT_POSITION = [String.unquoted('center'), String.unquoted('top')] + + +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() + + +@ns.declare +def position(p): + return _position(False, p) + + +@ns.declare +def opposite_position(p): + return _position(True, p) + + +# ------------------------------------------------------------------------------ +# Math + +@ns.declare +def pi(): + return Number(math.pi) + + +@ns.declare +def e(): + return Number(math.e) + + +@ns.declare +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) + + +@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 + FONTS_ROOT = _fonts_root() + if callable(FONTS_ROOT): + try: + _file, _storage = list(FONTS_ROOT(filepath))[0] + except IndexError: + filetime = None + else: + filetime = getmtime(_file, _storage) + if filetime is None: + filetime = 'NA' + elif inline: + file = _storage.open(_file) + else: + _path = os.path.join(FONTS_ROOT, filepath.strip('/')) + filetime = getmtime(_path) + if filetime is None: + filetime = 'NA' + elif inline: + file = open(_path, 'rb') + + BASE_URL = config.FONTS_URL or config.STATIC_URL + if file and inline: + font_type = None + if re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value): + font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value).groups()[1]).value + + if not FONT_TYPES.get(font_type): + raise Exception('Could not determine font type for "%s"' % path.value) + + mime = FONT_TYPES.get(font_type) + if font_type == 'woff': + mime = 'application/font-woff' + elif font_type == 'eot': + mime = 'application/vnd.ms-fontobject' + url = make_data_url( + (mime if '/' in mime else 'font/%s' % mime), + file.read()) + file.close() + else: + url = '%s/%s' % (BASE_URL.rstrip('/'), filepath.lstrip('/')) + if cache_buster and filetime != 'NA': + url = add_cache_buster(url, filetime) + + if not only_path: + url = 'url(%s)' % escape(url) + return String.unquoted(url) + + +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=',') + + +@ns.declare +def font_url(path, only_path=False, cache_buster=True): + """ + Generates a path to an asset found relative to the project's font directory. + Passing a true value as the second argument will cause the only the path to + be returned instead of a `url()` function + """ + return _font_url(path, only_path, cache_buster, False) + + +@ns.declare +def font_files(*args): + return _font_files(args, inline=False) + + +@ns.declare +def inline_font_files(*args): + return _font_files(args, inline=True) + + +# ------------------------------------------------------------------------------ +# External stylesheets + +@ns.declare +def stylesheet_url(path, only_path=False, cache_buster=True): + """ + Generates a path to an asset found relative to the project's css directory. + Passing a true value as the second argument will cause the only the path to + be returned instead of a `url()` function + """ + filepath = String.unquoted(path).value + if callable(config.STATIC_ROOT): + try: + _file, _storage = list(config.STATIC_ROOT(filepath))[0] + except IndexError: + filetime = None + else: + filetime = getmtime(_file, _storage) + if filetime is None: + filetime = 'NA' + else: + _path = os.path.join(config.STATIC_ROOT, filepath.strip('/')) + filetime = getmtime(_path) + if filetime is None: + filetime = 'NA' + BASE_URL = config.STATIC_URL + + url = '%s%s' % (BASE_URL, filepath) + if cache_buster: + url = add_cache_buster(url, filetime) + if not only_path: + url = 'url("%s")' % (url) + return String.unquoted(url) diff --git a/scss/extension/compass/images.py b/scss/extension/compass/images.py new file mode 100644 index 0000000..a3721fb --- /dev/null +++ b/scss/extension/compass/images.py @@ -0,0 +1,275 @@ +"""Image utilities ported from Compass.""" +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import logging +import mimetypes +import os.path + +import six +from six.moves import xrange + +from . import _image_size_cache +from .helpers import add_cache_buster +from scss import config +from scss.namespace import Namespace +from scss.types import Color, List, Number, String +from scss.util import escape, getmtime, make_data_url, make_filename_hash + +try: + from PIL import Image +except ImportError: + try: + import Image + except: + Image = None + +log = logging.getLogger(__name__) +ns = images_namespace = Namespace() +__all__ = ['gradients_namespace'] + + +def _images_root(): + return config.STATIC_ROOT if config.IMAGES_ROOT is None else config.IMAGES_ROOT + + +def _image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, inline=False, mime_type=None, spacing=None, collapse_x=None, collapse_y=None): + """ + src_color - a list of or a single color to be replaced by each corresponding dst_color colors + spacing - spaces to be added to the image + collapse_x, collapse_y - collapsable (layered) image of the given size (x, y) + """ + if inline or dst_color or spacing: + if not Image: + raise Exception("Images manipulation require PIL") + filepath = String.unquoted(path).value + fileext = os.path.splitext(filepath)[1].lstrip('.').lower() + if mime_type: + mime_type = String.unquoted(mime_type).value + if not mime_type: + mime_type = mimetypes.guess_type(filepath)[0] + if not mime_type: + mime_type = 'image/%s' % fileext + path = None + IMAGES_ROOT = _images_root() + if callable(IMAGES_ROOT): + try: + _file, _storage = list(IMAGES_ROOT(filepath))[0] + except IndexError: + filetime = None + else: + filetime = getmtime(_file, _storage) + if filetime is None: + filetime = 'NA' + elif inline or dst_color or spacing: + path = _storage.open(_file) + else: + _path = os.path.join(IMAGES_ROOT.rstrip(os.sep), filepath.strip('\\/')) + filetime = getmtime(_path) + if filetime is None: + filetime = 'NA' + elif inline or dst_color or spacing: + path = open(_path, 'rb') + + BASE_URL = config.IMAGES_URL or config.STATIC_URL + if path: + dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_color) if v] + + src_color = Color.from_name('black') if src_color is None else src_color + src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_color)] + + 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] + + spacing = Number(0) if spacing is None else spacing + spacing = [int(Number(v).value) for v in List.from_maybe(spacing)] + spacing = (spacing * 4)[:4] + + file_name, file_ext = os.path.splitext(os.path.normpath(filepath).replace(os.sep, '_')) + key = (filetime, src_color, dst_color, spacing) + asset_file = file_name + '-' + make_filename_hash(key) + file_ext + ASSETS_ROOT = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets') + asset_path = os.path.join(ASSETS_ROOT, asset_file) + + if os.path.exists(asset_path): + filepath = asset_file + BASE_URL = config.ASSETS_URL + if inline: + path = open(asset_path, 'rb') + url = make_data_url(mime_type, path.read()) + else: + url = '%s%s' % (BASE_URL, filepath) + if cache_buster: + filetime = getmtime(asset_path) + url = add_cache_buster(url, filetime) + else: + simply_process = False + image = None + + if fileext in ('cur',): + simply_process = True + else: + try: + image = Image.open(path) + except IOError: + if not collapse_x and not collapse_y and not dst_colors: + simply_process = True + + if simply_process: + if inline: + url = make_data_url(mime_type, path.read()) + else: + url = '%s%s' % (BASE_URL, filepath) + if cache_buster: + filetime = getmtime(asset_path) + url = add_cache_buster(url, filetime) + else: + width, height = collapse_x or image.size[0], collapse_y or image.size[1] + new_image = Image.new( + mode='RGBA', + size=(width + spacing[1] + spacing[3], height + spacing[0] + spacing[2]), + color=(0, 0, 0, 0) + ) + for i, dst_color in enumerate(dst_colors): + src_color = src_colors[i] + pixdata = image.load() + for _y in xrange(image.size[1]): + for _x in xrange(image.size[0]): + pixel = pixdata[_x, _y] + if pixel[:3] == src_color: + pixdata[_x, _y] = tuple([int(c) for c in dst_color] + [pixel[3] if len(pixel) == 4 else 255]) + iwidth, iheight = image.size + if iwidth != width or iheight != height: + cy = 0 + while cy < iheight: + cx = 0 + while cx < iwidth: + cropped_image = image.crop((cx, cy, cx + width, cy + height)) + new_image.paste(cropped_image, (int(spacing[3]), int(spacing[0])), cropped_image) + cx += width + cy += height + else: + new_image.paste(image, (int(spacing[3]), int(spacing[0]))) + + if not inline: + try: + new_image.save(asset_path) + filepath = asset_file + BASE_URL = config.ASSETS_URL + if cache_buster: + filetime = getmtime(asset_path) + except IOError: + log.exception("Error while saving image") + inline = True # Retry inline version + url = os.path.join(config.ASSETS_URL.rstrip(os.sep), asset_file.lstrip(os.sep)) + if cache_buster: + url = add_cache_buster(url, filetime) + if inline: + output = six.BytesIO() + new_image.save(output, format='PNG') + contents = output.getvalue() + output.close() + url = make_data_url(mime_type, contents) + else: + url = os.path.join(BASE_URL.rstrip('/'), filepath.lstrip('\\/')) + if cache_buster and filetime != 'NA': + url = add_cache_buster(url, filetime) + + if not os.sep == '/': + url = url.replace(os.sep, '/') + + if not only_path: + url = 'url(%s)' % escape(url) + return String.unquoted(url) + + +@ns.declare +def inline_image(image, mime_type=None, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None): + """ + Embeds the contents of a file directly inside your stylesheet, eliminating + the need for another HTTP request. For small files such images or fonts, + this can be a performance benefit at the cost of a larger generated CSS + file. + """ + return _image_url(image, False, False, dst_color, src_color, True, mime_type, spacing, collapse_x, collapse_y) + + +@ns.declare +def image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None): + """ + Generates a path to an asset found relative to the project's images + directory. + Passing a true value as the second argument will cause the only the path to + be returned instead of a `url()` function + """ + return _image_url(path, only_path, cache_buster, dst_color, src_color, False, None, spacing, collapse_x, collapse_y) + + +@ns.declare +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 Exception("Images manipulation require PIL") + 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') + + +@ns.declare +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(os.sep)) + 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') diff --git a/scss/extension/compass/layouts.py b/scss/extension/compass/layouts.py new file mode 100644 index 0000000..ae086ce --- /dev/null +++ b/scss/extension/compass/layouts.py @@ -0,0 +1,347 @@ +"""Functions used for generating packed CSS sprite maps. + +These are ported from the Binary Tree Bin Packing Algorithm: +http://codeincomplete.com/posts/2011/5/7/bin_packing/ +""" +from __future__ import absolute_import +from __future__ import unicode_literals + +# Copyright (c) 2011, 2012, 2013 Jake Gordon and contributors +# Copyright (c) 2013 German M. Bravo + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +class LayoutNode(object): + def __init__(self, x, y, w, h, down=None, right=None, used=False): + self.x = x + self.y = y + self.w = w + self.h = h + self.down = down + self.right = right + self.used = used + self.width = 0 + self.height = 0 + + @property + def area(self): + return self.width * self.height + + def __repr__(self): + return '<%s (%s, %s) [%sx%s]>' % (self.__class__.__name__, self.x, self.y, self.w, self.h) + + +class SpritesLayout(object): + def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None): + self.num_blocks = len(blocks) + + if margin is None: + margin = [[0] * 4] * self.num_blocks + elif not isinstance(margin, (tuple, list)): + margin = [[margin] * 4] * self.num_blocks + elif not isinstance(margin[0], (tuple, list)): + margin = [margin] * self.num_blocks + + if padding is None: + padding = [[0] * 4] * self.num_blocks + elif not isinstance(padding, (tuple, list)): + padding = [[padding] * 4] * self.num_blocks + elif not isinstance(padding[0], (tuple, list)): + padding = [padding] * self.num_blocks + + if pmargin is None: + pmargin = [[0.0] * 4] * self.num_blocks + elif not isinstance(pmargin, (tuple, list)): + pmargin = [[pmargin] * 4] * self.num_blocks + elif not isinstance(pmargin[0], (tuple, list)): + pmargin = [pmargin] * self.num_blocks + + if ppadding is None: + ppadding = [[0.0] * 4] * self.num_blocks + elif not isinstance(ppadding, (tuple, list)): + ppadding = [[ppadding] * 4] * self.num_blocks + elif not isinstance(ppadding[0], (tuple, list)): + ppadding = [ppadding] * self.num_blocks + + self.blocks = tuple(( + b[0] + padding[i][3] + padding[i][1] + margin[i][3] + margin[i][1] + int(round(b[0] * (ppadding[i][3] + ppadding[i][1] + pmargin[i][3] + pmargin[i][1]))), + b[1] + padding[i][0] + padding[i][2] + margin[i][0] + margin[i][2] + int(round(b[1] * (ppadding[i][0] + ppadding[i][2] + pmargin[i][0] + pmargin[i][2]))), + b[0], + b[1], + i + ) for i, b in enumerate(blocks)) + + self.margin = margin + self.padding = padding + self.pmargin = pmargin + self.ppadding = ppadding + + +class PackedSpritesLayout(SpritesLayout): + @staticmethod + def MAXSIDE(a, b): + """maxside: Sort pack by maximum sides""" + return cmp(max(b[0], b[1]), max(a[0], a[1])) or cmp(min(b[0], b[1]), min(a[0], a[1])) or cmp(b[1], a[1]) or cmp(b[0], a[0]) + + @staticmethod + def WIDTH(a, b): + """width: Sort pack by width""" + return cmp(b[0], a[0]) or cmp(b[1], a[1]) + + @staticmethod + def HEIGHT(a, b): + """height: Sort pack by height""" + return cmp(b[1], a[1]) or cmp(b[0], a[0]) + + @staticmethod + def AREA(a, b): + """area: Sort pack by area""" + return cmp(b[0] * b[1], a[0] * a[1]) or cmp(b[1], a[1]) or cmp(b[0], a[0]) + + def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, methods=None): + super(PackedSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) + + ratio = 0 + + if methods is None: + methods = (self.MAXSIDE, self.WIDTH, self.HEIGHT, self.AREA) + + for method in methods: + sorted_blocks = sorted( + self.blocks, + cmp=method, + ) + root = LayoutNode( + x=0, + y=0, + w=sorted_blocks[0][0] if sorted_blocks else 0, + h=sorted_blocks[0][1] if sorted_blocks else 0 + ) + + area = 0 + nodes = [None] * self.num_blocks + + for block in sorted_blocks: + w, h, width, height, i = block + node = self._findNode(root, w, h) + if node: + node = self._splitNode(node, w, h) + else: + root = self._growNode(root, w, h) + node = self._findNode(root, w, h) + if node: + node = self._splitNode(node, w, h) + else: + node = None + nodes[i] = node + node.width = width + node.height = height + area += node.area + + this_ratio = area / float(root.w * root.h) + # print method.__doc__, "%g%%" % (this_ratio * 100) + if ratio < this_ratio: + self.root = root + self.nodes = nodes + self.method = method + ratio = this_ratio + if ratio > 0.96: + break + # print self.method.__doc__, "%g%%" % (ratio * 100) + + def __iter__(self): + for i, node in enumerate(self.nodes): + margin, padding = self.margin[i], self.padding[i] + pmargin, ppadding = self.pmargin[i], self.ppadding[i] + cssw = node.width + padding[3] + padding[1] + int(round(node.width * (ppadding[3] + ppadding[1]))) # image width plus padding + cssh = node.height + padding[0] + padding[2] + int(round(node.height * (ppadding[0] + ppadding[2]))) # image height plus padding + cssx = node.x + margin[3] + int(round(node.width * pmargin[3])) + cssy = node.y + margin[0] + int(round(node.height * pmargin[0])) + x = cssx + padding[3] + int(round(node.width * ppadding[3])) + y = cssy + padding[0] + int(round(node.height * ppadding[0])) + yield x, y, node.width, node.height, cssx, cssy, cssw, cssh + + @property + def width(self): + return self.root.w + + @property + def height(self): + return self.root.h + + def _findNode(self, root, w, h): + if root.used: + return self._findNode(root.right, w, h) or self._findNode(root.down, w, h) + elif w <= root.w and h <= root.h: + return root + else: + return None + + def _splitNode(self, node, w, h): + node.used = True + node.down = LayoutNode( + x=node.x, + y=node.y + h, + w=node.w, + h=node.h - h + ) + node.right = LayoutNode( + x=node.x + w, + y=node.y, + w=node.w - w, + h=h + ) + return node + + def _growNode(self, root, w, h): + canGrowDown = w <= root.w + canGrowRight = h <= root.h + + shouldGrowRight = canGrowRight and (root.h >= root.w + w) # attempt to keep square-ish by growing right when height is much greater than width + shouldGrowDown = canGrowDown and (root.w >= root.h + h) # attempt to keep square-ish by growing down when width is much greater than height + + if shouldGrowRight: + return self._growRight(root, w, h) + elif shouldGrowDown: + return self._growDown(root, w, h) + elif canGrowRight: + return self._growRight(root, w, h) + elif canGrowDown: + return self._growDown(root, w, h) + else: + # need to ensure sensible root starting size to avoid this happening + assert False, "Blocks must be properly sorted!" + + def _growRight(self, root, w, h): + root = LayoutNode( + used=True, + x=0, + y=0, + w=root.w + w, + h=root.h, + down=root, + right=LayoutNode( + x=root.w, + y=0, + w=w, + h=root.h + ) + ) + return root + + def _growDown(self, root, w, h): + root = LayoutNode( + used=True, + x=0, + y=0, + w=root.w, + h=root.h + h, + down=LayoutNode( + x=0, + y=root.h, + w=root.w, + h=h + ), + right=root + ) + return root + + +class HorizontalSpritesLayout(SpritesLayout): + def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, position=None): + super(HorizontalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) + + self.width = sum(block[0] for block in self.blocks) + self.height = max(block[1] for block in self.blocks) + + if position is None: + position = [0.0] * self.num_blocks + elif not isinstance(position, (tuple, list)): + position = [position] * self.num_blocks + self.position = position + + def __iter__(self): + cx = 0 + for i, block in enumerate(self.blocks): + w, h, width, height, i = block + margin, padding = self.margin[i], self.padding[i] + pmargin, ppadding = self.pmargin[i], self.ppadding[i] + position = self.position[i] + cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding + cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding + cssx = cx + margin[3] + int(round(width * pmargin[3])) # anchored at x + cssy = int(round((self.height - cssh) * position)) # centered vertically + x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding + y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding + yield x, y, width, height, cssx, cssy, cssw, cssh + cx += cssw + margin[3] + margin[1] + int(round(width * (pmargin[3] + pmargin[1]))) + + +class VerticalSpritesLayout(SpritesLayout): + def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, position=None): + super(VerticalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) + + self.width = max(block[0] for block in self.blocks) + self.height = sum(block[1] for block in self.blocks) + + if position is None: + position = [0.0] * self.num_blocks + elif not isinstance(position, (tuple, list)): + position = [position] * self.num_blocks + self.position = position + + def __iter__(self): + cy = 0 + for i, block in enumerate(self.blocks): + w, h, width, height, i = block + margin, padding = self.margin[i], self.padding[i] + pmargin, ppadding = self.pmargin[i], self.ppadding[i] + position = self.position[i] + cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding + cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding + cssx = int(round((self.width - cssw) * position)) # centered horizontally + cssy = cy + margin[0] + int(round(height * pmargin[0])) # anchored at y + x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding + y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding + yield x, y, width, height, cssx, cssy, cssw, cssh + cy += cssh + margin[0] + margin[2] + int(round(height * (pmargin[0] + pmargin[2]))) + + +class DiagonalSpritesLayout(SpritesLayout): + def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None): + super(DiagonalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) + self.width = sum(block[0] for block in self.blocks) + self.height = sum(block[1] for block in self.blocks) + + def __iter__(self): + cx, cy = 0, 0 + for i, block in enumerate(self.blocks): + w, h, width, height, i = block + margin, padding = self.margin[i], self.padding[i] + pmargin, ppadding = self.pmargin[i], self.ppadding[i] + cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding + cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding + cssx = cx + margin[3] + int(round(width * pmargin[3])) # anchored at x + cssy = cy + margin[0] + int(round(height * pmargin[0])) # anchored at y + x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding + y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding + yield x, y, width, height, cssx, cssy, cssw, cssh + cx += cssw + margin[3] + margin[1] + int(round(width * (pmargin[3] + pmargin[1]))) + cy += cssh + margin[0] + margin[2] + int(round(height * (pmargin[0] + pmargin[2]))) diff --git a/scss/extension/compass/sprites.py b/scss/extension/compass/sprites.py new file mode 100644 index 0000000..d3fea1d --- /dev/null +++ b/scss/extension/compass/sprites.py @@ -0,0 +1,559 @@ +"""Functions used for generating CSS sprites. + +These are ported from the Compass sprite library: +http://compass-style.org/reference/compass/utilities/sprites/ +""" +from __future__ import absolute_import +from __future__ import unicode_literals + +import six + +import glob +import logging +import os.path +import tempfile +import time +import sys + +try: + import cPickle as pickle +except ImportError: + import pickle + +try: + from PIL import Image +except ImportError: + try: + import Image + except: + Image = None + +from six.moves import xrange + +from . import _image_size_cache +from .layouts import PackedSpritesLayout, HorizontalSpritesLayout, VerticalSpritesLayout, DiagonalSpritesLayout +from scss import config +from scss.namespace import Namespace +from scss.types import Color, List, Number, String, Boolean +from scss.util import escape, getmtime, make_data_url, make_filename_hash + +log = logging.getLogger(__name__) +ns = sprites_namespace = Namespace() +__all__ = ['gradients_namespace'] + +MAX_SPRITE_MAPS = 4096 +KEEP_SPRITE_MAPS = int(MAX_SPRITE_MAPS * 0.8) + + +# ------------------------------------------------------------------------------ +# Compass-like functionality for sprites and images + +sprite_maps = {} + + +def alpha_composite(im1, im2, offset=None, box=None, opacity=1): + im1size = im1.size + im2size = im2.size + if offset is None: + offset = (0, 0) + if box is None: + box = (0, 0) + im2size + o1x, o1y = offset + o2x, o2y, o2w, o2h = box + width = o2w - o2x + height = o2h - o2y + im1_data = im1.load() + im2_data = im2.load() + for y in xrange(height): + for x in xrange(width): + pos1 = o1x + x, o1y + y + if pos1[0] >= im1size[0] or pos1[1] >= im1size[1]: + continue + pos2 = o2x + x, o2y + y + if pos2[0] >= im2size[0] or pos2[1] >= im2size[1]: + continue + dr, dg, db, da = im1_data[pos1] + sr, sg, sb, sa = im2_data[pos2] + da /= 255.0 + sa /= 255.0 + sa *= opacity + ida = da * (1 - sa) + oa = (sa + ida) + if oa: + pixel = ( + int(round((sr * sa + dr * ida)) / oa), + int(round((sg * sa + dg * ida)) / oa), + int(round((sb * sa + db * ida)) / oa), + int(round(255 * oa)) + ) + else: + pixel = (0, 0, 0, 0) + im1_data[pos1] = pixel + return im1 + + +@ns.declare +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`) + $-position - Position of a given sprite. + + $padding, $spacing - Adds paddings to sprites (top, right, bottom, left). (defaults to `0, 0, 0, 0`) + $-padding, $-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`). + $-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`) + $-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() + + globs = String(g, quotes=None).value + globs = sorted(g.strip() for g in globs.split(',')) + + _k_ = ','.join(globs) + + files = None + rfiles = None + tfiles = None + map_name = None + + if _k_ in sprite_maps: + sprite_maps[_k_]['*'] = now_time + else: + files = [] + rfiles = [] + tfiles = [] + for _glob in globs: + if '..' not in _glob: # Protect against going to prohibited places... + if callable(config.STATIC_ROOT): + _glob_path = _glob + _rfiles = _files = sorted(config.STATIC_ROOT(_glob)) + else: + _glob_path = os.path.join(config.STATIC_ROOT, _glob) + _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 _files: + files.extend(_files) + rfiles.extend(_rfiles) + base_name = os.path.normpath(os.path.dirname(_glob)).replace('\\', '_').replace('/', '_') + _map_name, _, _map_type = base_name.partition('.') + if _map_type: + _map_type += '-' + if not map_name: + map_name = _map_name + tfiles.extend([_map_type] * len(_files)) + else: + glob_path = _glob_path + + if files is not None: + if not files: + log.error("Nothing found at '%s'", glob_path) + return String.unquoted('') + + 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) + tnames = tuple(tfiles[i] + n for i, n in enumerate(names)) + + 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(tnames, 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() + if sys.platform == 'win32' and os.path.isfile(cache_path): + # on windows, cannot rename a file to a path that matches + # an existing file, we have to remove it first + os.remove(cache_path) + 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 + + +@ns.declare +def sprite_map_name(map): + """ + Returns the name of a sprite map The name is derived from the folder than + contains the sprites. + """ + map = map.render() + sprite_map = sprite_maps.get(map) + if not sprite_map: + log.error("No sprite map found: %s", map, extra={'stack': True}) + if sprite_map: + return String.unquoted(sprite_map['*n*']) + return String.unquoted('') + + +@ns.declare +def sprite_file(map, sprite): + """ + Returns the relative path (from the images directory) to the original file + used when construction the sprite. This is suitable for passing to the + image_width and image_height helpers. + """ + 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: + return String(sprite[1][0]) + return String.unquoted('') + + +@ns.declare_alias('sprite-names') +@ns.declare +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('*')))]) + + +@ns.declare +def sprite_classes(map): + return sprites(map, True) + + +@ns.declare +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)]) + + +@ns.declare +def sprite_url(map, cache_buster=True): + """ + Returns a url to the sprite image. + """ + map = map.render() + sprite_map = sprite_maps.get(map) + if not sprite_map: + log.error("No sprite map found: %s", map, extra={'stack': True}) + if sprite_map: + url = '%s%s' % (config.ASSETS_URL, sprite_map['*f*']) + if cache_buster: + url += '?_=%s' % sprite_map['*t*'] + url = "url(%s)" % escape(url) + return String.unquoted(url) + return String.unquoted('') + + +@ns.declare +def has_sprite(map, sprite): + 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}) + return Boolean(bool(sprite)) + + +@ns.declare +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)]) diff --git a/scss/functions/compass/__init__.py b/scss/functions/compass/__init__.py deleted file mode 100644 index 73cb9df..0000000 --- a/scss/functions/compass/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Global cache of image sizes, shared between sprites and images libraries. -_image_size_cache = {} diff --git a/scss/functions/compass/gradients.py b/scss/functions/compass/gradients.py deleted file mode 100644 index 2d92ba5..0000000 --- a/scss/functions/compass/gradients.py +++ /dev/null @@ -1,434 +0,0 @@ -"""Utilities for working with gradients. Inspired by Compass, but not quite -the same. -""" -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - -import base64 -import logging - -import six - -from scss.functions.library import FunctionLibrary -from scss.functions.compass.helpers import opposite_position, position -from scss.types import Color, List, Number, String -from scss.util import escape, split_params, to_float, to_str - -log = logging.getLogger(__name__) - -COMPASS_GRADIENTS_LIBRARY = FunctionLibrary() -register = COMPASS_GRADIENTS_LIBRARY.register - - -# ------------------------------------------------------------------------------ - -def __color_stops(percentages, *args): - if len(args) == 1: - if isinstance(args[0], (list, tuple, List)): - list(args[0]) - elif isinstance(args[0], (String, six.string_types)): - color_stops = [] - colors = split_params(getattr(args[0], 'value', args[0])) - for color in colors: - color = color.strip() - if color.startswith('color-stop('): - s, c = split_params(color[11:].rstrip(')')) - s = s.strip() - c = c.strip() - else: - c, s = color.split() - color_stops.append((to_float(s), c)) - return color_stops - - colors = [] - stops = [] - prev_color = False - for c in args: - for c in List.from_maybe(c): - if isinstance(c, Color): - if prev_color: - stops.append(None) - colors.append(c) - prev_color = True - elif isinstance(c, Number): - stops.append(c) - prev_color = False - - if prev_color: - stops.append(None) - stops = stops[:len(colors)] - if stops[0] is None: - stops[0] = Number(0, '%') - if stops[-1] is None: - stops[-1] = Number(100, '%') - - maxable_stops = [s for s in stops if s and not s.is_simple_unit('%')] - if maxable_stops: - max_stops = max(maxable_stops) - else: - max_stops = None - - stops = [_s / max_stops if _s and not _s.is_simple_unit('%') else _s for _s in stops] - - init = 0 - start = None - for i, s in enumerate(stops + [1.0]): - if s is None: - if start is None: - start = i - end = i - else: - final = s - if start is not None: - stride = (final - init) / Number(end - start + 1 + (1 if i < len(stops) else 0)) - for j in range(start, end + 1): - stops[j] = init + stride * Number(j - start + 1) - init = final - start = None - - if not max_stops or percentages: - pass - else: - stops = [s if s.is_simple_unit('%') else s * max_stops for s in stops] - - return List(List(pair) for pair in zip(stops, colors)) - - -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) - - -@register('grad-color-stops') -def grad_color_stops(*args): - args = List.from_maybe_starargs(args) - color_stops = __color_stops(True, *args) - ret = ', '.join(['color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops]) - return String.unquoted(ret) - - -def __grad_end_position(radial, color_stops): - return __grad_position(-1, 100, radial, color_stops) - - -@register('grad-point') -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 __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 - - -@register('grad-end-position') -def grad_end_position(*color_stops): - color_stops = __color_stops(False, *color_stops) - return Number(__grad_end_position(False, color_stops)) - - -@register('color-stops') -def color_stops(*args): - args = List.from_maybe_starargs(args) - color_stops = __color_stops(False, *args) - ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops]) - return String.unquoted(ret) - - -@register('color-stops-in-percentages') -def color_stops_in_percentages(*args): - args = List.from_maybe_starargs(args) - color_stops = __color_stops(True, *args) - ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops]) - return String.unquoted(ret) - - -def _get_gradient_position_and_angle(args): - for arg in args: - ret = None - skip = False - for a in arg: - if isinstance(a, Color): - skip = True - break - elif isinstance(a, Number): - ret = arg - if skip: - continue - if ret is not None: - return ret - for seek in ( - 'center', - 'top', 'bottom', - 'left', 'right', - ): - if String(seek) in arg: - return arg - return None - - -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 _get_gradient_color_stops(args): - color_stops = [] - for arg in args: - for a in List.from_maybe(arg): - if isinstance(a, Color): - color_stops.append(arg) - break - return color_stops or None - - -# TODO these functions need to be -# 1. well-defined -# 2. guaranteed to never wreck css3 syntax -# 3. updated to whatever current compass does -# 4. fixed to use a custom type instead of monkeypatching - - -@register('radial-gradient') -def radial_gradient(*args): - args = List.from_maybe_starargs(args) - - try: - # Do a rough check for standard syntax first -- `shape at position` - at_position = list(args[0]).index(String('at')) - except (IndexError, ValueError): - shape_and_size = _get_gradient_shape_and_size(args) - position_and_angle = _get_gradient_position_and_angle(args) - else: - shape_and_size = List.maybe_new(args[0][:at_position]) - position_and_angle = List.maybe_new(args[0][at_position + 1:]) - - color_stops = _get_gradient_color_stops(args) - if color_stops is None: - raise Exception('No color stops provided to radial-gradient function') - color_stops = __color_stops(False, *color_stops) - - if position_and_angle: - rendered_position = position(position_and_angle) - else: - rendered_position = None - rendered_color_stops = _render_standard_color_stops(color_stops) - - args = [] - if shape_and_size and rendered_position: - args.append(List([shape_and_size, String.unquoted('at'), rendered_position], use_comma=False)) - elif rendered_position: - args.append(rendered_position) - elif shape_and_size: - args.append(shape_and_size) - args.extend(rendered_color_stops) - - legacy_args = [] - if rendered_position: - legacy_args.append(rendered_position) - if shape_and_size: - legacy_args.append(shape_and_size) - legacy_args.extend(rendered_color_stops) - - ret = String.unquoted( - 'radial-gradient(' + ', '.join(a.render() for a in args) + ')') - - legacy_ret = 'radial-gradient(' + ', '.join(a.render() for a in legacy_args) + ')' - - def to__css2(): - return String.unquoted('') - ret.to__css2 = to__css2 - - def to__moz(): - return String.unquoted('-moz-' + legacy_ret) - ret.to__moz = to__moz - - def to__pie(): - log.warn("PIE does not support radial-gradient.") - return String.unquoted('-pie-radial-gradient(unsupported)') - ret.to__pie = to__pie - - def to__webkit(): - return String.unquoted('-webkit-' + legacy_ret) - ret.to__webkit = to__webkit - - def to__owg(): - args = [ - 'radial', - grad_point(*position_and_angle) if position_and_angle is not None else 'center', - '0', - grad_point(*position_and_angle) if position_and_angle is not None else 'center', - __grad_end_position(True, color_stops), - ] - args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops) - ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')' - return String.unquoted(ret) - ret.to__owg = to__owg - - def to__svg(): - return radial_svg_gradient(*(list(color_stops) + list(position_and_angle or [String('center')]))) - ret.to__svg = to__svg - - return ret - - -@register('linear-gradient') -def linear_gradient(*args): - args = List.from_maybe_starargs(args) - - position_and_angle = _get_gradient_position_and_angle(args) - color_stops = _get_gradient_color_stops(args) - if color_stops is None: - raise Exception('No color stops provided to linear-gradient function') - color_stops = __color_stops(False, *color_stops) - - args = [ - position(position_and_angle) if position_and_angle is not None else None, - ] - args.extend(_render_standard_color_stops(color_stops)) - - to__s = 'linear-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')' - ret = String.unquoted(to__s) - - def to__css2(): - return String.unquoted('') - ret.to__css2 = to__css2 - - def to__moz(): - return String.unquoted('-moz-' + to__s) - ret.to__moz = to__moz - - def to__pie(): - return String.unquoted('-pie-' + to__s) - ret.to__pie = to__pie - - def to__ms(): - return String.unquoted('-ms-' + to__s) - ret.to__ms = to__ms - - def to__o(): - return String.unquoted('-o-' + to__s) - ret.to__o = to__o - - def to__webkit(): - return String.unquoted('-webkit-' + to__s) - ret.to__webkit = to__webkit - - def to__owg(): - args = [ - 'linear', - position(position_and_angle or None), - opposite_position(position_and_angle or None), - ] - args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops) - ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args if a is not None) + ')' - return String.unquoted(ret) - ret.to__owg = to__owg - - def to__svg(): - return linear_svg_gradient(color_stops, position_and_angle or 'top') - ret.to__svg = to__svg - - return ret - - -@register('radial-svg-gradient') -def radial_svg_gradient(*args): - args = List.from_maybe_starargs(args) - color_stops = args - center = None - if isinstance(args[-1], (String, Number)): - center = args[-1] - color_stops = args[:-1] - color_stops = __color_stops(False, *color_stops) - cx, cy = grad_point(center) - r = __grad_end_position(True, color_stops) - svg = __radial_svg(color_stops, cx, cy, r) - url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg) - inline = 'url("%s")' % escape(url) - return String.unquoted(inline) - - -@register('linear-svg-gradient') -def linear_svg_gradient(*args): - args = List.from_maybe_starargs(args) - color_stops = args - start = None - if isinstance(args[-1], (String, Number)): - start = args[-1] - color_stops = args[:-1] - color_stops = __color_stops(False, *color_stops) - x1, y1 = grad_point(start) - x2, y2 = grad_point(opposite_position(start)) - svg = _linear_svg(color_stops, x1, y1, x2, y2) - url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg) - inline = 'url("%s")' % escape(url) - return String.unquoted(inline) - - -def __color_stops_svg(color_stops): - ret = ''.join('' % (to_str(s), c) for s, c in color_stops) - return ret - - -def __svg_template(gradient): - ret = '\ -\ -%s\ -\ -' % gradient - return ret - - -def _linear_svg(color_stops, x1, y1, x2, y2): - gradient = '%s' % ( - 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 __radial_svg(color_stops, cx, cy, r): - gradient = '%s' % ( - to_str(Number(cx)), - to_str(Number(cy)), - to_str(Number(r)), - __color_stops_svg(color_stops) - ) - return __svg_template(gradient) diff --git a/scss/functions/compass/helpers.py b/scss/functions/compass/helpers.py deleted file mode 100644 index 2cbb94b..0000000 --- a/scss/functions/compass/helpers.py +++ /dev/null @@ -1,656 +0,0 @@ -"""Miscellaneous helper functions ported from Compass. - -See: http://compass-style.org/reference/compass/helpers/ - -This collection is not necessarily complete or up-to-date. -""" -from __future__ import absolute_import -from __future__ import unicode_literals - -import logging -import math -import os.path - -import six - -from scss import config -from scss.functions.library import FunctionLibrary -from scss.types import Boolean, List, Null, Number, String -from scss.util import escape, to_str, getmtime, make_data_url -import re - -log = logging.getLogger(__name__) - - -COMPASS_HELPERS_LIBRARY = FunctionLibrary() -register = COMPASS_HELPERS_LIBRARY.register - -FONT_TYPES = { - 'woff': 'woff', - 'otf': 'opentype', - 'opentype': 'opentype', - 'ttf': 'truetype', - 'truetype': 'truetype', - 'svg': 'svg', - 'eot': 'embedded-opentype' -} - - -def add_cache_buster(url, mtime): - fragment = url.split('#') - query = fragment[0].split('?') - if len(query) > 1 and query[1] != '': - cb = '&_=%s' % (mtime) - url = '?'.join(query) + cb - else: - cb = '?_=%s' % (mtime) - url = query[0] + cb - if len(fragment) > 1: - url += '#' + fragment[1] - return url - - -# ------------------------------------------------------------------------------ -# Data manipulation - -@register('blank') -def blank(*objs): - """Returns true when the object is false, an empty string, or an empty list""" - for o in objs: - if isinstance(o, Boolean): - is_blank = not o - elif isinstance(o, String): - is_blank = not len(o.value.strip()) - elif isinstance(o, List): - is_blank = all(blank(el) for el in o) - else: - is_blank = False - - if not is_blank: - return Boolean(False) - - return Boolean(True) - - -@register('compact') -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, - ) - - -@register('reject') -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) - - -@register('first-value-of') -def first_value_of(*args): - if len(args) == 1 and isinstance(args[0], String): - first = args[0].value.split()[0] - return type(args[0])(first) - - args = List.from_maybe_starargs(args) - if len(args): - return args[0] - else: - return Null() - - -@register('-compass-list') -def dash_compass_list(*args): - return List.from_maybe_starargs(args) - - -@register('-compass-space-list') -def dash_compass_space_list(*lst): - """ - If the argument is a list, it will return a new list that is space delimited - Otherwise it returns a new, single element, space-delimited list. - """ - ret = dash_compass_list(*lst) - ret.value.pop('_', None) - return ret - - -@register('-compass-slice', 3) -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) - - -# ------------------------------------------------------------------------------ -# Property prefixing - -@register('prefixed') -def prefixed(prefix, *args): - to_fnct_str = 'to_' + to_str(prefix).replace('-', '_') - for arg in List.from_maybe_starargs(args): - if hasattr(arg, to_fnct_str): - return Boolean(True) - return Boolean(False) - - -@register('prefix') -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) - - -@register('-moz') -def dash_moz(*args): - return prefix('_moz', *args) - - -@register('-svg') -def dash_svg(*args): - return prefix('_svg', *args) - - -@register('-css2') -def dash_css2(*args): - return prefix('_css2', *args) - - -@register('-pie') -def dash_pie(*args): - return prefix('_pie', *args) - - -@register('-webkit') -def dash_webkit(*args): - return prefix('_webkit', *args) - - -@register('-owg') -def dash_owg(*args): - return prefix('_owg', *args) - - -@register('-khtml') -def dash_khtml(*args): - return prefix('_khtml', *args) - - -@register('-ms') -def dash_ms(*args): - return prefix('_ms', *args) - - -@register('-o') -def dash_o(*args): - return prefix('_o', *args) - - -# ------------------------------------------------------------------------------ -# Selector generation - -@register('append-selector', 2) -def append_selector(selector, to_append): - if isinstance(selector, List): - lst = selector.value - else: - lst = String.unquoted(selector).value.split(',') - to_append = String.unquoted(to_append).value.strip() - ret = sorted(set(s.strip() + to_append for s in lst if s.strip())) - ret = dict(enumerate(ret)) - ret['_'] = ',' - return ret - - -_elements_of_type_block = 'address, article, aside, blockquote, center, dd, details, dir, div, dl, dt, fieldset, figcaption, figure, footer, form, frameset, h1, h2, h3, h4, h5, h6, header, hgroup, hr, isindex, menu, nav, noframes, noscript, ol, p, pre, section, summary, ul' -_elements_of_type_inline = 'a, abbr, acronym, audio, b, basefont, bdo, big, br, canvas, cite, code, command, datalist, dfn, em, embed, font, i, img, input, kbd, keygen, label, mark, meter, output, progress, q, rp, rt, ruby, s, samp, select, small, span, strike, strong, sub, sup, textarea, time, tt, u, var, video, wbr' -_elements_of_type_table = 'table' -_elements_of_type_list_item = 'li' -_elements_of_type_table_row_group = 'tbody' -_elements_of_type_table_header_group = 'thead' -_elements_of_type_table_footer_group = 'tfoot' -_elements_of_type_table_row = 'tr' -_elements_of_type_table_cel = 'td, th' -_elements_of_type_html5_block = 'article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary' -_elements_of_type_html5_inline = 'audio, canvas, command, datalist, embed, keygen, mark, meter, output, progress, rp, rt, ruby, time, video, wbr' -_elements_of_type_html5 = 'article, aside, audio, canvas, command, datalist, details, embed, figcaption, figure, footer, header, hgroup, keygen, mark, menu, meter, nav, output, progress, rp, rt, ruby, section, summary, time, video, wbr' -_elements_of_type = { - 'block': sorted(_elements_of_type_block.replace(' ', '').split(',')), - 'inline': sorted(_elements_of_type_inline.replace(' ', '').split(',')), - 'table': sorted(_elements_of_type_table.replace(' ', '').split(',')), - 'list-item': sorted(_elements_of_type_list_item.replace(' ', '').split(',')), - 'table-row-group': sorted(_elements_of_type_table_row_group.replace(' ', '').split(',')), - 'table-header-group': sorted(_elements_of_type_table_header_group.replace(' ', '').split(',')), - 'table-footer-group': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')), - 'table-row': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')), - 'table-cell': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')), - 'html5-block': sorted(_elements_of_type_html5_block.replace(' ', '').split(',')), - 'html5-inline': sorted(_elements_of_type_html5_inline.replace(' ', '').split(',')), - 'html5': sorted(_elements_of_type_html5.replace(' ', '').split(',')), -} - - -@register('elements-of-type', 1) -def elements_of_type(display): - d = String.unquoted(display) - ret = _elements_of_type.get(d.value, None) - if ret is None: - raise Exception("Elements of type '%s' not found!" % d.value) - return List(map(String, ret), use_comma=True) - - -@register('enumerate', 3) -@register('enumerate', 4) -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) - - -@register('headers', 0) -@register('headers', 1) -@register('headers', 2) -@register('headings', 0) -@register('headings', 1) -@register('headings', 2) -def headers(frm=None, to=None): - if frm and to is None: - if isinstance(frm, String) and frm.value.lower() == 'all': - frm = 1 - to = 6 - else: - try: - to = int(getattr(frm, 'value', frm)) - except ValueError: - to = 6 - frm = 1 - else: - try: - frm = 1 if frm is None else int(getattr(frm, 'value', frm)) - except ValueError: - frm = 1 - try: - to = 6 if to is None else int(getattr(to, 'value', to)) - except ValueError: - to = 6 - ret = [String.unquoted('h' + six.text_type(i)) for i in range(frm, to + 1)] - return List(ret, use_comma=True) - - -@register('nest') -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) - - -# This isn't actually from Compass, but it's just a shortcut for enumerate(). -# DEVIATION: allow reversed ranges (range() uses enumerate() which allows reversed values, like '@for .. from .. through') -@register('range', 1) -@register('range', 2) -def range_(frm, through=None): - if through is None: - through = frm - frm = 1 - return enumerate_(None, frm, through) - -# ------------------------------------------------------------------------------ -# Working with CSS constants - -OPPOSITE_POSITIONS = { - 'top': String.unquoted('bottom'), - 'bottom': String.unquoted('top'), - 'left': String.unquoted('right'), - 'right': String.unquoted('left'), - 'center': String.unquoted('center'), -} -DEFAULT_POSITION = [String.unquoted('center'), String.unquoted('top')] - - -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() - - -@register('position') -def position(p): - return _position(False, p) - - -@register('opposite-position') -def opposite_position(p): - return _position(True, p) - - -# ------------------------------------------------------------------------------ -# Math - -@register('pi', 0) -def pi(): - return Number(math.pi) - - -@register('e', 0) -def e(): - return Number(math.e) - - -@register('log', 1) -@register('log', 2) -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) - - -@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 - FONTS_ROOT = _fonts_root() - if callable(FONTS_ROOT): - try: - _file, _storage = list(FONTS_ROOT(filepath))[0] - except IndexError: - filetime = None - else: - filetime = getmtime(_file, _storage) - if filetime is None: - filetime = 'NA' - elif inline: - file = _storage.open(_file) - else: - _path = os.path.join(FONTS_ROOT, filepath.strip('/')) - filetime = getmtime(_path) - if filetime is None: - filetime = 'NA' - elif inline: - file = open(_path, 'rb') - - BASE_URL = config.FONTS_URL or config.STATIC_URL - if file and inline: - font_type = None - if re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value): - font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value).groups()[1]).value - - if not FONT_TYPES.get(font_type): - raise Exception('Could not determine font type for "%s"' % path.value) - - mime = FONT_TYPES.get(font_type) - if font_type == 'woff': - mime = 'application/font-woff' - elif font_type == 'eot': - mime = 'application/vnd.ms-fontobject' - url = make_data_url( - (mime if '/' in mime else 'font/%s' % mime), - file.read()) - file.close() - else: - url = '%s/%s' % (BASE_URL.rstrip('/'), filepath.lstrip('/')) - if cache_buster and filetime != 'NA': - url = add_cache_buster(url, filetime) - - if not only_path: - url = 'url(%s)' % escape(url) - return String.unquoted(url) - - -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=',') - - -@register('font-url', 1) -@register('font-url', 2) -def font_url(path, only_path=False, cache_buster=True): - """ - Generates a path to an asset found relative to the project's font directory. - Passing a true value as the second argument will cause the only the path to - be returned instead of a `url()` function - """ - return _font_url(path, only_path, cache_buster, False) - - -@register('font-files') -def font_files(*args): - return _font_files(args, inline=False) - - -@register('inline-font-files') -def inline_font_files(*args): - return _font_files(args, inline=True) - - -# ------------------------------------------------------------------------------ -# External stylesheets - -@register('stylesheet-url', 1) -@register('stylesheet-url', 2) -def stylesheet_url(path, only_path=False, cache_buster=True): - """ - Generates a path to an asset found relative to the project's css directory. - Passing a true value as the second argument will cause the only the path to - be returned instead of a `url()` function - """ - filepath = String.unquoted(path).value - if callable(config.STATIC_ROOT): - try: - _file, _storage = list(config.STATIC_ROOT(filepath))[0] - except IndexError: - filetime = None - else: - filetime = getmtime(_file, _storage) - if filetime is None: - filetime = 'NA' - else: - _path = os.path.join(config.STATIC_ROOT, filepath.strip('/')) - filetime = getmtime(_path) - if filetime is None: - filetime = 'NA' - BASE_URL = config.STATIC_URL - - url = '%s%s' % (BASE_URL, filepath) - if cache_buster: - url = add_cache_buster(url, filetime) - if not only_path: - url = 'url("%s")' % (url) - return String.unquoted(url) diff --git a/scss/functions/compass/images.py b/scss/functions/compass/images.py deleted file mode 100644 index e9b4e6c..0000000 --- a/scss/functions/compass/images.py +++ /dev/null @@ -1,287 +0,0 @@ -"""Image utilities ported from Compass.""" -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - -import logging -import mimetypes -import os.path - -import six -from six.moves import xrange - -from scss import config -from scss.functions.compass import _image_size_cache -from scss.functions.compass.helpers import add_cache_buster -from scss.functions.library import FunctionLibrary -from scss.types import Color, List, Number, String -from scss.util import escape, getmtime, make_data_url, make_filename_hash - -try: - from PIL import Image -except ImportError: - try: - import Image - except: - Image = None - -log = logging.getLogger(__name__) - -COMPASS_IMAGES_LIBRARY = FunctionLibrary() -register = COMPASS_IMAGES_LIBRARY.register - - -# ------------------------------------------------------------------------------ - -def _images_root(): - return config.STATIC_ROOT if config.IMAGES_ROOT is None else config.IMAGES_ROOT - - -def _image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, inline=False, mime_type=None, spacing=None, collapse_x=None, collapse_y=None): - """ - src_color - a list of or a single color to be replaced by each corresponding dst_color colors - spacing - spaces to be added to the image - collapse_x, collapse_y - collapsable (layered) image of the given size (x, y) - """ - if inline or dst_color or spacing: - if not Image: - raise Exception("Images manipulation require PIL") - filepath = String.unquoted(path).value - fileext = os.path.splitext(filepath)[1].lstrip('.').lower() - if mime_type: - mime_type = String.unquoted(mime_type).value - if not mime_type: - mime_type = mimetypes.guess_type(filepath)[0] - if not mime_type: - mime_type = 'image/%s' % fileext - path = None - IMAGES_ROOT = _images_root() - if callable(IMAGES_ROOT): - try: - _file, _storage = list(IMAGES_ROOT(filepath))[0] - except IndexError: - filetime = None - else: - filetime = getmtime(_file, _storage) - if filetime is None: - filetime = 'NA' - elif inline or dst_color or spacing: - path = _storage.open(_file) - else: - _path = os.path.join(IMAGES_ROOT.rstrip(os.sep), filepath.strip('\\/')) - filetime = getmtime(_path) - if filetime is None: - filetime = 'NA' - elif inline or dst_color or spacing: - path = open(_path, 'rb') - - BASE_URL = config.IMAGES_URL or config.STATIC_URL - if path: - dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_color) if v] - - src_color = Color.from_name('black') if src_color is None else src_color - src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_color)] - - 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] - - spacing = Number(0) if spacing is None else spacing - spacing = [int(Number(v).value) for v in List.from_maybe(spacing)] - spacing = (spacing * 4)[:4] - - file_name, file_ext = os.path.splitext(os.path.normpath(filepath).replace(os.sep, '_')) - key = (filetime, src_color, dst_color, spacing) - asset_file = file_name + '-' + make_filename_hash(key) + file_ext - ASSETS_ROOT = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets') - asset_path = os.path.join(ASSETS_ROOT, asset_file) - - if os.path.exists(asset_path): - filepath = asset_file - BASE_URL = config.ASSETS_URL - if inline: - path = open(asset_path, 'rb') - url = make_data_url(mime_type, path.read()) - else: - url = '%s%s' % (BASE_URL, filepath) - if cache_buster: - filetime = getmtime(asset_path) - url = add_cache_buster(url, filetime) - else: - simply_process = False - image = None - - if fileext in ('cur',): - simply_process = True - else: - try: - image = Image.open(path) - except IOError: - if not collapse_x and not collapse_y and not dst_colors: - simply_process = True - - if simply_process: - if inline: - url = make_data_url(mime_type, path.read()) - else: - url = '%s%s' % (BASE_URL, filepath) - if cache_buster: - filetime = getmtime(asset_path) - url = add_cache_buster(url, filetime) - else: - width, height = collapse_x or image.size[0], collapse_y or image.size[1] - new_image = Image.new( - mode='RGBA', - size=(width + spacing[1] + spacing[3], height + spacing[0] + spacing[2]), - color=(0, 0, 0, 0) - ) - for i, dst_color in enumerate(dst_colors): - src_color = src_colors[i] - pixdata = image.load() - for _y in xrange(image.size[1]): - for _x in xrange(image.size[0]): - pixel = pixdata[_x, _y] - if pixel[:3] == src_color: - pixdata[_x, _y] = tuple([int(c) for c in dst_color] + [pixel[3] if len(pixel) == 4 else 255]) - iwidth, iheight = image.size - if iwidth != width or iheight != height: - cy = 0 - while cy < iheight: - cx = 0 - while cx < iwidth: - cropped_image = image.crop((cx, cy, cx + width, cy + height)) - new_image.paste(cropped_image, (int(spacing[3]), int(spacing[0])), cropped_image) - cx += width - cy += height - else: - new_image.paste(image, (int(spacing[3]), int(spacing[0]))) - - if not inline: - try: - new_image.save(asset_path) - filepath = asset_file - BASE_URL = config.ASSETS_URL - if cache_buster: - filetime = getmtime(asset_path) - except IOError: - log.exception("Error while saving image") - inline = True # Retry inline version - url = os.path.join(config.ASSETS_URL.rstrip(os.sep), asset_file.lstrip(os.sep)) - if cache_buster: - url = add_cache_buster(url, filetime) - if inline: - output = six.BytesIO() - new_image.save(output, format='PNG') - contents = output.getvalue() - output.close() - url = make_data_url(mime_type, contents) - else: - url = os.path.join(BASE_URL.rstrip('/'), filepath.lstrip('\\/')) - if cache_buster and filetime != 'NA': - url = add_cache_buster(url, filetime) - - if not os.sep == '/': - url = url.replace(os.sep, '/') - - if not only_path: - url = 'url(%s)' % escape(url) - return String.unquoted(url) - - -@register('inline-image', 1) -@register('inline-image', 2) -@register('inline-image', 3) -@register('inline-image', 4) -@register('inline-image', 5) -def inline_image(image, mime_type=None, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None): - """ - Embeds the contents of a file directly inside your stylesheet, eliminating - the need for another HTTP request. For small files such images or fonts, - this can be a performance benefit at the cost of a larger generated CSS - file. - """ - return _image_url(image, False, False, dst_color, src_color, True, mime_type, spacing, collapse_x, collapse_y) - - -@register('image-url', 1) -@register('image-url', 2) -@register('image-url', 3) -@register('image-url', 4) -@register('image-url', 5) -@register('image-url', 6) -def image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None): - """ - Generates a path to an asset found relative to the project's images - directory. - Passing a true value as the second argument will cause the only the path to - be returned instead of a `url()` function - """ - return _image_url(path, only_path, cache_buster, dst_color, src_color, False, None, spacing, collapse_x, collapse_y) - - -@register('image-width', 1) -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 Exception("Images manipulation require PIL") - 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') - - -@register('image-height', 1) -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(os.sep)) - 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') diff --git a/scss/functions/compass/layouts.py b/scss/functions/compass/layouts.py deleted file mode 100644 index ae086ce..0000000 --- a/scss/functions/compass/layouts.py +++ /dev/null @@ -1,347 +0,0 @@ -"""Functions used for generating packed CSS sprite maps. - -These are ported from the Binary Tree Bin Packing Algorithm: -http://codeincomplete.com/posts/2011/5/7/bin_packing/ -""" -from __future__ import absolute_import -from __future__ import unicode_literals - -# Copyright (c) 2011, 2012, 2013 Jake Gordon and contributors -# Copyright (c) 2013 German M. Bravo - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - - -class LayoutNode(object): - def __init__(self, x, y, w, h, down=None, right=None, used=False): - self.x = x - self.y = y - self.w = w - self.h = h - self.down = down - self.right = right - self.used = used - self.width = 0 - self.height = 0 - - @property - def area(self): - return self.width * self.height - - def __repr__(self): - return '<%s (%s, %s) [%sx%s]>' % (self.__class__.__name__, self.x, self.y, self.w, self.h) - - -class SpritesLayout(object): - def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None): - self.num_blocks = len(blocks) - - if margin is None: - margin = [[0] * 4] * self.num_blocks - elif not isinstance(margin, (tuple, list)): - margin = [[margin] * 4] * self.num_blocks - elif not isinstance(margin[0], (tuple, list)): - margin = [margin] * self.num_blocks - - if padding is None: - padding = [[0] * 4] * self.num_blocks - elif not isinstance(padding, (tuple, list)): - padding = [[padding] * 4] * self.num_blocks - elif not isinstance(padding[0], (tuple, list)): - padding = [padding] * self.num_blocks - - if pmargin is None: - pmargin = [[0.0] * 4] * self.num_blocks - elif not isinstance(pmargin, (tuple, list)): - pmargin = [[pmargin] * 4] * self.num_blocks - elif not isinstance(pmargin[0], (tuple, list)): - pmargin = [pmargin] * self.num_blocks - - if ppadding is None: - ppadding = [[0.0] * 4] * self.num_blocks - elif not isinstance(ppadding, (tuple, list)): - ppadding = [[ppadding] * 4] * self.num_blocks - elif not isinstance(ppadding[0], (tuple, list)): - ppadding = [ppadding] * self.num_blocks - - self.blocks = tuple(( - b[0] + padding[i][3] + padding[i][1] + margin[i][3] + margin[i][1] + int(round(b[0] * (ppadding[i][3] + ppadding[i][1] + pmargin[i][3] + pmargin[i][1]))), - b[1] + padding[i][0] + padding[i][2] + margin[i][0] + margin[i][2] + int(round(b[1] * (ppadding[i][0] + ppadding[i][2] + pmargin[i][0] + pmargin[i][2]))), - b[0], - b[1], - i - ) for i, b in enumerate(blocks)) - - self.margin = margin - self.padding = padding - self.pmargin = pmargin - self.ppadding = ppadding - - -class PackedSpritesLayout(SpritesLayout): - @staticmethod - def MAXSIDE(a, b): - """maxside: Sort pack by maximum sides""" - return cmp(max(b[0], b[1]), max(a[0], a[1])) or cmp(min(b[0], b[1]), min(a[0], a[1])) or cmp(b[1], a[1]) or cmp(b[0], a[0]) - - @staticmethod - def WIDTH(a, b): - """width: Sort pack by width""" - return cmp(b[0], a[0]) or cmp(b[1], a[1]) - - @staticmethod - def HEIGHT(a, b): - """height: Sort pack by height""" - return cmp(b[1], a[1]) or cmp(b[0], a[0]) - - @staticmethod - def AREA(a, b): - """area: Sort pack by area""" - return cmp(b[0] * b[1], a[0] * a[1]) or cmp(b[1], a[1]) or cmp(b[0], a[0]) - - def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, methods=None): - super(PackedSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) - - ratio = 0 - - if methods is None: - methods = (self.MAXSIDE, self.WIDTH, self.HEIGHT, self.AREA) - - for method in methods: - sorted_blocks = sorted( - self.blocks, - cmp=method, - ) - root = LayoutNode( - x=0, - y=0, - w=sorted_blocks[0][0] if sorted_blocks else 0, - h=sorted_blocks[0][1] if sorted_blocks else 0 - ) - - area = 0 - nodes = [None] * self.num_blocks - - for block in sorted_blocks: - w, h, width, height, i = block - node = self._findNode(root, w, h) - if node: - node = self._splitNode(node, w, h) - else: - root = self._growNode(root, w, h) - node = self._findNode(root, w, h) - if node: - node = self._splitNode(node, w, h) - else: - node = None - nodes[i] = node - node.width = width - node.height = height - area += node.area - - this_ratio = area / float(root.w * root.h) - # print method.__doc__, "%g%%" % (this_ratio * 100) - if ratio < this_ratio: - self.root = root - self.nodes = nodes - self.method = method - ratio = this_ratio - if ratio > 0.96: - break - # print self.method.__doc__, "%g%%" % (ratio * 100) - - def __iter__(self): - for i, node in enumerate(self.nodes): - margin, padding = self.margin[i], self.padding[i] - pmargin, ppadding = self.pmargin[i], self.ppadding[i] - cssw = node.width + padding[3] + padding[1] + int(round(node.width * (ppadding[3] + ppadding[1]))) # image width plus padding - cssh = node.height + padding[0] + padding[2] + int(round(node.height * (ppadding[0] + ppadding[2]))) # image height plus padding - cssx = node.x + margin[3] + int(round(node.width * pmargin[3])) - cssy = node.y + margin[0] + int(round(node.height * pmargin[0])) - x = cssx + padding[3] + int(round(node.width * ppadding[3])) - y = cssy + padding[0] + int(round(node.height * ppadding[0])) - yield x, y, node.width, node.height, cssx, cssy, cssw, cssh - - @property - def width(self): - return self.root.w - - @property - def height(self): - return self.root.h - - def _findNode(self, root, w, h): - if root.used: - return self._findNode(root.right, w, h) or self._findNode(root.down, w, h) - elif w <= root.w and h <= root.h: - return root - else: - return None - - def _splitNode(self, node, w, h): - node.used = True - node.down = LayoutNode( - x=node.x, - y=node.y + h, - w=node.w, - h=node.h - h - ) - node.right = LayoutNode( - x=node.x + w, - y=node.y, - w=node.w - w, - h=h - ) - return node - - def _growNode(self, root, w, h): - canGrowDown = w <= root.w - canGrowRight = h <= root.h - - shouldGrowRight = canGrowRight and (root.h >= root.w + w) # attempt to keep square-ish by growing right when height is much greater than width - shouldGrowDown = canGrowDown and (root.w >= root.h + h) # attempt to keep square-ish by growing down when width is much greater than height - - if shouldGrowRight: - return self._growRight(root, w, h) - elif shouldGrowDown: - return self._growDown(root, w, h) - elif canGrowRight: - return self._growRight(root, w, h) - elif canGrowDown: - return self._growDown(root, w, h) - else: - # need to ensure sensible root starting size to avoid this happening - assert False, "Blocks must be properly sorted!" - - def _growRight(self, root, w, h): - root = LayoutNode( - used=True, - x=0, - y=0, - w=root.w + w, - h=root.h, - down=root, - right=LayoutNode( - x=root.w, - y=0, - w=w, - h=root.h - ) - ) - return root - - def _growDown(self, root, w, h): - root = LayoutNode( - used=True, - x=0, - y=0, - w=root.w, - h=root.h + h, - down=LayoutNode( - x=0, - y=root.h, - w=root.w, - h=h - ), - right=root - ) - return root - - -class HorizontalSpritesLayout(SpritesLayout): - def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, position=None): - super(HorizontalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) - - self.width = sum(block[0] for block in self.blocks) - self.height = max(block[1] for block in self.blocks) - - if position is None: - position = [0.0] * self.num_blocks - elif not isinstance(position, (tuple, list)): - position = [position] * self.num_blocks - self.position = position - - def __iter__(self): - cx = 0 - for i, block in enumerate(self.blocks): - w, h, width, height, i = block - margin, padding = self.margin[i], self.padding[i] - pmargin, ppadding = self.pmargin[i], self.ppadding[i] - position = self.position[i] - cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding - cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding - cssx = cx + margin[3] + int(round(width * pmargin[3])) # anchored at x - cssy = int(round((self.height - cssh) * position)) # centered vertically - x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding - y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding - yield x, y, width, height, cssx, cssy, cssw, cssh - cx += cssw + margin[3] + margin[1] + int(round(width * (pmargin[3] + pmargin[1]))) - - -class VerticalSpritesLayout(SpritesLayout): - def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, position=None): - super(VerticalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) - - self.width = max(block[0] for block in self.blocks) - self.height = sum(block[1] for block in self.blocks) - - if position is None: - position = [0.0] * self.num_blocks - elif not isinstance(position, (tuple, list)): - position = [position] * self.num_blocks - self.position = position - - def __iter__(self): - cy = 0 - for i, block in enumerate(self.blocks): - w, h, width, height, i = block - margin, padding = self.margin[i], self.padding[i] - pmargin, ppadding = self.pmargin[i], self.ppadding[i] - position = self.position[i] - cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding - cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding - cssx = int(round((self.width - cssw) * position)) # centered horizontally - cssy = cy + margin[0] + int(round(height * pmargin[0])) # anchored at y - x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding - y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding - yield x, y, width, height, cssx, cssy, cssw, cssh - cy += cssh + margin[0] + margin[2] + int(round(height * (pmargin[0] + pmargin[2]))) - - -class DiagonalSpritesLayout(SpritesLayout): - def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None): - super(DiagonalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) - self.width = sum(block[0] for block in self.blocks) - self.height = sum(block[1] for block in self.blocks) - - def __iter__(self): - cx, cy = 0, 0 - for i, block in enumerate(self.blocks): - w, h, width, height, i = block - margin, padding = self.margin[i], self.padding[i] - pmargin, ppadding = self.pmargin[i], self.ppadding[i] - cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding - cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding - cssx = cx + margin[3] + int(round(width * pmargin[3])) # anchored at x - cssy = cy + margin[0] + int(round(height * pmargin[0])) # anchored at y - x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding - y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding - yield x, y, width, height, cssx, cssy, cssw, cssh - cx += cssw + margin[3] + margin[1] + int(round(width * (pmargin[3] + pmargin[1]))) - cy += cssh + margin[0] + margin[2] + int(round(height * (pmargin[0] + pmargin[2]))) diff --git a/scss/functions/compass/sprites.py b/scss/functions/compass/sprites.py deleted file mode 100644 index 908adcd..0000000 --- a/scss/functions/compass/sprites.py +++ /dev/null @@ -1,566 +0,0 @@ -"""Functions used for generating CSS sprites. - -These are ported from the Compass sprite library: -http://compass-style.org/reference/compass/utilities/sprites/ -""" -from __future__ import absolute_import -from __future__ import unicode_literals - -import six - -import glob -import logging -import os.path -import tempfile -import time -import sys - -try: - import cPickle as pickle -except ImportError: - import pickle - -try: - from PIL import Image -except ImportError: - try: - import Image - except: - Image = None - -from six.moves import xrange - -from scss import config -from scss.functions.compass import _image_size_cache -from scss.functions.compass.layouts import PackedSpritesLayout, HorizontalSpritesLayout, VerticalSpritesLayout, DiagonalSpritesLayout -from scss.functions.library import FunctionLibrary -from scss.types import Color, List, Number, String, Boolean -from scss.util import escape, getmtime, make_data_url, make_filename_hash - -log = logging.getLogger(__name__) - -MAX_SPRITE_MAPS = 4096 -KEEP_SPRITE_MAPS = int(MAX_SPRITE_MAPS * 0.8) - -COMPASS_SPRITES_LIBRARY = FunctionLibrary() -register = COMPASS_SPRITES_LIBRARY.register - - -# ------------------------------------------------------------------------------ -# Compass-like functionality for sprites and images - -sprite_maps = {} - - -def alpha_composite(im1, im2, offset=None, box=None, opacity=1): - im1size = im1.size - im2size = im2.size - if offset is None: - offset = (0, 0) - if box is None: - box = (0, 0) + im2size - o1x, o1y = offset - o2x, o2y, o2w, o2h = box - width = o2w - o2x - height = o2h - o2y - im1_data = im1.load() - im2_data = im2.load() - for y in xrange(height): - for x in xrange(width): - pos1 = o1x + x, o1y + y - if pos1[0] >= im1size[0] or pos1[1] >= im1size[1]: - continue - pos2 = o2x + x, o2y + y - if pos2[0] >= im2size[0] or pos2[1] >= im2size[1]: - continue - dr, dg, db, da = im1_data[pos1] - sr, sg, sb, sa = im2_data[pos2] - da /= 255.0 - sa /= 255.0 - sa *= opacity - ida = da * (1 - sa) - oa = (sa + ida) - if oa: - pixel = ( - int(round((sr * sa + dr * ida)) / oa), - int(round((sg * sa + dg * ida)) / oa), - int(round((sb * sa + db * ida)) / oa), - int(round(255 * oa)) - ) - else: - pixel = (0, 0, 0, 0) - im1_data[pos1] = pixel - return im1 - - -@register('sprite-map') -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`) - $-position - Position of a given sprite. - - $padding, $spacing - Adds paddings to sprites (top, right, bottom, left). (defaults to `0, 0, 0, 0`) - $-padding, $-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`). - $-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`) - $-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() - - globs = String(g, quotes=None).value - globs = sorted(g.strip() for g in globs.split(',')) - - _k_ = ','.join(globs) - - files = None - rfiles = None - tfiles = None - map_name = None - - if _k_ in sprite_maps: - sprite_maps[_k_]['*'] = now_time - else: - files = [] - rfiles = [] - tfiles = [] - for _glob in globs: - if '..' not in _glob: # Protect against going to prohibited places... - if callable(config.STATIC_ROOT): - _glob_path = _glob - _rfiles = _files = sorted(config.STATIC_ROOT(_glob)) - else: - _glob_path = os.path.join(config.STATIC_ROOT, _glob) - _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 _files: - files.extend(_files) - rfiles.extend(_rfiles) - base_name = os.path.normpath(os.path.dirname(_glob)).replace('\\', '_').replace('/', '_') - _map_name, _, _map_type = base_name.partition('.') - if _map_type: - _map_type += '-' - if not map_name: - map_name = _map_name - tfiles.extend([_map_type] * len(_files)) - else: - glob_path = _glob_path - - if files is not None: - if not files: - log.error("Nothing found at '%s'", glob_path) - return String.unquoted('') - - 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) - tnames = tuple(tfiles[i] + n for i, n in enumerate(names)) - - 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(tnames, 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() - if sys.platform == 'win32' and os.path.isfile(cache_path): - # on windows, cannot rename a file to a path that matches - # an existing file, we have to remove it first - os.remove(cache_path) - 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 - - -@register('sprite-map-name', 1) -def sprite_map_name(map): - """ - Returns the name of a sprite map The name is derived from the folder than - contains the sprites. - """ - map = map.render() - sprite_map = sprite_maps.get(map) - if not sprite_map: - log.error("No sprite map found: %s", map, extra={'stack': True}) - if sprite_map: - return String.unquoted(sprite_map['*n*']) - return String.unquoted('') - - -@register('sprite-file', 2) -def sprite_file(map, sprite): - """ - Returns the relative path (from the images directory) to the original file - used when construction the sprite. This is suitable for passing to the - image_width and image_height helpers. - """ - 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: - return String(sprite[1][0]) - return String.unquoted('') - - -@register('sprites', 1) -@register('sprite-names', 1) -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('*')))]) - - -@register('sprite-classes', 1) -def sprite_classes(map): - return sprites(map, True) - - -@register('sprite', 2) -@register('sprite', 3) -@register('sprite', 4) -@register('sprite', 5) -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)]) - - -@register('sprite-url', 1) -@register('sprite-url', 2) -def sprite_url(map, cache_buster=True): - """ - Returns a url to the sprite image. - """ - map = map.render() - sprite_map = sprite_maps.get(map) - if not sprite_map: - log.error("No sprite map found: %s", map, extra={'stack': True}) - if sprite_map: - url = '%s%s' % (config.ASSETS_URL, sprite_map['*f*']) - if cache_buster: - url += '?_=%s' % sprite_map['*t*'] - url = "url(%s)" % escape(url) - return String.unquoted(url) - return String.unquoted('') - - -@register('has-sprite', 2) -def has_sprite(map, sprite): - 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}) - return Boolean(bool(sprite)) - - -@register('sprite-position', 2) -@register('sprite-position', 3) -@register('sprite-position', 4) -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)]) diff --git a/scss/legacy.py b/scss/legacy.py index 90016a2..c4fb984 100644 --- a/scss/legacy.py +++ b/scss/legacy.py @@ -10,9 +10,9 @@ import scss.config as config from scss.expression import Calculator from scss.extension.bootstrap import BootstrapExtension from scss.extension.core import CoreExtension +from scss.extension.compass import CompassExtension from scss.extension.extra import ExtraExtension from scss.extension.fonts import FontsExtension -from scss.functions import COMPASS_LIBRARY from scss.namespace import Namespace from scss.scss_meta import ( BUILD_INFO, PROJECT, VERSION, REVISION, URL, AUTHOR, AUTHOR_EMAIL, LICENSE, @@ -128,7 +128,7 @@ class Scss(object): CoreExtension, ExtraExtension, FontsExtension, - Namespace(functions=COMPASS_LIBRARY), + CompassExtension, BootstrapExtension, ], search_path=search_paths, diff --git a/scss/tests/functions/compass/test_gradients.py b/scss/tests/functions/compass/test_gradients.py index a141740..f93e335 100644 --- a/scss/tests/functions/compass/test_gradients.py +++ b/scss/tests/functions/compass/test_gradients.py @@ -3,7 +3,8 @@ from __future__ import absolute_import from __future__ import unicode_literals from scss.expression import Calculator -from scss.functions.compass.gradients import COMPASS_GRADIENTS_LIBRARY, linear_gradient +from scss.extension.compass.gradients import gradients_namespace +from scss.extension.compass.gradients import linear_gradient from scss.rule import Namespace from scss.types import String, List, Number, Color @@ -12,8 +13,7 @@ import pytest @pytest.fixture def calc(): - ns = Namespace(functions=COMPASS_GRADIENTS_LIBRARY) - return Calculator(ns).evaluate_expression + return Calculator(gradients_namespace).evaluate_expression def test_linear_gradient(): diff --git a/scss/tests/functions/compass/test_helpers.py b/scss/tests/functions/compass/test_helpers.py index a0615aa..bbcaaf0 100644 --- a/scss/tests/functions/compass/test_helpers.py +++ b/scss/tests/functions/compass/test_helpers.py @@ -12,24 +12,21 @@ Ruby code. from __future__ import absolute_import from __future__ import unicode_literals +import os + +from scss import config from scss.expression import Calculator -from scss.functions.compass.helpers import COMPASS_HELPERS_LIBRARY +from scss.extension.compass.helpers import helpers_namespace from scss.rule import Namespace - import pytest -from scss import config -import os -from _pytest.monkeypatch import monkeypatch -xfail = pytest.mark.xfail # TODO many of these tests could also stand to test for failure cases @pytest.fixture def calc(): - ns = Namespace(functions=COMPASS_HELPERS_LIBRARY) - return Calculator(ns).evaluate_expression + return Calculator(helpers_namespace).evaluate_expression # ------------------------------------------------------------------------------ @@ -179,12 +176,12 @@ def test_font_files(calc): # inline-font-files -def test_inline_font_files(calc): +def test_inline_font_files(calc, monkeypatch): """ @author: funvit @note: adapted from compass / test / units / sass_extensions_test.rb """ - monkeypatch().setattr(config, 'FONTS_ROOT', os.path.join(config.PROJECT_ROOT, 'tests/files/fonts')) + monkeypatch.setattr(config, 'FONTS_ROOT', os.path.join(config.PROJECT_ROOT, 'tests/files/fonts')) with open(os.path.join(config.PROJECT_ROOT, 'tests/files/fonts/bgrove.base64.txt'), 'r') as f: font_base64 = ''.join((f.readlines())) diff --git a/scss/tests/functions/compass/test_images.py b/scss/tests/functions/compass/test_images.py index 1478fa7..dc1d6e2 100644 --- a/scss/tests/functions/compass/test_images.py +++ b/scss/tests/functions/compass/test_images.py @@ -12,25 +12,21 @@ Ruby code. from __future__ import absolute_import from __future__ import unicode_literals -from scss.expression import Calculator -from scss.functions.compass.images import COMPASS_IMAGES_LIBRARY -from scss.rule import Namespace - +import os +import sys import pytest + from scss import config -import os -import sys -from _pytest.monkeypatch import monkeypatch -xfail = pytest.mark.xfail +from scss.expression import Calculator +from scss.extension.compass.images import images_namespace -# TODO many of these tests could also stand to test for failure cases +# TODO many of these tests could also stand to test for failure cases @pytest.fixture def calc(): - ns = Namespace(functions=COMPASS_IMAGES_LIBRARY) - return Calculator(ns).evaluate_expression + return Calculator(images_namespace).evaluate_expression def test_image_url(calc): @@ -40,8 +36,8 @@ def test_image_url(calc): # inline-image -def test_inline_image(calc): - monkeypatch().setattr(config, 'IMAGES_ROOT', os.path.join(config.PROJECT_ROOT, 'tests/files/images')) +def test_inline_image(calc, monkeypatch): + monkeypatch.setattr(config, 'IMAGES_ROOT', os.path.join(config.PROJECT_ROOT, 'tests/files/images')) with open(os.path.join(config.PROJECT_ROOT, 'tests/files/images/test-qr.base64.txt'), 'r') as f: font_base64 = f.read() @@ -49,8 +45,8 @@ def test_inline_image(calc): @pytest.mark.skipif(sys.platform == 'win32', reason='cur mimetype is defined on windows') -def test_inline_cursor(calc): - monkeypatch().setattr(config, 'IMAGES_ROOT', os.path.join(config.PROJECT_ROOT, 'tests/files/cursors')) +def test_inline_cursor(calc, monkeypatch): + monkeypatch.setattr(config, 'IMAGES_ROOT', os.path.join(config.PROJECT_ROOT, 'tests/files/cursors')) with open(os.path.join(config.PROJECT_ROOT, 'tests/files/cursors/fake.base64.txt'), 'r') as f: font_base64 = f.read() -- cgit v1.2.1