├── README.md ├── plugin └── colors.vim └── scratch.txt /README.md: -------------------------------------------------------------------------------- 1 | # ConvertColorTo 2 | 3 | A simple and easy to use plugin that can convert various color strings to 4 | different formats. The goal was to create a simple and flexible API that 5 | required no 3rd party dependencies. 6 | 7 | 8 | ### Quick Usage Reference 9 | 10 | Given a line like this, with the cursor anywhere on it 11 | ``` 12 | background-color: rgb(241, 23, 102); 13 | ``` 14 | 15 | simply perform 16 | ``` 17 | :ConvertColorTo hex 18 | ``` 19 | 20 | and you'll get this: 21 | ``` 22 | background-color: #f11766; 23 | ``` 24 | 25 | You can also optionally make a visual selection of the color value, and 26 | execute: 27 | ``` 28 | :'<,'>ConvertColorTo hex 29 | ``` 30 | 31 | Currently supported color formats are: 32 | 33 | * `hex` -> `#001122` 34 | * `hexa` -> `#00112233` 35 | * `hex_num` -> `0x001122` 36 | * `rgb` -> `rgb(0, 100, 200)` 37 | * `rgb_int` -> `rgb(0, 100, 200)` 38 | * `rgb_float` -> `rgb(0.1, 0.2, 0.3)` 39 | * `rgba` -> `rgba(0, 100, 200, 0.3)` 40 | * `hsl` -> `hsl(200, 20%, 10%)` 41 | * `hsla` -> `hsla(100, 20%, 30%, 0.2)` 42 | 43 | **Protip**: The plugin will always attempt to maintain a transparency value if 44 | it exists in the original color (i.e. using type `rgb` on an `hsla`, will 45 | actually convert it to `rgba` automatically). However, if you are converting a 46 | color that does not have a transparency, but specify a type with transparency, 47 | the plugin will add a transparency of `1`. 48 | 49 | Another **Protip**: You can use this plugin as a simple text formatter if you 50 | apply the same type as the source color. 51 | 52 | 53 | ### Detailed Usage 54 | 55 | There are a couple different ways to use the plugin. 56 | 57 | Via the commandline: 58 | ``` 59 | :ConvertColorTo [type] [color_string] 60 | :'<,'>ConvertColorTo [type] 61 | ``` 62 | 63 | Via the expression register: 64 | ``` 65 | =ConvertColorTo('[type]', '[color_string]') 66 | :put =ConverColorTo('[type]', '[color_string]') 67 | ``` 68 | 69 | Or if you just want to see an echo of the color conversion: 70 | ``` 71 | :call ConvertColorTo('[type]', '[color_string]') 72 | ``` 73 | 74 | Both `type` and `color` are optional. 75 | 76 | `type` is simply one of the above listed color formats. If no format is 77 | specified then the plugin will attempt to convert the color to `hex`. If it's 78 | already `hex` then it will convert to `rgb` instead. `type` must be specified 79 | if you want to manually pass in a `color_string`. If it becomes annoying I may 80 | fix this later... 81 | 82 | `color_string` is only needed if you don't have a text selection or an existing 83 | color on the current line. It's simply a color in any supported format to 84 | convert. 85 | 86 | 87 | ### Configuration 88 | 89 | You can specify a global or local config when using `rgb` or `rgba` to be 90 | either `int` (default) or `float` 91 | 92 | ``` 93 | let g:convert_color_default_rgb = 'float' 94 | let b:convert_color_default_rgb = 'int' 95 | ``` 96 | 97 | 98 | ### Minor Philosophical Things 99 | 100 | While this plugin does some basic validation of the color formats, it is by no 101 | means a perfect and fully accurate parser. There are cases where you could 102 | potentially specify invalid strings and they might get converted or vice versa, 103 | valid strings that don't get detected properly. I did this mostly because I 104 | just didn't care at the time, it should work 99% of the time for the common 105 | cases. 106 | 107 | With that said, if you do find a bug or what not, please let me know, maybe 108 | it's easy to patch and we can improve the plugin. 109 | 110 | 111 | ### License 112 | 113 | MIT License 114 | -------------------------------------------------------------------------------- /plugin/colors.vim: -------------------------------------------------------------------------------- 1 | let s:hex_shorthand_regex = '#\([a-fA-F0-9]\)\([a-fA-F0-9]\)\([a-fA-F0-9]\)\([a-fA-F0-9]\)\=\>' 2 | let s:hex_num_shorthand_regex = '0x\([a-fA-F0-9]\)\([a-fA-F0-9]\)\([a-fA-F0-9]\)\([a-fA-F0-9]\)\=\>' 3 | let s:hex_regex = '#\([a-fA-F0-9]\{2}\)\([a-fA-F0-9]\{2}\)\([a-fA-F0-9]\{2}\)\([a-fA-F0-9]\{2}\)\=\>' 4 | let s:hex_num_regex = '0x\([a-fA-F0-9]\{2}\)\([a-fA-F0-9]\{2}\)\([a-fA-F0-9]\{2}\)\([a-fA-F0-9]\{2}\)\=\>' 5 | let s:rgb_regex = 'rgba\=(\(\s*[0-9.]\+[ ,]*\)\(\s*[0-9.]\+[ ,]*\)\(\s*[0-9.]\+[ ,]*\)\(\s*[0-9.]\+[ ,]*\)\=)' 6 | let s:hsl_regex = 'hsla\=(\(\s*[0-9.]\+\%(deg\|rad\|turn\|grad\)\=[ ,]\+\)\([0-9]\+%[ ,]\+\)\([0-9]\+%[ ,/]*\)\([0-9.]\+%\= *\)\=)' 7 | let s:detect_shorthand = '\<\([a-fA-F0-9]\)\1\([a-fA-F0-9]\)\2\([a-fA-F0-9]\)\3\%(\([a-fA-F0-9]\)\4\)\=\>' 8 | let s:normalize_hex_digits = {idx, val -> str2nr('0x'.val, 16) / 255.0} 9 | let s:normalize_hex_shorthand_digits = {idx, val -> str2nr('0x'.val.val, 16) / 255.0} 10 | let s:pi = 3.14159265359 11 | 12 | " Native viml min/max functions cannot accept floats... 13 | function! s:Min(vals) abort 14 | let l:min = 2.0 15 | for l:item in a:vals 16 | if l:item < l:min 17 | let l:min = l:item 18 | endif 19 | endfor 20 | return l:min 21 | endfunction 22 | 23 | function! s:Max(vals) abort 24 | let l:max = -1.0 25 | for l:item in a:vals 26 | if l:item > l:max 27 | let l:max = l:item 28 | endif 29 | endfor 30 | return l:max 31 | endfunction 32 | 33 | function! s:NormalizeDigits(idx, val) abort 34 | if a:idx <= 2 35 | " If the value has is an integer - convert to float percentage of 0-255 36 | return matchstr(a:val, '\.') == '.' ? str2float(a:val) : str2nr(a:val) / 255.0 37 | endif 38 | return str2float(a:val) 39 | endfunction 40 | 41 | function! s:NormalizeHSLDigits(idx, val) abort 42 | if a:val =~# '%' 43 | return str2nr(a:val) / 100.0 44 | endif 45 | if a:val =~# 'rad' 46 | let l:rad = str2float(a:val) 47 | let l:degree = float2nr(round(l:rad * 180.0 / s:pi)) 48 | " Ensure we never have values over 360 49 | return (l:degree % 360) / 360.0 50 | endif 51 | if a:val =~# 'grad' 52 | let l:grad = float2nr(round(str2float(a:val))) 53 | " Ensure we never have values over 400 54 | return (l:grad % 400) / 400.0 55 | endif 56 | if a:val =~# '\.' 57 | return str2float(a:val) 58 | endif 59 | return str2nr(a:val) / 360.0 60 | endfunction 61 | 62 | " Converting to and from hsl is a bit complicated, and destructive 63 | " https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion 64 | function! s:HueToRGB(p, q, t) abort 65 | let l:p = a:p 66 | let l:q = a:q 67 | let l:t = a:t 68 | if l:t < 0 69 | let l:t = l:t + 1.0 70 | endif 71 | if l:t > 1 72 | let l:t = l:t - 1.0 73 | endif 74 | 75 | if l:t < 1.0 / 6.0 76 | return l:p + (l:q - l:p) * 6.0 * l:t 77 | endif 78 | if l:t < 1.0 / 2.0 79 | return l:q 80 | endif 81 | if l:t < 2.0 / 3.0 82 | return l:p + (l:q - l:p) * (2.0 / 3.0 - l:t) * 6.0 83 | endif 84 | return l:p 85 | endfunction 86 | 87 | function! s:HSLToRGB(vals) abort 88 | let l:red = 0.0 89 | let l:green = 0.0 90 | let l:blue = 0.0 91 | let l:hue = a:vals[0] 92 | let l:saturation = a:vals[1] 93 | let l:lightness = a:vals[2] 94 | if l:saturation == 0 95 | let l:red = l:lightness 96 | let l:green = l:lightness 97 | let l:blue = l:lightness 98 | else 99 | let l:q = l:lightness < 0.5 ? l:lightness * (1.0 + l:saturation) : l:lightness + l:saturation - l:lightness * l:saturation 100 | let l:p = 2.0 * l:lightness - l:q 101 | let l:_hue = l:hue + 1.0 / 3.0 102 | let l:red = s:HueToRGB(l:p, l:q, l:_hue) 103 | let l:green = s:HueToRGB(l:p, l:q, l:hue) 104 | let l:_hue = l:hue - 1.0 / 3.0 105 | let l:blue = s:HueToRGB(l:p, l:q, l:_hue) 106 | endif 107 | 108 | " Append alpha if it exists 109 | let l:colors = [l:red, l:green, l:blue] 110 | if len(a:vals) == 4 111 | call add(l:colors, a:vals[3]) 112 | endif 113 | return l:colors 114 | endfunction 115 | 116 | " Attempt to parse and normalize the string for a specific color 117 | function! s:ParseString(str, matcher, normalize) abort 118 | let l:list = matchlist(a:str, a:matcher) 119 | let l:match = v:null 120 | if len(l:list) > 0 121 | let l:match = l:list[0] 122 | endif 123 | 124 | let l:data = map( 125 | \ filter(list, {idx, val -> idx == 0 || val == '' ? 0 : 1}), 126 | \ a:normalize 127 | \ ) 128 | 129 | if len(l:data) == 0 130 | return v:null 131 | endif 132 | 133 | return { 134 | \ 'match': l:match, 135 | \ 'match_pos': match(a:str, a:matcher), 136 | \ 'normalized_data': l:data, 137 | \ 'color_string': '' 138 | \ } 139 | endfunction 140 | 141 | " Attempt to figure out string type and normalize the data from it 142 | function! s:NormalizeString(str) abort 143 | " Is it a valid shorthand hex[a]? 144 | let l:match = s:ParseString(a:str, s:hex_shorthand_regex, s:normalize_hex_shorthand_digits) 145 | if type(l:match) == type({}) 146 | let l:match['type'] = 'hex' 147 | return l:match 148 | endif 149 | 150 | " Is it a valid shorthand hex_num[a]? 151 | let l:match = s:ParseString(a:str, s:hex_num_shorthand_regex, s:normalize_hex_shorthand_digits) 152 | if type(l:match) == type({}) 153 | let l:match['type'] = 'hex' 154 | return l:match 155 | endif 156 | 157 | " Is it a valid hex[a]? 158 | let l:match = s:ParseString(a:str, s:hex_regex, s:normalize_hex_digits) 159 | if type(l:match) == type({}) 160 | let l:match['type'] = 'hex' 161 | return l:match 162 | endif 163 | 164 | " Is it a valid hex_num[a]? 165 | let l:match = s:ParseString(a:str, s:hex_num_regex, s:normalize_hex_digits) 166 | if type(l:match) == type({}) 167 | let l:match['type'] = 'hex' 168 | return l:match 169 | endif 170 | 171 | " Is it a valid rgb[a]? 172 | let l:match = s:ParseString(a:str, s:rgb_regex, function('s:NormalizeDigits')) 173 | if type(l:match) == type({}) 174 | let l:match['type'] = 'rgb' 175 | return l:match 176 | endif 177 | 178 | " Is it a valid hsl[a]? 179 | let l:match = s:ParseString(a:str, s:hsl_regex, function('s:NormalizeHSLDigits')) 180 | if type(l:match) == type({}) 181 | let l:match['type'] = 'hsl' 182 | " Convert hsla decimals into rgba decimals 183 | let l:match['normalized_data'] = s:HSLToRGB(l:match['normalized_data']) 184 | return l:match 185 | endif 186 | 187 | return v:null 188 | endfunction 189 | 190 | function! s:FormatRGBString(vals) abort 191 | return 'rgb'.(len(a:vals) == 4 ? "a" : "").'('.join(map(a:vals, {idx, val -> val == 0 ? 0 : string(val)}), ', ').')' 192 | endfunction 193 | 194 | function! s:FormatDefaultRGBString(vals) abort 195 | let l:default = 'int' 196 | if exists('b:convert_color_default_rgb') 197 | let l:default = b:convert_color_default_rgb 198 | elseif exists('g:convert_color_default_rgb') 199 | let l:default = g:convert_color_default_rgb 200 | endif 201 | 202 | if l:default == 'float' 203 | return s:FormatRGBFloatString(a:vals) 204 | else 205 | return s:FormatRGBIntString(a:vals) 206 | endif 207 | endfunction 208 | 209 | function! s:FormatRGBIntString(vals) abort 210 | return s:FormatRGBString(map(a:vals, {idx, val -> idx == 3 ? val : float2nr(round(val * 255))})) 211 | endfunction 212 | 213 | function! s:FormatRGBFloatString(vals) abort 214 | return s:FormatRGBString(a:vals) 215 | endfunction 216 | 217 | function! s:FormatRGBAString(vals) abort 218 | let l:vals = a:vals 219 | if len(l:vals) == 3 220 | call add(l:vals, 1) 221 | endif 222 | let l:Formatter = get(s:Formatters, 'rgb') 223 | return l:Formatter(l:vals) 224 | endfunction 225 | 226 | function! s:FormatHEXString(vals) abort 227 | let l:string = join(map(a:vals, {idx, val -> printf('%02x', float2nr(round(val * 255)))}), '') 228 | 229 | " Attempt to convert to a shorthand hex 230 | let l:shorthand = filter( 231 | \ matchlist(l:string, s:detect_shorthand), 232 | \ {idx, val -> idx == 0 || val == '' ? 0 : 1} 233 | \ ) 234 | if len(l:shorthand) > 0 235 | return '#'.join(l:shorthand, '') 236 | endif 237 | 238 | return '#'.l:string 239 | endfunction 240 | 241 | function! s:FormatHEXNum(vals) abort 242 | let l:string = join(map(a:vals, {idx, val -> printf('%02x', float2nr(round(val * 255)))}), '') 243 | return '0x'.toupper(l:string) 244 | endfunction 245 | 246 | function! s:FormatHEXAString(vals) abort 247 | let l:vals = a:vals 248 | if len(l:vals) == 3 249 | call add(l:vals, 1) 250 | endif 251 | let l:Formatter = get(s:Formatters, 'hex') 252 | return l:Formatter(l:vals) 253 | endfunction 254 | 255 | function! s:FormatHSLString(vals) abort 256 | let l:colors = a:vals[0:2] 257 | let l:min = s:Min(colors) 258 | let l:max = s:Max(colors) 259 | let l:lightness = (l:min + l:max) / 2 260 | let l:saturation = 0 261 | let l:hue = 0 262 | 263 | if l:min != l:max 264 | let l:diff = l:max - l:min 265 | let [l:red, l:green, l:blue] = l:colors 266 | if l:lightness > 0.5 267 | let l:saturation = l:diff / (2 - l:max - l:min) 268 | else 269 | let l:saturation = l:diff / (l:max + l:min) 270 | endif 271 | if l:max == l:red 272 | let l:hue = (l:green - l:blue) / l:diff + (l:green < l:blue ? 6.0 : 0.0) 273 | elseif l:max == l:green 274 | let l:hue = (l:blue - l:red) / l:diff + 2 275 | elseif l:max == l:blue 276 | let l:hue = (l:red - l:green) / l:diff + 4 277 | endif 278 | let l:hue = l:hue / 6 279 | endif 280 | let l:has_alpha = len(a:vals) == 4 281 | 282 | let l:formatted_vars = [float2nr(round(l:hue * 360)), float2nr(round(l:saturation * 100)), float2nr(round(l:lightness * 100))] 283 | if l:has_alpha == 1 284 | call add(l:formatted_vars, a:vals[3]) 285 | endif 286 | 287 | return 'hsl'.(l:has_alpha == 1 ? 'a' : '').'('.join(map(l:formatted_vars, {idx, val -> idx == 0 || idx == 3 ? string(val): printf('%d%%', val)}), ', ').')' 288 | endfunction 289 | 290 | function! s:FormatHSLAString(vals) abort 291 | let l:vals = a:vals 292 | if len(l:vals) == 3 293 | call add(l:vals, 1) 294 | endif 295 | let l:Formatter = get(s:Formatters, 'hsl') 296 | return l:Formatter(l:vals) 297 | endfunction 298 | 299 | let s:Formatters = { 300 | \ 'hex': function('s:FormatHEXString'), 301 | \ 'hex_num': function('s:FormatHEXNum'), 302 | \ 'hexa': function('s:FormatHEXAString'), 303 | \ 'rgb': function('s:FormatDefaultRGBString'), 304 | \ 'rgb_int': function('s:FormatRGBIntString'), 305 | \ 'rgb_float': function('s:FormatRGBFloatString'), 306 | \ 'rgba': function('s:FormatRGBAString'), 307 | \ 'hsl': function('s:FormatHSLString'), 308 | \ 'hsla': function('s:FormatHSLAString') 309 | \ } 310 | 311 | function! s:ConvertColor(str, format) abort 312 | let l:data = s:NormalizeString(a:str) 313 | if type(l:data) == type(v:null) 314 | echom 'No valid color to convert' 315 | return v:null 316 | endif 317 | 318 | " If the format is not specified, all -> hex, hex -> rgb 319 | " The reason hsl isn't in here is because it can be destructive, 320 | " it's also not commonly used 321 | let l:format = a:format 322 | let l:Formatter = get(s:Formatters, l:format, v:null) 323 | if l:Formatter == v:null 324 | if l:data['type'] != 'hex' 325 | let l:format = 'hex' 326 | let l:Formatter = get(s:Formatters, 'hex') 327 | else 328 | let l:format = 'rgb' 329 | let l:Formatter = get(s:Formatters, 'rgb') 330 | endif 331 | endif 332 | 333 | let l:data['color_string'] = l:Formatter(l:data['normalized_data']) 334 | return l:data 335 | endfunction 336 | 337 | function! s:GetContentSelection(line_start, line_end) abort 338 | let l:lines = getline(a:line_start, a:line_end) 339 | if len(l:lines) == 0 340 | return v:null 341 | endif 342 | 343 | let l:is_multiline = len(l:lines) > 1 344 | if is_multiline 345 | let l:column_start = getpos("'<")[2] 346 | let l:column_end = getpos("'>")[2] 347 | let l:lines[-1] = l:lines[-1][: l:column_end - (&selection == 'inclusive' ? 1 : 2)] 348 | let l:lines[0] = l:lines[0][l:column_start - 1:] 349 | else 350 | let l:column_start = 0 351 | let l:column_end = len(l:lines[0]) 352 | endif 353 | 354 | return { 355 | \ 'selected_text': join(l:lines, "\n"), 356 | \ 'range': l:is_multiline ? "'<,'>" : a:line_start.','.a:line_end, 357 | \ 'row': a:line_start, 358 | \ 'column': l:column_start 359 | \} 360 | endfunction 361 | 362 | function! ConvertColorTo(...) range abort 363 | if !&modifiable 364 | echomsg 'Error: Cannot modify current file.' 365 | return 366 | endif 367 | 368 | let l:type = len(a:000) > 0 ? a:000['0'] : v:null 369 | 370 | " If calling with 2 args, then just return the new color and log the 371 | " conversion since we are assuming the user just wants display output or 372 | " evaluate to the expression register 373 | if len(a:000) == 2 374 | let l:converted_data = s:ConvertColor(a:000['1'], l:type) 375 | if type(l:converted_data) == type(v:null) 376 | " No error message because it should've already been displayed earlier 377 | return 378 | endif 379 | echom 'Converted '.l:converted_data['match'].' -> '.l:converted_data['color_string'] 380 | return l:converted_data['color_string'] 381 | endif 382 | 383 | let l:selection_data = s:GetContentSelection(a:firstline, a:lastline) 384 | let l:converted_data = s:ConvertColor(l:selection_data.selected_text, l:type) 385 | if type(l:converted_data) == type(v:null) 386 | " No error message because it should've already been displayed earlier 387 | return 388 | endif 389 | execute "silent ".l:selection_data.range."s/".escape(l:converted_data['match'], '/')."/".l:converted_data['color_string'] 390 | " Move cursor to the start of the chage... not quite working on multiline 391 | " selects tho 392 | call setpos('.', [0, l:selection_data.row, l:selection_data.column + l:converted_data['match_pos'], 0]) 393 | echom 'Converted '.l:converted_data['match'].' -> '.l:converted_data['color_string'] 394 | endfunction 395 | 396 | command! -nargs=* -range -complete=custom,s:CompleteConvertColorTo 397 | \ ConvertColorTo ,call ConvertColorTo() 398 | 399 | function! s:CompleteConvertColorTo(arg_lead, cmd_line, cursor_pos) 400 | return join(sort(keys(s:Formatters)), "\n") 401 | endfunction 402 | -------------------------------------------------------------------------------- /scratch.txt: -------------------------------------------------------------------------------- 1 | This file is just here for testing, plis ignore 2 | 3 | #ff0 4 | #ff0a 5 | #fff000 6 | #fff000aa 7 | hsl(.75turn, 60%, 70%) 8 | hsl(0, 100%, 50%) 9 | hsl(23grad, 60%, 70%) 10 | hsl(270 60% 50% / .15) 11 | hsl(270 60% 50% / 15%) 12 | hsl(270 60% 70%) 13 | hsl(270, 60%, 50%, .15) 14 | hsl(270, 60%, 50%, 15%) 15 | hsl(270, 60%, 70%) 16 | hsl(270, 60%, 70%) 17 | hsl(270,60%,70%) 18 | hsl(270,60%,70%) 19 | hsl(270deg, 60%, 70%) 20 | hsl(4.71239rad, 60%, 70%) 21 | hsl(56, 100%, 50%) 22 | rgb(127, 51, 204) 23 | rgb(244 , 244,2 ) 24 | rgb(244, 244, 2) 25 | rgb(244,244,2) 26 | rgb(26, 77, 77) 27 | rgba( 0, 0, 0, 0.3) 28 | rgba( 255, 151, 154, 0.3) 29 | rgba(0.2 , 0.3,0.1 , 1) 30 | rgba(127, 51, 204, 0.15) 31 | rgba(244 , 244,2 , 1) 32 | rgba(244,244,2,0.4) 33 | rgba(255, 240, 0, 0.666667) 34 | rgba(33, 33, 33, 0.2) 35 | 36 | invalid lines 37 | #zzaacc 38 | --------------------------------------------------------------------------------