diff --git a/scss/compiler.py b/scss/compiler.py index 2394258a..9e70a247 100644 --- a/scss/compiler.py +++ b/scss/compiler.py @@ -844,32 +844,32 @@ def _at_magic_import(self, calculator, rule, scope, block): Implements @import for sprite-maps Imports magic sprite map directories """ - # TODO check that the found file is actually under the root + + to_import = block.argument.strip('"') if callable(config.STATIC_ROOT): - files = sorted(config.STATIC_ROOT(block.argument)) + files = sorted(config.STATIC_ROOT(to_import)) else: - glob_path = os.path.join(config.STATIC_ROOT, block.argument) + glob_path = os.path.join(config.STATIC_ROOT, to_import) files = glob.glob(glob_path) files = sorted((file[len(config.STATIC_ROOT):], None) for file in files) if not files: return - # Build magic context - map_name = os.path.normpath(os.path.dirname(block.argument)).replace('\\', '_').replace('/', '_') + # Populate namespace with sprite variables + map_name = os.path.normpath(os.path.dirname(to_import)).replace(os.sep, '_') kwargs = {} def setdefault(var, val): _var = '$' + map_name + '-' + var - if _var in rule.context: - kwargs[var] = calculator.interpolate(rule.context[_var], rule, self._library) - else: - rule.context[_var] = val - kwargs[var] = calculator.interpolate(val, rule, self._library) - return rule.context[_var] + if _var not in rule.namespace.variables: + rule.namespace.set_variable(_var, val) + kwargs[var] = calculator.interpolate(_var) + return rule.namespace.variable(_var) setdefault('sprite-base-class', String('.' + map_name + '-sprite', quotes=None)) setdefault('sprite-dimensions', Boolean(False)) + setdefault('layout', String('vertical')) position = setdefault('position', Number(0, '%')) spacing = setdefault('spacing', Number(0)) repeat = setdefault('repeat', String('no-repeat', quotes=None)) @@ -878,7 +878,7 @@ def setdefault(var, val): setdefault(n + '-position', position) setdefault(n + '-spacing', spacing) setdefault(n + '-repeat', repeat) - rule.context['$' + map_name + '-' + 'sprites'] = sprite_map(block.argument, **kwargs) + rule.namespace.set_variable('$' + map_name + '-' + 'sprites', sprite_map(block.argument, **kwargs)) ret = ''' @import "compass/utilities/sprites/base"; diff --git a/scss/functions/compass/sprites.py b/scss/functions/compass/sprites.py index 908adcd0..b62b7a44 100644 --- a/scss/functions/compass/sprites.py +++ b/scss/functions/compass/sprites.py @@ -99,7 +99,7 @@ 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`. + $direction, $layout - 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. @@ -123,7 +123,7 @@ def sprite_map(g, **kwargs): now_time = time.time() globs = String(g, quotes=None).value - globs = sorted(g.strip() for g in globs.split(',')) + globs = sorted(g.strip(' "') for g in globs.split(',')) _k_ = ','.join(globs) @@ -151,7 +151,7 @@ def sprite_map(g, **kwargs): if _files: files.extend(_files) rfiles.extend(_rfiles) - base_name = os.path.normpath(os.path.dirname(_glob)).replace('\\', '_').replace('/', '_') + base_name = os.path.normpath(os.path.dirname(_glob)).replace(os.sep, '_') _map_name, _, _map_type = base_name.partition('.') if _map_type: _map_type += '-' @@ -166,7 +166,13 @@ def sprite_map(g, **kwargs): log.error("Nothing found at '%s'", glob_path) return String.unquoted('') - key = [f for (f, s) in files] + [repr(kwargs), config.ASSETS_URL] + # sorting kwargs representation to ensure hash repeatability + kwargs_repr = "" + for k in sorted(kwargs.keys()): + kwargs_repr += ("'%s': %s, " % (k, repr(kwargs[k]))) + kwargs_repr = str("{%s}" % kwargs_repr.strip(', ')) + + key = [f for (f, s) in files] + [kwargs_repr, 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') @@ -204,7 +210,9 @@ def sprite_map(g, **kwargs): 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 + direction = String.unquoted(kwargs.get('direction', + kwargs.get('layout', + config.SPRTE_MAP_DIRECTION))).value repeat = String.unquoted(kwargs.get('repeat', 'no-repeat')).value collapse = kwargs.get('collapse', Number(0)) if isinstance(collapse, List): @@ -326,6 +334,7 @@ def images(f=lambda x: x): offsets_x = [] offsets_y = [] + selectors = [] for i, image in enumerate(images()): x, y, width, height, cssx, cssy, cssw, cssh = layout_positions[i] iwidth, iheight = image.size @@ -359,6 +368,13 @@ def images(f=lambda x: x): offsets_x.append(cssx) offsets_y.append(cssy) + # extracting selector for compass spriting's magic selectors + # http://compass-style.org/help/tutorials/spriting/magic-selectors/ + name = os.path.splitext(os.path.basename(image.filename))[0] + spl = name.split('_') + selector = spl[-1] if len(spl) > 1 else None + selectors.append(selector) + if useless_dst_color: log.warning("Useless use of $dst-color in sprite map for files at '%s' (never used for)" % glob_path) @@ -388,12 +404,13 @@ def images(f=lambda x: x): 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 = dict(zip(tnames, zip(sizes, rfiles, offsets_x, offsets_y, selectors))) sprite_map['*'] = now_time sprite_map['*f*'] = asset_file sprite_map['*k*'] = key sprite_map['*n*'] = map_name sprite_map['*t*'] = filetime + sprite_map['*s*'] = new_image.size sizes = zip(files, sizes) cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT) @@ -469,7 +486,9 @@ def sprite_classes(map): @register('sprite', 3) @register('sprite', 4) @register('sprite', 5) -def sprite(map, sprite, offset_x=None, offset_y=None, cache_buster=True): +@register('sprite', 6) +def sprite(map, sprite, offset_x=None, offset_y=None, use_percentages=False, + cache_buster=True): """ Returns the image and background position for use in a single shorthand property @@ -486,14 +505,20 @@ def sprite(map, sprite, offset_x=None, offset_y=None, cache_buster=True): 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') + unit = '%' if use_percentages else 'px' + coors = [Number(offset_x or 0, unit), Number(offset_y or 0, unit)] + map_size = sprite_map['*s*'] + for i, coor in enumerate(coors): + if not coor.value or (coor.value <= -1 or coor.value >= 1) \ + and (coor.is_simple_unit('%') and unit == '%' or + coor.is_simple_unit('px') and unit == 'px'): + value = sprite[i + 2] + if use_percentages and value: + value = -100.0 * value / (map_size[i] - sprite[0][i]) + coors[i] -= Number(value, unit) + url = "url(%s)" % escape(url) - return List([String.unquoted(url), x, y]) + return List([String.unquoted(url)] + coors) return List([Number(0), Number(0)]) @@ -530,7 +555,9 @@ def has_sprite(map, sprite): @register('sprite-position', 2) @register('sprite-position', 3) @register('sprite-position', 4) -def sprite_position(map, sprite, offset_x=None, offset_y=None): +@register('sprite-position', 5) +def sprite_position(map, sprite, offset_x=None, offset_y=None, + use_percentages=False): """ Returns the position for the original image in the sprite. This is suitable for use as a value to background-position. @@ -544,23 +571,50 @@ def sprite_position(map, sprite, offset_x=None, offset_y=None): 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]) + unit = '%' if use_percentages else 'px' + map_size = sprite_map['*s*'] + + coors = [offset_x, offset_y] + positions = (('left', 'right', 'center'), ('top', 'bottom', 'center')) + + for i, coor in enumerate(coors): + c = None + if coor is not None and not isinstance(coor, Number): + c = coor + if not c or c.value not in positions[i]: + if c: + coor = None + c = Number(offset_x or 0, unit) + if not c.value or (c.value <= -1 or c.value >= 1) \ + and (c.is_simple_unit('%') and unit == '%' or + c.is_simple_unit('px') and unit == 'px'): + value = sprite[i + 2] + if use_percentages and value: + value = -100.0 * value / (map_size[i] - sprite[0][i]) + coors[i] = c - Number(value, unit) + else: + coors[i] = c + else: + coors[i] = None + + return List(coors) return List([Number(0), Number(0)]) + + +@register('sprite-does-not-have-parent', 2) +def sprite_does_not_have_parent(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 there is no selector, the sprite does not have any parents + return Boolean(not sprite[4]) + + +@register('sprite-has-selector', 3) +def sprite_has_selector(map, sprite, selector): + map = map.render() + sprite_map = sprite_maps.get(map) + sprite_name = String.unquoted(sprite).value + sprite = sprite_map and sprite_map.get(sprite_name + '_' + selector.value) + return Boolean(sprite) diff --git a/scss/tests/files/kronuz/include/compass/utilities/sprites/_base.scss b/scss/tests/files/kronuz/include/compass/utilities/sprites/_base.scss new file mode 100644 index 00000000..3a94e686 --- /dev/null +++ b/scss/tests/files/kronuz/include/compass/utilities/sprites/_base.scss @@ -0,0 +1,68 @@ +// This file comes from compass (compass-style.org) and is only used in sprite-import test + +// Determines those states for which you want to enable magic sprite selectors +$sprite-selectors: hover, target, active !default; + +// Set the width and height of an element to the original +// dimensions of an image before it was included in the sprite. +@mixin sprite-dimensions($map, $sprite) { + height: image-height(sprite-file($map, $sprite)); + width: image-width(sprite-file($map, $sprite)); +} + +// Set the background position of the given sprite `$map` to display the +// sprite of the given `$sprite` name. You can move the image relative to its +// natural position by passing `$offset-x` and `$offset-y`. +@mixin sprite-background-position($map, $sprite, $offset-x: 0, $offset-y: 0) { + background-position: sprite-position($map, $sprite, $offset-x, $offset-y); +} + + +// Determines if you want to include magic selectors in your sprites +$disable-magic-sprite-selectors:false !default; + +// Include the position and (optionally) dimensions of this `$sprite` +// in the given sprite `$map`. The sprite url should come from either a base +// class or you can specify the `sprite-url` explicitly like this: +// +// background: $map no-repeat; +@mixin sprite($map, $sprite, $dimensions: false, $offset-x: 0, $offset-y: 0) { + @include sprite-background-position($map, $sprite, $offset-x, $offset-y); + @if $dimensions { + @include sprite-dimensions($map, $sprite); + } + @if not $disable-magic-sprite-selectors { + @include sprite-selectors($map, $sprite, $sprite, $offset-x, $offset-y); + } +} + +// Include the selectors for the `$sprite` given the `$map` and the +// `$full-sprite-name` +// @private +@mixin sprite-selectors($map, $sprite-name, $full-sprite-name, $offset-x: 0, $offset-y: 0) { + @each $selector in $sprite-selectors { + @if sprite_has_selector($map, $sprite-name, $selector) { + &:#{$selector}, &.#{$full-sprite-name}_#{$selector}, &.#{$full-sprite-name}-#{$selector} { + @include sprite-background-position($map, "#{$sprite-name}_#{$selector}", $offset-x, $offset-y); + } + } + } +} + +// Generates a class for each space separated name in `$sprite-names`. +// The class will be of the form .-. +// +// If a base class is provided, then each class will extend it. +// +// If `$dimensions` is `true`, the sprite dimensions will specified. +@mixin sprites($map, $sprite-names, $base-class: false, $dimensions: false, $prefix: sprite-map-name($map), $offset-x: 0, $offset-y: 0) { + @each $sprite-name in $sprite-names { + @if sprite_does_not_have_parent($map, $sprite-name) { + $full-sprite-name: "#{$prefix}-#{$sprite-name}"; + .#{$full-sprite-name} { + @if $base-class { @extend #{$base-class}; } + @include sprite($map, $sprite-name, $dimensions, $offset-x, $offset-y); + } + } + } +} \ No newline at end of file diff --git a/scss/tests/files/kronuz/sprite-import.css b/scss/tests/files/kronuz/sprite-import.css new file mode 100644 index 00000000..dbb99c0d --- /dev/null +++ b/scss/tests/files/kronuz/sprite-import.css @@ -0,0 +1,10 @@ +.squares-sprite, .squares-ten-by-ten, .squares-twenty-by-twenty { + background: url(static/assets/squares-frdGCNpwVAfawnKv35k2NA.png) no-repeat; +} +.squares-ten-by-ten { + background-position: 0px 0px; +} + +.squares-twenty-by-twenty { + background-position: -10px 0px; +} diff --git a/scss/tests/files/kronuz/sprite-import.py b/scss/tests/files/kronuz/sprite-import.py new file mode 100644 index 00000000..9a7bc831 --- /dev/null +++ b/scss/tests/files/kronuz/sprite-import.py @@ -0,0 +1,20 @@ +""" +Disables cache_buster on sprite_map for sprite_import test +""" + +from scss import compiler, types + +sprite_map_0 = compiler.sprite_map + +def sprite_map_patch(g, **kwargs): + global sprite_map_0 + kwargs.setdefault('cache_buster', types.Boolean(False)) + return sprite_map_0(g, **kwargs) + + +def setUp(): + compiler.sprite_map = sprite_map_patch + + +def tearDown(): + compiler.sprite_map = sprite_map_0 diff --git a/scss/tests/files/kronuz/sprite-import.scss b/scss/tests/files/kronuz/sprite-import.scss new file mode 100644 index 00000000..52717d7b --- /dev/null +++ b/scss/tests/files/kronuz/sprite-import.scss @@ -0,0 +1,4 @@ +$squares-layout:horizontal; +@import "squares/*.png"; + +@include all-squares-sprites; diff --git a/scss/tests/files/kronuz/sprite-pos-percentage.css b/scss/tests/files/kronuz/sprite-pos-percentage.css new file mode 100644 index 00000000..02fad7d6 --- /dev/null +++ b/scss/tests/files/kronuz/sprite-pos-percentage.css @@ -0,0 +1,11 @@ +.mod { + background: url(static/assets/squares-nafpKaM4RlNNZcIbS0wv_g.png) no-repeat; +} +.mod.ten-by-ten { + background: url(static/assets/squares-nafpKaM4RlNNZcIbS0wv_g.png) 0% 0%; +} +.mod.twenty-by-twenty { + width: 20px; + height: 20px; + background-position: 0% 100%; +} diff --git a/scss/tests/files/kronuz/sprite-pos-percentage.scss b/scss/tests/files/kronuz/sprite-pos-percentage.scss new file mode 100644 index 00000000..b9968aa8 --- /dev/null +++ b/scss/tests/files/kronuz/sprite-pos-percentage.scss @@ -0,0 +1,16 @@ +@option style:legacy; + +$images: sprite-map("squares/*.png", $cache-buster: false); + +.mod { + background: $images; + &.ten-by-ten { + background: sprite($images, "ten-by-ten", $use-percentages: true, $cache-buster: false); + } + &.twenty-by-twenty { + $file: sprite-file($images, "twenty-by-twenty"); + width: image-width($file); + height: image-height($file); + background-position: sprite-position($images, "twenty-by-twenty", $use-percentages: true); + } +} diff --git a/scss/tests/test_files.py b/scss/tests/test_files.py index eaaba4c3..9ff0421f 100644 --- a/scss/tests/test_files.py +++ b/scss/tests/test_files.py @@ -10,8 +10,10 @@ from __future__ import absolute_import, unicode_literals -import os.path +import os import logging +import sys +from importlib import import_module import six @@ -31,7 +33,16 @@ def test_pair_programmatic(scss_file_pair): scss_fn, css_fn = scss_file_pair - with open(scss_fn, 'rb') as fh: + # look for a python module related to the pair and execute it if found + mod = None + cfg_script = scss_fn.replace('.scss', '.py') + if os.path.exists(cfg_script): + sys.path[0:0] = [os.path.dirname(scss_fn)] + mod = import_module(os.path.splitext(os.path.split(scss_fn)[1])[0]) + getattr(mod, 'setUp', lambda: None)() + sys.path = sys.path[1:] + + with open(scss_fn) as fh: source = fh.read() with open(css_fn, 'r', encoding='utf8') as fh: expected = fh.read() @@ -40,11 +51,22 @@ def test_pair_programmatic(scss_file_pair): include_dir = os.path.join(directory, 'include') scss.config.STATIC_ROOT = os.path.join(directory, 'static') - compiler = scss.Scss(scss_opts=dict(style='expanded'), search_paths=[include_dir, directory]) - actual = compiler.compile(source) + try: + compiler = scss.Scss(scss_opts=dict(style='expanded'), search_paths=[include_dir, directory]) + actual = compiler.compile(source) + + getattr(mod, 'tearDown', lambda:None)() + + # Normalize leading and trailing newlines + actual = actual.strip('\n') + expected = expected.strip('\n') - # Normalize leading and trailing newlines - actual = actual.strip('\n') - expected = expected.strip('\n') + assert expected == actual - assert expected == actual + finally: + # cleanup generated assets if any + assets_dir = os.path.join(directory, 'static', 'assets') + if os.path.isdir(assets_dir): + for x in os.listdir(assets_dir): + if x != '.placeholder': + os.remove(os.path.join(assets_dir, x)) diff --git a/scss/types.py b/scss/types.py index f310a1cd..57c42990 100644 --- a/scss/types.py +++ b/scss/types.py @@ -275,6 +275,13 @@ def __repr__(self): def __hash__(self): return hash((self.value, self.unit_numer, self.unit_denom)) + def __bool__(self): + return bool(self.value) + + def __nonzero__(self): + # Py 2's name for __bool__ + return self.__bool__() + def __int__(self): return int(self.value) diff --git a/scss/util.py b/scss/util.py index 481a0c0c..063239bd 100644 --- a/scss/util.py +++ b/scss/util.py @@ -160,17 +160,17 @@ def wrapper(*args, **kwargs): profiler.disable() stats = pstats.Stats(profiler, stream=stream) stats.sort_stats('time') - print >>stream, "" - print >>stream, "=" * 100 - print >>stream, "Stats:" + print >> stream, "" + print >> stream, "=" * 100 + print >> stream, "Stats:" stats.print_stats() - print >>stream, "=" * 100 - print >>stream, "Callers:" + print >> stream, "=" * 100 + print >> stream, "Callers:" stats.print_callers() - print >>stream, "=" * 100 - print >>stream, "Callees:" + print >> stream, "=" * 100 + print >> stream, "Callees:" stats.print_callees() - print >>sys.stderr, stream.getvalue() + print >> sys.stderr, stream.getvalue() stream.close() return res return wrapper diff --git a/tox.ini b/tox.ini index ca9ec35c..2331ac5f 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ commands = {toxworkdir}/{envname}/Scripts/py.test [] deps = {[testenv]deps} enum34 + importlib [testenv:py27] deps =