aboutsummaryrefslogtreecommitdiff
path: root/lua/colorizer
diff options
context:
space:
mode:
Diffstat (limited to 'lua/colorizer')
-rw-r--r--lua/colorizer/buffer_utils.lua196
-rw-r--r--lua/colorizer/color_utils.lua393
-rw-r--r--lua/colorizer/matcher_utils.lua132
-rw-r--r--lua/colorizer/trie.lua27
-rw-r--r--lua/colorizer/utils.lua106
5 files changed, 843 insertions, 11 deletions
diff --git a/lua/colorizer/buffer_utils.lua b/lua/colorizer/buffer_utils.lua
new file mode 100644
index 0000000..0f8517f
--- /dev/null
+++ b/lua/colorizer/buffer_utils.lua
@@ -0,0 +1,196 @@
+---Helper functions to highlight buffer smartly
+--@module colorizer.buffer_utils
+local api = vim.api
+local buf_set_virtual_text = api.nvim_buf_set_extmark
+local buf_get_lines = api.nvim_buf_get_lines
+local create_namespace = api.nvim_create_namespace
+local clear_namespace = api.nvim_buf_clear_namespace
+local set_highlight = api.nvim_set_hl
+
+local color_utils = require "colorizer.color_utils"
+local color_is_bright = color_utils.color_is_bright
+
+local matcher_utils = require "colorizer.matcher_utils"
+local make_matcher = matcher_utils.make_matcher
+
+--- Default namespace used in `highlight_buffer` and `colorizer.attach_to_buffer`.
+-- @see highlight_buffer
+-- @see colorizer.attach_to_buffer
+local DEFAULT_NAMESPACE = create_namespace "colorizer"
+local HIGHLIGHT_NAME_PREFIX = "colorizer"
+--- Highlight mode which will be use to render the colour
+local HIGHLIGHT_MODE_NAMES = {
+ background = "mb",
+ foreground = "mf",
+ virtualtext = "mv",
+}
+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, 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 highlight_name then
+ return highlight_name
+ end
+
+ -- convert from #fff to #ffffff
+ 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
+ set_highlight(0, highlight_name, { fg = "#" .. rgb_hex })
+ else
+ local rr, gg, bb = rgb_hex:sub(1, 2), rgb_hex:sub(3, 4), rgb_hex:sub(5, 6)
+ local r, g, b = tonumber(rr, 16), tonumber(gg, 16), tonumber(bb, 16)
+ local fg_color
+ if color_is_bright(r, g, b) then
+ fg_color = "Black"
+ else
+ fg_color = "White"
+ end
+ set_highlight(0, highlight_name, { fg = fg_color, bg = "#" .. rgb_hex })
+ end
+ HIGHLIGHT_CACHE[cache_key] = highlight_name
+ return highlight_name
+end
+
+local function add_highlight(options, buf, ns, data)
+ if vim.tbl_contains({ "foreground", "background" }, options.mode) then
+ for linenr, hls in pairs(data) do
+ for _, hl in ipairs(hls) do
+ api.nvim_buf_add_highlight(buf, ns, hl.name, linenr, hl.range[1], hl.range[2])
+ end
+ end
+ elseif options.mode == "virtualtext" then
+ for linenr, hls in pairs(data) do
+ for _, hl in ipairs(hls) do
+ buf_set_virtual_text(0, ns, linenr, hl.range[2], {
+ end_col = hl.range[2],
+ virt_text = { { options.virtualtext or "■", hl.name } },
+ })
+ end
+ end
+ end
+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`.
+---@param buf number: buffer id
+---@param ns number: The namespace id. Default is DEFAULT_NAMESPACE. Create it with `vim.api.create_namespace`
+---@param lines table: the lines to highlight from the buffer.
+---@param line_start number: line_start should be 0-indexed
+---@param options table: Configuration options as described in `setup`
+local function highlight_buffer(buf, ns, lines, line_start, options)
+ if buf == 0 or buf == nil then
+ buf = api.nvim_get_current_buf()
+ end
+
+ ns = ns or DEFAULT_NAMESPACE
+ local loop_parse_fn = make_matcher(options)
+ if not loop_parse_fn then
+ return false
+ end
+
+ local data = {}
+ local mode = options.mode == "background" and { mode = "background" } or { mode = "foreground" }
+ for current_linenum, line in ipairs(lines) do
+ current_linenum = current_linenum - 1 + line_start
+ -- Upvalues are options and current_linenum
+ local i = 1
+ while i < #line do
+ local length, rgb_hex = loop_parse_fn(line, i)
+ if length then
+ local name = create_highlight(rgb_hex, mode)
+ local d = data[current_linenum] or {}
+ table.insert(d, { name = name, range = { i - 1, i + length - 1 } })
+ data[current_linenum] = d
+ i = i + length
+ else
+ i = i + 1
+ end
+ end
+ end
+ add_highlight(options, buf, ns, data)
+end
+
+local BUFFER_LINES = {}
+--- Rehighlight the buffer if colorizer is active
+---@param buf number: Buffer number
+---@param options table: Buffer options
+local function rehighlight_buffer(buf, options)
+ if buf == 0 or buf == nil then
+ buf = api.nvim_get_current_buf()
+ end
+
+ local ns = DEFAULT_NAMESPACE
+
+ if not BUFFER_LINES[buf] then
+ BUFFER_LINES[buf] = {}
+ end
+
+ local a = api.nvim_buf_call(buf, function()
+ return {
+ vim.fn.line "w0",
+ vim.fn.line "w$",
+ }
+ end)
+ local min, max
+ local new_min, new_max = a[1] - 1, a[2]
+ local old_min, old_max = BUFFER_LINES[buf]["min"], BUFFER_LINES[buf]["max"]
+
+ if old_min and old_max then
+ -- Triggered for TextChanged autocmds
+ -- TODO: Find a way to just apply highlight to changed text lines
+ if old_max == new_max then
+ min, max = new_min, new_max
+ -- Triggered for WinScrolled autocmd - Scroll Down
+ elseif old_max < new_max then
+ min = old_max
+ max = new_max
+ -- Triggered for WinScrolled autocmd - Scroll Up
+ elseif old_max > new_max then
+ min = new_min
+ max = new_min + (old_max - new_max)
+ end
+ -- just in case a long jump was made
+ if max - min > new_max - new_min then
+ min = new_min
+ max = new_max
+ end
+ end
+
+ min = min or new_min
+ max = max or new_max
+ clear_namespace(buf, ns, min, max)
+ local lines = buf_get_lines(buf, min, max, false)
+ highlight_buffer(buf, ns, lines, min, options)
+ -- store current window position to be used later to incremently highlight
+ BUFFER_LINES[buf]["max"] = new_max
+ BUFFER_LINES[buf]["min"] = new_min
+end
+
+--- @export
+return {
+ DEFAULT_NAMESPACE = DEFAULT_NAMESPACE,
+ HIGHLIGHT_MODE_NAMES = HIGHLIGHT_MODE_NAMES,
+ rehighlight_buffer = rehighlight_buffer,
+ highlight_buffer = highlight_buffer,
+}
diff --git a/lua/colorizer/color_utils.lua b/lua/colorizer/color_utils.lua
new file mode 100644
index 0000000..433cea7
--- /dev/null
+++ b/lua/colorizer/color_utils.lua
@@ -0,0 +1,393 @@
+---Helper functions to parse different colour formats
+--@module colorizer.color_utils
+local Trie = require "colorizer.trie"
+
+local utils = require "colorizer.utils"
+local byte_is_alphanumeric = utils.byte_is_alphanumeric
+local byte_is_hex = utils.byte_is_hex
+local parse_hex = utils.parse_hex
+local percent_or_hex = utils.percent_or_hex
+
+local bit = require "bit"
+local floor, min, max = math.floor, math.min, math.max
+local band, rshift, lshift, tohex = bit.band, bit.rshift, bit.lshift, bit.tohex
+
+local api = vim.api
+
+---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
+---@param r number: Red
+---@param g number: Green
+---@param b number: Blue
+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
+
+---Convert hsl colour values to rgb.
+-- Source: https://gist.github.com/mjackson/5311256
+---@param p number
+---@param q number
+---@param t number
+---@return number
+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 COLOR_MAP
+local COLOR_TRIE
+local COLOR_NAME_MINLEN, COLOR_NAME_MAXLEN
+local COLOR_NAME_SETTINGS = { lowercase = true, strip_digits = false }
+--- Grab all the colour values from `vim.api.nvim_get_color_map` and create a lookup table.
+-- COLOR_MAP is used to store the colour values
+---@param line string: Line to parse
+---@param i number: Index of line from where to start parsing
+local function color_name_parser(line, i)
+ --- Setup the COLOR_MAP and COLOR_TRIE
+ if not COLOR_TRIE then
+ COLOR_MAP = {}
+ COLOR_TRIE = Trie()
+ for k, v in pairs(api.nvim_get_color_map()) do
+ if not (COLOR_NAME_SETTINGS.strip_digits and k:match "%d+$") then
+ COLOR_NAME_MINLEN = COLOR_NAME_MINLEN and min(#k, COLOR_NAME_MINLEN) or #k
+ COLOR_NAME_MAXLEN = COLOR_NAME_MAXLEN and max(#k, COLOR_NAME_MAXLEN) or #k
+ local rgb_hex = tohex(v, 6)
+ COLOR_MAP[k] = rgb_hex
+ COLOR_TRIE:insert(k)
+ if COLOR_NAME_SETTINGS.lowercase then
+ local lowercase = k:lower()
+ COLOR_MAP[lowercase] = rgb_hex
+ COLOR_TRIE:insert(lowercase)
+ end
+ end
+ end
+ end
+
+ if #line < i + COLOR_NAME_MINLEN - 1 then
+ return
+ end
+
+ if i > 1 and byte_is_alphanumeric(line:byte(i - 1)) then
+ return
+ end
+
+ local prefix = COLOR_TRIE:longest_prefix(line, i)
+ if prefix then
+ -- 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
+
+--- Converts an HSL color value to RGB.
+---@param h number: Hue
+---@param s number: Saturation
+---@param l number: Lightness
+---@return number|nil,number|nil,number|nil
+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
+
+local CSS_RGB_FN_MINIMUM_LENGTH = #"rgb(0,0,0)" - 1
+---Parse for rgb() css function and return rgb hex.
+---@param line string: Line to parse
+---@param i number: Index of line from where to start parsing
+---@return number|nil: Index of line where the rgb function ended
+---@return string|nil: rgb hex value
+local function rgb_function_parser(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
+ r, g, b, match_end = line:sub(i):match "^rgb%(%s*(%d+%%?)%s+(%d+%%?)%s+(%d+%%?)%s*%)()"
+ if not match_end then
+ return
+ end
+ 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 = string.format("%02x%02x%02x", r, g, b)
+ return match_end - 1, rgb_hex
+end
+
+local CSS_RGBA_FN_MINIMUM_LENGTH = #"rgba(0,0,0,0)" - 1
+---Parse for rgba() css function and return rgb hex.
+-- Todo consider removing the regexes here
+-- Todo this might not be the best approach to alpha channel.
+-- Things like pumblend might be useful here.
+---@param line string: Line to parse
+---@param i number: Index of line from where to start parsing
+---@return number|nil: Index of line where the rgba function ended
+---@return string|nil: rgb hex value
+local function rgba_function_parser(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
+ r, g, b, a, match_end = line:sub(i):match "^rgba%(%s*(%d+%%?)%s+(%d+%%?)%s+(%d+%%?)%s+([.%d]+)%s*%)()"
+ if not match_end then
+ return
+ end
+ 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 = string.format("%02x%02x%02x", r * a, g * a, b * a)
+ return match_end - 1, rgb_hex
+end
+
+local CSS_HSL_FN_MINIMUM_LENGTH = #"hsl(0,0%,0%)" - 1
+---Parse for hsl() css function and return rgb hex.
+---@param line string: Line to parse
+---@param i number: Index of line from where to start parsing
+---@return number|nil: Index of line where the hsl function ended
+---@return string|nil: rgb hex value
+local function hsl_function_parser(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
+ h, s, l, match_end = line:sub(i):match "^hsl%(%s*(%d+)%s+(%d+)%%%s+(%d+)%%%s*%)()"
+ if not match_end then
+ return
+ end
+ 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 = string.format("%02x%02x%02x", r, g, b)
+ return match_end - 1, rgb_hex
+end
+
+local CSS_HSLA_FN_MINIMUM_LENGTH = #"hsla(0,0%,0%,0)" - 1
+---Parse for hsl() css function and return rgb hex.
+---@param line string: Line to parse
+---@param i number: Index of line from where to start parsing
+---@return number|nil: Index of line where the hsla function ended
+---@return string|nil: rgb hex value
+local function hsla_function_parser(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
+ h, s, l, a, match_end = line:sub(i):match "^hsla%(%s*(%d+)%s+(%d+)%%%s+(%d+)%%%s+([.%d]+)%s*%)()"
+ if not match_end then
+ return
+ end
+ 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 = string.format("%02x%02x%02x", r * a, g * a, b * a)
+ return match_end - 1, rgb_hex
+end
+
+local ARGB_MINIMUM_LENGTH = #"0xAARRGGBB" - 1
+---parse for 0xaarrggbb and return rgb hex.
+-- a format used in android apps
+---@param line string: line to parse
+---@param i number: index of line from where to start parsing
+---@return number|nil: index of line where the hex value ended
+---@return string|nil: rgb hex value
+local function argb_hex_parser(line, i)
+ if #line < i + ARGB_MINIMUM_LENGTH then
+ return
+ end
+
+ local j = i + 2
+
+ local n = j + 8
+ local alpha
+ local v = 0
+ while j <= min(n, #line) do
+ local b = line:byte(j)
+ if not byte_is_hex(b) then
+ break
+ end
+ if j - i <= 3 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 ~= 10 then
+ return
+ end
+ alpha = tonumber(alpha) / 255
+ local r = floor(band(rshift(v, 16), 0xFF) * alpha)
+ local g = floor(band(rshift(v, 8), 0xFF) * alpha)
+ local b = floor(band(v, 0xFF) * alpha)
+ local rgb_hex = string.format("%02x%02x%02x", r, g, b)
+ return length, rgb_hex
+end
+
+---parse for #rrggbbaa and return rgb hex.
+-- a format used in android apps
+---@param line string: line to parse
+---@param i number: index of line from where to start parsing
+---@param opts table: Containing minlen, maxlen, valid_lengths
+---@return number|nil: index of line where the hex value ended
+---@return string|nil: rgb hex value
+local function rgba_hex_parser(line, i, opts)
+ local minlen, maxlen, valid_lengths = opts.minlen, opts.maxlen, opts.valid_lengths
+ local j = i + 1
+ if #line < j + minlen - 1 then
+ return
+ end
+
+ if i > 1 and byte_is_alphanumeric(line:byte(i - 1)) then
+ return
+ end
+
+ local n = j + maxlen
+ local alpha
+ local v = 0
+
+ while j <= 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(rshift(v, 16), 0xFF) * alpha)
+ local g = floor(band(rshift(v, 8), 0xFF) * alpha)
+ local b = floor(band(v, 0xFF) * alpha)
+ local rgb_hex = string.format("%02x%02x%02x", r, g, b)
+ return 9, rgb_hex
+ end
+ return (valid_lengths[length - 1] and length), line:sub(i + 1, i + length - 1)
+end
+
+--- @export
+return {
+ color_is_bright = color_is_bright,
+ color_name_parser = color_name_parser,
+ rgba_hex_parser = rgba_hex_parser,
+ argb_hex_parser = argb_hex_parser,
+ rgb_function_parser = rgb_function_parser,
+ rgba_function_parser = rgba_function_parser,
+ hsl_function_parser = hsl_function_parser,
+ hsla_function_parser = hsla_function_parser,
+}
diff --git a/lua/colorizer/matcher_utils.lua b/lua/colorizer/matcher_utils.lua
new file mode 100644
index 0000000..84ddece
--- /dev/null
+++ b/lua/colorizer/matcher_utils.lua
@@ -0,0 +1,132 @@
+---Helper functions for colorizer to enable required parsers
+--@module colorizer.matcher_utils
+local Trie = require "colorizer.trie"
+local min, max = math.min, math.max
+
+local color_utils = require "colorizer.color_utils"
+local color_name_parser = color_utils.color_name_parser
+local rgba_hex_parser = color_utils.rgba_hex_parser
+
+local parser = {}
+parser["_0x"] = color_utils.argb_hex_parser
+parser["_rgb"] = color_utils.rgb_function_parser
+parser["_rgba"] = color_utils.rgba_function_parser
+parser["_hsl"] = color_utils.hsl_function_parser
+parser["_hsla"] = color_utils.hsla_function_parser
+
+---Form a trie stuct with the given prefixes
+---@param matchers table: List of prefixes, {"rgb", "hsl"}
+---@param matchers_trie table: Table containing information regarding non-trie based parsers
+---@return function: function which will just parse the line for enabled parsers
+local function compile_matcher(matchers, matchers_trie)
+ local trie = Trie(matchers_trie)
+
+ local b_hash = ("#"):byte()
+ local function parse_fn(line, i)
+ -- prefix #
+ if matchers.rgba_hex_parser then
+ if line:byte(i) == b_hash then
+ return rgba_hex_parser(line, i, matchers.rgba_hex_parser)
+ end
+ end
+
+ -- Prefix 0x, rgba, rgb, hsla, hsl
+ local prefix = trie:longest_prefix(line, i)
+ if prefix then
+ local fn = "_" .. prefix
+ return parser[fn](line, i, matchers[fn])
+ end
+
+ -- Colour names
+ if matchers.color_name_parser then
+ return color_name_parser(line, i)
+ end
+ end
+ return parse_fn
+end
+
+local MATCHER_CACHE = {}
+---Parse the given options and return a function with enabled parsers.
+--if no parsers enabled then return false
+--Do not try make the function again if it is present in the cache
+---@param options table: options created in `colorizer.setup`
+---@return function|boolean: function which will just parse the line for enabled parsers
+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_AARRGGBB = options.AARRGGBB
+ 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 matcher_key = 0
+ + (enable_names and 1 or 0)
+ + (enable_RGB and 1 or 1)
+ + (enable_RRGGBB and 1 or 2)
+ + (enable_RRGGBBAA and 1 or 3)
+ + (enable_AARRGGBB and 1 or 4)
+ + (enable_rgb and 1 or 5)
+ + (enable_hsl and 1 or 6)
+
+ if matcher_key == 0 then
+ return false
+ end
+
+ local loop_parse_fn = MATCHER_CACHE[matcher_key]
+ if loop_parse_fn then
+ return loop_parse_fn
+ end
+
+ local matchers = {}
+ local matchers_prefix = {}
+ matchers.max_prefix_length = 0
+
+ if enable_names then
+ matchers.color_name_parser = true
+ end
+
+ 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 = minlen and min(k, minlen) or k
+ maxlen = maxlen and max(k, maxlen) or k
+ end
+ end
+
+ if minlen then
+ matchers.rgba_hex_parser = {}
+ matchers.rgba_hex_parser.valid_lengths = valid_lengths
+ matchers.rgba_hex_parser.maxlen = maxlen
+ matchers.rgba_hex_parser.minlen = minlen
+ end
+
+ if enable_AARRGGBB then
+ table.insert(matchers_prefix, "0x")
+ end
+
+ -- do not mess with the sequence, hsla before hsl, etc
+ if enable_rgb and enable_hsl then
+ table.insert(matchers_prefix, "hsla")
+ table.insert(matchers_prefix, "rgba")
+ table.insert(matchers_prefix, "rgb")
+ table.insert(matchers_prefix, "hsl")
+ elseif enable_rgb then
+ table.insert(matchers_prefix, "rgba")
+ table.insert(matchers_prefix, "rgb")
+ elseif enable_hsl then
+ table.insert(matchers_prefix, "hsla")
+ table.insert(matchers_prefix, "hsl")
+ end
+
+ loop_parse_fn = compile_matcher(matchers, matchers_prefix)
+ MATCHER_CACHE[matcher_key] = loop_parse_fn
+
+ return loop_parse_fn
+end
+
+--- @export
+return {
+ make_matcher = make_matcher,
+}
diff --git a/lua/colorizer/trie.lua b/lua/colorizer/trie.lua
index 21ea543..82a0d2d 100644
--- a/lua/colorizer/trie.lua
+++ b/lua/colorizer/trie.lua
@@ -1,18 +1,21 @@
---- Trie implementation in luajit
--- Copyright © 2019 Ashkan Kiani
+---Trie implementation in luajit.
+--todo: write documentation
+-- Copyright © 2019 Ashkan Kiani
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
-
+--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
-
+--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+--@module trie
local ffi = require "ffi"
ffi.cdef [[
@@ -47,11 +50,12 @@ local function trie_destroy(trie)
ffi.C.free(trie)
end
-local INDEX_LOOKUP_TABLE = ffi.new "uint8_t[256]"
+local total_char = 255
+local INDEX_LOOKUP_TABLE = ffi.new("uint8_t[?]", total_char)
local CHAR_LOOKUP_TABLE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
do
local b = string.byte
- for i = 0, 255 do
+ for i = 0, total_char 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
@@ -59,7 +63,7 @@ do
elseif i >= b "a" and i <= b "z" then
INDEX_LOOKUP_TABLE[i] = i - b "a" + 10 + 26
else
- INDEX_LOOKUP_TABLE[i] = 255
+ INDEX_LOOKUP_TABLE[i] = total_char
end
end
end
@@ -71,7 +75,7 @@ local function trie_insert(trie, value)
local node = trie
for i = 1, #value do
local index = INDEX_LOOKUP_TABLE[value:byte(i)]
- if index == 255 then
+ if index == total_char then
return false
end
if node.character[index] == nil then
@@ -90,7 +94,7 @@ local function trie_search(trie, value, start)
local node = trie
for i = (start or 1), #value do
local index = INDEX_LOOKUP_TABLE[value:byte(i)]
- if index == 255 then
+ if index == total_char then
return
end
local child = node.character[index]
@@ -113,7 +117,7 @@ local function trie_longest_prefix(trie, value, start)
for i = start, #value do
local index = INDEX_LOOKUP_TABLE[value:byte(i)]
-- local index = INDEX_LOOKUP_TABLE[bor(insensitive, value:byte(i))]
- if index == 255 then
+ if index == total_char then
break
end
local child = node.character[index]
@@ -189,7 +193,7 @@ local function print_trie_table(s)
end
local lines = {}
for _, child in ipairs(s.children) do
- local child_lines = print_trie_table(child, thicc)
+ local child_lines = print_trie_table(child)
for _, child_line in ipairs(child_lines) do
table.insert(lines, child_line)
end
@@ -242,6 +246,7 @@ local Trie_mt = {
search = trie_search,
longest_prefix = trie_longest_prefix,
extend = trie_extend,
+ destroy = trie_destroy,
},
__tostring = trie_to_string,
__gc = trie_destroy,
diff --git a/lua/colorizer/utils.lua b/lua/colorizer/utils.lua
new file mode 100644
index 0000000..0cb09ee
--- /dev/null
+++ b/lua/colorizer/utils.lua
@@ -0,0 +1,106 @@
+---Helper utils
+--@module utils
+local bit, ffi = require "bit", require "ffi"
+local band, bor, rshift, lshift = bit.band, bit.bor, bit.rshift, bit.lshift
+
+-- -- TODO use rgb as the return value from the matcher functions
+-- -- instead of the rgb_hex. Can be the highlight key as well
+-- -- when you shift it left 8 bits. Use the lower 8 bits for
+-- -- indicating which highlight mode to use.
+-- ffi.cdef [[
+-- typedef struct { uint8_t r, g, b; } colorizer_rgb;
+-- ]]
+-- local rgb_t = ffi.typeof 'colorizer_rgb'
+
+-- 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 not run the loop multiple times
+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
+ 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
+ end
+ BYTE_CATEGORY[i] = v
+end
+
+---Obvious.
+---@param byte number
+---@return boolean
+local function byte_is_alphanumeric(byte)
+ local category = BYTE_CATEGORY[byte]
+ return band(category, CATEGORY_ALPHANUM) ~= 0
+end
+
+---Obvious.
+---@param byte number
+---@return boolean
+local function byte_is_hex(byte)
+ return band(BYTE_CATEGORY[byte], CATEGORY_HEX) ~= 0
+end
+
+---Merge two tables.
+--
+-- todo: Remove this and use `vim.tbl_deep_extend`
+---@return table
+local function merge(...)
+ local res = {}
+ for i = 1, select("#", ...) do
+ local o = select(i, ...)
+ if type(o) ~= "table" then
+ return {}
+ end
+ for k, v in pairs(o) do
+ res[k] = v
+ end
+ end
+ return res
+end
+
+--- Obvious.
+---@param byte number
+---@return number
+local function parse_hex(byte)
+ return rshift(BYTE_CATEGORY[byte], 4)
+end
+
+--- Obvious.
+---@param v string
+---@return number|nil
+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
+
+--- @export
+return {
+ byte_is_alphanumeric = byte_is_alphanumeric,
+ byte_is_hex = byte_is_hex,
+ merge = merge,
+ parse_hex = parse_hex,
+ percent_or_hex = percent_or_hex,
+}