From 83ddc656b3455f066196c7f535402ec7ee4f34de Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Fri, 18 Oct 2019 21:51:56 -0700 Subject: Improve algorithm performance and correctness. (#15) - Disable highlighting for HEX codes and NAME codes when preceded by alphanumeric codes. - Hand written parser with 1 pass parsing. - Update Trie to be more ergonomic. - Add test file with expected outputs for edge cases. - Cache matcher creation and refactor. - Refactor Faster than ever baby --- lua/colorizer.lua | 525 ++++++++++++++++++++++++++++++--------------------- lua/trie.lua | 96 +++++----- test/expectation.txt | 45 +++++ 3 files changed, 405 insertions(+), 261 deletions(-) create mode 100644 test/expectation.txt diff --git a/lua/colorizer.lua b/lua/colorizer.lua index 22e28c8..f127b19 100644 --- a/lua/colorizer.lua +++ b/lua/colorizer.lua @@ -2,26 +2,19 @@ -- @module colorizer local nvim = require 'nvim' local Trie = require 'trie' +local bit = require 'bit' +local ffi = require 'ffi' local nvim_buf_add_highlight = vim.api.nvim_buf_add_highlight -local nvim_get_current_buf = vim.api.nvim_get_current_buf -local nvim_buf_get_lines = vim.api.nvim_buf_get_lines local nvim_buf_clear_namespace = vim.api.nvim_buf_clear_namespace - ---- Default namespace used in `highlight_buffer` and `attach_to_buffer`. --- The name is "terminal_highlight" --- @see highlight_buffer --- @see attach_to_buffer -local DEFAULT_NAMESPACE = nvim.create_namespace 'colorizer' +local nvim_buf_get_lines = vim.api.nvim_buf_get_lines +local nvim_get_current_buf = vim.api.nvim_get_current_buf +local band, lshift, bor, tohex = bit.band, bit.lshift, bit.bor, bit.tohex +local rshift = bit.rshift +local floor = math.floor 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() @@ -29,7 +22,8 @@ local function initialize_trie() COLOR_MAP = nvim.get_color_map() COLOR_TRIE = Trie() - for k in pairs(COLOR_MAP) do + for k, v in pairs(COLOR_MAP) do + COLOR_MAP[k] = tohex(v, 6) COLOR_TRIE:insert(k) end end @@ -46,19 +40,6 @@ local function merge(...) return res end ---- Determine whether to use black or white text --- Ref: https://stackoverflow.com/a/1855903/837964 --- https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color -local function color_is_bright(r, g, b) - -- Counting the perceptive luminance - human eye favors green color - local luminance = (0.299*r + 0.587*g + 0.114*b)/255 - if luminance > 0.5 then - return true -- Bright colors, black font - else - return false -- Dark colors, white font - end -end - local DEFAULT_OPTIONS = { RGB = true; -- #RGB hex codes RRGGBB = true; -- #RRGGBB hex codes @@ -72,74 +53,48 @@ local DEFAULT_OPTIONS = { mode = 'background'; -- Set the display mode. } -local HIGHLIGHT_NAME_PREFIX = "colorizer" -local MODE_NAMES = { - background = 'mb'; - foreground = 'mf'; -} - -local HIGHLIGHT_CACHE = {} - ---- Make a deterministic name for a highlight given these attributes -local function make_highlight_name(rgb, mode) - return table.concat({HIGHLIGHT_NAME_PREFIX, MODE_NAMES[mode], rgb}, '_') -end - -local function create_highlight(rgb_hex, options) - local mode = options.mode or 'background' - -- TODO validate rgb format? - rgb_hex = rgb_hex:lower() - local cache_key = table.concat({MODE_NAMES[mode], rgb_hex}, "_") - local highlight_name = HIGHLIGHT_CACHE[cache_key] - -- Look up in our cache. - if not highlight_name then - if #rgb_hex == 3 then - rgb_hex = table.concat { - rgb_hex:sub(1,1):rep(2); - rgb_hex:sub(2,2):rep(2); - rgb_hex:sub(3,3):rep(2); - } +-- Create a lookup table where the bottom 4 bits are used to indicate the +-- category and the top 4 bits are the hex value of the ASCII byte. +local BYTE_CATEGORY = ffi.new 'uint8_t[256]' +local CATEGORY_DIGIT = lshift(1, 0); +local CATEGORY_ALPHA = lshift(1, 1); +local CATEGORY_HEX = lshift(1, 2); +local CATEGORY_ALPHANUM = bor(CATEGORY_ALPHA, CATEGORY_DIGIT) +do + local b = string.byte + for i = 0, 255 do + local v = 0 + -- Digit is bit 1 + if i >= b'0' and i <= b'9' then + v = bor(v, lshift(1, 0)) + v = bor(v, lshift(1, 2)) + v = bor(v, lshift(i - b'0', 4)) end - -- Create the highlight - highlight_name = make_highlight_name(rgb_hex, mode) - if mode == 'foreground' then - nvim.ex.highlight(highlight_name, "guifg=#"..rgb_hex) - else - local r, g, b = rgb_hex:sub(1,2), rgb_hex:sub(3,4), rgb_hex:sub(5,6) - r, g, b = tonumber(r,16), tonumber(g,16), tonumber(b,16) - local fg_color - if color_is_bright(r,g,b) then - fg_color = "Black" - else - fg_color = "White" + local lowercase = bor(i, 0x20) + -- Alpha is bit 2 + if lowercase >= b'a' and lowercase <= b'z' then + v = bor(v, lshift(1, 1)) + if lowercase <= b'f' then + v = bor(v, lshift(1, 2)) + v = bor(v, lshift(lowercase - b'a'+10, 4)) end - nvim.ex.highlight(highlight_name, "guifg="..fg_color, "guibg=#"..rgb_hex) end - HIGHLIGHT_CACHE[cache_key] = highlight_name + BYTE_CATEGORY[i] = v end - return highlight_name end -local SETUP_SETTINGS = { - exclusions = {}; - 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 +local function byte_is_hex(byte) + return band(BYTE_CATEGORY[byte], CATEGORY_HEX) ~= 0 end -local css_fn = {} +local function byte_is_alphanumeric(byte) + local category = BYTE_CATEGORY[byte] + return band(category, CATEGORY_ALPHANUM) ~= 0 +end -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 parse_hex(b) + return rshift(BYTE_CATEGORY[b], 4) +end local function percent_or_hex(v) if v:sub(-1,-1) == "%" then @@ -150,38 +105,17 @@ local function percent_or_hex(v) 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 +--- Determine whether to use black or white text +-- Ref: https://stackoverflow.com/a/1855903/837964 +-- https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color +local function color_is_bright(r, g, b) + -- Counting the perceptive luminance - human eye favors green color + local luminance = (0.299*r + 0.587*g + 0.114*b)/255 + if luminance > 0.5 then + return true -- Bright colors, black font + else + return false -- Dark colors, white font end - return match_end - 1, rgb_hex end -- https://gist.github.com/mjackson/5311256 @@ -200,7 +134,6 @@ local function hsl_to_rgb(h, s, l) local r = l * 255 return r, r, r end - local q if l < 0.5 then q = l * (1 + s) @@ -211,55 +144,144 @@ local function hsl_to_rgb(h, s, l) 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)) +local function name_parser(line, i) + if i > 1 and byte_is_alphanumeric(line:byte(i-1)) then + return + end + local prefix = COLOR_TRIE:longest_prefix(line:sub(i)) if prefix then - return css_fn[prefix](line, i) + -- Check if there is a letter here so as to disallow matching here. + -- Take the Blue out of Blueberry + -- Line end or non-letter. + local next_byte_index = i + #prefix + if #line >= next_byte_index and byte_is_alphanumeric(line:byte(next_byte_index)) then + return + end + return #prefix, COLOR_MAP[prefix] 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) +local b_hash = ("#"):byte() +local function rgb_hex_parser(line, i, minlen, maxlen) + if i > 1 and byte_is_alphanumeric(line:byte(i-1)) then + return end + if line:byte(i) ~= b_hash then + return + end + local j = i + 1 + if #line < j + minlen - 1 then return end + local n = j + maxlen + local alpha + local v = 0 + while j <= math.min(n, #line) do + local b = line:byte(j) + if not byte_is_hex(b) then break end + if j - i >= 7 then + alpha = parse_hex(b) + lshift(alpha or 0, 4) + else + v = parse_hex(b) + lshift(v, 4) + end + j = j + 1 + end + if #line >= j and byte_is_alphanumeric(line:byte(j)) then + return + end + local length = j - i + if length ~= 4 and length ~= 7 and length ~= 9 then return end + if alpha then + alpha = tonumber(alpha)/255 + local r = floor(band(v, 0xFF)*alpha) + local g = floor(band(rshift(v, 8), 0xFF)*alpha) + local b = floor(band(rshift(v, 16), 0xFF)*alpha) + v = bor(lshift(r, 16), lshift(g, 8), b) + return 9, tohex(v, 6) + end + return length, line:sub(i+1, i+length-1) 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) +-- TODO consider removing the regexes here +-- TODO this might not be the best approach to alpha channel. +-- Things like pumblend might be useful here. +local css_fn = {} +do + 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 + function css_fn.rgb(line, i) + if #line < i + css_rgb_fn_minimum_length then return end + local r, g, b, match_end = line:sub(i):match("^rgb%(%s*(%d+%%?)%s*,%s*(%d+%%?)%s*,%s*(%d+%%?)%s*%)()") + if not match_end 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 + function css_fn.hsl(line, i) + if #line < i + css_hsl_fn_minimum_length then return end + local h, s, l, match_end = line:sub(i):match("^hsl%(%s*(%d+)%s*,%s*(%d+)%%%s*,%s*(%d+)%%%s*%)()") + if not match_end 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(floor(r), floor(g), floor(b)) + if #rgb_hex ~= 6 then return end + return match_end - 1, rgb_hex + end + function css_fn.rgba(line, i) + if #line < i + css_rgba_fn_minimum_length then return end + 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 match_end 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 + local rgb_hex = ("%02x%02x%02x"):format(floor(r*a), floor(g*a), floor(b*a)) + 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 + 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 match_end 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(floor(r*a), floor(g*a), floor(b*a)) + if #rgb_hex ~= 6 then return end + return match_end - 1, rgb_hex + end +end +local css_function_parser, rgb_function_parser, hsl_function_parser +do + local CSS_FUNCTION_TRIE = Trie {'rgb', 'rgba', 'hsl', 'hsla'} + local RGB_FUNCTION_TRIE = Trie {'rgb', 'rgba'} + local HSL_FUNCTION_TRIE = Trie {'hsl', 'hsla'} + css_function_parser = function(line, i) + local prefix = CSS_FUNCTION_TRIE:longest_prefix(line:sub(i)) + if prefix then + return css_fn[prefix](line, i) + end + end + rgb_function_parser = function(line, i) + local prefix = RGB_FUNCTION_TRIE:longest_prefix(line:sub(i)) + if prefix then + return css_fn[prefix](line, i) + end + end + hsl_function_parser = function(line, i) + local prefix = HSL_FUNCTION_TRIE:longest_prefix(line:sub(i)) + if prefix then + return css_fn[prefix](line, i) + end end end @@ -277,28 +299,104 @@ local function compile_matcher(matchers) 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`. +--- Default namespace used in `highlight_buffer` and `attach_to_buffer`. +-- The name is "terminal_highlight" +-- @see highlight_buffer +-- @see attach_to_buffer +local DEFAULT_NAMESPACE = nvim.create_namespace "colorizer" +local HIGHLIGHT_NAME_PREFIX = "colorizer" +local HIGHLIGHT_MODE_NAMES = { + background = "mb"; + foreground = "mf"; +} +local HIGHLIGHT_CACHE = {} -@tparam integer buf buffer id. -@tparam[opt=DEFAULT_NAMESPACE] integer ns the namespace id. Create it with `vim.api.create_namespace` -@tparam {string,...} lines the lines to highlight from the buffer. -@tparam integer line_start should be 0-indexed -@param options Configuration options as described in `setup` -@see setup -]] -local function highlight_buffer(buf, ns, lines, line_start, options) - local enable_names = options.names +--- Make a deterministic name for a highlight given these attributes +local function make_highlight_name(rgb, mode) + return table.concat({HIGHLIGHT_NAME_PREFIX, HIGHLIGHT_MODE_NAMES[mode], rgb}, '_') +end + +local function create_highlight(rgb_hex, options) + local mode = options.mode or 'background' + -- TODO validate rgb format? + rgb_hex = rgb_hex:lower() + local cache_key = table.concat({HIGHLIGHT_MODE_NAMES[mode], rgb_hex}, "_") + local highlight_name = HIGHLIGHT_CACHE[cache_key] + -- Look up in our cache. + if not highlight_name then + if #rgb_hex == 3 then + rgb_hex = table.concat { + rgb_hex:sub(1,1):rep(2); + rgb_hex:sub(2,2):rep(2); + rgb_hex:sub(3,3):rep(2); + } + end + -- Create the highlight + highlight_name = make_highlight_name(rgb_hex, mode) + if mode == 'foreground' then + nvim.ex.highlight(highlight_name, "guifg=#"..rgb_hex) + else + local r, g, b = rgb_hex:sub(1,2), rgb_hex:sub(3,4), rgb_hex:sub(5,6) + r, g, b = tonumber(r,16), tonumber(g,16), tonumber(b,16) + local fg_color + if color_is_bright(r,g,b) then + fg_color = "Black" + else + fg_color = "White" + end + nvim.ex.highlight(highlight_name, "guifg="..fg_color, "guibg=#"..rgb_hex) + end + HIGHLIGHT_CACHE[cache_key] = highlight_name + end + return highlight_name +end + +local MATCHER_CACHE = {} +local function make_matcher(options) + local enable_names = options.css or 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 matcher_key = bor( + lshift(enable_names and 1 or 0, 0), + lshift(enable_RGB and 1 or 0, 1), + lshift(enable_RRGGBB and 1 or 0, 2), + lshift(enable_RRGGBBAA and 1 or 0, 3), + lshift(enable_rgb and 1 or 0, 4), + lshift(enable_hsl and 1 or 0, 5)) + + if matcher_key == 0 then return end + + local loop_parse_fn = MATCHER_CACHE[matcher_key] + if loop_parse_fn then + return loop_parse_fn + end + local loop_matchers = {} - if enable_names then table.insert(loop_matchers, name_parser) end + if enable_names then + table.insert(loop_matchers, name_parser) + end + do + local valid_lengths = {[3] = enable_RGB, [6] = enable_RRGGBB, [8] = enable_RRGGBBAA} + local minlen, maxlen + for k, v in pairs(valid_lengths) do + if v then + minlen = math.min(k, minlen or 99) + maxlen = math.max(k, maxlen or 0) + end + end + if minlen then + table.insert(loop_matchers, function(line, i) + local length, rgb_hex = rgb_hex_parser(line, i, minlen, maxlen) + if length and valid_lengths[length-1] then + return length, rgb_hex + end + end) + end + end if enable_rgb and enable_hsl then table.insert(loop_matchers, css_function_parser) elseif enable_rgb then @@ -306,57 +404,52 @@ local function highlight_buffer(buf, ns, lines, line_start, options) 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 + loop_parse_fn = compile_matcher(loop_matchers) + MATCHER_CACHE[matcher_key] = loop_parse_fn + return loop_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`. + +@tparam integer buf buffer id. +@tparam[opt=DEFAULT_NAMESPACE] integer ns the namespace id. Create it with `vim.api.create_namespace` +@tparam {string,...} lines the lines to highlight from the buffer. +@tparam integer line_start should be 0-indexed +@param options Configuration options as described in `setup` +@see setup +]] +local function highlight_buffer(buf, ns, lines, line_start, options) -- TODO do I have to put this here? initialize_trie() ns = ns or DEFAULT_NAMESPACE + local loop_parse_fn = make_matcher(options) for current_linenum, line in ipairs(lines) do current_linenum = current_linenum - 1 + line_start -- Upvalues are options and current_linenum - local function highlight_line_rgb_hex(match_start, rgb_hex, match_end) - 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 - -- 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) - -- Pattern for #RGB, part 2. Ending code. - line:gsub("()#([%da-fA-F][%da-fA-F][%da-fA-F])()$", highlight_line_rgb_hex) - end - if enable_RRGGBB then - -- 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_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 - 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 + local i = 1 + while i < #line do + local length, rgb_hex = loop_parse_fn(line, i) + if length then + local highlight_name = create_highlight(rgb_hex, options) + nvim_buf_add_highlight(buf, ns, highlight_name, current_linenum, i-1, i+length-1) + i = i + length + else + i = i + 1 end end end end +--- +-- USER FACING FUNCTIONALITY +--- + +local SETUP_SETTINGS = { + exclusions = {}; + default_options = DEFAULT_OPTIONS; +} local BUFFER_OPTIONS = {} local FILETYPE_OPTIONS = {} @@ -478,7 +571,7 @@ local function setup(filetypes, default_options) nvim.err_writeln("colorizer: Invalid option type for filetype "..filetype) else options = merge(SETUP_SETTINGS.default_options, v) - assert(MODE_NAMES[options.mode or 'background'], "colorizer: Invalid mode: "..tostring(options.mode)) + assert(HIGHLIGHT_MODE_NAMES[options.mode or 'background'], "colorizer: Invalid mode: "..tostring(options.mode)) end else filetype = v diff --git a/lua/trie.lua b/lua/trie.lua index e74d9eb..9d84487 100644 --- a/lua/trie.lua +++ b/lua/trie.lua @@ -14,6 +14,7 @@ -- You should have received a copy of the GNU General Public License -- along with this program. If not, see . local ffi = require 'ffi' +local bit = require 'bit' local bnot = bit.bnot local band, bor, bxor = bit.band, bit.bor, bit.bxor @@ -76,32 +77,30 @@ local function verify_byte_to_index() end end -local function new_trie() +local function trie_create() local ptr = ffi.C.malloc(Trie_size) ffi.fill(ptr, Trie_size) return ffi.cast(Trie_ptr_t, ptr) end -local INDEX_LOOKUP_TABLE = ffi.new('uint8_t[256]') -local b_a = string.byte('a') -local b_z = string.byte('z') -local b_A = string.byte('A') -local b_Z = string.byte('Z') -local b_0 = string.byte('0') -local b_9 = string.byte('9') -for i = 0, 255 do - if i >= b_0 and i <= b_9 then - INDEX_LOOKUP_TABLE[i] = i - b_0 - elseif i >= b_A and i <= b_Z then - INDEX_LOOKUP_TABLE[i] = i - b_A + 10 - elseif i >= b_a and i <= b_z then - INDEX_LOOKUP_TABLE[i] = i - b_a + 10 + 26 - else - INDEX_LOOKUP_TABLE[i] = 255 +local INDEX_LOOKUP_TABLE = ffi.new 'uint8_t[256]' +local CHAR_LOOKUP_TABLE = ffi.new('char[62]', '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') +do + local b = string.byte + for i = 0, 255 do + if i >= b'0' and i <= b'9' then + INDEX_LOOKUP_TABLE[i] = i - b'0' + elseif i >= b'A' and i <= b'Z' then + INDEX_LOOKUP_TABLE[i] = i - b'A' + 10 + elseif i >= b'a' and i <= b'z' then + INDEX_LOOKUP_TABLE[i] = i - b'a' + 10 + 26 + else + INDEX_LOOKUP_TABLE[i] = 255 + end end end -local function insert(trie, value) +local function trie_insert(trie, value) if trie == nil then return false end local node = trie for i = 1, #value do @@ -110,7 +109,7 @@ local function insert(trie, value) return false end if node.character[index] == nil then - node.character[index] = new_trie() + node.character[index] = trie_create() end node = node.character[index] end @@ -118,7 +117,7 @@ local function insert(trie, value) return node, trie end -local function search(trie, value) +local function trie_search(trie, value) if trie == nil then return false end local node = trie for i = 1, #value do @@ -135,7 +134,7 @@ local function search(trie, value) return node.is_leaf end -local function longest_prefix(trie, value) +local function trie_longest_prefix(trie, value) if trie == nil then return false end local node = trie local last_i = nil @@ -158,19 +157,21 @@ local function longest_prefix(trie, value) end end +local function trie_extend(trie, t) + assert(type(t) == 'table') + for _, v in ipairs(t) do + trie_insert(trie, v) + end +end + --- Printing utilities local function index_to_char(index) - if index < 10 then - return string.char(index + b_0) - elseif index < 36 then - return string.char(index - 10 + b_A) - else - return string.char(index - 26 - 10 + b_a) - end + if index < 0 or index > 61 then return end + return CHAR_LOOKUP_TABLE[index] end -local function trie_structure(trie) +local function trie_as_table(trie) if trie == nil then return nil end @@ -178,7 +179,7 @@ local function trie_structure(trie) for i = 0, 61 do local child = trie.character[i] if child ~= nil then - local child_table = trie_structure(child) + local child_table = trie_as_table(child) child_table.c = index_to_char(i) table.insert(children, child_table) end @@ -189,10 +190,10 @@ local function trie_structure(trie) } end -local function print_structure(s) +local function print_trie_table(s) local mark if not s then - return nil + return {'nil'} end if s.c then if s.is_leaf then @@ -208,7 +209,7 @@ local function print_structure(s) end local lines = {} for _, child in ipairs(s.children) do - local child_lines = print_structure(child) + local child_lines = print_trie_table(child) for _, child_line in ipairs(child_lines) do table.insert(lines, child_line) end @@ -235,35 +236,40 @@ local function print_structure(s) return lines end -local function free_trie(trie) +local function trie_destroy(trie) if trie == nil then return end for i = 0, 61 do local child = trie.character[i] if child ~= nil then - free_trie(child) + trie_destroy(child) end end ffi.C.free(trie) end local Trie_mt = { - __new = new_trie; + __new = function(_, init) + local trie = trie_create() + if type(init) == 'table' then + trie_extend(trie, init) + end + return trie + end; __index = { - insert = insert; - search = search; - longest_prefix = longest_prefix; + insert = trie_insert; + search = trie_search; + longest_prefix = trie_longest_prefix; + extend = trie_extend; }; __tostring = function(trie) - local structure = trie_structure(trie) - if structure then - return table.concat(print_structure(structure), '\n') - else + if trie == nil then return 'nil' end + return table.concat(print_trie_table(trie_as_table(trie)), '\n') end; - __gc = free_trie; + __gc = trie_destroy; } return ffi.metatype('struct Trie', Trie_mt) @@ -305,4 +311,4 @@ return ffi.metatype('struct Trie', Trie_mt) -- end -- print(os.clock() - start) --- print(table.concat(print_structure(trie_structure(trie)), '\n')) +-- print(table.concat(print_trie_table(trie_as_table(trie)), '\n')) diff --git a/test/expectation.txt b/test/expectation.txt new file mode 100644 index 0000000..915d4de --- /dev/null +++ b/test/expectation.txt @@ -0,0 +1,45 @@ +-- vim:ft=lua +require'colorizer'.attach_to_buffer(0, {css=true}) + +--[[ SUCCESS +#F0F +#FF00FF +#FFF00F8F + #F0F + #FF00FF + #FFF00F8F + #F0F 1 + #FF00FF 1 + #FFF00F8F 1 +Blue Gray LightBlue Gray100 White +White +#def +#deadbeef +rgba(0,0,0,0) +rgb(0,0,0) +rgb(10, 100 , 100) +hsl(300,50%,50%) +hsla(300,50%,50%,0.5) +hsla(300,50%,50%,1.0000000000000001) +hsla(360,50%,50%,1.0000000000000001) +]] + +--[[ FAIL +#F0FF +#F0FFF +#F0FFF0F +#F0FFF0FFF +Blueberry Gray1000 BlueGree BlueGray +#define +#def0 +matcher#add +rgb(10,256,100) +rgb (10,255,100) +rgb(10, 1 00 , 100) +hsla(300,50%,50%,05) +hsla(300,50%,50%,1.000000000000001) +hsla(300,50%,50,1.0000000000000001) +hsla(300,50,50,1.0000000000000001) +hsla(361,50,50,1.0000000000000001) +]] + -- cgit v1.2.3-70-g09d2