diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fb68f4a..9b9996b 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: '22' - 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/CHANGES.md b/CHANGES.md index e7a6efd..c3d47ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,9 @@ # ExportHtml -## 2.17.5 +## 2.18.0 +- **NEW**: New colorblind filters. +- **CHORE**: Updates to support newer dependencies. - **FIX**: Remove unnecessary dependencies. - **FIX**: Fix typo in settings configuration. 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/lib/color_scheme_matcher.py b/lib/color_scheme_matcher.py index 7256d00..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 @@ -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..55cb0bd 100644 --- a/lib/color_scheme_tweaker.py +++ b/lib/color_scheme_tweaker.py @@ -6,8 +6,8 @@ """ from __future__ import absolute_import import sublime -from .st_colormod import Color -from mdpopups.coloraide import util +from mdpopups.st_colormod import Color +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": @@ -358,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])) @@ -416,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": @@ -516,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/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. diff --git a/lib/st_colormod.py b/lib/st_colormod.py deleted file mode 100644 index 1018d51..0000000 --- a/lib/st_colormod.py +++ /dev/null @@ -1,632 +0,0 @@ -"""Color-mod.""" -import re -from mdpopups.coloraide import Color as ColorCSS -from mdpopups.coloraide import ColorMatch -from mdpopups.coloraide.spaces import _parse -from mdpopups.coloraide import util -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'(?i)\s+a(?:lpha)?\(\s*(?:(\+\s+|\-\s+)?({percent}|{float})|(\*)?\s*({percent}|{float}))\s*\)'.format( - **_parse.COLOR_PARTS - ) - ), - "saturation": re.compile( - r'(?i)\s+s(?:aturation)?\((\+\s|\-\s|\*)?\s*({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_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_BRACKETS = re.compile(r'(?:(\()|(\))|[^()]+)') -RE_MIN_CONTRAST_END = re.compile(r'(?i)\s+({float})\s*\)'.format(**_parse.COLOR_PARTS)) -RE_VARS = re.compile(r'(?i)(?:(?<=^)|(?<=[\s\t\(,/]))(var\(\s*([-\w][-\w\d]*)\s*\))(?!\()(?=[\s\t\),/]|$)') - - -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 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.fit(method="clip", in_place=True) - - 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.fit(method="clip", in_place=True) - 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.fit(method="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('%')) - 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 = util.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.alpha = 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 - max_mix = 100.0 - else: - primary = "blackness" - secondary = "whiteness" - min_mix = orig.blackness - max_mix = 100.0 - 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.0 - orig.get(primary))) * orig.get(secondary) - ) - temp.set(primary, mid_mix) - temp.set(secondary, mid_other) - 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 - final = orig.new("hwb", [orig.hue, last_mix, last_other] if is_dark else [orig.hue, last_other, last_mix]) - 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) - 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) - if not alpha: - new_color.alpha = color.alpha - self._color.update(new_color) - - def alpha(self, value, op=""): - """Alpha.""" - - this = self._color - op = self.OP_MAP.get(op, self._op_null) - this.alpha = op(this.alpha, 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(ColorCSS): - """Color modify class.""" - - def __init__(self, color, data=None, alpha=util.DEF_ALPHA, *, filters=None, variables=None, **kwargs): - """Initialize.""" - - super().__init__(color, data, alpha, filters=None, variables=variables, **kwargs) - - def _parse(self, color, data=None, alpha=util.DEF_ALPHA, filters=None, 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) - 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 - 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): - """ - 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 ColorMatch(obj._space, 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 - - @classmethod - def match(cls, string, start=0, fullmatch=False, *, filters=None, variables=None): - """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 - - def new(self, color, data=None, alpha=util.DEF_ALPHA, *, filters=None, variables=None, **kwargs): - """Create new color object.""" - - return type(self)(color, data, alpha, filters=filters, variables=variables, **kwargs) - - def update(self, color, data=None, alpha=util.DEF_ALPHA, *, filters=None, 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) - return self - - def mutate(self, color, data=None, alpha=util.DEF_ALPHA, *, filters=None, variables=None, **kwargs): - """Mutate the current color to a new color.""" - - self._attach(self._parse(color, data, alpha, filters=filters, variables=variables, **kwargs)) - return self 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(?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/mkdocs.yml b/mkdocs.yml index d672eb8..54fd278 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: @@ -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 @@ -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 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 = '''