aboutsummaryrefslogtreecommitdiff
path: root/lua/colorizer/color_utils.lua
diff options
context:
space:
mode:
authorAkianonymus <anonymus.aki@gmail.com>2022-08-28 12:30:35 +0530
committerAkianonymus <anonymus.aki@gmail.com>2022-09-03 17:24:25 +0530
commit28b41de2f491ef598197823c04fc7e86ae76a625 (patch)
treeb480ea1c0f58e4802e92a6de9baf26f27b6e855d /lua/colorizer/color_utils.lua
parentfeat: Incremental highlight loading (diff)
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
Diffstat (limited to 'lua/colorizer/color_utils.lua')
-rw-r--r--lua/colorizer/color_utils.lua393
1 files changed, 393 insertions, 0 deletions
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,
+}