diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_traits.py b/python/ipywidgets/ipywidgets/widgets/tests/test_traits.py index efcaf5651f..9727d1a657 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_traits.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_traits.py @@ -52,6 +52,15 @@ class TestColor(TraitTestBase): 'hsl(0.0, .0, 0)', # hsl 'hsl( 0.5,0.3,0 )', # hsl with spaces 'hsla(10,10,10, 0.5)', # rgba with float alpha + 'var(--my-color)', # CSS variable without fallback + 'var(--my-color,)', # CSS variable with empty fallback + 'var(--my-color-æ)', # CSS variable with non-ascii characters + 'var(--my-color-\u1234)', # CSS variable with unicode characters + 'var(--my-color-\.)', # CSS variable with escaped characters + 'var(--my-color,black)', # CSS variable with named color fallback + 'var(--my-color, black)', # CSS variable with named color fallback + 'var(--my-color, rgb(20, 70, 50))', # CSS variable with rgb color fallback + 'var(--my-color, #fff)', # CSS variable with rgb color fallback ] _bad_values = [ "vanilla", "blues", # Invalid color names @@ -61,6 +70,11 @@ class TestColor(TraitTestBase): 'hsl(0.4, 512, -40)', 'rgba(0, 0, 0)', 'hsla(0, 0, 0)', + 'var(-my-color)', # wrong identifier + 'var(my-color, black)', # wrong identifier + 'var(my-color-., black)', # invalid character in identifier + 'var(--my-color, vanilla)', # wrong fallback + 'var(--my-color, rgba(0,0,0))', # wrong fallback None, ] diff --git a/python/ipywidgets/ipywidgets/widgets/trait_types.py b/python/ipywidgets/ipywidgets/widgets/trait_types.py index 57300e7281..5753f581a3 100644 --- a/python/ipywidgets/ipywidgets/widgets/trait_types.py +++ b/python/ipywidgets/ipywidgets/widgets/trait_types.py @@ -13,9 +13,11 @@ _color_names = ['aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beiae', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgrey', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'grey', 'green', 'greenyellow', 'honeydew', 'hotpink', 'indianred ', 'indigo ', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgrey', 'lightgreen', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'transparent', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen'] # Regex colors #fff and #ffffff -_color_hex_re = re.compile(r'#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$') +_color_hex = r'#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?' +_color_hex_re = re.compile(fr'^{_color_hex}$') # Regex colors #ffff and #ffffffff (includes alpha value) -_color_hexa_re = re.compile(r'^#[a-fA-F0-9]{4}(?:[a-fA-F0-9]{4})?$') +_color_hexa = r'#[a-fA-F0-9]{4}(?:[a-fA-F0-9]{4})?' +_color_hexa_re = re.compile(fr'^{_color_hexa}$') # Helpers (float percent, int percent with optional surrounding whitespace) _color_frac_percent = r'\s*(\d+(\.\d*)?|\.\d+)?%?\s*' @@ -28,9 +30,50 @@ _color_hsla = r'hsla\({fp},{fp},{fp},{fp}\)' # Regex colors rgb/rgba/hsl/hsla -_color_rgbhsl_re = re.compile('({})|({})|({})|({})'.format( +_color_rgbhsl = '({})|({})|({})|({})'.format( _color_rgb, _color_rgba, _color_hsl, _color_hsla -).format(ip=_color_int_percent, fp=_color_frac_percent)) +).format(ip=_color_int_percent, fp=_color_frac_percent) +_color_rgbhsl_re = re.compile(_color_rgbhsl) + +# Support for CSS variables. +# For production rules, see: https://drafts.csswg.org/css-syntax-3/#tokenization + +_escape = r'\\?([0-9a-fA-F]{1-6}\s?|[^0-9a-fA-F\s])' +_non_ascii = r''.join( + ( + r'\u00B7', + r'\u00C0-\u00D6', + r'\u00C0-\u00D6', + r'\u00D8-\u00F6', + r'\u00F8-\u037D', + r'\u037F-\u1FFF', + r'\u200C', + r'\u200D', + r'\u203F', + r'\u2040', + r'\u2070-\u218F', + r'\u2C00-\u2FEF', + r'\u3001-\uD7FF', + r'\uF900-\uFDCF', + r'\uFDF0-\uFFFD', + r'\u10000' + ) +) + +# Custom CSS identifier +_custom_ident = fr'--([a-zA-Z0-9{_non_ascii}]|{_escape})+' + +# Matching for CSS variables with valid color fallback declaration values. +# +# A CSS variable consists of a custom identifier starting with '--'. +# The 'var()' function can be used for substituting the custom property into +# the value of another property. +# +# Here we further restrict the fallback values to be valid colors. + +_css_color = fr'({"|".join(_color_names)}|({_color_rgbhsl})|({_color_hex})|({_color_hexa}))' +_css_var_fallback_color = fr'var\({_custom_ident}(,\s*{_css_color}\s*)?\)' +_color_var_re = re.compile(_css_var_fallback_color) class Color(traitlets.Unicode): @@ -44,7 +87,8 @@ def validate(self, obj, value): return value if isinstance(value, str): if (value.lower() in _color_names or _color_hex_re.match(value) or - _color_hexa_re.match(value) or _color_rgbhsl_re.match(value)): + _color_hexa_re.match(value) or _color_rgbhsl_re.match(value) or + _color_var_re.match(value)): return value self.error(obj, value)