summaryrefslogtreecommitdiff
path: root/scss/extension/compass/helpers.py
diff options
context:
space:
mode:
Diffstat (limited to 'scss/extension/compass/helpers.py')
-rw-r--r--scss/extension/compass/helpers.py645
1 files changed, 645 insertions, 0 deletions
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)