From 6c3d4e4b4ce8a4eb0b5ac012e38ca1f66af4b077 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Fri, 18 Oct 2019 14:27:02 -0700 Subject: Update color functions (#13) - Add hsl, hsla, rgba, RRGGBBAA - Redo the parser for rgb,rgba,hsl,hsla codes. - Fix the trie implementation. - Bugfix and refactor alpha/error handling --- README.md | 19 ++-- doc/colorizer-lua.txt | 17 ++-- lua/colorizer.lua | 246 +++++++++++++++++++++++++++++++++++++++++--------- lua/trie.lua | 40 ++++---- 4 files changed, 245 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index cf25c87..a0f499d 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,16 @@ Additionally, having a Lua API that's available means users can use this as a li ```lua DEFAULT_OPTIONS = { - RGB = true; -- #RGB hex codes - RRGGBB = true; -- #RRGGBB hex codes - names = true; -- "Name" codes like Blue - rgb_fn = false; -- CSS rgb() and rgba() functions - hsl_fn = false; -- CSS hsl() and hsla() functions - css = false; -- Enable all features above. - css_fn = false; -- Enable all CSS *functions*: rgb_fn, hsl_fn - -- Available modes: foreground, background - mode = 'background'; -- Set the display mode. + RGB = true; -- #RGB hex codes + RRGGBB = true; -- #RRGGBB hex codes + names = true; -- "Name" codes like Blue + RRGGBBAA = false; -- #RRGGBBAA hex codes + rgb_fn = false; -- CSS rgb() and rgba() functions + hsl_fn = false; -- CSS hsl() and hsla() functions + css = false; -- Enable all CSS features: rgb_fn, hsl_fn, names, RGB, RRGGBB + css_fn = false; -- Enable all CSS *functions*: rgb_fn, hsl_fn + -- Available modes: foreground, background + mode = 'background'; -- Set the display mode. } ``` diff --git a/doc/colorizer-lua.txt b/doc/colorizer-lua.txt index 9f2afda..39a97a2 100644 --- a/doc/colorizer-lua.txt +++ b/doc/colorizer-lua.txt @@ -50,15 +50,16 @@ Assumes the module is imported as `colorizer` > DEFAULT_OPTIONS = { - RGB = true; -- #RGB hex codes - RRGGBB = true; -- #RRGGBB hex codes - names = true; -- "Name" codes like Blue - rgb_fn = false; -- CSS rgb() and rgba() functions - hsl_fn = false; -- CSS hsl() and hsla() functions - css = false; -- Enable all features above. - css_fn = false; -- Enable all CSS *functions*: rgb_fn, hsl_fn + RGB = true; -- #RGB hex codes + RRGGBB = true; -- #RRGGBB hex codes + names = true; -- "Name" codes like Blue + RRGGBBAA = false; -- #RRGGBBAA hex codes + rgb_fn = false; -- CSS rgb() and rgba() functions + hsl_fn = false; -- CSS hsl() and hsla() functions + css = false; -- Enable all CSS features: rgb_fn, hsl_fn, names, RGB, RRGGBB + css_fn = false; -- Enable all CSS *functions*: rgb_fn, hsl_fn -- Available modes: foreground, background - mode = 'background'; -- Set the display mode. + mode = 'background'; -- Set the display mode. } < diff --git a/lua/colorizer.lua b/lua/colorizer.lua index 32717e5..22e28c8 100644 --- a/lua/colorizer.lua +++ b/lua/colorizer.lua @@ -16,6 +16,12 @@ local DEFAULT_NAMESPACE = nvim.create_namespace 'colorizer' local COLOR_MAP local COLOR_TRIE +local CSS_FUNCTION_TRIE = Trie() +for _, v in ipairs{'rgb', 'rgba', 'hsl', 'hsla'} do CSS_FUNCTION_TRIE:insert(v) end +local RGB_FUNCTION_TRIE = Trie() +for _, v in ipairs{'rgb', 'rgba'} do RGB_FUNCTION_TRIE:insert(v) end +local HSL_FUNCTION_TRIE = Trie() +for _, v in ipairs{'hsl', 'hsla'} do HSL_FUNCTION_TRIE:insert(v) end --- Setup the COLOR_MAP and COLOR_TRIE local function initialize_trie() @@ -54,15 +60,16 @@ local function color_is_bright(r, g, b) end local DEFAULT_OPTIONS = { - RGB = true; -- #RGB hex codes - RRGGBB = true; -- #RRGGBB hex codes - names = true; -- "Name" codes like Blue - rgb_fn = false; -- CSS rgb() and rgba() functions - hsl_fn = false; -- CSS hsl() and hsla() functions - css = false; -- Enable all CSS features: rgb_fn, hsl_fn, names, RGB, RRGGBB - css_fn = false; -- Enable all CSS *functions*: rgb_fn, hsl_fn + RGB = true; -- #RGB hex codes + RRGGBB = true; -- #RRGGBB hex codes + names = true; -- "Name" codes like Blue + RRGGBBAA = false; -- #RRGGBBAA hex codes + rgb_fn = false; -- CSS rgb() and rgba() functions + hsl_fn = false; -- CSS hsl() and hsla() functions + css = false; -- Enable all CSS features: rgb_fn, hsl_fn, names, RGB, RRGGBB + css_fn = false; -- Enable all CSS *functions*: rgb_fn, hsl_fn -- Available modes: foreground, background - mode = 'background'; -- Set the display mode. + mode = 'background'; -- Set the display mode. } local HIGHLIGHT_NAME_PREFIX = "colorizer" @@ -118,6 +125,158 @@ local SETUP_SETTINGS = { default_options = DEFAULT_OPTIONS; } +local function name_parser(line, i) + local prefix = COLOR_TRIE:longest_prefix(line:sub(i)) + if prefix then + local rgb = COLOR_MAP[prefix] + local rgb_hex = bit.tohex(rgb):sub(-6) + return #prefix, rgb_hex + end +end + +local css_fn = {} + +local css_rgb_fn_minimum_length = #'rgb(0,0,0)' - 1 +local css_rgba_fn_minimum_length = #'rgba(0,0,0,0)' - 1 +local css_hsl_fn_minimum_length = #'hsl(0,0%,0%)' - 1 +local css_hsla_fn_minimum_length = #'hsla(0,0%,0%,0)' - 1 + +local function percent_or_hex(v) + if v:sub(-1,-1) == "%" then + return tonumber(v:sub(1,-2))/100*255 + end + local x = tonumber(v) + if x > 255 then return end + return x +end + +function css_fn.rgb(line, i) + if #line < i + css_rgb_fn_minimum_length then return end + -- TODO this might be able to be improved. + local r, g, b, match_end = line:sub(i):match("^rgb%(%s*(%d+%%?)%s*,%s*(%d+%%?)%s*,%s*(%d+%%?)%s*%)()") + if not r then return end + r = percent_or_hex(r) if not r then return end + g = percent_or_hex(g) if not g then return end + b = percent_or_hex(b) if not b then return end + local rgb_hex = ("%02x%02x%02x"):format(r,g,b) + if #rgb_hex ~= 6 then return end + return match_end - 1, rgb_hex +end + +-- Pattern for rgba() functions from CSS +function css_fn.rgba(line, i) + if #line < i + css_rgba_fn_minimum_length then return end + -- TODO this might be able to be improved. + local r, g, b, a, match_end = line:sub(i):match("^rgba%(%s*(%d+%%?)%s*,%s*(%d+%%?)%s*,%s*(%d+%%?)%s*,%s*([.%d]+)%s*%)()") + if not r then return end + a = tonumber(a) if not a or a > 1 then return end + r = percent_or_hex(r) if not r then return end + g = percent_or_hex(g) if not g then return end + b = percent_or_hex(b) if not b then return end + -- TODO this might not be the best approach to alpha channel. + -- Things like pumblend might be useful here. + r, g, b = r*a, g*a, b*a + r, g, b = math.floor(r), math.floor(g), math.floor(b) + local rgb_hex = ("%02x%02x%02x"):format(r,g,b) + if #rgb_hex ~= 6 then + return + end + return match_end - 1, rgb_hex +end + +-- https://gist.github.com/mjackson/5311256 +local function hue_to_rgb(p, q, t) + if t < 0 then t = t + 1 end + if t > 1 then t = t - 1 end + if t < 1/6 then return p + (q - p) * 6 * t end + if t < 1/2 then return q end + if t < 2/3 then return p + (q - p) * (2/3 - t) * 6 end + return p +end + +local function hsl_to_rgb(h, s, l) + if h > 1 or s > 1 or l > 1 then return end + if s == 0 then + local r = l * 255 + return r, r, r + end + + local q + if l < 0.5 then + q = l * (1 + s) + else + q = l + s - l * s + end + local p = 2 * l - q + return 255*hue_to_rgb(p, q, h + 1/3), 255*hue_to_rgb(p, q, h), 255*hue_to_rgb(p, q, h - 1/3) +end + +function css_fn.hsl(line, i) + if #line < i + css_hsl_fn_minimum_length then return end + -- TODO this might be able to be improved. + local h, s, l, match_end = line:sub(i):match("^hsl%(%s*(%d+)%s*,%s*(%d+)%%%s*,%s*(%d+)%%%s*%)()") + if not h then return end + h = tonumber(h) if h > 360 then return end + s = tonumber(s) if s > 100 then return end + l = tonumber(l) if l > 100 then return end + local r, g, b = hsl_to_rgb(h/360, s/100, l/100) + if r == nil or g == nil or b == nil then return end + local rgb_hex = ("%02x%02x%02x"):format(math.floor(r), math.floor(g), math.floor(b)) + if #rgb_hex ~= 6 then return end + return match_end-1, rgb_hex +end + +function css_fn.hsla(line, i) + if #line < i + css_hsla_fn_minimum_length then return end + -- TODO this might be able to be improved. + local h, s, l, a, match_end = line:sub(i):match("^hsla%(%s*(%d+)%s*,%s*(%d+)%%%s*,%s*(%d+)%%%s*,%s*([.%d]+)%s*%)()") + if not h then return end + a = tonumber(a) if not a or a > 1 then return end + h = tonumber(h) if h > 360 then return end + s = tonumber(s) if s > 100 then return end + l = tonumber(l) if l > 100 then return end + local r, g, b = hsl_to_rgb(h/360, s/100, l/100) + if r == nil or g == nil or b == nil then return end + local rgb_hex = ("%02x%02x%02x"):format(math.floor(r*a), math.floor(g*a), math.floor(b*a)) + if #rgb_hex ~= 6 then return end + return match_end-1, rgb_hex +end + +local function css_function_parser(line, i) + local prefix = CSS_FUNCTION_TRIE:longest_prefix(line:sub(i)) + if prefix then + return css_fn[prefix](line, i) + end +end + +local function rgb_function_parser(line, i) + local prefix = RGB_FUNCTION_TRIE:longest_prefix(line:sub(i)) + if prefix then + return css_fn[prefix](line, i) + end +end + +local function hsl_function_parser(line, i) + local prefix = HSL_FUNCTION_TRIE:longest_prefix(line:sub(i)) + if prefix then + return css_fn[prefix](line, i) + end +end + +local function compile_matcher(matchers) + local parse_fn = matchers[1] + for j = 2, #matchers do + local old_parse_fn = parse_fn + local new_parse_fn = matchers[j] + parse_fn = function(line, i) + local length, rgb_hex = new_parse_fn(line, i) + if length then return length, rgb_hex end + return old_parse_fn(line, i) + end + end + return parse_fn +end + --[[-- Highlight the buffer region. Highlight starting from `line_start` (0-indexed) for each line described by `lines` in the buffer `buf` and attach it to the namespace `ns`. @@ -130,13 +289,26 @@ buffer `buf` and attach it to the namespace `ns`. @see setup ]] local function highlight_buffer(buf, ns, lines, line_start, options) - local enable_names = options.names - local enable_RGB = options.css or options.RGB - local enable_RRGGBB = options.css or options.RRGGBB - local enable_rgb = options.css or options.css_fns or options.rgb_fn - local enable_rgba = options.css or options.css_fns or options.rgb_fn - local enable_hsl = options.css or options.css_fns or options.hsl_fn - local enable_hsla = options.css or options.css_fns or options.hsl_fn + local enable_names = options.names + local enable_RGB = options.css or options.RGB + local enable_RRGGBB = options.css or options.RRGGBB + local enable_RRGGBBAA = options.css or options.RRGGBBAA + local enable_rgb = options.css or options.css_fns or options.rgb_fn + local enable_hsl = options.css or options.css_fns or options.hsl_fn + + local loop_parse_fn + local loop_matchers = {} + if enable_names then table.insert(loop_matchers, name_parser) end + if enable_rgb and enable_hsl then + table.insert(loop_matchers, css_function_parser) + elseif enable_rgb then + table.insert(loop_matchers, rgb_function_parser) + elseif enable_hsl then + table.insert(loop_matchers, hsl_function_parser) + end + if #loop_matchers > 0 then + loop_parse_fn = compile_matcher(loop_matchers) + end -- TODO do I have to put this here? initialize_trie() ns = ns or DEFAULT_NAMESPACE @@ -147,21 +319,6 @@ local function highlight_buffer(buf, ns, lines, line_start, options) local highlight_name = create_highlight(rgb_hex, options) nvim_buf_add_highlight(buf, ns, highlight_name, current_linenum, match_start-1, match_end-1) end - if enable_rgb then - -- TODO this can have improved performance by either reusing my trie or - -- doing a byte comp. - -- Pattern for rgb() functions from CSS - line:gsub("()rgb%(%s*(%d+%%?)%s*,%s*(%d+%%?)%s*,%s*(%d+%%?)%s*%)()", function(match_start, r,g,b, match_end) - if r:sub(-1,-1) == "%" then r = math.floor(r:sub(1,-2)/100*255) end - if g:sub(-1,-1) == "%" then g = math.floor(g:sub(1,-2)/100*255) end - if b:sub(-1,-1) == "%" then b = math.floor(b:sub(1,-2)/100*255) end - local rgb_hex = ("%02x%02x%02x"):format(r,g,b) - if #rgb_hex ~= 6 then - return - end - highlight_line_rgb_hex(match_start, rgb_hex, match_end) - end) - end if enable_RGB then -- Pattern for #RGB, part 1. No trailing characters allowed line:gsub("()#([%da-fA-F][%da-fA-F][%da-fA-F])()%W", highlight_line_rgb_hex) @@ -172,17 +329,26 @@ local function highlight_buffer(buf, ns, lines, line_start, options) -- Pattern for #RRGGBB line:gsub("()#([%da-fA-F][%da-fA-F][%da-fA-F][%da-fA-F][%da-fA-F][%da-fA-F])()", highlight_line_rgb_hex) end - if enable_names then + if enable_RRGGBBAA then + -- Pattern for #RRGGBB + line:gsub("()#([%da-fA-F][%da-fA-F])([%da-fA-F][%da-fA-F])([%da-fA-F][%da-fA-F])([%da-fA-F][%da-fA-F])()", function(match_start, r, g, b, a, match_end) + a = tonumber(a, 16) if a > 255 then return end + r = tonumber(r, 16) if r > 255 then return end + g = tonumber(g, 16) if g > 255 then return end + b = tonumber(b, 16) if b > 255 then return end + a = a / 255 + local rgb_hex = ("%02x%02x%02x"):format(math.floor(r*a), math.floor(g*a), math.floor(b*a)) + if #rgb_hex ~= 6 then return end + highlight_line_rgb_hex(match_start, rgb_hex, match_end) + end) + end + if loop_parse_fn then local i = 1 while i < #line do - -- TODO skip if the remaining length is less than the shortest length - -- of an entry in our trie. - local prefix = COLOR_TRIE:longest_prefix(line:sub(i)) - if prefix then - local rgb = COLOR_MAP[prefix] - local rgb_hex = bit.tohex(rgb):sub(-6) - highlight_line_rgb_hex(i, rgb_hex, i+#prefix) - i = i + #prefix + local length, rgb_hex = loop_parse_fn(line, i) + if length then + highlight_line_rgb_hex(i, rgb_hex, i+length) + i = i + length else i = i + 1 end @@ -238,7 +404,6 @@ local function attach_to_buffer(buf, options) nvim_buf_clear_namespace(buf, ns, firstline, new_lastline) local lines = nvim_buf_get_lines(buf, firstline, new_lastline, true) highlight_buffer(buf, ns, lines, firstline, BUFFER_OPTIONS[buf]) --- highlight_buffer(buf, ns, lines, firstline, BUFFER_OPTIONS[buf] or options) end; on_detach = function() BUFFER_OPTIONS[buf] = nil @@ -356,6 +521,5 @@ return { highlight_buffer = highlight_buffer; reload_all_buffers = reload_all_buffers; get_buffer_options = get_buffer_options; - -- initialize = initialize_trie; } diff --git a/lua/trie.lua b/lua/trie.lua index b0751aa..e74d9eb 100644 --- a/lua/trie.lua +++ b/lua/trie.lua @@ -30,6 +30,7 @@ void free(void *ptr); local Trie_t = ffi.typeof('struct Trie') local Trie_ptr_t = ffi.typeof('$ *', Trie_t) +local Trie_size = ffi.sizeof(Trie_t) local function byte_to_index(b) -- 0-9 starts at string.byte('0') == 0x30 == 48 == 0b0011_0000 @@ -75,8 +76,6 @@ local function verify_byte_to_index() end end -local Trie_size = ffi.sizeof(Trie_t) - local function new_trie() local ptr = ffi.C.malloc(Trie_size) ffi.fill(ptr, Trie_size) @@ -237,6 +236,9 @@ local function print_structure(s) end local function free_trie(trie) + if trie == nil then + return + end for i = 0, 61 do local child = trie.character[i] if child ~= nil then @@ -247,24 +249,24 @@ local function free_trie(trie) end local Trie_mt = { - __index = { - insert = insert; - search = search; - longest_prefix = longest_prefix; - }; - __tostring = function(trie) - local structure = trie_structure(trie) - if structure then - return table.concat(print_structure(structure), '\n') - else - return 'nil' - end - end; - __gc = free_trie; - } -local Trie = ffi.metatype(Trie_t, Trie_mt) + __new = new_trie; + __index = { + insert = insert; + search = search; + longest_prefix = longest_prefix; + }; + __tostring = function(trie) + local structure = trie_structure(trie) + if structure then + return table.concat(print_structure(structure), '\n') + else + return 'nil' + end + end; + __gc = free_trie; +} -return Trie +return ffi.metatype('struct Trie', Trie_mt) -- local tests = { -- "cat"; -- cgit v1.2.3-70-g09d2