From 28b41de2f491ef598197823c04fc7e86ae76a625 Mon Sep 17 00:00:00 2001 From: Akianonymus Date: Sun, 28 Aug 2022 12:30:35 +0530 Subject: fragment | Implement better autocmd management | refactor add a all_buffers option - colorizer will activate on all buffers, empty or not, still respect filetypes option handle errors when detach is called multiple times from the same buffer use bufdelete and bufdelete to remove the autocmds use a more efficient compile parse_fn function use custom ldoc template to generate vim help --- lua/colorizer/buffer_utils.lua | 196 ++++++++++++++++++++ lua/colorizer/color_utils.lua | 393 ++++++++++++++++++++++++++++++++++++++++ lua/colorizer/matcher_utils.lua | 132 ++++++++++++++ lua/colorizer/trie.lua | 27 +-- lua/colorizer/utils.lua | 106 +++++++++++ 5 files changed, 843 insertions(+), 11 deletions(-) create mode 100644 lua/colorizer/buffer_utils.lua create mode 100644 lua/colorizer/color_utils.lua create mode 100644 lua/colorizer/matcher_utils.lua create mode 100644 lua/colorizer/utils.lua (limited to 'lua/colorizer') 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 . + +--@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, +} -- cgit v1.2.3-70-g09d2