Skip to content

Commit 6db437c

Browse files
authored
Merge pull request #3796 from nurbo/refactor/allow-css-variables-in-color-trait
Allow CSS variables to be used as values in the Color trait
2 parents 0db5089 + ec9b378 commit 6db437c

File tree

2 files changed

+66
-5
lines changed

2 files changed

+66
-5
lines changed

python/ipywidgets/ipywidgets/widgets/tests/test_traits.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ class TestColor(TraitTestBase):
5252
'hsl(0.0, .0, 0)', # hsl
5353
'hsl( 0.5,0.3,0 )', # hsl with spaces
5454
'hsla(10,10,10, 0.5)', # rgba with float alpha
55+
'var(--my-color)', # CSS variable without fallback
56+
'var(--my-color-with_separators)', # CSS variable without fallback
57+
'var(--my-color,)', # CSS variable with empty fallback
58+
'var(--my-color-æ)', # CSS variable with non-ascii characters
59+
'var(--my-color-\u1234)', # CSS variable with unicode characters
60+
r'var(--my-color-\\1234)', # CSS variable escaped hex character
61+
'var(--my-color-\.)', # CSS variable with escaped characters
62+
'var(--my-color,black)', # CSS variable with named color fallback
63+
'var(--my-color, black)', # CSS variable with named color fallback
64+
'var(--my-color, rgb(20, 70, 50))', # CSS variable with rgb color fallback
65+
'var(--my-color, #fff)', # CSS variable with rgb color fallback
5566
]
5667
_bad_values = [
5768
"vanilla", "blues", # Invalid color names
@@ -61,6 +72,12 @@ class TestColor(TraitTestBase):
6172
'hsl(0.4, 512, -40)',
6273
'rgba(0, 0, 0)',
6374
'hsla(0, 0, 0)',
75+
'var(-my-color)', # wrong identifier
76+
'var(--my-color-\u2041)', # invalid unicode codepoint
77+
'var(my-color, black)', # wrong identifier
78+
'var(my-color-., black)', # invalid character in identifier
79+
'var(--my-color, vanilla)', # wrong fallback
80+
'var(--my-color, rgba(0,0,0))', # wrong fallback
6481
None,
6582
]
6683

python/ipywidgets/ipywidgets/widgets/trait_types.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
_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']
1414

1515
# Regex colors #fff and #ffffff
16-
_color_hex_re = re.compile(r'#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$')
16+
_color_hex = r'#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?'
17+
_color_hex_re = re.compile(fr'^{_color_hex}$')
1718
# Regex colors #ffff and #ffffffff (includes alpha value)
18-
_color_hexa_re = re.compile(r'^#[a-fA-F0-9]{4}(?:[a-fA-F0-9]{4})?$')
19+
_color_hexa = r'#[a-fA-F0-9]{4}(?:[a-fA-F0-9]{4})?'
20+
_color_hexa_re = re.compile(fr'^{_color_hexa}$')
1921

2022
# Helpers (float percent, int percent with optional surrounding whitespace)
2123
_color_frac_percent = r'\s*(\d+(\.\d*)?|\.\d+)?%?\s*'
@@ -28,9 +30,50 @@
2830
_color_hsla = r'hsla\({fp},{fp},{fp},{fp}\)'
2931

3032
# Regex colors rgb/rgba/hsl/hsla
31-
_color_rgbhsl_re = re.compile('({})|({})|({})|({})'.format(
33+
_color_rgbhsl = '({})|({})|({})|({})'.format(
3234
_color_rgb, _color_rgba, _color_hsl, _color_hsla
33-
).format(ip=_color_int_percent, fp=_color_frac_percent))
35+
).format(ip=_color_int_percent, fp=_color_frac_percent)
36+
_color_rgbhsl_re = re.compile(_color_rgbhsl)
37+
38+
# Support for CSS variables.
39+
# For production rules, see: https://drafts.csswg.org/css-syntax-3/#tokenization
40+
41+
_escape = r'\\([0-9a-fA-F]{1-6}\s?|[^0-9a-fA-F\s])'
42+
_non_ascii = r''.join(
43+
(
44+
r'\u00B7',
45+
r'\u00C0-\u00D6',
46+
r'\u00C0-\u00D6',
47+
r'\u00D8-\u00F6',
48+
r'\u00F8-\u037D',
49+
r'\u037F-\u1FFF',
50+
r'\u200C',
51+
r'\u200D',
52+
r'\u203F',
53+
r'\u2040',
54+
r'\u2070-\u218F',
55+
r'\u2C00-\u2FEF',
56+
r'\u3001-\uD7FF',
57+
r'\uF900-\uFDCF',
58+
r'\uFDF0-\uFFFD',
59+
r'\u10000'
60+
)
61+
)
62+
63+
# Custom CSS identifier
64+
_custom_ident = fr'--([a-zA-Z0-9_\-{_non_ascii}]|{_escape})+'
65+
66+
# Matching for CSS variables with valid color fallback declaration values.
67+
#
68+
# A CSS variable consists of a custom identifier starting with '--'.
69+
# The 'var()' function can be used for substituting the custom property into
70+
# the value of another property.
71+
#
72+
# Here we further restrict the fallback values to be valid colors.
73+
74+
_css_color = fr'({"|".join(_color_names)}|({_color_rgbhsl})|({_color_hex})|({_color_hexa}))'
75+
_css_var_fallback_color = fr'var\({_custom_ident}(,\s*({_css_color}\s*)?)?\)'
76+
_color_var_re = re.compile(_css_var_fallback_color)
3477

3578

3679
class Color(traitlets.Unicode):
@@ -44,7 +87,8 @@ def validate(self, obj, value):
4487
return value
4588
if isinstance(value, str):
4689
if (value.lower() in _color_names or _color_hex_re.match(value) or
47-
_color_hexa_re.match(value) or _color_rgbhsl_re.match(value)):
90+
_color_hexa_re.match(value) or _color_rgbhsl_re.match(value) or
91+
_color_var_re.match(value)):
4892
return value
4993

5094
self.error(obj, value)

0 commit comments

Comments
 (0)