Skip to content

Commit b54bcf5

Browse files
committed
support overflow/wraparound values, stricter adherence to formats, added support for different angle units, hwb(), and decimal values
1 parent a696232 commit b54bcf5

File tree

1 file changed

+95
-17
lines changed

1 file changed

+95
-17
lines changed

autoload/css_color.vim

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,56 @@ if ! ( v:version >= 700 && has('syntax') && ( has('gui_running') || has('nvim')
1111
finish
1212
endif
1313

14+
let s:_invalid = -9999 " used after bounding/wrapping, so this is safe
15+
16+
function! s:bound(n, min, max)
17+
let n = a:n
18+
if n < a:min | let n = a:min
19+
elseif n > a:max | let n = a:max | endif
20+
return str2nr( string(n + 0.5) )
21+
endfunction
22+
23+
function! s:rgb2hex(r, g, b)
24+
let [r,g,b] = map( [a:r,a:g,a:b], 's:bound(v:val, 0, 255)' )
25+
return printf( '%02x%02x%02x', r, g, b )
26+
endfunction
27+
1428
function! s:rgb2color(r,g,b)
15-
" Convert 80% -> 204, 100% -> 255, etc.
16-
let rgb = map( [a:r,a:g,a:b], 'v:val =~ "%$" ? ( 255 * v:val ) / 100 : v:val' )
17-
return printf( '%02x%02x%02x', rgb[0], rgb[1], rgb[2] )
29+
" Convert 35.6% -> 90.78, 80% -> 204, 100% -> 255, etc.
30+
let [r,g,b] = map( [a:r,a:g,a:b], 'v:val =~ "%$" ? str2float(v:val) * 2.55 : str2float(v:val)' )
31+
return s:rgb2hex(r, g, b)
1832
endfunction
1933

20-
function! s:hsl2color(h,s,l)
21-
" Convert 80% -> 0.8, 100% -> 1.0, etc.
22-
let [s,l] = map( [a:s, a:l], 'v:val =~ "%$" ? v:val / 100.0 : v:val + 0.0' )
34+
" Vimscript modulo can't handle decimals. Fix that.
35+
function! s:modulo(a, b)
36+
let num = str2nr( string(a:a) )
37+
let extra = a:a - num
38+
return str2nr(a:a - extra) % a:b + extra
39+
endfunction
40+
41+
function! s:angle2deg(angle, units)
42+
let deg = str2float(a:angle)
43+
" angles: https://developer.mozilla.org/en-US/docs/Web/CSS/angle
44+
if a:units == "rad"
45+
let deg = deg * 57.29577951308232 " deg = rad * 180 / pi, 360deg == 6.2832rad
46+
elseif a:units == "grad"
47+
let deg = deg * 0.9 " 360deg = 400grad
48+
elseif a:units == "turn"
49+
let deg = deg * 360.0 " 360deg = 1turn
50+
elseif a:units != "" && a:units != "deg"
51+
return s:_invalid " percent is invalid here
52+
endif
53+
let deg = s:modulo(deg, 360)
54+
if deg < 0 | let deg = deg + 360 | endif
55+
return deg
56+
endfunction
57+
58+
function! s:hsl2rgb(h,u,s,l)
59+
let [s,l] = map( [a:s, a:l], 'str2nr(v:val) >= 1 ? 1.0 : str2float(v:val)' )
2360
" algorithm transcoded to vim from http://www.w3.org/TR/css3-color/#hsl-color
24-
let hh = ( a:h % 360 ) / 360.0
61+
let hh = s:angle2deg(a:h, a:u)
62+
if hh == s:_invalid | return [hh,hh,hh] | endif
63+
let hh = hh / 360.0
2564
let m2 = l <= 0.5 ? l * ( s + 1 ) : l + s - l * s
2665
let m1 = l * 2 - m2
2766
let rgb = []
@@ -32,12 +71,48 @@ function! s:hsl2color(h,s,l)
3271
\ h * 2 < 1 ? m2 :
3372
\ h * 3 < 2 ? m1 + ( m2 - m1 ) * ( 2/3.0 - h ) * 6 :
3473
\ m1
35-
if v > 1.0 | return '' | endif
74+
if v > 1.0 | return [s:_invalid, s:_invalid, s:_invalid] | endif
3675
let rgb += [ float2nr( 255 * v ) ]
3776
endfor
38-
return printf( '%02x%02x%02x', rgb[0], rgb[1], rgb[2] )
77+
return rgb
3978
endfunction
4079

80+
function! s:pctvet(h,s,l)
81+
" hue cannot be a percent, the others must be percents. same with hwb
82+
if a:h =~ "%$" || a:s !~ "%$" || a:l !~ "%$"
83+
return [s:_invalid, s:_invalid, s:_invalid]
84+
endif
85+
let [s,l] = map( [a:s, a:l], 'v:val >= 100 ? 1.0 : v:val / 100.0' )
86+
return [a:h, s, l]
87+
endfunction
88+
89+
function! s:hsl2color(h,u,s,l)
90+
let [h,s,l] = s:pctvet(a:h, a:s, a:l) " convert saturation & luminance % -> num
91+
if h == s:_invalid | return '' | endif
92+
let [r,g,b] = s:hsl2rgb(h, a:u, s, l)
93+
if r == s:_invalid | return '' | endif
94+
return s:rgb2hex(r, g, b)
95+
endfunction
96+
97+
function! s:hwb2color(h,u,w,b)
98+
let [h,w,b] = s:pctvet(a:h, a:w, a:b) " convert whiteness & blackness % -> num
99+
if h == s:_invalid | return '' | endif
100+
" algorithm transcoded to vim from https://drafts.csswg.org/css-color/#hwb-to-rgb
101+
if w + b >= 1
102+
let gray = w / (w + b)
103+
let [r,g,b] = [gray,gray,gray]
104+
else
105+
let [r,g,b] = map( s:hsl2rgb(a:h, a:u, 1.0, 0.5), 'v:val * (1 - w - b) + w' )
106+
endif
107+
return s:rgb2hex(r, g, b)
108+
endfunction
109+
110+
" TODO (probably not): new "device-independent colors":
111+
" lch(), oklch(), lab(), oklab(), and color().
112+
" These look hard and I don't see reference algorithms to convert to sRGB.
113+
" https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
114+
" https://drafts.csswg.org/css-color/#lab-colors
115+
41116
let s:_1_3 = 1.0/3
42117
let s:_16_116 = 16.0/116.0
43118
let s:cos16 = cos(16*(180/atan2(0,-1)))
@@ -170,14 +245,17 @@ function! s:create_syn_match()
170245
let hex = submatch(1)
171246
let funcname = submatch(2)
172247

248+
if funcname =~ 'rgb' && submatch(4) != '' | return '' | endif " rgb() doesn't support units
249+
173250
let rgb_color
174-
\ = funcname == 'rgb' ? s:rgb2color(submatch(3),submatch(4),submatch(5))
175-
\ : funcname == 'hsl' ? s:hsl2color(submatch(3),submatch(4),submatch(5))
251+
\ = funcname =~ '^rgb' ? s:rgb2color(submatch(3),submatch(5),submatch(6))
252+
\ : funcname =~ '^hsl' ? s:hsl2color(submatch(3),submatch(4),submatch(5),submatch(6))
253+
\ : funcname == 'hwb' ? s:hwb2color(submatch(3),submatch(4),submatch(5),submatch(6))
176254
\ : strlen(hex) >= 6 ? tolower(hex[0:5])
177255
\ : strlen(hex) >= 3 ? tolower(hex[0].hex[0].hex[1].hex[1].hex[2].hex[2])
178256
\ : ''
179257

180-
if rgb_color == '' | throw 'css_color: create_syn_match invoked on bad match data' | endif
258+
if rgb_color == '' | return '' | endif
181259
let s:pattern_color[pattern] = rgb_color
182260
endif
183261

@@ -229,14 +307,14 @@ endfunction
229307

230308
let s:_hexcolor = '#\(\x\{3}\%(\>\|\x\{3}\>\)\)' " submatch 1
231309
let s:_rgbacolor = '#\(\x\{3}\%(\>\|\x\%(\>\|\x\{2}\%(\>\|\x\{2}\>\)\)\)\)' " submatch 1
232-
let s:_funcname = '\(rgb\|hsl\)a\?' " submatch 2
233-
let s:_funcprep = '\%(\s*\S\+\%(\%(\s\+\S\+\)\{2}\|\%(\s*,\s*\S\+\)\{2}\)\)\@=' " lookahead for 12,34,56 vs 12 34 56 to avoid 12 34,56
310+
let s:_funcname = '\(rgb\a\?\|hsla\?\|hwb\)' " submatch 2
311+
let s:_consistent = '\%(\%(\s\+[0-9.%]\+\)\{2}\s*\%(\/\s*[-0-9.%]\+\)\?[)]\|\%(\s*,\s*[-0-9.%]\+\)\{2,3}[)]\)\@=' " lookahead: 1 2 3 or 1 2 3/4 or 1,2,3 or 1,2,3,4 after 1
234312
let s:_ws_ = '\s*'
235-
"let s:_numval = '\(\d\{1,3}\%(\.\d*\)\?%\?\)' " submatch 3,4,5 (use this after implementing ceilings for the output of b:css_color_pat)
236-
let s:_numval = '\(\d\{1,2}\%(\.\d*\)\?%\|100%\|255\|\%(1\?\d\{1,2}\|2[0-4]\d\|25[0-5]\)\%(\.\d*\)\?\)' " submatch 3,4,5
313+
let s:_numval = '\(-\?\d\{1,3}\%(\.\d*\)\?%\?\)' " submatch 3,5,6
314+
let s:_units = '\(deg\|g\?rad\|turn\)\?' " submatch 4
237315
let s:_listsep = s:_ws_ . '[,[:space:]]\+'
238316
let s:_otherargs_ = s:_ws_ . '\%([,\/][^)]*\)\?' " ignore alpha
239-
let s:_funcexpr = s:_funcname . '[(]' . s:_funcprep . s:_ws_ . s:_numval . s:_listsep . s:_numval . s:_listsep . s:_numval . s:_otherargs_ . '[)]'
317+
let s:_funcexpr = s:_funcname . '[(]' . s:_ws_ . s:_numval . s:_units . s:_consistent . s:_listsep . s:_numval . s:_listsep . s:_numval . s:_otherargs_ . '[)]'
240318
let s:_csscolor = s:_rgbacolor . '\|' . s:_funcexpr
241319
" N.B. sloppy heuristic constants for performance reasons:
242320
" a) start somewhere left of screen in case of partially visible colorref

0 commit comments

Comments
 (0)