# -*- coding: utf8 -*- """ .. module:: lesscpy.lessc.utility :synopsis: various utility functions Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson <jtm@robot.is> """ from __future__ import print_function import collections import itertools import math import re import sys def flatten(lst): """Flatten list. Args: lst (list): List to flatten Returns: generator """ for elm in lst: if isinstance(elm, collections.Iterable) and not isinstance(elm, str): for sub in flatten(elm): yield sub else: yield elm def pairwise(lst): """ yield item i and item i+1 in lst. e.g. (lst[0], lst[1]), (lst[1], lst[2]), ..., (lst[-1], None) Args: lst (list): List to process Returns: list """ if not lst: return length = len(lst) for i in range(length - 1): yield lst[i], lst[i + 1] yield lst[-1], None def rename(blocks, scope, stype): """ Rename all sub-blocks moved under another block. (mixins) Args: lst (list): block list scope (object): Scope object """ for p in blocks: if isinstance(p, stype): p.tokens[0].parse(scope) if p.tokens[1]: scope.push() scope.current = p.tokens[0] rename(p.tokens[1], scope, stype) scope.pop() def blocksearch(block, name): """ Recursive search for name in block (inner blocks) Args: name (str): search term Returns: Block OR False """ if hasattr(block, 'tokens'): for b in block.tokens[1]: b = (b if hasattr(b, 'raw') and b.raw() == name else blocksearch(b, name)) if b: return b return False def reverse_guard(lst): """ Reverse guard expression. not (@a > 5) -> (@a =< 5) Args: lst (list): Expression returns: list """ rev = { '<': '>=', '>': '=<', '>=': '<', '=<': '>' } return [rev[l] if l in rev else l for l in lst] def debug_print(lst, lvl=0): """ Print scope tree args: lst (list): parse result lvl (int): current nesting level """ pad = ''.join(['\t.'] * lvl) t = type(lst) if t is list: for p in lst: debug_print(p, lvl) elif hasattr(lst, 'tokens'): print(pad, t) debug_print(list(flatten(lst.tokens)), lvl + 1) def destring(value): """ Strip quotes from string args: value (str) returns: str """ return value.strip('"\'') def analyze_number(var, err=''): """ Analyse number for type and split from unit 1px -> (q, 'px') args: var (str): number string kwargs: err (str): Error message raises: SyntaxError returns: tuple """ n, u = split_unit(var) if not isinstance(var, str): return (var, u) if is_color(var): return (var, 'color') if is_int(n): n = int(n) elif is_float(n): n = float(n) else: raise SyntaxError('%s ´%s´' % (err, var)) return (n, u) def with_unit(number, unit=None): """ Return number with unit args: number (mixed): Number unit (str): Unit returns: str """ if isinstance(number, tuple): number, unit = number if number == 0: return '0' if unit: number = str(number) if number.startswith('.'): number = '0' + number return "%s%s" % (number, unit) return number if isinstance(number, str) else str(number) def is_color(value): """ Is string CSS color args: value (str): string returns: bool """ if not value or not isinstance(value, str): return False if value[0] == '#' and len(value) in [4, 5, 7, 9]: try: int(value[1:], 16) return True except ValueError: pass return False def is_variable(value): """ Check if string is LESS variable args: value (str): string returns: bool """ if isinstance(value, str): return (value.startswith('@') or value.startswith('-@')) elif isinstance(value, tuple): value = ''.join(value) return (value.startswith('@') or value.startswith('-@')) return False def is_int(value): """ Is value integer args: value (str): string returns: bool """ try: int(str(value)) return True except (ValueError, TypeError): pass return False def is_float(value): """ Is value float args: value (str): string returns: bool """ if not is_int(value): try: float(str(value)) return True except (ValueError, TypeError): pass return False def split_unit(value): """ Split a number from its unit 1px -> (q, 'px') Args: value (str): input returns: tuple """ r = re.search('^(\-?[\d\.]+)(.*)$', str(value)) return r.groups() if r else ('', '') def away_from_zero_round(value, ndigits=0): """Round half-way away from zero. Python2's round() method. """ if sys.version_info[0] >= 3: p = 10 ** ndigits return float(math.floor((value * p) + math.copysign(0.5, value))) / p else: return round(value, ndigits) def convergent_round(value, ndigits=0): """Convergent rounding. Round to neareas even, similar to Python3's round() method. """ if sys.version_info[0] < 3: if value < 0.0: return -convergent_round(-value) epsilon = 0.0000001 integral_part, _ = divmod(value, 1) if abs(value - (integral_part + 0.5)) < epsilon: if integral_part % 2.0 < epsilon: return integral_part else: nearest_even = integral_part + 0.5 return math.ceil(nearest_even) return round(value, ndigits) def pc_or_float(s): """ Utility function to process strings that contain either percentiles or floats args: str: s returns: float """ if isinstance(s, str) and '%' in s: return float(s.strip('%')) / 100.0 return float(s) def permutations_with_replacement(iterable, r=None): """Return successive r length permutations of elements in the iterable. Similar to itertools.permutation but withouth repeated values filtering. """ pool = tuple(iterable) n = len(pool) r = n if r is None else r for indices in itertools.product(range(n), repeat=r): yield list(pool[i] for i in indices)