""" ColorHelper. Copyright (c) 2015 - 2017 Isaac Muse <isaacmuse@gmail.com> License: MIT """ import sublime import re import decimal from .lib import csscolors from .lib.rgba import RGBA, round_int, clamp from textwrap import dedent import platform import math CONVERT_TURN = 360 CONVERT_GRAD = 90 / 100 LINE_HEIGHT_WORKAROUND = platform.system() == "Windows" FLOAT_TRIM_RE = re.compile(r'^(?P<keep>\d+)(?P<trash>\.0+|(?P<keep2>\.\d*[1-9])0+)$') COLOR_PARTS = { "percent": r"[+\-]?(?:(?:\d*\.\d+)|\d+)%", "float": r"[+\-]?(?:(?:\d*\.\d+)|\d+)", "angle": r"[+\-]?(?:(?:\d*\.\d+)|\d+)(deg|rad|turn|grad)?" } COMPLETE = r''' (?P<hexa>\#(?P<hexa_content>[\dA-Fa-f]{8}))\b | (?P<hex>\#(?P<hex_content>[\dA-Fa-f]{6}))\b | (?P<hexa_compressed>\#(?P<hexa_compressed_content>[\dA-Fa-f]{4}))\b | (?P<hex_compressed>\#(?P<hex_compressed_content>[\dA-Fa-f]{3}))\b | \b(?P<rgb>rgb\(\s*(?P<rgb_content>(?:%(float)s\s*,\s*){2}%(float)s | (?:%(percent)s\s*,\s*){2}%(percent)s)\s*\)) | \b(?P<rgba>rgba\(\s*(?P<rgba_content> (?:%(float)s\s*,\s*){3}(?:%(percent)s|%(float)s) | (?:%(percent)s\s*,\s*){3}(?:%(percent)s|%(float)s) )\s*\)) | \b(?P<hsl>hsl\(\s*(?P<hsl_content>%(angle)s\s*,\s*%(percent)s\s*,\s*%(percent)s)\s*\)) | \b(?P<hsla>hsla?\(\s*(?P<hsla_content>%(angle)s\s*,\s*(?:%(percent)s\s*,\s*){2}(?:%(percent)s|%(float)s))\s*\)) | \b(?P<hwb>hwb\(\s*(?P<hwb_content>%(angle)s\s*,\s*%(percent)s\s*,\s*%(percent)s)\s*\)) | \b(?P<hwba>hwb\(\s*(?P<hwba_content>%(angle)s\s*,\s*(?:%(percent)s\s*,\s*){2}(?:%(percent)s|%(float)s))\s*\)) | \b(?P<gray>gray\(\s*(?P<gray_content>%(float)s|%(percent)s)\s*\)) | \b(?P<graya>gray\(\s*(?P<graya_content>(?:%(float)s|%(percent)s)\s*,\s*(?:%(percent)s|%(float)s))\s*\)) ''' % COLOR_PARTS INCOMPLETE = r''' (?P<hash>\#) | \b(?P<rgb_open>rgb\() | \b(?P<rgba_open>rgba\() | \b(?P<hsl_open>hsl\() | \b(?P<hsla_open>hsla\() | \b(?P<hwb_open>hwb\() | \b(?P<gray_open>hwb\() ''' COLOR_NAMES = r'\b(?P<webcolors>%s)\b(?!\()' % '|'.join([name for name in csscolors.name2hex_map.keys()]) TAG_HTML_RE = re.compile( br'''(?x)(?i) (?: (?P<comments>(\r?\n?\s*)<!--[\s\S]*?-->(\s*)(?=\r?\n)|<!--[\s\S]*?-->)| (?P<style><style((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^>\s]+))?)*)\s*>(?P<css>.*?)<\/style[^>]*>) | (?P<open><[\w\:\.\-]+) (?P<attr>(?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'))?)*) (?P<close>\s*(?:\/?)>) ) ''', re.DOTALL ) TAG_STYLE_ATTR_RE = re.compile( br'''(?x) (?P<attr> (?: \s+style (?:\s*=\s*(?P<content>"[^"]*"|'[^']*')) ) ) ''', re.DOTALL ) HEX_IS_GRAY_RE = re.compile(r'(?i)^#([0-9a-f]{2})\1\1') HEX_COMPRESS_RE = re.compile(r'(?i)^#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3(?:([0-9a-f])\4)?$') COLOR_RE = re.compile(r'(?x)(?i)(?<![@#$.\-_])(?:%s|%s)(?![@#$.\-_])' % (COMPLETE, COLOR_NAMES)) COLOR_ALL_RE = re.compile(r'(?x)(?i)(?<![@#$.\-_])(?:%s|%s|%s)(?![@#$.\-_])' % (COMPLETE, COLOR_NAMES, INCOMPLETE)) INDEX_ALL_RE = re.compile((r'(?x)(?i)(?<![@#$.\-_])(?:%s|%s)(?![@#$.\-_])' % (COMPLETE, COLOR_NAMES)).encode('utf-8')) ADD_CSS = dedent( ''' div.color-helper { margin: 0; padding: 0.5rem; } .color-helper .small { font-size: 0.8rem; } .color-helper .alpha { text-decoration: underline; } ''' ) WRAPPER_CLASS = "color-helper content" CSS3 = ("webcolors", "hex", "hex_compressed", "rgb", "rgba", "hsl", "hsla") CSS4 = CSS3 + ("gray", "graya", "hwb", "hwba", "hexa", "hexa_compressed") ALL = CSS4 def norm_angle(angle): """Normalize angle units.""" if angle.endswith('turn'): value = float(angle[:-4]) * CONVERT_TURN elif angle.endswith('grad'): value = float(angle[:-4]) * CONVERT_GRAD elif angle.endswith('rad'): value = math.degrees(float(angle[:-3])) elif angle.endswith('deg'): value = float(angle[:-3]) else: value = float(angle) return value def log(*args): """Log.""" text = ['\nColorHelper: '] for arg in args: text.append(str(arg)) text.append('\n') print(''.join(text)) def debug(*args): """Log if debug enabled.""" if sublime.load_settings("color_helper.sublime-settings").get('debug', False): log(*args) def get_line_height(view): """Get the line height.""" height = view.line_height() settings = sublime.load_settings("color_helper.sublime-settings") return int((height / 2.0) if LINE_HEIGHT_WORKAROUND and settings.get('line_height_workaround', False) else height) def color_picker_available(): """Check if color picker is available.""" s = sublime.load_settings('color_helper_share.sublime-settings') s.set('color_pick_return', None) sublime.run_command('color_pick_api_is_available', {'settings': 'color_helper_share.sublime-settings'}) return s.get('color_pick_return', None) def fmt_float(f, p=0): """Set float precision and trim precision zeros.""" string = str( decimal.Decimal(f).quantize(decimal.Decimal('0.' + ('0' * p) if p > 0 else '0'), decimal.ROUND_HALF_UP) ) m = FLOAT_TRIM_RE.match(string) if m: string = m.group('keep') if m.group('keep2'): string += m.group('keep2') return string def get_rules(view): """Get auto-popup scope rule.""" rules = view.settings().get("color_helper.scan") return rules if rules is not None and rules.get("enabled", False) else None def get_scope(view, rules, skip_sel_check=False): """Get auto-popup scope rule.""" scopes = None if rules is not None: scopes = ','.join(rules.get('scan_scopes', [])) sels = view.sel() if not skip_sel_check: if len(sels) == 0 or not scopes or view.score_selector(sels[0].begin(), scopes) == 0: scopes = None return scopes def get_scope_completion(view, rules, skip_sel_check=False): """Get additional auto-popup scope rules for incomplete colors only.""" scopes = None if rules is not None: scopes = ','.join(rules.get('scan_completion_scopes', [])) sels = view.sel() if not skip_sel_check: if len(sels) == 0 or not scopes or view.score_selector(sels[0].begin(), scopes) == 0: scopes = None return scopes def get_favs(): """Get favorites object.""" bookmark_colors = sublime.load_settings('color_helper.palettes').get("favorites", []) return {"name": "Favorites", "colors": bookmark_colors} def save_palettes(palettes, favs=False): """Save palettes.""" s = sublime.load_settings('color_helper.palettes') if favs: s.set('favorites', palettes) else: s.set('palettes', palettes) sublime.save_settings('color_helper.palettes') def save_project_palettes(window, palettes): """Save project palettes.""" data = window.project_data() if data is None: data = {'color_helper_palettes': palettes} else: data['color_helper_palettes'] = palettes window.set_project_data(data) def get_palettes(): """Get palettes.""" return sublime.load_settings('color_helper.palettes').get("palettes", []) def get_project_palettes(window): """Get project palettes.""" data = window.project_data() if data is None: data = {} return data.get('color_helper_palettes', []) def get_project_folders(window): """Get project folder.""" data = window.project_data() if data is None: data = {'folders': [{'path': f} for f in window.folders()]} return data.get('folders', []) def is_gray(color): """Check if color is gray (all channels the same).""" m = HEX_IS_GRAY_RE.match(color) return m is not None def compress_hex(color): """Compress hex.""" m = HEX_COMPRESS_RE.match(color) if m: color = '#' + m.group(1) + m.group(2) + m.group(3) if m.group(4): color += m.group(4) return color def alpha_dec_normalize(dec): """Normalize a decimal alpha value.""" temp = float(dec) if temp < 0.0 or temp > 1.0: dec = fmt_float(clamp(float(temp), 0.0, 1.0), 3) alpha_dec = dec alpha = "%02X" % round_int(float(alpha_dec) * 255.0) return alpha, alpha_dec def alpha_percent_normalize(perc): """Normalize a percent alpha value.""" alpha_float = clamp(float(perc.strip('%')), 0.0, 100.0) / 100.0 alpha_dec = fmt_float(alpha_float, 3) alpha = "%02X" % round_int(alpha_float * 255.0) return alpha, alpha_dec def translate_color(m, use_hex_argb=False, decode=False): """Translate the match object to a color w/ alpha.""" color = None alpha = None alpha_dec = None if m.group('hex_compressed'): if decode: content = m.group('hex_compressed_content').decode('utf-8') else: content = m.group('hex_compressed_content') color = "#%02x%02x%02x" % ( int(content[0:1] * 2, 16), int(content[1:2] * 2, 16), int(content[2:3] * 2, 16) ) elif m.group('hexa_compressed') and use_hex_argb: if decode: content = m.group('hexa_compressed_content').decode('utf-8') else: content = m.group('hexa_compressed_content') color = "#%02x%02x%02x" % ( int(content[1:2] * 2, 16), int(content[2:3] * 2, 16), int(content[3:] * 2, 16) ) alpha = content[0:1] * 2 alpha_dec = fmt_float(float(int(alpha, 16)) / 255.0, 3) elif m.group('hexa_compressed'): if decode: content = m.group('hexa_compressed_content').decode('utf-8') else: content = m.group('hexa_compressed_content') color = "#%02x%02x%02x" % ( int(content[0:1] * 2, 16), int(content[1:2] * 2, 16), int(content[2:3] * 2, 16) ) alpha = content[3:] * 2 alpha_dec = fmt_float(float(int(alpha, 16)) / 255.0, 3) elif m.group('hex'): if decode: content = m.group('hex_content').decode('utf-8') else: content = m.group('hex_content') if len(content) == 6: color = "#%02x%02x%02x" % ( int(content[0:2], 16), int(content[2:4], 16), int(content[4:6], 16) ) else: color = "#%02x%02x%02x" % ( int(content[0:1] * 2, 16), int(content[1:2] * 2, 16), int(content[2:3] * 2, 16) ) elif m.group('hexa') and use_hex_argb: if decode: content = m.group('hexa_content').decode('utf-8') else: content = m.group('hexa_content') if len(content) == 8: color = "#%02x%02x%02x" % ( int(content[2:4], 16), int(content[4:6], 16), int(content[6:], 16) ) alpha = content[0:2] alpha_dec = fmt_float(float(int(alpha, 16)) / 255.0, 3) else: color = "#%02x%02x%02x" % ( int(content[1:2] * 2, 16), int(content[2:3] * 2, 16), int(content[3:] * 2, 16) ) alpha = content[0:1] alpha_dec = fmt_float(float(int(alpha, 16)) / 255.0, 3) elif m.group('hexa'): if decode: content = m.group('hexa_content').decode('utf-8') else: content = m.group('hexa_content') if len(content) == 8: color = "#%02x%02x%02x" % ( int(content[0:2], 16), int(content[2:4], 16), int(content[4:6], 16) ) alpha = content[6:] alpha_dec = fmt_float(float(int(alpha, 16)) / 255.0, 3) else: color = "#%02x%02x%02x" % ( int(content[0:1] * 2, 16), int(content[1:2] * 2, 16), int(content[2:3] * 2, 16) ) alpha = content[3:] alpha_dec = fmt_float(float(int(alpha, 16)) / 255.0, 3) elif m.group('rgb'): if decode: content = [x.strip() for x in m.group('rgb_content').decode('utf-8').split(',')] else: content = [x.strip() for x in m.group('rgb_content').split(',')] if content[0].endswith('%'): r = round_int(clamp(float(content[0].strip('%')), 0.0, 255.0) * (255.0 / 100.0)) g = round_int(clamp(float(content[1].strip('%')), 0.0, 255.0) * (255.0 / 100.0)) b = round_int(clamp(float(content[2].strip('%')), 0.0, 255.0) * (255.0 / 100.0)) color = "#%02x%02x%02x" % (r, g, b) else: color = "#%02x%02x%02x" % ( clamp(round_int(float(content[0])), 0, 255), clamp(round_int(float(content[1])), 0, 255), clamp(round_int(float(content[2])), 0, 255) ) elif m.group('rgba'): if decode: content = [x.strip() for x in m.group('rgba_content').decode('utf-8').split(',')] else: content = [x.strip() for x in m.group('rgba_content').split(',')] if content[0].endswith('%'): r = round_int(clamp(float(content[0].strip('%')), 0.0, 255.0) * (255.0 / 100.0)) g = round_int(clamp(float(content[1].strip('%')), 0.0, 255.0) * (255.0 / 100.0)) b = round_int(clamp(float(content[2].strip('%')), 0.0, 255.0) * (255.0 / 100.0)) color = "#%02x%02x%02x" % (r, g, b) else: color = "#%02x%02x%02x" % ( clamp(round_int(float(content[0])), 0, 255), clamp(round_int(float(content[1])), 0, 255), clamp(round_int(float(content[2])), 0, 255) ) if content[3].endswith('%'): alpha, alpha_dec = alpha_percent_normalize(content[3]) else: alpha, alpha_dec = alpha_dec_normalize(content[3]) elif m.group('gray'): if decode: content = m.group('gray_content').decode('utf-8') else: content = m.group('gray_content') if content.endswith('%'): g = round_int(clamp(float(content.strip('%')), 0.0, 255.0) * (255.0 / 100.0)) else: g = clamp(round_int(float(content)), 0, 255) color = "#%02x%02x%02x" % (g, g, g) elif m.group('graya'): if decode: content = [x.strip() for x in m.group('graya_content').decode('utf-8').split(',')] else: content = [x.strip() for x in m.group('graya_content').split(',')] if content[0].endswith('%'): g = round_int(clamp(float(content[0].strip('%')), 0.0, 255.0) * (255.0 / 100.0)) else: g = clamp(round_int(float(content[0])), 0, 255) color = "#%02x%02x%02x" % (g, g, g) if content[1].endswith('%'): alpha, alpha_dec = alpha_percent_normalize(content[1]) else: alpha, alpha_dec = alpha_dec_normalize(content[1]) elif m.group('hsl') or m.group('hsla'): content = m.group('hsl_content') if m.group('hsl') else m.group('hsla_content') if decode: content = [x.strip() for x in content.decode('utf-8').split(',')] else: content = [x.strip() for x in content.split(',')] rgba = RGBA() hue = norm_angle(content[0]) if hue < 0.0 or hue > 360.0: hue = hue % 360.0 h = hue / 360.0 s = clamp(float(content[1].strip('%')), 0.0, 100.0) / 100.0 l = clamp(float(content[2].strip('%')), 0.0, 100.0) / 100.0 rgba.fromhls(h, l, s) color = rgba.get_rgb() if len(content) == 4: if content[3].endswith('%'): alpha, alpha_dec = alpha_percent_normalize(content[3]) else: alpha, alpha_dec = alpha_dec_normalize(content[3]) elif m.group('hwb') or m.group('hwba'): content = m.group('hwb_content') if m.group('hwb') else m.group('hwba_content') if decode: content = [x.strip() for x in content.decode('utf-8').split(',')] else: content = [x.strip() for x in content.split(',')] rgba = RGBA() hue = norm_angle(content[0]) if hue < 0.0 or hue > 360.0: hue = hue % 360.0 h = hue / 360.0 w = clamp(float(content[1].strip('%')), 0.0, 100.0) / 100.0 b = clamp(float(content[2].strip('%')), 0.0, 100.0) / 100.0 rgba.fromhwb(h, w, b) color = rgba.get_rgb() if len(content) == 4: if content[3].endswith('%'): alpha, alpha_dec = alpha_percent_normalize(content[3]) else: alpha, alpha_dec = alpha_dec_normalize(content[3]) elif m.group('webcolors'): try: if decode: color = csscolors.name2hex(m.group('webcolors').decode('utf-8')).lower() else: color = csscolors.name2hex(m.group('webcolors')).lower() except Exception: pass return color, alpha, alpha_dec