1
"""Miscellaneous helper functions ported from Compass.
3
See: http://compass-style.org/reference/compass/helpers/
5
This collection is not necessarily complete or up-to-date.
8
from __future__ import absolute_import
18
from scss import config
19
from scss.functions.library import FunctionLibrary
20
from scss.types import Boolean, List, Null, Number, String
21
from scss.util import escape, to_str
24
log = logging.getLogger(__name__)
27
COMPASS_HELPERS_LIBRARY = FunctionLibrary()
28
register = COMPASS_HELPERS_LIBRARY.register
33
'opentype': 'opentype',
35
'truetype': 'truetype',
37
'eot': 'embedded-opentype'
41
def add_cache_buster(url, mtime):
42
fragment = url.split('#')
43
query = fragment[0].split('?')
44
if len(query) > 1 and query[1] != '':
45
cb = '&_=%s' % (mtime)
46
url = '?'.join(query) + cb
48
cb = '?_=%s' % (mtime)
51
url += '#' + fragment[1]
55
# ------------------------------------------------------------------------------
60
"""Returns true when the object is false, an empty string, or an empty list"""
62
if isinstance(o, Boolean):
64
elif isinstance(o, String):
65
is_blank = not len(o.value.strip())
66
elif isinstance(o, List):
67
is_blank = all(blank(el) for el in o)
79
"""Returns a new list after removing any non-true values"""
81
if len(args) == 1 and isinstance(args[0], List):
82
use_comma = args[0].use_comma
86
[arg for arg in args if arg],
92
def reject(lst, *values):
93
"""Removes the given values from the list"""
94
lst = List.from_maybe(lst)
95
values = frozenset(List.from_maybe_starargs(values))
99
if item not in values:
101
return List(ret, use_comma=lst.use_comma)
104
@register('first-value-of')
105
def first_value_of(*args):
106
if len(args) == 1 and isinstance(args[0], String):
107
first = args[0].value.split()[0]
108
return type(args[0])(first)
110
args = List.from_maybe_starargs(args)
117
@register('-compass-list')
118
def dash_compass_list(*args):
119
return List.from_maybe_starargs(args)
122
@register('-compass-space-list')
123
def dash_compass_space_list(*lst):
125
If the argument is a list, it will return a new list that is space delimited
126
Otherwise it returns a new, single element, space-delimited list.
128
ret = dash_compass_list(*lst)
129
ret.value.pop('_', None)
133
@register('-compass-slice', 3)
134
def dash_compass_slice(lst, start_index, end_index=None):
135
start_index = Number(start_index).value
136
end_index = Number(end_index).value if end_index is not None else None
140
# This function has an inclusive end, but Python slicing is exclusive
142
ret = lst.value[start_index:end_index]
143
return List(ret, use_comma=lst.use_comma)
146
# ------------------------------------------------------------------------------
149
@register('prefixed')
150
def prefixed(prefix, *args):
151
to_fnct_str = 'to_' + to_str(prefix).replace('-', '_')
152
for arg in List.from_maybe_starargs(args):
153
if hasattr(arg, to_fnct_str):
155
return Boolean(False)
159
def prefix(prefix, *args):
160
to_fnct_str = 'to_' + to_str(prefix).replace('-', '_')
162
for i, arg in enumerate(args):
163
if isinstance(arg, List):
166
to_fnct = getattr(iarg, to_fnct_str, None)
168
_value.append(to_fnct())
171
args[i] = List(_value)
173
to_fnct = getattr(arg, to_fnct_str, None)
177
return List.maybe_new(args, use_comma=True)
182
return prefix('_moz', *args)
187
return prefix('_svg', *args)
191
def dash_css2(*args):
192
return prefix('_css2', *args)
197
return prefix('_pie', *args)
201
def dash_webkit(*args):
202
return prefix('_webkit', *args)
207
return prefix('_owg', *args)
211
def dash_khtml(*args):
212
return prefix('_khtml', *args)
217
return prefix('_ms', *args)
222
return prefix('_o', *args)
225
# ------------------------------------------------------------------------------
226
# Selector generation
228
@register('append-selector', 2)
229
def append_selector(selector, to_append):
230
if isinstance(selector, List):
233
lst = String.unquoted(selector).value.split(',')
234
to_append = String.unquoted(to_append).value.strip()
235
ret = sorted(set(s.strip() + to_append for s in lst if s.strip()))
236
ret = dict(enumerate(ret))
241
_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'
242
_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'
243
_elements_of_type_table = 'table'
244
_elements_of_type_list_item = 'li'
245
_elements_of_type_table_row_group = 'tbody'
246
_elements_of_type_table_header_group = 'thead'
247
_elements_of_type_table_footer_group = 'tfoot'
248
_elements_of_type_table_row = 'tr'
249
_elements_of_type_table_cel = 'td, th'
250
_elements_of_type_html5_block = 'article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary'
251
_elements_of_type_html5_inline = 'audio, canvas, command, datalist, embed, keygen, mark, meter, output, progress, rp, rt, ruby, time, video, wbr'
252
_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'
253
_elements_of_type = {
254
'block': sorted(_elements_of_type_block.replace(' ', '').split(',')),
255
'inline': sorted(_elements_of_type_inline.replace(' ', '').split(',')),
256
'table': sorted(_elements_of_type_table.replace(' ', '').split(',')),
257
'list-item': sorted(_elements_of_type_list_item.replace(' ', '').split(',')),
258
'table-row-group': sorted(_elements_of_type_table_row_group.replace(' ', '').split(',')),
259
'table-header-group': sorted(_elements_of_type_table_header_group.replace(' ', '').split(',')),
260
'table-footer-group': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')),
261
'table-row': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')),
262
'table-cell': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')),
263
'html5-block': sorted(_elements_of_type_html5_block.replace(' ', '').split(',')),
264
'html5-inline': sorted(_elements_of_type_html5_inline.replace(' ', '').split(',')),
265
'html5': sorted(_elements_of_type_html5.replace(' ', '').split(',')),
269
@register('elements-of-type', 1)
270
def elements_of_type(display):
271
d = String.unquoted(display)
272
ret = _elements_of_type.get(d.value, None)
274
raise Exception("Elements of type '%s' not found!" % d.value)
275
return List(ret, use_comma=True)
278
@register('enumerate', 3)
279
@register('enumerate', 4)
280
def enumerate_(prefix, frm, through, separator='-'):
281
separator = String.unquoted(separator).value
283
frm = int(getattr(frm, 'value', frm))
287
through = int(getattr(through, 'value', through))
291
# DEVIATION: allow reversed enumerations (and ranges as range() uses enumerate, like '@for .. from .. through')
292
frm, through = through, frm
298
for i in rev(range(frm, through + 1)):
299
if prefix and prefix.value:
300
ret.append(String.unquoted(prefix.value + separator + str(i)))
302
ret.append(Number(i))
304
return List(ret, use_comma=True)
307
@register('headers', 0)
308
@register('headers', 1)
309
@register('headers', 2)
310
@register('headings', 0)
311
@register('headings', 1)
312
@register('headings', 2)
313
def headers(frm=None, to=None):
314
if frm and to is None:
315
if isinstance(frm, String) and frm.value.lower() == 'all':
320
to = int(getattr(frm, 'value', frm))
326
frm = 1 if frm is None else int(getattr(frm, 'value', frm))
330
to = 6 if to is None else int(getattr(to, 'value', to))
333
ret = [String.unquoted('h' + str(i)) for i in range(frm, to + 1)]
334
return List(ret, use_comma=True)
338
def nest(*arguments):
339
if isinstance(arguments[0], List):
341
elif isinstance(arguments[0], String):
342
lst = arguments[0].value.split(',')
344
raise TypeError("Expected list or string, got %r" % (arguments[0],))
348
if isinstance(s, String):
350
elif isinstance(s, six.string_types):
353
raise TypeError("Expected string, got %r" % (s,))
361
for arg in arguments[1:]:
362
if isinstance(arg, List):
364
elif isinstance(arg, String):
365
lst = arg.value.split(',')
367
raise TypeError("Expected list or string, got %r" % (arg,))
371
if isinstance(s, String):
373
elif isinstance(s, six.string_types):
376
raise TypeError("Expected string, got %r" % (s,))
384
new_ret.append(s.replace('&', r))
386
if not r or r[-1] in ('.', ':', '#'):
387
new_ret.append(r + s)
389
new_ret.append(r + ' ' + s)
392
ret = [String.unquoted(s) for s in sorted(set(ret))]
393
return List(ret, use_comma=True)
396
# This isn't actually from Compass, but it's just a shortcut for enumerate().
397
# DEVIATION: allow reversed ranges (range() uses enumerate() which allows reversed values, like '@for .. from .. through')
398
@register('range', 1)
399
@register('range', 2)
400
def range_(frm, through=None):
404
return enumerate_(None, frm, through)
406
# ------------------------------------------------------------------------------
407
# Working with CSS constants
409
OPPOSITE_POSITIONS = {
410
'top': String.unquoted('bottom'),
411
'bottom': String.unquoted('top'),
412
'left': String.unquoted('right'),
413
'right': String.unquoted('left'),
414
'center': String.unquoted('center'),
416
DEFAULT_POSITION = [String.unquoted('center'), String.unquoted('top')]
419
def _position(opposite, positions):
420
if positions is None:
421
positions = DEFAULT_POSITION
423
positions = List.from_maybe(positions)
426
for pos in positions:
427
if isinstance(pos, (String, six.string_types)):
428
pos_value = getattr(pos, 'value', pos)
429
if pos_value in OPPOSITE_POSITIONS:
431
ret.append(OPPOSITE_POSITIONS[pos_value])
435
elif pos_value == 'to':
436
# Gradient syntax keyword; leave alone
440
elif isinstance(pos, Number):
441
if pos.is_simple_unit('%'):
443
ret.append(Number(100 - pos.value, '%'))
447
elif pos.is_simple_unit('deg'):
448
# TODO support other angle types?
450
ret.append(Number((pos.value + 180) % 360, 'deg'))
456
log.warn("Can't find opposite for position %r" % (pos,))
459
return List(ret, use_comma=False).maybe()
462
@register('position')
464
return _position(False, p)
467
@register('opposite-position')
468
def opposite_position(p):
469
return _position(True, p)
472
# ------------------------------------------------------------------------------
477
return Number(math.pi)
482
return Number(math.e)
487
def log_(number, base=None):
488
if not isinstance(number, Number):
489
raise TypeError("Expected number, got %r" % (number,))
490
elif not number.is_unitless:
491
raise ValueError("Expected unitless number, got %r" % (number,))
495
elif not isinstance(base, Number):
496
raise TypeError("Expected number, got %r" % (base,))
497
elif not base.is_unitless:
498
raise ValueError("Expected unitless number, got %r" % (base,))
501
ret = math.log(number.value)
503
ret = math.log(number.value, base.value)
509
def pow(number, exponent):
510
return number ** exponent
513
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.sqrt), 'sqrt', 1)
514
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.sin), 'sin', 1)
515
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.cos), 'cos', 1)
516
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.tan), 'tan', 1)
519
# ------------------------------------------------------------------------------
523
return config.STATIC_ROOT if config.FONTS_ROOT is None else config.FONTS_ROOT
526
def _font_url(path, only_path=False, cache_buster=True, inline=False):
527
filepath = String.unquoted(path).value
529
FONTS_ROOT = _fonts_root()
530
if callable(FONTS_ROOT):
532
_file, _storage = list(FONTS_ROOT(filepath))[0]
533
d_obj = _storage.modified_time(_file)
534
filetime = int(time.mktime(d_obj.timetuple()))
536
file = _storage.open(_file)
540
_path = os.path.join(FONTS_ROOT, filepath.strip('/'))
541
if os.path.exists(_path):
542
filetime = int(os.path.getmtime(_path))
544
file = open(_path, 'rb')
548
BASE_URL = config.FONTS_URL or config.STATIC_URL
551
if re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value):
552
font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value).groups()[1]).value
554
if not FONT_TYPES.get(font_type):
555
raise Exception('Could not determine font type for "%s"' % path.value)
557
mime = FONT_TYPES.get(font_type)
558
if font_type == 'woff':
559
mime = 'application/font-woff'
560
elif font_type == 'eot':
561
mime = 'application/vnd.ms-fontobject'
562
url = 'data:' + (mime if '/' in mime else 'font/%s' % mime) + ';base64,' + base64.b64encode(file.read())
565
url = '%s/%s' % (BASE_URL.rstrip('/'), filepath.lstrip('/'))
566
if cache_buster and filetime != 'NA':
567
url = add_cache_buster(url, filetime)
570
url = 'url(%s)' % escape(url)
571
return String.unquoted(url)
574
def _font_files(args, inline):
576
return String.unquoted("")
581
for index in range(len(args)):
584
font_type = args[index + 1] if args_len > (index + 1) else None
585
if font_type and font_type.value in FONT_TYPES:
588
if re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value):
589
font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value).groups()[1])
591
if font_type.value in FONT_TYPES:
592
fonts.append(String.unquoted('%s format("%s")' % (_font_url(arg, inline=inline), String.unquoted(FONT_TYPES[font_type.value]).value)))
594
raise Exception('Could not determine font type for "%s"' % arg.value)
598
return List(fonts, separator=',')
601
@register('font-url', 1)
602
@register('font-url', 2)
603
def font_url(path, only_path=False, cache_buster=True):
605
Generates a path to an asset found relative to the project's font directory.
606
Passing a true value as the second argument will cause the only the path to
607
be returned instead of a `url()` function
609
return _font_url(path, only_path, cache_buster, False)
612
@register('font-files')
613
def font_files(*args):
614
return _font_files(args, inline=False)
617
@register('inline-font-files')
618
def inline_font_files(*args):
619
return _font_files(args, inline=True)
622
# ------------------------------------------------------------------------------
623
# External stylesheets
625
@register('stylesheet-url', 1)
626
@register('stylesheet-url', 2)
627
def stylesheet_url(path, only_path=False, cache_buster=True):
629
Generates a path to an asset found relative to the project's css directory.
630
Passing a true value as the second argument will cause the only the path to
631
be returned instead of a `url()` function
633
filepath = String.unquoted(path).value
634
if callable(config.STATIC_ROOT):
636
_file, _storage = list(config.STATIC_ROOT(filepath))[0]
637
d_obj = _storage.modified_time(_file)
638
filetime = int(time.mktime(d_obj.timetuple()))
642
_path = os.path.join(config.STATIC_ROOT, filepath.strip('/'))
643
if os.path.exists(_path):
644
filetime = int(os.path.getmtime(_path))
647
BASE_URL = config.STATIC_URL
649
url = '%s%s' % (BASE_URL, filepath)
651
url = add_cache_buster(url, filetime)
653
url = 'url("%s")' % (url)
654
return String.unquoted(url)