def mixin_exists(namespace, name): expect_type(name, String) # TODO invasive, but there's no other way to ask for this at the moment for fname, arity in namespace._mixins.keys(): if name.value == fname: return Boolean(True) return Boolean(False)
def change_color(color, red=None, green=None, blue=None, hue=None, saturation=None, lightness=None, alpha=None): do_rgb = red or green or blue do_hsl = hue or saturation or lightness if do_rgb and do_hsl: raise ValueError("Can't change both RGB and HSL channels at the same time") if alpha is None: alpha = color.alpha else: alpha = alpha.value if do_rgb: channels = list(color.rgba[:3]) if red: channels[0] = _interpret_percentage(red, relto=255) if green: channels[1] = _interpret_percentage(green, relto=255) if blue: channels[2] = _interpret_percentage(blue, relto=255) return Color.from_rgb(*channels, alpha=alpha) else: channels = list(color.hsl) if hue: expect_type(hue, Number, unit=None) channels[0] = (hue.value / 360) % 1 # Ruby sass treats plain numbers for saturation and lightness as though # they were percentages, just without the % if saturation: channels[1] = _interpret_percentage(saturation, relto=100) if lightness: channels[2] = _interpret_percentage(lightness, relto=100) return Color.from_hsl(*channels, alpha=alpha)
def set_nth(list, n, value): expect_type(n, Number, unit=None) py_n = n.to_python_index(len(list)) return List( tuple(list[:py_n]) + (value,) + tuple(list[py_n + 1:]), use_comma=list.use_comma)
def join(lst1, lst2, separator=String.unquoted('auto')): expect_type(separator, String) ret = [] ret.extend(List.from_maybe(lst1)) ret.extend(List.from_maybe(lst2)) if separator.value == 'comma': use_comma = True elif separator.value == 'space': use_comma = False elif separator.value == 'auto': # The Sass docs are slightly misleading here, but the algorithm is: use # the delimiter from the first list that has at least 2 items, or # default to spaces. if len(lst1) > 1: use_comma = lst1.use_comma elif len(lst2) > 1: use_comma = lst2.use_comma else: use_comma = False else: raise ValueError("separator for join() must be comma, space, or auto") return List(ret, use_comma=use_comma)
def absolute_path(relative_path): """Return an absolute path for the given relative path, relative to the calling file. """ expect_type(relative_path, String) # TODO i can't get the calling file until "rule" or something else helpful # is actually passed in! return String(os.path.abspath(relative_path.value))
def variable_exists(namespace, name): expect_type(name, String) try: namespace.variable('$' + name.value) except KeyError: return Boolean(False) else: return Boolean(True)
def invert(color): """Returns the inverse (negative) of a color. The red, green, and blue values are inverted, while the opacity is left alone. """ if isinstance(color, Number): # invert(n) and invert(n%) are CSS3 filters and should be left # intact return String.unquoted("invert(%s)" % (color.render(), )) expect_type(color, Color) r, g, b, a = color.rgba return Color.from_rgb(1 - r, 1 - g, 1 - b, alpha=a)
def invert(color): """Returns the inverse (negative) of a color. The red, green, and blue values are inverted, while the opacity is left alone. """ if isinstance(color, Number): # invert(n) and invert(n%) are CSS3 filters and should be left # intact return String.unquoted("invert(%s)" % (color.render(),)) expect_type(color, Color) r, g, b, a = color.rgba return Color.from_rgb(1 - r, 1 - g, 1 - b, alpha=a)
def join_file_segments(*segments): """Join path parts into a single path, using the appropriate OS-specific delimiter. """ parts = [] for segment in segments: expect_type(segment, String) parts.append(segment.value) if parts: return String(os.path.join(*parts)) else: return String('')
def global_variable_exists(namespace, name): expect_type(name, String) # TODO this is... imperfect and invasive, but should be a good # approximation scope = namespace._variables while len(scope.maps) > 1: scope = scope.maps[-1] try: scope['$' + name.value] except KeyError: return Boolean(False) else: return Boolean(True)
def _interpret_percentage(n, relto=1., clamp=True): expect_type(n, Number, unit='%') if n.is_unitless: ret = n.value / relto else: ret = n.value / 100 if clamp: if ret < 0: return 0 elif ret > 1: return 1 return ret
def nth(lst, n): """Return the nth item in the list.""" expect_type(n, (String, Number), unit=None) if isinstance(n, String): if n.value.lower() == 'first': i = 0 elif n.value.lower() == 'last': i = -1 else: raise ValueError("Invalid index %r" % (n,)) else: # DEVIATION: nth treats lists as circular lists i = n.to_python_index(len(lst), circular=True) return lst[i]
def _scale_channel(channel, scaleby): if scaleby is None: return channel expect_type(scaleby, Number) if not scaleby.is_simple_unit('%'): raise ValueError("Expected percentage, got %r" % (scaleby,)) factor = scaleby.value / 100 if factor > 0: # Add x% of the remaining range, up to 1 return channel + (1 - channel) * factor else: # Subtract x% of the existing channel. We add here because the factor # is already negative return channel * (1 + factor)
def nth(lst, n): """ Return the Nth item in the string """ expect_type(n, (String, Number), unit=None) if isinstance(n, String): if n.value.lower() == "first": i = 0 elif n.value.lower() == "last": i = -1 else: raise ValueError("Invalid index %r" % (n,)) else: i = (int(n.value) - 1) % len(lst) return lst[i]
def append(lst, val, separator=String.unquoted('auto')): expect_type(separator, String) ret = [] ret.extend(List.from_maybe(lst)) ret.append(val) separator = separator.value if separator == 'comma': use_comma = True elif separator == 'space': use_comma = False elif separator == 'auto': if len(lst) < 2: use_comma = False else: use_comma = lst.use_comma else: raise ValueError('Separator must be auto, comma, or space') return List(ret, use_comma=use_comma)
def str_insert(string, insert, index): expect_type(string, String) expect_type(insert, String) expect_type(index, Number, unit=None) py_index = index.to_python_index(len(string.value), check_bounds=False) return String(string.value[:py_index] + insert.value + string.value[py_index:], quotes=string.quotes)
def str_insert(string, insert, index): expect_type(string, String) expect_type(insert, String) expect_type(index, Number, unit=None) py_index = index.to_python_index(len(string.value), check_bounds=False) return String( string.value[:py_index] + insert.value + string.value[py_index:], quotes=string.quotes)
def str_slice(string, start_at, end_at=None): expect_type(string, String) expect_type(start_at, Number, unit=None) py_start_at = start_at.to_python_index(len(string.value)) if end_at is None: py_end_at = None else: expect_type(end_at, Number, unit=None) # Endpoint is inclusive, unlike Python py_end_at = end_at.to_python_index(len(string.value)) + 1 return String(string.value[py_start_at:py_end_at], quotes=string.quotes)
def str_slice(string, start_at, end_at=None): expect_type(string, String) expect_type(start_at, Number, unit=None) py_start_at = start_at.to_python_index(len(string.value)) if end_at is None: py_end_at = None else: expect_type(end_at, Number, unit=None) # Endpoint is inclusive, unlike Python py_end_at = end_at.to_python_index(len(string.value)) + 1 return String( string.value[py_start_at:py_end_at], quotes=string.quotes)
def str_length(string): expect_type(string, String) # nb: can't use `len(string)`, because that gives the Sass list length, # which is 1 return Number(len(string.value))
def keywords(value): """Extract named arguments, as a map, from an argument list.""" expect_type(value, Arglist) return value.extract_keywords()
def split_filename(path): expect_type(path, String) dir_, file_ = os.path.split(path.value) base, ext = os.path.splitext(file_) return List([String(dir_), String(base), String(ext)], use_comma=False)
def str_index(string, substring): expect_type(string, String) expect_type(substring, String) # 1-based indexing, with 0 for failure return Number(string.value.find(substring.value) + 1)
def percentage(value): expect_type(value, Number, unit=None) return value * Number(100, unit='%')
def to_lower_case(string): expect_type(string, String) return String(string.value.lower(), quotes=string.quotes)