From 2e2e99b7294a9581e57030d4fad4e2ff2b908315 Mon Sep 17 00:00:00 2001 From: facelessuser Date: Sat, 20 Aug 2022 21:31:30 -0600 Subject: [PATCH 1/7] Prepare for ColorAide update Also add new color blind filters --- CHANGES.md | 2 + lib/color_scheme_matcher.py | 5 +- lib/color_scheme_tweaker.py | 90 +++++++----- lib/st_colormod.py | 273 ++++++++++++++++++++++++------------ lib/tmtheme.py | 125 +++++------------ 5 files changed, 280 insertions(+), 215 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e7a6efd..ce756f6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## 2.17.5 +- **NEW**: New colorblind filters. +- **CHORE**: Updates to support newer dependencies. - **FIX**: Remove unnecessary dependencies. - **FIX**: Fix typo in settings configuration. diff --git a/lib/color_scheme_matcher.py b/lib/color_scheme_matcher.py index 7256d00..408ab92 100644 --- a/lib/color_scheme_matcher.py +++ b/lib/color_scheme_matcher.py @@ -77,14 +77,13 @@ class SchemeColors( [ 'fg', 'fg_simulated', 'bg', "bg_simulated", "style", "color_gradient", "fg_selector", "bg_selector", "style_selectors", "color_gradient_selector" - ], - verbose=False + ] ) ): """Scheme colors.""" -class SchemeSelectors(namedtuple('SchemeSelectors', ['name', 'scope'], verbose=False)): +class SchemeSelectors(namedtuple('SchemeSelectors', ['name', 'scope'])): """Scheme selectors.""" diff --git a/lib/color_scheme_tweaker.py b/lib/color_scheme_tweaker.py index a9833aa..d589393 100644 --- a/lib/color_scheme_tweaker.py +++ b/lib/color_scheme_tweaker.py @@ -7,7 +7,7 @@ from __future__ import absolute_import import sublime from .st_colormod import Color -from mdpopups.coloraide import util +from mdpopups.coloraide import algebra as alg import re NEW_SCHEMES = int(sublime.version()) >= 3150 @@ -18,7 +18,7 @@ r'''(?x) ^(?: (brightness|saturation|hue|contrast|colorize|glow)\((-?[\d]+|[\d]*\.[\d]+)\)| - (sepia|grayscale|invert) + (sepia|grayscale|invert|protan|deutan|tritan) ) (?:@(fg|bg))?$ ''' @@ -107,38 +107,56 @@ def hue(color, deg): def contrast(color, factor): """Adjust contrast.""" - r, g, b = [util.round_half_up(util.clamp(c * 255, 0, 255)) for c in util.no_nan(color.coords())] + r, g, b = [alg.round_half_up(alg.clamp(c * 255, 0, 255)) for c in alg.no_nans(color[:-1])] # Algorithm can't handle any thing beyond +/-255 (or a factor from 0 - 2) # Convert factor between (-255, 255) - f = (util.clamp(factor, 0.0, 2.0) - 1.0) * 255.0 + f = (alg.clamp(factor, 0.0, 2.0) - 1.0) * 255.0 f = (259 * (f + 255)) / (255 * (259 - f)) # Increase/decrease contrast accordingly. - r = util.clamp(util.round_half_up((f * (r - 128)) + 128), 0, 255) - g = util.clamp(util.round_half_up((f * (g - 128)) + 128), 0, 255) - b = util.clamp(util.round_half_up((f * (b - 128)) + 128), 0, 255) - color.red = r / 255 - color.green = g / 255 - color.blue = b / 255 + r = alg.clamp(alg.round_half_up((f * (r - 128)) + 128), 0, 255) + g = alg.clamp(alg.round_half_up((f * (g - 128)) + 128), 0, 255) + b = alg.clamp(alg.round_half_up((f * (b - 128)) + 128), 0, 255) + color['red'] = r / 255 + color['green'] = g / 255 + color['blue'] = b / 255 + + @staticmethod + def protan(color): + """Invert the color.""" + + color.filter('protan', in_place=True) + + @staticmethod + def deutan(color): + """Invert the color.""" + + color.filter('deutan', in_place=True) + + @staticmethod + def tritan(color): + """Invert the color.""" + + color.filter('tritan', in_place=True) @staticmethod def invert(color): """Invert the color.""" - r, g, b = [int(util.round_half_up(util.clamp(c * 255, 0, 255))) for c in util.no_nan(color.coords())] + r, g, b = [int(alg.round_half_up(alg.clamp(c * 255, 0, 255))) for c in alg.no_nans(color[:-1])] r ^= 0xFF g ^= 0xFF b ^= 0xFF - color.red = r / 255 - color.green = g / 255 - color.blue = b / 255 + color['red'] = r / 255 + color['green'] = g / 255 + color['blue'] = b / 255 @staticmethod def saturation(color, factor): """Saturate or unsaturate the color by the given factor.""" - s = util.no_nan(color.get('hsl.saturation')) / 100.0 - s = util.clamp(s + factor - 1.0, 0.0, 1.0) + s = alg.no_nan(color.get('hsl.saturation')) / 100.0 + s = alg.clamp(s + factor - 1.0, 0.0, 1.0) color.set('hsl.saturation', s * 100) @staticmethod @@ -146,21 +164,21 @@ def grayscale(color): """Convert the color with a grayscale filter.""" luminance = color.luminance() - color.red = luminance - color.green = luminance - color.blue = luminance + color['red'] = luminance + color['green'] = luminance + color['blue'] = luminance @staticmethod def sepia(color): """Apply a sepia filter to the color.""" - red, green, blue = util.no_nan(color.coords()) - r = util.clamp((red * .393) + (green * .769) + (blue * .189), 0, 1) - g = util.clamp((red * .349) + (green * .686) + (blue * .168), 0, 1) - b = util.clamp((red * .272) + (green * .534) + (blue * .131), 0, 1) - color.red = r - color.green = g - color.blue = b + red, green, blue = alg.no_nans(color[:-1]) + r = alg.clamp((red * .393) + (green * .769) + (blue * .189), 0, 1) + g = alg.clamp((red * .349) + (green * .686) + (blue * .168), 0, 1) + b = alg.clamp((red * .272) + (green * .534) + (blue * .131), 0, 1) + color['red'] = r + color['green'] = g + color['blue'] = b @staticmethod def _get_overage(c): @@ -206,9 +224,9 @@ def brightness(cls, color, factor): Brightness is determined by perceived luminance. """ - red, green, blue = [util.round_half_up(util.clamp(c * 255, 0, 255)) for c in util.no_nan(color.coords())] + red, green, blue = [alg.round_half_up(alg.clamp(c * 255, 0, 255)) for c in alg.no_nans(color[:-1])] channels = ["r", "g", "b"] - total_lumes = util.clamp(util.clamp(color.luminance(), 0, 1) * 255 + (255.0 * factor) - 255.0, 0.0, 255.0) + total_lumes = alg.clamp(alg.clamp(color.luminance(), 0, 1) * 255 + (255.0 * factor) - 255.0, 0.0, 255.0) if total_lumes == 255.0: # white @@ -218,7 +236,7 @@ def brightness(cls, color, factor): r, g, b = 0, 0, 0 else: # Adjust Brightness - pts = (total_lumes - util.clamp(color.luminance(), 0, 1) * 255) + pts = (total_lumes - alg.clamp(color.luminance(), 0, 1) * 255) slots = set(channels) components = [float(red) + pts, float(green) + pts, float(blue) + pts] count = 0 @@ -229,12 +247,12 @@ def brightness(cls, color, factor): components = list(cls._distribute_overage(components, overage, slots)) count += 1 - r = util.clamp(util.round_half_up(components[0]), 0, 255) / 255.0 - g = util.clamp(util.round_half_up(components[1]), 0, 255) / 255.0 - b = util.clamp(util.round_half_up(components[2]), 0, 255) / 255.0 - color.red = r - color.green = g - color.blue = b + r = alg.clamp(alg.round_half_up(components[0]), 0, 255) / 255.0 + g = alg.clamp(alg.round_half_up(components[1]), 0, 255) / 255.0 + b = alg.clamp(alg.round_half_up(components[2]), 0, 255) / 255.0 + color['red'] = r + color['green'] = g + color['blue'] = b class ColorTweaker(object): @@ -292,7 +310,7 @@ def _filter_colors(self, *args, **kwargs): name = f[0] value = f[1] context = f[2] - if name in ("grayscale", "sepia", "invert"): + if name in ("grayscale", "sepia", "invert", "tritan", "protan", "deutan"): if context != "bg": self._apply_filter(rgba_fg, name) if context != "fg": diff --git a/lib/st_colormod.py b/lib/st_colormod.py index 1018d51..241145a 100644 --- a/lib/st_colormod.py +++ b/lib/st_colormod.py @@ -1,9 +1,13 @@ """Color-mod.""" import re -from mdpopups.coloraide import Color as ColorCSS +from mdpopups.coloraide import Color as Base from mdpopups.coloraide import ColorMatch -from mdpopups.coloraide.spaces import _parse +from mdpopups.coloraide.css import parse, serialize from mdpopups.coloraide import util +from mdpopups.coloraide import algebra as alg +from mdpopups.coloraide.spaces.hwb.css import HWB as HWBORIG +from collections.abc import Mapping +from itertools import zip_longest as zipl import functools import math @@ -21,34 +25,54 @@ \#(?:{hex}{{6}}(?:{hex}{{2}})?|{hex}{{3}}(?:{hex})?) | [\w][\w\d]* ) - """.format(**_parse.COLOR_PARTS) + """.format(**parse.COLOR_PARTS) ), "functions": re.compile(r'(?i)[\w][\w\d]*\('), - "separators": re.compile(r'(?:{comma}|{space}|{slash})'.format(**_parse.COLOR_PARTS)) + "separators": re.compile(r'(?:{comma}|{space}|{slash})'.format(**parse.COLOR_PARTS)) } RE_ADJUSTERS = { "alpha": re.compile( - r'(?i)\s+a(?:lpha)?\(\s*(?:(\+\s+|\-\s+)?({percent}|{float})|(\*)?\s*({percent}|{float}))\s*\)'.format( - **_parse.COLOR_PARTS + r""" + (?xi) + \s+a(?:lpha)?\(\s* + (?:(\+\s+|\-\s+)?({strict_percent}|{strict_float})|(\*)?\s*({strict_percent}|{strict_float})) + \s*\) + """.format( + **parse.COLOR_PARTS ) ), "saturation": re.compile( - r'(?i)\s+s(?:aturation)?\((\+\s|\-\s|\*)?\s*({percent})\s*\)'.format(**_parse.COLOR_PARTS) + r'(?i)\s+s(?:aturation)?\((\+\s|\-\s|\*)?\s*({strict_percent})\s*\)'.format(**parse.COLOR_PARTS) + ), + "lightness": re.compile( + r'(?i)\s+l(?:ightness)?\((\+\s|\-\s|\*)?\s*({strict_percent})\s*\)'.format(**parse.COLOR_PARTS) ), - "lightness": re.compile(r'(?i)\s+l(?:ightness)?\((\+\s|\-\s|\*)?\s*({percent})\s*\)'.format(**_parse.COLOR_PARTS)), "min-contrast_start": re.compile(r'(?i)\s+min-contrast\(\s*'), "blend_start": re.compile(r'(?i)\s+blenda?\(\s*'), "end": re.compile(r'(?i)\s*\)') } -RE_HUE = re.compile(r'(?i){angle}'.format(**_parse.COLOR_PARTS)) +RE_HUE = re.compile(r'(?i){angle}'.format(**parse.COLOR_PARTS)) RE_COLOR_START = re.compile(r'(?i)color\(\s*') -RE_BLEND_END = re.compile(r'(?i)\s+({percent})(?:\s+(rgb|hsl|hwb))?\s*\)'.format(**_parse.COLOR_PARTS)) +RE_BLEND_END = re.compile(r'(?i)\s+({strict_percent})(?:\s+(rgb|hsl|hwb))?\s*\)'.format(**parse.COLOR_PARTS)) RE_BRACKETS = re.compile(r'(?:(\()|(\))|[^()]+)') -RE_MIN_CONTRAST_END = re.compile(r'(?i)\s+({float})\s*\)'.format(**_parse.COLOR_PARTS)) +RE_MIN_CONTRAST_END = re.compile(r'(?i)\s+({strict_float})\s*\)'.format(**parse.COLOR_PARTS)) RE_VARS = re.compile(r'(?i)(?:(?<=^)|(?<=[\s\t\(,/]))(var\(\s*([-\w][-\w\d]*)\s*\))(?!\()(?=[\s\t\),/]|$)') +HWB_MATCH = re.compile( + r"""(?xi) + \b(hwb)\(\s* + (?: + # Space separated format + {angle}{space}{percent}{space}{percent}(?:{slash}(?:{percent}|{float}))? | + # comma separated format + {angle}{comma}{percent}{comma}{percent}(?:{comma}(?:{percent}|{float}))? + ) + \s*\) + """.format(**parse.COLOR_PARTS) +) + def bracket_match(match, string, start, fullmatch): """ @@ -153,6 +177,43 @@ def handle_vars(string, variables, parents=None): return RE_VARS.sub(functools.partial(_var_replace, var=temp_vars, parents=parent_vars), string) +class HWB(HWBORIG): + """HWB class that allows commas.""" + + @classmethod + def match(cls, string, start=0, fullmatch=True): + """Match a CSS color string.""" + + m = HWB_MATCH.match(string, start) + if m is not None and (not fullmatch or m.end(0) == len(string)): + return parse.parse_channels(string[m.end(1) + 1:m.end(0) - 1], cls.BOUNDS), m.end(0) + return None + + @classmethod + def to_string( + cls, + parent, + *, + alpha=None, + precision=None, + fit=True, + none=False, + **kwargs + ) -> str: + """Convert to CSS.""" + + return serialize.serialize_css( + parent, + func='hwb', + alpha=alpha, + precision=precision, + fit=fit, + none=none, + color=kwargs.get('color', False), + legacy=kwargs.get('comma', False) + ) + + class ColorMod: """Color utilities.""" @@ -211,7 +272,7 @@ def _adjust(self, string, start=0): start = m.end(0) m = RE_HUE.match(string, start) if m: - hue = _parse.norm_angle(m.group(0)) + hue = parse.norm_angle(m.group(0)) color = Color("hsl", [hue, 1, 0.5]).convert("srgb") start = m.end(0) if color is None: @@ -235,7 +296,7 @@ def _adjust(self, string, start=0): if color is not None: self._color = color - self._color.fit(method="clip", in_place=True) + self._color.clone().clip() while not done: m = None @@ -263,7 +324,7 @@ def _adjust(self, string, start=0): else: break - self._color.fit(method="clip", in_place=True) + self._color.clone().clip() else: raise ValueError('Could not calculate base color') except Exception: @@ -285,7 +346,7 @@ def adjust_base(self, base, string): """Adjust base.""" self._color = base - pattern = "color({} {})".format(self._color.fit(method="clip").to_string(precision=-1), string) + pattern = "color({} {})".format(self._color.clone().clip().to_string(precision=-1), string) color, start = self._adjust(pattern) if color is not None: self._color.update(color) @@ -308,7 +369,7 @@ def process_alpha(self, m, hue): else: value = m.group(4) if value.endswith('%'): - value = float(value.strip('%')) * _parse.SCALE_PERCENT + value = float(value.strip('%')) * parse.SCALE_PERCENT else: value = float(value) op = "" @@ -323,7 +384,7 @@ def process_hwb_hsl_channels(self, name, m, hue): """Process HWB and HSL channels (except hue).""" value = m.group(2) - value = float(value.strip('%')) + value = float(value.strip('%')) * parse.SCALE_PERCENT op = m.group(1).strip() if m.group(1) else "" getattr(self, name)(value, op=op, hue=hue) if not self._color.is_nan("hsl.hue"): @@ -350,7 +411,7 @@ def process_blend(self, m, string, hue): raise ValueError("Could not find a valid color for 'blend'") m = RE_BLEND_END.match(string, start) if m: - value = float(m.group(1).strip('%')) * _parse.SCALE_PERCENT + value = float(m.group(1).strip('%')) * parse.SCALE_PERCENT space = "srgb" if m.group(2): space = m.group(2).lower() @@ -360,7 +421,7 @@ def process_blend(self, m, string, hue): else: raise ValueError("Found unterminated or invalid 'blend('") - value = util.clamp(value, 0.0, 1.0) + value = alg.clamp(value, 0.0, 1.0) self.blend(color2, 1.0 - value, alpha, space=space) if not self._color.is_nan("hsl.hue"): hue = self._color.get("hsl.hue") @@ -391,7 +452,7 @@ def process_min_contrast(self, m, string, hue): this = self._color.convert("srgb") color2 = color2.convert("srgb") - color2.alpha = 1.0 + color2[-1] = 1.0 self.min_contrast(this, color2, value) self._color.update(this) @@ -423,13 +484,13 @@ def min_contrast(self, color1, color2, target): if is_dark: primary = "whiteness" secondary = "blackness" - min_mix = orig.whiteness - max_mix = 100.0 + min_mix = orig.whiteness * 100 + max_mix = 100 else: primary = "blackness" secondary = "whiteness" - min_mix = orig.blackness - max_mix = 100.0 + min_mix = orig.blackness * 100 + max_mix = 100 orig_ratio = ratio last_ratio = 0 last_mix = 0 @@ -440,10 +501,10 @@ def min_contrast(self, color1, color2, target): mid_mix = round((max_mix + min_mix) / 2, 1) mid_other = ( orig.get(secondary) - - ((mid_mix - orig.get(primary)) / (100.0 - orig.get(primary))) * orig.get(secondary) + ((mid_mix - orig.get(primary) * 100) / (1 - orig.get(primary) * 100)) * orig.get(secondary) * 100 ) - temp.set(primary, mid_mix) - temp.set(secondary, mid_other) + temp.set(primary, mid_mix / 100) + temp.set(secondary, mid_other / 100) ratio = temp.contrast(color2) if ratio < target: @@ -464,13 +525,22 @@ def min_contrast(self, color1, color2, target): return # Use the best, last values - final = orig.new("hwb", [orig.hue, last_mix, last_other] if is_dark else [orig.hue, last_other, last_mix]) + coords = [ + orig['hue'], + last_mix / 100, + last_other / 100 + ] if is_dark else [ + orig['hue'], + last_other / 100, + last_mix / 100 + ] + final = orig.new("hwb", coords) final = final.convert('srgb') # If we are lightening the color, then we'd like to round up to ensure we are over the luminance threshold # as sRGB will clip off decimals. If we are darkening, then we want to just floor the values as the algorithm # leans more to the light side. - rnd = util.round_half_up if is_dark else math.floor - final = Color("srgb", [rnd(c * 255.0) / 255.0 for c in final.coords()], final.alpha) + rnd = alg.round_half_up if is_dark else math.floor + final = Color("srgb", [rnd(c * 255.0) / 255.0 for c in final[:-1]], final[-1]) color1.update(final) def blend(self, color, percent, alpha=False, space="srgb"): @@ -486,9 +556,9 @@ def blend(self, color, percent, alpha=False, space="srgb"): if color.space() != space: color.convert(space, in_place=True) - new_color = this.mix(color, percent, space=space) + new_color = this.mix(color, percent, space=space, premultiplied=False) if not alpha: - new_color.alpha = color.alpha + new_color[-1] = color[-1] self._color.update(new_color) def alpha(self, value, op=""): @@ -496,7 +566,7 @@ def alpha(self, value, op=""): this = self._color op = self.OP_MAP.get(op, self._op_null) - this.alpha = op(this.alpha, value) + this[-1] = op(this[-1], value) self._color.update(this) def lightness(self, value, op="", hue=None): @@ -504,9 +574,9 @@ def lightness(self, value, op="", hue=None): this = self._color.convert("hsl") if self._color.space() != "hsl" else self._color if this.is_nan('hue') and hue is not None: - this.hue = hue + this['hue'] = hue op = self.OP_MAP.get(op, self._op_null) - this.lightness = op(this.lightness, value) + this['lightness'] = op(this['lightness'], value) self._color.update(this) def saturation(self, value, op="", hue=None): @@ -514,45 +584,76 @@ def saturation(self, value, op="", hue=None): this = self._color.convert("hsl") if self._color.space() != "hsl" else self._color if this.is_nan("hue") and hue is not None: - this.hue = hue + this['hue'] = hue op = self.OP_MAP.get(op, self._op_null) - this.saturation = op(this.saturation, value) + this['saturation'] = op(this['saturation'], value) self._color.update(this) -class Color(ColorCSS): +class Color(Base): """Color modify class.""" - def __init__(self, color, data=None, alpha=util.DEF_ALPHA, *, filters=None, variables=None, **kwargs): + def __init__(self, color, data=None, alpha=util.DEF_ALPHA, *, variables=None, **kwargs): """Initialize.""" - super().__init__(color, data, alpha, filters=None, variables=variables, **kwargs) + super().__init__(color, data, alpha, variables=variables, **kwargs) - def _parse(self, color, data=None, alpha=util.DEF_ALPHA, filters=None, variables=None, **kwargs): + @classmethod + def _parse( + cls, + color, + data=None, + alpha=util.DEF_ALPHA, + *, + variables=None, + **kwargs + ): """Parse the color.""" obj = None - if data is not None: - filters = set(filters) if filters is not None else set() - for space, space_class in self.CS_MAP.items(): - s = color.lower() - if space == s and (not filters or s in filters): - obj = space_class(data[:space_class.NUM_COLOR_CHANNELS], alpha) - return obj - elif isinstance(color, ColorCSS): - if not filters or color.space() in filters: - obj = self.CS_MAP[color.space()](color._space) + if isinstance(color, str): + + # Parse a color space name and coordinates + if data is not None: + s = color + space_class = cls.CS_MAP.get(s) + if not space_class: + raise ValueError("'{}' is not a registered color space") + num_channels = len(space_class.CHANNELS) + if len(data) < num_channels: + data = list(data) + [alg.NaN] * (num_channels - len(data)) + coords = [alg.clamp(float(v), *c.limit) for c, v in zipl(space_class.CHANNELS, data)] + coords.append(alg.clamp(float(alpha), *space_class.channels[-1].limit)) + obj = space_class, coords + # Parse a CSS string + else: + m = cls._match(color, fullmatch=True, variables=variables) + if m is None: + raise ValueError("'{}' is not a valid color".format(color)) + coords = [alg.clamp(float(v), *c.limit) for c, v in zipl(m[0].CHANNELS, m[1])] + coords.append(alg.clamp(float(m[2]), *m[0].channels[-1].limit)) + obj = m[0], coords + elif isinstance(color, Base): + # Handle a color instance + space_class = cls.CS_MAP.get(color.space()) + if not space_class: + raise ValueError("'{}' is not a registered color space") + obj = space_class, color[:] + elif isinstance(color, Mapping): + # Handle a color dictionary + space = color['space'] + coords = color['coords'] + alpha = color.get('alpha', 1.0) + obj = cls._parse(space, coords, alpha) else: - m = self._match(color, fullmatch=True, filters=filters, variables=variables) - if m is None: - raise ValueError("'{}' is not a valid color".format(color)) - obj = m.color + raise TypeError("'{}' is an unrecognized type".format(type(color))) + if obj is None: raise ValueError("Could not process the provided color") return obj @classmethod - def _match(cls, string, start=0, fullmatch=False, filters=None, variables=None): + def _match(cls, string, start=0, fullmatch=False, variables=None): """ Match a color in a buffer and return a color object. @@ -583,50 +684,48 @@ def _match(cls, string, start=0, fullmatch=False, filters=None, variables=None): string = handle_vars(string, variables) obj, match_end = ColorMod(fullmatch).adjust(string, start) if obj is not None: - return ColorMatch(obj._space, start, end if end is not None else match_end) + return obj._space, obj[:-1], obj[-1], start, (end if end is not None else match_end) else: - filters = set(filters) if filters is not None else set() - obj = None - for space, space_class in cls.CS_MAP.items(): - if filters and space not in filters: - continue - value, match_end = space_class.match(string, start, fullmatch) - if value is not None: - color = space_class(*value) - obj = ColorMatch(color, start, match_end) - if obj is not None and end: - obj.end = end - return obj + return super()._match(string, start, fullmatch) + return None @classmethod - def match(cls, string, start=0, fullmatch=False, *, filters=None, variables=None): + def match( + cls, + string: str, + start: int = 0, + fullmatch: bool = False + ): """Match color.""" - obj = cls._match(string, start, fullmatch, filters=filters, variables=variables) - if obj is not None: - obj.color = cls(obj.color.space(), obj.color.coords(), obj.color.alpha) - return obj + m = cls._match(string, start, fullmatch, variables=None) + if m is not None: + return ColorMatch(cls(m[0].NAME, m[1], m[2]), m[3], m[4]) + return None - def new(self, color, data=None, alpha=util.DEF_ALPHA, *, filters=None, variables=None, **kwargs): + def new(self, color, data=None, alpha=util.DEF_ALPHA, *, variables=None, **kwargs): """Create new color object.""" - return type(self)(color, data, alpha, filters=filters, variables=variables, **kwargs) + return type(self)(color, data, alpha, variables=variables, **kwargs) - def update(self, color, data=None, alpha=util.DEF_ALPHA, *, filters=None, variables=None, **kwargs): + def update(self, color, data=None, alpha=util.DEF_ALPHA, *, variables=None, **kwargs): """Update the existing color space with the provided color.""" - clone = self.clone() - obj = self._parse(color, data, alpha, filters=filters, variables=variables, **kwargs) - clone._attach(obj) - - if clone.space() != self.space(): - clone.convert(self.space(), in_place=True) - - self._attach(clone._space) + space = self.space() + self._space, self._coords = self._parse( + color, data=data, alpha=alpha, variables=variables, **kwargs + ) + if self._space.NAME != space: + self.convert(space, in_place=True) return self - def mutate(self, color, data=None, alpha=util.DEF_ALPHA, *, filters=None, variables=None, **kwargs): + def mutate(self, color, data=None, alpha=util.DEF_ALPHA, *, variables=None, **kwargs): """Mutate the current color to a new color.""" - self._attach(self._parse(color, data, alpha, filters=filters, variables=variables, **kwargs)) + self._space, self._coords = self._parse( + color, data=data, alpha=alpha, variables=variables, **kwargs + ) return self + + +Color.register(HWB(), overwrite=True) diff --git a/lib/tmtheme.py b/lib/tmtheme.py index e5cc423..31a538c 100644 --- a/lib/tmtheme.py +++ b/lib/tmtheme.py @@ -1,12 +1,21 @@ """Custom color that looks for colors of format `#RRGGBBAA` as `#AARRGGBB`.""" from mdpopups.coloraide import Color -from mdpopups.coloraide.spaces.srgb.css import SRGB -from mdpopups.coloraide.spaces import _parse -from mdpopups.coloraide import util -import copy +from mdpopups.coloraide.spaces.srgb.css import sRGB +from mdpopups.coloraide.css import parse, serialize import re -RE_COMPRESS = re.compile(r'(?i)^#({hex})\1({hex})\2({hex})\3(?:({hex})\4)?$'.format(**_parse.COLOR_PARTS)) +RE_COMPRESS = re.compile(r'(?i)^#({hex})\1({hex})\2({hex})\3(?:({hex})\4)?$'.format(**parse.COLOR_PARTS)) + +MATCH = re.compile( + r"""(?xi) + (?: + # Hex syntax + \#(?:{hex}{{6}}(?:{hex}{{2}})?|{hex}{{3}}(?:{hex})?)\b | + # Names + \b(? Date: Sat, 20 Aug 2022 22:09:25 -0600 Subject: [PATCH 2/7] Use mdpopupss st_colormod and fix colorblind filter corner cases --- lib/color_scheme_matcher.py | 2 +- lib/color_scheme_tweaker.py | 8 +- lib/st_colormod.py | 731 ------------------------------------ 3 files changed, 5 insertions(+), 736 deletions(-) delete mode 100644 lib/st_colormod.py diff --git a/lib/color_scheme_matcher.py b/lib/color_scheme_matcher.py index 408ab92..aba5256 100644 --- a/lib/color_scheme_matcher.py +++ b/lib/color_scheme_matcher.py @@ -28,8 +28,8 @@ import codecs import re from .file_strip.json import sanitize_json -from .st_colormod import Color from .tmtheme import ColorSRGBX11 +from mdpopups.st_colormod import Color from os import path from collections import namedtuple from plistlib import readPlistFromBytes diff --git a/lib/color_scheme_tweaker.py b/lib/color_scheme_tweaker.py index d589393..55cb0bd 100644 --- a/lib/color_scheme_tweaker.py +++ b/lib/color_scheme_tweaker.py @@ -6,7 +6,7 @@ """ from __future__ import absolute_import import sublime -from .st_colormod import Color +from mdpopups.st_colormod import Color from mdpopups.coloraide import algebra as alg import re @@ -376,7 +376,7 @@ def get_filters(self): filters = [] for f in self.filters: - if f[0] in ("invert", "grayscale", "sepia"): + if f[0] in ("invert", "grayscale", "sepia", "tritan", "protan", "deutan"): filters.append(f[0]) elif f[0] in ("hue", "colorize"): filters.append(f[0] + "(%d)" % int(f[1])) @@ -434,7 +434,7 @@ def _filter_colors(self, *args, **kwargs): name = f[0] value = f[1] context = f[2] - if name in ("grayscale", "sepia", "invert"): + if name in ("grayscale", "sepia", "invert", "tritan", "protan", "deutan"): if context != "bg": self._apply_filter(rgba_fg, name) if context != "fg": @@ -534,7 +534,7 @@ def get_filters(self): filters = [] for f in self.filters: - if f[0] in ("invert", "grayscale", "sepia"): + if f[0] in ("invert", "grayscale", "sepia", "tritan", "protan", "deutan"): filters.append(f[0]) elif f[0] in ("hue", "colorize"): filters.append(f[0] + "(%d)" % int(f[1])) diff --git a/lib/st_colormod.py b/lib/st_colormod.py deleted file mode 100644 index 241145a..0000000 --- a/lib/st_colormod.py +++ /dev/null @@ -1,731 +0,0 @@ -"""Color-mod.""" -import re -from mdpopups.coloraide import Color as Base -from mdpopups.coloraide import ColorMatch -from mdpopups.coloraide.css import parse, serialize -from mdpopups.coloraide import util -from mdpopups.coloraide import algebra as alg -from mdpopups.coloraide.spaces.hwb.css import HWB as HWBORIG -from collections.abc import Mapping -from itertools import zip_longest as zipl -import functools -import math - -WHITE = [1.0] * 3 -BLACK = [0.0] * 3 - -TOKENS = { - "units": re.compile( - r"""(?xi) - # Some number of units separated by valid separators - (?: - {float} | - {angle} | - {percent} | - \#(?:{hex}{{6}}(?:{hex}{{2}})?|{hex}{{3}}(?:{hex})?) | - [\w][\w\d]* - ) - """.format(**parse.COLOR_PARTS) - ), - "functions": re.compile(r'(?i)[\w][\w\d]*\('), - "separators": re.compile(r'(?:{comma}|{space}|{slash})'.format(**parse.COLOR_PARTS)) -} - -RE_ADJUSTERS = { - "alpha": re.compile( - r""" - (?xi) - \s+a(?:lpha)?\(\s* - (?:(\+\s+|\-\s+)?({strict_percent}|{strict_float})|(\*)?\s*({strict_percent}|{strict_float})) - \s*\) - """.format( - **parse.COLOR_PARTS - ) - ), - "saturation": re.compile( - r'(?i)\s+s(?:aturation)?\((\+\s|\-\s|\*)?\s*({strict_percent})\s*\)'.format(**parse.COLOR_PARTS) - ), - "lightness": re.compile( - r'(?i)\s+l(?:ightness)?\((\+\s|\-\s|\*)?\s*({strict_percent})\s*\)'.format(**parse.COLOR_PARTS) - ), - "min-contrast_start": re.compile(r'(?i)\s+min-contrast\(\s*'), - "blend_start": re.compile(r'(?i)\s+blenda?\(\s*'), - "end": re.compile(r'(?i)\s*\)') -} - -RE_HUE = re.compile(r'(?i){angle}'.format(**parse.COLOR_PARTS)) -RE_COLOR_START = re.compile(r'(?i)color\(\s*') -RE_BLEND_END = re.compile(r'(?i)\s+({strict_percent})(?:\s+(rgb|hsl|hwb))?\s*\)'.format(**parse.COLOR_PARTS)) -RE_BRACKETS = re.compile(r'(?:(\()|(\))|[^()]+)') -RE_MIN_CONTRAST_END = re.compile(r'(?i)\s+({strict_float})\s*\)'.format(**parse.COLOR_PARTS)) -RE_VARS = re.compile(r'(?i)(?:(?<=^)|(?<=[\s\t\(,/]))(var\(\s*([-\w][-\w\d]*)\s*\))(?!\()(?=[\s\t\),/]|$)') - -HWB_MATCH = re.compile( - r"""(?xi) - \b(hwb)\(\s* - (?: - # Space separated format - {angle}{space}{percent}{space}{percent}(?:{slash}(?:{percent}|{float}))? | - # comma separated format - {angle}{comma}{percent}{comma}{percent}(?:{comma}(?:{percent}|{float}))? - ) - \s*\) - """.format(**parse.COLOR_PARTS) -) - - -def bracket_match(match, string, start, fullmatch): - """ - Make sure we can acquire a complete `func()` before we replace variables. - - We mainly do this so we can judge the real size before we alter the string with variables. - """ - - end = None - if match.match(string, start): - brackets = 1 - for m in RE_BRACKETS.finditer(string, start + 6): - if m.group(2): - brackets -= 1 - elif m.group(1): - brackets += 1 - - if brackets == 0: - end = m.end(2) - break - return end if (not fullmatch or end == len(string)) else None - - -def validate_vars(var, good_vars): - """ - Validate variables. - - We will blindly replace values, but if we are fairly confident they follow - the pattern of a valid, complete unit, if you replace them in a bad place, - it will break the color (as it should) and if not, it is likely to parse fine, - unless it breaks the syntax of the color being evaluated. - """ - - for k, v in var.items(): - v = v.strip() - start = 0 - need_sep = False - length = len(v) - while True: - if start == length: - good_vars[k] = v - break - try: - # Each item should be separated by some valid separator - if need_sep: - m = TOKENS["separators"].match(v, start) - if m: - start = m.end(0) - need_sep = False - continue - else: - break - - # Validate things like `rgb()`, `contrast()` etc. - m = TOKENS["functions"].match(v, start) - if m: - end = None - brackets = 1 - for m in RE_BRACKETS.finditer(v, start + 6): - if m.group(2): - brackets -= 1 - elif m.group(1): - brackets += 1 - - if brackets == 0: - end = m.end(0) - break - if end is None: - break - start = end - need_sep = True - continue - - # Validate that units such as percents, floats, hex colors, etc. - m = TOKENS["units"].match(v, start) - if m: - start = m.end(0) - need_sep = True - continue - break - except Exception: - break - - -def _var_replace(m, var=None, parents=None): - """Replace variables but try to prevent infinite recursion.""" - - name = m.group(2) - replacement = var.get(m.group(2)) - string = replacement if replacement and name not in parents is not None else "" - parents.add(name) - return RE_VARS.sub(functools.partial(_var_replace, var=var, parents=parents), string) - - -def handle_vars(string, variables, parents=None): - """Handle CSS variables.""" - - temp_vars = {} - validate_vars(variables, temp_vars) - parent_vars = set() if parents is None else parents - - return RE_VARS.sub(functools.partial(_var_replace, var=temp_vars, parents=parent_vars), string) - - -class HWB(HWBORIG): - """HWB class that allows commas.""" - - @classmethod - def match(cls, string, start=0, fullmatch=True): - """Match a CSS color string.""" - - m = HWB_MATCH.match(string, start) - if m is not None and (not fullmatch or m.end(0) == len(string)): - return parse.parse_channels(string[m.end(1) + 1:m.end(0) - 1], cls.BOUNDS), m.end(0) - return None - - @classmethod - def to_string( - cls, - parent, - *, - alpha=None, - precision=None, - fit=True, - none=False, - **kwargs - ) -> str: - """Convert to CSS.""" - - return serialize.serialize_css( - parent, - func='hwb', - alpha=alpha, - precision=precision, - fit=fit, - none=none, - color=kwargs.get('color', False), - legacy=kwargs.get('comma', False) - ) - - -class ColorMod: - """Color utilities.""" - - def __init__(self, fullmatch=True): - """Associate with parent.""" - - self.OP_MAP = { - "": self._op_null, - "*": self._op_mult, - "+": self._op_add, - "-": self._op_sub - } - - self.adjusting = False - self._color = None - self.fullmatch = fullmatch - - @staticmethod - def _op_mult(a, b): - """Multiply.""" - - return a * b - - @staticmethod - def _op_add(a, b): - """Multiply.""" - - return a + b - - @staticmethod - def _op_sub(a, b): - """Multiply.""" - - return a - b - - @staticmethod - def _op_null(a, b): - """Multiply.""" - - return b - - def _adjust(self, string, start=0): - """Adjust.""" - - nested = self.adjusting - self.adjusting = True - - color = None - done = False - old_parent = self._color - hue = None - - try: - m = RE_COLOR_START.match(string, start) - if m: - start = m.end(0) - m = RE_HUE.match(string, start) - if m: - hue = parse.norm_angle(m.group(0)) - color = Color("hsl", [hue, 1, 0.5]).convert("srgb") - start = m.end(0) - if color is None: - m = RE_COLOR_START.match(string, start) - if m: - color2, start = self._adjust(string, start=start) - if color2 is None: - raise ValueError("Found unterminated or invalid 'color('") - color = color2.convert("srgb") - if not color.is_nan("hsl.hue"): - hue = color.get("hsl.hue") - if color is None: - obj = Color.match(string, start=start, fullmatch=False) - if obj is not None: - color = obj.color - if color.space != "srgb": - color = color.convert("srgb") - if not color.is_nan("hsl.hue"): - hue = color.get("hsl.hue") - start = obj.end - - if color is not None: - self._color = color - self._color.clone().clip() - - while not done: - m = None - name = None - for key, pattern in RE_ADJUSTERS.items(): - name = key - m = pattern.match(string, start) - if m: - start = m.end(0) - break - if m is None: - break - - if name == "alpha": - start, hue = self.process_alpha(m, hue) - elif name in ("saturation", "lightness"): - start, hue = self.process_hwb_hsl_channels(name, m, hue) - elif name == "min-contrast_start": - start, hue = self.process_min_contrast(m, string, hue) - elif name == "blend_start": - start, hue = self.process_blend(m, string, hue) - elif name == "end": - done = True - start = m.end(0) - else: - break - - self._color.clone().clip() - else: - raise ValueError('Could not calculate base color') - except Exception: - pass - - if not done or (self.fullmatch and start != len(string)): - result = None - else: - result = self._color - - self._color = old_parent - - if not nested: - self.adjusting = False - - return result, start - - def adjust_base(self, base, string): - """Adjust base.""" - - self._color = base - pattern = "color({} {})".format(self._color.clone().clip().to_string(precision=-1), string) - color, start = self._adjust(pattern) - if color is not None: - self._color.update(color) - else: - raise ValueError( - "'{}' doesn't appear to be a valid and/or supported CSS color or color-mod instruction".format(string) - ) - - def adjust(self, string, start=0): - """Adjust.""" - - color, end = self._adjust(string, start=start) - return color, end - - def process_alpha(self, m, hue): - """Process alpha.""" - - if m.group(2): - value = m.group(2) - else: - value = m.group(4) - if value.endswith('%'): - value = float(value.strip('%')) * parse.SCALE_PERCENT - else: - value = float(value) - op = "" - if m.group(1): - op = m.group(1).strip() - elif m.group(3): - op = m.group(3).strip() - self.alpha(value, op=op) - return m.end(0), hue - - def process_hwb_hsl_channels(self, name, m, hue): - """Process HWB and HSL channels (except hue).""" - - value = m.group(2) - value = float(value.strip('%')) * parse.SCALE_PERCENT - op = m.group(1).strip() if m.group(1) else "" - getattr(self, name)(value, op=op, hue=hue) - if not self._color.is_nan("hsl.hue"): - hue = self._color.get("hsl.hue") - return m.end(0), hue - - def process_blend(self, m, string, hue): - """Process blend.""" - - start = m.end(0) - alpha = m.group(0).strip().startswith('blenda') - m = RE_COLOR_START.match(string, start) - if m: - color2, start = self._adjust(string, start=start) - if color2 is None: - raise ValueError("Found unterminated or invalid 'color('") - else: - color2 = None - obj = Color.match(string, start=start, fullmatch=False) - if obj is not None: - color2 = obj.color - start = obj.end - if color2 is None: - raise ValueError("Could not find a valid color for 'blend'") - m = RE_BLEND_END.match(string, start) - if m: - value = float(m.group(1).strip('%')) * parse.SCALE_PERCENT - space = "srgb" - if m.group(2): - space = m.group(2).lower() - if space == "rgb": - space = "srgb" - start = m.end(0) - else: - raise ValueError("Found unterminated or invalid 'blend('") - - value = alg.clamp(value, 0.0, 1.0) - self.blend(color2, 1.0 - value, alpha, space=space) - if not self._color.is_nan("hsl.hue"): - hue = self._color.get("hsl.hue") - return start, hue - - def process_min_contrast(self, m, string, hue): - """Process blend.""" - - # Gather the min-contrast parameters - start = m.end(0) - m = RE_COLOR_START.match(string, start) - if m: - color2, start = self._adjust(string, start=start) - if color2 is None: - raise ValueError("Found unterminated or invalid 'color('") - else: - color2 = None - obj = Color.match(string, start=start, fullmatch=False) - if obj is not None: - color2 = obj.color - start = obj.end - m = RE_MIN_CONTRAST_END.match(string, start) - if m: - value = float(m.group(1)) - start = m.end(0) - else: - raise ValueError("Found unterminated or invalid 'min-contrast('") - - this = self._color.convert("srgb") - color2 = color2.convert("srgb") - color2[-1] = 1.0 - - self.min_contrast(this, color2, value) - self._color.update(this) - if not self._color.is_nan("hsl.hue"): - hue = self._color.get("hsl.hue") - return start, hue - - def min_contrast(self, color1, color2, target): - """ - Get the color with the best contrast. - - This mimics Sublime Text's custom `min-contrast` for `color-mod` (now defunct - the CSS version). - It ensure the color has at least the specified contrast ratio. - - While there seems to be slight differences with ours and Sublime, maybe due to some rounding, - this essentially fulfills the intention of their min-contrast. - """ - - ratio = color1.contrast(color2) - - # Already meet the minimum contrast or the request is impossible - if ratio > target or target < 1: - return - - lum2 = color2.luminance() - - is_dark = lum2 < 0.5 - orig = color1.convert("hwb") - if is_dark: - primary = "whiteness" - secondary = "blackness" - min_mix = orig.whiteness * 100 - max_mix = 100 - else: - primary = "blackness" - secondary = "whiteness" - min_mix = orig.blackness * 100 - max_mix = 100 - orig_ratio = ratio - last_ratio = 0 - last_mix = 0 - last_other = 0 - - temp = orig.clone() - while abs(min_mix - max_mix) > 0.2: - mid_mix = round((max_mix + min_mix) / 2, 1) - mid_other = ( - orig.get(secondary) - - ((mid_mix - orig.get(primary) * 100) / (1 - orig.get(primary) * 100)) * orig.get(secondary) * 100 - ) - temp.set(primary, mid_mix / 100) - temp.set(secondary, mid_other / 100) - ratio = temp.contrast(color2) - - if ratio < target: - min_mix = mid_mix - else: - max_mix = mid_mix - - if ( - (last_ratio < target and ratio > last_ratio) or - (ratio > target and ratio < last_ratio) - ): - last_ratio = ratio - last_mix = mid_mix - last_other = mid_other - - # Can't find a better color - if last_ratio < ratio and orig_ratio > last_ratio: - return - - # Use the best, last values - coords = [ - orig['hue'], - last_mix / 100, - last_other / 100 - ] if is_dark else [ - orig['hue'], - last_other / 100, - last_mix / 100 - ] - final = orig.new("hwb", coords) - final = final.convert('srgb') - # If we are lightening the color, then we'd like to round up to ensure we are over the luminance threshold - # as sRGB will clip off decimals. If we are darkening, then we want to just floor the values as the algorithm - # leans more to the light side. - rnd = alg.round_half_up if is_dark else math.floor - final = Color("srgb", [rnd(c * 255.0) / 255.0 for c in final[:-1]], final[-1]) - color1.update(final) - - def blend(self, color, percent, alpha=False, space="srgb"): - """Blend color.""" - - space = space.lower() - if space not in ("srgb", "hsl", "hwb"): - raise ValueError( - "ColorMod's does not support the '{}' colorspace, only 'srgb', 'hsl', and 'hwb' are supported" - ).format(space) - this = self._color.convert(space) if self._color.space() != space else self._color - - if color.space() != space: - color.convert(space, in_place=True) - - new_color = this.mix(color, percent, space=space, premultiplied=False) - if not alpha: - new_color[-1] = color[-1] - self._color.update(new_color) - - def alpha(self, value, op=""): - """Alpha.""" - - this = self._color - op = self.OP_MAP.get(op, self._op_null) - this[-1] = op(this[-1], value) - self._color.update(this) - - def lightness(self, value, op="", hue=None): - """Lightness.""" - - this = self._color.convert("hsl") if self._color.space() != "hsl" else self._color - if this.is_nan('hue') and hue is not None: - this['hue'] = hue - op = self.OP_MAP.get(op, self._op_null) - this['lightness'] = op(this['lightness'], value) - self._color.update(this) - - def saturation(self, value, op="", hue=None): - """Saturation.""" - - this = self._color.convert("hsl") if self._color.space() != "hsl" else self._color - if this.is_nan("hue") and hue is not None: - this['hue'] = hue - op = self.OP_MAP.get(op, self._op_null) - this['saturation'] = op(this['saturation'], value) - self._color.update(this) - - -class Color(Base): - """Color modify class.""" - - def __init__(self, color, data=None, alpha=util.DEF_ALPHA, *, variables=None, **kwargs): - """Initialize.""" - - super().__init__(color, data, alpha, variables=variables, **kwargs) - - @classmethod - def _parse( - cls, - color, - data=None, - alpha=util.DEF_ALPHA, - *, - variables=None, - **kwargs - ): - """Parse the color.""" - - obj = None - if isinstance(color, str): - - # Parse a color space name and coordinates - if data is not None: - s = color - space_class = cls.CS_MAP.get(s) - if not space_class: - raise ValueError("'{}' is not a registered color space") - num_channels = len(space_class.CHANNELS) - if len(data) < num_channels: - data = list(data) + [alg.NaN] * (num_channels - len(data)) - coords = [alg.clamp(float(v), *c.limit) for c, v in zipl(space_class.CHANNELS, data)] - coords.append(alg.clamp(float(alpha), *space_class.channels[-1].limit)) - obj = space_class, coords - # Parse a CSS string - else: - m = cls._match(color, fullmatch=True, variables=variables) - if m is None: - raise ValueError("'{}' is not a valid color".format(color)) - coords = [alg.clamp(float(v), *c.limit) for c, v in zipl(m[0].CHANNELS, m[1])] - coords.append(alg.clamp(float(m[2]), *m[0].channels[-1].limit)) - obj = m[0], coords - elif isinstance(color, Base): - # Handle a color instance - space_class = cls.CS_MAP.get(color.space()) - if not space_class: - raise ValueError("'{}' is not a registered color space") - obj = space_class, color[:] - elif isinstance(color, Mapping): - # Handle a color dictionary - space = color['space'] - coords = color['coords'] - alpha = color.get('alpha', 1.0) - obj = cls._parse(space, coords, alpha) - else: - raise TypeError("'{}' is an unrecognized type".format(type(color))) - - if obj is None: - raise ValueError("Could not process the provided color") - return obj - - @classmethod - def _match(cls, string, start=0, fullmatch=False, variables=None): - """ - Match a color in a buffer and return a color object. - - This must return the color space, not the Color object. - """ - - # Handle variable - end = None - is_mod = False - if variables: - m = RE_VARS.match(string, start) - if m and (not fullmatch or len(string) == m.end(0)): - end = m.end(0) - start = 0 - string = string[start:end] - string = handle_vars(string, variables) - variables = None - - temp = bracket_match(RE_COLOR_START, string, start, fullmatch) - if end is None and temp: - end = temp - is_mod = True - elif end is not None and temp is not None: - is_mod = True - - if is_mod: - if variables: - string = handle_vars(string, variables) - obj, match_end = ColorMod(fullmatch).adjust(string, start) - if obj is not None: - return obj._space, obj[:-1], obj[-1], start, (end if end is not None else match_end) - else: - return super()._match(string, start, fullmatch) - return None - - @classmethod - def match( - cls, - string: str, - start: int = 0, - fullmatch: bool = False - ): - """Match color.""" - - m = cls._match(string, start, fullmatch, variables=None) - if m is not None: - return ColorMatch(cls(m[0].NAME, m[1], m[2]), m[3], m[4]) - return None - - def new(self, color, data=None, alpha=util.DEF_ALPHA, *, variables=None, **kwargs): - """Create new color object.""" - - return type(self)(color, data, alpha, variables=variables, **kwargs) - - def update(self, color, data=None, alpha=util.DEF_ALPHA, *, variables=None, **kwargs): - """Update the existing color space with the provided color.""" - - space = self.space() - self._space, self._coords = self._parse( - color, data=data, alpha=alpha, variables=variables, **kwargs - ) - if self._space.NAME != space: - self.convert(space, in_place=True) - return self - - def mutate(self, color, data=None, alpha=util.DEF_ALPHA, *, variables=None, **kwargs): - """Mutate the current color to a new color.""" - - self._space, self._coords = self._parse( - color, data=data, alpha=alpha, variables=variables, **kwargs - ) - return self - - -Color.register(HWB(), overwrite=True) From b15a679928adc2959e9e2572f895e1edb3fbaaf2 Mon Sep 17 00:00:00 2001 From: facelessuser Date: Sun, 28 Aug 2022 11:45:49 -0600 Subject: [PATCH 3/7] Update version and upgrade message --- CHANGES.md | 2 +- messages.json | 2 +- messages/recent.md | 10 ++++++++-- support.py | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ce756f6..c3d47ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # ExportHtml -## 2.17.5 +## 2.18.0 - **NEW**: New colorblind filters. - **CHORE**: Updates to support newer dependencies. diff --git a/messages.json b/messages.json index e25593a..0980de2 100644 --- a/messages.json +++ b/messages.json @@ -1,4 +1,4 @@ { "install": "messages/install.md", - "2.17.0": "messages/recent.md" + "2.18.0": "messages/recent.md" } diff --git a/messages/recent.md b/messages/recent.md index 62d5b72..d3c9327 100644 --- a/messages/recent.md +++ b/messages/recent.md @@ -1,8 +1,14 @@ -# ExportHtml 2.17.0 +# ExportHtml 2.18.0 New release! Please see `Preferences->Package Settings->ExportHtml->Changelog` for more info about the release. -BBCode support has been dropped in 2.17.0+. +A restart of Sublime Text is most likely needed. + +## 2.18.0 + +- **NEW**: New colorblind filters. +- **CHORE**: Updates to support newer dependencies. +- **FIX**: Remove unnecessary dependencies. diff --git a/support.py b/support.py index 9c6d1b6..538365e 100644 --- a/support.py +++ b/support.py @@ -5,7 +5,7 @@ import webbrowser import re -__version__ = "2.17.5" +__version__ = "2.18.0" __pc_name__ = 'ExportHtml' CSS = ''' From 8e1fec3a3d32560117e5ee24e5681c66526ba9b5 Mon Sep 17 00:00:00 2001 From: facelessuser Date: Sat, 27 Sep 2025 07:30:06 -0600 Subject: [PATCH 4/7] Remove links and update copyright --- LICENSE.md | 2 +- mkdocs.yml | 33 ++++++++++++++++++++++++++++++--- readme.md | 6 ------ 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index fbc5bd4..b4eac88 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,7 +2,7 @@ ExportHtml is released under the MIT license. -Copyright (c) 2012 - 2023 Isaac Muse +Copyright (c) 2012 - 2025 Isaac Muse Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/mkdocs.yml b/mkdocs.yml index d672eb8..0fbc196 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,7 +4,7 @@ repo_url: https://github.com/facelessuser/ExportHtml edit_uri: tree/master/docs/src/markdown site_description: Sublime Text plugin that exports code to HTML or BBCODE for copying, printing, or saving. copyright: | - Copyright © 2012 - 2023 Isaac Muse + Copyright © 2012 - 2025 Isaac Muse docs_dir: docs/src/markdown theme: @@ -98,6 +98,35 @@ markdown_extensions: - example - quote - pymdownx.blocks.details: + types: + - name: details-new + class: new + - name: details-settings + class: settings + - name: details-note + class: note + - name: details-abstract + class: abstract + - name: details-info + class: info + - name: details-tip + class: tip + - name: details-success + class: success + - name: details-question + class: question + - name: details-warning + class: warning + - name: details-failure + class: failure + - name: details-danger + class: danger + - name: details-bug + class: bug + - name: details-example + class: example + - name: details-quote + class: quote - pymdownx.blocks.html: - pymdownx.blocks.definition: - pymdownx.blocks.tab: @@ -107,8 +136,6 @@ extra: social: - icon: fontawesome/brands/github link: https://github.com/facelessuser - - icon: fontawesome/brands/discord - link: https://discord.gg/TWs8Tgr plugins: - search diff --git a/readme.md b/readme.md index b83d276..416c56e 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,4 @@ [![Donate via PayPal][donate-image]][donate-link] -[![Discord][discord-image]][discord-link] -[![Build][github-ci-image]][github-ci-link] [![Package Control Downloads][pc-image]][pc-link] ![License][license-image] # ExportHtml @@ -40,10 +38,6 @@ https://facelessuser.github.io/ExportHtml ExportHtml is released under the MIT license. -[github-ci-image]: https://github.com/facelessuser/ExportHtml/workflows/build/badge.svg?branch=master&event=push -[github-ci-link]: https://github.com/facelessuser/ExportHtml/actions?query=workflow%3Abuild+branch%3Amaster -[discord-image]: https://img.shields.io/discord/678289859768745989?logo=discord&logoColor=aaaaaa&color=mediumpurple&labelColor=333333 -[discord-link]: https://discord.gg/TWs8Tgr [pc-image]: https://img.shields.io/packagecontrol/dt/ExportHtml.svg?labelColor=333333&logo=sublime%20text [pc-link]: https://packagecontrol.io/packages/ExportHtml [license-image]: https://img.shields.io/badge/license-MIT-blue.svg?labelColor=333333 From 433c415d3351f678229684156b3080b98c82ae3d Mon Sep 17 00:00:00 2001 From: facelessuser Date: Sat, 27 Sep 2025 07:38:29 -0600 Subject: [PATCH 5/7] Fix CI --- .github/workflows/build.yml | 14 +++++++------- lib/file_strip/json.py | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fb68f4a..f2a3772 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,14 +19,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: 3.11 - name: Install dependencies run: | - python -m pip install --upgrade pip setuptools tox + python -m pip install --upgrade pip setuptools tox build - name: Tests run: | python -m tox @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: @@ -47,11 +47,11 @@ jobs: - name: Setup Node uses: actions/setup-node@v1 with: - node-version: '14' + node-version: '16' - name: Install dependencies run: | - python -m pip install --upgrade pip setuptools tox npm install -g jshint + python -m pip install --upgrade pip setuptools tox build - name: Lint run: | python -m tox @@ -65,14 +65,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: 3.11 - name: Install dependencies run: | - python -m pip install --upgrade pip setuptools tox + python -m pip install --upgrade pip setuptools tox build - name: Install Aspell run: | sudo apt-get install aspell aspell-en diff --git a/lib/file_strip/json.py b/lib/file_strip/json.py index a25118d..1c17dfe 100644 --- a/lib/file_strip/json.py +++ b/lib/file_strip/json.py @@ -1,3 +1,4 @@ +# noqa: A005 """ File Strip. From de13cb63f9938db1564d9ed206f4085cf2df4457 Mon Sep 17 00:00:00 2001 From: facelessuser Date: Sat, 27 Sep 2025 07:40:25 -0600 Subject: [PATCH 6/7] Bump node --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f2a3772..9b9996b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v1 with: - node-version: '16' + node-version: '22' - name: Install dependencies run: | npm install -g jshint From 28f10c00ea75c616e007e811bb7a874294953dc8 Mon Sep 17 00:00:00 2001 From: facelessuser Date: Sat, 27 Sep 2025 07:41:08 -0600 Subject: [PATCH 7/7] Fix emoji settings --- mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 0fbc196..54fd278 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,8 +66,8 @@ markdown_extensions: - pymdownx.caret: - pymdownx.smartsymbols: - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.escapeall: hardbreak: true nbsp: true